開發者指南結帳整合
錯誤處理
處理付款失敗與結帳錯誤
錯誤處理
Recur SDK 提供完整的錯誤處理機制,讓您可以優雅地處理付款失敗和其他結帳錯誤。
錯誤類型
SDK 區分兩種錯誤類型:
| 回調 | 觸發時機 | 說明 |
|---|---|---|
onPaymentFailed | 用戶送出付款後失敗 | 卡片被拒絕、餘額不足等 |
onError | 結帳流程中發生錯誤 | 網路問題、API 錯誤、重複訂閱等 |
onPaymentFailed 是專門為付款失敗設計的回調,讓您可以根據失敗原因提供更好的用戶體驗。
API 錯誤碼
Recur API 使用標準的 HTTP 狀態碼和錯誤碼:
| HTTP 狀態 | 錯誤碼 | 說明 |
|---|---|---|
| 400 | bad_request | 請求參數無效 |
| 401 | unauthorized | API Key 無效或缺失 |
| 402 | payment_required | 付款失敗 |
| 403 | forbidden | 無權存取此資源 |
| 404 | not_found | 找不到資源 |
| 409 | conflict | 資源衝突(如重複訂閱) |
| 422 | validation_error | 驗證失敗 |
| 429 | rate_limit_exceeded | 請求過於頻繁 |
| 500 | internal_server_error | 伺服器錯誤 |
處理重複訂閱錯誤(409)
當客戶嘗試訂閱已有有效訂閱的商品時,API 會返回 409 conflict 錯誤:
await checkout({
productId: 'prod_xxx',
customerEmail: 'user@example.com',
onError: (error) => {
if (error.code === 'conflict') {
// 客戶已有該商品的有效訂閱
const details = error.details?.[0];
if (details?.existing_subscription_id) {
console.log('現有訂閱 ID:', details.existing_subscription_id);
console.log('訂閱狀態:', details.status);
// 引導用戶到 Customer Portal 管理訂閱
window.location.href = '/account/subscriptions';
}
}
},
});付款失敗錯誤碼
當付款失敗時,error.details.failure_code 會包含以下其中一個錯誤碼:
| 錯誤碼 | 說明 | 可重試 |
|---|---|---|
PAYUNI_DECLINED | 銀行拒絕交易 | ❌ |
UNAPPROVED | 交易未獲授權 | ❌ |
INSUFFICIENT_FUNDS | 餘額不足 | ❌ |
CARD_DECLINED | 卡片被拒絕 | ❌ |
EXPIRED_CARD | 卡片已過期 | ❌ |
INVALID_CARD | 無效的卡號 | ❌ |
NETWORK_ERROR | 網路連線問題 | ✅ |
TIMEOUT | 交易逾時 | ✅ |
UNKNOWN | 未知錯誤 | ❌ |
「可重試」表示問題可能是暫時性的,用戶可以使用相同的卡片再次嘗試。不可重試的錯誤通常需要用戶更換卡片或聯繫銀行。
使用 onPaymentFailed
基本用法
onPaymentFailed 讓您可以自訂付款失敗的處理方式:
await checkout({
productId: 'prod_xxx',
onPaymentFailed: (error) => {
console.log('付款失敗:', {
code: error.details?.failure_code,
message: error.details?.failure_message,
canRetry: error.details?.can_retry,
});
// 回傳 undefined 使用 SDK 預設行為
},
});自訂處理邏輯
您可以根據不同的失敗原因回傳不同的處理動作:
await checkout({
productId: 'prod_xxx',
onPaymentFailed: (error) => {
const failureCode = error.details?.failure_code;
// 餘額不足 - 顯示自訂訊息
if (failureCode === 'INSUFFICIENT_FUNDS') {
return {
action: 'custom',
customTitle: '餘額不足',
customMessage: '您的卡片餘額不足,請使用其他付款方式或聯繫銀行。',
};
}
// 卡片過期 - 關閉結帳視窗
if (failureCode === 'EXPIRED_CARD') {
return { action: 'close' };
}
// 其他錯誤 - 使用預設行為(允許重試)
return undefined;
},
});處理動作
onPaymentFailed 可以回傳以下動作:
| action | 說明 |
|---|---|
'retry' | 顯示錯誤訊息,保持結帳視窗開啟讓用戶重試(預設行為) |
'close' | 關閉結帳視窗,觸發 onError 回調 |
'custom' | 顯示自訂的標題和訊息 |
// 允許重試(預設)
return { action: 'retry' };
// 關閉視窗
return { action: 'close' };
// 自訂訊息
return {
action: 'custom',
customTitle: '付款失敗',
customMessage: '請聯繫客服取得協助',
};完整範例
React 範例
'use client';
import { useRecur } from 'recur-tw';
export function SubscribeButton({ productId }: { productId: string }) {
const { checkout, isCheckingOut } = useRecur();
const handleSubscribe = async () => {
await checkout({
productId,
onPaymentComplete: (result) => {
console.log('訂閱成功!', result);
window.location.href = '/success';
},
onPaymentFailed: (error) => {
// 根據錯誤類型自訂處理
const code = error.details?.failure_code;
if (code === 'INSUFFICIENT_FUNDS') {
return {
action: 'custom',
customTitle: '餘額不足',
customMessage: '請確認卡片餘額或使用其他卡片',
};
}
if (code === 'EXPIRED_CARD') {
return {
action: 'custom',
customTitle: '卡片已過期',
customMessage: '請使用有效的信用卡',
};
}
// 可重試的錯誤 - 使用預設行為
if (error.details?.can_retry) {
return { action: 'retry' };
}
// 其他錯誤 - 使用預設行為
return undefined;
},
onError: (error) => {
// 處理其他錯誤(網路問題等)
alert('發生錯誤:' + error.message);
},
});
};
return (
<button onClick={handleSubscribe} disabled={isCheckingOut}>
{isCheckingOut ? '處理中...' : '立即訂閱'}
</button>
);
}'use client';
import { useSubscribe } from 'recur-tw';
export function SubscribeButton({ productId }: { productId: string }) {
const { subscribe, isLoading, error } = useSubscribe({
onPaymentComplete: (result) => {
console.log('訂閱成功!', result);
window.location.href = '/success';
},
onPaymentFailed: (error) => {
const code = error.details?.failure_code;
// 根據錯誤碼自訂處理
switch (code) {
case 'INSUFFICIENT_FUNDS':
return {
action: 'custom',
customTitle: '餘額不足',
customMessage: '請確認卡片餘額或使用其他卡片',
};
case 'EXPIRED_CARD':
return {
action: 'custom',
customTitle: '卡片已過期',
customMessage: '請使用有效的信用卡',
};
default:
return undefined; // 使用預設行為
}
},
onError: (error) => {
console.error('結帳錯誤:', error);
},
});
return (
<div>
<button
onClick={() => subscribe({ productId })}
disabled={isLoading}
>
{isLoading ? '處理中...' : '立即訂閱'}
</button>
{error && <p className="text-red-500">{error.message}</p>}
</div>
);
}Vanilla JS 範例
const recur = RecurCheckout.init({
publishableKey: 'pk_test_xxx',
});
document.getElementById('subscribe-btn').addEventListener('click', async () => {
await recur.checkout({
productId: 'prod_xxx',
mode: 'modal',
onPaymentFailed: (error) => {
console.log('付款失敗:', error);
// 根據錯誤碼處理
switch (error.details?.failure_code) {
case 'INSUFFICIENT_FUNDS':
return {
action: 'custom',
customTitle: '餘額不足',
customMessage: '請確認卡片餘額或使用其他卡片。',
};
case 'NETWORK_ERROR':
case 'TIMEOUT':
// 網路問題 - 允許重試
return { action: 'retry' };
default:
// 使用 SDK 預設行為
return undefined;
}
},
onPaymentComplete: (result) => {
window.location.href = '/success';
},
onError: (error) => {
alert('發生錯誤:' + error.message);
},
});
});錯誤物件結構
interface CheckoutError {
/** 錯誤代碼(如 'conflict', 'not_found', 'payment_required') */
code: string;
/** 人類可讀的錯誤訊息 */
message: string;
/** 相關文件連結(選填) */
docUrl?: string;
/** 錯誤詳情(選填) */
details?: Array<{
// 付款失敗詳情
failure_code?: string; // 付款失敗代碼
failure_message?: string; // 付款失敗訊息
can_retry?: boolean; // 是否可重試
// 重複訂閱詳情(當 code 為 'conflict')
existing_subscription_id?: string; // 現有訂閱 ID
status?: string; // 現有訂閱狀態
}>;
}最佳實踐
1. 提供清楚的錯誤訊息
用戶需要知道發生了什麼以及該怎麼做:
onPaymentFailed: (error) => {
if (error.details?.failure_code === 'CARD_DECLINED') {
return {
action: 'custom',
customTitle: '卡片被拒絕',
customMessage: '您的銀行拒絕了此筆交易。請聯繫發卡銀行或使用其他卡片。',
};
}
}2. 根據 can_retry 決定處理方式
onPaymentFailed: (error) => {
if (error.details?.can_retry) {
// 可重試的錯誤 - 讓用戶再試一次
return { action: 'retry' };
} else {
// 不可重試 - 提供替代方案
return {
action: 'custom',
customTitle: '付款失敗',
customMessage: '請使用其他付款方式或聯繫客服。',
};
}
}3. 記錄錯誤以便分析
onPaymentFailed: (error) => {
// 記錄到分析服務
analytics.track('payment_failed', {
failure_code: error.details?.failure_code,
failure_message: error.details?.failure_message,
can_retry: error.details?.can_retry,
});
return undefined; // 使用預設行為
}4. 後端驗證
永遠在後端使用 Webhook 來驗證付款狀態。前端回調僅用於改善用戶體驗,不應作為業務邏輯的依據。
下一步
- Webhook 整合 - 在後端接收付款通知
- 基本結帳範例 - 查看完整的結帳範例
- 自訂樣式 - 自訂結帳視窗外觀