Embedded Checkout
Thêm các trường nhập thẻ bảo mật cùng Apple Pay hoặc Google Pay vào trang thanh toán của riêng bạn.
Đặt các trường nhập thẻ bảo mật ngay trong trang thanh toán của riêng bạn. Bạn quản lý biểu mẫu và nút gửi; Tapaya xử lý việc kiểm tra thẻ, tokenization và 3-D Secure. Thanh toán nhúng đã hoạt động chính thức cho các merchant đã được phê duyệt, và có sẵn sandbox để kiểm thử. Hướng dẫn này dùng Tapaya.js 0.2.0 và Node.js SDK 0.3.0.
- 1. Your frontend to Your backend: Prepare payment.
- 2. Your backend to Tapaya: Create or resume payment.
- 3. Tapaya to Your backend: Payment and client secret.
- 4. Your backend to Your frontend: Client secret and status.
- 5. Your frontend to Tapaya: Confirm card and 3-D Secure.
- 6. Tapaya to Your frontend: Payment outcome.
- 7. Your backend to Tapaya: Read order's payment.
- 8. Tapaya to Your backend: Payment status.
- 9. Your backend to Your frontend: Show verified result.
Phương thức thanh toán
Embedded Checkout hỗ trợ thẻ, Apple Pay và Google Pay. Dùng nút thanh toán của riêng bạn cho thẻ và các nút ví chính thức cho Apple Pay và Google Pay. Xem Thêm Apple Pay và Google Pay.
Trước khi bắt đầu
Cài đặt SDK
Một gói duy nhất chứa client cho máy chủ, SDK cho trình duyệt và React hook:
npm install @tapayadot/checkout| Import | Dùng ở đâu |
|---|---|
@tapayadot/checkout | Chỉ trên máy chủ của bạn. Import gói này trong bản build cho trình duyệt sẽ gây lỗi, nên API key của bạn không bao giờ đến được trang web. |
@tapayadot/checkout/client | Trang thanh toán của bạn. |
@tapayadot/checkout/react | Trang thanh toán của bạn, nếu bạn dùng React. |
Cấu hình máy chủ
Thanh toán nhúng không cần URL trả về. Các ví dụ máy chủ bên dưới có các phiên bản HTTP (fetch), Node.js SDK và cURL, tất cả đều gọi cùng một merchant API và không thay đổi các trường nhập thẻ trong trình duyệt.
| Node.js SDK | HTTP (fetch) và cURL | |
|---|---|---|
| Key | TAPAYA_SECRET_KEY, được new Tapaya() đọc | TAPAYA_CHECKOUT_API_KEY |
| Môi trường | TAPAYA_ENVIRONMENT=sandbox hoặc production. Nếu không có biến này, key tapaya_live_ sẽ chọn production, còn các key khác mặc định dùng sandbox. | TAPAYA_CHECKOUT_API_URL: https://api.sandbox.tapaya.com hoặc https://api.tapaya.com |
| Chế độ nền tảng | Truyền merchantToken của đơn hàng vào mọi lệnh gọi, hoặc đặt nó bằng new Tapaya(key, { merchantToken }) | Gửi X-Tapaya-Merchant-Token trong mọi yêu cầu. Hàm hỗ trợ fetch đọc giá trị này từ TAPAYA_MERCHANT_TOKEN. |
Checkout key của merchant không cần thêm gì khác. Ở chế độ nền tảng, hãy dùng Secret key của tổ chức, và để backend phục vụ nhiều merchant chọn token từ đơn hàng gắn với phiên đăng nhập. Xem Xác thực Checkout. Client secret là riêng cho từng khoản thanh toán.
Hoàn tất onboarding với thanh toán trực tuyến và kiểm tra rằng merchant ở trạng thái Ready trong Cài đặt Checkout.
Cho phép Tapaya trong CSP
Trình tải lấy Tapaya.js 0.2.0 từ https://js.tapaya.com/checkout/0.2.0/checkout.js; phiên bản script trình duyệt
độc lập với phiên bản Node.js SDK. Thêm origin tài nguyên của Tapaya vào Content Security
Policy hiện có của bạn:
script-src https://js.tapaya.com
frame-src https://js.tapaya.comTrình duyệt nhận publishable key và client secret riêng của khoản thanh toán. Merchant API key bí mật của bạn
vẫn nằm trên máy chủ, còn dữ liệu thẻ và các script của nhà cung cấp dịch vụ thanh toán nằm bên trong frame của Tapaya. Để kiểm thử cục bộ,
hãy tự phục vụ Tapaya.js và truyền origin của nó cho trình tải, ví dụ
loadTapaya(publishableKey, { apiOrigin, origin: 'http://localhost:4191' }).
Cách hoạt động
- Trang của bạn gắn các trường nhập thẻ của Tapaya và trỏ chúng đến một endpoint trên máy chủ của bạn.
- Khi trang tải và khi khách hàng thanh toán, các trường nhập thẻ gọi endpoint đó. Endpoint trả về khoản thanh toán
của đơn hàng dưới dạng
clientSecret: một thông tin xác thực dùng cho một khoản thanh toán, mà các trường nhập thẻ dùng để xác nhận thanh toán trực tiếp với Tapaya. Mã trang của bạn không bao giờ đọc giá trị này. - Trang của bạn theo dõi
outcomecủa khoản thanh toán. Khi thanh toán hoàn tất, máy chủ của bạn xác minh khoản thanh toán trước khi bạn hoàn tất đơn hàng.
Nhận một khoản thanh toán
Lấy cài đặt cho trình duyệt
Lấy cài đặt cho trình duyệt một lần trên máy chủ và thêm chúng vào cấu hình frontend của bạn.
Giữ hàm hỗ trợ này trên máy chủ của bạn. Hàm kiểm tra lỗi HTTP, coi khoản thanh toán không tồn tại của đơn hàng là null khi được yêu cầu,
và không tự động thử lại.
const apiOrigin = new URL(process.env.TAPAYA_CHECKOUT_API_URL).origin;
const apiKey = process.env.TAPAYA_CHECKOUT_API_KEY;
const merchantToken = process.env.TAPAYA_MERCHANT_TOKEN;
export async function paymentRequest(path, { method = 'GET', body, key, allowMissing = false } = {}) {
const response = await fetch(new URL(path, apiOrigin), {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
...(merchantToken ? { 'X-Tapaya-Merchant-Token': merchantToken } : {}),
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
...(key ? { 'Idempotency-Key': key } : {}),
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
redirect: 'error',
cache: 'no-store',
signal: AbortSignal.timeout(35000),
});
if (allowMissing && response.status === 404) return null;
if (!response.ok) throw new Error(`Payment request failed (${response.status})`);
return response.json();
}
const { publishableKey } = await paymentRequest('/merchant/payments/configuration');
export const config = { publishableKey, apiOrigin };API trả về publishableKey và environment. Đặt apiOrigin từ URL API mà bạn đã gọi; đây không phải là một trường
trong phản hồi cấu hình.
Cả hai giá trị đều an toàn khi đưa vào trang của bạn. Publishable key dùng pk_test_ với key của cổng thanh toán thử nghiệm và pk_live_
với key của cổng thanh toán chính thức. Luôn truyền apiOrigin do máy chủ của bạn trả về cho trình tải, để trình duyệt và máy chủ
luôn ở cùng một môi trường.
Thêm các trường nhập thẻ
Thêm một container vào biểu mẫu thanh toán hiện có của bạn:
<form id="checkout">
<!-- Your other checkout inputs go here. -->
<div id="card-fields"></div>
<button type="submit" disabled>Pay</button>
<p id="message" role="status"></p>
</form>Tải Tapaya và gắn các trường. prepare là endpoint bạn thêm ở bước tiếp theo.
import { loadTapaya } from '@tapayadot/checkout/client'
import { config } from './config' // { publishableKey, apiOrigin } from your server
const form = document.querySelector<HTMLFormElement>('#checkout')!
const button = form.querySelector<HTMLButtonElement>('button')!
const message = document.querySelector<HTMLElement>('#message')!
const tapaya = await loadTapaya(config.publishableKey, { apiOrigin: config.apiOrigin })
const payment = tapaya.createPayment({
prepare: '/api/payments',
onChange: ({ canSubmit, outcome }) => {
button.disabled = !canSubmit
if (outcome === 'completed') location.assign('/checkout/confirmation')
if (outcome === 'ended') message.textContent = 'This payment cannot be completed. Start a new checkout.'
},
})
await payment.mount('#card-fields')Khi các trường được gắn, chúng gọi prepare để tiếp tục khoản thanh toán hiện có của đơn hàng. Việc tải lại trang trong khi thanh toán không bao giờ
trừ tiền lần nữa: thay vào đó, trang hiển thị trạng thái hiện tại của khoản thanh toán.
Trả về khoản thanh toán của đơn hàng
Kết nối trình xử lý này với POST /api/payments. getCheckoutOrder() là hàm của chính ứng dụng bạn: xác thực
khách hàng, kiểm tra quyền truy cập vào đơn hàng của họ, và trả về id, amount, currency và status đã lưu của đơn hàng. Áp dụng
cơ chế bảo vệ CSRF thông thường của bạn cho route này.
Đọc khoản thanh toán hiện có trước khi tạo một lần thử. Dùng lại khoản thanh toán chưa có kết quả, và chỉ tạo lần thử khác
sau khi bị từ chối hoặc bị hủy với retryAllowed: true.
import { paymentRequest } from './tapaya.js';
import { getCheckoutOrder } from './orders.js';
export async function findPayment(order) {
const query = new URLSearchParams({ merchantOrderId: order.id });
const payment = await paymentRequest(`/merchant/payments?${query}`, { allowMissing: true });
if (payment && (payment.merchantOrderId !== order.id ||
payment.amount !== order.amount || payment.currency !== order.currency)) {
throw new Error('Payment does not match the order');
}
return payment;
}
export function browserStatus(payment) {
if (payment.status === 'successful') return 'completed';
if (['declined', 'cancelled'].includes(payment.status) && !payment.retryAllowed) return 'ended';
return 'open';
}
export async function POST(request) {
const order = await getCheckoutOrder(request);
const payable = order.status === 'pending_payment';
let payment = await findPayment(order);
if (payable && (!payment ||
(['declined', 'cancelled'].includes(payment.status) && payment.retryAllowed))) {
// Derive the same key on retries, including after a lost creation response.
let key = `${order.id}:${payment ? `after:${payment.id}` : 'initial'}`;
if (/[^\x21-\x7e]/.test(key)) {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));
key = `prepare:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')}`;
}
payment = await paymentRequest('/merchant/payments', {
method: 'POST', key,
body: { merchantOrderId: order.id, amount: order.amount, currency: order.currency },
});
if (payment.merchantOrderId !== order.id || payment.amount !== order.amount || payment.currency !== order.currency) {
throw new Error('Payment does not match the order');
}
}
if (!payment) return Response.json({ error: 'Order cannot accept payment' }, { status: 409 });
return Response.json(
{ clientSecret: payable ? payment.clientSecret : null, status: browserStatus(payment) },
{ headers: { 'Cache-Control': 'no-store' } },
);
}Dùng mã tiền tệ viết hoa và ID đơn hàng đã lưu dài tối đa 200 ký tự. Giữ nguyên thông tin đơn hàng sau khi thanh toán bắt đầu. Khi hết thời gian chờ, hãy đọc lại khoản thanh toán của đơn hàng trước khi quyết định có cần lần thử mới hay không. Không bao giờ tạo key mới chỉ vì một phản hồi bị mất.
REST API trả về các trạng thái thanh toán. Node SDK ánh xạ chúng sang status mà các trường nhập thẻ cần; các tích hợp fetch và cURL
áp dụng cùng cách ánh xạ trong endpoint prepare của riêng mình:
status của API | status của endpoint prepare |
|---|---|
successful | completed |
declined hoặc cancelled, với retryAllowed: false | ended |
ready, pending, action_required, hoặc một lần bị từ chối hay bị hủy có thể thử lại | open |
Gửi thanh toán từ trình xử lý của bạn
Thêm đoạn này vào cùng script. Trình xử lý của bạn kiểm tra biểu mẫu và gọi submit():
form.addEventListener('submit', async (event) => {
event.preventDefault()
if (!form.reportValidity() || !payment.snapshot.canSubmit) return
await payment.submit().catch(() => {
// The card fields show the reason. The shopper can try again when canSubmit allows it.
})
})submit() gọi prepare cho khoản thanh toán, sau đó kiểm tra thẻ, chạy 3-D Secure và xác nhận thanh toán
với Tapaya.
Hiển thị kết quả
onChange nhận snapshot của khoản thanh toán mỗi khi nó thay đổi, kể cả khi trang tải với một khoản thanh toán
đã có kết quả cuối cùng. Hãy làm theo snapshot.outcome:
outcome | Hiển thị gì |
|---|---|
open | Nút thanh toán, được bật khi canSubmit là true. Khi bị từ chối, trạng thái vẫn là open, và lần gọi submit() tiếp theo sẽ có một lần thử mới. |
pending | Chỉ báo đang xử lý. Kết quả chưa được biết, nên không thể thanh toán lại. SDK tự động kiểm tra trạng thái khi trang đang hiển thị. |
completed | Trang xác nhận của bạn. Máy chủ của bạn xác minh đơn hàng tại đó. |
ended | Một lượt checkout mới. Không thể thực hiện thêm lần thử nào cho đơn hàng này. |
Gọi payment.destroy() khi giao diện checkout của bạn bị gỡ (unmount). Với React, hãy dùng usePayment() từ
@tapayadot/checkout/react. Hook này nhận cùng các tùy chọn, cộng thêm publishableKey và apiOrigin, và trả về một
fieldRef cho container, snapshot hiện tại và submit().
Xác minh trước khi hoàn tất đơn hàng
Kết quả trên trình duyệt chỉ để hiển thị. Trên trang xác nhận, hãy đọc khoản thanh toán của đơn hàng trên máy chủ, và chỉ hoàn tất đơn hàng khi khoản thanh toán đã hoàn tất:
import { findPayment } from './payments.js';
const order = await getCheckoutOrder(request);
const payment = await findPayment(order); // Checks order ID, amount, and currency.
if (payment?.status === 'successful') await markOrderPaid(order.id, payment.id);Hãy để markOrderPaid() ghi nhận khoản thanh toán đúng một lần, kể cả khi được gọi nhiều lần, và kích hoạt việc hoàn tất đơn hàng từ
trạng thái đã lưu đó.
Đối soát các đơn hàng có khách hàng rời đi
Khách hàng có thể đóng tab trước khi trang xác nhận mở ra. Hãy chạy một tác vụ định kỳ gọi verify() hoặc
đọc GET /merchant/payments?merchantOrderId=... cho các đơn hàng chưa thanh toán, với cùng các bước kiểm tra. Các lần thử bị từ chối và
bị hủy đều cho phép một lần thử mới, nên đơn hàng chưa thanh toán vẫn ở trạng thái open: hãy giải phóng đơn hàng theo lịch của riêng bạn, và
chỉ khi paymentStatus của nó không phải là pending. Một yêu cầu thất bại hoặc hết thời gian chờ không phải là bị từ chối, vì vậy hãy tiếp tục giữ đơn hàng
cho đến khi khoản thanh toán có kết quả xác định.
Thêm Apple Pay và Google Pay
Ví xuất hiện bên trong frame của Tapaya, cạnh các trường nhập thẻ. Để ví hiển thị, hãy kiểm tra rằng:
- Ví đã được bật trong Cài đặt Checkout.
- Apple Pay: cửa hàng có một tên miền cửa hàng đang hoạt động.
- Google Pay trên production: đã cấu hình Google Pay merchant ID.
- Thiết bị, trình duyệt và ví của khách hàng hỗ trợ phương thức này.
Truyền wallets với tổng tiền thanh toán tính bằng đơn vị nhỏ nhất của tiền tệ. Tapaya hiển thị các phương thức khả dụng, còn các ví không được hỗ trợ hoặc
bị tắt sẽ được ẩn. Dùng đoạn này thay cho ví dụ createPayment() chỉ dành cho thẻ:
const payment = tapaya.createPayment({
prepare: '/api/payments',
wallets: { amount: order.totalInMinorUnits, currency: order.currency },
onWalletClick: async () => {
if (!form.reportValidity()) throw new Error('Complete the checkout details.')
// Validate the cart and reserve the order on your server.
const response = await fetch('/api/payments', { method: 'POST' })
if (!response.ok) throw new Error('Could not prepare the payment.')
return response.json() // { clientSecret, status }
},
onChange: (snapshot) => {
button.hidden = snapshot.paymentMethod !== 'card'
button.disabled = !snapshot.canSubmit
if (snapshot.outcome === 'completed') location.assign('/checkout/confirmation')
},
})
await payment.mount('#card-fields')Tổng tiền hiển thị trên trình duyệt phải đến từ máy chủ của bạn, nơi vẫn tính toán và kiểm tra số tiền khi chuẩn bị khoản thanh toán.
Khi nhấn nút ví
Nút ví chính thức mở cửa sổ thanh toán gốc của ví từ thao tác nhấn của khách hàng. onWalletClick kiểm tra
checkout của bạn và giữ chỗ khoản thanh toán khi nút đó được nhấn, không bao giờ khi gắn trường hay khi chọn phương thức. Nếu không có
callback này, ví dùng prepare hoặc clientSecret. Nếu bạn chỉ giữ chỗ đơn hàng khi nhấn, hãy để callback
prepare lúc gắn trường tiếp tục một đơn hàng hiện có mà không tạo đơn hàng mới.
Chỉ hiển thị nút thanh toán của riêng bạn khi snapshot.paymentMethod là card; ví dùng các nút mang thương hiệu của chúng
thay cho payment.submit(). Kết quả từ ví dùng cùng luồng xác minh và khôi phục như thẻ. Việc đóng
cửa sổ thanh toán của ví vẫn giữ checkout khả dụng; một khoản thanh toán chưa chắc chắn vẫn ở trạng thái chờ và chặn một lần trừ tiền khác.
Cập nhật tổng tiền
Truyền wallets: { amount, currency, disabled: true } khi các thông tin checkout bắt buộc chưa đầy đủ. Cập nhật
tổng tiền hoặc bật các nút mà không cần gắn lại:
payment.update({ wallets: { amount: 2450, currency: 'EUR', disabled: false } })Truyền wallets: null để ẩn ví cho đến khi tổng tiền sẵn sàng. Tapaya từ chối xác nhận nếu khoản thanh toán đã chuẩn bị
khác với tổng tiền trong ví. Hook React usePayment() chấp nhận các tùy chọn này và cập nhật tổng tiền khi
wallets thay đổi.
Bộ chọn phương thức của riêng bạn
Nếu trang của bạn tự quản lý bộ chọn phương thức, hãy truyền paymentMethod: 'card', 'apple_pay' hoặc 'google_pay' để chỉ hiển thị
phương thức đó. Một ví đã chọn nhưng không khả dụng sẽ hiển thị nhãn walletUnavailable và báo
snapshot.error.code: 'wallet_unavailable' khi có thể bắt đầu một khoản thanh toán khác một cách an toàn; hãy đưa ra phương thức khác trong
bộ chọn của bạn. Khóa bộ chọn khi snapshot.busy là true hoặc khoản thanh toán đang ở trạng thái pending hay action_required.
Frame và CSP
SDK thêm allow="payment" vào frame của nó. Nếu cửa hàng của bạn nằm trong một frame khác, frame bên ngoài đó cũng phải cho phép
thanh toán. Apple Pay khác origin (cross-origin) yêu cầu Safari 17 trở lên. Các script của nhà cung cấp ví nằm bên trong frame của Tapaya, nên
trang của bạn không cần thêm origin nào của nhà cung cấp vào CSP.
Xử lý quyền truy cập trình duyệt đã hết hạn
Client secret hết hạn sau 24 giờ, và khi đó clientSecret là null. Một khoản thanh toán chưa từng được gửi khi đó
sẽ bị hủy, và lần gọi prepare() tiếp theo bắt đầu một lần thử mới cho cùng đơn hàng. Một khoản thanh toán đang chờ vẫn ở trạng thái pending
trong trình duyệt, và SDK tiếp tục gọi endpoint của bạn cho đến khi máy chủ của bạn báo kết quả của nó dưới dạng status. Việc đọc
từ máy chủ vẫn hoạt động, nên dù thế nào tác vụ định kỳ của bạn cũng sẽ xử lý xong khoản thanh toán đó.
Chẩn đoán lỗi máy chủ
Node SDK ném PaymentsApiError cho các yêu cầu API bị từ chối. Kiểm tra status, code, message,
fieldErrors và requestId của lỗi trên máy chủ. Không để thông tin xác thực và client secret xuất hiện trong log, và hiển thị
nội dung dành cho khách hàng do chính bạn soạn.
Khớp với thương hiệu của bạn
Các trường nhập thẻ dùng màu checkout đang có hiệu lực của merchant từ Cài đặt
Checkout làm màu nhấn khi focus (colorFocus).
Các giá trị bạn truyền trong appearance.variables sẽ ghi đè màu này, và chỉ những biến bạn đặt mới được gửi đi.