Skip to content

Luồng dựa trên token ​

Token hóa thẻ trên trình duyệt, gửi token đến backend, và để backend tạo và xác nhận thanh toán. Dùng luồng này khi server của bạn chốt số tiền cuối cùng sau khi biết thẻ — giảm giá, phụ phí, kiểm tra gian lận — hoặc khi bạn xác nhận thanh toán phía server.

Nếu số tiền đã biết trước khi thanh toán, Xác nhận thanh toán đơn giản hơn.

Apple Pay và Google Pay dùng chính luồng này khi bạn gọi bahn.mountApplePay()/bahn.mountGooglePay() mà không có clientSecret: nút thanh toán phát sự kiện "token" kèm confirmation_token thay vì xác nhận thanh toán, và bạn tiếp tục từ bước 2.

Ví dụ đầy đủ

Code trình duyệt và server Node.js cho luồng này: Ví dụ tích hợp → Luồng dựa trên token.

Bước 1: Tạo confirmation token ​

typescript
const bahn = new Bahn({ pk: "pk_live_...", environment: "production" });
bahn.mountCard("#card-container");

const { confirmation_token } = await bahn.createConfirmationToken({
  payment_method: {
    billing_details: {
      name: "Nguyen Van A",
      email: "[email protected]",
      phone: "+84901234567",
      address: {
        country: "VN",
        city: "Ho Chi Minh City",
        line1: "123 Nguyen Hue",
        line2: "District 1",
        postal_code: "70000",
      },
    },
  },
});

Bước 2: Gửi token đến backend ​

typescript
const response = await fetch("/api/confirm-payment", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    confirmation_token,
    amount: 100000,
    currency: "vnd",
  }),
});

const { client_secret } = await response.json();

Backend của bạn tạo và xác nhận payment intent bằng token. Xem Hoàn tất thanh toán trên server.

Bước 3: Xử lý hành động tiếp theo ở phía client ​

typescript
try {
  await bahn.handleNextAction(client_secret, {
    onReady: () => console.log("Setting up..."),
    onSuccess: (paymentIntent) => redirectToSuccess(paymentIntent),
    onFailure: (paymentIntentOrError) => redirectToFailure(),
    onTimeout: () => console.warn("Polling timed out"),
    onError: (error) => console.error("Error:", error),
    onProgress: (attempt, max, status) => console.log(`Polling: ${attempt}/${max} — ${status}`),
  });
} catch (error) {
  // Chưa mount iframe thanh toán, hoặc không lấy được payment intent
  showGenericError();
}

Cách hoạt động ​

sequenceDiagram
    participant App as Your App
    participant SDK as Bahn SDK
    participant Iframe as Card Iframe
    participant Backend as Your Backend
    participant API as Cake API

    App->>SDK: mountCard
    App->>SDK: createConfirmationToken({ payment_method })
    SDK->>Iframe: Delegate + merge card data
    Iframe->>API: POST /api/sdk/confirmation_token
    API-->>Iframe: { confirmation_token }
    Iframe-->>SDK: Result (card data stays in iframe)
    SDK-->>App: { confirmation_token }

    App->>Backend: Send token + amount/currency
    Backend->>API: Create and confirm PaymentIntent with token
    API-->>Backend: PaymentIntent
    Backend-->>App: { client_secret }

    App->>SDK: handleNextAction(client_secret, callbacks)
    SDK->>Iframe: get-payment-intent (relay)
    Iframe->>API: GET payment intent status
    API-->>Iframe: PaymentIntent
    Iframe-->>SDK: PaymentIntent

    alt requires_action (3DS)
        SDK->>SDK: Decode 3DS payload, mount challenge
        SDK->>SDK: On success → start polling (via Iframe)
    else succeeded
        SDK-->>App: callbacks.onSuccess(paymentIntent)
    else failed
        SDK-->>App: callbacks.onFailure(paymentIntent)
    end

bahn.createConfirmationToken(payload) ​

