開發者指南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
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
customerId | string | 條件必填 | 您系統中的客戶 ID(external ID) |
recurCustomerId | string | 條件必填 | Recur 內部客戶 ID(二擇一) |
publishableKey | string | 選填 | 不使用 RecurProvider 時必填 |
baseUrl | string | 選填 | 自訂 API URL(預設 https://api.recur.tw) |
customerId 或 recurCustomerId 至少需要一個。這是 API 的安全要求,用於防止匿名暴力列舉優惠碼。
Return 值
| 屬性 | 類型 | 說明 |
|---|---|---|
apply | (code: string) => Promise<void> | 驗證優惠碼 |
clear | () => void | 清除狀態 |
status | PromoCodeStatus | 當前狀態 |
isLoading | boolean | 驗證進行中 |
isValid | boolean | 優惠碼有效 |
code | string | null | 驗證通過的優惠碼,直接傳給 subscribe() |
discount | PromoCodeDiscount | null | 折扣資訊 |
appliesToAllProducts | boolean | 是否適用所有產品 |
applicableProducts | ApplicableProduct[] | 適用產品(含折後價) |
error | string | 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 | 類型 | 預設 | 說明 |
|---|---|---|---|
promo | UsePromoCodeReturn | 必填 | usePromoCode() 的回傳值 |
placeholder | string | "優惠碼" | Input placeholder |
applyText | string | "套用" | 套用按鈕文字 |
clearText | string | "移除" | 移除按鈕文字 |
disabled | boolean | false | 停用 |
unstyled | boolean | false | 關閉預設樣式 |
className | string | - | 容器 CSS class |
style | CSSProperties | - | 容器 inline style |
inputClassName | string | - | Input CSS class |
inputStyle | CSSProperties | - | Input inline style |
buttonClassName | string | - | 按鈕 CSS class |
buttonStyle | CSSProperties | - | 按鈕 inline style |
messageClassName | string | - | 訊息 CSS class |
messageStyle | CSSProperties | - | 訊息 inline style |
連續輸入處理
apply() 內建自動取消機制 — 如果用戶快速連續輸入,只有最後一次請求會生效,不會產生 race condition。
API 參考
底層使用 POST /v1/promotion-codes/validate,完整 API 文件請參考 Promotion Codes API。
Last updated on