Apple Pay
Chấp nhận thanh toán Apple Pay trên web. bahn.mountApplePay() hiển thị nút Apple Pay chính thức và xử lý việc xác thực merchant (merchant validation), ủy quyền thanh toán, và (nếu được kích hoạt) 3D Secure — bạn chỉ cần mount nó, lắng nghe một vài sự kiện, và xác nhận thanh toán.
Yêu cầu
Trước khi tích hợp, hãy hoàn tất các bước sau với Apple và với Cake:
| Yêu cầu | Ở đâu | Ghi chú |
|---|---|---|
| Xác minh domain | Apple Developer → Merchant IDs → Apple Pay on the Web | Domain production của bạn phải phục vụ /.well-known/apple-developer-merchantid-domain-association tại root, và được đăng ký với Apple merchant identifier của bạn. Điều này bắt buộc ngay cả khi nút được hiển thị bên trong iframe do Cake host — Apple xác thực dựa trên domain của trang cấp cao nhất (top-level page), không phải domain của iframe. |
| Tài khoản merchant với Cake | Onboarding | Domain của bạn cũng phải được đăng ký với Cake gắn với merchant của bạn, khớp với những gì bạn đã đăng ký với Apple. |
| Safari 17 / iOS 17+ | — | Bắt buộc để nút có thể hiển thị (xem Hỗ trợ trình duyệt). |
| Đăng ký cross-browser (tùy chọn) | Apple Developer | Một đăng ký riêng với Apple cho "Apple Pay on the Web for other browsers", nếu bạn cũng muốn hỗ trợ Chrome, Firefox, và Edge. |
Bắt đầu nhanh
const bahn = new Bahn({ pk: "pk_live_...", environment: "sandbox" });
const ap = bahn.mountApplePay("#apple-pay-container", clientSecret, {
amount: 100000, // đơn vị tiền tệ nhỏ nhất — VND không chia cho 100
currency: "VND",
country: "VN",
});
ap.on("ready", () => console.log("Apple Pay button rendered"));
ap.on("success", (paymentIntent) => (window.location.href = "/success"));
ap.on("error", (err) => console.error((err as { message: string }).message));
ap.on("cancel", () => console.log("Shopper dismissed the sheet"));mountApplePay() gắn nút tại CSS selector bạn cung cấp, tự động điều chỉnh kích thước theo container, và tự quản lý mọi thứ khác — kiểm tra xem Apple Pay có thể sử dụng được không, hiển thị nút, và chạy toàn bộ payment sheet. Nếu Apple Pay không khả dụng trên trình duyệt của người mua, nút sẽ không được hiển thị và sự kiện "error" sẽ được kích hoạt thay vào đó — hãy dùng sự kiện này để chuyển sang phương thức thanh toán khác. merchantName, supportedNetworks, và merchantCapabilities đều mặc định lấy theo giá trị đã cấu hình trên tài khoản merchant của bạn, chỉ truyền các giá trị này khi bạn muốn ghi đè.
Tham số của mountApplePay(selector, clientSecret, options)
| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
selector | string | Có | CSS selector cho container để mount |
clientSecret | string | undefined | Có (truyền undefined cho luồng dựa trên token) | Client secret của payment intent cần xác nhận — xem Xác nhận thanh toán |
options.amount | number | Có | Số tiền giao dịch, theo đơn vị tiền tệ nhỏ nhất |
options.currency | string | Có | Mã tiền tệ ISO 4217 (ví dụ "VND", "USD") |
options.country | string | Có | Mã quốc gia ISO 3166-1 alpha-2 (ví dụ "VN", "US") |
options.priceLabel | string | Không | Nhãn hiển thị cho tổng giao dịch trong sheet |
options.lineItems | Array<{ label, amount }> | Không | Bảng chi tiết từng khoản hiển thị trong sheet |
options.merchantName | string | Không | Tên merchant hiển thị trong sheet — mặc định lấy theo tên đã cấu hình của merchant |
options.type | ApplePayButtonType | Không | Biến thể nhãn của nút (mặc định "plain") |
options.style | ApplePayButtonStyle | Không | "black" | "white" | "white-outline" (mặc định "black") |
options.locale | string | Không | Ngôn ngữ của nút (mặc định "en") |
options.borderRadius | number | Không | Bo góc nút, tính bằng pixel (mặc định 4) |
options.size | { width?, height? } | Không | Kích thước nút (mặc định 100% × 48px) |
options.supportedNetworks | ApplePayCardNetwork[] | Không | Các mạng thẻ được hỗ trợ — mặc định theo cấu hình của merchant |
options.merchantCapabilities | ApplePayMerchantCapability[] | Không | Các merchant capability được khai báo — mặc định theo cấu hình của merchant |
options.requestBillingAddress | boolean | Không | Yêu cầu địa chỉ thanh toán (billing address) |
options.requestPayerDetails | ("name" | "email" | "phone")[] | Không | Yêu cầu thông tin liên hệ của người mua |
options.requestShipping | boolean | Không | Thu thập địa chỉ giao hàng — xem Thu thập địa chỉ giao hàng |
options.shippingMethods | Array<{ identifier, label, detail?, amount }> | Không | Các tùy chọn giao hàng ban đầu hiển thị trước khi địa chỉ được chọn |
Giá trị trả về: IApplePayButtonComponent — .on(event, handler) (xem Sự kiện), .unmount(). bahn.cleanup()/bahn.unmountApplePay() cũng sẽ gỡ nút này.
Xác nhận thanh toán
mountApplePay() hỗ trợ hai cách xác nhận thanh toán, được chọn tùy theo việc bạn có truyền clientSecret hay không.
Xác nhận trực tiếp
Truyền client_secret của một payment intent bạn đã tạo phía server. Sheet sẽ tự xác nhận thanh toán và báo cáo kết quả:
const ap = bahn.mountApplePay("#apple-pay-container", clientSecret, {
amount: 100000,
currency: "VND",
country: "VN",
});
ap.on("success", (paymentIntent) => (window.location.href = "/success"));
ap.on("error", (err) => console.error((err as { message: string }).message));Nếu payment intent yêu cầu xác thực bổ sung, mountApplePay() sẽ tự động gọi bahn.mount3DS() để hiển thị thử thách, sau đó báo cáo "success"/"error" khi hoàn tất — trường hợp này hiếm khi xảy ra trong thực tế, vì việc ủy quyền bằng Face ID / Touch ID đã đáp ứng phần lớn yêu cầu của các ngân hàng phát hành thẻ.
Luồng dựa trên token
Bỏ qua clientSecret để xác nhận thanh toán thông qua backend của riêng bạn — gọi mountApplePay(selector, undefined, options):
const ap = bahn.mountApplePay("#apple-pay-container", undefined, {
amount: 100000,
currency: "VND",
country: "VN",
merchantName: "Your Merchant Name",
});
ap.on("token", async ({ confirmation_token }) => {
// Gửi confirmation_token đến backend của riêng bạn, nơi xác nhận
// PaymentIntent bằng confirmation_token và secret key của bạn, rồi
// trả về client_secret.
const { client_secret } = await fetch("/api/confirm-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirmation_token, amount: 100000, currency: "vnd" }),
}).then((r) => r.json());
// Giữ nút Apple Pay ở trạng thái mount cho đến khi việc này hoàn tất — handleNextAction()
// cần nó để kiểm tra trạng thái của payment intent.
await bahn.handleNextAction(client_secret, {
onSuccess: () => (window.location.href = "/success"),
onFailure: () => (window.location.href = "/failed"),
onTimeout: () => (window.location.href = "/processing"),
});
});
ap.on("error", (err) => console.error((err as { message: string }).message));
ap.on("cancel", () => console.log("Shopper dismissed the sheet"));Trong luồng này, Apple Pay sheet hoàn tất ngay khi confirmation_token được tạo — người mua sẽ thấy sheet đóng lại thành công ngay lập tức, mà không cần chờ backend của bạn. Hãy xác nhận thanh toán ở backend của bạn và hiển thị kết quả cuối cùng thông qua redirect hoặc giao diện riêng của bạn, không thông qua nút Apple Pay; sự kiện "success" sẽ không được phát ra cho luồng này.
Thu thập địa chỉ giao hàng
Đặt requestShipping: true và cung cấp các tùy chọn giao hàng khi người mua nhập địa chỉ:
const ap = bahn.mountApplePay("#apple-pay-container", clientSecret, {
amount: 100000,
currency: "VND",
country: "VN",
requestShipping: true,
// Hiển thị trước khi người mua nhập địa chỉ. Bỏ qua nếu mức phí ship
// luôn phụ thuộc vào địa chỉ — cung cấp chúng từ "shippingcontactselected" thay vào đó.
shippingMethods: [{ identifier: "standard", label: "Standard", detail: "3-5 days", amount: 0 }],
});
ap.on("shippingcontactselected", (event) => {
const { contact, resolve, reject } = event;
if (contact.country !== "VN") {
reject(); // hiển thị lỗi inline "không thể giao đến đây" trong sheet
return;
}
resolve({
shippingMethods: [
{ identifier: "standard", label: "Standard", detail: "3-5 days", amount: 0 },
{ identifier: "express", label: "Express", detail: "1-2 days", amount: 20000 },
],
});
});
ap.on("shippingmethodselected", (event) => {
const { shippingMethod, resolve } = event;
// Tính lại tổng số tiền để bao gồm phí ship đã chọn.
resolve({ amount: 100000 + shippingMethod.amount });
});
ap.on("token", async ({ confirmation_token }) => { /* same as the token-based flow above */ });Sheet sẽ tạm dừng sau mỗi lựa chọn cho đến khi bạn gọi resolve() (kèm các tùy chọn giao hàng/tổng số tiền đã cập nhật) hoặc reject() (hiển thị lỗi inline). contact được truyền vào "shippingcontactselected" cố tình chỉ mang thông tin sơ bộ — chỉ gồm thành phố, tỉnh/bang, mã bưu điện, và quốc gia, không có địa chỉ đường phố cụ thể — đây là hành vi tiêu chuẩn cho loại thu thập địa chỉ này; địa chỉ đầy đủ sẽ có sẵn khi thanh toán được ủy quyền. Mọi trường amount ở đây đều tuân theo cùng quy ước đơn vị tiền tệ nhỏ nhất như options.amount.
Kiểm tra khả dụng trước khi mount
mountApplePay() đã tự kiểm tra khả dụng nội bộ và đơn giản là sẽ không hiển thị nút nếu Apple Pay không thể sử dụng. Chỉ gọi bahn.isApplePayAvailable() khi bạn muốn kiểm soát bố cục trang trước bước đó — ví dụ, ẩn toàn bộ phần "Hoặc thanh toán bằng Apple Pay" (nút và mọi giao diện xung quanh), thay vì phản ứng lại sự kiện "error" sau khi container đã hiển thị.
if (!(await bahn.isApplePayAvailable())) {
// ẩn điểm truy cập — xem phần Hỗ trợ trình duyệt bên dưới để biết phạm vi áp dụng
}Đây chỉ là một kiểm tra phía client — nó không xác nhận rằng Apple Pay đã được bật cho tài khoản merchant của bạn. Đây không phải là điều cần kiểm tra riêng trước: nếu merchant của bạn chưa được thiết lập đầy đủ, việc xác thực của sheet sẽ thất bại khi người mua nhấn nút, và được hiển thị qua cùng sự kiện "error" như mọi lỗi khác.
Sự kiện
.on(event, handler) trên đối tượng mà mountApplePay() trả về:
| Sự kiện | Kích hoạt khi | Handler nhận được |
|---|---|---|
"ready" | Nút đã được hiển thị và có thể nhấn | — |
"success" | Thanh toán đã được xác nhận (và mọi thử thách 3D Secure đã pass) | payment_intent — chỉ áp dụng cho xác nhận trực tiếp |
"token" | Một confirmation token đã được tạo; sheet đã đóng thành công | { confirmation_token } — chỉ áp dụng cho luồng dựa trên token |
"error" | Xác thực merchant hoặc xác nhận thất bại | { message } |
"cancel" | Người mua đóng sheet mà không thanh toán | — |
"shippingcontactselected" | Người mua nhập hoặc thay đổi địa chỉ giao hàng | { contact, resolve, reject } — yêu cầu requestShipping: true |
"shippingmethodselected" | Người mua chọn tùy chọn giao hàng khác | { shippingMethod, resolve, reject } — yêu cầu requestShipping: true |
Hỗ trợ trình duyệt
| Trình duyệt | Hỗ trợ |
|---|---|
| Safari (macOS / iOS) | Safari 17 / iOS 17+ |
| Chrome / Edge / Firefox | Chỉ được hỗ trợ nếu merchant của bạn đã hoàn tất đăng ký riêng với Apple cho "Apple Pay on the Web for other browsers" |
bahn.isApplePayAvailable() phản ánh việc trình duyệt của người mua có khả năng hiển thị sheet hay không. Trên Chrome, Firefox, và Edge, việc kiểm tra khả năng này chỉ xác nhận phiên bản trình duyệt — nó không thể xác nhận trạng thái đăng ký của bạn với Apple, vì vậy kết quả true ở đây chỉ có nghĩa là trình duyệt có thể chạy luồng thanh toán, không đảm bảo nó sẽ thành công cho merchant của bạn nếu chưa hoàn tất đăng ký đó. Hãy xác nhận trạng thái đăng ký của bạn với Apple trực tiếp trước khi phụ thuộc vào việc hỗ trợ ngoài Safari trong môi trường production.
Kiểm thử
Sử dụng trình duyệt Safari 17+ (macOS) hoặc thiết bị iOS 17+ đã đăng nhập vào một Apple ID sandbox, với một thẻ test Apple Pay Sandbox đã được thêm vào Wallet, trỏ đến môi trường sandbox của bạn.