Tạo một token dùng một lần cho thẻ trong các trường thẻ đã mount. Dữ liệu thẻ được ghép bên trong iframe — token an toàn để gửi đến backend của bạn.

typescript
createConfirmationToken(payload: Bahn.CreateConfirmationTokenOptions): Promise<Bahn.ConfirmationTokenResult>;

Tham số — CreateConfirmationTokenOptions:

TrườngKiểuBắt buộcMô tả
payment_method.billing_detailsConfirmationTokenBillingDetailsKhôngTên, email, số điện thoại và địa chỉ của khách hàng

Giá trị trả về — ConfirmationTokenResult:

typescript
interface ConfirmationTokenResult {
  confirmation_token: string;
}

Reject với APIError nếu pk chưa được cấu hình, cùng mọi trường hợp đã liệt kê cho confirmPayment(): chưa gọi mountCard(), trường thẻ không hợp lệ, lỗi HTTP và lỗi mạng.


bahn.handleNextAction(input, callbacks) ​

Xử lý hành động tiếp theo cần thiết cho một payment intent: thử thách 3DS, polling, hoặc trạng thái cuối ngay lập tức.

typescript
handleNextAction(input: Bahn.PaymentIntentInput, callbacks: Bahn.HandleNextActionCallbacks): Promise<void>;

input (PaymentIntentInput) chấp nhận một trong hai dạng:

  • client_secret (string) — SDK tự lấy payment intent qua get-payment-intent. Yêu cầu mountCard(), mountApplePay(), hoặc mountGooglePay() đang hoạt động — lệnh gọi này phải đi qua một trong các iframe đó (backend không cho phép CORS trên endpoint này, nên một fetch trực tiếp từ trang của bạn sẽ bị chặn). Nó không chạm vào dữ liệu thẻ, nên không nhất thiết cần iframe thẻ — một iframe Apple Pay hoặc Google Pay còn được mount sau khi luồng dựa trên token của nó hoàn tất cũng hoạt động tương tự. Nếu requires_action, nó poll trạng thái payment intent sau khi thử thách 3DS thành công cho đến khi đạt trạng thái cuối.
  • một đối tượng payment_intent — nếu backend của bạn đã xác nhận thanh toán và trả về payment_intent trong response, hãy truyền trực tiếp đối tượng đó (phải bao gồm client_secret). Cách này bỏ qua việc fetch get-payment-intent, nên không cần mount iframe thanh toán nào. Nó không bao giờ poll: nếu requires_action, onSuccess kích hoạt ngay khi thử thách 3DS thành công, với cùng đối tượng payment_intent bạn đã truyền vào — trạng thái cuối không được xác minh lại với backend.

Callback — HandleNextActionCallbacks:

CallbackChữ kýKhi nào
onReady() => voidThử thách 3DS đã sẵn sàng
onSuccess(paymentIntent: Record<string, unknown>) => voidThành công (trạng thái cuối)
onFailure(errorOrIntent?: ComponentError | Record<string, unknown>) => voidThất bại (trạng thái cuối), hoặc thử thách 3DS thất bại (ComponentError)
onTimeout() => voidPolling đạt số lần tối đa mà chưa có trạng thái cuối
onError(error: Error) => voidPolling hết số lần thử và lần poll cuối thất bại (xem pollPaymentIntentStatus), hoặc đầu vào là đối tượng payment_intent có trạng thái không phải cuối
onProgress(attempt: number, maxAttempts: number, status: string) => voidMỗi lần poll trả về payment intent

Reject với APIError — chỉ với đầu vào client_secret — trước khi bất kỳ callback nào được gọi, khi chưa mount iframe thanh toán nào hoặc lần fetch get-payment-intent đầu tiên thất bại (lỗi HTTP hoặc lỗi mạng). Mọi kết quả sau đó, kể cả 3DS và polling, đều được báo qua callback. Đầu vào là đối tượng payment_intent không bao giờ reject.

