@payos-inc/payos-js
Version:
PayOS JavaScript SDK for browser-based checkout and wallet onboarding
473 lines (470 loc) • 12.8 kB
JavaScript
// src/checkout/CheckoutClient.ts
var CheckoutClient = class {
constructor(config) {
this.popup = null;
this.messageHandler = null;
this.currentState = null;
this.popupCheckInterval = null;
if (config?.baseUrl) {
this.defaultUrl = `${config.baseUrl}/checkout`;
} else {
this.defaultUrl = "https://wallet-onboard.payos.ai/checkout";
}
this.defaultParams = config?.defaultParams;
}
/**
* Open PayOS Checkout with a token
*/
open(options) {
const {
token,
mode = "popup",
environment = "sandbox",
returnUrl,
baseUrl,
customParams,
onReady,
onComplete,
onError,
onCancel
} = options;
if (!token) {
const error = new Error("Token is required");
onError?.(error);
throw error;
}
const checkoutUrl = baseUrl ? `${baseUrl}/checkout` : this.defaultUrl;
this.currentState = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const params = new URLSearchParams({
token,
mode,
env: environment,
state: this.currentState,
...this.defaultParams,
// Global params from config
...customParams
// Per-request params
});
if (returnUrl) {
params.set("returnUrl", returnUrl);
}
const fullUrl = `${checkoutUrl}?${params.toString()}`;
switch (mode) {
case "redirect":
this.openRedirect(fullUrl);
break;
case "popup":
default:
this.openPopup(fullUrl, returnUrl, onReady, onComplete, onError, onCancel);
break;
}
}
/**
* Close checkout
*/
close() {
if (this.popupCheckInterval) {
clearInterval(this.popupCheckInterval);
this.popupCheckInterval = null;
}
if (this.popup) {
this.popup.close();
this.popup = null;
}
if (this.messageHandler) {
window.removeEventListener("message", this.messageHandler);
this.messageHandler = null;
}
}
openRedirect(url) {
window.location.href = url;
}
openPopup(url, returnUrl, onReady, onComplete, onError, onCancel) {
const width = 500;
const height = 700;
const left = (window.innerWidth - width) / 2 + window.screenLeft;
const top = (window.innerHeight - height) / 2 + window.screenTop;
this.popup = window.open(
url,
"payos-checkout",
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
);
if (!this.popup || this.popup.closed) {
if (returnUrl) {
window.location.href = url;
return;
}
onError?.(new Error("Popup blocked"));
return;
}
this.setupMessageListener(onReady, onComplete, onError, onCancel);
this.popupCheckInterval = setInterval(() => {
if (this.popup?.closed) {
if (this.popupCheckInterval) {
clearInterval(this.popupCheckInterval);
this.popupCheckInterval = null;
}
onCancel?.();
this.close();
}
}, 500);
}
setupMessageListener(onReady, onComplete, onError, onCancel) {
this.messageHandler = (event) => {
const allowedOrigins = [
"https://payos.app",
"https://checkout.payos.ai",
"https://calm-moss-06c4a301e.2.azurestaticapps.net",
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:5173"
// Vite dev server
];
if (!allowedOrigins.some((origin) => event.origin === origin))
return;
const data = event.data;
if (data?.status) {
if (data.state && data.state !== this.currentState)
return;
if (data.status === "success")
onComplete?.(data.data || {});
else if (data.status === "cancel")
onCancel?.();
else if (data.status === "error")
onError?.(new Error(data.error || "Unknown error"));
this.close();
return;
}
if (data?.source !== "payos-checkout")
return;
switch (data.type) {
case "CHECKOUT_READY":
onReady?.();
break;
case "CHECKOUT_COMPLETE":
onComplete?.(data.payload);
this.close();
break;
case "CHECKOUT_ERROR":
onError?.(new Error(data.error || "Checkout error"));
this.close();
break;
case "CHECKOUT_CANCELLED":
onCancel?.();
this.close();
break;
}
};
window.addEventListener("message", this.messageHandler);
setTimeout(() => this.close(), 5 * 60 * 1e3);
}
};
// src/wallet-onboard/WalletOnboardClient.ts
var WalletOnboardClient = class {
constructor(config) {
this.popup = null;
if (config?.baseUrl) {
this.defaultUrl = config.baseUrl;
} else {
this.defaultUrl = "https://wallet-onboard.payos.ai";
}
this.defaultParams = config?.defaultParams;
}
/**
* Open wallet onboard with specified options
* Supports popup and redirect modes
*/
open(options) {
const { mode = "popup" } = options;
this.close();
switch (mode) {
case "popup":
return this.openPopup(options);
case "redirect":
return this.openRedirect(options);
default:
throw new Error(`Invalid mode: ${mode}`);
}
}
/**
* Open wallet onboard in popup mode
*/
openPopup(options) {
const {
token,
baseUrl,
customParams,
environment = "sandbox",
returnUrl,
merchantName,
walletUserId,
onComplete,
onError,
onCancel
} = options;
const walletUrl = baseUrl || this.defaultUrl;
const state = crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const params = new URLSearchParams({
token,
env: environment,
state,
...this.defaultParams,
// Global testing params
...customParams
// Per-request params
});
if (merchantName) {
params.set("merchantName", merchantName);
}
if (walletUserId) {
params.set("walletUserId", walletUserId);
}
const url = `${walletUrl}?${params.toString()}`;
this.popup = window.open(
url,
"payos-wallet-onboard",
"width=500,height=700,left=100,top=100"
);
if (!this.popup || this.popup.closed) {
if (returnUrl) {
window.location.href = url;
return { close: () => {
} };
}
onError?.(new Error("Popup blocked"));
return { close: () => {
} };
}
const messageHandler = (event) => {
const allowedOrigins = [
"https://payos.app",
"https://wallet-onboard.payos.ai",
"https://staging.wallet-onboard.payos.ai",
"https://purple-grass-0eb0a1b1e.1.azurestaticapps.net",
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:5173"
// Vite dev server
];
if (!allowedOrigins.some((origin) => event.origin === origin))
return;
const data = event.data;
if (data?.status) {
if (data.state && data.state !== state)
return;
if (data.status === "success") {
onComplete?.({ walletUserId: data.data?.walletUserId, linkedCardIds: data.data?.linkedCardIds || [] });
} else if (data.status === "cancel") {
onCancel?.();
} else if (data.status === "error") {
onError?.(new Error(data.error || "Unknown error"));
}
window.removeEventListener("message", messageHandler);
this.popup?.close();
this.popup = null;
return;
}
const { type, success, walletUserId: walletUserId2 } = data;
if (type === "PAYOS_LINK_CLOSE") {
window.removeEventListener("message", messageHandler);
if (success && onComplete) {
onComplete({ walletUserId: walletUserId2, linkedCardIds: [] });
} else if (!success && onCancel) {
onCancel();
}
this.popup = null;
}
};
window.addEventListener("message", messageHandler);
const timeout = setTimeout(() => {
window.removeEventListener("message", messageHandler);
this.popup?.close();
this.popup = null;
}, 5 * 60 * 1e3);
const checkClosed = setInterval(() => {
if (this.popup && this.popup.closed) {
clearInterval(checkClosed);
clearTimeout(timeout);
window.removeEventListener("message", messageHandler);
if (onCancel) {
onCancel();
}
this.popup = null;
}
}, 500);
return {
close: () => {
if (this.popup && !this.popup.closed) {
this.popup.close();
this.popup = null;
}
}
};
}
/**
* Open wallet onboard in redirect mode
*/
openRedirect(options) {
const {
token,
baseUrl,
customParams,
environment = "sandbox",
merchantName,
walletUserId,
returnUrl = window.location.href
} = options;
const walletUrl = baseUrl || this.defaultUrl;
const params = new URLSearchParams({
token,
env: environment,
returnUrl,
...this.defaultParams,
// Global testing params
...customParams
// Per-request params
});
if (merchantName) {
params.set("merchantName", merchantName);
}
if (walletUserId) {
params.set("walletUserId", walletUserId);
}
const url = `${walletUrl}?${params.toString()}`;
window.location.href = url;
return {
close: () => {
}
};
}
/**
* Close the current wallet onboard instance
*/
close() {
if (this.popup && !this.popup.closed) {
this.popup.close();
this.popup = null;
}
}
};
// src/index.ts
var PayOS = class {
constructor(config) {
this._checkout = new CheckoutClient(config);
this._walletOnboard = new WalletOnboardClient(config);
}
/**
* Checkout module for payment authentication
* Opens hosted checkout UI with a token from your backend
*
* @example
* ```javascript
* // Get token from your backend
* const { token } = await fetch('/api/create-checkout-token', {...});
*
* // Open checkout
* payos.checkout.open({
* token: token,
* mode: 'iframe',
* onComplete: (result) => console.log('Payment complete', result)
* });
* ```
*/
get checkout() {
return this._checkout;
}
/**
* Wallet Onboard module for adding payment methods
* Opens hosted wallet onboard UI with a token from your backend
*
* @example
* ```javascript
* // Get token from your backend
* const { token } = await fetch('/api/create-onboard-token', {...});
*
* // Open wallet onboard
* payos.walletOnboard.open({
* token: token,
* mode: 'iframe',
* onComplete: (result) => console.log('Card added', result)
* });
* ```
*/
get walletOnboard() {
return this._walletOnboard;
}
/**
* Static method to check if PayOS.js is loaded
*/
static isLoaded() {
return true;
}
/**
* Version of the SDK
*/
static get version() {
return "1.0.0";
}
};
/**
* Initialize PayOS Wallet Onboard
*
* @example
* ```javascript
* // Simple: init with just token
* payos.walletOnboard.init('token');
*
* // With options
* payos.walletOnboard.init({ token: 'token', mode: 'popup' });
* ```
*/
PayOS.walletOnboard = {
init: initWalletOnboard
};
function initWalletOnboard(tokenOrOptions) {
const walletClient = new WalletOnboardClient();
let options;
if (typeof tokenOrOptions === "string") {
options = { token: tokenOrOptions };
} else {
options = tokenOrOptions;
if (!options.token) {
throw new Error("Token is required");
}
}
const instance = walletClient.open({
token: options.token,
mode: options.mode || "popup",
environment: options.environment,
merchantName: options.merchantName,
returnUrl: options.returnUrl,
walletUserId: void 0,
// This would come from elsewhere if needed
onComplete: (data) => {
if (options.onSuccess) {
options.onSuccess(data.walletUserId);
}
},
onError: options.onError,
onCancel: options.onClose
});
return instance;
}
var PayOSWalletOnboardSDK = {
init: initWalletOnboard
};
if (typeof window !== "undefined") {
window.PayOS = PayOS;
window.PayOSWalletOnboard = PayOSWalletOnboardSDK;
window.initPayOS = (token, options) => {
const simpleOptions = { token, ...options };
return initWalletOnboard(simpleOptions);
};
}
var src_default = PayOS;
export {
CheckoutClient,
PayOS,
WalletOnboardClient,
src_default as default
};