Recur
開發者指南React Hooks

usePromoCode

驗證優惠碼、取得折扣資訊的 React Hook

usePromoCode

usePromoCode 讓你在前端即時驗證優惠碼,取得折扣資訊和適用產品。

這是唯一可以不需要 RecurProvider 獨立使用的 Hook。傳入 publishableKey 即可。

基本用法

import { usePromoCode } from 'recur-tw';

function PromoInput({ userId }) {
  // publishableKey 從 Provider context 取得
  const promo = usePromoCode({ customerId: userId });

  return (
    <div>
      <input
        placeholder="輸入優惠碼"
        onBlur={(e) => promo.apply(e.target.value)}
      />
      {promo.isLoading && <span>驗證中...</span>}
      {promo.isValid && <span className="text-green-600">{promo.discount?.label}</span>}
      {promo.error && <span className="text-red-500">{promo.error}</span>}
    </div>
  );
}
import { usePromoCode } from 'recur-tw';

function PromoInput({ userId }) {
  // 直接傳入 publishableKey,不需要 RecurProvider
  const promo = usePromoCode({
    publishableKey: 'pk_test_xxx',
    customerId: userId,
  });

  return (
    <div>
      <input
        placeholder="輸入優惠碼"
        onBlur={(e) => promo.apply(e.target.value)}
      />
      {promo.isValid && <span className="text-green-600">{promo.discount?.label}</span>}
      {promo.error && <span className="text-red-500">{promo.error}</span>}
    </div>
  );
}

Options

參數類型必填說明
customerIdstring條件必填您系統中的客戶 ID(external ID)
recurCustomerIdstring條件必填Recur 內部客戶 ID(二擇一)
publishableKeystring選填不使用 RecurProvider 時必填
baseUrlstring選填自訂 API URL(預設 https://api.recur.tw

customerIdrecurCustomerId 至少需要一個。這是 API 的安全要求,用於防止匿名暴力列舉優惠碼。

Return 值

屬性類型說明
apply(code: string) => Promise<void>驗證優惠碼
clear() => void清除狀態
statusPromoCodeStatus當前狀態
isLoadingboolean驗證進行中
isValidboolean優惠碼有效
codestring | null驗證通過的優惠碼,直接傳給 subscribe()
discountPromoCodeDiscount | null折扣資訊
appliesToAllProductsboolean是否適用所有產品
applicableProductsApplicableProduct[]適用產品(含折後價)
errorstring | null錯誤訊息

PromoCodeStatus

type PromoCodeStatus = 'idle' | 'validating' | 'valid' | 'invalid' | 'already_used';

PromoCodeDiscount

interface PromoCodeDiscount {
  type: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FIRST_PERIOD_PRICE';
  amount: number;
  discountedAmount: number;
  label: string;           // "20% off", "NT$100 off"
  bonusTrialDays?: number;
  bonusMonths?: number;
}

ApplicableProduct

interface ApplicableProduct {
  id: string;
  name: string;
  price: number;
  priceAfterDiscount: number;
  interval?: string;
  intervalCount?: number;
}

顯示折後價

當優惠碼限定特定產品時,applicableProducts 會包含每個產品的折後價:

function PricingWithPromo({ userId }) {
  const promo = usePromoCode({ customerId: userId });

  return (
    <div>
      <input onBlur={(e) => promo.apply(e.target.value)} />

      {promo.isValid && !promo.appliesToAllProducts && (
        <div>
          <p>此優惠碼適用於以下產品:</p>
          {promo.applicableProducts.map((product) => (
            <div key={product.id}>
              <span>{product.name}</span>
              <span className="line-through">NT${product.price}</span>
              <span className="text-green-600 font-bold">
                NT${product.priceAfterDiscount}
              </span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

搭配 useSubscribe 結帳

promo.code 在優惠碼有效時是驗證過的字串,無效或未輸入時是 null,可以直接傳給 subscribe()

import { useSubscribe, usePromoCode } from 'recur-tw';

function CheckoutPage({ userId, userEmail }) {
  const promo = usePromoCode({ customerId: userId });
  const { subscribe, isLoading } = useSubscribe({
    onPaymentComplete: () => window.location.href = '/welcome',
  });

  return (
    <div>
      <input
        placeholder="優惠碼(選填)"
        onBlur={(e) => promo.apply(e.target.value)}
      />
      {promo.isValid && <p className="text-green-600">{promo.discount?.label}</p>}

      <button
        disabled={isLoading}
        onClick={() => subscribe({
          productId: 'prod_xxx',
          customerEmail: userEmail,
          promoCode: promo.code,  // null if no valid promo
        })}
      >
        {isLoading ? '處理中...' : '訂閱'}
      </button>
    </div>
  );
}

PromoCodeInput 元件

不想自己建 UI?使用 <PromoCodeInput /> 即可獲得完整的優惠碼輸入體驗:

import { usePromoCode, PromoCodeInput } from 'recur-tw';

function Checkout({ userId }) {
  const promo = usePromoCode({ customerId: userId });

  return <PromoCodeInput promo={promo} />;
}

包含:input 欄位 + 套用/移除按鈕 + loading 狀態 + 成功/錯誤訊息。

自訂樣式

// 開箱即用,有基本外觀
<PromoCodeInput promo={promo} />
// 用 className 覆蓋樣式
<PromoCodeInput
  promo={promo}
  unstyled
  className="flex flex-col gap-1"
  inputClassName="border rounded-lg px-3 py-2 uppercase tracking-wider"
  buttonClassName="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700"
  messageClassName="text-sm mt-1"
/>
// 用 inline style 微調
<PromoCodeInput
  promo={promo}
  inputStyle={{ border: '2px solid #6366f1', borderRadius: 8 }}
  buttonStyle={{ backgroundColor: '#6366f1', color: 'white' }}
  style={{ maxWidth: 400 }}
/>
// 不用元件,只用 hook 自己建 UI
const promo = usePromoCode({ customerId: userId });

<input onBlur={(e) => promo.apply(e.target.value)} />
<button onClick={() => promo.clear()}>清除</button>
{promo.isValid && <span>{promo.discount?.label}</span>}
{promo.error && <span>{promo.error}</span>}

PromoCodeInput Props

Prop類型預設說明
promoUsePromoCodeReturn必填usePromoCode() 的回傳值
placeholderstring"優惠碼"Input placeholder
applyTextstring"套用"套用按鈕文字
clearTextstring"移除"移除按鈕文字
disabledbooleanfalse停用
unstyledbooleanfalse關閉預設樣式
classNamestring-容器 CSS class
styleCSSProperties-容器 inline style
inputClassNamestring-Input CSS class
inputStyleCSSProperties-Input inline style
buttonClassNamestring-按鈕 CSS class
buttonStyleCSSProperties-按鈕 inline style
messageClassNamestring-訊息 CSS class
messageStyleCSSProperties-訊息 inline style

連續輸入處理

apply() 內建自動取消機制 — 如果用戶快速連續輸入,只有最後一次請求會生效,不會產生 race condition。

API 參考

底層使用 POST /v1/promotion-codes/validate,完整 API 文件請參考 Promotion Codes API

Last updated on

On this page