GOSKY · 手機結帳變更閱讀

origin/main a7c25efc2 → 71d18d522 · 5 個檔案,+115/−26 [1]

手機結帳先顯示主按鈕,
其他方案按需展開

只改手機第一步。資訊卡仍在最上方,新購仍預選年繳。主按鈕移到資訊卡下方,顯示換算月價及固定年繳優惠。按「查看其他方案」可展開選項;改選月繳後,按鈕顯示月價與「月繳方案」。[1] [2] [3] [4]

直接看前後畫面

桌面、第 2 到 4 步、共用付款與訂單程式、計價均未改。[1] [3] 本頁解說候選程式與 mock 證據,未驗證正式環境或真實付款。

Marcus 已決定所有產品與幣別的手機年繳按鈕副文案,都寫「(已套用年繳免費 2 個月優惠)」。Messenger 價格來源在 TWD 約等於 2.15 個月、USD 約 2.02 個月;固定的「2」是文案選擇。[10]

缺價時,停用按鈕仍顯示 0 元,是來源確認的顯示問題;產品畫面尚未重現。[14]

資訊卡之後,直接接下一步

原本先列兩種方案,再顯示「繼續」。現在先放主按鈕,換方案時再展開。新購預選年繳是既有規則。[1] [2] [5]

Instagram · 繁中 · 390px · 年繳預選 · 相同 mock 月價 NT$389、年價 NT$3,890。[5] [7]

原本 · main a7c25efc2方案選項在前,「繼續」在下方。
原本 · main a7c25efc2。方案選項在前,「繼續」在下方。

impl-v2/v3/shots/main-default-full.png

現在 · 71d18d522NT$324/月與固定 2 個月優惠,點擊仍前往第二步。
現在 · 71d18d522。NT$324/月與固定 2 個月優惠,點擊仍前往第二步。

impl-v2/v5/shots/instagram-zh_tw-annual-full.png

年繳仍一次收年費。這裡的 NT$324 是測試年價 NT$3,890 ÷ 12 四捨五入後的月額,並非每月扣款。[2] [5] [7]

查看改選月繳與 Messenger 的畫面

這兩張都是 target 的操作狀態,不作同狀態的前後配對。既有證據沒有 main 390px 月繳第一步原圖。[5] [7]

Instagram · 改選月繳展開後可選月繳,按鈕顯示 NT$389/月與「月繳方案」。
Instagram · 改選月繳。展開後可選月繳,按鈕顯示 NT$389/月與「月繳方案」。

impl-v2/v5/shots/instagram-zh_tw-monthly-full.png

Messenger · 年繳預選價格仍依方案資料換算;年繳副文案同樣固定 2 個月。
Messenger · 年繳預選。價格仍依方案資料換算;年繳副文案同樣固定 2 個月。

impl-v2/v5/shots/messenger-zh_tw-annual-full.png

狀態決定選項,所選方案決定按鈕

結帳元件管理這些顯示條件;翻譯檔提供四語系文字。按鈕沿用同一份方案資料,不新增訂單或折扣算法。[1] [2] [4]

libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx

只有手機第一步預設收起方案

isMobileStepOne 在寬度低於 1024px 且 activeStep 為 0 時成立。showOtherPlans 初始為 false;桌面仍顯示原方案清單。這些狀態只管呈現,不替使用者改付款週期。[2] [3]

依寬度與步驟決定是否收起 · 原始 diff 第 82 至 100 行

