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