# SDK JavaScript: nhúng form và nhận kết quả

SDK mặc định mở modal ngay trong trang merchant. Không mở cửa sổ có thanh địa chỉ Pay2S, không đổi URL website. Phần nhập thẻ do ePay cung cấp; merchant không nhận số thẻ/CVV.

## HTML và JavaScript
```html
<script src="https://payment.pay2s.vn/sdk/pay2s-card.js"></script>
<button id="pay" type="button">Thanh toán thẻ</button>
<p id="result" role="status"></p>
```

```js
const labels={ready:'Sẵn sàng',pending:'Đang xác minh',paid:'Thanh toán thành công',
  failed:'Thanh toán thất bại',cancelled:'Đã hủy',review:'Cần đối soát',expired:'Hết hạn'};
const result=document.getElementById('result');
async function backend(path,body){
  const r=await fetch(path,{method:'POST',credentials:'same-origin',
    headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
  if(!r.ok)throw new Error('Backend chưa xử lý được yêu cầu');
  return r.json();
}
document.getElementById('pay').onclick=async()=>{
  try{
    await Pay2SCard.open({
      display:'modal',
      createSession:()=>backend('/checkout/card-session',{orderId:'ORDER001'}),
      getStatus:({orderId})=>backend('/checkout/card-status',{orderId}),
      onStatus:({status})=>{result.textContent=labels[status]||status;},
      onClose:({status})=>{
        if(status==='unknown')result.textContent='Đã đóng form; backend vẫn kiểm tra thanh toán.';
      }
    });
  }catch(e){result.textContent=e.message;}
};
```

Hai route /checkout/* là **route do merchant tự xây**, không phải API Pay2S. Bổ sung đăng nhập, kiểm tra quyền sở hữu đơn và CSRF theo ứng dụng. Backend đọc amount/buyer/connectionId từ cấu hình và đơn của mình; không tin số tiền gửi từ frontend.

## Hợp đồng createSession / getStatus

createSession trả **data**, không trả nguyên envelope Pay2S:
```json
{
  "connectionId": 12,
  "sessionId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "orderId": "ORDER001",
  "environment": "sandbox",
  "expiresAt": 1792314900,
  "popupUrl": "https://payment.pay2s.vn/sdk/card-popup.html#bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
```

getStatus nhận {orderId,sessionId}, gọi API status bằng bộ khóa server rồi trả data:
```json
{
  "connectionId": 12,
  "sessionId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "orderId": "ORDER001",
  "environment": "sandbox",
  "status": "paid",
  "amount": 100000,
  "merchantOrigin": "https://shop.example.com",
  "expiresAt": 1792314900,
  "launched": true
}
```

Nếu backend trả nguyên {status,success,message,data}, frontend phải lấy .data trước khi trả cho SDK. Không đưa secret/header ký xuống browser.

| Tùy chọn | Bắt buộc | Ý nghĩa |
| --- | --- | --- |
| createSession | Có | Hàm async tạo/lấy lại cùng phiên cho đơn |
| getStatus | Có | Hàm async đọc trạng thái từ backend merchant |
| display | Không | modal mặc định; popup mở cửa sổ riêng nếu ứng dụng chủ động chọn |
| onStatus | Không | Nhận {sessionId,orderId,status}, chỉ cập nhật giao diện |
| onClose | Không | Nhận {orderId,status}; unknown là đóng khi chưa rõ kết quả, closed là đã có trạng thái cuối |

open() resolve {sessionId,close}. close() chỉ đóng UI, không hủy/hoàn tiền. Escape cũng chỉ đóng modal. Bản hiện tại không thêm thanh tiêu đề/nút X bên ngoài form ePay. Modal tự đóng khi getStatus trả paid, failed, cancelled, review hoặc expired; ứng dụng cần hiển thị thông báo trong onStatus.

SDK poll khoảng 4 giây, tối đa 30 phút mỗi lần mở. Thông điệp browser chỉ kích hoạt đọc backend; không làm bằng chứng thanh toán. Backend merchant và webhook phải xử lý đơn dù người mua đóng tab/mất mạng.

## Return và đối soát

Bridge /sdk/card-popup.html, API /api/card-popup-browser.php và /api/card-popup-return.php là nội bộ SDK. Không gọi thủ công, không nhúng khóa vào chúng. Callback trình duyệt không thay thế webhook.

Khung modal chặn điều hướng thay trang chính. Nếu callback không về được, API trạng thái có inquiry dự phòng theo giới hạn; nếu ePay chưa có kết quả, vẫn pending. Không tạo lại giao dịch chỉ vì form đóng hoặc thời gian chờ hết.

## Domain, CSP và triển khai

merchantOrigin phải đúng origin trang nhúng, ví dụ https://shop.example.com; không dùng URL có đường dẫn. Production cần tên miền/phạm vi kết nối được duyệt. SDK không tự cấp quyền dùng Merchant ID trên mọi miền.

Merchant CSP cần cho phép script-src và frame-src https://payment.pay2s.vn cùng các nguồn sẵn có của website. Không thay toàn bộ CSP bằng ví dụ rút gọn. Server payment không được đặt X-Frame-Options/CSP chặn nhúng trên bridge và đường trả về cần nhúng; không gỡ bảo vệ toàn bộ website.

Desktop dùng windowType=0 và CSS ePay; mobile dùng windowType=1. Chính sách iframe, popup xác thực và 3DS của issuer/provider có thể khác nhau: cần thử sandbox trên domain thực trước production, không khẳng định mọi bước 3DS đã được hỗ trợ chỉ từ kiểm thử mô phỏng. display=popup là tùy chọn cho tích hợp cần cửa sổ riêng; không tự chuyển chế độ và không tự gửi lại giao dịch.

## Mã nguồn tải riêng, không nén

- [PHP client và xác minh webhook](/downloads/card-popup/client.php)
- [Node.js client và xác minh webhook](/downloads/card-popup/client.mjs)
- [Python client và xác minh webhook](/downloads/card-popup/pay2s_client.py)
- [Backend Node mẫu](/downloads/card-popup/server.cjs) và [HTML mẫu](/downloads/card-popup/index.html)
- [Request/response JSON](/downloads/card-popup/examples.json)
- [Webhook fixture có chữ ký thử](/downloads/card-popup/webhook-fixtures.json)
- [OpenAPI 3.1](/downloads/card-popup/openapi.json)

Backend Node mẫu dùng giá cố định 2.000 VND, cấu hình PAY2S_PARTNER_CODE, PAY2S_ACCESS_KEY, PAY2S_SECRET_KEY, MERCHANT_ORIGIN, MERCHANT_WEBHOOK_URL; PAY2S_CONNECTION_ID tùy chọn. Chạy node server.cjs và đặt sau HTTPS. Mẫu chỉ lưu webhook inbox; bổ sung database, quyền đơn, xử lý idempotent trước khi dùng production.

## Tích hợp bằng trang thanh toán riêng

Không cần nạp SDK: tạo phiên checkoutMode=hosted, truyền returnUrl rồi chuyển khách tới paymentUrl. Xem [mẫu đầy đủ](/docs/card-popup/create-session/). Không đưa paymentUrl vào Pay2SCard.open; phương thức này vẫn nhận popupUrl cho phiên SDK.
