Skip to content

Xác thực 3DS ​

Bahn SDK sử dụng luồng thử thách 3D Secure v2 tự lưu trữ (self-hosted) — không phụ thuộc vào CDN bên ngoài.

Trang này mô tả cách xử lý một thử thách (challenge) trên trình duyệt sau khi nó được kích hoạt. Để kiểm soát liệu 3DS có chạy hay không — tùy chọn request_three_d_secure phía server — xem Kiểm soát 3DS.

Hành vi 3DS trong Sandbox

Trong môi trường sandbox, luồng 3DS phụ thuộc vào số tiền thanh toán (VND):

Số tiềnKết quả 3DS
< 2.000.000 VNDFrictionless — 3DS tự động pass, không hiển thị thử thách
2.000.000 – 5.000.000 VNDChallenge — luồng thử thách 3DS đầy đủ được kích hoạt
> 5.000.000 VNDRejected — giao dịch bị từ chối

Sử dụng các ngưỡng này để kiểm thử cả ba kết quả 3DS trong sandbox trước khi lên production.

bahn.mount3DS(sessionId, callbacks, options?) ​

Gắn (mount) modal thử thách 3DS.

typescript
bahn.mount3DS(
  threeDsPayload.id,
  {
    onReady: () => console.log("3DS challenge loaded"),
    onSuccess: () => console.log("3DS authentication passed"),
    onFailure: (error) => console.error("3DS failed:", error.code, error.message),
  },
  { container: "#tds-wrapper" }
);

Tham số:

Tham sốKiểuBắt buộcMặc địnhMô tả
sessionIdstringCó—ID phiên 3DS lấy từ payload đã giải mã
callbacks.onReady() => voidKhông—Kích hoạt khi giao diện 3DS đã được gắn và sẵn sàng
callbacks.onSuccess() => voidCó—Thử thách 3DS thành công
callbacks.onFailure(error?) => voidCó—Thử thách 3DS thất bại
optionsThreeDSecureOptionsKhông—Các tùy chọn thử thách (selector container, v.v.)

ThreeDSCallbacks ​

typescript
interface ThreeDSCallbacks {
  onReady?: () => void;
  onSuccess: () => void;
  onFailure: (error?: ComponentError | Record<string, unknown>) => void;
}

Đối tượng error truyền vào onFailure bao gồm:

TrườngKiểuMô tả
codestringMã lỗi có thể đọc bằng máy
messagestringMô tả lỗi có thể đọc được bởi con người

Mã lỗi 3DS ​

error.code truyền vào onFailure là một trong các chuỗi kebab-case dưới đây (ví dụ "challenge-failed").

Giá trị error.codeThông báo mặc định
boot-failedGiao diện 3DS khởi tạo thất bại. Vui lòng thử lại.
frame-load-errorGiao diện 3DS tải thất bại. Vui lòng kiểm tra kết nối và thử lại.
session-completePhiên 3DS này đã hoàn tất. Không cần thao tác thêm.
session-not-foundKhông tìm thấy phiên 3DS. Vui lòng đảm bảo bạn đang sử dụng session ID hợp lệ.
session-unknown-errorĐã xảy ra lỗi không mong muốn khi giao tiếp với dịch vụ 3DS.
challenge-failedNgân hàng phát hành thẻ không thể xác minh giao dịch này. Vui lòng thử phương thức thanh toán khác.
challenge-cancelledViệc xác minh đã bị hủy.
challenge-abortedCần xác minh bổ sung nhưng đã bị bỏ qua bởi cấu hình thanh toán của bạn.
something-went-wrongĐã có lỗi xảy ra. Vui lòng thử lại.

Switch theo mã lỗi:

typescript
bahn.mount3DS(sessionId, {
  onSuccess: () => {},
  onFailure: (error) => {
    switch (error?.code) {
      case "challenge-cancelled":
        showMessage("You cancelled verification. Please try again.");
        break;
      case "challenge-failed":
        showMessage("Your bank could not verify this card. Try another card.");
        break;
      default:
        showMessage(error?.message ?? "3DS authentication failed.");
    }
  },
});

Gỡ bỏ (unmounting) ​

Modal sẽ tự động gỡ bỏ khi thử thách đạt trạng thái cuối (thành công hoặc thất bại). Để đóng sớm, gọi unmount() trên component được trả về từ mount3DS():

typescript
const tds = bahn.mount3DS(sessionId, callbacks);
// sau đó, ví dụ khi người dùng hủy
tds.unmount();

Modal thử thách 3DS được hiển thị bởi SDK

Modal này được hiển thị bởi iframe 3DS tự lưu trữ và được gắn vào options.container (hoặc vào document body nếu không truyền). Không có script bên ngoài nào được tải.


Sơ đồ luồng 3DS ​

