@ozura/elements 1.2.1 → 1.2.3

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.
Files changed (60) hide show
  1. package/README.md +105 -0
  2. package/dist/frame/element-frame.js +49 -4
  3. package/dist/frame/element-frame.js.map +1 -1
  4. package/dist/frame/tokenizer-frame.js +145 -61
  5. package/dist/frame/tokenizer-frame.js.map +1 -1
  6. package/dist/oz-elements.esm.js +221 -97
  7. package/dist/oz-elements.esm.js.map +1 -1
  8. package/dist/oz-elements.umd.js +221 -97
  9. package/dist/oz-elements.umd.js.map +1 -1
  10. package/dist/react/frame/elementFrame.d.ts +9 -0
  11. package/dist/react/frame/tokenizerFrame.d.ts +17 -1
  12. package/dist/react/index.cjs.js +221 -97
  13. package/dist/react/index.cjs.js.map +1 -1
  14. package/dist/react/index.esm.js +221 -97
  15. package/dist/react/index.esm.js.map +1 -1
  16. package/dist/react/sdk/OzElement.d.ts +4 -1
  17. package/dist/react/sdk/OzVault.d.ts +3 -9
  18. package/dist/react/sdk/createSessionFetcher.d.ts +2 -2
  19. package/dist/react/server/index.d.ts +26 -0
  20. package/dist/react/types/index.d.ts +30 -11
  21. package/dist/react/utils/billingUtils.d.ts +2 -1
  22. package/dist/react/vue/index.d.ts +88 -0
  23. package/dist/server/frame/elementFrame.d.ts +9 -0
  24. package/dist/server/frame/tokenizerFrame.d.ts +17 -1
  25. package/dist/server/index.cjs.js +65 -24
  26. package/dist/server/index.cjs.js.map +1 -1
  27. package/dist/server/index.esm.js +65 -24
  28. package/dist/server/index.esm.js.map +1 -1
  29. package/dist/server/sdk/OzElement.d.ts +4 -1
  30. package/dist/server/sdk/OzVault.d.ts +3 -9
  31. package/dist/server/sdk/createSessionFetcher.d.ts +2 -2
  32. package/dist/server/server/index.d.ts +26 -0
  33. package/dist/server/types/index.d.ts +30 -11
  34. package/dist/server/utils/billingUtils.d.ts +2 -1
  35. package/dist/server/vue/index.d.ts +88 -0
  36. package/dist/types/frame/elementFrame.d.ts +9 -0
  37. package/dist/types/frame/tokenizerFrame.d.ts +17 -1
  38. package/dist/types/sdk/OzElement.d.ts +4 -1
  39. package/dist/types/sdk/OzVault.d.ts +3 -9
  40. package/dist/types/sdk/createSessionFetcher.d.ts +2 -2
  41. package/dist/types/server/index.d.ts +26 -0
  42. package/dist/types/types/index.d.ts +30 -11
  43. package/dist/types/utils/billingUtils.d.ts +2 -1
  44. package/dist/types/vue/index.d.ts +88 -0
  45. package/dist/vue/frame/protocol.d.ts +12 -0
  46. package/dist/vue/index.cjs.js +2335 -0
  47. package/dist/vue/index.cjs.js.map +1 -0
  48. package/dist/vue/index.esm.js +2327 -0
  49. package/dist/vue/index.esm.js.map +1 -0
  50. package/dist/vue/sdk/OzElement.d.ts +99 -0
  51. package/dist/vue/sdk/OzVault.d.ts +250 -0
  52. package/dist/vue/sdk/createSessionFetcher.d.ts +29 -0
  53. package/dist/vue/sdk/errors.d.ts +65 -0
  54. package/dist/vue/sdk/index.d.ts +14 -0
  55. package/dist/vue/types/index.d.ts +667 -0
  56. package/dist/vue/utils/appearance.d.ts +22 -0
  57. package/dist/vue/utils/billingUtils.d.ts +61 -0
  58. package/dist/vue/utils/uuid.d.ts +12 -0
  59. package/dist/vue/vue/index.d.ts +88 -0
  60. package/package.json +15 -3
