Skip to content

Thanh toán thẻ ​

Thu thập thông tin thẻ tuân thủ PCI thông qua các iframe cô lập. Giá trị thẻ không bao giờ rời khỏi iframe — chỉ trạng thái hợp lệ (boolean) được báo cáo về ứng dụng của bạn.

bahn.mountCard(containerId, options?) ​

Gắn component nhập liệu thẻ bên trong một phần tử container.

typescript
const card = bahn.mountCard("#card-container", {
  fields: {
    number: {
      label: "Card Number",
      placeholder: "1234 5678 9012 3456",
      errorMsg: { incomplete_number: "Please enter a complete card number" },
    },
    expiry: {
      label: "Expiry",
      placeholder: "MM / YY",
    },
    cvc: {
      label: "CVC",
      placeholder: "123",
      errorMsg: "Invalid CVC",
    },
  },
  layout: {
    gap: "12px",
    cardNumberHeight: "85px",
    fieldHeight: "85px",
  },
  inputStyle: {
    fontFamily: "Inter, sans-serif",
    fontSize: "16px",
    color: "#1a1a2e",
    backgroundColor: "#f8f9fa",
    borderRadius: "8px",
  },
});

Tham số:

Tham sốKiểuBắt buộcMặc địnhMô tả
containerIdstringCó—CSS selector cho container để mount
options.fieldsobjectKhông—Cấu hình label, placeholder, và thông báo lỗi cho từng field
options.layoutCardLayoutConfigKhông—Cấu hình bố cục grid
options.inputStyleCardInputStyleKhông—Style áp dụng cho tất cả các phần tử input trong iframe
options.stateStylesCardStateStylesKhông—Style theo trạng thái (base/focus/invalid). Xem Tạo kiểu theo trạng thái.
options.cvvPeekbooleanKhôngfalseHiển thị từng chữ số CVV trong ~500ms rồi tự động ẩn thành •. Độ dài tuân theo scheme thẻ được phát hiện (3 chữ số, 4 với Amex) kèm hiệu ứng rung cảnh báo khi nhập sai.
options.allowedSchemesArray<CardScheme>Không—Giới hạn việc nhập số thẻ theo các scheme cụ thể (ví dụ ["visa", "mastercard"]). Các scheme không được liệt kê sẽ bị coi là không hợp lệ.

Giá trị trả về: CardComponent

Component nhập thẻ với ba iframe — trường số thẻ, ngày hết hạn, và CVC


CardComponent ​

Đối tượng được trả về bởi mountCard().

typescript
interface CardComponent {
  mount(selector: string): void;
  unmount(): void;
  on(event: string, handler: (data?: any) => void): void;
  validate(): void;
  readonly values: {
    isComplete: boolean;
    isValid: boolean;
    errors?: Record<string, string>;
  };
}

Sự kiện ​

Sự kiệnKhi nàoDữ liệu
readyCả ba iframe đã tải xongkhông có
changeTrạng thái hợp lệ của bất kỳ field nào thay đổiđối tượng values
completeTất cả các field đều hợp lệđối tượng values (isComplete: true)

card.values ​

typescript
card.on("change", (data) => {
  const values = data as { isComplete: boolean; isValid: boolean; errors?: Record<string, string> };
  if (values.isComplete) {
    console.log("All fields valid — ready to submit");
  }
  if (values.errors) {
    console.log("Validation errors:", values.errors);
    // ví dụ { number: "Invalid card number", cvc: "Invalid CVC" }
  }
});

Handler của change nhận data?: unknown (giá trị thẻ không bao giờ vượt qua ranh giới iframe), vì vậy hãy ép kiểu (cast) về dạng validity như trên.

Đối tượng values chỉ báo cáo tính hợp lệ. Số thẻ không bao giờ được lộ ra cho ứng dụng của bạn.

card.validate() ​

Kích hoạt việc kiểm tra hợp lệ trên tất cả các iframe và phát ra sự kiện change với values mới nhất. Sử dụng phương thức này để buộc hiển thị lỗi trước khi submit form.

Ô nhập thẻ hiển thị lỗi kiểm tra hợp lệ trên từng trường


card.unmount() ​

Xóa ô nhập thẻ khỏi DOM và dọn dẹp các event listener. Được gọi trên CardComponent trả về từ mountCard(). An toàn khi gọi nhiều lần.

typescript
const card = bahn.mountCard("card-container");
// sau đó
card.unmount();

card.applyConfig(fields, inputStyle?) ​

Áp dụng lại cấu hình field cho các iframe đã được mount mà không cần mount lại. Hữu ích cho các thay đổi động như chuyển đổi ngôn ngữ. Được gọi trên CardComponent trả về từ mountCard().

typescript
const card = bahn.mountCard("card-container");
card.applyConfig(
  {
    number: { label: "Số thẻ" },     // nhãn tiếng Việt
    expiry: { label: "Hết hạn" },
    cvc: { label: "Mã CVC" },
  },
  { fontSize: "14px" }
);

CardLayoutConfig ​

Kiểm soát bố cục grid của ba iframe.

typescript
interface CardLayoutConfig {
  columns?: 1 | 2;           // Số cột (mặc định: 2). 1 = mỗi field trên một hàng riêng.
  gap?: string;              // Khoảng cách giữa các field (mặc định: "8px")
  columnGap?: string;        // Khoảng cách giữa cột expiry và CVC (ghi đè gap)
  rowGap?: string;           // Khoảng cách giữa hàng số thẻ và hàng expiry/CVC (ghi đè gap)
  cardNumberHeight?: string; // Chiều cao của iframe số thẻ (mặc định bằng fieldHeight)
  fieldHeight?: string;      // Chiều cao của các iframe expiry/CVC (mặc định: "92px")
}

