@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/react/index.cjs.js
CHANGED
|
@@ -133,6 +133,15 @@ function mergeStyleConfigs(a, b) {
|
|
|
133
133
|
* Resolution order: theme defaults → variable overrides.
|
|
134
134
|
* The returned config is then used as the "base appearance" that
|
|
135
135
|
* per-element `style` overrides merge on top of.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* - `appearance: undefined` → no styles injected (element iframes use their
|
|
139
|
+
* own minimal built-in defaults).
|
|
140
|
+
* - `appearance: {}` or `appearance: { variables: {...} }` without an explicit
|
|
141
|
+
* `theme` → the `'default'` theme is used as the base. Omitting `theme`
|
|
142
|
+
* does NOT mean "no theme" — it means `theme: 'default'`. To opt out of
|
|
143
|
+
* the preset themes entirely, use per-element `style` overrides without
|
|
144
|
+
* passing an `appearance` prop at all.
|
|
136
145
|
*/
|
|
137
146
|
function resolveAppearance(appearance) {
|
|
138
147
|
var _a, _b;
|
|
@@ -158,7 +167,117 @@ function mergeAppearanceWithElementStyle(appearance, elementStyle) {
|
|
|
158
167
|
return mergeStyleConfigs(appearance, elementStyle);
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
|
|
170
|
+
/**
|
|
171
|
+
* errors.ts — error types and normalisation for OzElements.
|
|
172
|
+
*
|
|
173
|
+
* Three normalisation functions:
|
|
174
|
+
* - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
|
|
175
|
+
* - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
|
|
176
|
+
* - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
|
|
177
|
+
*
|
|
178
|
+
* Error keys in normalizeCardSaleError are taken directly from checkout's
|
|
179
|
+
* errorMapping.ts so the same error strings produce the same user-facing copy.
|
|
180
|
+
*/
|
|
181
|
+
const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
|
|
182
|
+
/** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
|
|
183
|
+
function isOzErrorCode(value) {
|
|
184
|
+
return typeof value === 'string' && OZ_ERROR_CODES.has(value);
|
|
185
|
+
}
|
|
186
|
+
class OzError extends Error {
|
|
187
|
+
constructor(message, raw, errorCode) {
|
|
188
|
+
super(message);
|
|
189
|
+
this.name = 'OzError';
|
|
190
|
+
this.raw = raw !== null && raw !== void 0 ? raw : message;
|
|
191
|
+
this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
|
|
192
|
+
this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Shared patterns that apply to both card and bank vault errors. */
|
|
196
|
+
function normalizeCommonVaultError(msg) {
|
|
197
|
+
if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
|
|
198
|
+
return 'Authentication failed. Check your vault API key configuration.';
|
|
199
|
+
}
|
|
200
|
+
if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
|
|
201
|
+
return 'A network error occurred. Please check your connection and try again.';
|
|
202
|
+
}
|
|
203
|
+
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
204
|
+
return 'The request timed out. Please try again.';
|
|
205
|
+
}
|
|
206
|
+
if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
|
|
207
|
+
return 'A server error occurred. Please try again shortly.';
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Maps a raw vault /tokenize error string to a user-facing message for card flows.
|
|
213
|
+
* Falls back to the original string if no pattern matches.
|
|
214
|
+
*/
|
|
215
|
+
function normalizeVaultError(raw) {
|
|
216
|
+
const msg = raw.toLowerCase();
|
|
217
|
+
if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
|
|
218
|
+
return 'The card number is invalid. Please check and try again.';
|
|
219
|
+
}
|
|
220
|
+
if (msg.includes('expir')) {
|
|
221
|
+
return 'The card expiration date is invalid or the card has expired.';
|
|
222
|
+
}
|
|
223
|
+
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
224
|
+
return 'The CVV code is invalid. Please check and try again.';
|
|
225
|
+
}
|
|
226
|
+
if (msg.includes('insufficient') || msg.includes('funds')) {
|
|
227
|
+
return 'Your card has insufficient funds. Please use a different card.';
|
|
228
|
+
}
|
|
229
|
+
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
230
|
+
return 'Your card was declined. Please try a different card or contact your bank.';
|
|
231
|
+
}
|
|
232
|
+
const common = normalizeCommonVaultError(msg);
|
|
233
|
+
if (common)
|
|
234
|
+
return common;
|
|
235
|
+
return raw;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
|
|
239
|
+
* Uses bank-specific pattern matching so card-specific messages are never shown for
|
|
240
|
+
* bank errors. Falls back to the original string if no pattern matches.
|
|
241
|
+
*/
|
|
242
|
+
function normalizeBankVaultError(raw) {
|
|
243
|
+
const msg = raw.toLowerCase();
|
|
244
|
+
if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
|
|
245
|
+
return 'The bank account number is invalid. Please check and try again.';
|
|
246
|
+
}
|
|
247
|
+
if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
|
|
248
|
+
return 'The routing number is invalid. Please check and try again.';
|
|
249
|
+
}
|
|
250
|
+
const common = normalizeCommonVaultError(msg);
|
|
251
|
+
if (common)
|
|
252
|
+
return common;
|
|
253
|
+
return raw;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Generates a RFC 4122 v4 UUID.
|
|
258
|
+
*
|
|
259
|
+
* Uses `crypto.randomUUID()` when available (Chrome 92+, Firefox 95+,
|
|
260
|
+
* Safari 15.4+, Node 14.17+) and falls back to `crypto.getRandomValues()`
|
|
261
|
+
* for older environments (Safari 14, some embedded WebViews, older Node).
|
|
262
|
+
*
|
|
263
|
+
* Both paths use the same CSPRNG — the difference is only in API surface.
|
|
264
|
+
*
|
|
265
|
+
* @internal
|
|
266
|
+
*/
|
|
267
|
+
function uuid() {
|
|
268
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
269
|
+
return crypto.randomUUID();
|
|
270
|
+
}
|
|
271
|
+
// Fallback: build UUID v4 from random bytes
|
|
272
|
+
const bytes = new Uint8Array(16);
|
|
273
|
+
crypto.getRandomValues(bytes);
|
|
274
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
275
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant RFC 4122
|
|
276
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
277
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
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;
|
|
162
281
|
const CSS_BREAKOUT = /[{};<>]/;
|
|
163
282
|
const MAX_CSS_VALUE_LEN = 200;
|
|
164
283
|
function sanitizeStyleObj(obj) {
|
|
@@ -190,6 +309,11 @@ function sanitizeStyles(style) {
|
|
|
190
309
|
function sanitizeOptions(options) {
|
|
191
310
|
var _a;
|
|
192
311
|
const result = Object.assign(Object.assign({}, options), { placeholder: (_a = options.placeholder) === null || _a === void 0 ? void 0 : _a.slice(0, 100) });
|
|
312
|
+
// Coerce to boolean so a string "false" (truthy in JS) does not accidentally
|
|
313
|
+
// disable the input when the SDK is consumed from plain JavaScript.
|
|
314
|
+
if (options.disabled !== undefined) {
|
|
315
|
+
result.disabled = Boolean(options.disabled);
|
|
316
|
+
}
|
|
193
317
|
// Only set style when provided; omitting it avoids clobbering existing style
|
|
194
318
|
// when merging (e.g. update({ placeholder: 'new' }) must not overwrite style with undefined).
|
|
195
319
|
if (options.style !== undefined) {
|
|
@@ -217,7 +341,7 @@ class OzElement {
|
|
|
217
341
|
this.frameOrigin = new URL(frameBaseUrl).origin;
|
|
218
342
|
this.fonts = fonts;
|
|
219
343
|
this.appearanceStyle = appearanceStyle;
|
|
220
|
-
this.frameId = `oz-${elementType}-${
|
|
344
|
+
this.frameId = `oz-${elementType}-${uuid()}`;
|
|
221
345
|
}
|
|
222
346
|
/** The element type this proxy represents. */
|
|
223
347
|
get type() {
|
|
@@ -235,22 +359,27 @@ class OzElement {
|
|
|
235
359
|
mount(target) {
|
|
236
360
|
var _a;
|
|
237
361
|
if (this._destroyed)
|
|
238
|
-
throw new
|
|
362
|
+
throw new OzError('Cannot mount a destroyed element.');
|
|
239
363
|
if (this.iframe)
|
|
240
364
|
this.unmount();
|
|
241
365
|
const container = typeof target === 'string'
|
|
242
366
|
? document.querySelector(target)
|
|
243
367
|
: target;
|
|
244
368
|
if (!container)
|
|
245
|
-
throw new
|
|
246
|
-
? `
|
|
247
|
-
:
|
|
369
|
+
throw new OzError(typeof target === 'string'
|
|
370
|
+
? `Mount target not found — no element matches "${target}"`
|
|
371
|
+
: 'Mount target not found — the provided HTMLElement is null or undefined');
|
|
248
372
|
const iframe = document.createElement('iframe');
|
|
249
373
|
iframe.setAttribute('frameborder', '0');
|
|
250
374
|
iframe.setAttribute('scrolling', 'no');
|
|
251
375
|
iframe.setAttribute('allowtransparency', 'true');
|
|
252
376
|
iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
|
|
253
377
|
iframe.title = `Secure ${this.elementType} input`;
|
|
378
|
+
// Note: the `sandbox` attribute is intentionally NOT set. Field values are
|
|
379
|
+
// delivered to the tokenizer iframe via a MessageChannel port (transferred
|
|
380
|
+
// in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
|
|
381
|
+
// The security boundary is the vault URL hardcoded at build time and the
|
|
382
|
+
// origin checks on every postMessage, not the sandbox flag.
|
|
254
383
|
// Use hash instead of query string — survives clean-URL redirects from static servers.
|
|
255
384
|
// parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
|
|
256
385
|
const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
@@ -266,6 +395,11 @@ class OzElement {
|
|
|
266
395
|
}
|
|
267
396
|
}, timeout);
|
|
268
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Subscribe to an element event. Returns `this` for chaining.
|
|
400
|
+
* @param event - Event name: `'change'`, `'focus'`, `'blur'`, `'ready'`, or `'loaderror'`.
|
|
401
|
+
* @param callback - Handler invoked with the event payload.
|
|
402
|
+
*/
|
|
269
403
|
on(event, callback) {
|
|
270
404
|
if (this._destroyed)
|
|
271
405
|
return this;
|
|
@@ -274,6 +408,10 @@ class OzElement {
|
|
|
274
408
|
this.handlers.get(event).push(callback);
|
|
275
409
|
return this;
|
|
276
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Remove a previously registered event handler.
|
|
413
|
+
* Has no effect if the handler is not registered.
|
|
414
|
+
*/
|
|
277
415
|
off(event, callback) {
|
|
278
416
|
const list = this.handlers.get(event);
|
|
279
417
|
if (list) {
|
|
@@ -283,6 +421,10 @@ class OzElement {
|
|
|
283
421
|
}
|
|
284
422
|
return this;
|
|
285
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Subscribe to an event for a single invocation. The handler is automatically
|
|
426
|
+
* removed after it fires once.
|
|
427
|
+
*/
|
|
286
428
|
once(event, callback) {
|
|
287
429
|
const wrapper = (payload) => {
|
|
288
430
|
this.off(event, wrapper);
|
|
@@ -290,13 +432,24 @@ class OzElement {
|
|
|
290
432
|
};
|
|
291
433
|
return this.on(event, wrapper);
|
|
292
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Dynamically update element options (placeholder, style, etc.) without
|
|
437
|
+
* re-mounting the iframe. Only the provided keys are merged; omitted keys
|
|
438
|
+
* retain their current values.
|
|
439
|
+
*/
|
|
293
440
|
update(options) {
|
|
294
441
|
if (this._destroyed)
|
|
295
442
|
return;
|
|
296
443
|
const safe = sanitizeOptions(options);
|
|
444
|
+
// Re-merge vault appearance when style is updated so focus/invalid/complete/
|
|
445
|
+
// placeholder buckets from the theme are not stripped by a partial style object.
|
|
446
|
+
if (safe.style !== undefined) {
|
|
447
|
+
safe.style = mergeAppearanceWithElementStyle(this.appearanceStyle, safe.style);
|
|
448
|
+
}
|
|
297
449
|
this.options = Object.assign(Object.assign({}, this.options), safe);
|
|
298
450
|
this.post({ type: 'OZ_UPDATE', options: safe });
|
|
299
451
|
}
|
|
452
|
+
/** Clear the current field value without removing the element from the DOM. */
|
|
300
453
|
clear() {
|
|
301
454
|
if (this._destroyed)
|
|
302
455
|
return;
|
|
@@ -309,6 +462,8 @@ class OzElement {
|
|
|
309
462
|
*/
|
|
310
463
|
unmount() {
|
|
311
464
|
var _a;
|
|
465
|
+
if (this._destroyed)
|
|
466
|
+
return;
|
|
312
467
|
if (this._loadTimer) {
|
|
313
468
|
clearTimeout(this._loadTimer);
|
|
314
469
|
this._loadTimer = null;
|
|
@@ -341,18 +496,31 @@ class OzElement {
|
|
|
341
496
|
this._destroyed = true;
|
|
342
497
|
}
|
|
343
498
|
// ─── Called by OzVault ───────────────────────────────────────────────────
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Sends OZ_BEGIN_COLLECT to the element iframe, transferring `port` so the
|
|
501
|
+
* iframe can post its field value directly to the tokenizer without going
|
|
502
|
+
* through the merchant page (no named-window lookup required).
|
|
503
|
+
* @internal
|
|
504
|
+
*/
|
|
505
|
+
beginCollect(requestId, port) {
|
|
506
|
+
if (this._destroyed)
|
|
507
|
+
return;
|
|
508
|
+
this.sendWithTransfer({ type: 'OZ_BEGIN_COLLECT', requestId }, [port]);
|
|
349
509
|
}
|
|
350
|
-
/**
|
|
510
|
+
/**
|
|
511
|
+
* Tell a CVV element how many digits to expect. Called automatically when card brand changes.
|
|
512
|
+
* @internal
|
|
513
|
+
*/
|
|
351
514
|
setCvvLength(length) {
|
|
515
|
+
if (this._destroyed)
|
|
516
|
+
return;
|
|
352
517
|
this.post({ type: 'OZ_SET_CVV_LENGTH', length });
|
|
353
518
|
}
|
|
519
|
+
/** @internal */
|
|
354
520
|
handleMessage(msg) {
|
|
355
521
|
var _a, _b;
|
|
522
|
+
if (this._destroyed)
|
|
523
|
+
return;
|
|
356
524
|
switch (msg.type) {
|
|
357
525
|
case 'OZ_FRAME_READY': {
|
|
358
526
|
this._ready = true;
|
|
@@ -366,6 +534,19 @@ class OzElement {
|
|
|
366
534
|
this.pendingMessages.forEach(m => this.send(m));
|
|
367
535
|
this.pendingMessages = [];
|
|
368
536
|
this.emit('ready', undefined);
|
|
537
|
+
// Warn if the mount container collapses to zero height — the input will
|
|
538
|
+
// be invisible but functional, which is hard to debug. Check after one
|
|
539
|
+
// animation frame so the browser has completed layout. Guard against
|
|
540
|
+
// non-rendering environments (jsdom, SSR) where all rects are zero.
|
|
541
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
542
|
+
requestAnimationFrame(() => {
|
|
543
|
+
const inRealBrowser = document.documentElement.getBoundingClientRect().height > 0;
|
|
544
|
+
if (inRealBrowser && this.iframe && this.iframe.getBoundingClientRect().height === 0) {
|
|
545
|
+
console.warn(`[OzElement] "${this.elementType}" mounted but has zero height. ` +
|
|
546
|
+
`Check that the container has a visible height (not overflow:hidden or height:0).`);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
369
550
|
break;
|
|
370
551
|
}
|
|
371
552
|
case 'OZ_CHANGE':
|
|
@@ -392,7 +573,12 @@ class OzElement {
|
|
|
392
573
|
break;
|
|
393
574
|
case 'OZ_RESIZE':
|
|
394
575
|
if (this.iframe) {
|
|
395
|
-
|
|
576
|
+
// Clamp to a sensible range to prevent a rogue or compromised frame
|
|
577
|
+
// from zeroing out the input or stretching the layout.
|
|
578
|
+
const h = typeof msg.height === 'number' ? msg.height : 0;
|
|
579
|
+
if (h > 0 && h <= 300) {
|
|
580
|
+
this.iframe.style.height = `${h}px`;
|
|
581
|
+
}
|
|
396
582
|
}
|
|
397
583
|
break;
|
|
398
584
|
}
|
|
@@ -400,8 +586,16 @@ class OzElement {
|
|
|
400
586
|
// ─── Internal ────────────────────────────────────────────────────────────
|
|
401
587
|
emit(event, payload) {
|
|
402
588
|
const list = this.handlers.get(event);
|
|
403
|
-
if (list)
|
|
404
|
-
|
|
589
|
+
if (!list)
|
|
590
|
+
return;
|
|
591
|
+
[...list].forEach(fn => {
|
|
592
|
+
try {
|
|
593
|
+
fn(payload);
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
console.error(`[OzElement] Unhandled error in '${event}' listener:`, err);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
405
599
|
}
|
|
406
600
|
post(data) {
|
|
407
601
|
const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
|
|
@@ -416,87 +610,18 @@ class OzElement {
|
|
|
416
610
|
var _a;
|
|
417
611
|
(_a = this._frameWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin);
|
|
418
612
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
* errorMapping.ts so the same error strings produce the same user-facing copy.
|
|
431
|
-
*/
|
|
432
|
-
class OzError extends Error {
|
|
433
|
-
constructor(message, raw, errorCode) {
|
|
434
|
-
super(message);
|
|
435
|
-
this.name = 'OzError';
|
|
436
|
-
this.raw = raw !== null && raw !== void 0 ? raw : message;
|
|
437
|
-
this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
|
|
438
|
-
this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
/** Shared patterns that apply to both card and bank vault errors. */
|
|
442
|
-
function normalizeCommonVaultError(msg) {
|
|
443
|
-
if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
|
|
444
|
-
return 'Authentication failed. Check your vault API key configuration.';
|
|
445
|
-
}
|
|
446
|
-
if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
|
|
447
|
-
return 'A network error occurred. Please check your connection and try again.';
|
|
448
|
-
}
|
|
449
|
-
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
450
|
-
return 'The request timed out. Please try again.';
|
|
451
|
-
}
|
|
452
|
-
if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
|
|
453
|
-
return 'A server error occurred. Please try again shortly.';
|
|
454
|
-
}
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Maps a raw vault /tokenize error string to a user-facing message for card flows.
|
|
459
|
-
* Falls back to the original string if no pattern matches.
|
|
460
|
-
*/
|
|
461
|
-
function normalizeVaultError(raw) {
|
|
462
|
-
const msg = raw.toLowerCase();
|
|
463
|
-
if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
|
|
464
|
-
return 'The card number is invalid. Please check and try again.';
|
|
465
|
-
}
|
|
466
|
-
if (msg.includes('expir')) {
|
|
467
|
-
return 'The card expiration date is invalid or the card has expired.';
|
|
468
|
-
}
|
|
469
|
-
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
470
|
-
return 'The CVV code is invalid. Please check and try again.';
|
|
471
|
-
}
|
|
472
|
-
if (msg.includes('insufficient') || msg.includes('funds')) {
|
|
473
|
-
return 'Your card has insufficient funds. Please use a different card.';
|
|
474
|
-
}
|
|
475
|
-
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
476
|
-
return 'Your card was declined. Please try a different card or contact your bank.';
|
|
477
|
-
}
|
|
478
|
-
const common = normalizeCommonVaultError(msg);
|
|
479
|
-
if (common)
|
|
480
|
-
return common;
|
|
481
|
-
return raw;
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
|
|
485
|
-
* Uses bank-specific pattern matching so card-specific messages are never shown for
|
|
486
|
-
* bank errors. Falls back to the original string if no pattern matches.
|
|
487
|
-
*/
|
|
488
|
-
function normalizeBankVaultError(raw) {
|
|
489
|
-
const msg = raw.toLowerCase();
|
|
490
|
-
if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
|
|
491
|
-
return 'The bank account number is invalid. Please check and try again.';
|
|
492
|
-
}
|
|
493
|
-
if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
|
|
494
|
-
return 'The routing number is invalid. Please check and try again.';
|
|
613
|
+
/** Posts a message with transferable objects (e.g. MessagePort). Bypasses the
|
|
614
|
+
* pending-message queue — only call when the frame is already ready. */
|
|
615
|
+
sendWithTransfer(data, transfer) {
|
|
616
|
+
if (this._destroyed)
|
|
617
|
+
return;
|
|
618
|
+
if (!this._frameWindow) {
|
|
619
|
+
console.error('[OzElement] sendWithTransfer called before frame window is available — port will not be transferred. This is a bug.');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
|
|
623
|
+
this._frameWindow.postMessage(msg, this.frameOrigin, transfer);
|
|
495
624
|
}
|
|
496
|
-
const common = normalizeCommonVaultError(msg);
|
|
497
|
-
if (common)
|
|
498
|
-
return common;
|
|
499
|
-
return raw;
|
|
500
625
|
}
|
|
501
626
|
|
|
502
627
|
/**
|
|
@@ -548,6 +673,14 @@ const US_STATES = {
|
|
|
548
673
|
'south dakota': 'SD', tennessee: 'TN', texas: 'TX', utah: 'UT',
|
|
549
674
|
vermont: 'VT', virginia: 'VA', washington: 'WA', 'west virginia': 'WV',
|
|
550
675
|
wisconsin: 'WI', wyoming: 'WY',
|
|
676
|
+
// US territories
|
|
677
|
+
'puerto rico': 'PR', guam: 'GU', 'virgin islands': 'VI',
|
|
678
|
+
'us virgin islands': 'VI', 'u.s. virgin islands': 'VI',
|
|
679
|
+
'american samoa': 'AS', 'northern mariana islands': 'MP',
|
|
680
|
+
'commonwealth of the northern mariana islands': 'MP',
|
|
681
|
+
// Military / diplomatic addresses
|
|
682
|
+
'armed forces europe': 'AE', 'armed forces pacific': 'AP',
|
|
683
|
+
'armed forces americas': 'AA',
|
|
551
684
|
};
|
|
552
685
|
const US_ABBREVS = new Set(Object.values(US_STATES));
|
|
553
686
|
const CA_PROVINCES = {
|
|
@@ -656,8 +789,15 @@ function validateBilling(billing) {
|
|
|
656
789
|
errors.push('billing.address.line2 must be 1–50 characters if provided');
|
|
657
790
|
if (!isValidBillingField(city))
|
|
658
791
|
errors.push('billing.address.city must be 1–50 characters');
|
|
659
|
-
if (!isValidBillingField(state))
|
|
792
|
+
if (!isValidBillingField(state)) {
|
|
660
793
|
errors.push('billing.address.state must be 1–50 characters');
|
|
794
|
+
}
|
|
795
|
+
else if (country === 'US' && !US_ABBREVS.has(state)) {
|
|
796
|
+
errors.push(`billing.address.state "${state}" is not a recognized US state or territory abbreviation (e.g. "CA", "NY", "PR")`);
|
|
797
|
+
}
|
|
798
|
+
else if (country === 'CA' && !CA_ABBREVS.has(state)) {
|
|
799
|
+
errors.push(`billing.address.state "${state}" is not a recognized Canadian province or territory abbreviation (e.g. "ON", "BC", "QC")`);
|
|
800
|
+
}
|
|
661
801
|
// cardSale backend uses strict enum validation on country — must be exactly 2 uppercase letters
|
|
662
802
|
if (!/^[A-Z]{2}$/.test(country)) {
|
|
663
803
|
errors.push('billing.address.country must be a 2-letter ISO 3166-1 alpha-2 code (e.g. "US", "CA", "GB")');
|
|
@@ -681,7 +821,13 @@ function validateBilling(billing) {
|
|
|
681
821
|
return { valid: errors.length === 0, errors, normalized };
|
|
682
822
|
}
|
|
683
823
|
|
|
684
|
-
|
|
824
|
+
function isCardMetadata(v) {
|
|
825
|
+
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
826
|
+
}
|
|
827
|
+
function isBankAccountMetadata(v) {
|
|
828
|
+
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
829
|
+
}
|
|
830
|
+
const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
|
|
685
831
|
/**
|
|
686
832
|
* The main entry point for OzElements. Creates and manages iframe-based
|
|
687
833
|
* card input elements that keep raw card data isolated from the merchant page.
|
|
@@ -692,7 +838,7 @@ const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.
|
|
|
692
838
|
* const vault = await OzVault.create({
|
|
693
839
|
* pubKey: 'pk_live_...',
|
|
694
840
|
* fetchWaxKey: async (sessionId) => {
|
|
695
|
-
* // Call your backend — which calls ozura.mintWaxKey() from
|
|
841
|
+
* // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
|
|
696
842
|
* const { waxKey } = await fetch('/api/mint-wax', {
|
|
697
843
|
* method: 'POST',
|
|
698
844
|
* body: JSON.stringify({ sessionId }),
|
|
@@ -711,9 +857,10 @@ class OzVault {
|
|
|
711
857
|
* Internal constructor — use `OzVault.create()` instead.
|
|
712
858
|
* The constructor mounts the tokenizer iframe immediately so it can start
|
|
713
859
|
* loading in parallel while `fetchWaxKey` is being awaited.
|
|
860
|
+
* @internal
|
|
714
861
|
*/
|
|
715
862
|
constructor(options, waxKey, tokenizationSessionId) {
|
|
716
|
-
var _a, _b;
|
|
863
|
+
var _a, _b, _c;
|
|
717
864
|
this.elements = new Map();
|
|
718
865
|
this.elementsByType = new Map();
|
|
719
866
|
this.bankElementsByType = new Map();
|
|
@@ -726,8 +873,16 @@ class OzVault {
|
|
|
726
873
|
this.tokenizerReady = false;
|
|
727
874
|
this._tokenizing = null;
|
|
728
875
|
this._destroyed = false;
|
|
876
|
+
// Tracks successful tokenizations against the per-key call budget so the SDK
|
|
877
|
+
// can proactively refresh the wax key after it has been consumed rather than
|
|
878
|
+
// waiting for the next createToken() call to fail.
|
|
879
|
+
this._tokenizeSuccessCount = 0;
|
|
729
880
|
this._pendingMount = null;
|
|
881
|
+
this._storedFetchWaxKey = null;
|
|
882
|
+
this._waxRefreshing = null;
|
|
730
883
|
this.loadErrorTimeoutId = null;
|
|
884
|
+
// Proactive wax refresh on visibility restore after long idle
|
|
885
|
+
this._hiddenAt = null;
|
|
731
886
|
this.waxKey = waxKey;
|
|
732
887
|
this.tokenizationSessionId = tokenizationSessionId;
|
|
733
888
|
this.pubKey = options.pubKey;
|
|
@@ -735,13 +890,15 @@ class OzVault {
|
|
|
735
890
|
this.frameOrigin = new URL(this.frameBaseUrl).origin;
|
|
736
891
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
737
892
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
738
|
-
this.vaultId = `vault-${
|
|
739
|
-
this.
|
|
893
|
+
this.vaultId = `vault-${uuid()}`;
|
|
894
|
+
this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
|
|
740
895
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
741
896
|
window.addEventListener('message', this.boundHandleMessage);
|
|
897
|
+
this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
|
|
898
|
+
document.addEventListener('visibilitychange', this.boundHandleVisibility);
|
|
742
899
|
this.mountTokenizerFrame();
|
|
743
900
|
if (options.onLoadError) {
|
|
744
|
-
const timeout = (
|
|
901
|
+
const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
|
|
745
902
|
this.loadErrorTimeoutId = setTimeout(() => {
|
|
746
903
|
this.loadErrorTimeoutId = null;
|
|
747
904
|
if (!this._destroyed && !this.tokenizerReady) {
|
|
@@ -749,6 +906,8 @@ class OzVault {
|
|
|
749
906
|
}
|
|
750
907
|
}, timeout);
|
|
751
908
|
}
|
|
909
|
+
this._onWaxRefresh = options.onWaxRefresh;
|
|
910
|
+
this._onReady = options.onReady;
|
|
752
911
|
}
|
|
753
912
|
/**
|
|
754
913
|
* Creates and returns a ready `OzVault` instance.
|
|
@@ -765,45 +924,95 @@ class OzVault {
|
|
|
765
924
|
* The returned vault is ready to create elements immediately. `createToken()`
|
|
766
925
|
* additionally requires `vault.isReady` (tokenizer iframe loaded).
|
|
767
926
|
*
|
|
768
|
-
* @throws {OzError} if `fetchWaxKey` throws or returns an empty string.
|
|
927
|
+
* @throws {OzError} if `fetchWaxKey` throws, returns a non-string value, or returns an empty/whitespace-only string.
|
|
769
928
|
*/
|
|
770
|
-
static async create(options) {
|
|
929
|
+
static async create(options, signal) {
|
|
771
930
|
if (!options.pubKey || !options.pubKey.trim()) {
|
|
772
931
|
throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
|
|
773
932
|
}
|
|
774
933
|
if (typeof options.fetchWaxKey !== 'function') {
|
|
775
934
|
throw new OzError('fetchWaxKey must be a function. See OzVault.create() docs for the expected signature.');
|
|
776
935
|
}
|
|
777
|
-
const tokenizationSessionId =
|
|
936
|
+
const tokenizationSessionId = uuid();
|
|
778
937
|
// Construct the vault immediately — this mounts the tokenizer iframe so it
|
|
779
938
|
// starts loading while fetchWaxKey is in flight. The waxKey field starts
|
|
780
939
|
// empty and is set below before create() returns.
|
|
781
940
|
const vault = new OzVault(options, '', tokenizationSessionId);
|
|
941
|
+
// If the caller provides an AbortSignal (e.g. React useEffect cleanup),
|
|
942
|
+
// destroy the vault immediately on abort so the tokenizer iframe and message
|
|
943
|
+
// listener are removed synchronously rather than waiting for fetchWaxKey to
|
|
944
|
+
// settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
945
|
+
const onAbort = () => vault.destroy();
|
|
946
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
|
|
782
947
|
let waxKey;
|
|
783
948
|
try {
|
|
784
949
|
waxKey = await options.fetchWaxKey(tokenizationSessionId);
|
|
785
950
|
}
|
|
786
951
|
catch (err) {
|
|
952
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
787
953
|
vault.destroy();
|
|
954
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
955
|
+
throw new OzError('OzVault.create() was cancelled.');
|
|
956
|
+
// Preserve errorCode/retryable from OzError (e.g. timeout/network from createFetchWaxKey)
|
|
957
|
+
// so callers can distinguish transient failures from config errors.
|
|
958
|
+
const originalCode = err instanceof OzError ? err.errorCode : undefined;
|
|
788
959
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
789
|
-
throw new OzError(`fetchWaxKey threw an error: ${msg}
|
|
960
|
+
throw new OzError(`fetchWaxKey threw an error: ${msg}`, undefined, originalCode);
|
|
961
|
+
}
|
|
962
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
963
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
964
|
+
vault.destroy();
|
|
965
|
+
throw new OzError('OzVault.create() was cancelled.');
|
|
790
966
|
}
|
|
791
|
-
if (
|
|
967
|
+
if (typeof waxKey !== 'string' || !waxKey.trim()) {
|
|
792
968
|
vault.destroy();
|
|
793
969
|
throw new OzError('fetchWaxKey must return a non-empty wax key string. Check your mint endpoint.');
|
|
794
970
|
}
|
|
795
971
|
// Static methods can access private fields of instances of the same class.
|
|
796
972
|
vault.waxKey = waxKey;
|
|
973
|
+
vault._storedFetchWaxKey = options.fetchWaxKey;
|
|
974
|
+
// If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
|
|
975
|
+
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
976
|
+
// so the tokenizer has the key stored before any createToken() call.
|
|
977
|
+
if (vault.tokenizerReady) {
|
|
978
|
+
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
979
|
+
}
|
|
797
980
|
return vault;
|
|
798
981
|
}
|
|
799
982
|
/**
|
|
800
983
|
* True once the hidden tokenizer iframe has loaded and signalled ready.
|
|
801
984
|
* Use this to gate the pay button when building custom UIs without React.
|
|
802
985
|
* React consumers should use the `ready` value returned by `useOzElements()`.
|
|
986
|
+
*
|
|
987
|
+
* Once `true`, remains `true` for the lifetime of this vault instance.
|
|
988
|
+
* It only reverts to `false` after `vault.destroy()` is called, at which
|
|
989
|
+
* point the vault is unusable and a new instance must be created.
|
|
990
|
+
*
|
|
991
|
+
* @remarks
|
|
992
|
+
* This tracks **tokenizer readiness only** — it says nothing about whether
|
|
993
|
+
* the individual element iframes (card number, CVV, etc.) have loaded.
|
|
994
|
+
* A vault can be `isReady === true` while elements are still mounting.
|
|
995
|
+
* To gate a submit button correctly in vanilla JS, wait for every element's
|
|
996
|
+
* `'ready'` event in addition to this flag. In React, use the `ready` value
|
|
997
|
+
* from `useOzElements()` instead, which combines both checks automatically.
|
|
803
998
|
*/
|
|
804
999
|
get isReady() {
|
|
805
1000
|
return this.tokenizerReady;
|
|
806
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Number of successful tokenize calls made against the current wax key.
|
|
1004
|
+
*
|
|
1005
|
+
* Resets to `0` each time the wax key is refreshed (proactively or reactively).
|
|
1006
|
+
* Useful in vanilla JS integrations to display "attempts remaining" UI.
|
|
1007
|
+
* In React, use `tokenizeCount` from `useOzElements()` instead.
|
|
1008
|
+
*
|
|
1009
|
+
* @example
|
|
1010
|
+
* const remaining = 3 - vault.tokenizeCount;
|
|
1011
|
+
* payButton.textContent = remaining > 0 ? `Pay (${remaining} attempts left)` : 'Pay';
|
|
1012
|
+
*/
|
|
1013
|
+
get tokenizeCount() {
|
|
1014
|
+
return this._tokenizeSuccessCount;
|
|
1015
|
+
}
|
|
807
1016
|
/**
|
|
808
1017
|
* Creates a new OzElement of the given type. Call `.mount(selector)` on the
|
|
809
1018
|
* returned element to attach it to the DOM.
|
|
@@ -838,6 +1047,9 @@ class OzVault {
|
|
|
838
1047
|
if (this._destroyed) {
|
|
839
1048
|
throw new OzError('Cannot create elements on a destroyed vault. Call await OzVault.create() to get a new instance.');
|
|
840
1049
|
}
|
|
1050
|
+
if (this._tokenizing) {
|
|
1051
|
+
throw new OzError('Cannot create or replace elements while a tokenization is in progress. Wait for the active createToken() / createBankToken() call to settle first.');
|
|
1052
|
+
}
|
|
841
1053
|
const existing = typeMap.get(type);
|
|
842
1054
|
if (existing) {
|
|
843
1055
|
this.elements.delete(existing.frameId);
|
|
@@ -885,10 +1097,10 @@ class OzVault {
|
|
|
885
1097
|
throw new OzError('lastName is required for bank account tokenization.');
|
|
886
1098
|
}
|
|
887
1099
|
if (options.firstName.trim().length > 50) {
|
|
888
|
-
throw new OzError('firstName must be 50 characters or fewer');
|
|
1100
|
+
throw new OzError('firstName must be 50 characters or fewer.');
|
|
889
1101
|
}
|
|
890
1102
|
if (options.lastName.trim().length > 50) {
|
|
891
|
-
throw new OzError('lastName must be 50 characters or fewer');
|
|
1103
|
+
throw new OzError('lastName must be 50 characters or fewer.');
|
|
892
1104
|
}
|
|
893
1105
|
const accountEl = this.bankElementsByType.get('accountNumber');
|
|
894
1106
|
const routingEl = this.bankElementsByType.get('routingNumber');
|
|
@@ -905,31 +1117,47 @@ class OzVault {
|
|
|
905
1117
|
}
|
|
906
1118
|
const readyBankElements = [accountEl, routingEl];
|
|
907
1119
|
this._tokenizing = 'bank';
|
|
908
|
-
const requestId = `req-${
|
|
1120
|
+
const requestId = `req-${uuid()}`;
|
|
909
1121
|
return new Promise((resolve, reject) => {
|
|
910
1122
|
const cleanup = () => { this._tokenizing = null; };
|
|
911
1123
|
this.bankTokenizeResolvers.set(requestId, {
|
|
912
1124
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
913
1125
|
reject: (e) => { cleanup(); reject(e); },
|
|
914
|
-
});
|
|
915
|
-
this.sendToTokenizer({
|
|
916
|
-
type: 'OZ_BANK_TOKENIZE',
|
|
917
|
-
requestId,
|
|
918
|
-
waxKey: this.waxKey,
|
|
919
|
-
tokenizationSessionId: this.tokenizationSessionId,
|
|
920
|
-
pubKey: this.pubKey,
|
|
921
1126
|
firstName: options.firstName.trim(),
|
|
922
1127
|
lastName: options.lastName.trim(),
|
|
1128
|
+
readyElements: readyBankElements,
|
|
923
1129
|
fieldCount: readyBankElements.length,
|
|
924
1130
|
});
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1131
|
+
try {
|
|
1132
|
+
const bankChannels = readyBankElements.map(() => new MessageChannel());
|
|
1133
|
+
this.sendToTokenizer({
|
|
1134
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
1135
|
+
requestId,
|
|
1136
|
+
waxKey: this.waxKey,
|
|
1137
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1138
|
+
pubKey: this.pubKey,
|
|
1139
|
+
firstName: options.firstName.trim(),
|
|
1140
|
+
lastName: options.lastName.trim(),
|
|
1141
|
+
fieldCount: readyBankElements.length,
|
|
1142
|
+
}, bankChannels.map(ch => ch.port1));
|
|
1143
|
+
readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
|
|
1144
|
+
const bankTimeoutId = setTimeout(() => {
|
|
1145
|
+
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
1146
|
+
this.bankTokenizeResolvers.delete(requestId);
|
|
1147
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1148
|
+
cleanup();
|
|
1149
|
+
reject(new OzError('Bank tokenization timed out after 30 seconds', undefined, 'timeout'));
|
|
1150
|
+
}
|
|
1151
|
+
}, 30000);
|
|
1152
|
+
const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
|
|
1153
|
+
if (bankPendingEntry)
|
|
1154
|
+
bankPendingEntry.timeoutId = bankTimeoutId;
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
this.bankTokenizeResolvers.delete(requestId);
|
|
1158
|
+
cleanup();
|
|
1159
|
+
reject(err instanceof OzError ? err : new OzError('Bank tokenization failed to start'));
|
|
1160
|
+
}
|
|
933
1161
|
});
|
|
934
1162
|
}
|
|
935
1163
|
/**
|
|
@@ -951,29 +1179,33 @@ class OzVault {
|
|
|
951
1179
|
? 'A bank tokenization is already in progress. Wait for it to complete before calling createToken().'
|
|
952
1180
|
: 'A card tokenization is already in progress. Wait for it to complete before calling createToken() again.');
|
|
953
1181
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
//
|
|
957
|
-
// created
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
|
|
963
|
-
const
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
|
|
1182
|
+
// All synchronous validation runs before _tokenizing is set so these throws
|
|
1183
|
+
// need no manual cleanup — _tokenizing is still null when they fire.
|
|
1184
|
+
// Collect all card elements that have been created (mounted or not) so we
|
|
1185
|
+
// can require every created field to be ready before proceeding. An element
|
|
1186
|
+
// that was created but whose iframe hasn't loaded yet will never send
|
|
1187
|
+
// OZ_FIELD_VALUE — tokenizing without it would silently submit an empty or
|
|
1188
|
+
// incomplete card and produce an opaque vault rejection.
|
|
1189
|
+
// Bank elements (accountNumber/routingNumber) share this.elements but live
|
|
1190
|
+
// in elementsByType under bank-only keys, so they are excluded by the Set.
|
|
1191
|
+
const cardElements = [...this.elementsByType.values()];
|
|
1192
|
+
if (cardElements.length === 0) {
|
|
1193
|
+
throw new OzError('No card elements have been created. Call vault.createElement() for each field before calling createToken.');
|
|
1194
|
+
}
|
|
1195
|
+
const notReady = cardElements.filter(el => !el.isReady);
|
|
1196
|
+
if (notReady.length > 0) {
|
|
1197
|
+
throw new OzError(`Not all elements are ready. Wait for all fields to finish loading before calling createToken. ` +
|
|
1198
|
+
`Not yet ready: ${notReady.map(el => el.type).join(', ')}.`);
|
|
967
1199
|
}
|
|
1200
|
+
const readyElements = cardElements;
|
|
968
1201
|
// Validate billing details if provided and extract firstName/lastName for the vault payload.
|
|
969
1202
|
// billing.firstName/lastName take precedence over the deprecated top-level params.
|
|
970
1203
|
let normalizedBilling;
|
|
971
|
-
let firstName = (_a = options.firstName) !== null && _a !== void 0 ? _a : '';
|
|
972
|
-
let lastName = (_b = options.lastName) !== null && _b !== void 0 ? _b : '';
|
|
1204
|
+
let firstName = ((_a = options.firstName) !== null && _a !== void 0 ? _a : '').trim();
|
|
1205
|
+
let lastName = ((_b = options.lastName) !== null && _b !== void 0 ? _b : '').trim();
|
|
973
1206
|
if (options.billing) {
|
|
974
1207
|
const result = validateBilling(options.billing);
|
|
975
1208
|
if (!result.valid) {
|
|
976
|
-
this._tokenizing = null;
|
|
977
1209
|
throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
|
|
978
1210
|
}
|
|
979
1211
|
normalizedBilling = result.normalized;
|
|
@@ -982,41 +1214,57 @@ class OzVault {
|
|
|
982
1214
|
}
|
|
983
1215
|
else {
|
|
984
1216
|
if (firstName.length > 50) {
|
|
985
|
-
|
|
986
|
-
throw new OzError('firstName must be 50 characters or fewer');
|
|
1217
|
+
throw new OzError('firstName must be 50 characters or fewer.');
|
|
987
1218
|
}
|
|
988
1219
|
if (lastName.length > 50) {
|
|
989
|
-
|
|
990
|
-
throw new OzError('lastName must be 50 characters or fewer');
|
|
1220
|
+
throw new OzError('lastName must be 50 characters or fewer.');
|
|
991
1221
|
}
|
|
992
1222
|
}
|
|
1223
|
+
this._tokenizing = 'card';
|
|
1224
|
+
const requestId = `req-${uuid()}`;
|
|
993
1225
|
return new Promise((resolve, reject) => {
|
|
994
1226
|
const cleanup = () => { this._tokenizing = null; };
|
|
995
1227
|
this.tokenizeResolvers.set(requestId, {
|
|
996
1228
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
997
1229
|
reject: (e) => { cleanup(); reject(e); },
|
|
998
1230
|
billing: normalizedBilling,
|
|
999
|
-
});
|
|
1000
|
-
// Tell tokenizer frame to expect N field values, then tokenize
|
|
1001
|
-
this.sendToTokenizer({
|
|
1002
|
-
type: 'OZ_TOKENIZE',
|
|
1003
|
-
requestId,
|
|
1004
|
-
waxKey: this.waxKey,
|
|
1005
|
-
tokenizationSessionId: this.tokenizationSessionId,
|
|
1006
|
-
pubKey: this.pubKey,
|
|
1007
1231
|
firstName,
|
|
1008
1232
|
lastName,
|
|
1233
|
+
readyElements,
|
|
1009
1234
|
fieldCount: readyElements.length,
|
|
1010
1235
|
});
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1236
|
+
try {
|
|
1237
|
+
// Tell tokenizer frame to expect N field values, then tokenize
|
|
1238
|
+
const cardChannels = readyElements.map(() => new MessageChannel());
|
|
1239
|
+
this.sendToTokenizer({
|
|
1240
|
+
type: 'OZ_TOKENIZE',
|
|
1241
|
+
requestId,
|
|
1242
|
+
waxKey: this.waxKey,
|
|
1243
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1244
|
+
pubKey: this.pubKey,
|
|
1245
|
+
firstName,
|
|
1246
|
+
lastName,
|
|
1247
|
+
fieldCount: readyElements.length,
|
|
1248
|
+
}, cardChannels.map(ch => ch.port1));
|
|
1249
|
+
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1250
|
+
readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
|
|
1251
|
+
const cardTimeoutId = setTimeout(() => {
|
|
1252
|
+
if (this.tokenizeResolvers.has(requestId)) {
|
|
1253
|
+
this.tokenizeResolvers.delete(requestId);
|
|
1254
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1255
|
+
cleanup();
|
|
1256
|
+
reject(new OzError('Tokenization timed out after 30 seconds', undefined, 'timeout'));
|
|
1257
|
+
}
|
|
1258
|
+
}, 30000);
|
|
1259
|
+
const cardPendingEntry = this.tokenizeResolvers.get(requestId);
|
|
1260
|
+
if (cardPendingEntry)
|
|
1261
|
+
cardPendingEntry.timeoutId = cardTimeoutId;
|
|
1262
|
+
}
|
|
1263
|
+
catch (err) {
|
|
1264
|
+
this.tokenizeResolvers.delete(requestId);
|
|
1265
|
+
cleanup();
|
|
1266
|
+
reject(err instanceof OzError ? err : new OzError('Tokenization failed to start'));
|
|
1267
|
+
}
|
|
1020
1268
|
});
|
|
1021
1269
|
}
|
|
1022
1270
|
/**
|
|
@@ -1030,6 +1278,7 @@ class OzVault {
|
|
|
1030
1278
|
return;
|
|
1031
1279
|
this._destroyed = true;
|
|
1032
1280
|
window.removeEventListener('message', this.boundHandleMessage);
|
|
1281
|
+
document.removeEventListener('visibilitychange', this.boundHandleVisibility);
|
|
1033
1282
|
if (this._pendingMount) {
|
|
1034
1283
|
document.removeEventListener('DOMContentLoaded', this._pendingMount);
|
|
1035
1284
|
this._pendingMount = null;
|
|
@@ -1040,11 +1289,17 @@ class OzVault {
|
|
|
1040
1289
|
}
|
|
1041
1290
|
// Reject any pending tokenize promises so callers aren't left hanging
|
|
1042
1291
|
this._tokenizing = null;
|
|
1043
|
-
this.tokenizeResolvers.forEach(({ reject }) => {
|
|
1292
|
+
this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1293
|
+
if (timeoutId != null)
|
|
1294
|
+
clearTimeout(timeoutId);
|
|
1295
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1044
1296
|
reject(new OzError('Vault destroyed before tokenization completed.'));
|
|
1045
1297
|
});
|
|
1046
1298
|
this.tokenizeResolvers.clear();
|
|
1047
|
-
this.bankTokenizeResolvers.forEach(({ reject }) => {
|
|
1299
|
+
this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1300
|
+
if (timeoutId != null)
|
|
1301
|
+
clearTimeout(timeoutId);
|
|
1302
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1048
1303
|
reject(new OzError('Vault destroyed before bank tokenization completed.'));
|
|
1049
1304
|
});
|
|
1050
1305
|
this.bankTokenizeResolvers.clear();
|
|
@@ -1053,17 +1308,49 @@ class OzVault {
|
|
|
1053
1308
|
this.elementsByType.clear();
|
|
1054
1309
|
this.bankElementsByType.clear();
|
|
1055
1310
|
this.completionState.clear();
|
|
1311
|
+
this._tokenizeSuccessCount = 0;
|
|
1056
1312
|
(_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.remove();
|
|
1057
1313
|
this.tokenizerFrame = null;
|
|
1058
1314
|
this.tokenizerWindow = null;
|
|
1059
1315
|
this.tokenizerReady = false;
|
|
1060
1316
|
}
|
|
1061
1317
|
// ─── Private ─────────────────────────────────────────────────────────────
|
|
1318
|
+
/**
|
|
1319
|
+
* Proactively re-mints the wax key when the page becomes visible again after
|
|
1320
|
+
* a long idle period. Wax keys have a fixed TTL (~30 minutes); a user who
|
|
1321
|
+
* leaves the tab in the background and returns could have an expired key.
|
|
1322
|
+
* Rather than waiting for a failed tokenization to trigger the reactive
|
|
1323
|
+
* refresh path, this pre-empts the failure when the vault is idle.
|
|
1324
|
+
*
|
|
1325
|
+
* Threshold: 20 minutes hidden. Chosen to be comfortably inside the ~30m TTL
|
|
1326
|
+
* while avoiding spurious refreshes for brief tab-switches.
|
|
1327
|
+
*/
|
|
1328
|
+
handleVisibilityChange() {
|
|
1329
|
+
if (this._destroyed)
|
|
1330
|
+
return;
|
|
1331
|
+
const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
|
|
1332
|
+
if (document.hidden) {
|
|
1333
|
+
this._hiddenAt = Date.now();
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
if (this._hiddenAt !== null &&
|
|
1337
|
+
Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
|
|
1338
|
+
this._storedFetchWaxKey &&
|
|
1339
|
+
!this._tokenizing &&
|
|
1340
|
+
!this._waxRefreshing) {
|
|
1341
|
+
this.refreshWaxKey().catch((err) => {
|
|
1342
|
+
// Proactive refresh failure is non-fatal — the reactive path on the
|
|
1343
|
+
// next createToken() call will handle it, including the auth retry.
|
|
1344
|
+
console.warn('[OzVault] Proactive wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
this._hiddenAt = null;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1062
1350
|
mountTokenizerFrame() {
|
|
1063
1351
|
const mount = () => {
|
|
1064
1352
|
this._pendingMount = null;
|
|
1065
1353
|
const iframe = document.createElement('iframe');
|
|
1066
|
-
iframe.name = this.tokenizerName;
|
|
1067
1354
|
iframe.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;';
|
|
1068
1355
|
iframe.setAttribute('aria-hidden', 'true');
|
|
1069
1356
|
iframe.tabIndex = -1;
|
|
@@ -1082,6 +1369,8 @@ class OzVault {
|
|
|
1082
1369
|
}
|
|
1083
1370
|
handleMessage(event) {
|
|
1084
1371
|
var _a;
|
|
1372
|
+
if (this._destroyed)
|
|
1373
|
+
return;
|
|
1085
1374
|
// Only accept messages from our frame origin (defense in depth; prevents
|
|
1086
1375
|
// arbitrary pages from injecting OZ_TOKEN_RESULT etc. with a guessed vaultId).
|
|
1087
1376
|
if (event.origin !== this.frameOrigin)
|
|
@@ -1099,14 +1388,17 @@ class OzVault {
|
|
|
1099
1388
|
if (frameId) {
|
|
1100
1389
|
const el = this.elements.get(frameId);
|
|
1101
1390
|
if (el) {
|
|
1391
|
+
// Reset stale completion state when an element iframe re-loads (e.g. after
|
|
1392
|
+
// unmount() + mount() in a SPA). Without this, wasComplete stays true from
|
|
1393
|
+
// the previous session and justCompleted never fires, breaking auto-advance.
|
|
1394
|
+
if (msg.type === 'OZ_FRAME_READY') {
|
|
1395
|
+
this.completionState.set(frameId, false);
|
|
1396
|
+
}
|
|
1102
1397
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1103
1398
|
if (msg.type === 'OZ_CHANGE') {
|
|
1104
1399
|
this.handleElementChange(msg, el);
|
|
1105
1400
|
}
|
|
1106
1401
|
el.handleMessage(msg);
|
|
1107
|
-
if (msg.type === 'OZ_FRAME_READY' && this.tokenizerReady) {
|
|
1108
|
-
el.setTokenizerName(this.tokenizerName);
|
|
1109
|
-
}
|
|
1110
1402
|
}
|
|
1111
1403
|
}
|
|
1112
1404
|
}
|
|
@@ -1120,7 +1412,7 @@ class OzVault {
|
|
|
1120
1412
|
const complete = msg.complete;
|
|
1121
1413
|
const valid = msg.valid;
|
|
1122
1414
|
const wasComplete = (_a = this.completionState.get(el.frameId)) !== null && _a !== void 0 ? _a : false;
|
|
1123
|
-
this.completionState.set(el.frameId, complete);
|
|
1415
|
+
this.completionState.set(el.frameId, complete && valid);
|
|
1124
1416
|
// Require valid too — avoids advancing at 13 digits for unknown-brand cards
|
|
1125
1417
|
// where isComplete() fires before the user has finished typing.
|
|
1126
1418
|
const justCompleted = complete && valid && !wasComplete;
|
|
@@ -1143,7 +1435,7 @@ class OzVault {
|
|
|
1143
1435
|
}
|
|
1144
1436
|
}
|
|
1145
1437
|
handleTokenizerMessage(msg) {
|
|
1146
|
-
var _a, _b;
|
|
1438
|
+
var _a, _b, _c;
|
|
1147
1439
|
switch (msg.type) {
|
|
1148
1440
|
case 'OZ_FRAME_READY':
|
|
1149
1441
|
this.tokenizerReady = true;
|
|
@@ -1152,73 +1444,370 @@ class OzVault {
|
|
|
1152
1444
|
this.loadErrorTimeoutId = null;
|
|
1153
1445
|
}
|
|
1154
1446
|
this.tokenizerWindow = (_b = (_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
|
|
1155
|
-
|
|
1156
|
-
|
|
1447
|
+
// Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
|
|
1448
|
+
// If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
|
|
1449
|
+
// sent again from create() once the key is available.
|
|
1450
|
+
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1451
|
+
(_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
1157
1452
|
break;
|
|
1158
1453
|
case 'OZ_TOKEN_RESULT': {
|
|
1454
|
+
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
1455
|
+
console.error('[OzVault] OZ_TOKEN_RESULT missing requestId — discarding message.');
|
|
1456
|
+
break;
|
|
1457
|
+
}
|
|
1159
1458
|
const pending = this.tokenizeResolvers.get(msg.requestId);
|
|
1160
1459
|
if (pending) {
|
|
1161
1460
|
this.tokenizeResolvers.delete(msg.requestId);
|
|
1162
|
-
|
|
1163
|
-
|
|
1461
|
+
if (pending.timeoutId != null)
|
|
1462
|
+
clearTimeout(pending.timeoutId);
|
|
1463
|
+
const token = msg.token;
|
|
1464
|
+
if (typeof token !== 'string' || !token) {
|
|
1465
|
+
pending.reject(new OzError('Vault returned a token result with a missing or empty token — possible vault API change.', undefined, 'server'));
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
const card = isCardMetadata(msg.card) ? msg.card : undefined;
|
|
1469
|
+
const cvcSession = typeof msg.cvcSession === 'string' && msg.cvcSession ? msg.cvcSession : undefined;
|
|
1470
|
+
if (!cvcSession) {
|
|
1471
|
+
pending.reject(new OzError('Vault returned a token result without a cvcSession — possible vault misconfiguration or API change.', undefined, 'server'));
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
if (!card) {
|
|
1475
|
+
pending.reject(new OzError('Vault returned a token result without card metadata — possible vault API change.', undefined, 'server'));
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
pending.resolve(Object.assign({ token,
|
|
1479
|
+
cvcSession,
|
|
1480
|
+
card }, (pending.billing ? { billing: pending.billing } : {})));
|
|
1481
|
+
// Increment the per-key success counter and proactively refresh once
|
|
1482
|
+
// the budget is exhausted so the next createToken() call uses a fresh
|
|
1483
|
+
// key without waiting for a vault rejection.
|
|
1484
|
+
this._tokenizeSuccessCount++;
|
|
1485
|
+
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1486
|
+
this.refreshWaxKey().catch((err) => {
|
|
1487
|
+
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1164
1490
|
}
|
|
1165
1491
|
break;
|
|
1166
1492
|
}
|
|
1167
1493
|
case 'OZ_TOKEN_ERROR': {
|
|
1494
|
+
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
1495
|
+
console.error('[OzVault] OZ_TOKEN_ERROR missing requestId — discarding message.');
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
const raw = typeof msg.error === 'string' ? msg.error : '';
|
|
1499
|
+
const errorCode = isOzErrorCode(msg.errorCode) ? msg.errorCode : 'unknown';
|
|
1168
1500
|
const pending = this.tokenizeResolvers.get(msg.requestId);
|
|
1169
1501
|
if (pending) {
|
|
1170
1502
|
this.tokenizeResolvers.delete(msg.requestId);
|
|
1171
|
-
|
|
1172
|
-
|
|
1503
|
+
if (pending.timeoutId != null)
|
|
1504
|
+
clearTimeout(pending.timeoutId);
|
|
1505
|
+
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1506
|
+
// already retried for this request, transparently re-mint and retry.
|
|
1507
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
|
|
1508
|
+
this.refreshWaxKey().then(() => {
|
|
1509
|
+
if (this._destroyed) {
|
|
1510
|
+
pending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const newRequestId = `req-${uuid()}`;
|
|
1514
|
+
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1515
|
+
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
1516
|
+
try {
|
|
1517
|
+
const retryCardChannels = pending.readyElements.map(() => new MessageChannel());
|
|
1518
|
+
this.sendToTokenizer({
|
|
1519
|
+
type: 'OZ_TOKENIZE',
|
|
1520
|
+
requestId: newRequestId,
|
|
1521
|
+
waxKey: this.waxKey,
|
|
1522
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1523
|
+
pubKey: this.pubKey,
|
|
1524
|
+
firstName: pending.firstName,
|
|
1525
|
+
lastName: pending.lastName,
|
|
1526
|
+
fieldCount: pending.fieldCount,
|
|
1527
|
+
}, retryCardChannels.map(ch => ch.port1));
|
|
1528
|
+
pending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryCardChannels[i].port2));
|
|
1529
|
+
const retryCardTimeoutId = setTimeout(() => {
|
|
1530
|
+
if (this.tokenizeResolvers.has(newRequestId)) {
|
|
1531
|
+
this.tokenizeResolvers.delete(newRequestId);
|
|
1532
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
1533
|
+
pending.reject(new OzError('Tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
1534
|
+
}
|
|
1535
|
+
}, 30000);
|
|
1536
|
+
const retryCardEntry = this.tokenizeResolvers.get(newRequestId);
|
|
1537
|
+
if (retryCardEntry)
|
|
1538
|
+
retryCardEntry.timeoutId = retryCardTimeoutId;
|
|
1539
|
+
}
|
|
1540
|
+
catch (setupErr) {
|
|
1541
|
+
this.tokenizeResolvers.delete(newRequestId);
|
|
1542
|
+
pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
|
|
1543
|
+
}
|
|
1544
|
+
}).catch((refreshErr) => {
|
|
1545
|
+
const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1546
|
+
pending.reject(new OzError(msg, undefined, 'auth'));
|
|
1547
|
+
});
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1173
1550
|
pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
|
|
1174
1551
|
}
|
|
1175
1552
|
// Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR
|
|
1176
1553
|
const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1177
1554
|
if (bankPending) {
|
|
1178
1555
|
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1179
|
-
|
|
1180
|
-
|
|
1556
|
+
if (bankPending.timeoutId != null)
|
|
1557
|
+
clearTimeout(bankPending.timeoutId);
|
|
1558
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1559
|
+
this.refreshWaxKey().then(() => {
|
|
1560
|
+
if (this._destroyed) {
|
|
1561
|
+
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const newRequestId = `req-${uuid()}`;
|
|
1565
|
+
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1566
|
+
try {
|
|
1567
|
+
const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
|
|
1568
|
+
this.sendToTokenizer({
|
|
1569
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
1570
|
+
requestId: newRequestId,
|
|
1571
|
+
waxKey: this.waxKey,
|
|
1572
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1573
|
+
pubKey: this.pubKey,
|
|
1574
|
+
firstName: bankPending.firstName,
|
|
1575
|
+
lastName: bankPending.lastName,
|
|
1576
|
+
fieldCount: bankPending.fieldCount,
|
|
1577
|
+
}, retryBankChannels.map(ch => ch.port1));
|
|
1578
|
+
bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
|
|
1579
|
+
const retryBankTimeoutId = setTimeout(() => {
|
|
1580
|
+
if (this.bankTokenizeResolvers.has(newRequestId)) {
|
|
1581
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1582
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
1583
|
+
bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
1584
|
+
}
|
|
1585
|
+
}, 30000);
|
|
1586
|
+
const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
|
|
1587
|
+
if (retryBankEntry)
|
|
1588
|
+
retryBankEntry.timeoutId = retryBankTimeoutId;
|
|
1589
|
+
}
|
|
1590
|
+
catch (setupErr) {
|
|
1591
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1592
|
+
bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
|
|
1593
|
+
}
|
|
1594
|
+
}).catch((refreshErr) => {
|
|
1595
|
+
const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1596
|
+
bankPending.reject(new OzError(msg, undefined, 'auth'));
|
|
1597
|
+
});
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1181
1600
|
bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
|
|
1182
1601
|
}
|
|
1183
1602
|
break;
|
|
1184
1603
|
}
|
|
1185
1604
|
case 'OZ_BANK_TOKEN_RESULT': {
|
|
1605
|
+
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
1606
|
+
console.error('[OzVault] OZ_BANK_TOKEN_RESULT missing requestId — discarding message.');
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1186
1609
|
const pending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1187
1610
|
if (pending) {
|
|
1188
1611
|
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1189
|
-
|
|
1190
|
-
|
|
1612
|
+
if (pending.timeoutId != null)
|
|
1613
|
+
clearTimeout(pending.timeoutId);
|
|
1614
|
+
const token = msg.token;
|
|
1615
|
+
if (typeof token !== 'string' || !token) {
|
|
1616
|
+
pending.reject(new OzError('Vault returned a bank token result with a missing or empty token — possible vault API change.', undefined, 'server'));
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
|
|
1620
|
+
pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
|
|
1621
|
+
// Same proactive refresh logic as card tokenization.
|
|
1622
|
+
this._tokenizeSuccessCount++;
|
|
1623
|
+
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1624
|
+
this.refreshWaxKey().catch((err) => {
|
|
1625
|
+
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1191
1628
|
}
|
|
1192
1629
|
break;
|
|
1193
1630
|
}
|
|
1194
1631
|
}
|
|
1195
1632
|
}
|
|
1196
|
-
|
|
1633
|
+
/**
|
|
1634
|
+
* Returns true when an OZ_TOKEN_ERROR should trigger a wax key refresh.
|
|
1635
|
+
*
|
|
1636
|
+
* Primary path: vault returns 401/403 → errorCode 'auth'.
|
|
1637
|
+
* Defensive path: vault returns 400 → errorCode 'validation', but the raw
|
|
1638
|
+
* message contains wax-key-specific language (consumed, expired, invalid key,
|
|
1639
|
+
* etc.). This avoids a hard dependency on the vault returning a unified HTTP
|
|
1640
|
+
* status for consumed-key vs expired-key failures — both should refresh.
|
|
1641
|
+
*
|
|
1642
|
+
* Deliberately excludes 'network', 'timeout', and 'server' codes (transient
|
|
1643
|
+
* errors are already retried in fetchWithRetry) and 'unknown' (too broad).
|
|
1644
|
+
*/
|
|
1645
|
+
isRefreshableAuthError(errorCode, raw) {
|
|
1646
|
+
if (errorCode === 'auth')
|
|
1647
|
+
return true;
|
|
1648
|
+
if (errorCode === 'validation') {
|
|
1649
|
+
const msg = raw.toLowerCase();
|
|
1650
|
+
// Only treat validation errors as wax-related if the message explicitly
|
|
1651
|
+
// names the wax/tokenization session mechanism. A bare "session" match
|
|
1652
|
+
// was too broad — any message mentioning "session" (e.g. a merchant
|
|
1653
|
+
// session field error) would trigger a spurious re-mint.
|
|
1654
|
+
return (msg.includes('wax') ||
|
|
1655
|
+
msg.includes('expired') ||
|
|
1656
|
+
msg.includes('consumed') ||
|
|
1657
|
+
msg.includes('invalid key') ||
|
|
1658
|
+
msg.includes('key not found') ||
|
|
1659
|
+
msg.includes('tokenization session'));
|
|
1660
|
+
}
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Re-mints the wax key using the stored fetchWaxKey callback and updates
|
|
1665
|
+
* the tokenizer with the new key. Used for transparent auto-refresh when
|
|
1666
|
+
* the vault returns an auth error on tokenization.
|
|
1667
|
+
*
|
|
1668
|
+
* Only one refresh runs at a time — concurrent retries share the same promise.
|
|
1669
|
+
*/
|
|
1670
|
+
refreshWaxKey() {
|
|
1671
|
+
var _a;
|
|
1672
|
+
if (this._waxRefreshing)
|
|
1673
|
+
return this._waxRefreshing;
|
|
1674
|
+
if (!this._storedFetchWaxKey) {
|
|
1675
|
+
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'));
|
|
1676
|
+
}
|
|
1677
|
+
const newSessionId = uuid();
|
|
1678
|
+
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
1679
|
+
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
1680
|
+
.then(newWaxKey => {
|
|
1681
|
+
if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
|
|
1682
|
+
throw new OzError('fetchWaxKey returned an empty string during auto-refresh.', undefined, 'auth');
|
|
1683
|
+
}
|
|
1684
|
+
if (!this._destroyed) {
|
|
1685
|
+
this.waxKey = newWaxKey;
|
|
1686
|
+
this.tokenizationSessionId = newSessionId;
|
|
1687
|
+
this._tokenizeSuccessCount = 0;
|
|
1688
|
+
}
|
|
1689
|
+
if (!this._destroyed && this.tokenizerReady) {
|
|
1690
|
+
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
1691
|
+
}
|
|
1692
|
+
})
|
|
1693
|
+
.finally(() => {
|
|
1694
|
+
this._waxRefreshing = null;
|
|
1695
|
+
});
|
|
1696
|
+
return this._waxRefreshing;
|
|
1697
|
+
}
|
|
1698
|
+
sendToTokenizer(data, transfer) {
|
|
1197
1699
|
var _a;
|
|
1198
1700
|
const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
|
|
1199
|
-
(_a = this.tokenizerWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin);
|
|
1701
|
+
(_a = this.tokenizerWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin, transfer !== null && transfer !== void 0 ? transfer : []);
|
|
1200
1702
|
}
|
|
1201
1703
|
}
|
|
1202
1704
|
|
|
1705
|
+
/**
|
|
1706
|
+
* Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
|
|
1707
|
+
*
|
|
1708
|
+
* Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
|
|
1709
|
+
* Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
|
|
1710
|
+
* error through its normal error path.
|
|
1711
|
+
*
|
|
1712
|
+
* Each call enforces a 10-second per-attempt timeout. On a pure network-level
|
|
1713
|
+
* failure (connection refused, DNS failure, etc.) the call is retried once after
|
|
1714
|
+
* 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
|
|
1715
|
+
* an endpoint misconfiguration or an invalid key, not a transient failure.
|
|
1716
|
+
*
|
|
1717
|
+
* The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
|
|
1718
|
+
* from `@ozura/elements/server`.
|
|
1719
|
+
*
|
|
1720
|
+
* @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
|
|
1721
|
+
*
|
|
1722
|
+
* @example
|
|
1723
|
+
* // Vanilla JS
|
|
1724
|
+
* const vault = await OzVault.create({
|
|
1725
|
+
* pubKey: 'pk_live_...',
|
|
1726
|
+
* fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
|
|
1727
|
+
* });
|
|
1728
|
+
*
|
|
1729
|
+
* @example
|
|
1730
|
+
* // React
|
|
1731
|
+
* <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
|
|
1732
|
+
*/
|
|
1733
|
+
function createFetchWaxKey(mintUrl) {
|
|
1734
|
+
const TIMEOUT_MS = 10000;
|
|
1735
|
+
// Each attempt gets its own AbortController so a timeout on attempt 1 does
|
|
1736
|
+
// not bleed into the retry. Uses AbortController + setTimeout instead of
|
|
1737
|
+
// AbortSignal.timeout() to support environments without that API.
|
|
1738
|
+
const attemptFetch = (sessionId) => {
|
|
1739
|
+
const controller = new AbortController();
|
|
1740
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1741
|
+
return fetch(mintUrl, {
|
|
1742
|
+
method: 'POST',
|
|
1743
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1744
|
+
body: JSON.stringify({ sessionId }),
|
|
1745
|
+
signal: controller.signal,
|
|
1746
|
+
}).finally(() => clearTimeout(timer));
|
|
1747
|
+
};
|
|
1748
|
+
return async (sessionId) => {
|
|
1749
|
+
let res;
|
|
1750
|
+
try {
|
|
1751
|
+
res = await attemptFetch(sessionId);
|
|
1752
|
+
}
|
|
1753
|
+
catch (firstErr) {
|
|
1754
|
+
// Abort/timeout should not be retried — the server received nothing or
|
|
1755
|
+
// we already waited the full timeout duration.
|
|
1756
|
+
if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
|
|
1757
|
+
throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
|
|
1758
|
+
}
|
|
1759
|
+
// Pure network error (offline, DNS, connection refused) — retry once
|
|
1760
|
+
// after a short pause in case of a transient blip.
|
|
1761
|
+
await new Promise(resolve => setTimeout(resolve, 750));
|
|
1762
|
+
try {
|
|
1763
|
+
res = await attemptFetch(sessionId);
|
|
1764
|
+
}
|
|
1765
|
+
catch (retryErr) {
|
|
1766
|
+
const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
|
|
1767
|
+
throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const data = await res.json().catch(() => ({}));
|
|
1771
|
+
if (!res.ok) {
|
|
1772
|
+
throw new OzError(typeof data.error === 'string' && data.error
|
|
1773
|
+
? data.error
|
|
1774
|
+
: `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
1775
|
+
}
|
|
1776
|
+
if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
|
|
1777
|
+
throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
|
|
1778
|
+
}
|
|
1779
|
+
return data.waxKey;
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1203
1783
|
const OzContext = react.createContext({
|
|
1204
1784
|
vault: null,
|
|
1785
|
+
initError: null,
|
|
1205
1786
|
notifyReady: () => { },
|
|
1206
1787
|
notifyUnmount: () => { },
|
|
1207
1788
|
notifyMount: () => { },
|
|
1789
|
+
notifyTokenize: () => { },
|
|
1208
1790
|
mountedCount: 0,
|
|
1209
1791
|
readyCount: 0,
|
|
1792
|
+
tokenizeCount: 0,
|
|
1210
1793
|
});
|
|
1211
1794
|
/**
|
|
1212
1795
|
* Creates and owns an OzVault instance for the lifetime of this component.
|
|
1213
1796
|
* All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
|
|
1214
1797
|
* rendered inside this provider.
|
|
1215
1798
|
*/
|
|
1216
|
-
function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, appearance, children }) {
|
|
1799
|
+
function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, children }) {
|
|
1217
1800
|
const [vault, setVault] = react.useState(null);
|
|
1801
|
+
const [initError, setInitError] = react.useState(null);
|
|
1218
1802
|
const [mountedCount, setMountedCount] = react.useState(0);
|
|
1219
1803
|
const [readyCount, setReadyCount] = react.useState(0);
|
|
1804
|
+
const [tokenizeCount, setTokenizeCount] = react.useState(0);
|
|
1220
1805
|
const onLoadErrorRef = react.useRef(onLoadError);
|
|
1221
1806
|
onLoadErrorRef.current = onLoadError;
|
|
1807
|
+
const onWaxRefreshRef = react.useRef(onWaxRefresh);
|
|
1808
|
+
onWaxRefreshRef.current = onWaxRefresh;
|
|
1809
|
+
const onReadyRef = react.useRef(onReady);
|
|
1810
|
+
onReadyRef.current = onReady;
|
|
1222
1811
|
// Keep a ref to fetchWaxKey so changes don't trigger vault recreation
|
|
1223
1812
|
const fetchWaxKeyRef = react.useRef(fetchWaxKey);
|
|
1224
1813
|
fetchWaxKeyRef.current = fetchWaxKey;
|
|
@@ -1229,22 +1818,56 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1229
1818
|
let createdVault = null;
|
|
1230
1819
|
const parsedAppearance = appearanceKey ? JSON.parse(appearanceKey) : undefined;
|
|
1231
1820
|
const parsedFonts = fontsKey ? JSON.parse(fontsKey) : undefined;
|
|
1232
|
-
|
|
1821
|
+
// Guard: onLoadError must fire at most once per effect run. It can be
|
|
1822
|
+
// triggered by two independent paths — the vault's iframe load timeout
|
|
1823
|
+
// (inside OzVault constructor) and the .catch below when fetchWaxKey
|
|
1824
|
+
// rejects after the timeout has already fired. Without this guard both
|
|
1825
|
+
// paths would call the callback.
|
|
1826
|
+
let loadErrorFired = false;
|
|
1827
|
+
const fireLoadError = () => {
|
|
1828
|
+
var _a;
|
|
1829
|
+
if (loadErrorFired)
|
|
1830
|
+
return;
|
|
1831
|
+
loadErrorFired = true;
|
|
1832
|
+
(_a = onLoadErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onLoadErrorRef);
|
|
1833
|
+
};
|
|
1834
|
+
// AbortController passed to create() so that if this effect's cleanup runs
|
|
1835
|
+
// while fetchWaxKey is still in-flight (React StrictMode double-invoke or
|
|
1836
|
+
// rapid prop churn), the tokenizer iframe and message listener are destroyed
|
|
1837
|
+
// synchronously rather than waiting for the promise to settle. Without this,
|
|
1838
|
+
// two hidden iframes and two window listeners briefly coexist.
|
|
1839
|
+
const abortController = new AbortController();
|
|
1840
|
+
OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey, fetchWaxKey: (sessionId) => fetchWaxKeyRef.current(sessionId) }, (frameBaseUrl ? { frameBaseUrl } : {})), (parsedFonts ? { fonts: parsedFonts } : {})), (parsedAppearance ? { appearance: parsedAppearance } : {})), (onLoadErrorRef.current ? { onLoadError: fireLoadError, loadTimeoutMs } : {})), {
|
|
1841
|
+
// Always install onWaxRefresh internally so we can reset tokenizeCount
|
|
1842
|
+
// when any wax key refresh occurs (reactive TTL expiry, post-budget
|
|
1843
|
+
// proactive, or visibility-change proactive). Without this the React
|
|
1844
|
+
// counter accumulates across key refreshes and diverges from the vault's
|
|
1845
|
+
// internal _tokenizeSuccessCount, which resets to 0 on every refresh.
|
|
1846
|
+
onWaxRefresh: () => {
|
|
1847
|
+
var _a;
|
|
1848
|
+
setTokenizeCount(0);
|
|
1849
|
+
(_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
|
|
1850
|
+
} }), (onReadyRef.current ? { onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } } : {})), (maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), abortController.signal).then(v => {
|
|
1233
1851
|
if (cancelled) {
|
|
1234
1852
|
v.destroy();
|
|
1235
1853
|
return;
|
|
1236
1854
|
}
|
|
1237
1855
|
createdVault = v;
|
|
1238
1856
|
setVault(v);
|
|
1857
|
+
setInitError(null);
|
|
1239
1858
|
setMountedCount(0);
|
|
1240
1859
|
setReadyCount(0);
|
|
1860
|
+
setTokenizeCount(0);
|
|
1241
1861
|
}).catch((err) => {
|
|
1242
|
-
// fetchWaxKey threw
|
|
1243
|
-
//
|
|
1862
|
+
// fetchWaxKey threw, returned a non-string value, returned an empty string,
|
|
1863
|
+
// or create() was cancelled via the AbortSignal — all are suppressed when
|
|
1864
|
+
// cancelled is true (cleanup already ran).
|
|
1244
1865
|
if (cancelled)
|
|
1245
1866
|
return;
|
|
1867
|
+
const error = err instanceof Error ? err : new OzError('OzVault.create() failed.');
|
|
1868
|
+
setInitError(error);
|
|
1246
1869
|
if (onLoadErrorRef.current) {
|
|
1247
|
-
|
|
1870
|
+
fireLoadError();
|
|
1248
1871
|
}
|
|
1249
1872
|
else {
|
|
1250
1873
|
console.error('[OzElements] OzVault.create() failed. Provide an `onLoadError` prop to handle this gracefully.', err);
|
|
@@ -1252,17 +1875,20 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1252
1875
|
});
|
|
1253
1876
|
return () => {
|
|
1254
1877
|
cancelled = true;
|
|
1878
|
+
abortController.abort(); // Destroys vault (and its iframe/listener) synchronously if create() is in-flight
|
|
1255
1879
|
createdVault === null || createdVault === void 0 ? void 0 : createdVault.destroy();
|
|
1256
1880
|
setVault(null);
|
|
1881
|
+
setInitError(null);
|
|
1257
1882
|
};
|
|
1258
|
-
}, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey]);
|
|
1883
|
+
}, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls]);
|
|
1259
1884
|
const notifyMount = react.useCallback(() => setMountedCount(n => n + 1), []);
|
|
1260
1885
|
const notifyReady = react.useCallback(() => setReadyCount(n => n + 1), []);
|
|
1261
1886
|
const notifyUnmount = react.useCallback(() => {
|
|
1262
1887
|
setMountedCount(n => Math.max(0, n - 1));
|
|
1263
1888
|
setReadyCount(n => Math.max(0, n - 1));
|
|
1264
1889
|
}, []);
|
|
1265
|
-
const
|
|
1890
|
+
const notifyTokenize = react.useCallback(() => setTokenizeCount(n => n + 1), []);
|
|
1891
|
+
const value = react.useMemo(() => ({ vault, initError, notifyMount, notifyReady, notifyUnmount, notifyTokenize, mountedCount, readyCount, tokenizeCount }), [vault, initError, notifyMount, notifyReady, notifyUnmount, notifyTokenize, mountedCount, readyCount, tokenizeCount]);
|
|
1266
1892
|
return jsxRuntime.jsx(OzContext.Provider, { value: value, children: children });
|
|
1267
1893
|
}
|
|
1268
1894
|
/**
|
|
@@ -1270,15 +1896,25 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1270
1896
|
* an `<OzElements>` provider tree.
|
|
1271
1897
|
*/
|
|
1272
1898
|
function useOzElements() {
|
|
1273
|
-
const { vault, mountedCount, readyCount } = react.useContext(OzContext);
|
|
1274
|
-
const createToken = react.useCallback((options) => {
|
|
1899
|
+
const { vault, initError, mountedCount, readyCount, notifyTokenize, tokenizeCount } = react.useContext(OzContext);
|
|
1900
|
+
const createToken = react.useCallback(async (options) => {
|
|
1275
1901
|
if (!vault) {
|
|
1276
1902
|
return Promise.reject(new OzError('useOzElements must be called inside an <OzElements> provider.'));
|
|
1277
1903
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1904
|
+
const result = await vault.createToken(options);
|
|
1905
|
+
notifyTokenize();
|
|
1906
|
+
return result;
|
|
1907
|
+
}, [vault, notifyTokenize]);
|
|
1908
|
+
const createBankToken = react.useCallback(async (options) => {
|
|
1909
|
+
if (!vault) {
|
|
1910
|
+
return Promise.reject(new OzError('useOzElements must be called inside an <OzElements> provider.'));
|
|
1911
|
+
}
|
|
1912
|
+
const result = await vault.createBankToken(options);
|
|
1913
|
+
notifyTokenize();
|
|
1914
|
+
return result;
|
|
1915
|
+
}, [vault, notifyTokenize]);
|
|
1916
|
+
const ready = vault !== null && vault.isReady && mountedCount > 0 && readyCount >= mountedCount;
|
|
1917
|
+
return { createToken, createBankToken, ready, initError, tokenizeCount };
|
|
1282
1918
|
}
|
|
1283
1919
|
const SKELETON_STYLE = {
|
|
1284
1920
|
height: 46,
|
|
@@ -1308,7 +1944,7 @@ const LOAD_ERROR_STYLE = {
|
|
|
1308
1944
|
color: '#991b1b',
|
|
1309
1945
|
fontSize: 13,
|
|
1310
1946
|
};
|
|
1311
|
-
function
|
|
1947
|
+
function OzFieldBase({ type, variant, style, placeholder, disabled, loadTimeoutMs, onChange, onFocus, onBlur, onReady, onLoadError, className, }) {
|
|
1312
1948
|
const containerRef = react.useRef(null);
|
|
1313
1949
|
const elementRef = react.useRef(null);
|
|
1314
1950
|
const [loaded, setLoaded] = react.useState(false);
|
|
@@ -1334,7 +1970,9 @@ function OzField({ type, style, placeholder, disabled, loadTimeoutMs, onChange,
|
|
|
1334
1970
|
injectShimmerCSS();
|
|
1335
1971
|
setLoaded(false);
|
|
1336
1972
|
setLoadError(null);
|
|
1337
|
-
const element =
|
|
1973
|
+
const element = variant === 'bank'
|
|
1974
|
+
? vault.createBankElement(type, { style, placeholder, disabled, loadTimeoutMs })
|
|
1975
|
+
: vault.createElement(type, { style, placeholder, disabled, loadTimeoutMs });
|
|
1338
1976
|
elementRef.current = element;
|
|
1339
1977
|
notifyMount();
|
|
1340
1978
|
element.on('ready', () => {
|
|
@@ -1348,29 +1986,29 @@ function OzField({ type, style, placeholder, disabled, loadTimeoutMs, onChange,
|
|
|
1348
1986
|
element.on('blur', () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef); });
|
|
1349
1987
|
element.on('loaderror', (e) => {
|
|
1350
1988
|
var _a, _b;
|
|
1351
|
-
const err = (_a = e === null || e === void 0 ? void 0 : e.error) !== null && _a !== void 0 ? _a : 'Failed to load card field';
|
|
1989
|
+
const err = (_a = e === null || e === void 0 ? void 0 : e.error) !== null && _a !== void 0 ? _a : (variant === 'bank' ? 'Failed to load bank field' : 'Failed to load card field');
|
|
1352
1990
|
setLoadError(err);
|
|
1353
1991
|
(_b = onLoadErrorRef.current) === null || _b === void 0 ? void 0 : _b.call(onLoadErrorRef, err);
|
|
1354
1992
|
});
|
|
1355
1993
|
element.mount(containerRef.current);
|
|
1356
1994
|
return () => {
|
|
1357
|
-
element.
|
|
1995
|
+
element.destroy();
|
|
1358
1996
|
elementRef.current = null;
|
|
1359
1997
|
setLoaded(false);
|
|
1360
1998
|
setLoadError(null);
|
|
1361
1999
|
notifyUnmount();
|
|
1362
2000
|
};
|
|
1363
2001
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1364
|
-
}, [vault, type]);
|
|
2002
|
+
}, [vault, type, loadTimeoutMs]);
|
|
1365
2003
|
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', position: 'relative' }, children: [loadError && jsxRuntime.jsx("div", { style: LOAD_ERROR_STYLE, role: "alert", children: loadError }), !loaded && !loadError && jsxRuntime.jsx("div", { style: SKELETON_STYLE }), jsxRuntime.jsx("div", { ref: containerRef, style: Object.assign({ width: '100%', opacity: loaded ? 1 : 0, transition: 'opacity 0.2s' }, (loadError ? { display: 'none' } : {})) })] }));
|
|
1366
2004
|
}
|
|
1367
2005
|
// ─── Public field components ──────────────────────────────────────────────────
|
|
1368
2006
|
/** Renders a PCI-isolated card number input inside an Ozura iframe. */
|
|
1369
|
-
const OzCardNumber = (props) => jsxRuntime.jsx(
|
|
2007
|
+
const OzCardNumber = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "cardNumber", variant: "card" }, props));
|
|
1370
2008
|
/** Renders a PCI-isolated expiration date input inside an Ozura iframe. */
|
|
1371
|
-
const OzExpiry = (props) => jsxRuntime.jsx(
|
|
2009
|
+
const OzExpiry = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "expirationDate", variant: "card" }, props));
|
|
1372
2010
|
/** Renders a PCI-isolated CVV input inside an Ozura iframe. */
|
|
1373
|
-
const OzCvv = (props) => jsxRuntime.jsx(
|
|
2011
|
+
const OzCvv = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "cvv", variant: "card" }, props));
|
|
1374
2012
|
const DEFAULT_ERROR_STYLE = {
|
|
1375
2013
|
color: '#dc2626',
|
|
1376
2014
|
fontSize: 13,
|
|
@@ -1384,6 +2022,11 @@ const DEFAULT_LABEL_STYLE = {
|
|
|
1384
2022
|
marginBottom: 4,
|
|
1385
2023
|
color: '#374151',
|
|
1386
2024
|
};
|
|
2025
|
+
function renderFieldLabel(text, labelClassName, style) {
|
|
2026
|
+
if (!text)
|
|
2027
|
+
return null;
|
|
2028
|
+
return jsxRuntime.jsx("label", { className: labelClassName, style: style, children: text });
|
|
2029
|
+
}
|
|
1387
2030
|
function mergeStyles(base, override) {
|
|
1388
2031
|
if (!override)
|
|
1389
2032
|
return base;
|
|
@@ -1413,7 +2056,8 @@ function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName,
|
|
|
1413
2056
|
expiry: null,
|
|
1414
2057
|
cvv: null,
|
|
1415
2058
|
});
|
|
1416
|
-
const
|
|
2059
|
+
const readyFieldTypes = react.useRef(new Set());
|
|
2060
|
+
const onReadyFiredRef = react.useRef(false);
|
|
1417
2061
|
const vaultRef = react.useRef(vault);
|
|
1418
2062
|
const onChangeRef = react.useRef(onChange);
|
|
1419
2063
|
const onReadyRef = react.useRef(onReady);
|
|
@@ -1425,17 +2069,41 @@ function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName,
|
|
|
1425
2069
|
react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
1426
2070
|
// When the vault is recreated (e.g. appearance/fonts props change on OzElements),
|
|
1427
2071
|
// context readyCount is reset but this ref is not. Reset so onReady fires once when all 3 are ready.
|
|
2072
|
+
// The cleanup resets readyFieldTypes when the component unmounts (covers React StrictMode double-invoke
|
|
2073
|
+
// and SPA scenarios where the parent re-mounts this component).
|
|
1428
2074
|
react.useEffect(() => {
|
|
1429
2075
|
if (vault !== vaultRef.current) {
|
|
1430
2076
|
vaultRef.current = vault;
|
|
1431
|
-
|
|
2077
|
+
readyFieldTypes.current = new Set();
|
|
2078
|
+
onReadyFiredRef.current = false;
|
|
1432
2079
|
}
|
|
2080
|
+
return () => {
|
|
2081
|
+
readyFieldTypes.current = new Set();
|
|
2082
|
+
onReadyFiredRef.current = false;
|
|
2083
|
+
};
|
|
1433
2084
|
}, [vault]);
|
|
1434
2085
|
const [error, setError] = react.useState();
|
|
1435
|
-
const
|
|
2086
|
+
const handleCardNumberReady = react.useCallback(() => {
|
|
2087
|
+
var _a;
|
|
2088
|
+
readyFieldTypes.current.add('cardNumber');
|
|
2089
|
+
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2090
|
+
onReadyFiredRef.current = true;
|
|
2091
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2092
|
+
}
|
|
2093
|
+
}, []);
|
|
2094
|
+
const handleExpiryReady = react.useCallback(() => {
|
|
1436
2095
|
var _a;
|
|
1437
|
-
|
|
1438
|
-
if (
|
|
2096
|
+
readyFieldTypes.current.add('expiry');
|
|
2097
|
+
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2098
|
+
onReadyFiredRef.current = true;
|
|
2099
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2100
|
+
}
|
|
2101
|
+
}, []);
|
|
2102
|
+
const handleCvvReady = react.useCallback(() => {
|
|
2103
|
+
var _a;
|
|
2104
|
+
readyFieldTypes.current.add('cvv');
|
|
2105
|
+
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2106
|
+
onReadyFiredRef.current = true;
|
|
1439
2107
|
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
1440
2108
|
}
|
|
1441
2109
|
}, []);
|
|
@@ -1454,34 +2122,116 @@ function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName,
|
|
|
1454
2122
|
fields: Object.assign({}, fieldState.current),
|
|
1455
2123
|
});
|
|
1456
2124
|
}, []);
|
|
1457
|
-
const gapValue = typeof gap === 'number' ? gap : undefined;
|
|
1458
2125
|
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
1459
2126
|
const resolvedLabelStyle = labelStyle
|
|
1460
2127
|
? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
|
|
1461
|
-
const renderLabel = (text) =>
|
|
1462
|
-
if (!text)
|
|
1463
|
-
return null;
|
|
1464
|
-
return (jsxRuntime.jsx("label", { className: labelClassName, style: resolvedLabelStyle, children: text }));
|
|
1465
|
-
};
|
|
2128
|
+
const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
|
|
1466
2129
|
const showError = !hideErrors && error;
|
|
1467
2130
|
const errorNode = showError
|
|
1468
2131
|
? renderError
|
|
1469
2132
|
? renderError(error)
|
|
1470
2133
|
: (jsxRuntime.jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
|
|
1471
2134
|
: null;
|
|
1472
|
-
const cardNumberField = (jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cardNumber), jsxRuntime.jsx(OzCardNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cardNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.cardNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cardNumber) !== null && _a !== void 0 ? _a : 'Card number', disabled: disabled, onChange: (e) => { fieldState.current.cardNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cardNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cardNumber'); }, onReady:
|
|
1473
|
-
const expiryField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.expiry), jsxRuntime.jsx(OzExpiry, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.expiry), className: classNames === null || classNames === void 0 ? void 0 : classNames.expiry, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.expiry) !== null && _b !== void 0 ? _b : 'MM / YY', disabled: disabled, onChange: (e) => { fieldState.current.expiry = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'expiry'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'expiry'); }, onReady:
|
|
1474
|
-
const cvvField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cvv), jsxRuntime.jsx(OzCvv, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cvv), className: classNames === null || classNames === void 0 ? void 0 : classNames.cvv, placeholder: (_c = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cvv) !== null && _c !== void 0 ? _c : 'CVV', disabled: disabled, onChange: (e) => { fieldState.current.cvv = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cvv'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cvv'); }, onReady:
|
|
2135
|
+
const cardNumberField = (jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cardNumber), jsxRuntime.jsx(OzCardNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cardNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.cardNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cardNumber) !== null && _a !== void 0 ? _a : 'Card number', disabled: disabled, onChange: (e) => { fieldState.current.cardNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cardNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cardNumber'); }, onReady: handleCardNumberReady })] }));
|
|
2136
|
+
const expiryField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.expiry), jsxRuntime.jsx(OzExpiry, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.expiry), className: classNames === null || classNames === void 0 ? void 0 : classNames.expiry, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.expiry) !== null && _b !== void 0 ? _b : 'MM / YY', disabled: disabled, onChange: (e) => { fieldState.current.expiry = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'expiry'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'expiry'); }, onReady: handleExpiryReady })] }));
|
|
2137
|
+
const cvvField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cvv), jsxRuntime.jsx(OzCvv, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cvv), className: classNames === null || classNames === void 0 ? void 0 : classNames.cvv, placeholder: (_c = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cvv) !== null && _c !== void 0 ? _c : 'CVV', disabled: disabled, onChange: (e) => { fieldState.current.cvv = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cvv'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cvv'); }, onReady: handleCvvReady })] }));
|
|
1475
2138
|
if (layout === 'rows') {
|
|
1476
2139
|
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [cardNumberField, expiryField, cvvField, errorNode] }));
|
|
1477
2140
|
}
|
|
1478
|
-
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%' }, children: [cardNumberField, jsxRuntime.jsxs("div", { className: classNames === null || classNames === void 0 ? void 0 : classNames.row, style: { display: 'flex', gap:
|
|
2141
|
+
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%' }, children: [cardNumberField, jsxRuntime.jsxs("div", { className: classNames === null || classNames === void 0 ? void 0 : classNames.row, style: { display: 'flex', gap: gapStr, marginTop: gapStr }, children: [expiryField, cvvField] }), errorNode] }));
|
|
2142
|
+
}
|
|
2143
|
+
// ─── Public bank field components ─────────────────────────────────────────────
|
|
2144
|
+
/** Renders a PCI-isolated bank account number input inside an Ozura iframe. */
|
|
2145
|
+
const OzBankAccountNumber = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "accountNumber", variant: "bank" }, props));
|
|
2146
|
+
/** Renders a PCI-isolated routing number input inside an Ozura iframe. */
|
|
2147
|
+
const OzBankRoutingNumber = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "routingNumber", variant: "bank" }, props));
|
|
2148
|
+
/**
|
|
2149
|
+
* Combined bank account input — renders account number and routing number in a
|
|
2150
|
+
* single component with built-in layout, loading skeletons, and inline error display.
|
|
2151
|
+
*
|
|
2152
|
+
* For maximum layout control, use `<OzBankAccountNumber />` and `<OzBankRoutingNumber />`
|
|
2153
|
+
* individually instead.
|
|
2154
|
+
*/
|
|
2155
|
+
function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassName, gap = 8, hideErrors = false, errorStyle, errorClassName, renderError, onChange, onReady, onFocus, onBlur, disabled, className, placeholders, }) {
|
|
2156
|
+
var _a, _b;
|
|
2157
|
+
const { vault } = react.useContext(OzContext);
|
|
2158
|
+
const fieldState = react.useRef({
|
|
2159
|
+
accountNumber: null,
|
|
2160
|
+
routingNumber: null,
|
|
2161
|
+
});
|
|
2162
|
+
const readyFieldTypes = react.useRef(new Set());
|
|
2163
|
+
const onReadyFiredRef = react.useRef(false);
|
|
2164
|
+
const vaultRef = react.useRef(vault);
|
|
2165
|
+
const onChangeRef = react.useRef(onChange);
|
|
2166
|
+
const onReadyRef = react.useRef(onReady);
|
|
2167
|
+
const onFocusRef = react.useRef(onFocus);
|
|
2168
|
+
const onBlurRef = react.useRef(onBlur);
|
|
2169
|
+
react.useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
|
|
2170
|
+
react.useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
|
|
2171
|
+
react.useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
|
|
2172
|
+
react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
2173
|
+
react.useEffect(() => {
|
|
2174
|
+
if (vault !== vaultRef.current) {
|
|
2175
|
+
vaultRef.current = vault;
|
|
2176
|
+
readyFieldTypes.current = new Set();
|
|
2177
|
+
onReadyFiredRef.current = false;
|
|
2178
|
+
}
|
|
2179
|
+
return () => {
|
|
2180
|
+
readyFieldTypes.current = new Set();
|
|
2181
|
+
onReadyFiredRef.current = false;
|
|
2182
|
+
};
|
|
2183
|
+
}, [vault]);
|
|
2184
|
+
const [error, setError] = react.useState();
|
|
2185
|
+
const handleAccountReady = react.useCallback(() => {
|
|
2186
|
+
var _a;
|
|
2187
|
+
readyFieldTypes.current.add('accountNumber');
|
|
2188
|
+
if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
|
|
2189
|
+
onReadyFiredRef.current = true;
|
|
2190
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2191
|
+
}
|
|
2192
|
+
}, []);
|
|
2193
|
+
const handleRoutingReady = react.useCallback(() => {
|
|
2194
|
+
var _a;
|
|
2195
|
+
readyFieldTypes.current.add('routingNumber');
|
|
2196
|
+
if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
|
|
2197
|
+
onReadyFiredRef.current = true;
|
|
2198
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2199
|
+
}
|
|
2200
|
+
}, []);
|
|
2201
|
+
const emitChange = react.useCallback(() => {
|
|
2202
|
+
var _a;
|
|
2203
|
+
const { accountNumber, routingNumber } = fieldState.current;
|
|
2204
|
+
const complete = !!((accountNumber === null || accountNumber === void 0 ? void 0 : accountNumber.complete) && (accountNumber === null || accountNumber === void 0 ? void 0 : accountNumber.valid) &&
|
|
2205
|
+
(routingNumber === null || routingNumber === void 0 ? void 0 : routingNumber.complete) && (routingNumber === null || routingNumber === void 0 ? void 0 : routingNumber.valid));
|
|
2206
|
+
const err = (accountNumber === null || accountNumber === void 0 ? void 0 : accountNumber.error) || (routingNumber === null || routingNumber === void 0 ? void 0 : routingNumber.error) || undefined;
|
|
2207
|
+
setError(err);
|
|
2208
|
+
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, {
|
|
2209
|
+
complete,
|
|
2210
|
+
error: err,
|
|
2211
|
+
fields: Object.assign({}, fieldState.current),
|
|
2212
|
+
});
|
|
2213
|
+
}, []);
|
|
2214
|
+
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
2215
|
+
const resolvedLabelStyle = labelStyle
|
|
2216
|
+
? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
|
|
2217
|
+
const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
|
|
2218
|
+
const showError = !hideErrors && error;
|
|
2219
|
+
const errorNode = showError
|
|
2220
|
+
? renderError
|
|
2221
|
+
? renderError(error)
|
|
2222
|
+
: (jsxRuntime.jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
|
|
2223
|
+
: null;
|
|
2224
|
+
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsxRuntime.jsx(OzBankAccountNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.accountNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.accountNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.accountNumber) !== null && _a !== void 0 ? _a : 'Account number', disabled: disabled, onChange: (e) => { fieldState.current.accountNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'accountNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'accountNumber'); }, onReady: handleAccountReady })] }), jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.routingNumber), jsxRuntime.jsx(OzBankRoutingNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.routingNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.routingNumber, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.routingNumber) !== null && _b !== void 0 ? _b : 'Routing number', disabled: disabled, onChange: (e) => { fieldState.current.routingNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'routingNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'routingNumber'); }, onReady: handleRoutingReady })] }), errorNode] }));
|
|
1479
2225
|
}
|
|
1480
2226
|
|
|
2227
|
+
exports.OzBankAccountNumber = OzBankAccountNumber;
|
|
2228
|
+
exports.OzBankCard = OzBankCard;
|
|
2229
|
+
exports.OzBankRoutingNumber = OzBankRoutingNumber;
|
|
1481
2230
|
exports.OzCard = OzCard;
|
|
1482
2231
|
exports.OzCardNumber = OzCardNumber;
|
|
1483
2232
|
exports.OzCvv = OzCvv;
|
|
1484
2233
|
exports.OzElements = OzElements;
|
|
1485
2234
|
exports.OzExpiry = OzExpiry;
|
|
2235
|
+
exports.createFetchWaxKey = createFetchWaxKey;
|
|
1486
2236
|
exports.useOzElements = useOzElements;
|
|
1487
2237
|
//# sourceMappingURL=index.cjs.js.map
|