@ozura/elements 0.1.0-beta.7 → 1.0.1
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 +906 -663
- package/dist/frame/element-frame.js +77 -57
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.html +1 -1
- package/dist/frame/tokenizer-frame.js +211 -94
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/oz-elements.esm.js +817 -230
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +817 -229
- package/dist/oz-elements.umd.js.map +1 -1
- package/dist/react/frame/tokenizerFrame.d.ts +32 -0
- package/dist/react/index.cjs.js +968 -218
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +965 -219
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/react/index.d.ts +148 -6
- package/dist/react/sdk/OzElement.d.ts +34 -3
- package/dist/react/sdk/OzVault.d.ts +68 -4
- package/dist/react/sdk/errors.d.ts +9 -0
- package/dist/react/sdk/index.d.ts +29 -0
- package/dist/react/server/index.d.ts +181 -17
- package/dist/react/types/index.d.ts +69 -19
- package/dist/react/utils/appearance.d.ts +9 -0
- package/dist/react/utils/cardUtils.d.ts +14 -0
- package/dist/react/utils/uuid.d.ts +12 -0
- package/dist/server/frame/tokenizerFrame.d.ts +32 -0
- package/dist/server/index.cjs.js +608 -71
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +606 -72
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/sdk/OzElement.d.ts +34 -3
- package/dist/server/sdk/OzVault.d.ts +68 -4
- package/dist/server/sdk/errors.d.ts +9 -0
- package/dist/server/sdk/index.d.ts +29 -0
- package/dist/server/server/index.d.ts +181 -17
- package/dist/server/types/index.d.ts +69 -19
- package/dist/server/utils/appearance.d.ts +9 -0
- package/dist/server/utils/cardUtils.d.ts +14 -0
- package/dist/server/utils/uuid.d.ts +12 -0
- package/dist/types/frame/tokenizerFrame.d.ts +32 -0
- package/dist/types/sdk/OzElement.d.ts +34 -3
- package/dist/types/sdk/OzVault.d.ts +68 -4
- package/dist/types/sdk/errors.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +29 -0
- package/dist/types/server/index.d.ts +181 -17
- package/dist/types/types/index.d.ts +69 -19
- package/dist/types/utils/appearance.d.ts +9 -0
- package/dist/types/utils/cardUtils.d.ts +14 -0
- package/dist/types/utils/uuid.d.ts +12 -0
- package/package.json +7 -4
package/dist/oz-elements.esm.js
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errors.ts — error types and normalisation for OzElements.
|
|
3
|
+
*
|
|
4
|
+
* Three normalisation functions:
|
|
5
|
+
* - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
|
|
6
|
+
* - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
|
|
7
|
+
* - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
|
|
8
|
+
*
|
|
9
|
+
* Error keys in normalizeCardSaleError are taken directly from checkout's
|
|
10
|
+
* errorMapping.ts so the same error strings produce the same user-facing copy.
|
|
11
|
+
*/
|
|
12
|
+
const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
|
|
13
|
+
/** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
|
|
14
|
+
function isOzErrorCode(value) {
|
|
15
|
+
return typeof value === 'string' && OZ_ERROR_CODES.has(value);
|
|
16
|
+
}
|
|
17
|
+
class OzError extends Error {
|
|
18
|
+
constructor(message, raw, errorCode) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'OzError';
|
|
21
|
+
this.raw = raw !== null && raw !== void 0 ? raw : message;
|
|
22
|
+
this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
|
|
23
|
+
this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Shared patterns that apply to both card and bank vault errors. */
|
|
27
|
+
function normalizeCommonVaultError(msg) {
|
|
28
|
+
if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
|
|
29
|
+
return 'Authentication failed. Check your vault API key configuration.';
|
|
30
|
+
}
|
|
31
|
+
if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
|
|
32
|
+
return 'A network error occurred. Please check your connection and try again.';
|
|
33
|
+
}
|
|
34
|
+
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
35
|
+
return 'The request timed out. Please try again.';
|
|
36
|
+
}
|
|
37
|
+
if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
|
|
38
|
+
return 'A server error occurred. Please try again shortly.';
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Maps a raw vault /tokenize error string to a user-facing message for card flows.
|
|
44
|
+
* Falls back to the original string if no pattern matches.
|
|
45
|
+
*/
|
|
46
|
+
function normalizeVaultError(raw) {
|
|
47
|
+
const msg = raw.toLowerCase();
|
|
48
|
+
if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
|
|
49
|
+
return 'The card number is invalid. Please check and try again.';
|
|
50
|
+
}
|
|
51
|
+
if (msg.includes('expir')) {
|
|
52
|
+
return 'The card expiration date is invalid or the card has expired.';
|
|
53
|
+
}
|
|
54
|
+
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
55
|
+
return 'The CVV code is invalid. Please check and try again.';
|
|
56
|
+
}
|
|
57
|
+
if (msg.includes('insufficient') || msg.includes('funds')) {
|
|
58
|
+
return 'Your card has insufficient funds. Please use a different card.';
|
|
59
|
+
}
|
|
60
|
+
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
61
|
+
return 'Your card was declined. Please try a different card or contact your bank.';
|
|
62
|
+
}
|
|
63
|
+
const common = normalizeCommonVaultError(msg);
|
|
64
|
+
if (common)
|
|
65
|
+
return common;
|
|
66
|
+
return raw;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
|
|
70
|
+
* Uses bank-specific pattern matching so card-specific messages are never shown for
|
|
71
|
+
* bank errors. Falls back to the original string if no pattern matches.
|
|
72
|
+
*/
|
|
73
|
+
function normalizeBankVaultError(raw) {
|
|
74
|
+
const msg = raw.toLowerCase();
|
|
75
|
+
if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
|
|
76
|
+
return 'The bank account number is invalid. Please check and try again.';
|
|
77
|
+
}
|
|
78
|
+
if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
|
|
79
|
+
return 'The routing number is invalid. Please check and try again.';
|
|
80
|
+
}
|
|
81
|
+
const common = normalizeCommonVaultError(msg);
|
|
82
|
+
if (common)
|
|
83
|
+
return common;
|
|
84
|
+
return raw;
|
|
85
|
+
}
|
|
86
|
+
// ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
|
|
87
|
+
const CARD_SALE_ERROR_MAP = [
|
|
88
|
+
['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
|
|
89
|
+
['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
|
|
90
|
+
['Card expired', 'Your card has expired. Please use a different card.'],
|
|
91
|
+
['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
|
|
92
|
+
['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
|
|
93
|
+
['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
94
|
+
['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
95
|
+
['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
|
|
96
|
+
['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
|
|
97
|
+
['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
|
|
98
|
+
['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
|
|
99
|
+
['API key not found', 'Authentication failed. Please refresh the page.'],
|
|
100
|
+
['Access token expired', 'Your session has expired. Please refresh the page.'],
|
|
101
|
+
['Access token is invalid', 'Authentication failed. Please refresh the page.'],
|
|
102
|
+
['Unauthorized', 'Authentication failed. Please refresh the page.'],
|
|
103
|
+
['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
|
|
104
|
+
['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
|
|
105
|
+
['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
|
|
106
|
+
['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
|
|
107
|
+
['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
|
|
108
|
+
['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
|
|
109
|
+
['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* Maps a raw Ozura Pay API cardSale error string to a user-facing message.
|
|
113
|
+
*
|
|
114
|
+
* Uses the exact same error key table as checkout's `getUserFriendlyError()` in
|
|
115
|
+
* errorMapping.ts so both surfaces produce identical copy for the same errors.
|
|
116
|
+
*
|
|
117
|
+
* Falls back to the original string when it's under 100 characters, or to a
|
|
118
|
+
* generic message for long/opaque server errors — matching checkout's fallback
|
|
119
|
+
* behaviour exactly.
|
|
120
|
+
*
|
|
121
|
+
* **Trade-off:** Short unrecognised strings (e.g. processor codes like
|
|
122
|
+
* `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
|
|
123
|
+
* checkout so the same raw Pay API errors produce the same user-facing text on
|
|
124
|
+
* both surfaces. If the Pay API ever returns internal codes that should never
|
|
125
|
+
* reach the UI, the fix belongs in the Pay API error normalisation layer rather
|
|
126
|
+
* than here.
|
|
127
|
+
*/
|
|
128
|
+
function normalizeCardSaleError(raw) {
|
|
129
|
+
if (!raw)
|
|
130
|
+
return 'Payment processing failed. Please try again.';
|
|
131
|
+
for (const [key, message] of CARD_SALE_ERROR_MAP) {
|
|
132
|
+
if (raw.toLowerCase().includes(key.toLowerCase())) {
|
|
133
|
+
return message;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Checkout fallback: pass through short errors, genericise long ones
|
|
137
|
+
if (raw.length < 100)
|
|
138
|
+
return raw;
|
|
139
|
+
return 'Payment processing failed. Please try again or contact support.';
|
|
140
|
+
}
|
|
141
|
+
|
|
1
142
|
const THEME_DEFAULT = {
|
|
2
143
|
base: {
|
|
3
144
|
color: '#1a1a2e',
|
|
@@ -128,6 +269,15 @@ function mergeStyleConfigs(a, b) {
|
|
|
128
269
|
* Resolution order: theme defaults → variable overrides.
|
|
129
270
|
* The returned config is then used as the "base appearance" that
|
|
130
271
|
* per-element `style` overrides merge on top of.
|
|
272
|
+
*
|
|
273
|
+
* @remarks
|
|
274
|
+
* - `appearance: undefined` → no styles injected (element iframes use their
|
|
275
|
+
* own minimal built-in defaults).
|
|
276
|
+
* - `appearance: {}` or `appearance: { variables: {...} }` without an explicit
|
|
277
|
+
* `theme` → the `'default'` theme is used as the base. Omitting `theme`
|
|
278
|
+
* does NOT mean "no theme" — it means `theme: 'default'`. To opt out of
|
|
279
|
+
* the preset themes entirely, use per-element `style` overrides without
|
|
280
|
+
* passing an `appearance` prop at all.
|
|
131
281
|
*/
|
|
132
282
|
function resolveAppearance(appearance) {
|
|
133
283
|
var _a, _b;
|
|
@@ -153,7 +303,31 @@ function mergeAppearanceWithElementStyle(appearance, elementStyle) {
|
|
|
153
303
|
return mergeStyleConfigs(appearance, elementStyle);
|
|
154
304
|
}
|
|
155
305
|
|
|
156
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Generates a RFC 4122 v4 UUID.
|
|
308
|
+
*
|
|
309
|
+
* Uses `crypto.randomUUID()` when available (Chrome 92+, Firefox 95+,
|
|
310
|
+
* Safari 15.4+, Node 14.17+) and falls back to `crypto.getRandomValues()`
|
|
311
|
+
* for older environments (Safari 14, some embedded WebViews, older Node).
|
|
312
|
+
*
|
|
313
|
+
* Both paths use the same CSPRNG — the difference is only in API surface.
|
|
314
|
+
*
|
|
315
|
+
* @internal
|
|
316
|
+
*/
|
|
317
|
+
function uuid() {
|
|
318
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
319
|
+
return crypto.randomUUID();
|
|
320
|
+
}
|
|
321
|
+
// Fallback: build UUID v4 from random bytes
|
|
322
|
+
const bytes = new Uint8Array(16);
|
|
323
|
+
crypto.getRandomValues(bytes);
|
|
324
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
325
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant RFC 4122
|
|
326
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
327
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const BLOCKED_CSS_PATTERNS = /url\s*\(|expression\s*\(|javascript\s*:|vbscript\s*:|@import|behavior\s*:|binding\s*:|-moz-binding|-webkit-binding|<\s*script|<\s*style|\\[0-9a-fA-F]|var\s*\(/i;
|
|
157
331
|
const CSS_BREAKOUT = /[{};<>]/;
|
|
158
332
|
const MAX_CSS_VALUE_LEN = 200;
|
|
159
333
|
function sanitizeStyleObj(obj) {
|
|
@@ -185,6 +359,11 @@ function sanitizeStyles(style) {
|
|
|
185
359
|
function sanitizeOptions(options) {
|
|
186
360
|
var _a;
|
|
187
361
|
const result = Object.assign(Object.assign({}, options), { placeholder: (_a = options.placeholder) === null || _a === void 0 ? void 0 : _a.slice(0, 100) });
|
|
362
|
+
// Coerce to boolean so a string "false" (truthy in JS) does not accidentally
|
|
363
|
+
// disable the input when the SDK is consumed from plain JavaScript.
|
|
364
|
+
if (options.disabled !== undefined) {
|
|
365
|
+
result.disabled = Boolean(options.disabled);
|
|
366
|
+
}
|
|
188
367
|
// Only set style when provided; omitting it avoids clobbering existing style
|
|
189
368
|
// when merging (e.g. update({ placeholder: 'new' }) must not overwrite style with undefined).
|
|
190
369
|
if (options.style !== undefined) {
|
|
@@ -212,7 +391,7 @@ class OzElement {
|
|
|
212
391
|
this.frameOrigin = new URL(frameBaseUrl).origin;
|
|
213
392
|
this.fonts = fonts;
|
|
214
393
|
this.appearanceStyle = appearanceStyle;
|
|
215
|
-
this.frameId = `oz-${elementType}-${
|
|
394
|
+
this.frameId = `oz-${elementType}-${uuid()}`;
|
|
216
395
|
}
|
|
217
396
|
/** The element type this proxy represents. */
|
|
218
397
|
get type() {
|
|
@@ -230,22 +409,27 @@ class OzElement {
|
|
|
230
409
|
mount(target) {
|
|
231
410
|
var _a;
|
|
232
411
|
if (this._destroyed)
|
|
233
|
-
throw new
|
|
412
|
+
throw new OzError('Cannot mount a destroyed element.');
|
|
234
413
|
if (this.iframe)
|
|
235
414
|
this.unmount();
|
|
236
415
|
const container = typeof target === 'string'
|
|
237
416
|
? document.querySelector(target)
|
|
238
417
|
: target;
|
|
239
418
|
if (!container)
|
|
240
|
-
throw new
|
|
241
|
-
? `
|
|
242
|
-
:
|
|
419
|
+
throw new OzError(typeof target === 'string'
|
|
420
|
+
? `Mount target not found — no element matches "${target}"`
|
|
421
|
+
: 'Mount target not found — the provided HTMLElement is null or undefined');
|
|
243
422
|
const iframe = document.createElement('iframe');
|
|
244
423
|
iframe.setAttribute('frameborder', '0');
|
|
245
424
|
iframe.setAttribute('scrolling', 'no');
|
|
246
425
|
iframe.setAttribute('allowtransparency', 'true');
|
|
247
426
|
iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
|
|
248
427
|
iframe.title = `Secure ${this.elementType} input`;
|
|
428
|
+
// Note: the `sandbox` attribute is intentionally NOT set. Field values are
|
|
429
|
+
// delivered to the tokenizer iframe via a MessageChannel port (transferred
|
|
430
|
+
// in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
|
|
431
|
+
// The security boundary is the vault URL hardcoded at build time and the
|
|
432
|
+
// origin checks on every postMessage, not the sandbox flag.
|
|
249
433
|
// Use hash instead of query string — survives clean-URL redirects from static servers.
|
|
250
434
|
// parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
|
|
251
435
|
const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
@@ -261,6 +445,11 @@ class OzElement {
|
|
|
261
445
|
}
|
|
262
446
|
}, timeout);
|
|
263
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* Subscribe to an element event. Returns `this` for chaining.
|
|
450
|
+
* @param event - Event name: `'change'`, `'focus'`, `'blur'`, `'ready'`, or `'loaderror'`.
|
|
451
|
+
* @param callback - Handler invoked with the event payload.
|
|
452
|
+
*/
|
|
264
453
|
on(event, callback) {
|
|
265
454
|
if (this._destroyed)
|
|
266
455
|
return this;
|
|
@@ -269,6 +458,10 @@ class OzElement {
|
|
|
269
458
|
this.handlers.get(event).push(callback);
|
|
270
459
|
return this;
|
|
271
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Remove a previously registered event handler.
|
|
463
|
+
* Has no effect if the handler is not registered.
|
|
464
|
+
*/
|
|
272
465
|
off(event, callback) {
|
|
273
466
|
const list = this.handlers.get(event);
|
|
274
467
|
if (list) {
|
|
@@ -278,6 +471,10 @@ class OzElement {
|
|
|
278
471
|
}
|
|
279
472
|
return this;
|
|
280
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Subscribe to an event for a single invocation. The handler is automatically
|
|
476
|
+
* removed after it fires once.
|
|
477
|
+
*/
|
|
281
478
|
once(event, callback) {
|
|
282
479
|
const wrapper = (payload) => {
|
|
283
480
|
this.off(event, wrapper);
|
|
@@ -285,13 +482,24 @@ class OzElement {
|
|
|
285
482
|
};
|
|
286
483
|
return this.on(event, wrapper);
|
|
287
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Dynamically update element options (placeholder, style, etc.) without
|
|
487
|
+
* re-mounting the iframe. Only the provided keys are merged; omitted keys
|
|
488
|
+
* retain their current values.
|
|
489
|
+
*/
|
|
288
490
|
update(options) {
|
|
289
491
|
if (this._destroyed)
|
|
290
492
|
return;
|
|
291
493
|
const safe = sanitizeOptions(options);
|
|
494
|
+
// Re-merge vault appearance when style is updated so focus/invalid/complete/
|
|
495
|
+
// placeholder buckets from the theme are not stripped by a partial style object.
|
|
496
|
+
if (safe.style !== undefined) {
|
|
497
|
+
safe.style = mergeAppearanceWithElementStyle(this.appearanceStyle, safe.style);
|
|
498
|
+
}
|
|
292
499
|
this.options = Object.assign(Object.assign({}, this.options), safe);
|
|
293
500
|
this.post({ type: 'OZ_UPDATE', options: safe });
|
|
294
501
|
}
|
|
502
|
+
/** Clear the current field value without removing the element from the DOM. */
|
|
295
503
|
clear() {
|
|
296
504
|
if (this._destroyed)
|
|
297
505
|
return;
|
|
@@ -304,6 +512,8 @@ class OzElement {
|
|
|
304
512
|
*/
|
|
305
513
|
unmount() {
|
|
306
514
|
var _a;
|
|
515
|
+
if (this._destroyed)
|
|
516
|
+
return;
|
|
307
517
|
if (this._loadTimer) {
|
|
308
518
|
clearTimeout(this._loadTimer);
|
|
309
519
|
this._loadTimer = null;
|
|
@@ -336,18 +546,31 @@ class OzElement {
|
|
|
336
546
|
this._destroyed = true;
|
|
337
547
|
}
|
|
338
548
|
// ─── Called by OzVault ───────────────────────────────────────────────────
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Sends OZ_BEGIN_COLLECT to the element iframe, transferring `port` so the
|
|
551
|
+
* iframe can post its field value directly to the tokenizer without going
|
|
552
|
+
* through the merchant page (no named-window lookup required).
|
|
553
|
+
* @internal
|
|
554
|
+
*/
|
|
555
|
+
beginCollect(requestId, port) {
|
|
556
|
+
if (this._destroyed)
|
|
557
|
+
return;
|
|
558
|
+
this.sendWithTransfer({ type: 'OZ_BEGIN_COLLECT', requestId }, [port]);
|
|
344
559
|
}
|
|
345
|
-
/**
|
|
560
|
+
/**
|
|
561
|
+
* Tell a CVV element how many digits to expect. Called automatically when card brand changes.
|
|
562
|
+
* @internal
|
|
563
|
+
*/
|
|
346
564
|
setCvvLength(length) {
|
|
565
|
+
if (this._destroyed)
|
|
566
|
+
return;
|
|
347
567
|
this.post({ type: 'OZ_SET_CVV_LENGTH', length });
|
|
348
568
|
}
|
|
569
|
+
/** @internal */
|
|
349
570
|
handleMessage(msg) {
|
|
350
571
|
var _a, _b;
|
|
572
|
+
if (this._destroyed)
|
|
573
|
+
return;
|
|
351
574
|
switch (msg.type) {
|
|
352
575
|
case 'OZ_FRAME_READY': {
|
|
353
576
|
this._ready = true;
|
|
@@ -361,6 +584,19 @@ class OzElement {
|
|
|
361
584
|
this.pendingMessages.forEach(m => this.send(m));
|
|
362
585
|
this.pendingMessages = [];
|
|
363
586
|
this.emit('ready', undefined);
|
|
587
|
+
// Warn if the mount container collapses to zero height — the input will
|
|
588
|
+
// be invisible but functional, which is hard to debug. Check after one
|
|
589
|
+
// animation frame so the browser has completed layout. Guard against
|
|
590
|
+
// non-rendering environments (jsdom, SSR) where all rects are zero.
|
|
591
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
592
|
+
requestAnimationFrame(() => {
|
|
593
|
+
const inRealBrowser = document.documentElement.getBoundingClientRect().height > 0;
|
|
594
|
+
if (inRealBrowser && this.iframe && this.iframe.getBoundingClientRect().height === 0) {
|
|
595
|
+
console.warn(`[OzElement] "${this.elementType}" mounted but has zero height. ` +
|
|
596
|
+
`Check that the container has a visible height (not overflow:hidden or height:0).`);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
364
600
|
break;
|
|
365
601
|
}
|
|
366
602
|
case 'OZ_CHANGE':
|
|
@@ -387,7 +623,12 @@ class OzElement {
|
|
|
387
623
|
break;
|
|
388
624
|
case 'OZ_RESIZE':
|
|
389
625
|
if (this.iframe) {
|
|
390
|
-
|
|
626
|
+
// Clamp to a sensible range to prevent a rogue or compromised frame
|
|
627
|
+
// from zeroing out the input or stretching the layout.
|
|
628
|
+
const h = typeof msg.height === 'number' ? msg.height : 0;
|
|
629
|
+
if (h > 0 && h <= 300) {
|
|
630
|
+
this.iframe.style.height = `${h}px`;
|
|
631
|
+
}
|
|
391
632
|
}
|
|
392
633
|
break;
|
|
393
634
|
}
|
|
@@ -395,8 +636,16 @@ class OzElement {
|
|
|
395
636
|
// ─── Internal ────────────────────────────────────────────────────────────
|
|
396
637
|
emit(event, payload) {
|
|
397
638
|
const list = this.handlers.get(event);
|
|
398
|
-
if (list)
|
|
399
|
-
|
|
639
|
+
if (!list)
|
|
640
|
+
return;
|
|
641
|
+
[...list].forEach(fn => {
|
|
642
|
+
try {
|
|
643
|
+
fn(payload);
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
console.error(`[OzElement] Unhandled error in '${event}' listener:`, err);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
400
649
|
}
|
|
401
650
|
post(data) {
|
|
402
651
|
const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
|
|
@@ -411,135 +660,18 @@ class OzElement {
|
|
|
411
660
|
var _a;
|
|
412
661
|
(_a = this._frameWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin);
|
|
413
662
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
* - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
|
|
423
|
-
*
|
|
424
|
-
* Error keys in normalizeCardSaleError are taken directly from checkout's
|
|
425
|
-
* errorMapping.ts so the same error strings produce the same user-facing copy.
|
|
426
|
-
*/
|
|
427
|
-
class OzError extends Error {
|
|
428
|
-
constructor(message, raw, errorCode) {
|
|
429
|
-
super(message);
|
|
430
|
-
this.name = 'OzError';
|
|
431
|
-
this.raw = raw !== null && raw !== void 0 ? raw : message;
|
|
432
|
-
this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
|
|
433
|
-
this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
/** Shared patterns that apply to both card and bank vault errors. */
|
|
437
|
-
function normalizeCommonVaultError(msg) {
|
|
438
|
-
if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
|
|
439
|
-
return 'Authentication failed. Check your vault API key configuration.';
|
|
440
|
-
}
|
|
441
|
-
if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
|
|
442
|
-
return 'A network error occurred. Please check your connection and try again.';
|
|
443
|
-
}
|
|
444
|
-
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
445
|
-
return 'The request timed out. Please try again.';
|
|
446
|
-
}
|
|
447
|
-
if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
|
|
448
|
-
return 'A server error occurred. Please try again shortly.';
|
|
449
|
-
}
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Maps a raw vault /tokenize error string to a user-facing message for card flows.
|
|
454
|
-
* Falls back to the original string if no pattern matches.
|
|
455
|
-
*/
|
|
456
|
-
function normalizeVaultError(raw) {
|
|
457
|
-
const msg = raw.toLowerCase();
|
|
458
|
-
if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
|
|
459
|
-
return 'The card number is invalid. Please check and try again.';
|
|
460
|
-
}
|
|
461
|
-
if (msg.includes('expir')) {
|
|
462
|
-
return 'The card expiration date is invalid or the card has expired.';
|
|
463
|
-
}
|
|
464
|
-
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
465
|
-
return 'The CVV code is invalid. Please check and try again.';
|
|
466
|
-
}
|
|
467
|
-
if (msg.includes('insufficient') || msg.includes('funds')) {
|
|
468
|
-
return 'Your card has insufficient funds. Please use a different card.';
|
|
469
|
-
}
|
|
470
|
-
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
471
|
-
return 'Your card was declined. Please try a different card or contact your bank.';
|
|
472
|
-
}
|
|
473
|
-
const common = normalizeCommonVaultError(msg);
|
|
474
|
-
if (common)
|
|
475
|
-
return common;
|
|
476
|
-
return raw;
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
|
|
480
|
-
* Uses bank-specific pattern matching so card-specific messages are never shown for
|
|
481
|
-
* bank errors. Falls back to the original string if no pattern matches.
|
|
482
|
-
*/
|
|
483
|
-
function normalizeBankVaultError(raw) {
|
|
484
|
-
const msg = raw.toLowerCase();
|
|
485
|
-
if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
|
|
486
|
-
return 'The bank account number is invalid. Please check and try again.';
|
|
487
|
-
}
|
|
488
|
-
if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
|
|
489
|
-
return 'The routing number is invalid. Please check and try again.';
|
|
490
|
-
}
|
|
491
|
-
const common = normalizeCommonVaultError(msg);
|
|
492
|
-
if (common)
|
|
493
|
-
return common;
|
|
494
|
-
return raw;
|
|
495
|
-
}
|
|
496
|
-
// ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
|
|
497
|
-
const CARD_SALE_ERROR_MAP = [
|
|
498
|
-
['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
|
|
499
|
-
['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
|
|
500
|
-
['Card expired', 'Your card has expired. Please use a different card.'],
|
|
501
|
-
['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
|
|
502
|
-
['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
|
|
503
|
-
['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
504
|
-
['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
505
|
-
['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
|
|
506
|
-
['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
|
|
507
|
-
['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
|
|
508
|
-
['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
|
|
509
|
-
['API key not found', 'Authentication failed. Please refresh the page.'],
|
|
510
|
-
['Access token expired', 'Your session has expired. Please refresh the page.'],
|
|
511
|
-
['Access token is invalid', 'Authentication failed. Please refresh the page.'],
|
|
512
|
-
['Unauthorized', 'Authentication failed. Please refresh the page.'],
|
|
513
|
-
['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
|
|
514
|
-
['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
|
|
515
|
-
['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
|
|
516
|
-
['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
|
|
517
|
-
['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
|
|
518
|
-
['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
|
|
519
|
-
['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
|
|
520
|
-
];
|
|
521
|
-
/**
|
|
522
|
-
* Maps a raw Ozura Pay API cardSale error string to a user-facing message.
|
|
523
|
-
*
|
|
524
|
-
* Uses the exact same error key table as checkout's `getUserFriendlyError()` in
|
|
525
|
-
* errorMapping.ts so both surfaces produce identical copy for the same errors.
|
|
526
|
-
*
|
|
527
|
-
* Falls back to the original string when it's under 100 characters, or to a
|
|
528
|
-
* generic message for long/opaque server errors — matching checkout's fallback
|
|
529
|
-
* behaviour exactly.
|
|
530
|
-
*/
|
|
531
|
-
function normalizeCardSaleError(raw) {
|
|
532
|
-
if (!raw)
|
|
533
|
-
return 'Payment processing failed. Please try again.';
|
|
534
|
-
for (const [key, message] of CARD_SALE_ERROR_MAP) {
|
|
535
|
-
if (raw.toLowerCase().includes(key.toLowerCase())) {
|
|
536
|
-
return message;
|
|
663
|
+
/** Posts a message with transferable objects (e.g. MessagePort). Bypasses the
|
|
664
|
+
* pending-message queue — only call when the frame is already ready. */
|
|
665
|
+
sendWithTransfer(data, transfer) {
|
|
666
|
+
if (this._destroyed)
|
|
667
|
+
return;
|
|
668
|
+
if (!this._frameWindow) {
|
|
669
|
+
console.error('[OzElement] sendWithTransfer called before frame window is available — port will not be transferred. This is a bug.');
|
|
670
|
+
return;
|
|
537
671
|
}
|
|
672
|
+
const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
|
|
673
|
+
this._frameWindow.postMessage(msg, this.frameOrigin, transfer);
|
|
538
674
|
}
|
|
539
|
-
// Checkout fallback: pass through short errors, genericise long ones
|
|
540
|
-
if (raw.length < 100)
|
|
541
|
-
return raw;
|
|
542
|
-
return 'Payment processing failed. Please try again or contact support.';
|
|
543
675
|
}
|
|
544
676
|
|
|
545
677
|
/**
|
|
@@ -591,6 +723,14 @@ const US_STATES = {
|
|
|
591
723
|
'south dakota': 'SD', tennessee: 'TN', texas: 'TX', utah: 'UT',
|
|
592
724
|
vermont: 'VT', virginia: 'VA', washington: 'WA', 'west virginia': 'WV',
|
|
593
725
|
wisconsin: 'WI', wyoming: 'WY',
|
|
726
|
+
// US territories
|
|
727
|
+
'puerto rico': 'PR', guam: 'GU', 'virgin islands': 'VI',
|
|
728
|
+
'us virgin islands': 'VI', 'u.s. virgin islands': 'VI',
|
|
729
|
+
'american samoa': 'AS', 'northern mariana islands': 'MP',
|
|
730
|
+
'commonwealth of the northern mariana islands': 'MP',
|
|
731
|
+
// Military / diplomatic addresses
|
|
732
|
+
'armed forces europe': 'AE', 'armed forces pacific': 'AP',
|
|
733
|
+
'armed forces americas': 'AA',
|
|
594
734
|
};
|
|
595
735
|
const US_ABBREVS = new Set(Object.values(US_STATES));
|
|
596
736
|
const CA_PROVINCES = {
|
|
@@ -699,8 +839,15 @@ function validateBilling(billing) {
|
|
|
699
839
|
errors.push('billing.address.line2 must be 1–50 characters if provided');
|
|
700
840
|
if (!isValidBillingField(city))
|
|
701
841
|
errors.push('billing.address.city must be 1–50 characters');
|
|
702
|
-
if (!isValidBillingField(state))
|
|
842
|
+
if (!isValidBillingField(state)) {
|
|
703
843
|
errors.push('billing.address.state must be 1–50 characters');
|
|
844
|
+
}
|
|
845
|
+
else if (country === 'US' && !US_ABBREVS.has(state)) {
|
|
846
|
+
errors.push(`billing.address.state "${state}" is not a recognized US state or territory abbreviation (e.g. "CA", "NY", "PR")`);
|
|
847
|
+
}
|
|
848
|
+
else if (country === 'CA' && !CA_ABBREVS.has(state)) {
|
|
849
|
+
errors.push(`billing.address.state "${state}" is not a recognized Canadian province or territory abbreviation (e.g. "ON", "BC", "QC")`);
|
|
850
|
+
}
|
|
704
851
|
// cardSale backend uses strict enum validation on country — must be exactly 2 uppercase letters
|
|
705
852
|
if (!/^[A-Z]{2}$/.test(country)) {
|
|
706
853
|
errors.push('billing.address.country must be a 2-letter ISO 3166-1 alpha-2 code (e.g. "US", "CA", "GB")');
|
|
@@ -724,7 +871,13 @@ function validateBilling(billing) {
|
|
|
724
871
|
return { valid: errors.length === 0, errors, normalized };
|
|
725
872
|
}
|
|
726
873
|
|
|
727
|
-
|
|
874
|
+
function isCardMetadata(v) {
|
|
875
|
+
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
876
|
+
}
|
|
877
|
+
function isBankAccountMetadata(v) {
|
|
878
|
+
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
879
|
+
}
|
|
880
|
+
const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
728
881
|
/**
|
|
729
882
|
* The main entry point for OzElements. Creates and manages iframe-based
|
|
730
883
|
* card input elements that keep raw card data isolated from the merchant page.
|
|
@@ -735,7 +888,7 @@ const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.
|
|
|
735
888
|
* const vault = await OzVault.create({
|
|
736
889
|
* pubKey: 'pk_live_...',
|
|
737
890
|
* fetchWaxKey: async (sessionId) => {
|
|
738
|
-
* // Call your backend — which calls ozura.mintWaxKey() from
|
|
891
|
+
* // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
|
|
739
892
|
* const { waxKey } = await fetch('/api/mint-wax', {
|
|
740
893
|
* method: 'POST',
|
|
741
894
|
* body: JSON.stringify({ sessionId }),
|
|
@@ -754,9 +907,10 @@ class OzVault {
|
|
|
754
907
|
* Internal constructor — use `OzVault.create()` instead.
|
|
755
908
|
* The constructor mounts the tokenizer iframe immediately so it can start
|
|
756
909
|
* loading in parallel while `fetchWaxKey` is being awaited.
|
|
910
|
+
* @internal
|
|
757
911
|
*/
|
|
758
912
|
constructor(options, waxKey, tokenizationSessionId) {
|
|
759
|
-
var _a, _b;
|
|
913
|
+
var _a, _b, _c;
|
|
760
914
|
this.elements = new Map();
|
|
761
915
|
this.elementsByType = new Map();
|
|
762
916
|
this.bankElementsByType = new Map();
|
|
@@ -769,8 +923,16 @@ class OzVault {
|
|
|
769
923
|
this.tokenizerReady = false;
|
|
770
924
|
this._tokenizing = null;
|
|
771
925
|
this._destroyed = false;
|
|
926
|
+
// Tracks successful tokenizations against the per-key call budget so the SDK
|
|
927
|
+
// can proactively refresh the wax key after it has been consumed rather than
|
|
928
|
+
// waiting for the next createToken() call to fail.
|
|
929
|
+
this._tokenizeSuccessCount = 0;
|
|
772
930
|
this._pendingMount = null;
|
|
931
|
+
this._storedFetchWaxKey = null;
|
|
932
|
+
this._waxRefreshing = null;
|
|
773
933
|
this.loadErrorTimeoutId = null;
|
|
934
|
+
// Proactive wax refresh on visibility restore after long idle
|
|
935
|
+
this._hiddenAt = null;
|
|
774
936
|
this.waxKey = waxKey;
|
|
775
937
|
this.tokenizationSessionId = tokenizationSessionId;
|
|
776
938
|
this.pubKey = options.pubKey;
|
|
@@ -778,13 +940,15 @@ class OzVault {
|
|
|
778
940
|
this.frameOrigin = new URL(this.frameBaseUrl).origin;
|
|
779
941
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
780
942
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
781
|
-
this.vaultId = `vault-${
|
|
782
|
-
this.
|
|
943
|
+
this.vaultId = `vault-${uuid()}`;
|
|
944
|
+
this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
|
|
783
945
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
784
946
|
window.addEventListener('message', this.boundHandleMessage);
|
|
947
|
+
this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
|
|
948
|
+
document.addEventListener('visibilitychange', this.boundHandleVisibility);
|
|
785
949
|
this.mountTokenizerFrame();
|
|
786
950
|
if (options.onLoadError) {
|
|
787
|
-
const timeout = (
|
|
951
|
+
const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
|
|
788
952
|
this.loadErrorTimeoutId = setTimeout(() => {
|
|
789
953
|
this.loadErrorTimeoutId = null;
|
|
790
954
|
if (!this._destroyed && !this.tokenizerReady) {
|
|
@@ -792,6 +956,8 @@ class OzVault {
|
|
|
792
956
|
}
|
|
793
957
|
}, timeout);
|
|
794
958
|
}
|
|
959
|
+
this._onWaxRefresh = options.onWaxRefresh;
|
|
960
|
+
this._onReady = options.onReady;
|
|
795
961
|
}
|
|
796
962
|
/**
|
|
797
963
|
* Creates and returns a ready `OzVault` instance.
|
|
@@ -808,45 +974,95 @@ class OzVault {
|
|
|
808
974
|
* The returned vault is ready to create elements immediately. `createToken()`
|
|
809
975
|
* additionally requires `vault.isReady` (tokenizer iframe loaded).
|
|
810
976
|
*
|
|
811
|
-
* @throws {OzError} if `fetchWaxKey` throws or returns an empty string.
|
|
977
|
+
* @throws {OzError} if `fetchWaxKey` throws, returns a non-string value, or returns an empty/whitespace-only string.
|
|
812
978
|
*/
|
|
813
|
-
static async create(options) {
|
|
979
|
+
static async create(options, signal) {
|
|
814
980
|
if (!options.pubKey || !options.pubKey.trim()) {
|
|
815
981
|
throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
|
|
816
982
|
}
|
|
817
983
|
if (typeof options.fetchWaxKey !== 'function') {
|
|
818
984
|
throw new OzError('fetchWaxKey must be a function. See OzVault.create() docs for the expected signature.');
|
|
819
985
|
}
|
|
820
|
-
const tokenizationSessionId =
|
|
986
|
+
const tokenizationSessionId = uuid();
|
|
821
987
|
// Construct the vault immediately — this mounts the tokenizer iframe so it
|
|
822
988
|
// starts loading while fetchWaxKey is in flight. The waxKey field starts
|
|
823
989
|
// empty and is set below before create() returns.
|
|
824
990
|
const vault = new OzVault(options, '', tokenizationSessionId);
|
|
991
|
+
// If the caller provides an AbortSignal (e.g. React useEffect cleanup),
|
|
992
|
+
// destroy the vault immediately on abort so the tokenizer iframe and message
|
|
993
|
+
// listener are removed synchronously rather than waiting for fetchWaxKey to
|
|
994
|
+
// settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
995
|
+
const onAbort = () => vault.destroy();
|
|
996
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
|
|
825
997
|
let waxKey;
|
|
826
998
|
try {
|
|
827
999
|
waxKey = await options.fetchWaxKey(tokenizationSessionId);
|
|
828
1000
|
}
|
|
829
1001
|
catch (err) {
|
|
1002
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
830
1003
|
vault.destroy();
|
|
1004
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
1005
|
+
throw new OzError('OzVault.create() was cancelled.');
|
|
1006
|
+
// Preserve errorCode/retryable from OzError (e.g. timeout/network from createFetchWaxKey)
|
|
1007
|
+
// so callers can distinguish transient failures from config errors.
|
|
1008
|
+
const originalCode = err instanceof OzError ? err.errorCode : undefined;
|
|
831
1009
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
832
|
-
throw new OzError(`fetchWaxKey threw an error: ${msg}
|
|
1010
|
+
throw new OzError(`fetchWaxKey threw an error: ${msg}`, undefined, originalCode);
|
|
833
1011
|
}
|
|
834
|
-
|
|
1012
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
1013
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
1014
|
+
vault.destroy();
|
|
1015
|
+
throw new OzError('OzVault.create() was cancelled.');
|
|
1016
|
+
}
|
|
1017
|
+
if (typeof waxKey !== 'string' || !waxKey.trim()) {
|
|
835
1018
|
vault.destroy();
|
|
836
1019
|
throw new OzError('fetchWaxKey must return a non-empty wax key string. Check your mint endpoint.');
|
|
837
1020
|
}
|
|
838
1021
|
// Static methods can access private fields of instances of the same class.
|
|
839
1022
|
vault.waxKey = waxKey;
|
|
1023
|
+
vault._storedFetchWaxKey = options.fetchWaxKey;
|
|
1024
|
+
// If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
|
|
1025
|
+
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
1026
|
+
// so the tokenizer has the key stored before any createToken() call.
|
|
1027
|
+
if (vault.tokenizerReady) {
|
|
1028
|
+
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
1029
|
+
}
|
|
840
1030
|
return vault;
|
|
841
1031
|
}
|
|
842
1032
|
/**
|
|
843
1033
|
* True once the hidden tokenizer iframe has loaded and signalled ready.
|
|
844
1034
|
* Use this to gate the pay button when building custom UIs without React.
|
|
845
1035
|
* React consumers should use the `ready` value returned by `useOzElements()`.
|
|
1036
|
+
*
|
|
1037
|
+
* Once `true`, remains `true` for the lifetime of this vault instance.
|
|
1038
|
+
* It only reverts to `false` after `vault.destroy()` is called, at which
|
|
1039
|
+
* point the vault is unusable and a new instance must be created.
|
|
1040
|
+
*
|
|
1041
|
+
* @remarks
|
|
1042
|
+
* This tracks **tokenizer readiness only** — it says nothing about whether
|
|
1043
|
+
* the individual element iframes (card number, CVV, etc.) have loaded.
|
|
1044
|
+
* A vault can be `isReady === true` while elements are still mounting.
|
|
1045
|
+
* To gate a submit button correctly in vanilla JS, wait for every element's
|
|
1046
|
+
* `'ready'` event in addition to this flag. In React, use the `ready` value
|
|
1047
|
+
* from `useOzElements()` instead, which combines both checks automatically.
|
|
846
1048
|
*/
|
|
847
1049
|
get isReady() {
|
|
848
1050
|
return this.tokenizerReady;
|
|
849
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Number of successful tokenize calls made against the current wax key.
|
|
1054
|
+
*
|
|
1055
|
+
* Resets to `0` each time the wax key is refreshed (proactively or reactively).
|
|
1056
|
+
* Useful in vanilla JS integrations to display "attempts remaining" UI.
|
|
1057
|
+
* In React, use `tokenizeCount` from `useOzElements()` instead.
|
|
1058
|
+
*
|
|
1059
|
+
* @example
|
|
1060
|
+
* const remaining = 3 - vault.tokenizeCount;
|
|
1061
|
+
* payButton.textContent = remaining > 0 ? `Pay (${remaining} attempts left)` : 'Pay';
|
|
1062
|
+
*/
|
|
1063
|
+
get tokenizeCount() {
|
|
1064
|
+
return this._tokenizeSuccessCount;
|
|
1065
|
+
}
|
|
850
1066
|
/**
|
|
851
1067
|
* Creates a new OzElement of the given type. Call `.mount(selector)` on the
|
|
852
1068
|
* returned element to attach it to the DOM.
|
|
@@ -881,6 +1097,9 @@ class OzVault {
|
|
|
881
1097
|
if (this._destroyed) {
|
|
882
1098
|
throw new OzError('Cannot create elements on a destroyed vault. Call await OzVault.create() to get a new instance.');
|
|
883
1099
|
}
|
|
1100
|
+
if (this._tokenizing) {
|
|
1101
|
+
throw new OzError('Cannot create or replace elements while a tokenization is in progress. Wait for the active createToken() / createBankToken() call to settle first.');
|
|
1102
|
+
}
|
|
884
1103
|
const existing = typeMap.get(type);
|
|
885
1104
|
if (existing) {
|
|
886
1105
|
this.elements.delete(existing.frameId);
|
|
@@ -928,10 +1147,10 @@ class OzVault {
|
|
|
928
1147
|
throw new OzError('lastName is required for bank account tokenization.');
|
|
929
1148
|
}
|
|
930
1149
|
if (options.firstName.trim().length > 50) {
|
|
931
|
-
throw new OzError('firstName must be 50 characters or fewer');
|
|
1150
|
+
throw new OzError('firstName must be 50 characters or fewer.');
|
|
932
1151
|
}
|
|
933
1152
|
if (options.lastName.trim().length > 50) {
|
|
934
|
-
throw new OzError('lastName must be 50 characters or fewer');
|
|
1153
|
+
throw new OzError('lastName must be 50 characters or fewer.');
|
|
935
1154
|
}
|
|
936
1155
|
const accountEl = this.bankElementsByType.get('accountNumber');
|
|
937
1156
|
const routingEl = this.bankElementsByType.get('routingNumber');
|
|
@@ -948,31 +1167,47 @@ class OzVault {
|
|
|
948
1167
|
}
|
|
949
1168
|
const readyBankElements = [accountEl, routingEl];
|
|
950
1169
|
this._tokenizing = 'bank';
|
|
951
|
-
const requestId = `req-${
|
|
1170
|
+
const requestId = `req-${uuid()}`;
|
|
952
1171
|
return new Promise((resolve, reject) => {
|
|
953
1172
|
const cleanup = () => { this._tokenizing = null; };
|
|
954
1173
|
this.bankTokenizeResolvers.set(requestId, {
|
|
955
1174
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
956
1175
|
reject: (e) => { cleanup(); reject(e); },
|
|
957
|
-
});
|
|
958
|
-
this.sendToTokenizer({
|
|
959
|
-
type: 'OZ_BANK_TOKENIZE',
|
|
960
|
-
requestId,
|
|
961
|
-
waxKey: this.waxKey,
|
|
962
|
-
tokenizationSessionId: this.tokenizationSessionId,
|
|
963
|
-
pubKey: this.pubKey,
|
|
964
1176
|
firstName: options.firstName.trim(),
|
|
965
1177
|
lastName: options.lastName.trim(),
|
|
1178
|
+
readyElements: readyBankElements,
|
|
966
1179
|
fieldCount: readyBankElements.length,
|
|
967
1180
|
});
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1181
|
+
try {
|
|
1182
|
+
const bankChannels = readyBankElements.map(() => new MessageChannel());
|
|
1183
|
+
this.sendToTokenizer({
|
|
1184
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
1185
|
+
requestId,
|
|
1186
|
+
waxKey: this.waxKey,
|
|
1187
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1188
|
+
pubKey: this.pubKey,
|
|
1189
|
+
firstName: options.firstName.trim(),
|
|
1190
|
+
lastName: options.lastName.trim(),
|
|
1191
|
+
fieldCount: readyBankElements.length,
|
|
1192
|
+
}, bankChannels.map(ch => ch.port1));
|
|
1193
|
+
readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
|
|
1194
|
+
const bankTimeoutId = setTimeout(() => {
|
|
1195
|
+
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
1196
|
+
this.bankTokenizeResolvers.delete(requestId);
|
|
1197
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1198
|
+
cleanup();
|
|
1199
|
+
reject(new OzError('Bank tokenization timed out after 30 seconds', undefined, 'timeout'));
|
|
1200
|
+
}
|
|
1201
|
+
}, 30000);
|
|
1202
|
+
const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
|
|
1203
|
+
if (bankPendingEntry)
|
|
1204
|
+
bankPendingEntry.timeoutId = bankTimeoutId;
|
|
1205
|
+
}
|
|
1206
|
+
catch (err) {
|
|
1207
|
+
this.bankTokenizeResolvers.delete(requestId);
|
|
1208
|
+
cleanup();
|
|
1209
|
+
reject(err instanceof OzError ? err : new OzError('Bank tokenization failed to start'));
|
|
1210
|
+
}
|
|
976
1211
|
});
|
|
977
1212
|
}
|
|
978
1213
|
/**
|
|
@@ -994,29 +1229,33 @@ class OzVault {
|
|
|
994
1229
|
? 'A bank tokenization is already in progress. Wait for it to complete before calling createToken().'
|
|
995
1230
|
: 'A card tokenization is already in progress. Wait for it to complete before calling createToken() again.');
|
|
996
1231
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
//
|
|
1000
|
-
// created
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1232
|
+
// All synchronous validation runs before _tokenizing is set so these throws
|
|
1233
|
+
// need no manual cleanup — _tokenizing is still null when they fire.
|
|
1234
|
+
// Collect all card elements that have been created (mounted or not) so we
|
|
1235
|
+
// can require every created field to be ready before proceeding. An element
|
|
1236
|
+
// that was created but whose iframe hasn't loaded yet will never send
|
|
1237
|
+
// OZ_FIELD_VALUE — tokenizing without it would silently submit an empty or
|
|
1238
|
+
// incomplete card and produce an opaque vault rejection.
|
|
1239
|
+
// Bank elements (accountNumber/routingNumber) share this.elements but live
|
|
1240
|
+
// in elementsByType under bank-only keys, so they are excluded by the Set.
|
|
1241
|
+
const cardElements = [...this.elementsByType.values()];
|
|
1242
|
+
if (cardElements.length === 0) {
|
|
1243
|
+
throw new OzError('No card elements have been created. Call vault.createElement() for each field before calling createToken.');
|
|
1244
|
+
}
|
|
1245
|
+
const notReady = cardElements.filter(el => !el.isReady);
|
|
1246
|
+
if (notReady.length > 0) {
|
|
1247
|
+
throw new OzError(`Not all elements are ready. Wait for all fields to finish loading before calling createToken. ` +
|
|
1248
|
+
`Not yet ready: ${notReady.map(el => el.type).join(', ')}.`);
|
|
1010
1249
|
}
|
|
1250
|
+
const readyElements = cardElements;
|
|
1011
1251
|
// Validate billing details if provided and extract firstName/lastName for the vault payload.
|
|
1012
1252
|
// billing.firstName/lastName take precedence over the deprecated top-level params.
|
|
1013
1253
|
let normalizedBilling;
|
|
1014
|
-
let firstName = (_a = options.firstName) !== null && _a !== void 0 ? _a : '';
|
|
1015
|
-
let lastName = (_b = options.lastName) !== null && _b !== void 0 ? _b : '';
|
|
1254
|
+
let firstName = ((_a = options.firstName) !== null && _a !== void 0 ? _a : '').trim();
|
|
1255
|
+
let lastName = ((_b = options.lastName) !== null && _b !== void 0 ? _b : '').trim();
|
|
1016
1256
|
if (options.billing) {
|
|
1017
1257
|
const result = validateBilling(options.billing);
|
|
1018
1258
|
if (!result.valid) {
|
|
1019
|
-
this._tokenizing = null;
|
|
1020
1259
|
throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
|
|
1021
1260
|
}
|
|
1022
1261
|
normalizedBilling = result.normalized;
|
|
@@ -1025,41 +1264,57 @@ class OzVault {
|
|
|
1025
1264
|
}
|
|
1026
1265
|
else {
|
|
1027
1266
|
if (firstName.length > 50) {
|
|
1028
|
-
|
|
1029
|
-
throw new OzError('firstName must be 50 characters or fewer');
|
|
1267
|
+
throw new OzError('firstName must be 50 characters or fewer.');
|
|
1030
1268
|
}
|
|
1031
1269
|
if (lastName.length > 50) {
|
|
1032
|
-
|
|
1033
|
-
throw new OzError('lastName must be 50 characters or fewer');
|
|
1270
|
+
throw new OzError('lastName must be 50 characters or fewer.');
|
|
1034
1271
|
}
|
|
1035
1272
|
}
|
|
1273
|
+
this._tokenizing = 'card';
|
|
1274
|
+
const requestId = `req-${uuid()}`;
|
|
1036
1275
|
return new Promise((resolve, reject) => {
|
|
1037
1276
|
const cleanup = () => { this._tokenizing = null; };
|
|
1038
1277
|
this.tokenizeResolvers.set(requestId, {
|
|
1039
1278
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
1040
1279
|
reject: (e) => { cleanup(); reject(e); },
|
|
1041
1280
|
billing: normalizedBilling,
|
|
1042
|
-
});
|
|
1043
|
-
// Tell tokenizer frame to expect N field values, then tokenize
|
|
1044
|
-
this.sendToTokenizer({
|
|
1045
|
-
type: 'OZ_TOKENIZE',
|
|
1046
|
-
requestId,
|
|
1047
|
-
waxKey: this.waxKey,
|
|
1048
|
-
tokenizationSessionId: this.tokenizationSessionId,
|
|
1049
|
-
pubKey: this.pubKey,
|
|
1050
1281
|
firstName,
|
|
1051
1282
|
lastName,
|
|
1283
|
+
readyElements,
|
|
1052
1284
|
fieldCount: readyElements.length,
|
|
1053
1285
|
});
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1286
|
+
try {
|
|
1287
|
+
// Tell tokenizer frame to expect N field values, then tokenize
|
|
1288
|
+
const cardChannels = readyElements.map(() => new MessageChannel());
|
|
1289
|
+
this.sendToTokenizer({
|
|
1290
|
+
type: 'OZ_TOKENIZE',
|
|
1291
|
+
requestId,
|
|
1292
|
+
waxKey: this.waxKey,
|
|
1293
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1294
|
+
pubKey: this.pubKey,
|
|
1295
|
+
firstName,
|
|
1296
|
+
lastName,
|
|
1297
|
+
fieldCount: readyElements.length,
|
|
1298
|
+
}, cardChannels.map(ch => ch.port1));
|
|
1299
|
+
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1300
|
+
readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
|
|
1301
|
+
const cardTimeoutId = setTimeout(() => {
|
|
1302
|
+
if (this.tokenizeResolvers.has(requestId)) {
|
|
1303
|
+
this.tokenizeResolvers.delete(requestId);
|
|
1304
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1305
|
+
cleanup();
|
|
1306
|
+
reject(new OzError('Tokenization timed out after 30 seconds', undefined, 'timeout'));
|
|
1307
|
+
}
|
|
1308
|
+
}, 30000);
|
|
1309
|
+
const cardPendingEntry = this.tokenizeResolvers.get(requestId);
|
|
1310
|
+
if (cardPendingEntry)
|
|
1311
|
+
cardPendingEntry.timeoutId = cardTimeoutId;
|
|
1312
|
+
}
|
|
1313
|
+
catch (err) {
|
|
1314
|
+
this.tokenizeResolvers.delete(requestId);
|
|
1315
|
+
cleanup();
|
|
1316
|
+
reject(err instanceof OzError ? err : new OzError('Tokenization failed to start'));
|
|
1317
|
+
}
|
|
1063
1318
|
});
|
|
1064
1319
|
}
|
|
1065
1320
|
/**
|
|
@@ -1073,6 +1328,7 @@ class OzVault {
|
|
|
1073
1328
|
return;
|
|
1074
1329
|
this._destroyed = true;
|
|
1075
1330
|
window.removeEventListener('message', this.boundHandleMessage);
|
|
1331
|
+
document.removeEventListener('visibilitychange', this.boundHandleVisibility);
|
|
1076
1332
|
if (this._pendingMount) {
|
|
1077
1333
|
document.removeEventListener('DOMContentLoaded', this._pendingMount);
|
|
1078
1334
|
this._pendingMount = null;
|
|
@@ -1083,11 +1339,17 @@ class OzVault {
|
|
|
1083
1339
|
}
|
|
1084
1340
|
// Reject any pending tokenize promises so callers aren't left hanging
|
|
1085
1341
|
this._tokenizing = null;
|
|
1086
|
-
this.tokenizeResolvers.forEach(({ reject }) => {
|
|
1342
|
+
this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1343
|
+
if (timeoutId != null)
|
|
1344
|
+
clearTimeout(timeoutId);
|
|
1345
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1087
1346
|
reject(new OzError('Vault destroyed before tokenization completed.'));
|
|
1088
1347
|
});
|
|
1089
1348
|
this.tokenizeResolvers.clear();
|
|
1090
|
-
this.bankTokenizeResolvers.forEach(({ reject }) => {
|
|
1349
|
+
this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1350
|
+
if (timeoutId != null)
|
|
1351
|
+
clearTimeout(timeoutId);
|
|
1352
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1091
1353
|
reject(new OzError('Vault destroyed before bank tokenization completed.'));
|
|
1092
1354
|
});
|
|
1093
1355
|
this.bankTokenizeResolvers.clear();
|
|
@@ -1096,17 +1358,49 @@ class OzVault {
|
|
|
1096
1358
|
this.elementsByType.clear();
|
|
1097
1359
|
this.bankElementsByType.clear();
|
|
1098
1360
|
this.completionState.clear();
|
|
1361
|
+
this._tokenizeSuccessCount = 0;
|
|
1099
1362
|
(_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.remove();
|
|
1100
1363
|
this.tokenizerFrame = null;
|
|
1101
1364
|
this.tokenizerWindow = null;
|
|
1102
1365
|
this.tokenizerReady = false;
|
|
1103
1366
|
}
|
|
1104
1367
|
// ─── Private ─────────────────────────────────────────────────────────────
|
|
1368
|
+
/**
|
|
1369
|
+
* Proactively re-mints the wax key when the page becomes visible again after
|
|
1370
|
+
* a long idle period. Wax keys have a fixed TTL (~30 minutes); a user who
|
|
1371
|
+
* leaves the tab in the background and returns could have an expired key.
|
|
1372
|
+
* Rather than waiting for a failed tokenization to trigger the reactive
|
|
1373
|
+
* refresh path, this pre-empts the failure when the vault is idle.
|
|
1374
|
+
*
|
|
1375
|
+
* Threshold: 20 minutes hidden. Chosen to be comfortably inside the ~30m TTL
|
|
1376
|
+
* while avoiding spurious refreshes for brief tab-switches.
|
|
1377
|
+
*/
|
|
1378
|
+
handleVisibilityChange() {
|
|
1379
|
+
if (this._destroyed)
|
|
1380
|
+
return;
|
|
1381
|
+
const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
|
|
1382
|
+
if (document.hidden) {
|
|
1383
|
+
this._hiddenAt = Date.now();
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
if (this._hiddenAt !== null &&
|
|
1387
|
+
Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
|
|
1388
|
+
this._storedFetchWaxKey &&
|
|
1389
|
+
!this._tokenizing &&
|
|
1390
|
+
!this._waxRefreshing) {
|
|
1391
|
+
this.refreshWaxKey().catch((err) => {
|
|
1392
|
+
// Proactive refresh failure is non-fatal — the reactive path on the
|
|
1393
|
+
// next createToken() call will handle it, including the auth retry.
|
|
1394
|
+
console.warn('[OzVault] Proactive wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
this._hiddenAt = null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1105
1400
|
mountTokenizerFrame() {
|
|
1106
1401
|
const mount = () => {
|
|
1107
1402
|
this._pendingMount = null;
|
|
1108
1403
|
const iframe = document.createElement('iframe');
|
|
1109
|
-
iframe.name = this.tokenizerName;
|
|
1110
1404
|
iframe.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;';
|
|
1111
1405
|
iframe.setAttribute('aria-hidden', 'true');
|
|
1112
1406
|
iframe.tabIndex = -1;
|
|
@@ -1125,6 +1419,8 @@ class OzVault {
|
|
|
1125
1419
|
}
|
|
1126
1420
|
handleMessage(event) {
|
|
1127
1421
|
var _a;
|
|
1422
|
+
if (this._destroyed)
|
|
1423
|
+
return;
|
|
1128
1424
|
// Only accept messages from our frame origin (defense in depth; prevents
|
|
1129
1425
|
// arbitrary pages from injecting OZ_TOKEN_RESULT etc. with a guessed vaultId).
|
|
1130
1426
|
if (event.origin !== this.frameOrigin)
|
|
@@ -1142,14 +1438,17 @@ class OzVault {
|
|
|
1142
1438
|
if (frameId) {
|
|
1143
1439
|
const el = this.elements.get(frameId);
|
|
1144
1440
|
if (el) {
|
|
1441
|
+
// Reset stale completion state when an element iframe re-loads (e.g. after
|
|
1442
|
+
// unmount() + mount() in a SPA). Without this, wasComplete stays true from
|
|
1443
|
+
// the previous session and justCompleted never fires, breaking auto-advance.
|
|
1444
|
+
if (msg.type === 'OZ_FRAME_READY') {
|
|
1445
|
+
this.completionState.set(frameId, false);
|
|
1446
|
+
}
|
|
1145
1447
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1146
1448
|
if (msg.type === 'OZ_CHANGE') {
|
|
1147
1449
|
this.handleElementChange(msg, el);
|
|
1148
1450
|
}
|
|
1149
1451
|
el.handleMessage(msg);
|
|
1150
|
-
if (msg.type === 'OZ_FRAME_READY' && this.tokenizerReady) {
|
|
1151
|
-
el.setTokenizerName(this.tokenizerName);
|
|
1152
|
-
}
|
|
1153
1452
|
}
|
|
1154
1453
|
}
|
|
1155
1454
|
}
|
|
@@ -1163,7 +1462,7 @@ class OzVault {
|
|
|
1163
1462
|
const complete = msg.complete;
|
|
1164
1463
|
const valid = msg.valid;
|
|
1165
1464
|
const wasComplete = (_a = this.completionState.get(el.frameId)) !== null && _a !== void 0 ? _a : false;
|
|
1166
|
-
this.completionState.set(el.frameId, complete);
|
|
1465
|
+
this.completionState.set(el.frameId, complete && valid);
|
|
1167
1466
|
// Require valid too — avoids advancing at 13 digits for unknown-brand cards
|
|
1168
1467
|
// where isComplete() fires before the user has finished typing.
|
|
1169
1468
|
const justCompleted = complete && valid && !wasComplete;
|
|
@@ -1186,7 +1485,7 @@ class OzVault {
|
|
|
1186
1485
|
}
|
|
1187
1486
|
}
|
|
1188
1487
|
handleTokenizerMessage(msg) {
|
|
1189
|
-
var _a, _b;
|
|
1488
|
+
var _a, _b, _c;
|
|
1190
1489
|
switch (msg.type) {
|
|
1191
1490
|
case 'OZ_FRAME_READY':
|
|
1192
1491
|
this.tokenizerReady = true;
|
|
@@ -1195,53 +1494,341 @@ class OzVault {
|
|
|
1195
1494
|
this.loadErrorTimeoutId = null;
|
|
1196
1495
|
}
|
|
1197
1496
|
this.tokenizerWindow = (_b = (_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
|
|
1198
|
-
|
|
1199
|
-
|
|
1497
|
+
// Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
|
|
1498
|
+
// If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
|
|
1499
|
+
// sent again from create() once the key is available.
|
|
1500
|
+
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1501
|
+
(_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
1200
1502
|
break;
|
|
1201
1503
|
case 'OZ_TOKEN_RESULT': {
|
|
1504
|
+
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
1505
|
+
console.error('[OzVault] OZ_TOKEN_RESULT missing requestId — discarding message.');
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1202
1508
|
const pending = this.tokenizeResolvers.get(msg.requestId);
|
|
1203
1509
|
if (pending) {
|
|
1204
1510
|
this.tokenizeResolvers.delete(msg.requestId);
|
|
1205
|
-
|
|
1206
|
-
|
|
1511
|
+
if (pending.timeoutId != null)
|
|
1512
|
+
clearTimeout(pending.timeoutId);
|
|
1513
|
+
const token = msg.token;
|
|
1514
|
+
if (typeof token !== 'string' || !token) {
|
|
1515
|
+
pending.reject(new OzError('Vault returned a token result with a missing or empty token — possible vault API change.', undefined, 'server'));
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
const card = isCardMetadata(msg.card) ? msg.card : undefined;
|
|
1519
|
+
const cvcSession = typeof msg.cvcSession === 'string' && msg.cvcSession ? msg.cvcSession : undefined;
|
|
1520
|
+
if (!cvcSession) {
|
|
1521
|
+
pending.reject(new OzError('Vault returned a token result without a cvcSession — possible vault misconfiguration or API change.', undefined, 'server'));
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1524
|
+
if (!card) {
|
|
1525
|
+
pending.reject(new OzError('Vault returned a token result without card metadata — possible vault API change.', undefined, 'server'));
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
pending.resolve(Object.assign({ token,
|
|
1529
|
+
cvcSession,
|
|
1530
|
+
card }, (pending.billing ? { billing: pending.billing } : {})));
|
|
1531
|
+
// Increment the per-key success counter and proactively refresh once
|
|
1532
|
+
// the budget is exhausted so the next createToken() call uses a fresh
|
|
1533
|
+
// key without waiting for a vault rejection.
|
|
1534
|
+
this._tokenizeSuccessCount++;
|
|
1535
|
+
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1536
|
+
this.refreshWaxKey().catch((err) => {
|
|
1537
|
+
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1207
1540
|
}
|
|
1208
1541
|
break;
|
|
1209
1542
|
}
|
|
1210
1543
|
case 'OZ_TOKEN_ERROR': {
|
|
1544
|
+
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
1545
|
+
console.error('[OzVault] OZ_TOKEN_ERROR missing requestId — discarding message.');
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
const raw = typeof msg.error === 'string' ? msg.error : '';
|
|
1549
|
+
const errorCode = isOzErrorCode(msg.errorCode) ? msg.errorCode : 'unknown';
|
|
1211
1550
|
const pending = this.tokenizeResolvers.get(msg.requestId);
|
|
1212
1551
|
if (pending) {
|
|
1213
1552
|
this.tokenizeResolvers.delete(msg.requestId);
|
|
1214
|
-
|
|
1215
|
-
|
|
1553
|
+
if (pending.timeoutId != null)
|
|
1554
|
+
clearTimeout(pending.timeoutId);
|
|
1555
|
+
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1556
|
+
// already retried for this request, transparently re-mint and retry.
|
|
1557
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
|
|
1558
|
+
this.refreshWaxKey().then(() => {
|
|
1559
|
+
if (this._destroyed) {
|
|
1560
|
+
pending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const newRequestId = `req-${uuid()}`;
|
|
1564
|
+
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1565
|
+
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
1566
|
+
try {
|
|
1567
|
+
const retryCardChannels = pending.readyElements.map(() => new MessageChannel());
|
|
1568
|
+
this.sendToTokenizer({
|
|
1569
|
+
type: 'OZ_TOKENIZE',
|
|
1570
|
+
requestId: newRequestId,
|
|
1571
|
+
waxKey: this.waxKey,
|
|
1572
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1573
|
+
pubKey: this.pubKey,
|
|
1574
|
+
firstName: pending.firstName,
|
|
1575
|
+
lastName: pending.lastName,
|
|
1576
|
+
fieldCount: pending.fieldCount,
|
|
1577
|
+
}, retryCardChannels.map(ch => ch.port1));
|
|
1578
|
+
pending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryCardChannels[i].port2));
|
|
1579
|
+
const retryCardTimeoutId = setTimeout(() => {
|
|
1580
|
+
if (this.tokenizeResolvers.has(newRequestId)) {
|
|
1581
|
+
this.tokenizeResolvers.delete(newRequestId);
|
|
1582
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
1583
|
+
pending.reject(new OzError('Tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
1584
|
+
}
|
|
1585
|
+
}, 30000);
|
|
1586
|
+
const retryCardEntry = this.tokenizeResolvers.get(newRequestId);
|
|
1587
|
+
if (retryCardEntry)
|
|
1588
|
+
retryCardEntry.timeoutId = retryCardTimeoutId;
|
|
1589
|
+
}
|
|
1590
|
+
catch (setupErr) {
|
|
1591
|
+
this.tokenizeResolvers.delete(newRequestId);
|
|
1592
|
+
pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
|
|
1593
|
+
}
|
|
1594
|
+
}).catch((refreshErr) => {
|
|
1595
|
+
const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1596
|
+
pending.reject(new OzError(msg, undefined, 'auth'));
|
|
1597
|
+
});
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1216
1600
|
pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
|
|
1217
1601
|
}
|
|
1218
1602
|
// Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR
|
|
1219
1603
|
const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1220
1604
|
if (bankPending) {
|
|
1221
1605
|
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1222
|
-
|
|
1223
|
-
|
|
1606
|
+
if (bankPending.timeoutId != null)
|
|
1607
|
+
clearTimeout(bankPending.timeoutId);
|
|
1608
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1609
|
+
this.refreshWaxKey().then(() => {
|
|
1610
|
+
if (this._destroyed) {
|
|
1611
|
+
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const newRequestId = `req-${uuid()}`;
|
|
1615
|
+
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1616
|
+
try {
|
|
1617
|
+
const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
|
|
1618
|
+
this.sendToTokenizer({
|
|
1619
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
1620
|
+
requestId: newRequestId,
|
|
1621
|
+
waxKey: this.waxKey,
|
|
1622
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1623
|
+
pubKey: this.pubKey,
|
|
1624
|
+
firstName: bankPending.firstName,
|
|
1625
|
+
lastName: bankPending.lastName,
|
|
1626
|
+
fieldCount: bankPending.fieldCount,
|
|
1627
|
+
}, retryBankChannels.map(ch => ch.port1));
|
|
1628
|
+
bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
|
|
1629
|
+
const retryBankTimeoutId = setTimeout(() => {
|
|
1630
|
+
if (this.bankTokenizeResolvers.has(newRequestId)) {
|
|
1631
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1632
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
1633
|
+
bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
1634
|
+
}
|
|
1635
|
+
}, 30000);
|
|
1636
|
+
const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
|
|
1637
|
+
if (retryBankEntry)
|
|
1638
|
+
retryBankEntry.timeoutId = retryBankTimeoutId;
|
|
1639
|
+
}
|
|
1640
|
+
catch (setupErr) {
|
|
1641
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1642
|
+
bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
|
|
1643
|
+
}
|
|
1644
|
+
}).catch((refreshErr) => {
|
|
1645
|
+
const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1646
|
+
bankPending.reject(new OzError(msg, undefined, 'auth'));
|
|
1647
|
+
});
|
|
1648
|
+
break;
|
|
1649
|
+
}
|
|
1224
1650
|
bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
|
|
1225
1651
|
}
|
|
1226
1652
|
break;
|
|
1227
1653
|
}
|
|
1228
1654
|
case 'OZ_BANK_TOKEN_RESULT': {
|
|
1655
|
+
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
1656
|
+
console.error('[OzVault] OZ_BANK_TOKEN_RESULT missing requestId — discarding message.');
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1229
1659
|
const pending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1230
1660
|
if (pending) {
|
|
1231
1661
|
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1232
|
-
|
|
1233
|
-
|
|
1662
|
+
if (pending.timeoutId != null)
|
|
1663
|
+
clearTimeout(pending.timeoutId);
|
|
1664
|
+
const token = msg.token;
|
|
1665
|
+
if (typeof token !== 'string' || !token) {
|
|
1666
|
+
pending.reject(new OzError('Vault returned a bank token result with a missing or empty token — possible vault API change.', undefined, 'server'));
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
|
|
1670
|
+
pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
|
|
1671
|
+
// Same proactive refresh logic as card tokenization.
|
|
1672
|
+
this._tokenizeSuccessCount++;
|
|
1673
|
+
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1674
|
+
this.refreshWaxKey().catch((err) => {
|
|
1675
|
+
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1234
1678
|
}
|
|
1235
1679
|
break;
|
|
1236
1680
|
}
|
|
1237
1681
|
}
|
|
1238
1682
|
}
|
|
1239
|
-
|
|
1683
|
+
/**
|
|
1684
|
+
* Returns true when an OZ_TOKEN_ERROR should trigger a wax key refresh.
|
|
1685
|
+
*
|
|
1686
|
+
* Primary path: vault returns 401/403 → errorCode 'auth'.
|
|
1687
|
+
* Defensive path: vault returns 400 → errorCode 'validation', but the raw
|
|
1688
|
+
* message contains wax-key-specific language (consumed, expired, invalid key,
|
|
1689
|
+
* etc.). This avoids a hard dependency on the vault returning a unified HTTP
|
|
1690
|
+
* status for consumed-key vs expired-key failures — both should refresh.
|
|
1691
|
+
*
|
|
1692
|
+
* Deliberately excludes 'network', 'timeout', and 'server' codes (transient
|
|
1693
|
+
* errors are already retried in fetchWithRetry) and 'unknown' (too broad).
|
|
1694
|
+
*/
|
|
1695
|
+
isRefreshableAuthError(errorCode, raw) {
|
|
1696
|
+
if (errorCode === 'auth')
|
|
1697
|
+
return true;
|
|
1698
|
+
if (errorCode === 'validation') {
|
|
1699
|
+
const msg = raw.toLowerCase();
|
|
1700
|
+
// Only treat validation errors as wax-related if the message explicitly
|
|
1701
|
+
// names the wax/tokenization session mechanism. A bare "session" match
|
|
1702
|
+
// was too broad — any message mentioning "session" (e.g. a merchant
|
|
1703
|
+
// session field error) would trigger a spurious re-mint.
|
|
1704
|
+
return (msg.includes('wax') ||
|
|
1705
|
+
msg.includes('expired') ||
|
|
1706
|
+
msg.includes('consumed') ||
|
|
1707
|
+
msg.includes('invalid key') ||
|
|
1708
|
+
msg.includes('key not found') ||
|
|
1709
|
+
msg.includes('tokenization session'));
|
|
1710
|
+
}
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Re-mints the wax key using the stored fetchWaxKey callback and updates
|
|
1715
|
+
* the tokenizer with the new key. Used for transparent auto-refresh when
|
|
1716
|
+
* the vault returns an auth error on tokenization.
|
|
1717
|
+
*
|
|
1718
|
+
* Only one refresh runs at a time — concurrent retries share the same promise.
|
|
1719
|
+
*/
|
|
1720
|
+
refreshWaxKey() {
|
|
1721
|
+
var _a;
|
|
1722
|
+
if (this._waxRefreshing)
|
|
1723
|
+
return this._waxRefreshing;
|
|
1724
|
+
if (!this._storedFetchWaxKey) {
|
|
1725
|
+
return Promise.reject(new OzError('Wax key expired and no fetchWaxKey callback is available for auto-refresh. Call OzVault.create() again to obtain a new vault instance.', undefined, 'auth'));
|
|
1726
|
+
}
|
|
1727
|
+
const newSessionId = uuid();
|
|
1728
|
+
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
1729
|
+
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
1730
|
+
.then(newWaxKey => {
|
|
1731
|
+
if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
|
|
1732
|
+
throw new OzError('fetchWaxKey returned an empty string during auto-refresh.', undefined, 'auth');
|
|
1733
|
+
}
|
|
1734
|
+
if (!this._destroyed) {
|
|
1735
|
+
this.waxKey = newWaxKey;
|
|
1736
|
+
this.tokenizationSessionId = newSessionId;
|
|
1737
|
+
this._tokenizeSuccessCount = 0;
|
|
1738
|
+
}
|
|
1739
|
+
if (!this._destroyed && this.tokenizerReady) {
|
|
1740
|
+
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
1741
|
+
}
|
|
1742
|
+
})
|
|
1743
|
+
.finally(() => {
|
|
1744
|
+
this._waxRefreshing = null;
|
|
1745
|
+
});
|
|
1746
|
+
return this._waxRefreshing;
|
|
1747
|
+
}
|
|
1748
|
+
sendToTokenizer(data, transfer) {
|
|
1240
1749
|
var _a;
|
|
1241
1750
|
const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
|
|
1242
|
-
(_a = this.tokenizerWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin);
|
|
1751
|
+
(_a = this.tokenizerWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin, transfer !== null && transfer !== void 0 ? transfer : []);
|
|
1243
1752
|
}
|
|
1244
1753
|
}
|
|
1245
1754
|
|
|
1246
|
-
|
|
1755
|
+
/**
|
|
1756
|
+
* Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
|
|
1757
|
+
*
|
|
1758
|
+
* Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
|
|
1759
|
+
* Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
|
|
1760
|
+
* error through its normal error path.
|
|
1761
|
+
*
|
|
1762
|
+
* Each call enforces a 10-second per-attempt timeout. On a pure network-level
|
|
1763
|
+
* failure (connection refused, DNS failure, etc.) the call is retried once after
|
|
1764
|
+
* 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
|
|
1765
|
+
* an endpoint misconfiguration or an invalid key, not a transient failure.
|
|
1766
|
+
*
|
|
1767
|
+
* The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
|
|
1768
|
+
* from `@ozura/elements/server`.
|
|
1769
|
+
*
|
|
1770
|
+
* @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
|
|
1771
|
+
*
|
|
1772
|
+
* @example
|
|
1773
|
+
* // Vanilla JS
|
|
1774
|
+
* const vault = await OzVault.create({
|
|
1775
|
+
* pubKey: 'pk_live_...',
|
|
1776
|
+
* fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
|
|
1777
|
+
* });
|
|
1778
|
+
*
|
|
1779
|
+
* @example
|
|
1780
|
+
* // React
|
|
1781
|
+
* <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
|
|
1782
|
+
*/
|
|
1783
|
+
function createFetchWaxKey(mintUrl) {
|
|
1784
|
+
const TIMEOUT_MS = 10000;
|
|
1785
|
+
// Each attempt gets its own AbortController so a timeout on attempt 1 does
|
|
1786
|
+
// not bleed into the retry. Uses AbortController + setTimeout instead of
|
|
1787
|
+
// AbortSignal.timeout() to support environments without that API.
|
|
1788
|
+
const attemptFetch = (sessionId) => {
|
|
1789
|
+
const controller = new AbortController();
|
|
1790
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1791
|
+
return fetch(mintUrl, {
|
|
1792
|
+
method: 'POST',
|
|
1793
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1794
|
+
body: JSON.stringify({ sessionId }),
|
|
1795
|
+
signal: controller.signal,
|
|
1796
|
+
}).finally(() => clearTimeout(timer));
|
|
1797
|
+
};
|
|
1798
|
+
return async (sessionId) => {
|
|
1799
|
+
let res;
|
|
1800
|
+
try {
|
|
1801
|
+
res = await attemptFetch(sessionId);
|
|
1802
|
+
}
|
|
1803
|
+
catch (firstErr) {
|
|
1804
|
+
// Abort/timeout should not be retried — the server received nothing or
|
|
1805
|
+
// we already waited the full timeout duration.
|
|
1806
|
+
if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
|
|
1807
|
+
throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
|
|
1808
|
+
}
|
|
1809
|
+
// Pure network error (offline, DNS, connection refused) — retry once
|
|
1810
|
+
// after a short pause in case of a transient blip.
|
|
1811
|
+
await new Promise(resolve => setTimeout(resolve, 750));
|
|
1812
|
+
try {
|
|
1813
|
+
res = await attemptFetch(sessionId);
|
|
1814
|
+
}
|
|
1815
|
+
catch (retryErr) {
|
|
1816
|
+
const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
|
|
1817
|
+
throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
const data = await res.json().catch(() => ({}));
|
|
1821
|
+
if (!res.ok) {
|
|
1822
|
+
throw new OzError(typeof data.error === 'string' && data.error
|
|
1823
|
+
? data.error
|
|
1824
|
+
: `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
1825
|
+
}
|
|
1826
|
+
if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
|
|
1827
|
+
throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
|
|
1828
|
+
}
|
|
1829
|
+
return data.waxKey;
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
export { OzElement, OzError, OzVault, createFetchWaxKey, normalizeBankVaultError, normalizeCardSaleError, normalizeVaultError };
|
|
1247
1834
|
//# sourceMappingURL=oz-elements.esm.js.map
|