@@ -0,0 +1,2335 @@
1
+ 'use strict';
2
+
3
+ var vue = require('vue');
4
+
5
+ const THEME_DEFAULT = {
6
+ base: {
7
+ color: '#1a1a2e',
8
+ fontSize: '16px',
9
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
10
+ lineHeight: '1.5',
11
+ padding: '12px 14px',
12
+ backgroundColor: 'transparent',
13
+ caretColor: '#6366f1',
14
+ transition: 'color .15s ease',
15
+ },
16
+ focus: {
17
+ color: '#111827',
18
+ },
19
+ invalid: {
20
+ color: '#dc2626',
21
+ },
22
+ complete: {
23
+ color: '#16a34a',
24
+ },
25
+ placeholder: {
26
+ color: '#9ca3af',
27
+ },
28
+ };
29
+ const THEME_NIGHT = {
30
+ base: {
31
+ color: '#e5e7eb',
32
+ fontSize: '16px',
33
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
34
+ lineHeight: '1.5',
35
+ padding: '12px 14px',
36
+ backgroundColor: 'transparent',
37
+ caretColor: '#818cf8',
38
+ transition: 'color .15s ease',
39
+ },
40
+ focus: {
41
+ color: '#f9fafb',
42
+ },
43
+ invalid: {
44
+ color: '#fca5a5',
45
+ },
46
+ complete: {
47
+ color: '#86efac',
48
+ },
49
+ placeholder: {
50
+ color: '#6b7280',
51
+ },
52
+ };
53
+ const THEME_FLAT = {
54
+ base: {
55
+ color: '#374151',
56
+ fontSize: '16px',
57
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
58
+ lineHeight: '1.5',
59
+ padding: '12px 14px',
60
+ backgroundColor: 'transparent',
61
+ caretColor: '#6366f1',
62
+ transition: 'color .15s ease',
63
+ borderBottom: '2px solid #d1d5db',
64
+ borderRadius: '0px',
65
+ },
66
+ focus: {
67
+ color: '#111827',
68
+ borderBottom: '2px solid #6366f1',
69
+ },
70
+ invalid: {
71
+ color: '#dc2626',
72
+ borderBottom: '2px solid #dc2626',
73
+ },
74
+ complete: {
75
+ color: '#16a34a',
76
+ borderBottom: '2px solid #16a34a',
77
+ },
78
+ placeholder: {
79
+ color: '#9ca3af',
80
+ },
81
+ };
82
+ const THEMES = {
83
+ default: THEME_DEFAULT,
84
+ night: THEME_NIGHT,
85
+ flat: THEME_FLAT,
86
+ };
87
+ function variablesToStyle(vars) {
88
+ const base = {};
89
+ const focus = {};
90
+ const invalid = {};
91
+ const complete = {};
92
+ const placeholder = {};
93
+ if (vars.colorText)
94
+ base.color = vars.colorText;
95
+ if (vars.colorBackground)
96
+ base.backgroundColor = vars.colorBackground;
97
+ if (vars.fontFamily)
98
+ base.fontFamily = vars.fontFamily;
99
+ if (vars.fontSize)
100
+ base.fontSize = vars.fontSize;
101
+ if (vars.fontWeight)
102
+ base.fontWeight = vars.fontWeight;
103
+ if (vars.letterSpacing)
104
+ base.letterSpacing = vars.letterSpacing;
105
+ if (vars.lineHeight)
106
+ base.lineHeight = vars.lineHeight;
107
+ if (vars.padding)
108
+ base.padding = vars.padding;
109
+ if (vars.colorPrimary) {
110
+ focus.caretColor = vars.colorPrimary;
111
+ base.caretColor = vars.colorPrimary;
112
+ }
113
+ if (vars.colorDanger)
114
+ invalid.color = vars.colorDanger;
115
+ if (vars.colorSuccess)
116
+ complete.color = vars.colorSuccess;
117
+ if (vars.colorPlaceholder)
118
+ placeholder.color = vars.colorPlaceholder;
119
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (Object.keys(base).length > 0 ? { base } : {})), (Object.keys(focus).length > 0 ? { focus } : {})), (Object.keys(invalid).length > 0 ? { invalid } : {})), (Object.keys(complete).length > 0 ? { complete } : {})), (Object.keys(placeholder).length > 0 ? { placeholder } : {}));
120
+ }
121
+ function mergeStyleConfigs(a, b) {
122
+ return {
123
+ base: Object.assign(Object.assign({}, a.base), b.base),
124
+ focus: Object.assign(Object.assign({}, a.focus), b.focus),
125
+ invalid: Object.assign(Object.assign({}, a.invalid), b.invalid),
126
+ complete: Object.assign(Object.assign({}, a.complete), b.complete),
127
+ placeholder: Object.assign(Object.assign({}, a.placeholder), b.placeholder),
128
+ };
129
+ }
130
+ /**
131
+ * Resolves an `Appearance` config into a flat `ElementStyleConfig`.
132
+ * Resolution order: theme defaults → variable overrides.
133
+ * The returned config is then used as the "base appearance" that
134
+ * per-element `style` overrides merge on top of.
135
+ *
136
+ * @remarks
137
+ * - `appearance: undefined` → no styles injected (element iframes use their
138
+ * own minimal built-in defaults).
139
+ * - `appearance: {}` or `appearance: { variables: {...} }` without an explicit
140
+ * `theme` → the `'default'` theme is used as the base. Omitting `theme`
141
+ * does NOT mean "no theme" — it means `theme: 'default'`. To opt out of
142
+ * the preset themes entirely, use per-element `style` overrides without
143
+ * passing an `appearance` prop at all.
144
+ */
145
+ function resolveAppearance(appearance) {
146
+ var _a, _b;
147
+ if (!appearance)
148
+ return undefined;
149
+ const theme = (_b = THEMES[(_a = appearance.theme) !== null && _a !== void 0 ? _a : 'default']) !== null && _b !== void 0 ? _b : THEMES.default;
150
+ if (!appearance.variables)
151
+ return theme;
152
+ const varStyles = variablesToStyle(appearance.variables);
153
+ return mergeStyleConfigs(theme, varStyles);
154
+ }
155
+ /**
156
+ * Merges a resolved appearance with per-element style overrides.
157
+ * Element styles always win over appearance styles.
158
+ */
159
+ function mergeAppearanceWithElementStyle(appearance, elementStyle) {
160
+ if (!appearance && !elementStyle)
161
+ return undefined;
162
+ if (!appearance)
163
+ return elementStyle;
164
+ if (!elementStyle)
165
+ return appearance;
166
+ return mergeStyleConfigs(appearance, elementStyle);
167
+ }
168
+
169
+ /**
170
+ * errors.ts — error types and normalisation for OzElements.
171
+ *
172
+ * Three normalisation functions:
173
+ * - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
174
+ * - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
175
+ * - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
176
+ *
177
+ * Error keys in normalizeCardSaleError are taken directly from checkout's
178
+ * errorMapping.ts so the same error strings produce the same user-facing copy.
179
+ */
180
+ const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
181
+ /** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
182
+ function isOzErrorCode(value) {
183
+ return typeof value === 'string' && OZ_ERROR_CODES.has(value);
184
+ }
185
+ class OzError extends Error {
186
+ constructor(message, raw, errorCode) {
187
+ super(message);
188
+ this.name = 'OzError';
189
+ this.raw = raw !== null && raw !== void 0 ? raw : message;
190
+ this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
191
+ this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
192
+ }
193
+ }
194
+ /** Shared patterns that apply to both card and bank vault errors. */
195
+ function normalizeCommonVaultError(msg) {
196
+ if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
197
+ return 'Authentication failed. Check your vault API key configuration.';
198
+ }
199
+ if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
200
+ return 'A network error occurred. Please check your connection and try again.';
201
+ }
202
+ if (msg.includes('timeout') || msg.includes('timed out')) {
203
+ return 'The request timed out. Please try again.';
204
+ }
205
+ if (msg.includes('http 5') || /\b5\d{2}\b/.test(msg)) {
206
+ return 'A server error occurred. Please try again shortly.';
207
+ }
208
+ return null;
209
+ }
210
+ /**
211
+ * Maps a raw vault /tokenize error string to a user-facing message for card flows.
212
+ * Falls back to the original string if no pattern matches.
213
+ */
214
+ function normalizeVaultError(raw) {
215
+ const msg = raw.toLowerCase();
216
+ if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
217
+ return 'The card number is invalid. Please check and try again.';
218
+ }
219
+ if (msg.includes('expir')) {
220
+ return 'The card expiration date is invalid or the card has expired.';
221
+ }
222
+ if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
223
+ return 'The CVV code is invalid. Please check and try again.';
224
+ }
225
+ if (msg.includes('insufficient funds')) {
226
+ return 'Your card has insufficient funds. Please use a different card.';
227
+ }
228
+ if (msg.includes('declined') || msg.includes('do not honor')) {
229
+ return 'Your card was declined. Please try a different card or contact your bank.';
230
+ }
231
+ const common = normalizeCommonVaultError(msg);
232
+ if (common)
233
+ return common;
234
+ return raw;
235
+ }
236
+ /**
237
+ * Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
238
+ * Uses bank-specific pattern matching so card-specific messages are never shown for
239
+ * bank errors. Falls back to the original string if no pattern matches.
240
+ */
241
+ function normalizeBankVaultError(raw) {
242
+ const msg = raw.toLowerCase();
243
+ if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
244
+ return 'The bank account number is invalid. Please check and try again.';
245
+ }
246
+ if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
247
+ return 'The routing number is invalid. Please check and try again.';
248
+ }
249
+ const common = normalizeCommonVaultError(msg);
250
+ if (common)
251
+ return common;
252
+ return raw;
253
+ }
254
+
255
+ /**
256
+ * Generates a RFC 4122 v4 UUID.
257
+ *
258
+ * Uses `crypto.randomUUID()` when available (Chrome 92+, Firefox 95+,
259
+ * Safari 15.4+, Node 14.17+) and falls back to `crypto.getRandomValues()`
260
+ * for older environments (Safari 14, some embedded WebViews, older Node).
261
+ *
262
+ * Both paths use the same CSPRNG — the difference is only in API surface.
263
+ *
264
+ * @internal
265
+ */
266
+ function uuid() {
267
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
268
+ return crypto.randomUUID();
269
+ }
270
+ // Fallback: build UUID v4 from random bytes
271
+ const bytes = new Uint8Array(16);
272
+ crypto.getRandomValues(bytes);
273
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
274
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant RFC 4122
275
+ const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
276
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
277
+ }
278
+
279
+ 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;
280
+ const CSS_BREAKOUT = /[{};<>]/;
281
+ const MAX_CSS_VALUE_LEN = 200;
282
+ function sanitizeStyleObj(obj) {
283
+ if (!obj)
284
+ return obj;
285
+ const clean = {};
286
+ for (const [k, v] of Object.entries(obj)) {
287
+ if (v === undefined) {
288
+ clean[k] = v;
289
+ continue;
290
+ }
291
+ if (typeof v !== 'string' || v.length > MAX_CSS_VALUE_LEN || BLOCKED_CSS_PATTERNS.test(v) || CSS_BREAKOUT.test(v))
292
+ continue;
293
+ clean[k] = v;
294
+ }
295
+ return clean;
296
+ }
297
+ function sanitizeStyles(style) {
298
+ if (!style)
299
+ return style;
300
+ return {
301
+ base: sanitizeStyleObj(style.base),
302
+ focus: sanitizeStyleObj(style.focus),
303
+ invalid: sanitizeStyleObj(style.invalid),
304
+ complete: sanitizeStyleObj(style.complete),
305
+ placeholder: sanitizeStyleObj(style.placeholder),
306
+ };
307
+ }
308
+ function sanitizeOptions(options) {
309
+ var _a;
310
+ const result = Object.assign(Object.assign({}, options), { placeholder: (_a = options.placeholder) === null || _a === void 0 ? void 0 : _a.slice(0, 100) });
311
+ // Coerce to boolean so a string "false" (truthy in JS) does not accidentally
312
+ // disable the input when the SDK is consumed from plain JavaScript.
313
+ if (options.disabled !== undefined) {
314
+ result.disabled = Boolean(options.disabled);
315
+ }
316
+ // Only set style when provided; omitting it avoids clobbering existing style
317
+ // when merging (e.g. update({ placeholder: 'new' }) must not overwrite style with undefined).
318
+ if (options.style !== undefined) {
319
+ result.style = sanitizeStyles(options.style);
320
+ }
321
+ return result;
322
+ }
323
+ /**
324
+ * A proxy for one Ozura iframe element. Merchants interact with this object;
325
+ * it never holds raw card data — all sensitive values live in the iframe.
326
+ */
327
+ class OzElement {
328
+ constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy, debug = false) {
329
+ this.iframe = null;
330
+ this._frameWindow = null;
331
+ this._ready = false;
332
+ this._destroyed = false;
333
+ this._loadTimer = null;
334
+ this.pendingMessages = [];
335
+ this.handlers = new Map();
336
+ this.debug = false;
337
+ this.elementType = elementType;
338
+ this.options = sanitizeOptions(options);
339
+ this.vaultId = vaultId;
340
+ this.frameBaseUrl = frameBaseUrl;
341
+ this.frameOrigin = new URL(frameBaseUrl).origin;
342
+ this.fonts = fonts;
343
+ this.appearanceStyle = appearanceStyle;
344
+ this.frameId = `oz-${elementType}-${uuid()}`;
345
+ this._onDestroy = onDestroy;
346
+ this.debug = debug;
347
+ }
348
+ /** The element type this proxy represents. */
349
+ get type() {
350
+ return this.elementType;
351
+ }
352
+ /** True once the element iframe has loaded and signalled ready. */
353
+ get isReady() {
354
+ return this._ready;
355
+ }
356
+ /**
357
+ * Mounts the element iframe into a container.
358
+ * Accepts either a CSS selector string or a direct HTMLElement reference
359
+ * (useful when integrating with React refs).
360
+ */
361
+ mount(target) {
362
+ var _a, _b;
363
+ if (this._destroyed)
364
+ throw new OzError('Cannot mount a destroyed element.');
365
+ if (this.iframe)
366
+ this.unmount();
367
+ const container = typeof target === 'string'
368
+ ? document.querySelector(target)
369
+ : target;
370
+ if (!container)
371
+ throw new OzError(typeof target === 'string'
372
+ ? `Mount target not found — no element matches "${target}"`
373
+ : 'Mount target not found — the provided HTMLElement is null or undefined');
374
+ const iframe = document.createElement('iframe');
375
+ iframe.setAttribute('frameborder', '0');
376
+ iframe.setAttribute('scrolling', 'no');
377
+ iframe.setAttribute('allowtransparency', 'true');
378
+ iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
379
+ iframe.title = `Secure ${(_a = {
380
+ cardNumber: 'card number',
381
+ expirationDate: 'expiration date',
382
+ cvv: 'CVV',
383
+ accountNumber: 'account number',
384
+ routingNumber: 'routing number',
385
+ }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
386
+ // sandbox="allow-scripts" gives correct iframe isolation:
387
+ // - Scripts run (allow-scripts), so the field JS executes normally.
388
+ // - NO allow-same-origin: the frame cannot access window.parent's DOM,
389
+ // localStorage, or cookies — prevents sandbox escape even if served
390
+ // from the same origin.
391
+ // - NO allow-top-navigation: a rogue/compromised element frame cannot
392
+ // navigate window.top (clickjacking prevention).
393
+ // - NO allow-forms / allow-popups: reduces attack surface.
394
+ // Field values are delivered via postMessage, so no parent access is
395
+ // needed — allow-scripts alone is sufficient.
396
+ iframe.setAttribute('sandbox', 'allow-scripts');
397
+ // Use hash instead of query string — survives clean-URL redirects from static servers.
398
+ // parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
399
+ const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
400
+ const src = `${this.frameBaseUrl}/frame/element-frame.html#type=${this.elementType}&vaultId=${encodeURIComponent(this.vaultId)}&frameId=${encodeURIComponent(this.frameId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
401
+ iframe.src = src;
402
+ container.appendChild(iframe);
403
+ this.iframe = iframe;
404
+ this._frameWindow = iframe.contentWindow;
405
+ const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
406
+ this._loadTimer = setTimeout(() => {
407
+ if (!this._ready && !this._destroyed) {
408
+ this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
409
+ }
410
+ }, timeout);
411
+ }
412
+ /**
413
+ * Subscribe to an element event. Returns `this` for chaining.
414
+ * @param event - Event name: `'change'`, `'focus'`, `'blur'`, `'ready'`, or `'loaderror'`.
415
+ * @param callback - Handler invoked with the event payload.
416
+ */
417
+ on(event, callback) {
418
+ if (this._destroyed)
419
+ return this;
420
+ if (!this.handlers.has(event))
421
+ this.handlers.set(event, []);
422
+ this.handlers.get(event).push(callback);
423
+ return this;
424
+ }
425
+ /**
426
+ * Remove a previously registered event handler.
427
+ * Has no effect if the handler is not registered.
428
+ */
429
+ off(event, callback) {
430
+ const list = this.handlers.get(event);
431
+ if (list) {
432
+ const idx = list.indexOf(callback);
433
+ if (idx !== -1)
434
+ list.splice(idx, 1);
435
+ }
436
+ return this;
437
+ }
438
+ /**
439
+ * Subscribe to an event for a single invocation. The handler is automatically
440
+ * removed after it fires once.
441
+ */
442
+ once(event, callback) {
443
+ const wrapper = (payload) => {
444
+ this.off(event, wrapper);
445
+ callback(payload);
446
+ };
447
+ return this.on(event, wrapper);
448
+ }
449
+ /**
450
+ * Dynamically update element options (placeholder, style, etc.) without
451
+ * re-mounting the iframe. Only the provided keys are merged; omitted keys
452
+ * retain their current values.
453
+ */
454
+ update(options) {
455
+ if (this._destroyed)
456
+ return;
457
+ const safe = sanitizeOptions(options);
458
+ // Re-merge vault appearance when style is updated so focus/invalid/complete/
459
+ // placeholder buckets from the theme are not stripped by a partial style object.
460
+ if (safe.style !== undefined) {
461
+ safe.style = mergeAppearanceWithElementStyle(this.appearanceStyle, safe.style);
462
+ }
463
+ this.options = Object.assign(Object.assign({}, this.options), safe);
464
+ this.post({ type: 'OZ_UPDATE', options: safe });
465
+ }
466
+ /** Clear the current field value without removing the element from the DOM. */
467
+ clear() {
468
+ if (this._destroyed)
469
+ return;
470
+ this.post({ type: 'OZ_CLEAR' });
471
+ }
472
+ /**
473
+ * Removes the iframe from the DOM and resets internal state.
474
+ * Called automatically by `OzVault.destroy()`. Safe to call manually
475
+ * for partial teardown (e.g. swapping payment method in a SPA).
476
+ */
477
+ unmount() {
478
+ var _a;
479
+ if (this._destroyed)
480
+ return;
481
+ if (this._loadTimer) {
482
+ clearTimeout(this._loadTimer);
483
+ this._loadTimer = null;
484
+ }
485
+ (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.remove();
486
+ this.iframe = null;
487
+ this._frameWindow = null;
488
+ this._ready = false;
489
+ this.pendingMessages = [];
490
+ }
491
+ /** Programmatically focus this element's input. Used internally for auto-advance. */
492
+ focus() {
493
+ if (this._destroyed)
494
+ return;
495
+ this.post({ type: 'OZ_FOCUS_REQUEST' });
496
+ }
497
+ /** Programmatically blur this element's input. */
498
+ blur() {
499
+ if (this._destroyed)
500
+ return;
501
+ this.post({ type: 'OZ_BLUR_REQUEST' });
502
+ }
503
+ /**
504
+ * Permanently destroys this element: unmounts it, clears all event handlers,
505
+ * and prevents future use. Distinct from `unmount()` which allows re-mounting.
506
+ */
507
+ destroy() {
508
+ var _a;
509
+ this.unmount();
510
+ this.handlers.clear();
511
+ this._destroyed = true;
512
+ // Notify OzVault so it can prune the stale frameId entry from its elements
513
+ // and completionState maps. Without this, manually calling el.destroy() leaks
514
+ // map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
515
+ (_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
516
+ }
517
+ // ─── Called by OzVault ───────────────────────────────────────────────────
518
+ /**
519
+ * Sends OZ_BEGIN_COLLECT to the element iframe, transferring `port` so the
520
+ * iframe can post its field value directly to the tokenizer without going
521
+ * through the merchant page (no named-window lookup required).
522
+ * @internal
523
+ */
524
+ beginCollect(requestId, port) {
525
+ if (this._destroyed)
526
+ return;
527
+ this.sendWithTransfer({ type: 'OZ_BEGIN_COLLECT', requestId }, [port]);
528
+ }
529
+ /**
530
+ * Tell a CVV element how many digits to expect. Called automatically when card brand changes.
531
+ * @internal
532
+ */
533
+ setCvvLength(length) {
534
+ if (this._destroyed)
535
+ return;
536
+ this.post({ type: 'OZ_SET_CVV_LENGTH', length });
537
+ }
538
+ /** @internal */
539
+ handleMessage(msg) {
540
+ var _a, _b;
541
+ if (this._destroyed)
542
+ return;
543
+ switch (msg.type) {
544
+ case 'OZ_FRAME_READY': {
545
+ this._ready = true;
546
+ if (this._loadTimer) {
547
+ clearTimeout(this._loadTimer);
548
+ this._loadTimer = null;
549
+ }
550
+ this._frameWindow = (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
551
+ const mergedOptions = Object.assign(Object.assign({}, this.options), { style: mergeAppearanceWithElementStyle(this.appearanceStyle, this.options.style) });
552
+ this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId, debug: this.debug }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
553
+ this.pendingMessages.forEach(m => this.send(m));
554
+ this.pendingMessages = [];
555
+ this.emit('ready', undefined);
556
+ // Warn if the mount container collapses to zero height — the input will
557
+ // be invisible but functional, which is hard to debug. Check after one
558
+ // animation frame so the browser has completed layout. Guard against
559
+ // non-rendering environments (jsdom, SSR) where all rects are zero.
560
+ if (typeof requestAnimationFrame !== 'undefined') {
561
+ requestAnimationFrame(() => {
562
+ const inRealBrowser = document.documentElement.getBoundingClientRect().height > 0;
563
+ if (inRealBrowser && this.iframe && this.iframe.getBoundingClientRect().height === 0) {
564
+ console.warn(`[OzElement] "${this.elementType}" mounted but has zero height. ` +
565
+ `Check that the container has a visible height (not overflow:hidden or height:0).`);
566
+ }
567
+ });
568
+ }
569
+ break;
570
+ }
571
+ case 'OZ_CHANGE':
572
+ this.emit('change', {
573
+ empty: msg.empty,
574
+ complete: msg.complete,
575
+ valid: msg.valid,
576
+ cardBrand: msg.cardBrand,
577
+ month: msg.month,
578
+ year: msg.year,
579
+ error: msg.error,
580
+ });
581
+ break;
582
+ case 'OZ_FOCUS':
583
+ this.emit('focus', undefined);
584
+ break;
585
+ case 'OZ_BLUR':
586
+ this.emit('blur', {
587
+ empty: msg.empty,
588
+ complete: msg.complete,
589
+ valid: msg.valid,
590
+ error: msg.error,
591
+ });
592
+ break;
593
+ case 'OZ_RESIZE':
594
+ if (this.iframe) {
595
+ // Clamp to a sensible range to prevent a rogue or compromised frame
596
+ // from zeroing out the input or stretching the layout.
597
+ const h = typeof msg.height === 'number' ? msg.height : 0;
598
+ if (h > 0 && h <= 300) {
599
+ this.iframe.style.height = `${h}px`;
600
+ }
601
+ }
602
+ break;
603
+ }
604
+ }
605
+ // ─── Internal ────────────────────────────────────────────────────────────
606
+ emit(event, payload) {
607
+ const list = this.handlers.get(event);
608
+ if (!list)
609
+ return;
610
+ [...list].forEach(fn => {
611
+ try {
612
+ fn(payload);
613
+ }
614
+ catch (err) {
615
+ console.error(`[OzElement] Unhandled error in '${event}' listener:`, err);
616
+ }
617
+ });
618
+ }
619
+ post(data) {
620
+ const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
621
+ if (!this._ready) {
622
+ this.pendingMessages.push(msg);
623
+ }
624
+ else {
625
+ this.send(msg);
626
+ }
627
+ }
628
+ send(msg) {
629
+ var _a;
630
+ (_a = this._frameWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin);
631
+ }
632
+ /** Posts a message with transferable objects (e.g. MessagePort). Bypasses the
633
+ * pending-message queue — only call when the frame is already ready. */
634
+ sendWithTransfer(data, transfer) {
635
+ if (this._destroyed)
636
+ return;
637
+ if (!this._frameWindow) {
638
+ console.error('[OzElement] sendWithTransfer called before frame window is available — port will not be transferred. This is a bug.');
639
+ return;
640
+ }
641
+ const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
642
+ this._frameWindow.postMessage(msg, this.frameOrigin, transfer);
643
+ }
644
+ }
645
+
646
+ /**
647
+ * billingUtils.ts — billing detail validation and normalization.
648
+ *
649
+ * Mirrors the validation in checkout/page.tsx (pre-flight checks before cardSale)
650
+ * so that billing data passed to createToken() is guaranteed schema-compliant and
651
+ * ready to forward directly to the Ozura Pay API cardSale endpoint.
652
+ *
653
+ * All string fields enforced to 1–50 characters (cardSale schema constraint).
654
+ * State is normalized to 2-letter abbreviation for US and CA.
655
+ * Phone must be E.164 format (matches checkout's formatPhoneForAPI output).
656
+ */
657
+ // ─── Email ────────────────────────────────────────────────────────────────────
658
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
659
+ /** Returns true when the email is syntactically valid and ≤50 characters. */
660
+ function validateEmail(email) {
661
+ return EMAIL_RE.test(email) && email.length <= 50;
662
+ }
663
+ // ─── Phone ───────────────────────────────────────────────────────────────────
664
+ /**
665
+ * Validates E.164 phone format: starts with +, 1–3 digit country code,
666
+ * followed by 7–12 digits, max 15 digits total (E.164 spec cap = 16 chars
667
+ * including the leading +).
668
+ *
669
+ * Matches the output of checkout's formatPhoneForAPI() function.
670
+ * Examples: "+15551234567", "+447911123456", "+61412345678"
671
+ */
672
+ function validateE164Phone(phone) {
673
+ return /^\+[1-9]\d{6,14}$/.test(phone);
674
+ }
675
+ // ─── Field length ─────────────────────────────────────────────────────────────
676
+ /** Returns true when the string is non-empty and ≤50 characters (cardSale schema). */
677
+ function isValidBillingField(value) {
678
+ return value.length > 0 && value.length <= 50;
679
+ }
680
+ // ─── US state normalization ───────────────────────────────────────────────────
681
+ // Mirrors checkout's convertStateToAbbreviation() so the same input variants work.
682
+ const US_STATES = {
683
+ alabama: 'AL', alaska: 'AK', arizona: 'AZ', arkansas: 'AR',
684
+ california: 'CA', colorado: 'CO', connecticut: 'CT', delaware: 'DE',
685
+ 'district of columbia': 'DC', florida: 'FL', georgia: 'GA', hawaii: 'HI',
686
+ idaho: 'ID', illinois: 'IL', indiana: 'IN', iowa: 'IA', kansas: 'KS',
687
+ kentucky: 'KY', louisiana: 'LA', maine: 'ME', maryland: 'MD',
688
+ massachusetts: 'MA', michigan: 'MI', minnesota: 'MN', mississippi: 'MS',
689
+ missouri: 'MO', montana: 'MT', nebraska: 'NE', nevada: 'NV',
690
+ 'new hampshire': 'NH', 'new jersey': 'NJ', 'new mexico': 'NM', 'new york': 'NY',
691
+ 'north carolina': 'NC', 'north dakota': 'ND', ohio: 'OH', oklahoma: 'OK',
692
+ oregon: 'OR', pennsylvania: 'PA', 'rhode island': 'RI', 'south carolina': 'SC',
693
+ 'south dakota': 'SD', tennessee: 'TN', texas: 'TX', utah: 'UT',
694
+ vermont: 'VT', virginia: 'VA', washington: 'WA', 'west virginia': 'WV',
695
+ wisconsin: 'WI', wyoming: 'WY',
696
+ // US territories
697
+ 'puerto rico': 'PR', guam: 'GU', 'virgin islands': 'VI',
698
+ 'us virgin islands': 'VI', 'u.s. virgin islands': 'VI',
699
+ 'american samoa': 'AS', 'northern mariana islands': 'MP',
700
+ 'commonwealth of the northern mariana islands': 'MP',
701
+ // Military / diplomatic addresses
702
+ 'armed forces europe': 'AE', 'armed forces pacific': 'AP',
703
+ 'armed forces americas': 'AA',
704
+ };
705
+ const US_ABBREVS = new Set(Object.values(US_STATES));
706
+ const CA_PROVINCES = {
707
+ alberta: 'AB', 'british columbia': 'BC', manitoba: 'MB', 'new brunswick': 'NB',
708
+ 'newfoundland and labrador': 'NL', 'nova scotia': 'NS', ontario: 'ON',
709
+ 'prince edward island': 'PE', quebec: 'QC', saskatchewan: 'SK',
710
+ 'northwest territories': 'NT', nunavut: 'NU', yukon: 'YT',
711
+ };
712
+ const CA_ABBREVS = new Set(Object.values(CA_PROVINCES));
713
+ /**
714
+ * Converts a full US state or Canadian province name to its 2-letter abbreviation.
715
+ * If already a valid abbreviation (case-insensitive), returns it uppercased.
716
+ * For non-US/CA countries, returns the input uppercased unchanged.
717
+ *
718
+ * Matches checkout's convertStateToAbbreviation() behaviour exactly.
719
+ */
720
+ function normalizeState(state, country) {
721
+ var _a, _b;
722
+ const upper = state.trim().toUpperCase();
723
+ const lower = state.trim().toLowerCase();
724
+ if (country === 'US') {
725
+ if (US_ABBREVS.has(upper))
726
+ return upper;
727
+ return (_a = US_STATES[lower]) !== null && _a !== void 0 ? _a : upper;
728
+ }
729
+ if (country === 'CA') {
730
+ if (CA_ABBREVS.has(upper))
731
+ return upper;
732
+ return (_b = CA_PROVINCES[lower]) !== null && _b !== void 0 ? _b : upper;
733
+ }
734
+ return upper;
735
+ }
736
+ // ─── Postal code validation ───────────────────────────────────────────────────
737
+ const POSTAL_PATTERNS = {
738
+ US: /^\d{5}(-?\d{4})?$/, // 5-digit or ZIP+4 (with or without hyphen)
739
+ CA: /^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/, // A1A 1A1
740
+ GB: /^[A-Za-z]{1,2}\d[A-Za-z\d]?\s?\d[A-Za-z]{2}$/,
741
+ DE: /^\d{5}$/,
742
+ FR: /^\d{5}$/,
743
+ ES: /^\d{5}$/,
744
+ IT: /^\d{5}$/,
745
+ AU: /^\d{4}$/,
746
+ NL: /^\d{4}\s?[A-Za-z]{2}$/,
747
+ BR: /^\d{5}-?\d{3}$/,
748
+ JP: /^\d{3}-?\d{4}$/,
749
+ IN: /^\d{6}$/,
750
+ };
751
+ /**
752
+ * Validates a postal/ZIP code against country-specific format rules.
753
+ * For countries without a specific pattern, falls back to generic 1–50 char check.
754
+ */
755
+ function validatePostalCode(zip, country) {
756
+ if (!zip || zip.length === 0)
757
+ return { valid: false, error: 'Postal code is required' };
758
+ if (zip.length > 50)
759
+ return { valid: false, error: 'Postal code must be 50 characters or fewer' };
760
+ const pattern = POSTAL_PATTERNS[country.toUpperCase()];
761
+ if (pattern && !pattern.test(zip)) {
762
+ return { valid: false, error: `Invalid postal code format for ${country.toUpperCase()}` };
763
+ }
764
+ return { valid: true };
765
+ }
766
+ /**
767
+ * Validates and normalizes billing details against the Ozura cardSale API schema.
768
+ *
769
+ * Rules applied (same as checkout's pre-flight validation in page.tsx):
770
+ * - firstName, lastName: required, 1–50 chars
771
+ * - email: optional; if provided, must be valid format and ≤50 chars
772
+ * - phone: optional; if provided, must be E.164 and ≤50 chars
773
+ * - address fields: if address is provided, line1/city/state/zip/country are
774
+ * required (1–50 chars each); line2 is optional and omitted from normalized
775
+ * output if blank (cardSale schema: minLength 1 if present)
776
+ * - state: normalized to 2-letter abbreviation for US and CA
777
+ */
778
+ function validateBilling(billing) {
779
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
780
+ const errors = [];
781
+ const firstName = (_b = (_a = billing.firstName) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
782
+ const lastName = (_d = (_c = billing.lastName) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : '';
783
+ const email = (_f = (_e = billing.email) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : '';
784
+ const phone = (_h = (_g = billing.phone) === null || _g === void 0 ? void 0 : _g.trim()) !== null && _h !== void 0 ? _h : '';
785
+ if (!isValidBillingField(firstName)) {
786
+ errors.push('billing.firstName must be 1–50 characters');
787
+ }
788
+ if (!isValidBillingField(lastName)) {
789
+ errors.push('billing.lastName must be 1–50 characters');
790
+ }
791
+ if (email && !validateEmail(email)) {
792
+ errors.push('billing.email must be a valid address (max 50 characters)');
793
+ }
794
+ if (phone && !validateE164Phone(phone)) {
795
+ errors.push('billing.phone must be E.164 format, e.g. "+15551234567" (max 50 characters)');
796
+ }
797
+ let normalizedAddress;
798
+ if (billing.address) {
799
+ const a = billing.address;
800
+ const country = (_k = (_j = a.country) === null || _j === void 0 ? void 0 : _j.trim().toUpperCase()) !== null && _k !== void 0 ? _k : '';
801
+ const line1 = (_m = (_l = a.line1) === null || _l === void 0 ? void 0 : _l.trim()) !== null && _m !== void 0 ? _m : '';
802
+ const line2 = (_p = (_o = a.line2) === null || _o === void 0 ? void 0 : _o.trim()) !== null && _p !== void 0 ? _p : '';
803
+ const city = (_r = (_q = a.city) === null || _q === void 0 ? void 0 : _q.trim()) !== null && _r !== void 0 ? _r : '';
804
+ const zip = (_t = (_s = a.zip) === null || _s === void 0 ? void 0 : _s.trim()) !== null && _t !== void 0 ? _t : '';
805
+ const state = normalizeState((_v = (_u = a.state) === null || _u === void 0 ? void 0 : _u.trim()) !== null && _v !== void 0 ? _v : '', country);
806
+ if (!isValidBillingField(line1))
807
+ errors.push('billing.address.line1 must be 1–50 characters');
808
+ if (line2 && !isValidBillingField(line2))
809
+ errors.push('billing.address.line2 must be 1–50 characters if provided');
810
+ if (!isValidBillingField(city))
811
+ errors.push('billing.address.city must be 1–50 characters');
812
+ if (!isValidBillingField(state)) {
813
+ errors.push('billing.address.state must be 1–50 characters');
814
+ }
815
+ else if (country === 'US' && !US_ABBREVS.has(state)) {
816
+ errors.push(`billing.address.state "${state}" is not a recognized US state or territory abbreviation (e.g. "CA", "NY", "PR")`);
817
+ }
818
+ else if (country === 'CA' && !CA_ABBREVS.has(state)) {
819
+ errors.push(`billing.address.state "${state}" is not a recognized Canadian province or territory abbreviation (e.g. "ON", "BC", "QC")`);
820
+ }
821
+ // cardSale backend uses strict enum validation on country — must be exactly 2 uppercase letters
822
+ if (!/^[A-Z]{2}$/.test(country)) {
823
+ errors.push('billing.address.country must be a 2-letter ISO 3166-1 alpha-2 code (e.g. "US", "CA", "GB")');
824
+ }
825
+ if (!isValidBillingField(zip)) {
826
+ errors.push('billing.address.zip must be 1–50 characters');
827
+ }
828
+ else if (/^[A-Z]{2}$/.test(country)) {
829
+ const postalResult = validatePostalCode(zip, country);
830
+ if (!postalResult.valid) {
831
+ errors.push(`billing.address.zip: ${postalResult.error}`);
832
+ }
833
+ }
834
+ normalizedAddress = Object.assign(Object.assign({ line1 }, (line2 ? { line2 } : {})), { city,
835
+ state,
836
+ zip,
837
+ country });
838
+ }
839
+ const normalized = Object.assign(Object.assign(Object.assign({ firstName,
840
+ lastName }, (email ? { email } : {})), (phone ? { phone } : {})), (normalizedAddress ? { address: normalizedAddress } : {}));
841
+ return { valid: errors.length === 0, errors, normalized };
842
+ }
843
+
844
+ /**
845
+ * Shared postMessage protocol constants.
846
+ *
847
+ * PROTOCOL_VERSION must be incremented any time a breaking change is made to
848
+ * the postMessage message shape (new required fields, renamed types, removed
849
+ * fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
850
+ * messages and warns when the frame and SDK are out of sync.
851
+ *
852
+ * Non-breaking additions (new optional fields, new message types that old
853
+ * frames can safely ignore) do NOT require a version bump.
854
+ */
855
+ const PROTOCOL_VERSION = 1;
856
+
857
+ /**
858
+ * Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
859
+ *
860
+ * This is the recommended way to wire the SDK to your backend session endpoint.
861
+ * If you don't need custom headers or auth logic, pass `sessionUrl` directly to
862
+ * `OzVault.create()` or `<OzElements>` — it calls this helper internally.
863
+ *
864
+ * The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
865
+ * legacy `waxKey`) from the JSON response, so it is compatible with both the
866
+ * new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
867
+ *
868
+ * Each call enforces a **10-second timeout**. On pure network failures
869
+ * (offline, DNS, connection refused) the request is retried **once after 750ms**.
870
+ * HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
871
+ *
872
+ * @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
873
+ *
874
+ * @example
875
+ * // Simplest — just pass sessionUrl, no need to call this directly
876
+ * const vault = await OzVault.create({ pubKey: 'pk_prod_...', sessionUrl: '/api/oz-session' });
877
+ *
878
+ * @example
879
+ * // Manual — use when you need custom headers
880
+ * const vault = await OzVault.create({
881
+ * pubKey: 'pk_prod_...',
882
+ * getSessionKey: createSessionFetcher('/api/oz-session'),
883
+ * });
884
+ */
885
+ function createSessionFetcher(url) {
886
+ const TIMEOUT_MS = 10000;
887
+ // Each attempt gets its own AbortController so a timeout on attempt 1 does
888
+ // not bleed into the retry.
889
+ const attemptFetch = (sessionId) => {
890
+ const controller = new AbortController();
891
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
892
+ return fetch(url, {
893
+ method: 'POST',
894
+ headers: { 'Content-Type': 'application/json' },
895
+ body: JSON.stringify({ sessionId }),
896
+ signal: controller.signal,
897
+ }).finally(() => clearTimeout(timer));
898
+ };
899
+ return async (sessionId) => {
900
+ let res;
901
+ try {
902
+ res = await attemptFetch(sessionId);
903
+ }
904
+ catch (firstErr) {
905
+ // Timeout/abort — don't retry, we already waited the full duration.
906
+ if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
907
+ throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
908
+ }
909
+ // Pure network error — retry once after a short pause.
910
+ await new Promise(resolve => setTimeout(resolve, 750));
911
+ try {
912
+ res = await attemptFetch(sessionId);
913
+ }
914
+ catch (retryErr) {
915
+ const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
916
+ throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
917
+ }
918
+ }
919
+ // Parse JSON separately from the ok-check so that a non-JSON error body
920
+ // (HTML error page, WAF block, CDN 503) produces the right error code.
921
+ // Previously res.json() was attempted before res.ok was checked; a parse
922
+ // failure on a 5xx HTML body would fall through as {} and produce a
923
+ // misleading 'validation' code when the real cause is a server/network issue.
924
+ let data = {};
925
+ try {
926
+ data = await res.json();
927
+ }
928
+ catch (_a) {
929
+ if (!res.ok) {
930
+ throw new OzError(`Session endpoint returned HTTP ${res.status} with a non-JSON body`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
931
+ }
932
+ // HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
933
+ throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
934
+ }
935
+ if (!res.ok) {
936
+ throw new OzError(typeof data.error === 'string' && data.error
937
+ ? data.error
938
+ : `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
939
+ }
940
+ // Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
941
+ // with backends that haven't migrated to createSessionMiddleware yet.
942
+ const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
943
+ (typeof data.waxKey === 'string' ? data.waxKey : '');
944
+ if (!key.trim()) {
945
+ throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
946
+ }
947
+ return key;
948
+ };
949
+ }
950
+
951
+ function isCardMetadata(v) {
952
+ if (!v || typeof v !== 'object')
953
+ return false;
954
+ const r = v;
955
+ return (typeof r.last4 === 'string' &&
956
+ typeof r.brand === 'string' &&
957
+ typeof r.expMonth === 'string' &&
958
+ typeof r.expYear === 'string');
959
+ }
960
+ function isBankAccountMetadata(v) {
961
+ if (!v || typeof v !== 'object')
962
+ return false;
963
+ const r = v;
964
+ return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
965
+ }
966
+ const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
967
+ /**
968
+ * The main entry point for OzElements. Creates and manages iframe-based
969
+ * card input elements that keep raw card data isolated from the merchant page.
970
+ *
971
+ * Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
972
+ *
973
+ * @example
974
+ * // Recommended — pass sessionUrl and let the SDK call your backend automatically
975
+ * const vault = await OzVault.create({
976
+ * pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
977
+ * sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
978
+ * });
979
+ * const cardNum = vault.createElement('cardNumber');
980
+ * cardNum.mount('#card-number');
981
+ * const { token, cvcSession } = await vault.createToken({
982
+ * billing: { firstName: 'Jane', lastName: 'Doe' },
983
+ * });
984
+ */
985
+ class OzVault {
986
+ /**
987
+ * Internal constructor — use `OzVault.create()` instead.
988
+ * The constructor mounts the tokenizer iframe immediately so it can start
989
+ * loading in parallel while `fetchWaxKey` is being awaited.
990
+ * @internal
991
+ */
992
+ constructor(options, waxKey, tokenizationSessionId) {
993
+ var _a, _b, _c, _d, _e;
994
+ this.elements = new Map();
995
+ this.elementsByType = new Map();
996
+ this.bankElementsByType = new Map();
997
+ this.tokenizeResolvers = new Map();
998
+ this.bankTokenizeResolvers = new Map();
999
+ // Track completion state per element for auto-advance (only fire on transition)
1000
+ this.completionState = new Map();
1001
+ this.tokenizerFrame = null;
1002
+ this.tokenizerWindow = null;
1003
+ this.tokenizerReady = false;
1004
+ this._tokenizing = null;
1005
+ this._destroyed = false;
1006
+ // Incremented every time reset() cancels an active tokenization so that
1007
+ // any in-flight wax-key refresh retry can detect it was superseded.
1008
+ this._resetCount = 0;
1009
+ // Tracks successful tokenizations against the per-key call budget so the SDK
1010
+ // can proactively refresh the wax key after it has been consumed rather than
1011
+ // waiting for the next createToken() call to fail.
1012
+ this._tokenizeSuccessCount = 0;
1013
+ this._pendingMount = null;
1014
+ this._storedFetchWaxKey = null;
1015
+ this._waxRefreshing = null;
1016
+ this.loadErrorTimeoutId = null;
1017
+ // Proactive wax refresh on visibility restore after long idle
1018
+ this._hiddenAt = null;
1019
+ this.waxKey = waxKey;
1020
+ this.tokenizationSessionId = tokenizationSessionId;
1021
+ this.pubKey = options.pubKey;
1022
+ this.frameBaseUrl = options.frameBaseUrl || DEFAULT_FRAME_BASE_URL;
1023
+ // Validate immediately after assignment
1024
+ if (!this.frameBaseUrl.startsWith('https://') &&
1025
+ !this.frameBaseUrl.startsWith('http://localhost')) {
1026
+ throw new OzError('frameBaseUrl must use HTTPS (http://localhost is allowed for local development)', undefined, 'config');
1027
+ }
1028
+ this.frameOrigin = new URL(this.frameBaseUrl).origin;
1029
+ this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
1030
+ this.resolvedAppearance = resolveAppearance(options.appearance);
1031
+ this.vaultId = `vault-${uuid()}`;
1032
+ // sessionLimit takes precedence over legacy maxTokenizeCalls.
1033
+ // null means unlimited — use Infinity so the ">=" check never triggers.
1034
+ const rawLimit = options.sessionLimit !== undefined
1035
+ ? options.sessionLimit
1036
+ : ((_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3);
1037
+ this._maxTokenizeCalls = rawLimit === null ? Infinity : rawLimit;
1038
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
1039
+ this.boundHandleMessage = this.handleMessage.bind(this);
1040
+ window.addEventListener('message', this.boundHandleMessage);
1041
+ this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
1042
+ document.addEventListener('visibilitychange', this.boundHandleVisibility);
1043
+ this.mountTokenizerFrame();
1044
+ if (options.onLoadError) {
1045
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
1046
+ this.loadErrorTimeoutId = setTimeout(() => {
1047
+ this.loadErrorTimeoutId = null;
1048
+ if (!this._destroyed && !this.tokenizerReady) {
1049
+ options.onLoadError();
1050
+ }
1051
+ }, timeout);
1052
+ }
1053
+ // onSessionRefresh takes precedence over legacy onWaxRefresh
1054
+ this._onWaxRefresh = (_e = options.onSessionRefresh) !== null && _e !== void 0 ? _e : options.onWaxRefresh;
1055
+ this._onReady = options.onReady;
1056
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
1057
+ }
1058
+ /**
1059
+ * Creates and returns a ready `OzVault` instance.
1060
+ *
1061
+ * Internally this:
1062
+ * 1. Generates a session UUID.
1063
+ * 2. Starts loading the hidden tokenizer iframe immediately.
1064
+ * 3. Fetches a session key from your backend concurrently — either via
1065
+ * `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
1066
+ * deprecated `fetchWaxKey` callback.
1067
+ * 4. Resolves with the vault instance once the session key is stored. The iframe
1068
+ * has been loading the whole time, so `isReady` may already be true or
1069
+ * will fire shortly after.
1070
+ *
1071
+ * The returned vault is ready to create elements immediately. `createToken()`
1072
+ * additionally requires `vault.isReady` (tokenizer iframe loaded).
1073
+ *
1074
+ * @throws {OzError} if the session fetch fails, times out, or returns an empty string.
1075
+ */
1076
+ static async create(options, signal) {
1077
+ if (!options.pubKey || !options.pubKey.trim()) {
1078
+ throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
1079
+ }
1080
+ // Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
1081
+ // This allows merchants to use the clean new API without touching legacy code.
1082
+ let resolvedFetchKey;
1083
+ if (options.sessionUrl) {
1084
+ resolvedFetchKey = createSessionFetcher(options.sessionUrl);
1085
+ }
1086
+ else if (typeof options.getSessionKey === 'function') {
1087
+ resolvedFetchKey = options.getSessionKey;
1088
+ }
1089
+ else if (typeof options.fetchWaxKey === 'function') {
1090
+ resolvedFetchKey = options.fetchWaxKey;
1091
+ }
1092
+ else {
1093
+ throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
1094
+ }
1095
+ const tokenizationSessionId = uuid();
1096
+ // Construct the vault immediately — this mounts the tokenizer iframe so it
1097
+ // starts loading while the session fetch is in flight.
1098
+ const vault = new OzVault(options, '', tokenizationSessionId);
1099
+ // If the caller provides an AbortSignal (e.g. React useEffect cleanup),
1100
+ // destroy the vault immediately on abort so the tokenizer iframe and message
1101
+ // listener are removed synchronously rather than waiting for the session fetch
1102
+ // to settle. This eliminates the brief double-iframe window in React StrictMode.
1103
+ const onAbort = () => vault.destroy();
1104
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
1105
+ let waxKey;
1106
+ try {
1107
+ waxKey = await resolvedFetchKey(tokenizationSessionId);
1108
+ }
1109
+ catch (err) {
1110
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
1111
+ vault.destroy();
1112
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
1113
+ throw new OzError('OzVault.create() was cancelled.');
1114
+ // Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
1115
+ // so callers can distinguish transient failures from config errors.
1116
+ const originalCode = err instanceof OzError ? err.errorCode : undefined;
1117
+ const msg = err instanceof Error ? err.message : 'Unknown error';
1118
+ throw new OzError(`Session fetch threw an error: ${msg}`, undefined, originalCode);
1119
+ }
1120
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
1121
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
1122
+ vault.destroy();
1123
+ throw new OzError('OzVault.create() was cancelled.');
1124
+ }
1125
+ if (typeof waxKey !== 'string' || !waxKey.trim()) {
1126
+ vault.destroy();
1127
+ throw new OzError('Session fetch returned an empty key. Check your session endpoint response — it must return { sessionKey: "..." }.');
1128
+ }
1129
+ // Static methods can access private fields of instances of the same class.
1130
+ vault.waxKey = waxKey;
1131
+ vault._storedFetchWaxKey = resolvedFetchKey;
1132
+ // If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
1133
+ // the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
1134
+ // so the tokenizer has the key stored before any createToken() call.
1135
+ if (vault.tokenizerReady) {
1136
+ vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey, debug: vault._debug });
1137
+ }
1138
+ vault.log('wax key received — vault ready');
1139
+ return vault;
1140
+ }
1141
+ /**
1142
+ * True once the hidden tokenizer iframe has loaded and signalled ready.
1143
+ * Use this to gate the pay button when building custom UIs without React.
1144
+ * React consumers should use the `ready` value returned by `useOzElements()`.
1145
+ *
1146
+ * Once `true`, remains `true` for the lifetime of this vault instance.
1147
+ * It only reverts to `false` after `vault.destroy()` is called, at which
1148
+ * point the vault is unusable and a new instance must be created.
1149
+ *
1150
+ * @remarks
1151
+ * This tracks **tokenizer readiness only** — it says nothing about whether
1152
+ * the individual element iframes (card number, CVV, etc.) have loaded.
1153
+ * A vault can be `isReady === true` while elements are still mounting.
1154
+ * To gate a submit button correctly in vanilla JS, wait for every element's
1155
+ * `'ready'` event in addition to this flag. In React, use the `ready` value
1156
+ * from `useOzElements()` instead, which combines both checks automatically.
1157
+ */
1158
+ get isReady() {
1159
+ return this.tokenizerReady;
1160
+ }
1161
+ /**
1162
+ * Number of successful tokenize calls made against the current wax key.
1163
+ *
1164
+ * Resets to `0` each time the wax key is refreshed (proactively or reactively).
1165
+ * Useful in vanilla JS integrations to display "attempts remaining" UI.
1166
+ * In React, use `tokenizeCount` from `useOzElements()` instead.
1167
+ *
1168
+ * @example
1169
+ * const remaining = 3 - vault.tokenizeCount;
1170
+ * payButton.textContent = remaining > 0 ? `Pay (${remaining} attempts left)` : 'Pay';
1171
+ */
1172
+ get tokenizeCount() {
1173
+ return this._tokenizeSuccessCount;
1174
+ }
1175
+ /**
1176
+ * Creates a new OzElement of the given type. Call `.mount(selector)` on the
1177
+ * returned element to attach it to the DOM.
1178
+ */
1179
+ createElement(type, options = {}) {
1180
+ return this.createElementInto(this.elementsByType, type, options);
1181
+ }
1182
+ /** Returns the existing element of the given type, or null if none has been created. */
1183
+ getElement(type) {
1184
+ var _a;
1185
+ return (_a = this.elementsByType.get(type)) !== null && _a !== void 0 ? _a : null;
1186
+ }
1187
+ /**
1188
+ * Creates a bank account input element (accountNumber or routingNumber).
1189
+ * Call `.mount(selector)` on the returned element to attach it to the DOM.
1190
+ *
1191
+ * @example
1192
+ * const accountEl = vault.createBankElement('accountNumber');
1193
+ * const routingEl = vault.createBankElement('routingNumber');
1194
+ * accountEl.mount('#account-number');
1195
+ * routingEl.mount('#routing-number');
1196
+ */
1197
+ createBankElement(type, options = {}) {
1198
+ return this.createElementInto(this.bankElementsByType, type, options);
1199
+ }
1200
+ /** Returns the existing bank element of the given type, or null if none has been created. */
1201
+ getBankElement(type) {
1202
+ var _a;
1203
+ return (_a = this.bankElementsByType.get(type)) !== null && _a !== void 0 ? _a : null;
1204
+ }
1205
+ createElementInto(typeMap, type, options) {
1206
+ if (this._destroyed) {
1207
+ throw new OzError('Cannot create elements on a destroyed vault. Call await OzVault.create() to get a new instance.');
1208
+ }
1209
+ if (this._tokenizing) {
1210
+ throw new OzError('Cannot create or replace elements while a tokenization is in progress. Wait for the active createToken() / createBankToken() call to settle first.');
1211
+ }
1212
+ const existing = typeMap.get(type);
1213
+ if (existing) {
1214
+ this.elements.delete(existing.frameId);
1215
+ this.completionState.delete(existing.frameId);
1216
+ existing.destroy();
1217
+ }
1218
+ const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
1219
+ // Prune vault-level maps when the element is manually destroyed so they
1220
+ // don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
1221
+ this.elements.delete(el.frameId);
1222
+ this.completionState.delete(el.frameId);
1223
+ }, this._debug);
1224
+ this.elements.set(el.frameId, el);
1225
+ typeMap.set(type, el);
1226
+ return el;
1227
+ }
1228
+ /**
1229
+ * Tokenizes mounted bank account elements. Raw account and routing numbers
1230
+ * never leave the Ozura-origin iframes — the tokenizer iframe POSTs directly
1231
+ * to the vault API.
1232
+ *
1233
+ * Returns a token that can be used with any ACH-capable payment processor.
1234
+ *
1235
+ * **Note:** OzuraPay does not currently support bank account payments.
1236
+ * Use this token with your own ACH processor backend.
1237
+ *
1238
+ * @example
1239
+ * const { token, bank } = await vault.createBankToken({
1240
+ * firstName: 'Jane',
1241
+ * lastName: 'Smith',
1242
+ * });
1243
+ */
1244
+ async createBankToken(options) {
1245
+ var _a, _b;
1246
+ if (this._destroyed) {
1247
+ throw new OzError('Cannot tokenize on a destroyed vault. Call await OzVault.create() to get a new instance.');
1248
+ }
1249
+ if (!this.tokenizerReady) {
1250
+ throw new OzError('Vault not ready. Ensure the page is fully loaded before calling createBankToken.');
1251
+ }
1252
+ if (this._tokenizing) {
1253
+ throw new OzError(this._tokenizing === 'card'
1254
+ ? 'A card tokenization is already in progress. Wait for it to complete before calling createBankToken().'
1255
+ : 'A bank tokenization is already in progress. Wait for it to complete before calling createBankToken() again.');
1256
+ }
1257
+ if (!((_a = options.firstName) === null || _a === void 0 ? void 0 : _a.trim())) {
1258
+ throw new OzError('firstName is required for bank account tokenization.');
1259
+ }
1260
+ if (!((_b = options.lastName) === null || _b === void 0 ? void 0 : _b.trim())) {
1261
+ throw new OzError('lastName is required for bank account tokenization.');
1262
+ }
1263
+ if (options.firstName.trim().length > 50) {
1264
+ throw new OzError('firstName must be 50 characters or fewer.');
1265
+ }
1266
+ if (options.lastName.trim().length > 50) {
1267
+ throw new OzError('lastName must be 50 characters or fewer.');
1268
+ }
1269
+ const accountEl = this.bankElementsByType.get('accountNumber');
1270
+ const routingEl = this.bankElementsByType.get('routingNumber');
1271
+ const accountReady = !!accountEl && this.elements.has(accountEl.frameId) && accountEl.isReady;
1272
+ const routingReady = !!routingEl && this.elements.has(routingEl.frameId) && routingEl.isReady;
1273
+ if (!accountReady && !routingReady) {
1274
+ throw new OzError('No bank elements are mounted and ready. Mount accountNumber and routingNumber elements before calling createBankToken.');
1275
+ }
1276
+ if (!accountReady) {
1277
+ throw new OzError('accountNumber element is not mounted or not ready. Mount both accountNumber and routingNumber elements before calling createBankToken.');
1278
+ }
1279
+ if (!routingReady) {
1280
+ throw new OzError('routingNumber element is not mounted or not ready. Mount both accountNumber and routingNumber elements before calling createBankToken.');
1281
+ }
1282
+ const readyBankElements = [accountEl, routingEl];
1283
+ this._tokenizing = 'bank';
1284
+ const requestId = `req-${uuid()}`;
1285
+ this.log('createBankToken() called');
1286
+ return new Promise((resolve, reject) => {
1287
+ const resetCountAtStart = this._resetCount;
1288
+ const cleanup = () => {
1289
+ if (this._resetCount === resetCountAtStart)
1290
+ this._tokenizing = null;
1291
+ };
1292
+ this.bankTokenizeResolvers.set(requestId, {
1293
+ resolve: (v) => { cleanup(); resolve(v); },
1294
+ reject: (e) => { cleanup(); reject(e); },
1295
+ firstName: options.firstName.trim(),
1296
+ lastName: options.lastName.trim(),
1297
+ readyElements: readyBankElements,
1298
+ fieldCount: readyBankElements.length,
1299
+ });
1300
+ try {
1301
+ const bankChannels = readyBankElements.map(() => new MessageChannel());
1302
+ const bankTokenizeStartMs = Date.now();
1303
+ this.sendToTokenizer({
1304
+ type: 'OZ_BANK_TOKENIZE',
1305
+ requestId,
1306
+ tokenizationSessionId: this.tokenizationSessionId,
1307
+ pubKey: this.pubKey,
1308
+ firstName: options.firstName.trim(),
1309
+ lastName: options.lastName.trim(),
1310
+ fieldCount: readyBankElements.length,
1311
+ }, bankChannels.map(ch => ch.port1));
1312
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1313
+ readyBankElements.forEach((el, i) => {
1314
+ this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
1315
+ el.beginCollect(requestId, bankChannels[i].port2);
1316
+ });
1317
+ const bankTimeoutId = setTimeout(() => {
1318
+ if (this.bankTokenizeResolvers.has(requestId)) {
1319
+ this.bankTokenizeResolvers.delete(requestId);
1320
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1321
+ cleanup();
1322
+ reject(new OzError('Bank tokenization timed out after 30 seconds', undefined, 'timeout'));
1323
+ }
1324
+ }, 30000);
1325
+ const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1326
+ if (bankPendingEntry) {
1327
+ bankPendingEntry.timeoutId = bankTimeoutId;
1328
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1329
+ }
1330
+ }
1331
+ catch (err) {
1332
+ this.bankTokenizeResolvers.delete(requestId);
1333
+ cleanup();
1334
+ reject(err instanceof OzError ? err : new OzError('Bank tokenization failed to start'));
1335
+ }
1336
+ });
1337
+ }
1338
+ /**
1339
+ * Tokenizes all mounted elements. Raw card data never leaves the Ozura-origin
1340
+ * iframes — the tokenizer iframe POSTs directly to the vault API.
1341
+ *
1342
+ * Returns a token and cvcSession that can be passed to the Ozura Pay API.
1343
+ */
1344
+ async createToken(options = {}) {
1345
+ var _a, _b;
1346
+ if (this._destroyed) {
1347
+ throw new OzError('Cannot tokenize on a destroyed vault. Call await OzVault.create() to get a new instance.');
1348
+ }
1349
+ if (!this.tokenizerReady) {
1350
+ throw new OzError('Vault not ready. Ensure the page is fully loaded before calling createToken.');
1351
+ }
1352
+ if (this._tokenizing) {
1353
+ throw new OzError(this._tokenizing === 'bank'
1354
+ ? 'A bank tokenization is already in progress. Wait for it to complete before calling createToken().'
1355
+ : 'A card tokenization is already in progress. Wait for it to complete before calling createToken() again.');
1356
+ }
1357
+ // All synchronous validation runs before _tokenizing is set so these throws
1358
+ // need no manual cleanup — _tokenizing is still null when they fire.
1359
+ // Collect all card elements that have been created (mounted or not) so we
1360
+ // can require every created field to be ready before proceeding. An element
1361
+ // that was created but whose iframe hasn't loaded yet will never send
1362
+ // OZ_FIELD_VALUE — tokenizing without it would silently submit an empty or
1363
+ // incomplete card and produce an opaque vault rejection.
1364
+ // Bank elements (accountNumber/routingNumber) share this.elements but live
1365
+ // in elementsByType under bank-only keys, so they are excluded by the Set.
1366
+ const cardElements = [...this.elementsByType.values()];
1367
+ if (cardElements.length === 0) {
1368
+ throw new OzError('No card elements have been created. Call vault.createElement() for each field before calling createToken.');
1369
+ }
1370
+ const notReady = cardElements.filter(el => !el.isReady);
1371
+ if (notReady.length > 0) {
1372
+ throw new OzError(`Not all elements are ready. Wait for all fields to finish loading before calling createToken. ` +
1373
+ `Not yet ready: ${notReady.map(el => el.type).join(', ')}.`);
1374
+ }
1375
+ const readyElements = cardElements;
1376
+ // Validate billing details if provided and extract firstName/lastName for the vault payload.
1377
+ // billing.firstName/lastName take precedence over the deprecated top-level params.
1378
+ let normalizedBilling;
1379
+ let firstName = ((_a = options.firstName) !== null && _a !== void 0 ? _a : '').trim();
1380
+ let lastName = ((_b = options.lastName) !== null && _b !== void 0 ? _b : '').trim();
1381
+ if (options.billing) {
1382
+ const result = validateBilling(options.billing);
1383
+ if (!result.valid) {
1384
+ throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
1385
+ }
1386
+ normalizedBilling = result.normalized;
1387
+ firstName = normalizedBilling.firstName;
1388
+ lastName = normalizedBilling.lastName;
1389
+ }
1390
+ else {
1391
+ if (firstName.length > 50) {
1392
+ throw new OzError('firstName must be 50 characters or fewer.');
1393
+ }
1394
+ if (lastName.length > 50) {
1395
+ throw new OzError('lastName must be 50 characters or fewer.');
1396
+ }
1397
+ }
1398
+ this._tokenizing = 'card';
1399
+ const requestId = `req-${uuid()}`;
1400
+ this.log('createToken() called', {
1401
+ requestIdPrefix: requestId.slice(0, 12),
1402
+ fields: readyElements.map(el => el.type),
1403
+ billingPresent: Boolean(options.billing),
1404
+ });
1405
+ return new Promise((resolve, reject) => {
1406
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1407
+ // still belongs to this invocation — not a newer one that started after a reset.
1408
+ const resetCountAtStart = this._resetCount;
1409
+ const cleanup = () => {
1410
+ if (this._resetCount === resetCountAtStart)
1411
+ this._tokenizing = null;
1412
+ };
1413
+ this.tokenizeResolvers.set(requestId, {
1414
+ resolve: (v) => { cleanup(); resolve(v); },
1415
+ reject: (e) => { cleanup(); reject(e); },
1416
+ billing: normalizedBilling,
1417
+ firstName,
1418
+ lastName,
1419
+ readyElements,
1420
+ fieldCount: readyElements.length,
1421
+ });
1422
+ try {
1423
+ // Tell tokenizer frame to expect N field values, then tokenize
1424
+ const cardChannels = readyElements.map(() => new MessageChannel());
1425
+ const tokenizeStartMs = Date.now();
1426
+ this.sendToTokenizer({
1427
+ type: 'OZ_TOKENIZE',
1428
+ requestId,
1429
+ tokenizationSessionId: this.tokenizationSessionId,
1430
+ pubKey: this.pubKey,
1431
+ firstName,
1432
+ lastName,
1433
+ fieldCount: readyElements.length,
1434
+ }, cardChannels.map(ch => ch.port1));
1435
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1436
+ // Store start time for elapsed-ms logging on result
1437
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1438
+ if (cardEntry)
1439
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1440
+ // Tell each ready element frame to send its raw value to the tokenizer
1441
+ readyElements.forEach((el, i) => {
1442
+ this.log('OZ_BEGIN_COLLECT dispatched', { type: el.type, requestIdPrefix: `${requestId.slice(0, 12)}...` });
1443
+ el.beginCollect(requestId, cardChannels[i].port2);
1444
+ });
1445
+ const cardTimeoutId = setTimeout(() => {
1446
+ if (this.tokenizeResolvers.has(requestId)) {
1447
+ this.tokenizeResolvers.delete(requestId);
1448
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1449
+ cleanup();
1450
+ reject(new OzError('Tokenization timed out after 30 seconds', undefined, 'timeout'));
1451
+ }
1452
+ }, 30000);
1453
+ const cardPendingEntry = this.tokenizeResolvers.get(requestId);
1454
+ if (cardPendingEntry)
1455
+ cardPendingEntry.timeoutId = cardTimeoutId;
1456
+ }
1457
+ catch (err) {
1458
+ this.tokenizeResolvers.delete(requestId);
1459
+ cleanup();
1460
+ reject(err instanceof OzError ? err : new OzError('Tokenization failed to start'));
1461
+ }
1462
+ });
1463
+ }
1464
+ /**
1465
+ * Clears all mounted element fields without tearing down the vault.
1466
+ *
1467
+ * Call this after a failed payment (e.g. card declined) to let the customer
1468
+ * re-enter their details. The vault instance, tokenizer iframe, wax key, and
1469
+ * tokenization budget counter are all preserved — no network calls are made.
1470
+ *
1471
+ * **Wax key session model:** by design, one wax key covers the full checkout
1472
+ * session. The default `max_tokenize_calls: 3` supports two declined attempts
1473
+ * and one final attempt on the same key. Do not call `vault.destroy()` and
1474
+ * recreate the vault between declines — that unnecessarily re-mints the key
1475
+ * and discards the remaining budget.
1476
+ *
1477
+ * @example
1478
+ * try {
1479
+ * const { token, cvcSession } = await vault.createToken({ billing });
1480
+ * await chargeCard(token, cvcSession);
1481
+ * } catch (err) {
1482
+ * vault.reset(); // clear fields; let customer re-enter
1483
+ * showError(err.message);
1484
+ * }
1485
+ */
1486
+ reset() {
1487
+ if (this._destroyed)
1488
+ return;
1489
+ const cancelling = Boolean(this._tokenizing);
1490
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1491
+ if (this._tokenizing) {
1492
+ this._tokenizing = null;
1493
+ this._resetCount++;
1494
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1495
+ if (timeoutId != null)
1496
+ clearTimeout(timeoutId);
1497
+ this.log('OZ_TOKENIZE_CANCEL sent (reset)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
1498
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1499
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1500
+ });
1501
+ this.tokenizeResolvers.clear();
1502
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1503
+ if (timeoutId != null)
1504
+ clearTimeout(timeoutId);
1505
+ this.log('OZ_TOKENIZE_CANCEL sent (reset, bank)', { requestIdPrefix: `${requestId.slice(0, 12)}...` });
1506
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1507
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1508
+ });
1509
+ this.bankTokenizeResolvers.clear();
1510
+ }
1511
+ // Clear field values in all mounted element iframes
1512
+ this.elementsByType.forEach(el => el.clear());
1513
+ this.bankElementsByType.forEach(el => el.clear());
1514
+ // Reset per-element completion state so auto-advance starts fresh on re-entry
1515
+ for (const frameId of this.completionState.keys()) {
1516
+ this.completionState.set(frameId, false);
1517
+ }
1518
+ // NOTE: _tokenizeSuccessCount is intentionally NOT reset.
1519
+ // It reflects real server-side wax key budget consumption. Zeroing it
1520
+ // would desync the proactive refresh logic from the vault's state and
1521
+ // risk triggering a mid-session re-mint on what should be a clean retry.
1522
+ }
1523
+ /**
1524
+ * Tears down the vault: removes all element iframes, the tokenizer iframe,
1525
+ * and the global message listener. Call this when the checkout component
1526
+ * unmounts (e.g. in React's useEffect cleanup or a SPA route change).
1527
+ */
1528
+ destroy() {
1529
+ var _a;
1530
+ if (this._destroyed)
1531
+ return;
1532
+ this._destroyed = true;
1533
+ this.log('destroy() called');
1534
+ window.removeEventListener('message', this.boundHandleMessage);
1535
+ document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1536
+ if (this._pendingMount) {
1537
+ document.removeEventListener('DOMContentLoaded', this._pendingMount);
1538
+ this._pendingMount = null;
1539
+ }
1540
+ if (this.loadErrorTimeoutId != null) {
1541
+ clearTimeout(this.loadErrorTimeoutId);
1542
+ this.loadErrorTimeoutId = null;
1543
+ }
1544
+ // Reject any pending tokenize promises so callers aren't left hanging
1545
+ this._tokenizing = null;
1546
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1547
+ if (timeoutId != null)
1548
+ clearTimeout(timeoutId);
1549
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1550
+ reject(new OzError('Vault destroyed before tokenization completed.'));
1551
+ });
1552
+ this.tokenizeResolvers.clear();
1553
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1554
+ if (timeoutId != null)
1555
+ clearTimeout(timeoutId);
1556
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1557
+ reject(new OzError('Vault destroyed before bank tokenization completed.'));
1558
+ });
1559
+ this.bankTokenizeResolvers.clear();
1560
+ this.elements.forEach(el => el.destroy());
1561
+ this.elements.clear();
1562
+ this.elementsByType.clear();
1563
+ this.bankElementsByType.clear();
1564
+ this.completionState.clear();
1565
+ this._tokenizeSuccessCount = 0;
1566
+ (_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.remove();
1567
+ this.tokenizerFrame = null;
1568
+ this.tokenizerWindow = null;
1569
+ this.tokenizerReady = false;
1570
+ }
1571
+ // ─── Private ─────────────────────────────────────────────────────────────
1572
+ /**
1573
+ * Proactively re-mints the wax key when the page becomes visible again after
1574
+ * a long idle period. Wax keys have a fixed TTL (~30 minutes); a user who
1575
+ * leaves the tab in the background and returns could have an expired key.
1576
+ * Rather than waiting for a failed tokenization to trigger the reactive
1577
+ * refresh path, this pre-empts the failure when the vault is idle.
1578
+ *
1579
+ * Threshold: 20 minutes hidden. Chosen to be comfortably inside the ~30m TTL
1580
+ * while avoiding spurious refreshes for brief tab-switches.
1581
+ */
1582
+ handleVisibilityChange() {
1583
+ if (this._destroyed)
1584
+ return;
1585
+ const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1586
+ if (document.hidden) {
1587
+ this._hiddenAt = Date.now();
1588
+ this.log('tab hidden');
1589
+ }
1590
+ else {
1591
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1592
+ const willRefresh = (this._hiddenAt !== null &&
1593
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1594
+ Boolean(this._storedFetchWaxKey) &&
1595
+ !this._tokenizing &&
1596
+ !this._waxRefreshing);
1597
+ this.log('tab visible', { hiddenMs, willRefresh });
1598
+ if (willRefresh) {
1599
+ this.refreshWaxKey().catch((err) => {
1600
+ // Proactive refresh failure is non-fatal — the reactive path on the
1601
+ // next createToken() call will handle it, including the auth retry.
1602
+ console.warn('[OzVault] Proactive wax key refresh failed:', err instanceof Error ? err.message : err);
1603
+ });
1604
+ }
1605
+ this._hiddenAt = null;
1606
+ }
1607
+ }
1608
+ // ─── Debug ───────────────────────────────────────────────────────────────
1609
+ /**
1610
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1611
+ * not set. Never called with sensitive values — callers use presence flags only.
1612
+ */
1613
+ log(message, data) {
1614
+ if (!this._debug)
1615
+ return;
1616
+ if (data !== undefined) {
1617
+ console.log(`[OzVault] ${message}`, data);
1618
+ }
1619
+ else {
1620
+ console.log(`[OzVault] ${message}`);
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Returns a plain-object snapshot of the vault's current internal state.
1625
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1626
+ *
1627
+ * Available on all vault instances regardless of whether `debug` was enabled.
1628
+ *
1629
+ * @example
1630
+ * console.log(vault.debugState());
1631
+ * // {
1632
+ * // vaultId: 'vault-abc123',
1633
+ * // isReady: true,
1634
+ * // tokenizing: null,
1635
+ * // destroyed: false,
1636
+ * // waxKeyPresent: true,
1637
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1638
+ * // ...
1639
+ * // }
1640
+ */
1641
+ debugState() {
1642
+ return {
1643
+ vaultId: this.vaultId,
1644
+ isReady: this.tokenizerReady,
1645
+ tokenizing: this._tokenizing,
1646
+ destroyed: this._destroyed,
1647
+ waxKeyPresent: Boolean(this.waxKey),
1648
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1649
+ maxTokenizeCalls: this._maxTokenizeCalls,
1650
+ resetCount: this._resetCount,
1651
+ elements: [...this.elementsByType.keys()],
1652
+ bankElements: [...this.bankElementsByType.keys()],
1653
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1654
+ pendingTokenizations: this.tokenizeResolvers.size,
1655
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1656
+ };
1657
+ }
1658
+ mountTokenizerFrame() {
1659
+ const mount = () => {
1660
+ this._pendingMount = null;
1661
+ const iframe = document.createElement('iframe');
1662
+ iframe.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;';
1663
+ iframe.setAttribute('aria-hidden', 'true');
1664
+ iframe.tabIndex = -1;
1665
+ const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
1666
+ iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1667
+ document.body.appendChild(iframe);
1668
+ this.tokenizerFrame = iframe;
1669
+ this.log('mounting tokenizer iframe');
1670
+ };
1671
+ if (document.readyState === 'loading') {
1672
+ this._pendingMount = mount;
1673
+ document.addEventListener('DOMContentLoaded', mount);
1674
+ }
1675
+ else {
1676
+ mount();
1677
+ }
1678
+ }
1679
+ handleMessage(event) {
1680
+ var _a, _b;
1681
+ if (this._destroyed)
1682
+ return;
1683
+ // Only accept messages from our frame origin (defense in depth; prevents
1684
+ // arbitrary pages from injecting OZ_TOKEN_RESULT etc. with a guessed vaultId).
1685
+ if (event.origin !== this.frameOrigin)
1686
+ return;
1687
+ const msg = event.data;
1688
+ if (!msg || msg.__oz !== true || msg.vaultId !== this.vaultId)
1689
+ return;
1690
+ // Route tokenizer messages
1691
+ if (event.source === ((_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.contentWindow)) {
1692
+ this.handleTokenizerMessage(msg);
1693
+ return;
1694
+ }
1695
+ // OZ_TOKEN_ERROR can arrive from element frames when the MessagePort
1696
+ // transferred in OZ_BEGIN_COLLECT was dropped by the browser (e.g. the
1697
+ // frame navigated). These carry a requestId but no frameId — route them
1698
+ // through handleTokenizerMessage so the pending promise is rejected
1699
+ // immediately rather than waiting for the 30 s collect timeout.
1700
+ if (msg.type === 'OZ_TOKEN_ERROR') {
1701
+ this.handleTokenizerMessage(msg);
1702
+ return;
1703
+ }
1704
+ // Route to the matching element
1705
+ const frameId = msg.frameId;
1706
+ if (frameId) {
1707
+ const el = this.elements.get(frameId);
1708
+ if (el) {
1709
+ // Reset stale completion state when an element iframe re-loads (e.g. after
1710
+ // unmount() + mount() in a SPA). Without this, wasComplete stays true from
1711
+ // the previous session and justCompleted never fires, breaking auto-advance.
1712
+ if (msg.type === 'OZ_FRAME_READY') {
1713
+ this.completionState.set(frameId, false);
1714
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1715
+ console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
1716
+ `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1717
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1718
+ }
1719
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1720
+ }
1721
+ // Relay debug/warning messages from element iframes into the parent
1722
+ // DevTools console. Element frames run in cross-origin iframes whose
1723
+ // console context is invisible to developers without a frame selector switch.
1724
+ // Errors are always surfaced (genuine failures). Warnings are only emitted
1725
+ // when debug mode is on — CSS var() and font-host warnings fire on every
1726
+ // style update and would pollute production consoles otherwise.
1727
+ if (msg.type === 'OZ_DEBUG_LOG') {
1728
+ const level = typeof msg.level === 'string' ? msg.level : 'warn';
1729
+ const message = typeof msg.message === 'string' ? msg.message : String((_b = msg.message) !== null && _b !== void 0 ? _b : '');
1730
+ if (level === 'error') {
1731
+ console.error(`[OzVault:${el.type}] ${message}`);
1732
+ }
1733
+ else if (this._debug) {
1734
+ console.warn(`[OzVault:${el.type}] ${message}`);
1735
+ }
1736
+ return;
1737
+ }
1738
+ // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1739
+ if (msg.type === 'OZ_CHANGE') {
1740
+ this.handleElementChange(msg, el);
1741
+ }
1742
+ el.handleMessage(msg);
1743
+ }
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Handles side-effects that the SDK manages internally when a field changes:
1748
+ * - CVV length sync when card brand changes
1749
+ * - Auto-advance focus when a field completes
1750
+ */
1751
+ handleElementChange(msg, el) {
1752
+ var _a, _b, _c;
1753
+ const complete = msg.complete;
1754
+ const valid = msg.valid;
1755
+ const wasComplete = (_a = this.completionState.get(el.frameId)) !== null && _a !== void 0 ? _a : false;
1756
+ this.completionState.set(el.frameId, complete && valid);
1757
+ // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1758
+ // where isComplete() fires before the user has finished typing.
1759
+ const justCompleted = complete && valid && !wasComplete;
1760
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1761
+ // Sync CVV length when card brand changes
1762
+ if (el.type === 'cardNumber') {
1763
+ const brand = msg.cardBrand;
1764
+ const cvvEl = this.elementsByType.get('cvv');
1765
+ if (cvvEl && brand) {
1766
+ cvvEl.setCvvLength(brand === 'amex' ? 4 : 3);
1767
+ }
1768
+ }
1769
+ // Auto-advance focus on completion
1770
+ if (justCompleted) {
1771
+ if (el.type === 'cardNumber') {
1772
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1773
+ (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1774
+ }
1775
+ else if (el.type === 'expirationDate') {
1776
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1777
+ (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1778
+ }
1779
+ }
1780
+ }
1781
+ handleTokenizerMessage(msg) {
1782
+ var _a, _b, _c, _d, _e;
1783
+ switch (msg.type) {
1784
+ case 'OZ_FRAME_READY':
1785
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1786
+ console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
1787
+ `tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1788
+ 'This usually means the deployed frame files are stale. ' +
1789
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1790
+ }
1791
+ this.tokenizerReady = true;
1792
+ if (this.loadErrorTimeoutId != null) {
1793
+ clearTimeout(this.loadErrorTimeoutId);
1794
+ this.loadErrorTimeoutId = null;
1795
+ }
1796
+ this.tokenizerWindow = (_b = (_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
1797
+ // Deliver the wax key via OZ_INIT so the tokenizer stores it internally.
1798
+ // If waxKey is still empty (fetchWaxKey hasn't resolved yet), it will be
1799
+ // sent again from create() once the key is available.
1800
+ this.sendToTokenizer(Object.assign(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})), { debug: this._debug }));
1801
+ (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1802
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1803
+ this.log('vault state', this.debugState());
1804
+ break;
1805
+ case 'OZ_TOKEN_RESULT': {
1806
+ if (typeof msg.requestId !== 'string' || !msg.requestId) {
1807
+ console.error('[OzVault] OZ_TOKEN_RESULT missing requestId — discarding message.');
1808
+ break;
1809
+ }
1810
+ const pending = this.tokenizeResolvers.get(msg.requestId);
1811
+ if (pending) {
1812
+ this.tokenizeResolvers.delete(msg.requestId);
1813
+ if (pending.timeoutId != null)
1814
+ clearTimeout(pending.timeoutId);
1815
+ const token = msg.token;
1816
+ if (typeof token !== 'string' || !token) {
1817
+ pending.reject(new OzError('Vault returned a token result with a missing or empty token — possible vault API change.', undefined, 'server'));
1818
+ break;
1819
+ }
1820
+ const card = isCardMetadata(msg.card) ? msg.card : undefined;
1821
+ const cvcSession = typeof msg.cvcSession === 'string' && msg.cvcSession ? msg.cvcSession : undefined;
1822
+ if (!cvcSession) {
1823
+ pending.reject(new OzError('Vault returned a token result without a cvcSession — possible vault misconfiguration or API change.', undefined, 'server'));
1824
+ break;
1825
+ }
1826
+ pending.resolve(Object.assign(Object.assign({ token,
1827
+ cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1828
+ this.log('token received', {
1829
+ requestIdPrefix: msg.requestId.slice(0, 12),
1830
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1831
+ tokenPresent: true,
1832
+ cvcSessionPresent: true,
1833
+ cardMetadataPresent: Boolean(card),
1834
+ });
1835
+ // Increment the per-key success counter and proactively refresh once
1836
+ // the budget is exhausted so the next createToken() call uses a fresh
1837
+ // key without waiting for a vault rejection.
1838
+ this._tokenizeSuccessCount++;
1839
+ if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1840
+ this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1841
+ this.refreshWaxKey().catch((err) => {
1842
+ console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
1843
+ });
1844
+ }
1845
+ }
1846
+ break;
1847
+ }
1848
+ case 'OZ_TOKEN_ERROR': {
1849
+ if (typeof msg.requestId !== 'string' || !msg.requestId) {
1850
+ console.error('[OzVault] OZ_TOKEN_ERROR missing requestId — discarding message.');
1851
+ break;
1852
+ }
1853
+ const raw = typeof msg.error === 'string' ? msg.error : '';
1854
+ const errorCode = isOzErrorCode(msg.errorCode) ? msg.errorCode : 'unknown';
1855
+ const pending = this.tokenizeResolvers.get(msg.requestId);
1856
+ if (pending) {
1857
+ this.tokenizeResolvers.delete(msg.requestId);
1858
+ if (pending.timeoutId != null)
1859
+ clearTimeout(pending.timeoutId);
1860
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1861
+ this.log('token error', { requestIdPrefix: msg.requestId.slice(0, 12), errorCode, willRefresh });
1862
+ // Auto-refresh: if the wax key expired or was consumed and we haven't
1863
+ // already retried for this request, transparently re-mint and retry.
1864
+ if (willRefresh) {
1865
+ const resetCountAtRetry = this._resetCount;
1866
+ this.refreshWaxKey().then(() => {
1867
+ if (this._destroyed) {
1868
+ pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1869
+ return;
1870
+ }
1871
+ if (this._resetCount !== resetCountAtRetry) {
1872
+ // reset() was called while the wax key was refreshing — the fields
1873
+ // have been cleared and _tokenizing was zeroed. Reject the original
1874
+ // promise so it doesn't stay pending, and bail out without starting
1875
+ // a new retry (which would tokenize against empty fields).
1876
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1877
+ return;
1878
+ }
1879
+ // Verify all elements from the original call are still mounted and
1880
+ // ready. If any were destroyed (e.g. the merchant called
1881
+ // createElement() or destroy() during the async refresh), the
1882
+ // beginCollect() calls below would silently no-op — the tokenizer
1883
+ // would wait forever for field values that never arrive. Reject
1884
+ // immediately with a clear message instead of hanging 30 seconds.
1885
+ const allCardElementsStillReady = pending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
1886
+ if (!allCardElementsStillReady) {
1887
+ pending.reject(new OzError('Card fields changed during session refresh. Please re-enter your card details.'));
1888
+ return;
1889
+ }
1890
+ const newRequestId = `req-${uuid()}`;
1891
+ // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1892
+ this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
1893
+ try {
1894
+ const retryCardChannels = pending.readyElements.map(() => new MessageChannel());
1895
+ this.sendToTokenizer({
1896
+ type: 'OZ_TOKENIZE',
1897
+ requestId: newRequestId,
1898
+ tokenizationSessionId: this.tokenizationSessionId,
1899
+ pubKey: this.pubKey,
1900
+ firstName: pending.firstName,
1901
+ lastName: pending.lastName,
1902
+ fieldCount: pending.fieldCount,
1903
+ }, retryCardChannels.map(ch => ch.port1));
1904
+ pending.readyElements.forEach((el, i) => {
1905
+ this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
1906
+ el.beginCollect(newRequestId, retryCardChannels[i].port2);
1907
+ });
1908
+ const retryCardTimeoutId = setTimeout(() => {
1909
+ if (this.tokenizeResolvers.has(newRequestId)) {
1910
+ this.tokenizeResolvers.delete(newRequestId);
1911
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1912
+ pending.reject(new OzError('Tokenization timed out after wax key refresh.', undefined, 'timeout'));
1913
+ }
1914
+ }, 30000);
1915
+ const retryCardEntry = this.tokenizeResolvers.get(newRequestId);
1916
+ if (retryCardEntry)
1917
+ retryCardEntry.timeoutId = retryCardTimeoutId;
1918
+ }
1919
+ catch (setupErr) {
1920
+ this.tokenizeResolvers.delete(newRequestId);
1921
+ pending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry tokenization failed to start'));
1922
+ }
1923
+ }).catch((refreshErr) => {
1924
+ const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1925
+ pending.reject(new OzError(errMsg, undefined, 'auth'));
1926
+ });
1927
+ break;
1928
+ }
1929
+ pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
1930
+ }
1931
+ else {
1932
+ // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
1933
+ // Use else-if rather than sequential checks so a UUID collision (however
1934
+ // improbable) can never trigger double-rejection of two unrelated resolvers.
1935
+ const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1936
+ if (bankPending) {
1937
+ this.bankTokenizeResolvers.delete(msg.requestId);
1938
+ if (bankPending.timeoutId != null)
1939
+ clearTimeout(bankPending.timeoutId);
1940
+ if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1941
+ const resetCountAtRetry = this._resetCount;
1942
+ this.refreshWaxKey().then(() => {
1943
+ if (this._destroyed) {
1944
+ bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1945
+ return;
1946
+ }
1947
+ if (this._resetCount !== resetCountAtRetry) {
1948
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1949
+ return;
1950
+ }
1951
+ // Same stale-element guard as the card retry path above.
1952
+ const allBankElementsStillReady = bankPending.readyElements.every(el => this.elements.has(el.frameId) && el.isReady);
1953
+ if (!allBankElementsStillReady) {
1954
+ bankPending.reject(new OzError('Bank fields changed during session refresh. Please re-enter your account details.'));
1955
+ return;
1956
+ }
1957
+ const newRequestId = `req-${uuid()}`;
1958
+ this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1959
+ try {
1960
+ const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
1961
+ this.sendToTokenizer({
1962
+ type: 'OZ_BANK_TOKENIZE',
1963
+ requestId: newRequestId,
1964
+ tokenizationSessionId: this.tokenizationSessionId,
1965
+ pubKey: this.pubKey,
1966
+ firstName: bankPending.firstName,
1967
+ lastName: bankPending.lastName,
1968
+ fieldCount: bankPending.fieldCount,
1969
+ }, retryBankChannels.map(ch => ch.port1));
1970
+ bankPending.readyElements.forEach((el, i) => {
1971
+ this.log('OZ_BEGIN_COLLECT dispatched (retry)', { type: el.type, requestIdPrefix: `${newRequestId.slice(0, 12)}...` });
1972
+ el.beginCollect(newRequestId, retryBankChannels[i].port2);
1973
+ });
1974
+ const retryBankTimeoutId = setTimeout(() => {
1975
+ if (this.bankTokenizeResolvers.has(newRequestId)) {
1976
+ this.bankTokenizeResolvers.delete(newRequestId);
1977
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
1978
+ bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
1979
+ }
1980
+ }, 30000);
1981
+ const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
1982
+ if (retryBankEntry)
1983
+ retryBankEntry.timeoutId = retryBankTimeoutId;
1984
+ }
1985
+ catch (setupErr) {
1986
+ this.bankTokenizeResolvers.delete(newRequestId);
1987
+ bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
1988
+ }
1989
+ }).catch((refreshErr) => {
1990
+ const errMsg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
1991
+ bankPending.reject(new OzError(errMsg, undefined, 'auth'));
1992
+ });
1993
+ break;
1994
+ }
1995
+ bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1996
+ }
1997
+ } // end else (bank path)
1998
+ break;
1999
+ }
2000
+ case 'OZ_BANK_TOKEN_RESULT': {
2001
+ if (typeof msg.requestId !== 'string' || !msg.requestId) {
2002
+ console.error('[OzVault] OZ_BANK_TOKEN_RESULT missing requestId — discarding message.');
2003
+ break;
2004
+ }
2005
+ const pending = this.bankTokenizeResolvers.get(msg.requestId);
2006
+ if (pending) {
2007
+ this.bankTokenizeResolvers.delete(msg.requestId);
2008
+ if (pending.timeoutId != null)
2009
+ clearTimeout(pending.timeoutId);
2010
+ const token = msg.token;
2011
+ if (typeof token !== 'string' || !token) {
2012
+ pending.reject(new OzError('Vault returned a bank token result with a missing or empty token — possible vault API change.', undefined, 'server'));
2013
+ break;
2014
+ }
2015
+ const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
2016
+ pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
2017
+ this.log('bank token received', {
2018
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
2019
+ tokenPresent: true,
2020
+ bankMetadataPresent: Boolean(bank),
2021
+ });
2022
+ // Same proactive refresh logic as card tokenization.
2023
+ this._tokenizeSuccessCount++;
2024
+ if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
2025
+ this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
2026
+ this.refreshWaxKey().catch((err) => {
2027
+ console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
2028
+ });
2029
+ }
2030
+ }
2031
+ break;
2032
+ }
2033
+ case 'OZ_DEBUG_LOG': {
2034
+ // Relay warnings/errors from the tokenizer iframe into the parent page's
2035
+ // DevTools console. The tokenizer runs in a cross-origin iframe whose
2036
+ // console context is invisible to most developers unless they manually
2037
+ // switch the DevTools frame selector.
2038
+ // Errors are always surfaced; warnings only in debug mode to avoid
2039
+ // polluting production consoles.
2040
+ const level = typeof msg.level === 'string' ? msg.level : 'warn';
2041
+ const message = typeof msg.message === 'string' ? msg.message : String((_e = msg.message) !== null && _e !== void 0 ? _e : '');
2042
+ if (level === 'error') {
2043
+ console.error(`[OzVault:tokenizer] ${message}`);
2044
+ }
2045
+ else if (this._debug) {
2046
+ console.warn(`[OzVault:tokenizer] ${message}`);
2047
+ }
2048
+ break;
2049
+ }
2050
+ }
2051
+ }
2052
+ /**
2053
+ * Returns true when an OZ_TOKEN_ERROR should trigger a wax key refresh.
2054
+ *
2055
+ * Primary path: vault returns 401/403 → errorCode 'auth'.
2056
+ * Defensive path: vault returns 400 → errorCode 'validation', but the raw
2057
+ * message contains wax-key-specific language (consumed, expired, invalid key,
2058
+ * etc.). This avoids a hard dependency on the vault returning a unified HTTP
2059
+ * status for consumed-key vs expired-key failures — both should refresh.
2060
+ *
2061
+ * Deliberately excludes 'network', 'timeout', and 'server' codes (transient
2062
+ * errors are already retried in fetchWithRetry) and 'unknown' (too broad).
2063
+ */
2064
+ isRefreshableAuthError(errorCode, raw) {
2065
+ if (errorCode === 'auth')
2066
+ return true;
2067
+ if (errorCode === 'validation') {
2068
+ const msg = raw.toLowerCase();
2069
+ // Only treat validation errors as wax-related if the message explicitly
2070
+ // names the wax/tokenization session mechanism. A bare "session" match
2071
+ // was too broad — any message mentioning "session" (e.g. a merchant
2072
+ // session field error) would trigger a spurious re-mint.
2073
+ return (msg.includes('wax') ||
2074
+ msg.includes('expired') ||
2075
+ msg.includes('consumed') ||
2076
+ msg.includes('invalid key') ||
2077
+ msg.includes('key not found') ||
2078
+ msg.includes('tokenization session'));
2079
+ }
2080
+ return false;
2081
+ }
2082
+ /**
2083
+ * Re-mints the wax key using the stored fetchWaxKey callback and updates
2084
+ * the tokenizer with the new key. Used for transparent auto-refresh when
2085
+ * the vault returns an auth error on tokenization.
2086
+ *
2087
+ * Only one refresh runs at a time — concurrent retries share the same promise.
2088
+ */
2089
+ refreshWaxKey() {
2090
+ var _a;
2091
+ if (this._waxRefreshing)
2092
+ return this._waxRefreshing;
2093
+ if (!this._storedFetchWaxKey) {
2094
+ 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'));
2095
+ }
2096
+ const newSessionId = uuid();
2097
+ (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
2098
+ const refreshStartMs = Date.now();
2099
+ this.log('wax key refresh started');
2100
+ this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
2101
+ .then(newWaxKey => {
2102
+ if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
2103
+ throw new OzError('fetchWaxKey returned an empty string during auto-refresh.', undefined, 'auth');
2104
+ }
2105
+ if (!this._destroyed) {
2106
+ this.waxKey = newWaxKey;
2107
+ this.tokenizationSessionId = newSessionId;
2108
+ this._tokenizeSuccessCount = 0;
2109
+ }
2110
+ if (!this._destroyed && this.tokenizerReady) {
2111
+ this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey, debug: this._debug });
2112
+ }
2113
+ this.log('wax key refresh succeeded', { durationMs: Date.now() - refreshStartMs });
2114
+ })
2115
+ .catch((err) => {
2116
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - refreshStartMs });
2117
+ throw err;
2118
+ })
2119
+ .finally(() => {
2120
+ this._waxRefreshing = null;
2121
+ });
2122
+ return this._waxRefreshing;
2123
+ }
2124
+ sendToTokenizer(data, transfer) {
2125
+ var _a;
2126
+ const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
2127
+ (_a = this.tokenizerWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin, transfer !== null && transfer !== void 0 ? transfer : []);
2128
+ }
2129
+ }
2130
+
2131
+ /**
2132
+ * @ozura/elements/vue — Vue 3 wrapper for OzElements.
2133
+ *
2134
+ * Provides a context-based provider that creates and manages an OzVault instance,
2135
+ * five individual field components, and a composable to access createToken +
2136
+ * readiness state from anywhere inside the provider tree.
2137
+ *
2138
+ * Quick start:
2139
+ * @example
2140
+ * ```vue
2141
+ * <script setup lang="ts">
2142
+ * import { OzElements, OzCardNumber, OzExpiry, OzCvv, useOzElements } from '@ozura/elements/vue';
2143
+ * const { createToken, ready } = useOzElements();
2144
+ * </script>
2145
+ *
2146
+ * <template>
2147
+ * <OzElements pub-key="pk_live_..." session-url="/api/oz-session">
2148
+ * <OzCardNumber />
2149
+ * <OzExpiry />
2150
+ * <OzCvv />
2151
+ * <button :disabled="!ready" @click="createToken()">Pay</button>
2152
+ * </OzElements>
2153
+ * </template>
2154
+ * ```
2155
+ */
2156
+ const OZ_KEY = Symbol('oz-elements');
2157
+ // ─── Provider ─────────────────────────────────────────────────────────────────
2158
+ /**
2159
+ * Creates and owns an OzVault instance for the lifetime of this component.
2160
+ * All field components must be rendered inside this provider.
2161
+ */
2162
+ const OzElements = vue.defineComponent({
2163
+ name: 'OzElements',
2164
+ props: {
2165
+ pubKey: { type: String, required: true },
2166
+ sessionUrl: { type: String, default: undefined },
2167
+ getSessionKey: { type: Function, default: undefined },
2168
+ frameBaseUrl: { type: String, default: undefined },
2169
+ fonts: { type: Array, default: undefined },
2170
+ appearance: { type: Object, default: undefined },
2171
+ loadTimeoutMs: { type: Number, default: undefined },
2172
+ debug: { type: Boolean, default: undefined },
2173
+ },
2174
+ emits: ['ready'],
2175
+ setup(props, { slots, emit }) {
2176
+ const vault = vue.shallowRef(null);
2177
+ const initError = vue.ref(null);
2178
+ const mountedCount = vue.ref(0);
2179
+ const readyCount = vue.ref(0);
2180
+ const tokenizeCount = vue.ref(0);
2181
+ const notifyMount = () => { mountedCount.value++; };
2182
+ let readyEmitted = false;
2183
+ const notifyReady = () => {
2184
+ readyCount.value++;
2185
+ if (!readyEmitted && mountedCount.value > 0 && readyCount.value >= mountedCount.value) {
2186
+ readyEmitted = true;
2187
+ emit('ready');
2188
+ }
2189
+ };
2190
+ const notifyUnmount = () => {
2191
+ mountedCount.value = Math.max(0, mountedCount.value - 1);
2192
+ readyCount.value = Math.max(0, readyCount.value - 1);
2193
+ };
2194
+ const notifyTokenize = () => { tokenizeCount.value++; };
2195
+ vue.provide(OZ_KEY, {
2196
+ vault,
2197
+ initError,
2198
+ mountedCount,
2199
+ readyCount,
2200
+ tokenizeCount,
2201
+ notifyMount,
2202
+ notifyReady,
2203
+ notifyUnmount,
2204
+ notifyTokenize,
2205
+ });
2206
+ let createdVault = null;
2207
+ let abortController = null;
2208
+ vue.onMounted(() => {
2209
+ const ac = new AbortController();
2210
+ abortController = ac;
2211
+ OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey: props.pubKey }, (props.sessionUrl ? { sessionUrl: props.sessionUrl } : {})), (props.getSessionKey ? { getSessionKey: props.getSessionKey } : {})), (props.frameBaseUrl ? { frameBaseUrl: props.frameBaseUrl } : {})), (props.fonts ? { fonts: props.fonts } : {})), (props.appearance ? { appearance: props.appearance } : {})), (props.loadTimeoutMs !== undefined ? { loadTimeoutMs: props.loadTimeoutMs } : {})), (props.debug ? { debug: props.debug } : {})), ac.signal).then(v => {
2212
+ if (ac.signal.aborted) {
2213
+ v.destroy();
2214
+ return;
2215
+ }
2216
+ createdVault = v;
2217
+ vault.value = v;
2218
+ initError.value = null;
2219
+ }).catch((err) => {
2220
+ if (ac.signal.aborted)
2221
+ return;
2222
+ initError.value = err instanceof Error ? err : new Error('OzVault.create() failed.');
2223
+ });
2224
+ });
2225
+ vue.onUnmounted(() => {
2226
+ abortController === null || abortController === void 0 ? void 0 : abortController.abort();
2227
+ createdVault === null || createdVault === void 0 ? void 0 : createdVault.destroy();
2228
+ vault.value = null;
2229
+ });
2230
+ return () => { var _a; return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots); };
2231
+ },
2232
+ });
2233
+ /**
2234
+ * Returns createToken, createBankToken, ready, initError, tokenizeCount, and reset.
2235
+ * Must be called from inside an <OzElements> provider tree.
2236
+ *
2237
+ * @throws {Error} if called outside an <OzElements> provider
2238
+ */
2239
+ function useOzElements() {
2240
+ const ctx = vue.inject(OZ_KEY);
2241
+ if (!ctx) {
2242
+ throw new Error('[OzVault] useOzElements() must be called inside <OzElements>');
2243
+ }
2244
+ const { vault, initError, mountedCount, readyCount, tokenizeCount, notifyTokenize } = ctx;
2245
+ const ready = vue.computed(() => vault.value !== null &&
2246
+ vault.value.isReady &&
2247
+ mountedCount.value > 0 &&
2248
+ readyCount.value >= mountedCount.value);
2249
+ const createToken = async (options) => {
2250
+ if (!vault.value) {
2251
+ throw new Error('[OzVault] vault is not ready — wait for ready before calling createToken()');
2252
+ }
2253
+ const result = await vault.value.createToken(options);
2254
+ notifyTokenize();
2255
+ return result;
2256
+ };
2257
+ const createBankToken = async (options) => {
2258
+ if (!vault.value) {
2259
+ throw new Error('[OzVault] vault is not ready — wait for ready before calling createBankToken()');
2260
+ }
2261
+ const result = await vault.value.createBankToken(options);
2262
+ notifyTokenize();
2263
+ return result;
2264
+ };
2265
+ const reset = () => { var _a; (_a = vault.value) === null || _a === void 0 ? void 0 : _a.reset(); };
2266
+ return { createToken, createBankToken, ready, initError, tokenizeCount, reset };
2267
+ }
2268
+ function createFieldComponent(displayName, mountElement) {
2269
+ return vue.defineComponent({
2270
+ name: displayName,
2271
+ props: {
2272
+ placeholder: { type: String, default: undefined },
2273
+ disabled: { type: Boolean, default: undefined },
2274
+ style: { type: Object, default: undefined },
2275
+ },
2276
+ emits: ['change', 'focus', 'blur'],
2277
+ setup(props, { emit }) {
2278
+ const ctx = vue.inject(OZ_KEY);
2279
+ if (!ctx) {
2280
+ throw new Error('[OzVault] useOzElements() must be called inside <OzElements>');
2281
+ }
2282
+ const { vault, notifyMount, notifyReady, notifyUnmount } = ctx;
2283
+ const containerRef = vue.ref(null);
2284
+ let element = null;
2285
+ let notifyMountCalled = false;
2286
+ vue.watch(vault, (v) => {
2287
+ if (!v || !containerRef.value || element)
2288
+ return;
2289
+ element = mountElement(v, {
2290
+ style: props.style,
2291
+ placeholder: props.placeholder,
2292
+ disabled: props.disabled,
2293
+ });
2294
+ notifyMountCalled = true;
2295
+ notifyMount();
2296
+ element.on('ready', () => notifyReady());
2297
+ element.on('change', (e) => emit('change', e));
2298
+ element.on('focus', () => emit('focus'));
2299
+ element.on('blur', () => emit('blur'));
2300
+ element.mount(containerRef.value);
2301
+ }, { immediate: true });
2302
+ vue.onUnmounted(() => {
2303
+ element === null || element === void 0 ? void 0 : element.destroy();
2304
+ element = null;
2305
+ if (notifyMountCalled)
2306
+ notifyUnmount();
2307
+ });
2308
+ vue.watch(() => ({
2309
+ style: props.style,
2310
+ placeholder: props.placeholder,
2311
+ disabled: props.disabled,
2312
+ }), (opts) => { element === null || element === void 0 ? void 0 : element.update(opts); }, { deep: true });
2313
+ return () => vue.h('div', { ref: containerRef });
2314
+ },
2315
+ });
2316
+ }
2317
+ /** Card number field. Emits `change` (ElementChangeEvent), `focus`, `blur`. */
2318
+ const OzCardNumber = createFieldComponent('OzCardNumber', (v, opts) => v.createElement('cardNumber', opts));
2319
+ /** Expiry date field. Emits `change` (ElementChangeEvent), `focus`, `blur`. */
2320
+ const OzExpiry = createFieldComponent('OzExpiry', (v, opts) => v.createElement('expirationDate', opts));
2321
+ /** CVV / CVC field. Emits `change` (ElementChangeEvent), `focus`, `blur`. */
2322
+ const OzCvv = createFieldComponent('OzCvv', (v, opts) => v.createElement('cvv', opts));
2323
+ /** Bank account number field. Emits `change` (ElementChangeEvent), `focus`, `blur`. */
2324
+ const OzBankAccountNumber = createFieldComponent('OzBankAccountNumber', (v, opts) => v.createBankElement('accountNumber', opts));
2325
+ /** Bank routing number field. Emits `change` (ElementChangeEvent), `focus`, `blur`. */
2326
+ const OzBankRoutingNumber = createFieldComponent('OzBankRoutingNumber', (v, opts) => v.createBankElement('routingNumber', opts));
2327
+
2328
+ exports.OzBankAccountNumber = OzBankAccountNumber;
2329
+ exports.OzBankRoutingNumber = OzBankRoutingNumber;
2330
+ exports.OzCardNumber = OzCardNumber;
2331
+ exports.OzCvv = OzCvv;
2332
+ exports.OzElements = OzElements;
2333
+ exports.OzExpiry = OzExpiry;
2334
+ exports.useOzElements = useOzElements;
2335
+ //# sourceMappingURL=index.cjs.js.map