payluk-escrow-inline-checkout 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,7 @@ A lightweight client SDK that initializes your checkout configuration, creates a
6
6
  - Promise-based API.
7
7
  - First-class TypeScript types.
8
8
  - Optional React hook with loading/error state.
9
+ - Multi-escrow support: pay for multiple escrows in a single checkout session.
9
10
 
10
11
  ## Installation
11
12
 
@@ -178,6 +179,106 @@ export function CheckoutButton() {
178
179
  - Only use publishable keys in the browser. Keep any secret keys on your server.
179
180
  - Validate inputs on your backend and return the required session payload.
180
181
 
182
+ ## Multi-Escrow Payments
183
+
184
+ You can now pay for multiple escrows in a single checkout session by passing an array of payment tokens. This is useful when a customer wants to pay for multiple items or services at once.
185
+
186
+ ### Requirements
187
+
188
+ - All escrows must belong to the same merchant
189
+ - All escrows must be in `PENDING` status
190
+ - If using merchant escrows, a `customerId` is required
191
+ - The total amount from all escrows will be charged in a single transaction
192
+
193
+ ### Vanilla JS/TS Example
194
+
195
+ ```ts
196
+ import { initEscrowCheckout, pay } from 'payluk-escrow-inline-checkout';
197
+
198
+ // Initialize once
199
+ initEscrowCheckout({
200
+ publicKey: '<YOUR_PUBLISHABLE_KEY>'
201
+ });
202
+
203
+ // Pay for multiple escrows
204
+ async function onPayMultipleClick() {
205
+ try {
206
+ await pay({
207
+ // Pass an array of payment tokens
208
+ paymentToken: ['TOKEN_1', 'TOKEN_2', 'TOKEN_3'],
209
+ reference: '<REFERENCE_ID>',
210
+ redirectUrl: 'https://your-app.example.com/checkout/complete',
211
+ logoUrl: 'https://mediacloud.me/media/W8HU9TK245QF528ZULCFSJXX2SBBLT.jpg',
212
+ brand: 'YourBrand',
213
+ customerId: 'customer_123', // Required for merchant escrows
214
+ callback: (result) => {
215
+ // result.paymentId will contain comma-separated IDs: "id1,id2,id3"
216
+ // result.reference will contain unique refs: "REF_1,REF_2,REF_3"
217
+ console.log('Multi-escrow checkout result:', result);
218
+ },
219
+ onClose: () => {
220
+ console.log('Widget closed');
221
+ }
222
+ });
223
+ } catch (err) {
224
+ console.error('Payment failed:', err);
225
+ }
226
+ }
227
+ ```
228
+
229
+ ### React Example
230
+
231
+ ```tsx
232
+ import React from 'react';
233
+ import { useEscrowCheckout } from 'payluk-escrow-inline-checkout/react';
234
+
235
+ export function MultiEscrowCheckoutButton() {
236
+ const { pay } = useEscrowCheckout();
237
+
238
+ const handleClick = async () => {
239
+ try {
240
+ await pay({
241
+ // Array of payment tokens for multi-escrow checkout
242
+ paymentToken: ['TOKEN_1', 'TOKEN_2', 'TOKEN_3'],
243
+ reference: '<REFERENCE_ID>',
244
+ redirectUrl: 'https://your-app.example.com/checkout/complete',
245
+ customerId: 'customer_123',
246
+ callback: (result) => {
247
+ console.log('Payment successful:', result);
248
+ }
249
+ });
250
+ } catch (err) {
251
+ console.error('Payment failed:', err);
252
+ }
253
+ };
254
+
255
+ return (
256
+ <button onClick={handleClick}>
257
+ Pay for Multiple Items
258
+ </button>
259
+ );
260
+ }
261
+ ```
262
+
263
+ ### How It Works
264
+
265
+ 1. **Individual Processing**: Each escrow is processed individually with its own payment intent
266
+ 2. **Aggregated Total**: The checkout widget displays the total amount from all escrows
267
+ 3. **Unique References**: Multi-payments generate unique references for each escrow (e.g., `REF_1`, `REF_2`, `REF_3`)
268
+ 4. **Single Transaction**: The customer completes one payment for all escrows combined
269
+ 5. **Backward Compatible**: Single payment tokens (strings) still work exactly as before
270
+
271
+ ### Response Format
272
+
273
+ When using multi-escrow payments, the callback receives:
274
+
275
+ ```ts
276
+ {
277
+ paymentId: "token1,token2,token3", // Comma-separated escrow IDs
278
+ reference: "REF_1,REF_2,REF_3" // Comma-separated unique references
279
+ }
280
+ ```
281
+
181
282
  ## API
182
283
 
183
284
  ### `initEscrowCheckout(config)`
@@ -207,20 +308,27 @@ initEscrowCheckout({
207
308
  Creates a checkout session via your backend and opens the widget.
208
309
 
209
310
  **Required:**
210
- - `paymentToken`: `string`
311
+ - `paymentToken`: `string | string[]` — single payment token or array of tokens for multi-escrow checkout
211
312
  - `reference`: `string`
212
313
  - `redirectUrl`: `string`
213
314
 
214
315
  **Optional:**
215
316
  - `logoUrl?`: `string`
216
- - `customerId?`: `string` — only for merchants using customer vaulting
317
+ - `customerId?`: `string` — required for merchant escrows, optional for business escrows
217
318
  - `brand?`: `string`
218
319
  - `callback?`: `(result: unknown) => void`
219
320
  - `onClose?`: `() => void`
321
+ - `extra?`: `Record<string, unknown>` — additional widget configuration
220
322
 
221
323
  **Returns:**
222
324
  - `Promise<void>` that resolves when the widget is opened (and rejects on errors).
223
325
 
326
+ **Multi-Escrow Notes:**
327
+ - When using an array of payment tokens, all escrows must belong to the same merchant
328
+ - The checkout widget will display the aggregated total amount
329
+ - Each escrow is processed individually with its own payment intent
330
+ - Empty arrays or arrays containing empty strings will be rejected
331
+
224
332
  ### `useEscrowCheckout(): { ready, loading, error, pay }`
225
333
 
226
334
  React hook that exposes:
@@ -246,7 +354,22 @@ import { useEscrowCheckout } from 'payluk-escrow-inline-checkout/react';
246
354
  Common issues:
247
355
  - **Not initialized:** Ensure `initEscrowCheckout({ publicKey })` is called before `pay(...)`.
248
356
  - **Browser-only:** Do not call `pay(...)` on the server.
249
- - **Network/API errors:** If the session endpoint fails, `pay(...)` will reject with the error message from your backend (if any).
357
+ - **Network/API errors:** If the session endpoint fails, `pay(...)` will reject with an `EscrowCheckoutError` that includes a `code`, optional HTTP `status`, and `details`.
358
+
359
+ `EscrowCheckoutError` codes:
360
+ - `NOT_INITIALIZED`
361
+ - `BROWSER_ONLY`
362
+ - `INVALID_INPUT`
363
+ - `WIDGET_LOAD`
364
+ - `NETWORK`
365
+ - `SESSION_CREATE`
366
+ - `SESSION_RESPONSE`
367
+
368
+ You can import the error class if you want stricter checks in TypeScript:
369
+
370
+ ```ts
371
+ import { EscrowCheckoutError } from 'payluk-escrow-inline-checkout';
372
+ ```
250
373
 
251
374
  **Example:**
252
375
 
@@ -254,7 +377,12 @@ Common issues:
254
377
  try {
255
378
  await pay({ /* ... */ });
256
379
  } catch (err) {
257
- alert((err as Error).message);
380
+ if (err instanceof EscrowCheckoutError) {
381
+ console.error(err.code, err.status, err.message);
382
+ alert(err.message);
383
+ } else {
384
+ alert('Checkout failed');
385
+ }
258
386
  }
259
387
  ```
260
388
 
@@ -0,0 +1,183 @@
1
+ // src/index.ts
2
+ var EscrowCheckoutError = class extends Error {
3
+ constructor(message, code, options) {
4
+ super(message);
5
+ this.name = "EscrowCheckoutError";
6
+ this.code = code;
7
+ this.status = options?.status;
8
+ this.details = options?.details;
9
+ }
10
+ };
11
+ var DEFAULT_SCRIPT_URL = "https://checkout.payluk.ng/escrow-checkout.min.js";
12
+ var DEFAULT_GLOBAL_NAME = "EscrowCheckout";
13
+ var CONFIG = null;
14
+ function initEscrowCheckout(config) {
15
+ if (!isNonEmptyString(config?.publicKey)) {
16
+ throw new EscrowCheckoutError('initEscrowCheckout(...) requires "publicKey".', "INVALID_INPUT");
17
+ }
18
+ CONFIG = {
19
+ publicKey: config.publicKey.trim(),
20
+ scriptUrl: config.scriptUrlOverride || DEFAULT_SCRIPT_URL,
21
+ globalName: config.globalName || DEFAULT_GLOBAL_NAME,
22
+ crossOrigin: config.crossOrigin ?? "anonymous"
23
+ };
24
+ }
25
+ function assertConfigured(config) {
26
+ if (!config) {
27
+ throw new EscrowCheckoutError(
28
+ "Escrow checkout not initialized. Call initEscrowCheckout({ publicKey }) first.",
29
+ "NOT_INITIALIZED"
30
+ );
31
+ }
32
+ }
33
+ function normalizeError(error) {
34
+ if (error instanceof Error) return error;
35
+ if (typeof error === "string") return new Error(error);
36
+ return new Error("Unknown error");
37
+ }
38
+ function isNonEmptyString(value) {
39
+ return typeof value === "string" && value.trim().length > 0;
40
+ }
41
+ function isValidPaymentToken(value) {
42
+ if (isNonEmptyString(value)) {
43
+ return true;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ return value.length > 0 && value.every((token) => isNonEmptyString(token));
47
+ }
48
+ return false;
49
+ }
50
+ function isSessionResponse(value) {
51
+ if (!value || typeof value !== "object") return false;
52
+ return "session" in value;
53
+ }
54
+ async function parseErrorBody(resp) {
55
+ const text = await resp.text();
56
+ if (!text) return {};
57
+ try {
58
+ const json = JSON.parse(text);
59
+ const message = typeof json?.message === "string" ? json.message : void 0;
60
+ return { message, details: json };
61
+ } catch {
62
+ return { message: text, details: text };
63
+ }
64
+ }
65
+ async function parseJsonResponse(resp) {
66
+ try {
67
+ return await resp.json();
68
+ } catch {
69
+ throw new EscrowCheckoutError("Invalid JSON response from session endpoint.", "SESSION_RESPONSE", {
70
+ status: resp.status
71
+ });
72
+ }
73
+ }
74
+ async function loadWidget() {
75
+ assertConfigured(CONFIG);
76
+ const { scriptUrl, globalName, crossOrigin } = CONFIG;
77
+ if (typeof window === "undefined") {
78
+ throw new EscrowCheckoutError("EscrowCheckout can only be loaded in the browser.", "BROWSER_ONLY");
79
+ }
80
+ const win = window;
81
+ win.__escrowCheckoutLoader ?? (win.__escrowCheckoutLoader = {});
82
+ if (win.__escrowCheckoutLoader[scriptUrl]) {
83
+ return win.__escrowCheckoutLoader[scriptUrl];
84
+ }
85
+ if (typeof win[globalName] === "function") {
86
+ const fn = win[globalName];
87
+ win.__escrowCheckoutLoader[scriptUrl] = Promise.resolve(fn);
88
+ return fn;
89
+ }
90
+ win.__escrowCheckoutLoader[scriptUrl] = new Promise((resolve, reject) => {
91
+ const script = document.createElement("script");
92
+ script.src = scriptUrl;
93
+ script.async = true;
94
+ if (crossOrigin) script.crossOrigin = crossOrigin;
95
+ script.onload = () => {
96
+ const fn = win[globalName];
97
+ if (typeof fn === "function") resolve(fn);
98
+ else {
99
+ reject(
100
+ new EscrowCheckoutError(
101
+ `Escrow checkout script loaded, but window.${globalName} is not a function.`,
102
+ "WIDGET_LOAD"
103
+ )
104
+ );
105
+ }
106
+ };
107
+ script.onerror = () => reject(new EscrowCheckoutError(`Failed to load escrow checkout script: ${scriptUrl}`, "WIDGET_LOAD"));
108
+ document.head.appendChild(script);
109
+ });
110
+ return win.__escrowCheckoutLoader[scriptUrl];
111
+ }
112
+ async function createSession(input) {
113
+ assertConfigured(CONFIG);
114
+ const { publicKey } = CONFIG;
115
+ const apiBaseUrl = publicKey.startsWith("pk_live_") ? "https://live.payluk.ng" : "https://staging.live.payluk.ng";
116
+ let resp;
117
+ try {
118
+ resp = await fetch(`${apiBaseUrl}/v1/checkout/session`, {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify({
122
+ paymentToken: input.paymentToken,
123
+ redirectUrl: input.redirectUrl,
124
+ reference: input.reference,
125
+ customerId: input.customerId,
126
+ publicKey
127
+ })
128
+ });
129
+ } catch (error) {
130
+ const normalized = normalizeError(error);
131
+ throw new EscrowCheckoutError("Network error while creating checkout session.", "NETWORK", {
132
+ details: normalized.message
133
+ });
134
+ }
135
+ if (!resp.ok) {
136
+ const { message, details } = await parseErrorBody(resp);
137
+ throw new EscrowCheckoutError(message || `Failed to create session (HTTP ${resp.status}).`, "SESSION_CREATE", {
138
+ status: resp.status,
139
+ details
140
+ });
141
+ }
142
+ const data = await parseJsonResponse(resp);
143
+ if (!isSessionResponse(data)) {
144
+ throw new EscrowCheckoutError('Session response missing "session".', "SESSION_RESPONSE", {
145
+ status: resp.status,
146
+ details: data
147
+ });
148
+ }
149
+ return data;
150
+ }
151
+ async function pay(input) {
152
+ if (typeof window === "undefined") {
153
+ throw new EscrowCheckoutError("pay(...) can only run in the browser.", "BROWSER_ONLY");
154
+ }
155
+ assertConfigured(CONFIG);
156
+ if (!input || !isValidPaymentToken(input.paymentToken)) {
157
+ throw new EscrowCheckoutError(
158
+ 'pay(...) requires "paymentToken" (must be a non-empty string or array of non-empty strings).',
159
+ "INVALID_INPUT"
160
+ );
161
+ }
162
+ if (!isNonEmptyString(input.reference)) {
163
+ throw new EscrowCheckoutError('pay(...) requires "reference".', "INVALID_INPUT");
164
+ }
165
+ if (!isNonEmptyString(input.redirectUrl)) {
166
+ throw new EscrowCheckoutError('pay(...) requires "redirectUrl".', "INVALID_INPUT");
167
+ }
168
+ const [{ session }, widget] = await Promise.all([createSession(input), loadWidget()]);
169
+ widget({
170
+ session,
171
+ logoUrl: input.logoUrl,
172
+ brand: input.brand,
173
+ callback: input.callback,
174
+ onClose: input.onClose,
175
+ customerId: input.customerId,
176
+ publicKey: CONFIG.publicKey,
177
+ ...input.extra ?? {}
178
+ });
179
+ }
180
+
181
+ export { EscrowCheckoutError, initEscrowCheckout, pay };
182
+ //# sourceMappingURL=chunk-E6SFCKQP.js.map
183
+ //# sourceMappingURL=chunk-E6SFCKQP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"chunk-E6SFCKQP.js","sourcesContent":["export type EscrowWidget = (options: Record<string, unknown>) => void;\n\nexport type EscrowCheckoutErrorCode =\n | 'NOT_INITIALIZED'\n | 'BROWSER_ONLY'\n | 'INVALID_INPUT'\n | 'WIDGET_LOAD'\n | 'NETWORK'\n | 'SESSION_CREATE'\n | 'SESSION_RESPONSE';\n\nexport class EscrowCheckoutError extends Error {\n code: EscrowCheckoutErrorCode;\n status?: number;\n details?: unknown;\n\n constructor(message: string, code: EscrowCheckoutErrorCode, options?: { status?: number; details?: unknown }) {\n super(message);\n this.name = 'EscrowCheckoutError';\n this.code = code;\n this.status = options?.status;\n this.details = options?.details;\n }\n}\n\nexport interface InitConfig {\n publicKey: string; // publishable key only\n /**\n * Optional overrides for experts. End users don't need to set these.\n */\n scriptUrlOverride?: string;\n globalName?: string; // default 'EscrowCheckout'\n crossOrigin?: '' | 'anonymous' | 'use-credentials';\n}\n\nexport interface PayInput {\n /**\n * Single escrow payment token or array of payment tokens for multi-escrow checkout.\n * When providing multiple tokens, they must all belong to the same merchant.\n */\n paymentToken: string | string[];\n reference: string;\n redirectUrl: string;\n logoUrl?: string;\n brand?: string;\n customerId?: string;\n /**\n * Extra fields supported by the widget.\n */\n extra?: Record<string, unknown>;\n callback?: (result: any) => void;\n onClose?: () => void;\n}\n\nexport interface SessionResponse {\n session: unknown;\n}\n\nconst DEFAULT_SCRIPT_URL = 'https://checkout.payluk.ng/escrow-checkout.min.js';\nconst DEFAULT_GLOBAL_NAME = 'EscrowCheckout';\n\ntype ResolvedConfig = Required<Pick<InitConfig, 'publicKey'>> & {\n scriptUrl: string;\n globalName: string;\n crossOrigin: '' | 'anonymous' | 'use-credentials';\n};\n\nlet CONFIG: ResolvedConfig | null = null;\n\n/**\n * Initialize the library once (e.g., in app bootstrap).\n * Hides script URL and session creation from consumers.\n */\nexport function initEscrowCheckout(config: InitConfig): void {\n if (!isNonEmptyString(config?.publicKey)) {\n throw new EscrowCheckoutError('initEscrowCheckout(...) requires \"publicKey\".', 'INVALID_INPUT');\n }\n CONFIG = {\n publicKey: config.publicKey.trim(),\n scriptUrl: config.scriptUrlOverride || DEFAULT_SCRIPT_URL,\n globalName: config.globalName || DEFAULT_GLOBAL_NAME,\n crossOrigin: config.crossOrigin ?? 'anonymous'\n };\n}\n\n/**\n * Narrow a provided config to ResolvedConfig or throw if not initialized.\n */\nfunction assertConfigured(config: ResolvedConfig | null): asserts config is ResolvedConfig {\n if (!config) {\n throw new EscrowCheckoutError(\n 'Escrow checkout not initialized. Call initEscrowCheckout({ publicKey }) first.',\n 'NOT_INITIALIZED'\n );\n }\n}\n\nfunction normalizeError(error: unknown): Error {\n if (error instanceof Error) return error;\n if (typeof error === 'string') return new Error(error);\n return new Error('Unknown error');\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction isValidPaymentToken(value: unknown): value is string | string[] {\n // Check if it's a valid string\n if (isNonEmptyString(value)) {\n return true;\n }\n\n // Check if it's a valid array of strings\n if (Array.isArray(value)) {\n return value.length > 0 && value.every((token) => isNonEmptyString(token));\n }\n\n return false;\n}\n\nfunction isSessionResponse(value: unknown): value is SessionResponse {\n if (!value || typeof value !== 'object') return false;\n return 'session' in value;\n}\n\nasync function parseErrorBody(resp: Response): Promise<{ message?: string; details?: unknown }> {\n const text = await resp.text();\n if (!text) return {};\n\n try {\n const json = JSON.parse(text) as { message?: unknown };\n const message = typeof json?.message === 'string' ? json.message : undefined;\n return { message, details: json };\n } catch {\n return { message: text, details: text };\n }\n}\n\nasync function parseJsonResponse(resp: Response): Promise<unknown> {\n try {\n return await resp.json();\n } catch {\n throw new EscrowCheckoutError('Invalid JSON response from session endpoint.', 'SESSION_RESPONSE', {\n status: resp.status\n });\n }\n}\n\n/**\n * Internal: load the widget script once and return the global function.\n */\nasync function loadWidget(): Promise<EscrowWidget> {\n assertConfigured(CONFIG);\n const { scriptUrl, globalName, crossOrigin } = CONFIG;\n\n if (typeof window === 'undefined') {\n throw new EscrowCheckoutError('EscrowCheckout can only be loaded in the browser.', 'BROWSER_ONLY');\n }\n\n const win = window as any;\n win.__escrowCheckoutLoader ??= {};\n\n if (win.__escrowCheckoutLoader[scriptUrl]) {\n return win.__escrowCheckoutLoader[scriptUrl];\n }\n\n if (typeof win[globalName] === 'function') {\n const fn = win[globalName] as EscrowWidget;\n win.__escrowCheckoutLoader[scriptUrl] = Promise.resolve(fn);\n return fn;\n }\n\n win.__escrowCheckoutLoader[scriptUrl] = new Promise<EscrowWidget>((resolve, reject) => {\n const script = document.createElement('script');\n script.src = scriptUrl;\n script.async = true;\n if (crossOrigin) script.crossOrigin = crossOrigin;\n\n script.onload = () => {\n const fn = win[globalName];\n if (typeof fn === 'function') resolve(fn as EscrowWidget);\n else {\n reject(\n new EscrowCheckoutError(\n `Escrow checkout script loaded, but window.${globalName} is not a function.`,\n 'WIDGET_LOAD'\n )\n );\n }\n };\n\n script.onerror = () =>\n reject(new EscrowCheckoutError(`Failed to load escrow checkout script: ${scriptUrl}`, 'WIDGET_LOAD'));\n\n document.head.appendChild(script);\n });\n\n return win.__escrowCheckoutLoader[scriptUrl];\n}\n\n/**\n * Internal: create a checkout session by calling your API.\n * This is intentionally inside the lib (user doesn't implement it).\n */\nasync function createSession(input: PayInput): Promise<SessionResponse> {\n assertConfigured(CONFIG);\n const { publicKey } = CONFIG;\n const apiBaseUrl = publicKey.startsWith('pk_live_') ? 'https://live.payluk.ng' : 'https://staging.live.payluk.ng';\n let resp: Response;\n\n try {\n resp = await fetch(`${apiBaseUrl}/v1/checkout/session`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n paymentToken: input.paymentToken,\n redirectUrl: input.redirectUrl,\n reference: input.reference,\n customerId: input.customerId,\n publicKey\n })\n });\n } catch (error) {\n const normalized = normalizeError(error);\n throw new EscrowCheckoutError('Network error while creating checkout session.', 'NETWORK', {\n details: normalized.message\n });\n }\n\n if (!resp.ok) {\n const { message, details } = await parseErrorBody(resp);\n throw new EscrowCheckoutError(message || `Failed to create session (HTTP ${resp.status}).`, 'SESSION_CREATE', {\n status: resp.status,\n details\n });\n }\n\n const data = await parseJsonResponse(resp);\n if (!isSessionResponse(data)) {\n throw new EscrowCheckoutError('Session response missing \"session\".', 'SESSION_RESPONSE', {\n status: resp.status,\n details: data\n });\n }\n\n return data as SessionResponse;\n}\n\n/**\n * Public API: create the session and open the widget.\n * Consumers only supply business inputs (no script URL, no API calls).\n */\nexport async function pay(input: PayInput): Promise<void> {\n if (typeof window === 'undefined') {\n throw new EscrowCheckoutError('pay(...) can only run in the browser.', 'BROWSER_ONLY');\n }\n\n assertConfigured(CONFIG);\n if (!input || !isValidPaymentToken(input.paymentToken)) {\n throw new EscrowCheckoutError(\n 'pay(...) requires \"paymentToken\" (must be a non-empty string or array of non-empty strings).',\n 'INVALID_INPUT'\n );\n }\n if (!isNonEmptyString(input.reference)) {\n throw new EscrowCheckoutError('pay(...) requires \"reference\".', 'INVALID_INPUT');\n }\n if (!isNonEmptyString(input.redirectUrl)) {\n throw new EscrowCheckoutError('pay(...) requires \"redirectUrl\".', 'INVALID_INPUT');\n }\n\n const [{ session }, widget] = await Promise.all([createSession(input), loadWidget()]);\n\n widget({\n session,\n logoUrl: input.logoUrl,\n brand: input.brand,\n callback: input.callback,\n onClose: input.onClose,\n customerId: input.customerId,\n publicKey: CONFIG.publicKey,\n ...(input.extra ?? {})\n });\n}\n"]}
package/dist/index.cjs CHANGED
@@ -1,13 +1,24 @@
1
1
  'use strict';
2
2
 
3
3
  // src/index.ts
4
+ var EscrowCheckoutError = class extends Error {
5
+ constructor(message, code, options) {
6
+ super(message);
7
+ this.name = "EscrowCheckoutError";
8
+ this.code = code;
9
+ this.status = options?.status;
10
+ this.details = options?.details;
11
+ }
12
+ };
4
13
  var DEFAULT_SCRIPT_URL = "https://checkout.payluk.ng/escrow-checkout.min.js";
5
14
  var DEFAULT_GLOBAL_NAME = "EscrowCheckout";
6
15
  var CONFIG = null;
7
16
  function initEscrowCheckout(config) {
8
- if (!config?.publicKey) throw new Error('initEscrowCheckout: "publicKey" is required.');
17
+ if (!isNonEmptyString(config?.publicKey)) {
18
+ throw new EscrowCheckoutError('initEscrowCheckout(...) requires "publicKey".', "INVALID_INPUT");
19
+ }
9
20
  CONFIG = {
10
- publicKey: config.publicKey,
21
+ publicKey: config.publicKey.trim(),
11
22
  scriptUrl: config.scriptUrlOverride || DEFAULT_SCRIPT_URL,
12
23
  globalName: config.globalName || DEFAULT_GLOBAL_NAME,
13
24
  crossOrigin: config.crossOrigin ?? "anonymous"
@@ -15,14 +26,58 @@ function initEscrowCheckout(config) {
15
26
  }
16
27
  function assertConfigured(config) {
17
28
  if (!config) {
18
- throw new Error("Escrow checkout not initialized. Call initEscrowCheckout({ apiBaseUrl, publicKey }) first.");
29
+ throw new EscrowCheckoutError(
30
+ "Escrow checkout not initialized. Call initEscrowCheckout({ publicKey }) first.",
31
+ "NOT_INITIALIZED"
32
+ );
33
+ }
34
+ }
35
+ function normalizeError(error) {
36
+ if (error instanceof Error) return error;
37
+ if (typeof error === "string") return new Error(error);
38
+ return new Error("Unknown error");
39
+ }
40
+ function isNonEmptyString(value) {
41
+ return typeof value === "string" && value.trim().length > 0;
42
+ }
43
+ function isValidPaymentToken(value) {
44
+ if (isNonEmptyString(value)) {
45
+ return true;
46
+ }
47
+ if (Array.isArray(value)) {
48
+ return value.length > 0 && value.every((token) => isNonEmptyString(token));
49
+ }
50
+ return false;
51
+ }
52
+ function isSessionResponse(value) {
53
+ if (!value || typeof value !== "object") return false;
54
+ return "session" in value;
55
+ }
56
+ async function parseErrorBody(resp) {
57
+ const text = await resp.text();
58
+ if (!text) return {};
59
+ try {
60
+ const json = JSON.parse(text);
61
+ const message = typeof json?.message === "string" ? json.message : void 0;
62
+ return { message, details: json };
63
+ } catch {
64
+ return { message: text, details: text };
65
+ }
66
+ }
67
+ async function parseJsonResponse(resp) {
68
+ try {
69
+ return await resp.json();
70
+ } catch {
71
+ throw new EscrowCheckoutError("Invalid JSON response from session endpoint.", "SESSION_RESPONSE", {
72
+ status: resp.status
73
+ });
19
74
  }
20
75
  }
21
76
  async function loadWidget() {
22
77
  assertConfigured(CONFIG);
23
78
  const { scriptUrl, globalName, crossOrigin } = CONFIG;
24
79
  if (typeof window === "undefined") {
25
- throw new Error("EscrowCheckout can only be loaded in the browser.");
80
+ throw new EscrowCheckoutError("EscrowCheckout can only be loaded in the browser.", "BROWSER_ONLY");
26
81
  }
27
82
  const win = window;
28
83
  win.__escrowCheckoutLoader ?? (win.__escrowCheckoutLoader = {});
@@ -42,9 +97,16 @@ async function loadWidget() {
42
97
  script.onload = () => {
43
98
  const fn = win[globalName];
44
99
  if (typeof fn === "function") resolve(fn);
45
- else reject(new Error(`Escrow checkout script loaded, but window.${globalName} is not a function.`));
100
+ else {
101
+ reject(
102
+ new EscrowCheckoutError(
103
+ `Escrow checkout script loaded, but window.${globalName} is not a function.`,
104
+ "WIDGET_LOAD"
105
+ )
106
+ );
107
+ }
46
108
  };
47
- script.onerror = () => reject(new Error(`Failed to load escrow checkout script: ${scriptUrl}`));
109
+ script.onerror = () => reject(new EscrowCheckoutError(`Failed to load escrow checkout script: ${scriptUrl}`, "WIDGET_LOAD"));
48
110
  document.head.appendChild(script);
49
111
  });
50
112
  return win.__escrowCheckoutLoader[scriptUrl];
@@ -53,26 +115,57 @@ async function createSession(input) {
53
115
  assertConfigured(CONFIG);
54
116
  const { publicKey } = CONFIG;
55
117
  const apiBaseUrl = publicKey.startsWith("pk_live_") ? "https://live.payluk.ng" : "https://staging.live.payluk.ng";
56
- const resp = await fetch(`${apiBaseUrl}/v1/checkout/session`, {
57
- method: "POST",
58
- headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({
60
- paymentToken: input.paymentToken,
61
- redirectUrl: input.redirectUrl,
62
- reference: input.reference,
63
- customerId: input.customerId,
64
- publicKey
65
- })
66
- });
118
+ let resp;
119
+ try {
120
+ resp = await fetch(`${apiBaseUrl}/v1/checkout/session`, {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({
124
+ paymentToken: input.paymentToken,
125
+ redirectUrl: input.redirectUrl,
126
+ reference: input.reference,
127
+ customerId: input.customerId,
128
+ publicKey
129
+ })
130
+ });
131
+ } catch (error) {
132
+ const normalized = normalizeError(error);
133
+ throw new EscrowCheckoutError("Network error while creating checkout session.", "NETWORK", {
134
+ details: normalized.message
135
+ });
136
+ }
67
137
  if (!resp.ok) {
68
- const err = await resp.json().catch(() => ({}));
69
- throw new Error(err.message || "Failed to create session");
138
+ const { message, details } = await parseErrorBody(resp);
139
+ throw new EscrowCheckoutError(message || `Failed to create session (HTTP ${resp.status}).`, "SESSION_CREATE", {
140
+ status: resp.status,
141
+ details
142
+ });
143
+ }
144
+ const data = await parseJsonResponse(resp);
145
+ if (!isSessionResponse(data)) {
146
+ throw new EscrowCheckoutError('Session response missing "session".', "SESSION_RESPONSE", {
147
+ status: resp.status,
148
+ details: data
149
+ });
70
150
  }
71
- return await resp.json();
151
+ return data;
72
152
  }
73
153
  async function pay(input) {
74
154
  if (typeof window === "undefined") {
75
- throw new Error("pay(...) can only run in the browser.");
155
+ throw new EscrowCheckoutError("pay(...) can only run in the browser.", "BROWSER_ONLY");
156
+ }
157
+ assertConfigured(CONFIG);
158
+ if (!input || !isValidPaymentToken(input.paymentToken)) {
159
+ throw new EscrowCheckoutError(
160
+ 'pay(...) requires "paymentToken" (must be a non-empty string or array of non-empty strings).',
161
+ "INVALID_INPUT"
162
+ );
163
+ }
164
+ if (!isNonEmptyString(input.reference)) {
165
+ throw new EscrowCheckoutError('pay(...) requires "reference".', "INVALID_INPUT");
166
+ }
167
+ if (!isNonEmptyString(input.redirectUrl)) {
168
+ throw new EscrowCheckoutError('pay(...) requires "redirectUrl".', "INVALID_INPUT");
76
169
  }
77
170
  const [{ session }, widget] = await Promise.all([createSession(input), loadWidget()]);
78
171
  widget({
@@ -82,11 +175,12 @@ async function pay(input) {
82
175
  callback: input.callback,
83
176
  onClose: input.onClose,
84
177
  customerId: input.customerId,
85
- publicKey: CONFIG?.publicKey,
178
+ publicKey: CONFIG.publicKey,
86
179
  ...input.extra ?? {}
87
180
  });
88
181
  }
89
182
 
183
+ exports.EscrowCheckoutError = EscrowCheckoutError;
90
184
  exports.initEscrowCheckout = initEscrowCheckout;
91
185
  exports.pay = pay;
92
186
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export type EscrowWidget = (options: Record<string, unknown>) => void;\r\n\r\nexport interface InitConfig {\r\n publicKey: string; // publishable key only\r\n /**\r\n * Optional overrides for experts. End users don't need to set these.\r\n */\r\n scriptUrlOverride?: string;\r\n globalName?: string; // default 'EscrowCheckout'\r\n crossOrigin?: '' | 'anonymous' | 'use-credentials';\r\n}\r\n\r\nexport interface PayInput {\r\n paymentToken: string;\r\n reference: string;\r\n redirectUrl: string;\r\n logoUrl?: string;\r\n brand?: string;\r\n customerId?: string;\r\n /**\r\n * Extra fields supported by the widget.\r\n */\r\n extra?: Record<string, unknown>;\r\n callback?: (result: any) => void;\r\n onClose?: () => void;\r\n}\r\n\r\nexport interface SessionResponse {\r\n session: unknown;\r\n}\r\n\r\nconst DEFAULT_SCRIPT_URL = 'https://checkout.payluk.ng/escrow-checkout.min.js';\r\nconst DEFAULT_GLOBAL_NAME = 'EscrowCheckout';\r\n\r\ntype ResolvedConfig = Required<Pick<InitConfig, 'publicKey'>> & {\r\n scriptUrl: string;\r\n globalName: string;\r\n crossOrigin: '' | 'anonymous' | 'use-credentials';\r\n};\r\n\r\nlet CONFIG: ResolvedConfig | null = null;\r\n\r\n/**\r\n * Initialize the library once (e.g., in app bootstrap).\r\n * Hides script URL and session creation from consumers.\r\n */\r\nexport function initEscrowCheckout(config: InitConfig): void {\r\n if (!config?.publicKey) throw new Error('initEscrowCheckout: \"publicKey\" is required.');\r\n CONFIG = {\r\n publicKey: config.publicKey,\r\n scriptUrl: config.scriptUrlOverride || DEFAULT_SCRIPT_URL,\r\n globalName: config.globalName || DEFAULT_GLOBAL_NAME,\r\n crossOrigin: config.crossOrigin ?? 'anonymous'\r\n };\r\n}\r\n\r\n/**\r\n * Narrow a provided config to ResolvedConfig or throw if not initialized.\r\n */\r\nfunction assertConfigured(config: ResolvedConfig | null): asserts config is ResolvedConfig {\r\n if (!config) {\r\n throw new Error('Escrow checkout not initialized. Call initEscrowCheckout({ apiBaseUrl, publicKey }) first.');\r\n }\r\n}\r\n\r\n/**\r\n * Internal: load the widget script once and return the global function.\r\n */\r\nasync function loadWidget(): Promise<EscrowWidget> {\r\n assertConfigured(CONFIG);\r\n const { scriptUrl, globalName, crossOrigin } = CONFIG;\r\n\r\n if (typeof window === 'undefined') {\r\n throw new Error('EscrowCheckout can only be loaded in the browser.');\r\n }\r\n\r\n const win = window as any;\r\n win.__escrowCheckoutLoader ??= {};\r\n\r\n if (win.__escrowCheckoutLoader[scriptUrl]) {\r\n return win.__escrowCheckoutLoader[scriptUrl];\r\n }\r\n\r\n if (typeof win[globalName] === 'function') {\r\n const fn = win[globalName] as EscrowWidget;\r\n win.__escrowCheckoutLoader[scriptUrl] = Promise.resolve(fn);\r\n return fn;\r\n }\r\n\r\n win.__escrowCheckoutLoader[scriptUrl] = new Promise<EscrowWidget>((resolve, reject) => {\r\n const script = document.createElement('script');\r\n script.src = scriptUrl;\r\n script.async = true;\r\n if (crossOrigin) script.crossOrigin = crossOrigin;\r\n\r\n script.onload = () => {\r\n const fn = win[globalName];\r\n if (typeof fn === 'function') resolve(fn as EscrowWidget);\r\n else reject(new Error(`Escrow checkout script loaded, but window.${globalName} is not a function.`));\r\n };\r\n\r\n script.onerror = () => reject(new Error(`Failed to load escrow checkout script: ${scriptUrl}`));\r\n\r\n document.head.appendChild(script);\r\n });\r\n\r\n return win.__escrowCheckoutLoader[scriptUrl];\r\n}\r\n\r\n/**\r\n * Internal: create a checkout session by calling your API.\r\n * This is intentionally inside the lib (user doesn't implement it).\r\n */\r\nasync function createSession(input: PayInput): Promise<SessionResponse> {\r\n assertConfigured(CONFIG);\r\n const { publicKey } = CONFIG;\r\n const apiBaseUrl = publicKey.startsWith(\"pk_live_\") ? 'https://live.payluk.ng' : 'https://staging.live.payluk.ng';\r\n\r\n const resp = await fetch(`${apiBaseUrl}/v1/checkout/session`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({\r\n paymentToken: input.paymentToken,\r\n redirectUrl: input.redirectUrl,\r\n reference: input.reference,\r\n customerId: input.customerId,\r\n publicKey\r\n })\r\n });\r\n\r\n if (!resp.ok) {\r\n const err = await resp.json().catch(() => ({}));\r\n throw new Error(err.message || 'Failed to create session');\r\n }\r\n\r\n return (await resp.json()) as SessionResponse;\r\n}\r\n\r\n/**\r\n * Public API: create the session and open the widget.\r\n * Consumers only supply business inputs (no script URL, no API calls).\r\n */\r\nexport async function pay(input: PayInput): Promise<void> {\r\n if (typeof window === 'undefined') {\r\n throw new Error('pay(...) can only run in the browser.');\r\n }\r\n\r\n const [{ session }, widget] = await Promise.all([createSession(input), loadWidget()]);\r\n\r\n widget({\r\n session,\r\n logoUrl: input.logoUrl,\r\n brand: input.brand,\r\n callback: input.callback,\r\n onClose: input.onClose,\r\n customerId: input.customerId,\r\n publicKey: CONFIG?.publicKey,\r\n ...(input.extra ?? {})\r\n });\r\n}"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAWO,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EAK3C,WAAA,CAAY,OAAA,EAAiB,IAAA,EAA+B,OAAA,EAAkD;AAC1G,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,qBAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,SAAS,OAAA,EAAS,MAAA;AACvB,IAAA,IAAA,CAAK,UAAU,OAAA,EAAS,OAAA;AAAA,EAC5B;AACJ;AAmCA,IAAM,kBAAA,GAAqB,mDAAA;AAC3B,IAAM,mBAAA,GAAsB,gBAAA;AAQ5B,IAAI,MAAA,GAAgC,IAAA;AAM7B,SAAS,mBAAmB,MAAA,EAA0B;AACzD,EAAA,IAAI,CAAC,gBAAA,CAAiB,MAAA,EAAQ,SAAS,CAAA,EAAG;AACtC,IAAA,MAAM,IAAI,mBAAA,CAAoB,+CAAA,EAAiD,eAAe,CAAA;AAAA,EAClG;AACA,EAAA,MAAA,GAAS;AAAA,IACL,SAAA,EAAW,MAAA,CAAO,SAAA,CAAU,IAAA,EAAK;AAAA,IACjC,SAAA,EAAW,OAAO,iBAAA,IAAqB,kBAAA;AAAA,IACvC,UAAA,EAAY,OAAO,UAAA,IAAc,mBAAA;AAAA,IACjC,WAAA,EAAa,OAAO,WAAA,IAAe;AAAA,GACvC;AACJ;AAKA,SAAS,iBAAiB,MAAA,EAAiE;AACvF,EAAA,IAAI,CAAC,MAAA,EAAQ;AACT,IAAA,MAAM,IAAI,mBAAA;AAAA,MACN,gFAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AACJ;AAEA,SAAS,eAAe,KAAA,EAAuB;AAC3C,EAAA,IAAI,KAAA,YAAiB,OAAO,OAAO,KAAA;AACnC,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,IAAI,MAAM,KAAK,CAAA;AACrD,EAAA,OAAO,IAAI,MAAM,eAAe,CAAA;AACpC;AAEA,SAAS,iBAAiB,KAAA,EAAiC;AACvD,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,IAAA,GAAO,MAAA,GAAS,CAAA;AAC9D;AAEA,SAAS,oBAAoB,KAAA,EAA4C;AAErE,EAAA,IAAI,gBAAA,CAAiB,KAAK,CAAA,EAAG;AACzB,IAAA,OAAO,IAAA;AAAA,EACX;AAGA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtB,IAAA,OAAO,KAAA,CAAM,SAAS,CAAA,IAAK,KAAA,CAAM,MAAM,CAAC,KAAA,KAAU,gBAAA,CAAiB,KAAK,CAAC,CAAA;AAAA,EAC7E;AAEA,EAAA,OAAO,KAAA;AACX;AAEA,SAAS,kBAAkB,KAAA,EAA0C;AACjE,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,UAAU,OAAO,KAAA;AAChD,EAAA,OAAO,SAAA,IAAa,KAAA;AACxB;AAEA,eAAe,eAAe,IAAA,EAAkE;AAC5F,EAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,IAAA,EAAK;AAC7B,EAAA,IAAI,CAAC,IAAA,EAAM,OAAO,EAAC;AAEnB,EAAA,IAAI;AACA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC5B,IAAA,MAAM,UAAU,OAAO,IAAA,EAAM,OAAA,KAAY,QAAA,GAAW,KAAK,OAAA,GAAU,KAAA,CAAA;AACnE,IAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,IAAA,EAAK;AAAA,EACpC,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,OAAA,EAAS,IAAA,EAAK;AAAA,EAC1C;AACJ;AAEA,eAAe,kBAAkB,IAAA,EAAkC;AAC/D,EAAA,IAAI;AACA,IAAA,OAAO,MAAM,KAAK,IAAA,EAAK;AAAA,EAC3B,CAAA,CAAA,MAAQ;AACJ,IAAA,MAAM,IAAI,mBAAA,CAAoB,8CAAA,EAAgD,kBAAA,EAAoB;AAAA,MAC9F,QAAQ,IAAA,CAAK;AAAA,KAChB,CAAA;AAAA,EACL;AACJ;AAKA,eAAe,UAAA,GAAoC;AAC/C,EAAA,gBAAA,CAAiB,MAAM,CAAA;AACvB,EAAA,MAAM,EAAE,SAAA,EAAW,UAAA,EAAY,WAAA,EAAY,GAAI,MAAA;AAE/C,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,IAAA,MAAM,IAAI,mBAAA,CAAoB,mDAAA,EAAqD,cAAc,CAAA;AAAA,EACrG;AAEA,EAAA,MAAM,GAAA,GAAM,MAAA;AACZ,EAAA,GAAA,CAAI,sBAAA,KAAJ,GAAA,CAAI,sBAAA,GAA2B,EAAC,CAAA;AAEhC,EAAA,IAAI,GAAA,CAAI,sBAAA,CAAuB,SAAS,CAAA,EAAG;AACvC,IAAA,OAAO,GAAA,CAAI,uBAAuB,SAAS,CAAA;AAAA,EAC/C;AAEA,EAAA,IAAI,OAAO,GAAA,CAAI,UAAU,CAAA,KAAM,UAAA,EAAY;AACvC,IAAA,MAAM,EAAA,GAAK,IAAI,UAAU,CAAA;AACzB,IAAA,GAAA,CAAI,sBAAA,CAAuB,SAAS,CAAA,GAAI,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAC1D,IAAA,OAAO,EAAA;AAAA,EACX;AAEA,EAAA,GAAA,CAAI,uBAAuB,SAAS,CAAA,GAAI,IAAI,OAAA,CAAsB,CAAC,SAAS,MAAA,KAAW;AACnF,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,QAAQ,CAAA;AAC9C,IAAA,MAAA,CAAO,GAAA,GAAM,SAAA;AACb,IAAA,MAAA,CAAO,KAAA,GAAQ,IAAA;AACf,IAAA,IAAI,WAAA,SAAoB,WAAA,GAAc,WAAA;AAEtC,IAAA,MAAA,CAAO,SAAS,MAAM;AAClB,MAAA,MAAM,EAAA,GAAK,IAAI,UAAU,CAAA;AACzB,MAAA,IAAI,OAAO,EAAA,KAAO,UAAA,EAAY,OAAA,CAAQ,EAAkB,CAAA;AAAA,WACnD;AACD,QAAA,MAAA;AAAA,UACI,IAAI,mBAAA;AAAA,YACA,6CAA6C,UAAU,CAAA,mBAAA,CAAA;AAAA,YACvD;AAAA;AACJ,SACJ;AAAA,MACJ;AAAA,IACJ,CAAA;AAEA,IAAA,MAAA,CAAO,OAAA,GAAU,MACb,MAAA,CAAO,IAAI,oBAAoB,CAAA,uCAAA,EAA0C,SAAS,CAAA,CAAA,EAAI,aAAa,CAAC,CAAA;AAExG,IAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAAA,EACpC,CAAC,CAAA;AAED,EAAA,OAAO,GAAA,CAAI,uBAAuB,SAAS,CAAA;AAC/C;AAMA,eAAe,cAAc,KAAA,EAA2C;AACpE,EAAA,gBAAA,CAAiB,MAAM,CAAA;AACvB,EAAA,MAAM,EAAE,WAAU,GAAI,MAAA;AACtB,EAAA,MAAM,UAAA,GAAa,SAAA,CAAU,UAAA,CAAW,UAAU,IAAI,wBAAA,GAA2B,gCAAA;AACjF,EAAA,IAAI,IAAA;AAEJ,EAAA,IAAI;AACA,IAAA,IAAA,GAAO,MAAM,KAAA,CAAM,CAAA,EAAG,UAAU,CAAA,oBAAA,CAAA,EAAwB;AAAA,MACpD,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACjB,cAAc,KAAA,CAAM,YAAA;AAAA,QACpB,aAAa,KAAA,CAAM,WAAA;AAAA,QACnB,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,YAAY,KAAA,CAAM,UAAA;AAAA,QAClB;AAAA,OACH;AAAA,KACJ,CAAA;AAAA,EACL,SAAS,KAAA,EAAO;AACZ,IAAA,MAAM,UAAA,GAAa,eAAe,KAAK,CAAA;AACvC,IAAA,MAAM,IAAI,mBAAA,CAAoB,gDAAA,EAAkD,SAAA,EAAW;AAAA,MACvF,SAAS,UAAA,CAAW;AAAA,KACvB,CAAA;AAAA,EACL;AAEA,EAAA,IAAI,CAAC,KAAK,EAAA,EAAI;AACV,IAAA,MAAM,EAAE,OAAA,EAAS,OAAA,EAAQ,GAAI,MAAM,eAAe,IAAI,CAAA;AACtD,IAAA,MAAM,IAAI,mBAAA,CAAoB,OAAA,IAAW,kCAAkC,IAAA,CAAK,MAAM,MAAM,gBAAA,EAAkB;AAAA,MAC1G,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb;AAAA,KACH,CAAA;AAAA,EACL;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,CAAkB,IAAI,CAAA;AACzC,EAAA,IAAI,CAAC,iBAAA,CAAkB,IAAI,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,mBAAA,CAAoB,qCAAA,EAAuC,kBAAA,EAAoB;AAAA,MACrF,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,OAAA,EAAS;AAAA,KACZ,CAAA;AAAA,EACL;AAEA,EAAA,OAAO,IAAA;AACX;AAMA,eAAsB,IAAI,KAAA,EAAgC;AACtD,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,IAAA,MAAM,IAAI,mBAAA,CAAoB,uCAAA,EAAyC,cAAc,CAAA;AAAA,EACzF;AAEA,EAAA,gBAAA,CAAiB,MAAM,CAAA;AACvB,EAAA,IAAI,CAAC,KAAA,IAAS,CAAC,mBAAA,CAAoB,KAAA,CAAM,YAAY,CAAA,EAAG;AACpD,IAAA,MAAM,IAAI,mBAAA;AAAA,MACN,8FAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AACA,EAAA,IAAI,CAAC,gBAAA,CAAiB,KAAA,CAAM,SAAS,CAAA,EAAG;AACpC,IAAA,MAAM,IAAI,mBAAA,CAAoB,gCAAA,EAAkC,eAAe,CAAA;AAAA,EACnF;AACA,EAAA,IAAI,CAAC,gBAAA,CAAiB,KAAA,CAAM,WAAW,CAAA,EAAG;AACtC,IAAA,MAAM,IAAI,mBAAA,CAAoB,kCAAA,EAAoC,eAAe,CAAA;AAAA,EACrF;AAEA,EAAA,MAAM,CAAC,EAAE,OAAA,EAAQ,EAAG,MAAM,CAAA,GAAI,MAAM,OAAA,CAAQ,GAAA,CAAI,CAAC,aAAA,CAAc,KAAK,CAAA,EAAG,UAAA,EAAY,CAAC,CAAA;AAEpF,EAAA,MAAA,CAAO;AAAA,IACH,OAAA;AAAA,IACA,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,GAAI,KAAA,CAAM,KAAA,IAAS;AAAC,GACvB,CAAA;AACL","file":"index.cjs","sourcesContent":["export type EscrowWidget = (options: Record<string, unknown>) => void;\n\nexport type EscrowCheckoutErrorCode =\n | 'NOT_INITIALIZED'\n | 'BROWSER_ONLY'\n | 'INVALID_INPUT'\n | 'WIDGET_LOAD'\n | 'NETWORK'\n | 'SESSION_CREATE'\n | 'SESSION_RESPONSE';\n\nexport class EscrowCheckoutError extends Error {\n code: EscrowCheckoutErrorCode;\n status?: number;\n details?: unknown;\n\n constructor(message: string, code: EscrowCheckoutErrorCode, options?: { status?: number; details?: unknown }) {\n super(message);\n this.name = 'EscrowCheckoutError';\n this.code = code;\n this.status = options?.status;\n this.details = options?.details;\n }\n}\n\nexport interface InitConfig {\n publicKey: string; // publishable key only\n /**\n * Optional overrides for experts. End users don't need to set these.\n */\n scriptUrlOverride?: string;\n globalName?: string; // default 'EscrowCheckout'\n crossOrigin?: '' | 'anonymous' | 'use-credentials';\n}\n\nexport interface PayInput {\n /**\n * Single escrow payment token or array of payment tokens for multi-escrow checkout.\n * When providing multiple tokens, they must all belong to the same merchant.\n */\n paymentToken: string | string[];\n reference: string;\n redirectUrl: string;\n logoUrl?: string;\n brand?: string;\n customerId?: string;\n /**\n * Extra fields supported by the widget.\n */\n extra?: Record<string, unknown>;\n callback?: (result: any) => void;\n onClose?: () => void;\n}\n\nexport interface SessionResponse {\n session: unknown;\n}\n\nconst DEFAULT_SCRIPT_URL = 'https://checkout.payluk.ng/escrow-checkout.min.js';\nconst DEFAULT_GLOBAL_NAME = 'EscrowCheckout';\n\ntype ResolvedConfig = Required<Pick<InitConfig, 'publicKey'>> & {\n scriptUrl: string;\n globalName: string;\n crossOrigin: '' | 'anonymous' | 'use-credentials';\n};\n\nlet CONFIG: ResolvedConfig | null = null;\n\n/**\n * Initialize the library once (e.g., in app bootstrap).\n * Hides script URL and session creation from consumers.\n */\nexport function initEscrowCheckout(config: InitConfig): void {\n if (!isNonEmptyString(config?.publicKey)) {\n throw new EscrowCheckoutError('initEscrowCheckout(...) requires \"publicKey\".', 'INVALID_INPUT');\n }\n CONFIG = {\n publicKey: config.publicKey.trim(),\n scriptUrl: config.scriptUrlOverride || DEFAULT_SCRIPT_URL,\n globalName: config.globalName || DEFAULT_GLOBAL_NAME,\n crossOrigin: config.crossOrigin ?? 'anonymous'\n };\n}\n\n/**\n * Narrow a provided config to ResolvedConfig or throw if not initialized.\n */\nfunction assertConfigured(config: ResolvedConfig | null): asserts config is ResolvedConfig {\n if (!config) {\n throw new EscrowCheckoutError(\n 'Escrow checkout not initialized. Call initEscrowCheckout({ publicKey }) first.',\n 'NOT_INITIALIZED'\n );\n }\n}\n\nfunction normalizeError(error: unknown): Error {\n if (error instanceof Error) return error;\n if (typeof error === 'string') return new Error(error);\n return new Error('Unknown error');\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction isValidPaymentToken(value: unknown): value is string | string[] {\n // Check if it's a valid string\n if (isNonEmptyString(value)) {\n return true;\n }\n\n // Check if it's a valid array of strings\n if (Array.isArray(value)) {\n return value.length > 0 && value.every((token) => isNonEmptyString(token));\n }\n\n return false;\n}\n\nfunction isSessionResponse(value: unknown): value is SessionResponse {\n if (!value || typeof value !== 'object') return false;\n return 'session' in value;\n}\n\nasync function parseErrorBody(resp: Response): Promise<{ message?: string; details?: unknown }> {\n const text = await resp.text();\n if (!text) return {};\n\n try {\n const json = JSON.parse(text) as { message?: unknown };\n const message = typeof json?.message === 'string' ? json.message : undefined;\n return { message, details: json };\n } catch {\n return { message: text, details: text };\n }\n}\n\nasync function parseJsonResponse(resp: Response): Promise<unknown> {\n try {\n return await resp.json();\n } catch {\n throw new EscrowCheckoutError('Invalid JSON response from session endpoint.', 'SESSION_RESPONSE', {\n status: resp.status\n });\n }\n}\n\n/**\n * Internal: load the widget script once and return the global function.\n */\nasync function loadWidget(): Promise<EscrowWidget> {\n assertConfigured(CONFIG);\n const { scriptUrl, globalName, crossOrigin } = CONFIG;\n\n if (typeof window === 'undefined') {\n throw new EscrowCheckoutError('EscrowCheckout can only be loaded in the browser.', 'BROWSER_ONLY');\n }\n\n const win = window as any;\n win.__escrowCheckoutLoader ??= {};\n\n if (win.__escrowCheckoutLoader[scriptUrl]) {\n return win.__escrowCheckoutLoader[scriptUrl];\n }\n\n if (typeof win[globalName] === 'function') {\n const fn = win[globalName] as EscrowWidget;\n win.__escrowCheckoutLoader[scriptUrl] = Promise.resolve(fn);\n return fn;\n }\n\n win.__escrowCheckoutLoader[scriptUrl] = new Promise<EscrowWidget>((resolve, reject) => {\n const script = document.createElement('script');\n script.src = scriptUrl;\n script.async = true;\n if (crossOrigin) script.crossOrigin = crossOrigin;\n\n script.onload = () => {\n const fn = win[globalName];\n if (typeof fn === 'function') resolve(fn as EscrowWidget);\n else {\n reject(\n new EscrowCheckoutError(\n `Escrow checkout script loaded, but window.${globalName} is not a function.`,\n 'WIDGET_LOAD'\n )\n );\n }\n };\n\n script.onerror = () =>\n reject(new EscrowCheckoutError(`Failed to load escrow checkout script: ${scriptUrl}`, 'WIDGET_LOAD'));\n\n document.head.appendChild(script);\n });\n\n return win.__escrowCheckoutLoader[scriptUrl];\n}\n\n/**\n * Internal: create a checkout session by calling your API.\n * This is intentionally inside the lib (user doesn't implement it).\n */\nasync function createSession(input: PayInput): Promise<SessionResponse> {\n assertConfigured(CONFIG);\n const { publicKey } = CONFIG;\n const apiBaseUrl = publicKey.startsWith('pk_live_') ? 'https://live.payluk.ng' : 'https://staging.live.payluk.ng';\n let resp: Response;\n\n try {\n resp = await fetch(`${apiBaseUrl}/v1/checkout/session`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n paymentToken: input.paymentToken,\n redirectUrl: input.redirectUrl,\n reference: input.reference,\n customerId: input.customerId,\n publicKey\n })\n });\n } catch (error) {\n const normalized = normalizeError(error);\n throw new EscrowCheckoutError('Network error while creating checkout session.', 'NETWORK', {\n details: normalized.message\n });\n }\n\n if (!resp.ok) {\n const { message, details } = await parseErrorBody(resp);\n throw new EscrowCheckoutError(message || `Failed to create session (HTTP ${resp.status}).`, 'SESSION_CREATE', {\n status: resp.status,\n details\n });\n }\n\n const data = await parseJsonResponse(resp);\n if (!isSessionResponse(data)) {\n throw new EscrowCheckoutError('Session response missing \"session\".', 'SESSION_RESPONSE', {\n status: resp.status,\n details: data\n });\n }\n\n return data as SessionResponse;\n}\n\n/**\n * Public API: create the session and open the widget.\n * Consumers only supply business inputs (no script URL, no API calls).\n */\nexport async function pay(input: PayInput): Promise<void> {\n if (typeof window === 'undefined') {\n throw new EscrowCheckoutError('pay(...) can only run in the browser.', 'BROWSER_ONLY');\n }\n\n assertConfigured(CONFIG);\n if (!input || !isValidPaymentToken(input.paymentToken)) {\n throw new EscrowCheckoutError(\n 'pay(...) requires \"paymentToken\" (must be a non-empty string or array of non-empty strings).',\n 'INVALID_INPUT'\n );\n }\n if (!isNonEmptyString(input.reference)) {\n throw new EscrowCheckoutError('pay(...) requires \"reference\".', 'INVALID_INPUT');\n }\n if (!isNonEmptyString(input.redirectUrl)) {\n throw new EscrowCheckoutError('pay(...) requires \"redirectUrl\".', 'INVALID_INPUT');\n }\n\n const [{ session }, widget] = await Promise.all([createSession(input), loadWidget()]);\n\n widget({\n session,\n logoUrl: input.logoUrl,\n brand: input.brand,\n callback: input.callback,\n onClose: input.onClose,\n customerId: input.customerId,\n publicKey: CONFIG.publicKey,\n ...(input.extra ?? {})\n });\n}\n"]}