CardInputStyle ​

Style áp dụng cho các phần tử <input> bên trong iframe. Mỗi thuộc tính ánh xạ 1:1 với allowlist nội bộ của iframe — bất kỳ thuộc tính CSS nào không có trong danh sách này sẽ bị âm thầm bỏ qua (một biện pháp bảo vệ chống CSS injection).

typescript
interface CardInputStyle {
  color?: string;
  backgroundColor?: string;
  fontSize?: string;
  fontFamily?: string;
  fontWeight?: string;
  fontStyle?: string;
  lineHeight?: string;
  letterSpacing?: string;
  textAlign?: string;
  borderRadius?: string;
  border?: string;
  borderColor?: string;
  borderWidth?: string;
  padding?: string;
  paddingTop?: string;
  paddingBottom?: string;
  paddingLeft?: string;
  paddingRight?: string;
  height?: string;
  opacity?: string;
  boxShadow?: string;
  caretColor?: string;
}

Label, placeholder, text lỗi, và container/border của input được tạo kiểu nội bộ và không thể tùy chỉnh thông qua interface này.


Tạo kiểu theo trạng thái ​

inputStyle áp dụng cùng một giao diện cho mọi trạng thái. Để tạo kiểu khác nhau cho trạng thái focus và invalid, truyền stateStyles — ba đối tượng CardInputStyle được áp dụng cho từng trạng thái tương tác:

  • base — được áp dụng lại khi field mất focus
  • focus — được áp dụng khi field đang có focus
  • invalid — được áp dụng khi blur nếu field không vượt qua kiểm tra hợp lệ
typescript
bahn.mountCard("#card-container", {
  fields: { number: { label: "Card Number" } },
  inputStyle: { fontSize: "16px", borderRadius: "8px" },
  stateStyles: {
    focus: { borderColor: "#3b82f6", boxShadow: "0 0 0 3px rgba(59,130,246,0.2)" },
    invalid: { color: "#ef4444", borderColor: "#ef4444" },
  },
});

card.applyStyles(stateStyles) ​

Áp dụng lại stateStyles cho các iframe đã được mount mà không cần mount lại (ví dụ khi chuyển sang dark theme). Được gọi trên CardComponent trả về từ mountCard().

typescript
card.applyStyles({
  base: { color: "#e5e7eb", backgroundColor: "#1f2937" },
  focus: { borderColor: "#60a5fa" },
});

CardFieldConfig ​

Cấu hình cho từng field.

typescript
interface CardFieldConfig {
  label?: string;
  placeholder?: string;
  errorMsg?: Record<string, string> | string;
}

Đối với card-number và expiry, errorMsg là một Record<string, string> dùng mã lỗi làm khóa:

  • Số thẻ: incomplete_number, invalid_number
  • Ngày hết hạn: incomplete_expiry, invalid_expiry_month, invalid_expiry_year

Đối với cvc, errorMsg là một chuỗi thuần (chỉ có một trạng thái lỗi CVC duy nhất). Độ dài CVC bắt buộc được xác định tự động từ scheme thẻ được phát hiện — 4 chữ số cho American Express, 3 chữ số cho tất cả các scheme khác (Visa, Mastercard, JCB, Discover, Diners, UnionPay).


Tên chủ thẻ ​

Các trường thẻ gồm số thẻ, ngày hết hạn và CVC — không có trường tên. Hãy thu thập tên in trên thẻ bằng một <input> trên trang của bạn, đặt sau các trường thẻ và tạo kiểu giống chúng (autocomplete="cc-name" giúp trình duyệt tự điền), rồi gửi nó dưới dạng payment_method.billing_details.name khi xác nhận. Tên không phải dữ liệu thẻ, nên không cần đi qua iframe.

typescript
const name = document.querySelector<HTMLInputElement>("#cardholder-name")!.value.trim();

await bahn.confirmPayment({
  client_secret,
  payment_method: { billing_details: { name } },
});

createConfirmationToken() và confirmCardSetup() nhận cùng payment_method.billing_details. Xem trang đầy đủ, kèm CSS giúp ô nhập tên khớp với các trường thẻ, tại Ví dụ tích hợp → Xác nhận thanh toán.


Ví dụ đầy đủ ​

typescript
const bahn = new Bahn({ pk: "pk_live_...", environment: "sandbox" });

const card = bahn.mountCard("#card-input", {
  fields: {
    number: { label: "Card Number" },
    expiry: { label: "MM / YY" },
    cvc: { label: "CVC" },
  },
  layout: { gap: "10px" },
  inputStyle: {
    fontFamily: "system-ui",
    fontSize: "16px",
    borderRadius: "6px",
  },
});

card.on("complete", async () => {
  document.getElementById("pay-button")!.removeAttribute("disabled");
});

document.getElementById("pay-button")!.addEventListener("click", async () => {
  card.validate();
  if (!card.values.isValid) return;

  // client_secret đến từ backend của bạn (tạo payment intent phía server)
  try {
    const result = await bahn.confirmPayment({ client_secret });
    console.log("Payment intent:", result.payment_intent);
  } catch (err) {
    console.error("Payment failed:", err);
  }
});

Developers