動態優惠碼

透過專屬連結選擇 Stripe Coupon,在首頁顯示對應優惠,並在建立 Checkout 時自動套用。

Saavo 內建透過連結選擇動態優惠的功能。使用者開啟 /offer/:id 後,伺服器會記住這次選擇,首頁隨即顯示對應價格與促銷文案。購買指定方案時,系統會自動將預先設定的 Stripe Coupon 帶入 Checkout。

目前 config/products.ts 沒有設定任何動態優惠,因此雖然已有功能入口,預設不會產生優惠。如果要使用,需先在 Stripe 建立 Coupon,再將 Coupon ID 與公開活動 ID,設定於個別方案的 couponOffers。

這裡的「優惠碼」不需要使用者輸入

動態優惠透過專屬連結選擇,使用者不會在網站上手動輸入代碼。連結中的 id 是網站公開的活動識別碼,實際送到 Stripe 的則是伺服器設定中的 coupon。

運作流程

完整流程如下:

使用者開啟 /offer/autumn-20-off
→ 伺服器確認活動存在且尚未到期
→ 寫入有效期間為 30 分鐘的 HttpOnly Cookie
→ 重新導向目前語言的首頁
→ 首頁顯示該方案的活動價格與文案
→ 使用者建立 Checkout
→ 系統向 Stripe 提交活動對應的 Coupon ID
→ Checkout 建立成功後清除優惠 Cookie

專案預設支援下列功能:

功能目前行為
活動入口GET /offer/:id,支援語言前綴
選擇紀錄saavo_coupon_offer Cookie,有效期間為 30 分鐘
首頁顯示可覆寫指定方案的價格、標籤、特色與購買區塊文案
Checkout自動套用符合活動的 Stripe Coupon
到期控制可透過 expiresAt 設定活動截止時間
無效活動清除舊選擇並重新導向首頁,不顯示錯誤頁面

回應會設定 Cache-Control: private, no-store,避免包含個人優惠選擇的頁面被共用快取儲存。

在 Stripe 建立優惠券

先在 Stripe Dashboard 建立 Coupon,確認折扣比例或金額、幣別、適用產品、持續時間與兌換限制。儲存後,記下 Coupon ID,例如 coupon_launch_20_off。

這裡需要的是 Stripe Coupon ID,不是讓使用者輸入的 Promotion Code。專案只負責將既有 Coupon 套用至 Checkout,不會依設定自動建立、更新或停用 Stripe Coupon。

網站活動的截止時間,與 Stripe Coupon 的有效規則彼此獨立。即使 expiresAt 已到期,Stripe 中的 Coupon 仍可能有效。反過來,如果 Stripe Coupon 已失效,網站活動卻仍有效,Checkout 就會建立失敗。兩邊必須分別設定,並保持一致。

設定動態優惠活動

在 config/products.ts 找到要參與活動的方案,新增 couponOffers:

license: {
    type: 'recurring',
    interval: 'month',
    intervalCount: 1,
    priceId: 'price_hobby_monthly_replace_me',
    salePrice: '$19',
    // 省略其餘方案設定

    couponOffers: [
        {
            id: 'autumn-20-off',
            coupon: 'coupon_launch_20_off',
            expiresAt: Date.parse('2026-10-01T00:00:00Z'),
            salePrice: '$15.20',
            listPrice: '$19',
            discountLabel: { value: '限時 8 折' },
            badgeLabel: { value: '秋季優惠' },
            valueHint: { value: '活動至 2026 年 9 月 30 日止' },
        },
    ],
},

couponOffers 可以設定多項活動,但每項活動只能屬於一個方案。

必填設定

欄位說明
id公開活動 ID,用於 /offer/:id 連結
coupon建立 Checkout 時提交的 Stripe Coupon ID

id 在整份產品目錄中必須唯一,最長 64 個字元,只能使用小寫字母、數字與單一連字號,例如 autumn-20-off。

同一方案中的各項動態活動,必須使用不同 Coupon,也不能與方案預設的 coupon 相同。設定不符合這些要求時,專案會在啟動或建置階段報錯。

選用設定

expiresAt 是以毫秒為單位的 Unix 時間戳記,代表不包含截止時刻的活動期限。只要目前時間達到該值,活動就立即失效。未設定時則長期有效。

活動也可以覆寫下列顯示欄位:

  • title、description、features。
  • salePrice、listPrice、discountLabel、billingLabel。
  • savingsLabel、badgeLabel、valueHint。
  • presentation 中的購買標題、說明、產品資訊清單、按鈕、規格與亮點。

這些欄位只改變頁面顯示,不會修改 Stripe Price、實際折扣或使用者最終取得的權限。顯示金額必須由你依 Coupon 規則計算並填入,專案不會自動依折扣比例換算 salePrice。

為了避免活動連結改變產品結構,動態優惠不能覆寫下列內容:

  • priceId、方案類型與計費週期。
  • 角色、使用權益與試用期。
  • 是否推薦、是否預設、是否允許重複購買。
  • 方案啟用狀態或巢狀的 couponOffers。