@@ -784,6 +787,8 @@ export function SorghumFeaturesSettingUpgrade({
   tapPayAppID: number;
 }) {
   const { navigateTo404, navigateToProjectPage } = usePath();
+  const theme = useTheme();
+  const isMobile = useMediaQuery(theme.breakpoints.down('md'));
 
   // 手機版右下角 Intercom launcher 會蓋住付款流程的「下一步」按鈕
   useHideIntercomLauncher();
@@ -859,6 +864,9 @@ export function SorghumFeaturesSettingUpgrade({
   useGetAccountInfo();
 
   const [activeStep, setActiveStep] = useState(0);
+  const [showOtherPlans, setShowOtherPlans] = useState(false);
+  const isMobileStepOne = isMobile && activeStep === 0;
+  const showPlanOptions = !isMobileStepOne || showOtherPlans;
   const [billingData, setBillingData] = useState<BillingDataType>({
     last4CardNum: '',
     planDetail: {
按鈕移到資訊卡下方,保留前進與停用條件

continueButton 是原按鈕的共用片段。手機在方案區頂端放入,桌面仍在底端放入。手機改用滿寬與新的上下留白,點擊仍記住使用者已選週期並前往第二步。[1] [3]

手機留白與既有處理 · 原始 diff 第 105 至 123 行

+  const continueButton = (
+    <Button
+      id="upgrade_step1_continue"
+      variant="contained"
+      // 價格資料不完整時不得進入結帳(避免 0 元訂單流程);
+      // 預設週期未確認前也不得進入(避免月繳客戶帶著暫時的年繳結帳)
+      disabled={!hasPlanData || !isDefaultPeriodReady}
+      sx={{
+        mt: isMobileStepOne ? 0 : '48px',
+        width: isMobileStepOne ? '100%' : { sm: '100%', md: 'fit-content' },
+        py: isMobileStepOne ? '10px' : undefined,
+        mb: isMobileStepOne ? 0 : { sm: '96px', md: 0 },
+        alignSelf: 'flex-end',
+      }}
+      onClick={() => {
+        // 按下一步視為已確認週期,之後方案資料才回來也不再改動
+        hasPickedPeriod.current = true;
+        setActiveStep(1);
+      }}

手機先放主按鈕 · 原始 diff 第 155 至 167 行

@@ -1080,10 +1135,43 @@ export function SorghumFeaturesSettingUpgrade({
         </LeftContainerStyled>
         <RightContainerStyled>
           {activeStep === 0 && (
-            <RightContentContainerStyled>
-              <Typography variant="h5" sx={{ mb: '32px' }}>
-                {t('setting.upgrade.step1.selectMethod')}
-              </Typography>
+            <RightContentContainerStyled
+              sx={isMobileStepOne ? { width: '100%', px: '16px' } : undefined}
+            >
+              {isMobileStepOne && continueButton}
+              {isMobileStepOne && !isPlanLoading && hasPlanData && (

原底部按鈕改為只在桌面呈現 · 原始 diff 第 234 至 245 行

-                onClick={() => {
-                  // 按下一步視為已確認週期,之後方案資料才回來也不再改動
-                  hasPickedPeriod.current = true;
-                  setActiveStep(1);
-                }}
-              >
-                {t('setting.upgrade.step1.continue')}
-              </Button>
+              {!isMobileStepOne && continueButton}
             </RightContentContainerStyled>
           )}
           {activeStep === 1 && (
價格沿用計算,年繳副文案固定為 2

selectedPlan 為月繳時讀 monthlyPrice;年繳讀 yearlyPerMonthPrice。下行在「月繳方案」與固定年繳優惠間切換。優惠碼不另改這個按鈕上的月價,折扣後金額仍看資訊卡總額。[2] [4]

價格與年繳副文案 · 原始 diff 第 130 至 149 行

+          <Box component="span" sx={{ fontSize: '18px', fontWeight: 700 }}>
+            {t('setting.upgrade.step1.mobileMonthlyPrice', {
+              price: `${currencySymbol}${formatNumberWithCommas(
+                selectedPlan === PERIOD_MONTHLY
+                  ? monthlyPrice
+                  : yearlyPerMonthPrice,
+              )}`,
+            })}
+          </Box>
+          <Box component="span" sx={{ fontSize: '12px', fontWeight: 400 }}>
+            {t(
+              selectedPlan === PERIOD_MONTHLY
+                ? 'setting.upgrade.step1.mobileMonthlyPlan'
+                : 'setting.upgrade.step1.mobileYearlyOffer',
+            )}
+          </Box>
+        </Box>
+      ) : (
+        t('setting.upgrade.step1.continue')
+      )}
點「查看其他方案」才顯示原選項

底線控制以 showOtherPlans 切換展開,aria-expanded 描述狀態。方案清單原本固定 flex,現在收起時設為 none。原選取處理仍更新 billingData,按鈕讀同一份資料。[2] [3]

展開控制 · 原始 diff 第 168 至 175 行

+                <Link
+                  component="button"
+                  type="button"
+                  variant="body2"
+                  underline="always"
+                  aria-expanded={showOtherPlans}
+                  aria-controls="upgrade_step1_plan_options"
+                  onClick={() => setShowOtherPlans((shown) => !shown)}

箭頭與標題跟著展開 · 原始 diff 第 185 至 199 行

+                  {t('setting.upgrade.step1.mobileViewOtherPlans')}
+                  <ExpandMoreIcon
+                    sx={{
+                      width: '20px',
+                      height: '20px',
+                      transform: showOtherPlans ? 'rotate(180deg)' : 'none',
+                    }}
+                  />
+                </Link>
+              )}
+              {showPlanOptions && (
+                <Typography variant="h5" sx={{ mb: '32px' }}>
+                  {t('setting.upgrade.step1.selectMethod')}
+                </Typography>
+              )}

用 display 控制原方案清單 · 原始 diff 第 203 至 216 行

@@ -1115,7 +1203,12 @@ export function SorghumFeaturesSettingUpgrade({
               )}
               {!isPlanLoading && hasPlanData && (
                 <Box
-                  sx={{ gap: '12px', display: 'flex', flexDirection: 'column' }}
+                  id={isMobileStepOne ? 'upgrade_step1_plan_options' : undefined}
+                  sx={{
+                    gap: '12px',
+                    display: showPlanOptions ? 'flex' : 'none',
+                    flexDirection: 'column',
+                  }}
                 >
                   <PlanOptionWrapper
                     type={t('setting.upgrade.step1.monthly')}

四個翻譯檔各新增四個鍵

依序是月繳小字、月價格式、展開文字與固定年繳優惠。[4]

zh_tw/messenger.json

apps/sorghum/src/assets/locales/zh_tw/messenger.json

原始新增行 · 原始 diff 第 54 至 57 行

+        "mobileMonthlyPlan": "月繳方案",
+        "mobileMonthlyPrice": "{{price}}/月",
+        "mobileViewOtherPlans": "查看其他方案",
+        "mobileYearlyOffer": "(已套用年繳免費 2 個月優惠)",
en_us/messenger.json

apps/sorghum/src/assets/locales/en_us/messenger.json

原始新增行 · 原始 diff 第 9 至 12 行

+        "mobileMonthlyPlan": "Monthly plan",
+        "mobileMonthlyPrice": "{{price}}/month",
+        "mobileViewOtherPlans": "View other plans",
+        "mobileYearlyOffer": "(2 months free with annual billing applied)",
ja_jp/messenger.json

apps/sorghum/src/assets/locales/ja_jp/messenger.json

原始新增行 · 原始 diff 第 39 至 42 行

+        "mobileMonthlyPlan": "月払いプラン",
+        "mobileMonthlyPrice": "{{price}}/月",
+        "mobileViewOtherPlans": "他のプランを見る",
+        "mobileYearlyOffer": "(年払いで2か月分無料の特典を適用済み)",
es_es/messenger.json

apps/sorghum/src/assets/locales/es_es/messenger.json

原始新增行 · 原始 diff 第 24 至 27 行

+        "mobileMonthlyPlan": "Plan mensual",
+        "mobileMonthlyPrice": "{{price}}/mes",
+        "mobileViewOtherPlans": "Ver otros planes",
+        "mobileYearlyOffer": "(2 meses gratis por pago anual aplicados)",

既有 v5 紀錄,本輪重新核對

以下產品測試沒有在本輪重跑。本輪重算圖片差異、比對訂單原始 bytes,並核對 log 與 target 來源雜湊。[6] [7] [8] [9]

範圍結果能支持什麼
畫面96/96
0 像素差異
Instagram 繁中,48 個狀態各比可視區與全頁截圖。桌面第一步 16 組,後續步驟 80 組;48 個可見 DOM 也相同。手機第一步不在零差異比較內。[6]
文案24/24Instagram、Messenger、Threads,各測繁中、英文、日文、西文的年繳與月繳。月繳保留小字且沒有優惠副行。[7]
模擬送單10/10Instagram 繁中,TW 4、US 4、JP 2 組 request body 逐位元組相同。fixture 回傳 49999;沒有驗證成功成立訂單或真實付款。[8]
行為19 項包含上述 10 組訂單,以及初始、換月繳、套碼、返回、縮放、錯誤及語言案例。[6]
程式檢查exit 0TypeScript、lint;Jest upgrade 23、billing 6、currency 5,合計 34 tests、7 suites。[9]

Prototype 的另一組操作紀錄來自不同 checkout,僅作補充展示,不合併計數。[15]

已決定的差異與待追蹤事項

本輪只列問題與證據界線,未修產品。沒有以 mock 通過推導正式環境驗收或轉換率改善。

來源可在同一頁讀取

程式片段來自固定 Git objects 或原始 diff;log 摘錄保留原始路徑與行號。來源確認、既有紀錄及未驗證項目分開標示。

展開 claim 對照與來源摘錄
  1. [1] 比較兩個固定快照,沒有 WIP

    來源確認。本頁只包含這份淨 diff。四份翻譯各加 4 行,元件新增 99 行、刪除 26 行。完整原始 diff 收在下方。

    repo gosky-dashboard/frontend

    base a7c25efc2abd403b1b14837564a4c4ab7ca8b80a
    target 71d18d5226082261722a32da33919282de150be5
    
     .../src/assets/locales/en_us/messenger.json        |   4 +
     .../src/assets/locales/es_es/messenger.json        |   4 +
     .../src/assets/locales/ja_jp/messenger.json        |   4 +
     .../src/assets/locales/zh_tw/messenger.json        |   4 +
     .../src/lib/sorghum-features-setting-upgrade.tsx   | 125 ++++++++++++++++-----
     5 files changed, 115 insertions(+), 26 deletions(-)
    

    讀取完整原始 diff

  2. [2] 新購年繳預選與價格計算原本就有

    來源確認。plan-default.ts 在 base 與 target 相同。既有付費客戶、待生效及重試情況沿用原週期規則;本次沒有重新制定預設週期。年繳按鈕取既有換算月價,沒有使用優惠碼另算按鈕價格。

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/plan-default.ts · 第 46 至 74 行

    export const getDefaultPeriod = (
      billing?: GetBillingResponseType,
      payment?: GetBillingPaymentResponseType,
    ): number => {
      if (!billing) return PERIOD_YEARLY;
    
      // 方案 API 失敗(例如後端 504)時狀態不明,保守維持月繳,避免月繳客戶被默默換成年繳
      if (billing.isFallback) return PERIOD_MONTHLY;
    
      if (billing.plan === GetBillingPlanEnum.ADVANCE) {
        return billing.period === PERIOD_YEARLY ? PERIOD_YEARLY : PERIOD_MONTHLY;
      }
    
      // 續扣重試成功、已付費但下期尚未生效:沿用已付那一期的週期
      if (billing.pendingAdvanced) {
        return billing.pendingAdvanced.period === PERIOD_YEARLY
          ? PERIOD_YEARLY
          : PERIOD_MONTHLY;
      }
    
      // 續扣失敗緩衝期:方案顯示為 TRIAL 但其實是付費客戶,週期要從付款資料拿
      if (billing.paymentRetrying || billing.paymentRetryStopped) {
        return payment?.planDetail?.period === PERIOD_YEARLY
          ? PERIOD_YEARLY
          : PERIOD_MONTHLY;
      }
    
      return PERIOD_YEARLY;
    };

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/plan-default.ts · 第 86 至 99 行

    export const getPlanPrices = (
      billingPlans: BillingPlan[] | undefined,
      isUsd: boolean,
    ): PlanPrices => {
      const pick = (index: number) => {
        const item = billingPlans?.[index];
        return (isUsd ? item?.usdAmount : item?.amount) ?? 0;
      };
      const yearlyPrice = pick(1);
      return {
        monthlyPrice: pick(0),
        yearlyPrice,
        yearlyPerMonthPrice: Math.round(yearlyPrice / 12),
      };

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 1011 至 1017 行

    const handleSelectPeriod = useCallback((period: number) => {
      hasPickedPeriod.current = true;
      setBillingData((prev) => ({
        ...prev,
        planDetail: { ...prev.planDetail, period },
      }));
    }, []);
  3. [3] 手機條件、資訊卡與下一步

    來源確認。手機指寬度低於 md;theme 的 md 是 1024px,包含符合寬度的平板。PlanView 資訊卡與付款處理沿用。新增的程式改按鈕文字、位置與方案清單是否顯示。

    71d18d522:libs/components/external-providers/src/lib/ThemeProvider.tsx · 第 153 至 162 行

    breakpoints: {
      values: {
        sm: 0,
        xs: 444,
        s: 600,
        m: 900,
        md: 1024,
        lg: 1280,
        xl: 1920,
      },

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 1121 至 1142 行

    <ContentContainerStyled>
      <LeftContainerStyled>
        <PlanView
          featureList={featureList}
          plan={selectedPlan}
          couponCode={couponCode}
          setIsOpenEnterCouponModal={setIsOpenEnterCouponModal}
          isShowCouponCode={isShowCouponCode}
          isCouponChipError={isCouponChipError}
          couponDiscount={couponDiscount}
          resetCoupon={resetCoupon}
          prices={planPrices}
          currency={currencySymbol}
        />
      </LeftContainerStyled>
      <RightContainerStyled>
        {activeStep === 0 && (
          <RightContentContainerStyled
            sx={isMobileStepOne ? { width: '100%', px: '16px' } : undefined}
          >
            {isMobileStepOne && continueButton}
            {isMobileStepOne && !isPlanLoading && hasPlanData && (

    查看這些條件的實際 diff

  4. [4] 四份翻譯都使用固定的 2 個月

    來源確認。每個 locale 新增相同四個鍵。年繳優惠是字串常數,沒有依產品或價格選不同月份。月繳則使用 mobileMonthlyPlan。Instagram、Threads 缺少相應鍵時,沿用 messenger 翻譯,這是既有 fallback 設定。

    查看四個檔案的原始新增行

    71d18d522:libs/sorghum/utils/src/lib/i18n/index.ts · 第 40 至 42 行

    ns: ['messenger', 'instagram', 'threads'],
    defaultNS: 'messenger',
    fallbackNS: ['messenger'],
  5. [5] 前後畫面使用相同 mock 基準

    既有紀錄。本輪直接讀回原圖。前圖為 main a7c25efc2,後圖為 v5 target 71d18d522 的元件測試。v3 與 v5 的 main-default-full.png 位元組相同。全部為 390px 寬,full-page 保留完整內容;高度因頁面而異。

    impl-v2/v5/notes/verification/fixture.py · 第 5 至 7 行

    ROOT = Path(__file__).resolve().parents[2]
    ORIGIN = 'http://127.0.0.1:4387'
    PRICES = {1: (3899, 38388, 129, 1279), 2: (389, 3890, 13, 130), 3: (159, 1590, 6, 60)}

    impl-v2/v5/logs/copy-browser-results.json · 第 2 至 20 行

      "states": [
        {
          "case": "instagram-zh_tw-annual",
          "social": 2,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$324/月\n(已套用年繳免費 2 個月優惠)",
          "annualAmount": 3890,
          "derivedMonthlyPrice": 324
        },
        {
          "case": "instagram-zh_tw-monthly",
          "social": 2,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$389/月\n月繳方案",
          "monthlyAmount": 389,
          "offerAbsent": true
        },
  6. [6] 96 組比較與 19 項行為記錄

    既有 v5 紀錄,本輪重新比對 PNG 與 DOM JSON。96 組都用 Instagram、繁中 fixture,含桌面第一步 16 組、後續步驟 80 組;每個狀態各有 viewport 與 full-page,合計 48 個 DOM 狀態。改動中的手機第一步不在零差異主張內。19 項行為另含各產品初始狀態與語言案例,並包含下方的 10 組模擬送單,不另相加。

    impl-v2/v5/logs/browser-run.log · 第 1 至 1 行

    {"parityChecks": 96, "behaviorChecks": 19, "passed": true, "errors": []}

    impl-v2/v5/notes/verification/verify.py · 第 31 至 43 行

    def compare(name, documents):
        for suffix in ['', '-full']:
            a=Image.open(ROOT/'shots'/f'main-{name}{suffix}.png').convert('RGB')
            b=Image.open(ROOT/'shots'/f'new-{name}{suffix}.png').convert('RGB')
            count=None
            if a.size==b.size:
                diff=ImageChops.difference(a,b)
                count=sum(pixel!=(0,0,0) for pixel in diff.get_flattened_data())
                if count:
                    diff.save(ROOT/'cmp'/f'diff-{name}{suffix}.png')
            result={'case':name+suffix,'mainSize':a.size,'newSize':b.size,'changedPixels':count,'domEqual':documents['main']==documents['new']}
            RESULTS['parity'].append(result)
            assert count==0 and result['domEqual'], result

    impl-v2/v5/notes/verification/verify.py · 第 136 至 146 行

        for width in [1024,1440]:
            for state in ['default','monthly','coupon','coupon-40002']:
                documents={}
                name=f'desktop-{width}-{state}'
                for label in ['main','new']:
                    context,page,records,errors=open_page(browser,label,width)
                    if state=='monthly': select_monthly(page,label,width)
                    if state in ['coupon','coupon-40002']: coupon(page,'KOL10' if state=='coupon' else 'ERROR40002')
                    documents[label]=capture(page,label,name)
                    record(context,records,errors,label+'-'+name)
                compare(name,documents)

    impl-v2/v5/notes/verification/verify.py · 第 155 至 181 行

        scenarios=[('tw-annual','Taiwan',False,False,False),('tw-monthly-coupon','Taiwan',True,True,False),('us-annual','United States',False,False,False),('jp-monthly-coupon','Japan',True,True,False),('us-company','United States',False,False,True)]
        for width in [390,1440]:
            for flow,country,monthly,with_coupon,company in scenarios:
                documents={step:{} for step in ['step2','step2-filled','step3','step4']}
                orders={}
                for label in ['main','new']:
                    context,page,records,errors=open_page(browser,label,width)
                    if monthly: select_monthly(page,label,width)
                    if with_coupon: coupon(page,'KOL10')
                    page.locator('#upgrade_step1_continue').click()
                    documents['step2'][label]=capture(page,label,f'{width}-{flow}-step2')
                    invoice(page,country,company)
                    documents['step2-filled'][label]=capture(page,label,f'{width}-{flow}-step2-filled')
                    page.locator('#upgrade_step2_continue').click()
                    documents['step3'][label]=capture(page,label,f'{width}-{flow}-step3')
                    card(page)
                    documents['step4'][label]=capture(page,label,f'{width}-{flow}-step4')
                    page.get_by_role('checkbox').check()
                    expect(page.locator('#upgrade_step4_confirm')).to_be_enabled()
                    page.locator('#upgrade_step4_confirm').click()
                    page.wait_for_function('document.querySelector(".Toastify__toast") !== null')
                    assert len(records['orders'])==1, records
                    orders[label]=records['orders'][0]
                    record(context,records,errors,f'{label}-{width}-{flow}')
                for step,docs in documents.items(): compare(f'{width}-{flow}-{step}',docs)
                assert orders['main']==orders['new'], orders
                RESULTS['behavior'].append({'case':f'{width}-{flow}-mock-order','payloadEqual':True,'payload':orders['new']})
  7. [7] 24 種文字與價格狀態

    既有 v5 紀錄。Instagram、Messenger、Threads,各測繁中、英文、日文、西文的年繳與月繳。共 12 個年繳狀態、12 個月繳狀態。這些是 fixture 值,不是正式後端現價。

    impl-v2/v5/logs/copy-browser-run.log · 第 1 至 1 行

    {"passed": true, "states": 24, "errors": []}

    impl-v2/v5/logs/copy-browser-results.json · 第 2 至 219 行

      "states": [
        {
          "case": "instagram-zh_tw-annual",
          "social": 2,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$324/月\n(已套用年繳免費 2 個月優惠)",
          "annualAmount": 3890,
          "derivedMonthlyPrice": 324
        },
        {
          "case": "instagram-zh_tw-monthly",
          "social": 2,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$389/月\n月繳方案",
          "monthlyAmount": 389,
          "offerAbsent": true
        },
        {
          "case": "instagram-en_us-annual",
          "social": 2,
          "locale": "en_us",
          "currency": "US$",
          "cta": "US$11/month\n(2 months free with annual billing applied)",
          "annualAmount": 130,
          "derivedMonthlyPrice": 11
        },
        {
          "case": "instagram-en_us-monthly",
          "social": 2,
          "locale": "en_us",
          "currency": "US$",
          "cta": "US$13/month\nMonthly plan",
          "monthlyAmount": 13,
          "offerAbsent": true
        },
        {
          "case": "instagram-ja_jp-annual",
          "social": 2,
          "locale": "ja_jp",
          "currency": "US$",
          "cta": "US$11/月\n(年払いで2か月分無料の特典を適用済み)",
          "annualAmount": 130,
          "derivedMonthlyPrice": 11
        },
        {
          "case": "instagram-ja_jp-monthly",
          "social": 2,
          "locale": "ja_jp",
          "currency": "US$",
          "cta": "US$13/月\n月払いプラン",
          "monthlyAmount": 13,
          "offerAbsent": true
        },
        {
          "case": "instagram-es_es-annual",
          "social": 2,
          "locale": "es_es",
          "currency": "US$",
          "cta": "US$11/mes\n(2 meses gratis por pago anual aplicados)",
          "annualAmount": 130,
          "derivedMonthlyPrice": 11
        },
        {
          "case": "instagram-es_es-monthly",
          "social": 2,
          "locale": "es_es",
          "currency": "US$",
          "cta": "US$13/mes\nPlan mensual",
          "monthlyAmount": 13,
          "offerAbsent": true
        },
        {
          "case": "messenger-zh_tw-annual",
          "social": 1,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$3,199/月\n(已套用年繳免費 2 個月優惠)",
          "annualAmount": 38388,
          "derivedMonthlyPrice": 3199
        },
        {
          "case": "messenger-zh_tw-monthly",
          "social": 1,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$3,899/月\n月繳方案",
          "monthlyAmount": 3899,
          "offerAbsent": true
        },
        {
          "case": "messenger-en_us-annual",
          "social": 1,
          "locale": "en_us",
          "currency": "US$",
          "cta": "US$107/month\n(2 months free with annual billing applied)",
          "annualAmount": 1279,
          "derivedMonthlyPrice": 107
        },
        {
          "case": "messenger-en_us-monthly",
          "social": 1,
          "locale": "en_us",
          "currency": "US$",
          "cta": "US$129/month\nMonthly plan",
          "monthlyAmount": 129,
          "offerAbsent": true
        },
        {
          "case": "messenger-ja_jp-annual",
          "social": 1,
          "locale": "ja_jp",
          "currency": "US$",
          "cta": "US$107/月\n(年払いで2か月分無料の特典を適用済み)",
          "annualAmount": 1279,
          "derivedMonthlyPrice": 107
        },
        {
          "case": "messenger-ja_jp-monthly",
          "social": 1,
          "locale": "ja_jp",
          "currency": "US$",
          "cta": "US$129/月\n月払いプラン",
          "monthlyAmount": 129,
          "offerAbsent": true
        },
        {
          "case": "messenger-es_es-annual",
          "social": 1,
          "locale": "es_es",
          "currency": "US$",
          "cta": "US$107/mes\n(2 meses gratis por pago anual aplicados)",
          "annualAmount": 1279,
          "derivedMonthlyPrice": 107
        },
        {
          "case": "messenger-es_es-monthly",
          "social": 1,
          "locale": "es_es",
          "currency": "US$",
          "cta": "US$129/mes\nPlan mensual",
          "monthlyAmount": 129,
          "offerAbsent": true
        },
        {
          "case": "threads-zh_tw-annual",
          "social": 3,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$133/月\n(已套用年繳免費 2 個月優惠)",
          "annualAmount": 1590,
          "derivedMonthlyPrice": 133
        },
        {
          "case": "threads-zh_tw-monthly",
          "social": 3,
          "locale": "zh_tw",
          "currency": "NT$",
          "cta": "NT$159/月\n月繳方案",
          "monthlyAmount": 159,
          "offerAbsent": true
        },
        {
          "case": "threads-en_us-annual",
          "social": 3,
          "locale": "en_us",
          "currency": "US$",
          "cta": "US$5/month\n(2 months free with annual billing applied)",
          "annualAmount": 60,
          "derivedMonthlyPrice": 5
        },
        {
          "case": "threads-en_us-monthly",
          "social": 3,
          "locale": "en_us",
          "currency": "US$",
          "cta": "US$6/month\nMonthly plan",
          "monthlyAmount": 6,
          "offerAbsent": true
        },
        {
          "case": "threads-ja_jp-annual",
          "social": 3,
          "locale": "ja_jp",
          "currency": "US$",
          "cta": "US$5/月\n(年払いで2か月分無料の特典を適用済み)",
          "annualAmount": 60,
          "derivedMonthlyPrice": 5
        },
        {
          "case": "threads-ja_jp-monthly",
          "social": 3,
          "locale": "ja_jp",
          "currency": "US$",
          "cta": "US$6/月\n月払いプラン",
          "monthlyAmount": 6,
          "offerAbsent": true
        },
        {
          "case": "threads-es_es-annual",
          "social": 3,
          "locale": "es_es",
          "currency": "US$",
          "cta": "US$5/mes\n(2 meses gratis por pago anual aplicados)",
          "annualAmount": 60,
          "derivedMonthlyPrice": 5
        },
        {
          "case": "threads-es_es-monthly",
          "social": 3,
          "locale": "es_es",
          "currency": "US$",
          "cta": "US$6/mes\nPlan mensual",
          "monthlyAmount": 6,
          "offerAbsent": true
        }
      ],
  8. [8] 10 組模擬送單的原始 request body 相同

    既有 v5 紀錄,本輪重讀 orders 內的原始 bytes,逐組與雜湊核對。情境為 TW 年繳、TW 月繳套碼、US 年繳、JP 月繳套碼、US 公司,各測 390px 與 1440px。全用 Instagram、繁中 fixture;收到請求後回傳 49999,沒有模擬成功成立訂單或付款。年繳加優惠碼的完整送單不在這組證據內。

    impl-v2/v5/logs/order-byte-proof.json · 第 1 至 91 行

    {
      "rawPostBodies": true,
      "pairs": [
        {
          "case": "390-tw-annual",
          "country": "TW",
          "bytes": 334,
          "identical": true,
          "mainSha256": "97625cbfc4593091e799f0de4fdf44ac4b2feb79a384e2a4f79aca60bb5452e2",
          "newSha256": "97625cbfc4593091e799f0de4fdf44ac4b2feb79a384e2a4f79aca60bb5452e2"
        },
        {
          "case": "390-tw-monthly-coupon",
          "country": "TW",
          "bytes": 339,
          "identical": true,
          "mainSha256": "c459bad269c87837a2375d11f30da884a9d7383b473df06c73b0fea0649bcb75",
          "newSha256": "c459bad269c87837a2375d11f30da884a9d7383b473df06c73b0fea0649bcb75"
        },
        {
          "case": "390-us-annual",
          "country": "US",
          "bytes": 334,
          "identical": true,
          "mainSha256": "b6bddf46daf9add6e5e6899a788ce3290c1a362907f7ba15a6bfeeda07a0b314",
          "newSha256": "b6bddf46daf9add6e5e6899a788ce3290c1a362907f7ba15a6bfeeda07a0b314"
        },
        {
          "case": "390-jp-monthly-coupon",
          "country": "JP",
          "bytes": 339,
          "identical": true,
          "mainSha256": "289604a0f66b0e1321cb001aff167d9e364bacef5ca6aaa3938abeb9b9283fd5",
          "newSha256": "289604a0f66b0e1321cb001aff167d9e364bacef5ca6aaa3938abeb9b9283fd5"
        },
        {
          "case": "390-us-company",
          "country": "US",
          "bytes": 327,
          "identical": true,
          "mainSha256": "08eb21916c116659c8828ea516420732f6b083d81d80992e8d425f158fb9654a",
          "newSha256": "08eb21916c116659c8828ea516420732f6b083d81d80992e8d425f158fb9654a"
        },
        {
          "case": "1440-tw-annual",
          "country": "TW",
          "bytes": 334,
          "identical": true,
          "mainSha256": "97625cbfc4593091e799f0de4fdf44ac4b2feb79a384e2a4f79aca60bb5452e2",
          "newSha256": "97625cbfc4593091e799f0de4fdf44ac4b2feb79a384e2a4f79aca60bb5452e2"
        },
        {
          "case": "1440-tw-monthly-coupon",
          "country": "TW",
          "bytes": 339,
          "identical": true,
          "mainSha256": "c459bad269c87837a2375d11f30da884a9d7383b473df06c73b0fea0649bcb75",
          "newSha256": "c459bad269c87837a2375d11f30da884a9d7383b473df06c73b0fea0649bcb75"
        },
        {
          "case": "1440-us-annual",
          "country": "US",
          "bytes": 334,
          "identical": true,
          "mainSha256": "b6bddf46daf9add6e5e6899a788ce3290c1a362907f7ba15a6bfeeda07a0b314",
          "newSha256": "b6bddf46daf9add6e5e6899a788ce3290c1a362907f7ba15a6bfeeda07a0b314"
        },
        {
          "case": "1440-jp-monthly-coupon",
          "country": "JP",
          "bytes": 339,
          "identical": true,
          "mainSha256": "289604a0f66b0e1321cb001aff167d9e364bacef5ca6aaa3938abeb9b9283fd5",
          "newSha256": "289604a0f66b0e1321cb001aff167d9e364bacef5ca6aaa3938abeb9b9283fd5"
        },
        {
          "case": "1440-us-company",
          "country": "US",
          "bytes": 327,
          "identical": true,
          "mainSha256": "08eb21916c116659c8828ea516420732f6b083d81d80992e8d425f158fb9654a",
          "newSha256": "08eb21916c116659c8828ea516420732f6b083d81d80992e8d425f158fb9654a"
        }
      ],
      "countries": {
        "TW": 4,
        "US": 4,
        "JP": 2
      },
      "allByteIdentical": true
    }

    impl-v2/v5/notes/verification/fixture.py · 第 55 至 66 行

            elif u.path == '/api/v1/billing/coupon/check':
                coupon = parse_qs(u.query).get('code', [''])[0]
                if coupon == 'ERROR40002':
                    code = 40002
                else:
                    data = {'discountType': 1, 'discountValue': 0.9}
            elif u.path == '/api/v1/billing/order':
                records['orders'].append(json.loads(request.post_data))
                code = 49999
            else:
                raise AssertionError('Unexpected mock API: ' + request.url)
            request_route.fulfill(status=200, content_type='application/json', body=json.dumps({'code': code, 'msg': 'LOCAL MOCK ONLY', 'data': data}))
  9. [9] 型別、lint 與 Jest 的既有退出碼

    既有 v5 紀錄。本輪沒有重跑產品測試。upgrade 23、billing 6、currency 5,合計 34 tests、7 suites;各退出碼為 0。billing log 仍含測試中的 401 error 輸出,不把「通過」寫成完全沒有錯誤訊息。

    impl-v2/v5/logs/tsc.log · 第 1 至 3 行

    $ node node_modules/typescript/bin/tsc --noEmit -p apps/sorghum/tsconfig.app.json
    
    exit=0

    impl-v2/v5/logs/lint.log · 第 1 至 17 行

    $ node node_modules/nx/bin/nx.js lint sorghum-features-setting-upgrade --skip-nx-cache
    
    > nx run sorghum-features-setting-upgrade:lint
    
    
    Linting "sorghum-features-setting-upgrade"...
    
    All files pass linting.
    
    
     
    
     >  NX   Successfully ran target lint for project sorghum-features-setting-upgrade
    
    
    
    exit=0

    impl-v2/v5/logs/jest-upgrade.log · 第 7 至 13 行

    Test Suites: 2 passed, 2 total
    Tests:       23 passed, 23 total
    Snapshots:   0 total
    Time:        50.73 s
    Ran all test suites.
    
    exit=0

    impl-v2/v5/logs/jest-billing.log · 第 153 至 159 行

    Test Suites: 4 passed, 4 total
    Tests:       6 passed, 6 total
    Snapshots:   0 total
    Time:        4.081 s, estimated 5 s
    Ran all test suites matching /useGetBillingPlan\/|useGetBillingCouponCheck\/|useGetBillingPayment\/|usePostBillingOrder\//i.
    
    exit=0

    impl-v2/v5/logs/jest-currency.log · 第 12 至 18 行

    Test Suites: 1 passed, 1 total
    Tests:       5 passed, 5 total
    Snapshots:   0 total
    Time:        0.601 s, estimated 1 s
    Ran all test suites matching /currency\//i.
    
    exit=0

    impl-v2/v5/logs/checks.json · 第 40 至 47 行

      "verifiedSourceHashes": {
        "apps/sorghum/src/assets/locales/en_us/messenger.json": "ebcab3adf629dacdca68192a377f8fb5fa4e6470b225c42cb93d4e25ad25d75a",
        "apps/sorghum/src/assets/locales/es_es/messenger.json": "67c12d3d7cfdcc3b9e86f4b33b021cb7489e80fa9a262c74677f2e37def50709",
        "apps/sorghum/src/assets/locales/ja_jp/messenger.json": "e1d43cf1ab88ff192557d4cc45d54445dc5668d837c98c2a880f809e59bacddb",
        "apps/sorghum/src/assets/locales/zh_tw/messenger.json": "8c80fab484c0277b6652cf335a9b1a21181244721ad1bce9a85c64a5df46d530",
        "libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx": "e785c70c22788a499befc845ed65fc700415afd6d78a4a4cdea984be982952a5"
      }
    }
  10. [10] Messenger 的「2」是文案決策

    Marcus 在本輪 03:40 指示所有產品與幣別一律寫 2 個月。價格背景沿用已保存的本機後端來源證據,不代表正式環境現價。以下只取 pricing-proof.json 的 dataset=source、social=1 兩筆算式,不引用舊版 CTA 文案。v5 mock 的美元價格另為 129/1,279,換算約 2.09,不能與來源的 119/1,188 混用。

    來源幣別月價/年價換算免費月數
    TWD3,899/38,388(3,899 × 12 − 38,388) ÷ 3,899
    = 8,400 ÷ 3,899 ≈ 2.15
    USD119/1,188(119 × 12 − 1,188) ÷ 119
    = 240 ÷ 119 ≈ 2.02

    impl-v2/v4/logs/pricing-proof.json:4–28。只引用 monthlyPrice、yearlyPrice、savingAmount、actualMonths。

    impl-v2/v4/logs/source-prices-origin.json · 第 1 至 7 行

    {
      "path": "malasun/src/business/billing/constant.ts",
      "sha256": "08d323ffaa24b07b6e7b8cee649fc7939eb78b9db592c4380adba835f995346b",
      "repoHead": "16861b6517fa0853039b34185777a4bb6b4ed675",
      "fileStatus": "",
      "evidenceBoundary": "Local source only; no backend or Production calls"
    }

    impl-v2/v5/notes/verification/fixture.py · 第 7 至 7 行

    PRICES = {1: (3899, 38388, 129, 1279), 2: (389, 3890, 13, 130), 3: (159, 1590, 6, 60)}

    固定文案只用在手機年繳主按鈕。展開後的原方案卡仍依價格顯示節省月數。

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 844 至 858 行

    // 年繳卡片要寫明「省幾個月(省多少錢)」:只寫每月換算價時客戶看不出年繳划算在哪
    const yearlySaving = getYearlySaving(monthlyPrice, yearlyPrice);
    const yearlySavingAmount = `${currencySymbol}${formatNumberWithCommas(
      yearlySaving?.amount ?? 0,
    )}`;
    const yearlyDiscountText = !yearlySaving
      ? undefined
      : yearlySaving.months < 1
      ? t('setting.upgrade.step1.saveAmount', { amount: yearlySavingAmount })
      : t(
          yearlySaving.isExactMonths
            ? 'setting.upgrade.step1.saveMonths'
            : 'setting.upgrade.step1.saveOverMonths',
          { months: yearlySaving.months, amount: yearlySavingAmount },
        );

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 1225 至 1233 行

    discountText={yearlyDiscountText}
    billedNote={
      yearlyPrice
        ? t('setting.upgrade.step1.billedYearly', {
            amount: `${currencySymbol}${formatNumberWithCommas(
              yearlyPrice,
            )}`,
          })
        : undefined
  11. [11] JP 使用 USD 顯示

    來源確認。選國家後,TW 以外都用 USD;尚未選國家時依語系決定,日文使用 USD。這是顯示分支,不是本輪對實際扣款幣別的驗證。

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 827 至 836 行

    const [chosenCountry, setChosenCountry] = useState('');
    // 顯示幣別:在帳單資訊選了國家後以國家為準,否則看語系(沿用左側摘要原本的規則)。
    // 卡片、左側摘要、「省 N 個月」共用這一份,避免回上一步時卡片美金、摘要台幣。
    // 注意:這只是顯示;後端目前一律以台幣扣款(malasun createOrder 用 price+Currency.TWD)
    const isUsd = chosenCountry
      ? chosenCountry !== 'TW'
      : isUsdLanguage(i18next.language);
    const currencySymbol = isUsd ? 'US$' : 'NT$';
    const planPrices = getPlanPrices(billingPlans, isUsd);
    const { monthlyPrice, yearlyPrice, yearlyPerMonthPrice } = planPrices;

    71d18d522:libs/sorghum/utils/src/lib/currency/index.ts · 第 4 至 8 行

    export const USD_LANGUAGES = ['en', 'ja', 'es'];
    
    // i18next.language 在初始化前可能是 undefined,所以參數允許空值
    export const isUsdLanguage = (language?: string | null): boolean =>
      !!language && USD_LANGUAGES.some((code) => language.includes(code));

    impl-v2/v5/logs/jest-currency.log · 第 5 至 10 行

      isUsdLanguage
        ✓ en_us:美金 (1 ms)
        ✓ ja_jp:美金
        ✓ es_es:美金
        ✓ 繁中:台幣
        ✓ 語系尚未初始化:台幣,不會拋錯
  12. [12] 40002 查碼錯誤仍顯示通用訊息

    來源確認與既有紀錄。handleSubmitCoupon 只傳 code、socialType,非 20000 統一設 isCouponError;沒有依付款週期解釋 40002。main 與 target 對話框相同。這是既有問題,不是本次新增的下單錯誤處理。

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 954 至 975 行

    const handleSubmitCoupon = useCallback(() => {
      getCouponData(
        { code: couponCode, socialType: socialType as SocialTypeEnum },
        {
          onSuccess: (data, variables) => {
            if (data.code === 20000) {
              setIsOpenEnterCouponModal(false);
              setIsShowCouponCode(true);
              // 只更新兌換碼:用 functional update,避免等待回覆期間預設週期被自動校正後,
              // 又被送出當下的舊資料蓋回去(月繳客戶會被默默換成年繳)
              setBillingData((prev) => ({
                ...prev,
                planDetail: { ...prev.planDetail, couponCode: variables.code },
              }));
              setCouponDiscount(data.data);
            } else {
              setIsCouponError(true);
            }
          },
        },
      );
    }, [couponCode, getCouponData, socialType]);

    impl-v2/v5/logs/browser-results.json · 第 1298 至 1305 行

        },
        {
          "case": "main-coupon-40002",
          "dialogText": "填寫兌換碼\n\n兌換碼\n兌換碼\n\n已使用或失效的兌換碼\n\n取消\n套用"
        },
        {
          "case": "new-coupon-40002",
          "dialogText": "填寫兌換碼\n\n兌換碼\n兌換碼\n\n已使用或失效的兌換碼\n\n取消\n套用"
  13. [13] Commit 使用自動身分

    既有 log 與本輪 git show 已核對。target 的 Author、Committer 均為 redacted。本輪沒有改身分或 commit。

    impl-v2/v5/logs/commit.log · 第 2 至 5 行

    [mcta/mobile-annual-default-v2-20261023 71d18d522] fix(billing): use fixed two-month copy for the mobile annual CTA
     Committer: Shaokan Pi <redacted>
    Your name and email address were configured automatically based
    on your username and hostname. Please check that they are accurate.
  14. [14] 缺價時仍會組出 0 元按鈕文字

    來源確認。若 billingPlans 未取得,getPlanPrices 將缺值代為 0。手機第一步無條件放入 continueButton,文字直接讀價格;hasPlanData 只使按鈕停用。新購年繳仍顯示固定優惠。這支持顯示錯誤的靜態判斷;既有成功取價測試未覆蓋此分支,本輪未在產品瀏覽器重現。

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/plan-default.ts · 第 86 至 99 行

    export const getPlanPrices = (
      billingPlans: BillingPlan[] | undefined,
      isUsd: boolean,
    ): PlanPrices => {
      const pick = (index: number) => {
        const item = billingPlans?.[index];
        return (isUsd ? item?.usdAmount : item?.amount) ?? 0;
      };
      const yearlyPrice = pick(1);
      return {
        monthlyPrice: pick(0),
        yearlyPrice,
        yearlyPerMonthPrice: Math.round(yearlyPrice / 12),
      };

    71d18d522:libs/editor/utils/src/lib/convertNumber.ts · 第 29 至 40 行

    export const formatNumberWithCommas = (inputNumber: number) => {
      if (isUndefined(inputNumber)) return '';
    
      // Convert the number to a string
      const numberString = inputNumber.toString();
    
      // Check if the length of the string is greater than 3
      if (numberString.length > 3) {
        // Use regular expressions to add commas as thousands separators
        return numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
      } else {
        return numberString;

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 1048 至 1055 行

    const selectedPlan = get(billingData, 'planDetail.period', 1);
    const continueButton = (
      <Button
        id="upgrade_step1_continue"
        variant="contained"
        // 價格資料不完整時不得進入結帳(避免 0 元訂單流程);
        // 預設週期未確認前也不得進入(避免月繳客戶帶著暫時的年繳結帳)
        disabled={!hasPlanData || !isDefaultPeriodReady}

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 1074 至 1088 行

    <Box component="span" sx={{ fontSize: '18px', fontWeight: 700 }}>
      {t('setting.upgrade.step1.mobileMonthlyPrice', {
        price: `${currencySymbol}${formatNumberWithCommas(
          selectedPlan === PERIOD_MONTHLY
            ? monthlyPrice
            : yearlyPerMonthPrice,
        )}`,
      })}
    </Box>
    <Box component="span" sx={{ fontSize: '12px', fontWeight: 400 }}>
      {t(
        selectedPlan === PERIOD_MONTHLY
          ? 'setting.upgrade.step1.mobileMonthlyPlan'
          : 'setting.upgrade.step1.mobileYearlyOffer',
      )}

    71d18d522:libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx · 第 1137 至 1142 行

    {activeStep === 0 && (
      <RightContentContainerStyled
        sx={isMobileStepOne ? { width: '100%', px: '16px' } : undefined}
      >
        {isMobileStepOne && continueButton}
        {isMobileStepOne && !isPlanLoading && hasPlanData && (
  15. [15] Prototype 證據有自己的範圍

    既有紀錄。prototype 的 13 項檢查使用有 WIP 的另一個 checkout,僅補充操作展示。這些數字不合併進 target 的 96 組比較或 10 組訂單,也不代表正式付款驗證。

    impl-v2/v5/prototype/verification.json · 第 1 至 30 行

    {
      "pass": true,
      "verified_at": "2026-10-22T19:34:44.752356+00:00",
      "url": "(internal preview, removed)",
      "viewport": {
        "width": 390,
        "height": 844
      },
      "browser": "145.0.7632.6",
      "checks": [
        "Annual is selected on fresh load; options collapsed; CTA NT$324/月 with the exact v5 annual offer",
        "View other plans expands both real plan cards",
        "Monthly selection updates CTA price and label to NT$389/月 月繳方案",
        "ERROR shows original invalid-coupon message and does not change selected period",
        "SUCCESS shows coupon chip and discounted monthly summary NT$350; CTA retains source base price",
        "Monthly checkout proceeds to real step 2; coupon retained; payment continuation disabled",
        "Step 2 back preserves monthly/coupon; coupon removal and annual reselection work; plans collapse",
        "Annual SUCCESS discount NT$3,501 and annual step 2 display work",
        "Hash routing, local return page, direct checkout hash reload, and reset remain under artifact subpath",
        "messenger: annual fixed offer, computed price, monthly unchanged",
        "messenger: reset preserves the product and restores annual default",
        "threads: annual fixed offer, computed price, monthly unchanged",
        "threads: reset preserves the product and restores annual default"
      ],
      "screenshots": [
        "01-initial-annual-full.png",
        "01-initial-annual.png",
        "02-other-plans-full.png",
        "02-other-plans.png",
        "03-monthly-full.png",

    impl-v2/v5/prototype/source-proof.json · 第 1 至 10 行

    {
      "head": "1f4b321a34cf8ee0c201b83211363ce9ae98329e",
      "headUnchanged": true,
      "branch": "mcta/proto-v3-20261023",
      "status": " M apps/sorghum/src/assets/locales/en_us/messenger.json\n M apps/sorghum/src/assets/locales/es_es/messenger.json\n M apps/sorghum/src/assets/locales/ja_jp/messenger.json\n M apps/sorghum/src/assets/locales/zh_tw/messenger.json\n M libs/sorghum/features/setting-upgrade/src/lib/plan-default.ts\n M libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx\n?? proto/\n",
      "existingPlanDefaultRefactorUnchanged": true,
      "componentOnlySameSublineChange": true,
      "fourLocalesByteIdenticalToImplementation": true,
      "changedPrototypeFiles": [
        "proto/fixtures.ts",
完整原始 diff · 兩個固定快照

git -C gosky-dashboard/frontend --no-optional-locks diff --no-ext-diff --no-textconv --binary --full-index a7c25efc2abd403b1b14837564a4c4ab7ca8b80a 71d18d5226082261722a32da33919282de150be5

SHA-256 285fd8e8fadcf0063625fe1c804975f3acc84b7179f610f9b6e76a39114e5933

diff --git a/apps/sorghum/src/assets/locales/en_us/messenger.json b/apps/sorghum/src/assets/locales/en_us/messenger.json
index 0c6ec0068a2e101c2892a835bf5b804e4258283b..b2c7053f0cbdf484ffbcc059042bce7483189de4 100644
--- a/apps/sorghum/src/assets/locales/en_us/messenger.json
+++ b/apps/sorghum/src/assets/locales/en_us/messenger.json
@@ -2652,6 +2652,10 @@
         "billedYearly": "Billed {{amount}} yearly",
         "continue": "Continue",
         "loadFailed": "Plan information is currently unavailable. Please reload.",
+        "mobileMonthlyPlan": "Monthly plan",
+        "mobileMonthlyPrice": "{{price}}/month",
+        "mobileViewOtherPlans": "View other plans",
+        "mobileYearlyOffer": "(2 months free with annual billing applied)",
         "monthly": "Monthly",
         "note": "Automatically renews after subscription",
         "retryLoad": "Reload",
diff --git a/apps/sorghum/src/assets/locales/es_es/messenger.json b/apps/sorghum/src/assets/locales/es_es/messenger.json
index b4d1b0fafcc86ccdf740466df349af25f5cc9287..de2747286b6b5e4a25b35f55fde95f6e4e188a55 100644
--- a/apps/sorghum/src/assets/locales/es_es/messenger.json
+++ b/apps/sorghum/src/assets/locales/es_es/messenger.json
@@ -855,6 +855,10 @@
         "billedYearly": "Se cobra {{amount}} al año",
         "continue": "Continúa",
         "loadFailed": "La información del plan no está disponible en este momento. Vuelve a cargar.",
+        "mobileMonthlyPlan": "Plan mensual",
+        "mobileMonthlyPrice": "{{price}}/mes",
+        "mobileViewOtherPlans": "Ver otros planes",
+        "mobileYearlyOffer": "(2 meses gratis por pago anual aplicados)",
         "monthly": "Plan mensual",
         "note": "Se renueva automáticamente después de la suscripción",
         "retryLoad": "Recargar",
diff --git a/apps/sorghum/src/assets/locales/ja_jp/messenger.json b/apps/sorghum/src/assets/locales/ja_jp/messenger.json
index c66751d1e5a74c637f580ecb7360bf0cb34701fc..f2279c8ef473593be4a4496a0725ada4745b5b1e 100644
--- a/apps/sorghum/src/assets/locales/ja_jp/messenger.json
+++ b/apps/sorghum/src/assets/locales/ja_jp/messenger.json
@@ -2671,6 +2671,10 @@
         "billedYearly": "年額 {{amount}} 一括払い",
         "continue": "続行",
         "loadFailed": "現在プラン情報を取得できません。再読み込みしてください。",
+        "mobileMonthlyPlan": "月払いプラン",
+        "mobileMonthlyPrice": "{{price}}/月",
+        "mobileViewOtherPlans": "他のプランを見る",
+        "mobileYearlyOffer": "(年払いで2か月分無料の特典を適用済み)",
         "monthly": "月額プラン",
         "note": "サブスクリプションは自動更新されます",
         "retryLoad": "再読み込み",
diff --git a/apps/sorghum/src/assets/locales/zh_tw/messenger.json b/apps/sorghum/src/assets/locales/zh_tw/messenger.json
index 53c75dbe803069b5046c6fd3fb67ee8999c05fd5..405689c1e30c26d84a1870d0f4fb13a92a3bd0df 100644
--- a/apps/sorghum/src/assets/locales/zh_tw/messenger.json
+++ b/apps/sorghum/src/assets/locales/zh_tw/messenger.json
@@ -2662,6 +2662,10 @@
         "billedYearly": "一次收 {{amount}}/年",
         "continue": "繼續",
         "loadFailed": "目前無法取得方案資訊,請重新載入",
+        "mobileMonthlyPlan": "月繳方案",
+        "mobileMonthlyPrice": "{{price}}/月",
+        "mobileViewOtherPlans": "查看其他方案",
+        "mobileYearlyOffer": "(已套用年繳免費 2 個月優惠)",
         "monthly": "月方案",
         "note": "自動在訂閱後續訂",
         "retryLoad": "重新載入",
diff --git a/libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx b/libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx
index da0877b85aeb671e390da48052b3b75bf2482219..b0ee1ea410033be73c130a2c0545611ab8983dd6 100644
--- a/libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx
+++ b/libs/sorghum/features/setting-upgrade/src/lib/sorghum-features-setting-upgrade.tsx
@@ -49,6 +49,7 @@ import {
 } from '@frontend/sorghum/utils';
 import CheckCircleIcon from '@mui/icons-material/CheckCircle';
 import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
 import LocalOfferIcon from '@mui/icons-material/LocalOffer';
 import {
   Box,
@@ -58,6 +59,8 @@ import {
   Step,
   StepLabel,
   TextField,
+  useMediaQuery,
+  useTheme,
 } from '@mui/material';
 import i18next from 'i18next';
 import { get, isEmpty, toNumber } from 'lodash';
@@ -784,6 +787,8 @@ export function SorghumFeaturesSettingUpgrade({
   tapPayAppID: number;
 }) {
   const { navigateTo404, navigateToProjectPage } = usePath();
+  const theme = useTheme();
+  const isMobile = useMediaQuery(theme.breakpoints.down('md'));
 
   // 手機版右下角 Intercom launcher 會蓋住付款流程的「下一步」按鈕
   useHideIntercomLauncher();
@@ -859,6 +864,9 @@ export function SorghumFeaturesSettingUpgrade({
   useGetAccountInfo();
 
   const [activeStep, setActiveStep] = useState(0);
+  const [showOtherPlans, setShowOtherPlans] = useState(false);
+  const isMobileStepOne = isMobile && activeStep === 0;
+  const showPlanOptions = !isMobileStepOne || showOtherPlans;
   const [billingData, setBillingData] = useState<BillingDataType>({
     last4CardNum: '',
     planDetail: {
@@ -1038,6 +1046,53 @@ export function SorghumFeaturesSettingUpgrade({
   }, [account, hasPlanData, isDefaultPeriodReady, projectID, socialType]);
 
   const selectedPlan = get(billingData, 'planDetail.period', 1);
+  const continueButton = (
+    <Button
+      id="upgrade_step1_continue"
+      variant="contained"
+      // 價格資料不完整時不得進入結帳(避免 0 元訂單流程);
+      // 預設週期未確認前也不得進入(避免月繳客戶帶著暫時的年繳結帳)
+      disabled={!hasPlanData || !isDefaultPeriodReady}
+      sx={{
+        mt: isMobileStepOne ? 0 : '48px',
+        width: isMobileStepOne ? '100%' : { sm: '100%', md: 'fit-content' },
+        py: isMobileStepOne ? '10px' : undefined,
+        mb: isMobileStepOne ? 0 : { sm: '96px', md: 0 },
+        alignSelf: 'flex-end',
+      }}
+      onClick={() => {
+        // 按下一步視為已確認週期,之後方案資料才回來也不再改動
+        hasPickedPeriod.current = true;
+        setActiveStep(1);
+      }}
+    >
+      {isMobileStepOne ? (
+        <Box
+          component="span"
+          sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
+        >
+          <Box component="span" sx={{ fontSize: '18px', fontWeight: 700 }}>
+            {t('setting.upgrade.step1.mobileMonthlyPrice', {
+              price: `${currencySymbol}${formatNumberWithCommas(
+                selectedPlan === PERIOD_MONTHLY
+                  ? monthlyPrice
+                  : yearlyPerMonthPrice,
+              )}`,
+            })}
+          </Box>
+          <Box component="span" sx={{ fontSize: '12px', fontWeight: 400 }}>
+            {t(
+              selectedPlan === PERIOD_MONTHLY
+                ? 'setting.upgrade.step1.mobileMonthlyPlan'
+                : 'setting.upgrade.step1.mobileYearlyOffer',
+            )}
+          </Box>
+        </Box>
+      ) : (
+        t('setting.upgrade.step1.continue')
+      )}
+    </Button>
+  );
 
   return (
     <PageContainerStyled>
@@ -1080,10 +1135,43 @@ export function SorghumFeaturesSettingUpgrade({
         </LeftContainerStyled>
         <RightContainerStyled>
           {activeStep === 0 && (
-            <RightContentContainerStyled>
-              <Typography variant="h5" sx={{ mb: '32px' }}>
-                {t('setting.upgrade.step1.selectMethod')}
-              </Typography>
+            <RightContentContainerStyled
+              sx={isMobileStepOne ? { width: '100%', px: '16px' } : undefined}
+            >
+              {isMobileStepOne && continueButton}
+              {isMobileStepOne && !isPlanLoading && hasPlanData && (
+                <Link
+                  component="button"
+                  type="button"
+                  variant="body2"
+                  underline="always"
+                  aria-expanded={showOtherPlans}
+                  aria-controls="upgrade_step1_plan_options"
+                  onClick={() => setShowOtherPlans((shown) => !shown)}
+                  sx={{
+                    alignSelf: 'center',
+                    mt: '12px',
+                    mb: '24px',
+                    display: 'inline-flex',
+                    alignItems: 'center',
+                    gap: '4px',
+                  }}
+                >
+                  {t('setting.upgrade.step1.mobileViewOtherPlans')}
+                  <ExpandMoreIcon
+                    sx={{
+                      width: '20px',
+                      height: '20px',
+                      transform: showOtherPlans ? 'rotate(180deg)' : 'none',
+                    }}
+                  />
+                </Link>
+              )}
+              {showPlanOptions && (
+                <Typography variant="h5" sx={{ mb: '32px' }}>
+                  {t('setting.upgrade.step1.selectMethod')}
+                </Typography>
+              )}
               {isPlanLoading && (
                 <Box
                   sx={{ gap: '12px', display: 'flex', flexDirection: 'column' }}
@@ -1115,7 +1203,12 @@ export function SorghumFeaturesSettingUpgrade({
               )}
               {!isPlanLoading && hasPlanData && (
                 <Box
-                  sx={{ gap: '12px', display: 'flex', flexDirection: 'column' }}
+                  id={isMobileStepOne ? 'upgrade_step1_plan_options' : undefined}
+                  sx={{
+                    gap: '12px',
+                    display: showPlanOptions ? 'flex' : 'none',
+                    flexDirection: 'column',
+                  }}
                 >
                   <PlanOptionWrapper
                     type={t('setting.upgrade.step1.monthly')}
@@ -1150,27 +1243,7 @@ export function SorghumFeaturesSettingUpgrade({
               >
                 {t('setting.upgrade.step1.note')}
               </Typography>
-              <Button
-                id="upgrade_step1_continue"
-                variant="contained"
-                // 價格資料不完整時不得進入結帳(避免 0 元訂單流程);
-                // 預設週期未確認前也不得進入(避免月繳客戶帶著暫時的年繳結帳)
-                disabled={!hasPlanData || !isDefaultPeriodReady}
-                // 手機:滿版+底部留白,避免右下角浮動的 Intercom 客服鈕蓋住「下一步」
-                sx={{
-                  mt: '48px',
-                  width: { sm: '100%', md: 'fit-content' },
-                  mb: { sm: '96px', md: 0 },
-                  alignSelf: 'flex-end',
-                }}
-                onClick={() => {
-                  // 按下一步視為已確認週期,之後方案資料才回來也不再改動
-                  hasPickedPeriod.current = true;
-                  setActiveStep(1);
-                }}
-              >
-                {t('setting.upgrade.step1.continue')}
-              </Button>
+              {!isMobileStepOne && continueButton}
             </RightContentContainerStyled>
           )}
           {activeStep === 1 && (