GOSKY · 手機結帳變更閱讀
origin/main a7c25efc2 → 71d18d522 · 5 個檔案,+115/−26 [1]
手機結帳先顯示主按鈕,
其他方案按需展開
只改手機第一步。資訊卡仍在最上方,新購仍預選年繳。主按鈕移到資訊卡下方,顯示換算月價及固定年繳優惠。按「查看其他方案」可展開選項;改選月繳後,按鈕顯示月價與「月繳方案」。[1] [2] [3] [4]
桌面、第 2 到 4 步、共用付款與訂單程式、計價均未改。[1] [3] 本頁解說候選程式與 mock 證據,未驗證正式環境或真實付款。
資訊卡之後,直接接下一步
原本先列兩種方案,再顯示「繼續」。現在先放主按鈕,換方案時再展開。新購預選年繳是既有規則。[1] [2] [5]
Instagram · 繁中 · 390px · 年繳預選 · 相同 mock 月價 NT$389、年價 NT$3,890。[5] [7]
impl-v2/v3/shots/main-default-full.png
impl-v2/v5/shots/instagram-zh_tw-annual-full.png
年繳仍一次收年費。這裡的 NT$324 是測試年價 NT$3,890 ÷ 12 四捨五入後的月額,並非每月扣款。[2] [5] [7]
狀態決定選項,所選方案決定按鈕
結帳元件管理這些顯示條件;翻譯檔提供四語系文字。按鈕沿用同一份方案資料,不新增訂單或折扣算法。[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/24 | Instagram、Messenger、Threads,各測繁中、英文、日文、西文的年繳與月繳。月繳保留小字且沒有優惠副行。[7] |
| 模擬送單 | 10/10 | Instagram 繁中,TW 4、US 4、JP 2 組 request body 逐位元組相同。fixture 回傳 49999;沒有驗證成功成立訂單或真實付款。[8] |
| 行為 | 19 項 | 包含上述 10 組訂單,以及初始、換月繳、套碼、返回、縮放、錯誤及語言案例。[6] |
| 程式檢查 | exit 0 | TypeScript、lint;Jest upgrade 23、billing 6、currency 5,合計 34 tests、7 suites。[9] |
Prototype 的另一組操作紀錄來自不同 checkout,僅作補充展示,不合併計數。[15]
已決定的差異與待追蹤事項
- Messenger 主按鈕固定寫 2 個月,是 Marcus 已決定的文案。保存的價格來源在 TWD 約為 2.15、USD 約為 2.02;mock 的美元值另約為 2.09。展開後的方案卡仍按價格計算節省月數。這些數值不是正式現價。[10]
- 缺價時顯示 0 元是本輪列出的程式問題。觸發於手機第一步尚未取得價格;按鈕已停用,無法前往第二步。顯示持續時間與實際畫面仍未驗證。[14]
- 操作理解尚未驗證。按鈕只顯示價格與優惠,「一次收全年費用」的方案文字需展開才看到。資訊卡仍列年方案與總額;使用者能否明白按鈕會前往第二步,以及月額只是換算價,本輪沒有做使用者測試。[5]
- JP 使用 USD 顯示,沒有 JPY 分支。這是既有顯示規則,實際扣款未在本輪驗證。[11]
- 畫面價格都是 mock。既有 10 組訂單沒有年繳加優惠碼的完整流程,也沒有各產品的真實付款驗證。[7] [8]
- 查碼回傳 40002 時仍顯示「已使用或失效的兌換碼」,沒有說明週期不符。這是既有通用錯誤。[12]
- Commit 作者與提交者仍為自動身分
(自動身分,已隱藏)。[13]
本輪只列問題與證據界線,未修產品。沒有以 mock 通過推導正式環境驗收或轉換率改善。
來源可在同一頁讀取
程式片段來自固定 Git objects 或原始 diff;log 摘錄保留原始路徑與行號。來源確認、既有紀錄及未驗證項目分開標示。
展開 claim 對照與來源摘錄
[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(-)
[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] 手機條件、資訊卡與下一步
來源確認。手機指寬度低於 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 && ([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] 前後畫面使用相同 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] 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'], resultimpl-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] 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] 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] 型別、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=0impl-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=0impl-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=0impl-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=0impl-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=0impl-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] Messenger 的「2」是文案決策
Marcus 在本輪 03:40 指示所有產品與幣別一律寫 2 個月。價格背景沿用已保存的本機後端來源證據,不代表正式環境現價。以下只取 pricing-proof.json 的 dataset=source、social=1 兩筆算式,不引用舊版 CTA 文案。v5 mock 的美元價格另為 129/1,279,換算約 2.09,不能與來源的 119/1,188 混用。
來源幣別 月價/年價 換算免費月數 TWD 3,899/38,388 (3,899 × 12 − 38,388) ÷ 3,899
= 8,400 ÷ 3,899 ≈ 2.15USD 119/1,188 (119 × 12 − 1,188) ÷ 119
= 240 ÷ 119 ≈ 2.02impl-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] 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] 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] 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] 缺價時仍會組出 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] 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 && (