完成設定並部署後,即可發布活動連結:

https://example.com/offer/autumn-20-off

指定語言時,使用對應前綴:

https://example.com/zh-Hant/offer/autumn-20-off

成功存取後,使用者會被重新導向相同語言的首頁。如果活動 ID 不存在、已到期或格式錯誤,系統同樣會回到首頁,並清除瀏覽器原本的動態優惠選擇。

目前設定只有截止時間,沒有開始時間。如果活動需要定時開始,應在開始時才發布連結或部署對應設定,不能使用尚未提供的 startsAt 欄位預先安排。

優惠選擇何時失效

saavo_coupon_offer Cookie 有下列屬性:

  • HttpOnly:前端指令碼無法讀取或修改。
  • SameSite=Lax:允許使用者從外部行銷連結進入網站。
  • Secure:正式環境僅透過 HTTPS 傳送。
  • Path=/:首頁與 Checkout 都能讀取。
  • Max-Age=1800:選擇會保留 30 分鐘。

伺服器每次使用時,都會重新確認活動仍存在且尚未到期,因此無法透過手動偽造 Cookie,使用未設定的 Coupon。

不同情況的處理方式如下:

情況使用的優惠Cookie 處理
活動與目前方案相符動態活動的 couponCheckout 建立成功後清除
沒有活動選擇方案預設 coupon,若沒有則不自動套用不寫入
活動不存在或已到期改用方案預設設定立即清除
活動屬於其他方案目前方案使用預設設定暫時保留
Stripe 拒絕 Coupon,Checkout 建立失敗未建立 Checkout保留,方便修正後重試

優惠選擇是在 Checkout Session 建立成功後就被使用,不會等到付款成功。使用者進入 Stripe 頁面後,即使放棄付款,網站的優惠 Cookie 也已清除,但已建立的 Checkout Session 仍保留該 Coupon。

與其他優惠方式的差異

方案本身可以設定固定的 coupon,表示每次購買該方案,都會自動套用相同的 Stripe Coupon。符合動態活動時,會以活動的 coupon 覆寫預設值。

config/payment.ts 中的 stripe.enablePromoCodes,控制 Stripe Checkout 是否允許使用者手動輸入 Promotion Code。只要目前 Checkout 已帶有固定 Coupon,不論來自方案預設值或動態活動,專案就不會同時啟用 Promotion Code 輸入欄位,因為 Stripe 不允許同時提交這兩種方式。

選擇優惠方式時,可以依下列原則:

  • 提供給所有購買者的長期折扣:使用方案層級的 coupon。
  • 透過廣告、電子郵件或合作管道提供專屬價格:使用 couponOffers。
  • 讓使用者自行輸入公開或私下提供的代碼:啟用 enablePromoCodes,且不要為該 Checkout 預設 Coupon。

目前限制

動態優惠目前只串接首頁產品展示與一般付款 Checkout。OAuth2 授權流程中的升級 Checkout,仍讀取方案預設 Coupon,不會使用透過 /offer/:id 選擇的動態活動。

首頁的 JSON-LD 商品結構化資料,也仍使用預設方案價格,不包含單次請求的動態顯示價格。

專案也未內建下列功能:

  • 自動建立或停用 Stripe Coupon。
  • 活動開始時間、領取次數,或每位使用者的使用次數限制。
  • 動態優惠活動管理後台。
  • 依活動 ID 統計造訪、Checkout 與成交轉換。

Stripe 回呼同步的交易紀錄會儲存實際折扣金額,但不會將動態活動 ID 當成獨立的行銷歸因欄位。如果需要活動層級報表,應另外設計歸因與統計方式,不能只依賴 saavo_coupon_offer Cookie。

這個 Cookie 會在使用者主動開啟優惠連結時寫入,不受目前 Cookie 同意元件的分類開關控制。上線前,應依實際用途更新 Cookie 政策,並參考 Cookie 同意管理核對說明。

上線檢查

  • 已在 Stripe 建立 Coupon,ID 與 couponOffers[].coupon 完全一致。
  • Coupon 的折扣、幣別、適用產品、持續時間與限制,符合方案規則。
  • 活動 id 全域唯一,並符合小寫字母、數字與連字號格式。
  • expiresAt 使用毫秒時間戳記,且已核對時區與截止時刻。
  • 頁面顯示價格與 Stripe Checkout 的實際金額一致。
  • 活動連結會返回正確語言的首頁,並顯示預期文案。
  • 購買正確方案時,會自動套用活動 Coupon,購買其他方案時不會誤用。
  • Checkout 建立失敗時不會清除優惠選擇,建立成功後則會清除。
  • 固定 Coupon 與 enablePromoCodes 的組合符合產品預期。
  • Cookie 政策已說明 saavo_coupon_offer 的用途與有效期間。

常見問題

接下來