sequenceDiagram
    participant App as Your App
    participant SDK as Bahn SDK
    participant Iframe as 3DS Iframe
    participant ACS as Issuer ACS

    App->>SDK: confirmPayment(payload)
    SDK-->>App: { payment_intent, three_ds_payload }

    Note over App: three_ds_payload present → 3DS required

    App->>SDK: mount3DS(sessionId, callbacks)
    SDK->>Iframe: Load 3DS challenge
    Iframe->>ACS: Challenge request
    ACS-->>Iframe: Challenge HTML
    Iframe->>ACS: User completes challenge
    ACS-->>Iframe: Challenge result

    alt Challenge passed
        Iframe-->>SDK: success
        SDK-->>App: callbacks.onSuccess()
        App->>SDK: pollPaymentIntentStatus(clientSecret)
    else Challenge failed
        Iframe-->>SDK: failure
        SDK-->>App: callbacks.onFailure({ code, message })
    end

Cách dùng thông thường với confirmPayment ​

Ví dụ đầy đủ

Một trang thanh toán hoàn chỉnh có 3DS: Ví dụ tích hợp → Xác nhận thanh toán.

typescript
// client_secret đến từ backend của bạn (tạo payment intent phía server)
const { payment_intent, three_ds_payload } = await bahn.confirmPayment({ client_secret });

if (three_ds_payload) {
  bahn.mount3DS(three_ds_payload.id, {
    onSuccess: () => {
      // 3DS đã pass — poll trạng thái thanh toán cuối cùng
      bahn.pollPaymentIntentStatus(client_secret, {
        onProgress: (attempt, max, status) =>
          console.log(`Polling: ${attempt}/${max} — ${status}`),
        onSuccess: (pi) => redirectToSuccess(pi),
        onFailure: (pi) => redirectToFailure(pi),
        onTimeout: () => showTimeoutError(),
        onError: (err) => console.error("Poll error:", err),
      });
    },
    onFailure: (err) => {
      showError(`3DS failed: ${err?.message || "Unknown error"}`);
    },
  });
} else {
  // Không cần 3DS — payment intent đã sẵn sàng
  console.log("Payment confirmed:", payment_intent);
}

Trang thử thách độc lập (chuyển hướng toàn trang) ​

SDK hỗ trợ hai luồng 3DS:

LuồngPhương thứcPhù hợp cho
Iframe nội tuyếnmount3DS()Ứng dụng web nơi SDK có thể gắn overlay trên cùng một trang
Chuyển hướng toàn tranggetChallengeRedirectUrl()SDK di động, trang checkout bên ngoài, hoặc khi cần chuyển hướng toàn trang

Cách hoạt động ​

  1. Gọi getChallengeRedirectUrl(sessionId, returnUrl, options?) để tạo URL trang thử thách
  2. Chuyển hướng trình duyệt của người dùng đến URL đó
  3. Trang thử thách chạy toàn bộ luồng 3DS (browser fingerprint → challenge → kết quả)
  4. Khi hoàn tất, trang thử thách chuyển hướng về returnUrl kèm theo status và các tham số session

Hợp đồng URL ​

Đầu vào: {challengePageUrl}?session=X&redirect=X[&app=X][&team=X][&api=X][&brand=X]

Tham sốBắt buộcMô tả
sessionCóID phiên 3DS
redirectCóURL trả về của merchant
appKhôngTenant app ID cho header X-Cake-App-Id
teamKhôngTenant team ID cho header X-Cake-Team-Id
apiKhôngURL gốc của backend API (mặc định là same-origin)
brandKhôngTên thương hiệu hiển thị khi quay lại; mặc định "Cake"

Đầu ra: {returnUrl}?status=success|failure|error&session=X[&code=X]

Tham sốMô tả
statussuccess, failure, hoặc error
sessionID phiên 3DS
codeMã lỗi (khi thất bại/lỗi)

Cách sử dụng ​

Ví dụ đầy đủ

Trang thanh toán và trang quay về cho luồng này: Ví dụ tích hợp → 3DS trên trang riêng.

typescript
const { three_ds_payload } = await bahn.confirmPayment({ client_secret });

if (three_ds_payload) {
  // Xây dựng URL chuyển hướng toàn trang và điều hướng
  const url = bahn.getChallengeRedirectUrl(
    three_ds_payload.id,
    "https://merchant.com/callback",
    { brandName: "MyStore" },
  );
  window.location.href = url;
}

Trên trang callback của bạn (https://merchant.com/callback), đọc status từ URL:

typescript
const params = new URLSearchParams(window.location.search);
const status = params.get("status"); // "success" | "failure" | "error"
const sessionId = params.get("session");
const code = params.get("code");

Sơ đồ luồng ​

sequenceDiagram
    participant App as Your App
    participant SDK as Bahn SDK
    participant CP as Challenge Page
    participant ACS as Issuer ACS

    App->>SDK: confirmPayment(payload)
    SDK-->>App: { payment_intent, three_ds_payload }

    Note over App: three_ds_payload present → 3DS required

    App->>SDK: getChallengeRedirectUrl(sessionId, returnUrl, options?)
    SDK-->>App: https://bn.cake.vn/.../challenge.html?session=...&redirect=...

    App->>CP: window.location.href = url
    CP->>ACS: Browser fingerprint → challenge → complete
    ACS-->>CP: Challenge result
    CP-->>App: Redirect to returnUrl?status=success&session=...

    Note over App: Read status from URL, proceed accordingly

Developers