Cách hoạt động (đầu vào client_secret):

  1. Lấy payment intent hiện tại qua get-payment-intent
  2. Nếu succeeded/requires_capture/requires_confirmation → gọi onSuccess ngay
  3. Nếu canceled/failed/requires_payment_method → gọi onFailure ngay
  4. Nếu requires_action với 3DS → giải mã và mount thử thách 3DS, sau đó poll khi thành công cho đến khi đạt trạng thái cuối
  5. Với các trạng thái khác (processing) → poll trạng thái payment intent mỗi 3 giây cho đến khi đạt trạng thái cuối hoặc hết số lần thử tối đa

Cách hoạt động (đầu vào là đối tượng payment_intent): giống hệt bước 1–3 ở trên (không cần fetch, trạng thái được đọc trực tiếp từ đối tượng), nhưng bước 4 khác — sau khi thử thách 3DS thành công, onSuccess được gọi ngay với cùng đối tượng payment_intent, không poll và không cần iframe thanh toán. Một trạng thái không phải cuối, không phải requires_action (ví dụ processing) sẽ gọi onError — luồng này không bao giờ poll, nên không có cách nào giải quyết tiếp từ đây.

Truyền một payment_intent mà backend của bạn đã có ​

typescript
// Response từ backend đã bao gồm payment_intent, ví dụ:
// { payment_intent: { id, client_secret, status: "requires_action", next_action: {...}, ... } }
const { payment_intent } = await fetch("/api/confirm-payment", { method: "POST" }).then((r) => r.json());

await bahn.handleNextAction(payment_intent, {
  onSuccess: (paymentIntent) => redirectToSuccess(),
  onFailure: () => redirectToFailure(),
});

Lưu ý: luồng này bỏ qua lần fetch get-payment-intent ban đầu và không bao giờ poll sau đó. Hãy dùng đầu vào client_secret nếu bạn cần xác minh lại trạng thái cuối (ví dụ backend áp dụng thêm kiểm tra rủi ro sau 3DS) — khi đó bắt buộc phải có mountCard()/mountApplePay()/mountGooglePay(), vì polling luôn đi qua một iframe thanh toán đã mount (CORS).


bahn.pollPaymentIntentStatus(clientSecret, callbacks) ​

Poll trạng thái payment intent mỗi 3 giây cho đến khi đạt trạng thái cuối hoặc hết số lần thử tối đa. handleNextAction() gọi phương thức này thay bạn; hãy gọi trực tiếp sau một thử thách mount3DS() trong luồng Xác nhận thanh toán.

typescript
pollPaymentIntentStatus(clientSecret: string, callbacks: Bahn.PaymentIntentPollCallbacks): void;
typescript
bahn.pollPaymentIntentStatus("pi_secret_abc", {
  onProgress: (attempt, max, status) => console.log(`Poll ${attempt}/${max}: ${status}`),
  onSuccess: (paymentIntent) => redirectToSuccess(),
  onFailure: (paymentIntent) => redirectToFailure(),
  onTimeout: () => showTimeoutMessage(),
  onError: (error) => console.error("Poll error:", error.message),
});

Callback — PaymentIntentPollCallbacks:

CallbackKhi nào
onSuccessThành công (trạng thái cuối): succeeded, requires_capture, requires_confirmation
onFailureThất bại (trạng thái cuối): canceled, failed, requires_payment_method
onTimeoutĐạt số lần tối đa mà chưa có trạng thái cuối
onErrorĐạt số lần tối đa và lần poll cuối thất bại (lỗi mạng, lỗi HTTP hoặc response sai định dạng) — nhận lỗi đó. Các lần poll lỗi trước đó được thử lại mà không báo
onProgressMỗi lần poll trả về intent — nhận (attempt, maxAttempts, status)

Số lần tối đa được cấu hình qua paymentIntentPollAttempts trong BahnOptions (mặc định: 10).

bahn.stopPaymentIntentPolling() ​

Dừng mọi quá trình polling payment intent đang chạy.

typescript
window.addEventListener("beforeunload", () => {
  bahn.stopPaymentIntentPolling();
});

bahn.cleanup() cũng dừng polling và unmount mọi component. Xem Tham chiếu SDK.

Developers