@ozura/elements 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +717 -0
  3. package/dist/frame/element-frame.html +22 -0
  4. package/dist/frame/element-frame.js +731 -0
  5. package/dist/frame/element-frame.js.map +1 -0
  6. package/dist/frame/tokenizer-frame.html +11 -0
  7. package/dist/frame/tokenizer-frame.js +328 -0
  8. package/dist/frame/tokenizer-frame.js.map +1 -0
  9. package/dist/oz-elements.esm.js +1190 -0
  10. package/dist/oz-elements.esm.js.map +1 -0
  11. package/dist/oz-elements.umd.js +1202 -0
  12. package/dist/oz-elements.umd.js.map +1 -0
  13. package/dist/react/frame/elementFrame.d.ts +8 -0
  14. package/dist/react/frame/tokenizerFrame.d.ts +13 -0
  15. package/dist/react/index.cjs.js +1407 -0
  16. package/dist/react/index.cjs.js.map +1 -0
  17. package/dist/react/index.esm.js +1400 -0
  18. package/dist/react/index.esm.js.map +1 -0
  19. package/dist/react/react/index.d.ts +214 -0
  20. package/dist/react/sdk/OzElement.d.ts +65 -0
  21. package/dist/react/sdk/OzVault.d.ts +106 -0
  22. package/dist/react/sdk/errors.d.ts +55 -0
  23. package/dist/react/sdk/index.d.ts +5 -0
  24. package/dist/react/server/index.d.ts +140 -0
  25. package/dist/react/types/index.d.ts +432 -0
  26. package/dist/react/utils/appearance.d.ts +13 -0
  27. package/dist/react/utils/billingUtils.d.ts +60 -0
  28. package/dist/react/utils/cardUtils.d.ts +37 -0
  29. package/dist/server/frame/elementFrame.d.ts +8 -0
  30. package/dist/server/frame/tokenizerFrame.d.ts +13 -0
  31. package/dist/server/index.cjs.js +294 -0
  32. package/dist/server/index.cjs.js.map +1 -0
  33. package/dist/server/index.esm.js +290 -0
  34. package/dist/server/index.esm.js.map +1 -0
  35. package/dist/server/sdk/OzElement.d.ts +65 -0
  36. package/dist/server/sdk/OzVault.d.ts +106 -0
  37. package/dist/server/sdk/errors.d.ts +55 -0
  38. package/dist/server/sdk/index.d.ts +5 -0
  39. package/dist/server/server/index.d.ts +140 -0
  40. package/dist/server/types/index.d.ts +432 -0
  41. package/dist/server/utils/appearance.d.ts +13 -0
  42. package/dist/server/utils/billingUtils.d.ts +60 -0
  43. package/dist/server/utils/cardUtils.d.ts +37 -0
  44. package/dist/types/frame/elementFrame.d.ts +8 -0
  45. package/dist/types/frame/tokenizerFrame.d.ts +13 -0
  46. package/dist/types/sdk/OzElement.d.ts +65 -0
  47. package/dist/types/sdk/OzVault.d.ts +106 -0
  48. package/dist/types/sdk/errors.d.ts +55 -0
  49. package/dist/types/sdk/index.d.ts +5 -0
  50. package/dist/types/server/index.d.ts +140 -0
  51. package/dist/types/types/index.d.ts +432 -0
  52. package/dist/types/utils/appearance.d.ts +13 -0
  53. package/dist/types/utils/billingUtils.d.ts +60 -0
  54. package/dist/types/utils/cardUtils.d.ts +37 -0
  55. package/package.json +97 -0
@@ -0,0 +1,1202 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.OzElements = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ const THEME_DEFAULT = {
8
+ base: {
9
+ color: '#1a1a2e',
10
+ fontSize: '16px',
11
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
12
+ lineHeight: '1.5',
13
+ padding: '12px 14px',
14
+ backgroundColor: 'transparent',
15
+ caretColor: '#6366f1',
16
+ transition: 'color .15s ease',
17
+ },
18
+ focus: {
19
+ color: '#111827',
20
+ },
21
+ invalid: {
22
+ color: '#dc2626',
23
+ },
24
+ complete: {
25
+ color: '#16a34a',
26
+ },
27
+ placeholder: {
28
+ color: '#9ca3af',
29
+ },
30
+ };
31
+ const THEME_NIGHT = {
32
+ base: {
33
+ color: '#e5e7eb',
34
+ fontSize: '16px',
35
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
36
+ lineHeight: '1.5',
37
+ padding: '12px 14px',
38
+ backgroundColor: 'transparent',
39
+ caretColor: '#818cf8',
40
+ transition: 'color .15s ease',
41
+ },
42
+ focus: {
43
+ color: '#f9fafb',
44
+ },
45
+ invalid: {
46
+ color: '#fca5a5',
47
+ },
48
+ complete: {
49
+ color: '#86efac',
50
+ },
51
+ placeholder: {
52
+ color: '#6b7280',
53
+ },
54
+ };
55
+ const THEME_FLAT = {
56
+ base: {
57
+ color: '#374151',
58
+ fontSize: '16px',
59
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
60
+ lineHeight: '1.5',
61
+ padding: '12px 14px',
62
+ backgroundColor: 'transparent',
63
+ caretColor: '#6366f1',
64
+ transition: 'color .15s ease',
65
+ borderBottom: '2px solid #d1d5db',
66
+ borderRadius: '0px',
67
+ },
68
+ focus: {
69
+ color: '#111827',
70
+ borderBottom: '2px solid #6366f1',
71
+ },
72
+ invalid: {
73
+ color: '#dc2626',
74
+ borderBottom: '2px solid #dc2626',
75
+ },
76
+ complete: {
77
+ color: '#16a34a',
78
+ borderBottom: '2px solid #16a34a',
79
+ },
80
+ placeholder: {
81
+ color: '#9ca3af',
82
+ },
83
+ };
84
+ const THEMES = {
85
+ default: THEME_DEFAULT,
86
+ night: THEME_NIGHT,
87
+ flat: THEME_FLAT,
88
+ };
89
+ function variablesToStyle(vars) {
90
+ const base = {};
91
+ const focus = {};
92
+ const invalid = {};
93
+ const complete = {};
94
+ const placeholder = {};
95
+ if (vars.colorText)
96
+ base.color = vars.colorText;
97
+ if (vars.colorBackground)
98
+ base.backgroundColor = vars.colorBackground;
99
+ if (vars.fontFamily)
100
+ base.fontFamily = vars.fontFamily;
101
+ if (vars.fontSize)
102
+ base.fontSize = vars.fontSize;
103
+ if (vars.fontWeight)
104
+ base.fontWeight = vars.fontWeight;
105
+ if (vars.letterSpacing)
106
+ base.letterSpacing = vars.letterSpacing;
107
+ if (vars.lineHeight)
108
+ base.lineHeight = vars.lineHeight;
109
+ if (vars.padding)
110
+ base.padding = vars.padding;
111
+ if (vars.colorPrimary) {
112
+ focus.caretColor = vars.colorPrimary;
113
+ base.caretColor = vars.colorPrimary;
114
+ }
115
+ if (vars.colorDanger)
116
+ invalid.color = vars.colorDanger;
117
+ if (vars.colorSuccess)
118
+ complete.color = vars.colorSuccess;
119
+ if (vars.colorPlaceholder)
120
+ placeholder.color = vars.colorPlaceholder;
121
+ 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 } : {}));
122
+ }
123
+ function mergeStyleConfigs(a, b) {
124
+ return {
125
+ base: Object.assign(Object.assign({}, a.base), b.base),
126
+ focus: Object.assign(Object.assign({}, a.focus), b.focus),
127
+ invalid: Object.assign(Object.assign({}, a.invalid), b.invalid),
128
+ complete: Object.assign(Object.assign({}, a.complete), b.complete),
129
+ placeholder: Object.assign(Object.assign({}, a.placeholder), b.placeholder),
130
+ };
131
+ }
132
+ /**
133
+ * Resolves an `Appearance` config into a flat `ElementStyleConfig`.
134
+ * Resolution order: theme defaults → variable overrides.
135
+ * The returned config is then used as the "base appearance" that
136
+ * per-element `style` overrides merge on top of.
137
+ */
138
+ function resolveAppearance(appearance) {
139
+ var _a, _b;
140
+ if (!appearance)
141
+ return undefined;
142
+ const theme = (_b = THEMES[(_a = appearance.theme) !== null && _a !== void 0 ? _a : 'default']) !== null && _b !== void 0 ? _b : THEMES.default;
143
+ if (!appearance.variables)
144
+ return theme;
145
+ const varStyles = variablesToStyle(appearance.variables);
146
+ return mergeStyleConfigs(theme, varStyles);
147
+ }
148
+ /**
149
+ * Merges a resolved appearance with per-element style overrides.
150
+ * Element styles always win over appearance styles.
151
+ */
152
+ function mergeAppearanceWithElementStyle(appearance, elementStyle) {
153
+ if (!appearance && !elementStyle)
154
+ return undefined;
155
+ if (!appearance)
156
+ return elementStyle;
157
+ if (!elementStyle)
158
+ return appearance;
159
+ return mergeStyleConfigs(appearance, elementStyle);
160
+ }
161
+
162
+ 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]/i;
163
+ const CSS_BREAKOUT = /[{};<>]/;
164
+ const MAX_CSS_VALUE_LEN = 200;
165
+ function sanitizeStyleObj(obj) {
166
+ if (!obj)
167
+ return obj;
168
+ const clean = {};
169
+ for (const [k, v] of Object.entries(obj)) {
170
+ if (v === undefined) {
171
+ clean[k] = v;
172
+ continue;
173
+ }
174
+ if (typeof v !== 'string' || v.length > MAX_CSS_VALUE_LEN || BLOCKED_CSS_PATTERNS.test(v) || CSS_BREAKOUT.test(v))
175
+ continue;
176
+ clean[k] = v;
177
+ }
178
+ return clean;
179
+ }
180
+ function sanitizeStyles(style) {
181
+ if (!style)
182
+ return style;
183
+ return {
184
+ base: sanitizeStyleObj(style.base),
185
+ focus: sanitizeStyleObj(style.focus),
186
+ invalid: sanitizeStyleObj(style.invalid),
187
+ complete: sanitizeStyleObj(style.complete),
188
+ placeholder: sanitizeStyleObj(style.placeholder),
189
+ };
190
+ }
191
+ function sanitizeOptions(options) {
192
+ var _a;
193
+ const result = Object.assign(Object.assign({}, options), { placeholder: (_a = options.placeholder) === null || _a === void 0 ? void 0 : _a.slice(0, 100) });
194
+ // Only set style when provided; omitting it avoids clobbering existing style
195
+ // when merging (e.g. update({ placeholder: 'new' }) must not overwrite style with undefined).
196
+ if (options.style !== undefined) {
197
+ result.style = sanitizeStyles(options.style);
198
+ }
199
+ return result;
200
+ }
201
+ /**
202
+ * A proxy for one Ozura iframe element. Merchants interact with this object;
203
+ * it never holds raw card data — all sensitive values live in the iframe.
204
+ */
205
+ class OzElement {
206
+ constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
207
+ this.iframe = null;
208
+ this._frameWindow = null;
209
+ this._ready = false;
210
+ this._destroyed = false;
211
+ this._loadTimer = null;
212
+ this.pendingMessages = [];
213
+ this.handlers = new Map();
214
+ this.elementType = elementType;
215
+ this.options = sanitizeOptions(options);
216
+ this.vaultId = vaultId;
217
+ this.frameBaseUrl = frameBaseUrl;
218
+ this.frameOrigin = new URL(frameBaseUrl).origin;
219
+ this.fonts = fonts;
220
+ this.appearanceStyle = appearanceStyle;
221
+ this.frameId = `oz-${elementType}-${Math.random().toString(36).slice(2, 10)}`;
222
+ }
223
+ /** The element type this proxy represents. */
224
+ get type() {
225
+ return this.elementType;
226
+ }
227
+ /** True once the element iframe has loaded and signalled ready. */
228
+ get isReady() {
229
+ return this._ready;
230
+ }
231
+ /**
232
+ * Mounts the element iframe into a container.
233
+ * Accepts either a CSS selector string or a direct HTMLElement reference
234
+ * (useful when integrating with React refs).
235
+ */
236
+ mount(target) {
237
+ var _a;
238
+ if (this._destroyed)
239
+ throw new Error('OzElements: cannot mount a destroyed element.');
240
+ if (this.iframe)
241
+ this.unmount();
242
+ const container = typeof target === 'string'
243
+ ? document.querySelector(target)
244
+ : target;
245
+ if (!container)
246
+ throw new Error(typeof target === 'string'
247
+ ? `OzElements: mount target not found — no element matches "${target}"`
248
+ : `OzElements: mount target not found — the provided HTMLElement is null or undefined`);
249
+ const iframe = document.createElement('iframe');
250
+ iframe.setAttribute('frameborder', '0');
251
+ iframe.setAttribute('scrolling', 'no');
252
+ iframe.setAttribute('allowtransparency', 'true');
253
+ iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
254
+ iframe.title = `Secure ${this.elementType} input`;
255
+ // Use hash instead of query string — survives clean-URL redirects from static servers.
256
+ // parentOrigin lets the frame target postMessage to the merchant origin instead of '*'.
257
+ const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
258
+ const src = `${this.frameBaseUrl}/frame/element-frame.html#type=${this.elementType}&vaultId=${encodeURIComponent(this.vaultId)}&frameId=${encodeURIComponent(this.frameId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
259
+ iframe.src = src;
260
+ container.appendChild(iframe);
261
+ this.iframe = iframe;
262
+ this._frameWindow = iframe.contentWindow;
263
+ const timeout = (_a = this.options.loadTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
264
+ this._loadTimer = setTimeout(() => {
265
+ if (!this._ready && !this._destroyed) {
266
+ this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
267
+ }
268
+ }, timeout);
269
+ }
270
+ on(event, callback) {
271
+ if (this._destroyed)
272
+ return this;
273
+ if (!this.handlers.has(event))
274
+ this.handlers.set(event, []);
275
+ this.handlers.get(event).push(callback);
276
+ return this;
277
+ }
278
+ off(event, callback) {
279
+ const list = this.handlers.get(event);
280
+ if (list) {
281
+ const idx = list.indexOf(callback);
282
+ if (idx !== -1)
283
+ list.splice(idx, 1);
284
+ }
285
+ return this;
286
+ }
287
+ once(event, callback) {
288
+ const wrapper = (payload) => {
289
+ this.off(event, wrapper);
290
+ callback(payload);
291
+ };
292
+ return this.on(event, wrapper);
293
+ }
294
+ update(options) {
295
+ if (this._destroyed)
296
+ return;
297
+ const safe = sanitizeOptions(options);
298
+ this.options = Object.assign(Object.assign({}, this.options), safe);
299
+ this.post({ type: 'OZ_UPDATE', options: safe });
300
+ }
301
+ clear() {
302
+ if (this._destroyed)
303
+ return;
304
+ this.post({ type: 'OZ_CLEAR' });
305
+ }
306
+ /**
307
+ * Removes the iframe from the DOM and resets internal state.
308
+ * Called automatically by `OzVault.destroy()`. Safe to call manually
309
+ * for partial teardown (e.g. swapping payment method in a SPA).
310
+ */
311
+ unmount() {
312
+ var _a;
313
+ if (this._loadTimer) {
314
+ clearTimeout(this._loadTimer);
315
+ this._loadTimer = null;
316
+ }
317
+ (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.remove();
318
+ this.iframe = null;
319
+ this._frameWindow = null;
320
+ this._ready = false;
321
+ this.pendingMessages = [];
322
+ }
323
+ /** Programmatically focus this element's input. Used internally for auto-advance. */
324
+ focus() {
325
+ if (this._destroyed)
326
+ return;
327
+ this.post({ type: 'OZ_FOCUS_REQUEST' });
328
+ }
329
+ /** Programmatically blur this element's input. */
330
+ blur() {
331
+ if (this._destroyed)
332
+ return;
333
+ this.post({ type: 'OZ_BLUR_REQUEST' });
334
+ }
335
+ /**
336
+ * Permanently destroys this element: unmounts it, clears all event handlers,
337
+ * and prevents future use. Distinct from `unmount()` which allows re-mounting.
338
+ */
339
+ destroy() {
340
+ this.unmount();
341
+ this.handlers.clear();
342
+ this._destroyed = true;
343
+ }
344
+ // ─── Called by OzVault ───────────────────────────────────────────────────
345
+ setTokenizerName(tokenizerName) {
346
+ this.post({ type: 'OZ_SET_TOKENIZER_NAME', tokenizerName });
347
+ }
348
+ beginCollect(requestId) {
349
+ this.post({ type: 'OZ_BEGIN_COLLECT', requestId });
350
+ }
351
+ /** Tell a CVV element how many digits to expect. Called automatically when card brand changes. */
352
+ setCvvLength(length) {
353
+ this.post({ type: 'OZ_SET_CVV_LENGTH', length });
354
+ }
355
+ handleMessage(msg) {
356
+ var _a, _b;
357
+ switch (msg.type) {
358
+ case 'OZ_FRAME_READY': {
359
+ this._ready = true;
360
+ if (this._loadTimer) {
361
+ clearTimeout(this._loadTimer);
362
+ this._loadTimer = null;
363
+ }
364
+ this._frameWindow = (_b = (_a = this.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
365
+ const mergedOptions = Object.assign(Object.assign({}, this.options), { style: mergeAppearanceWithElementStyle(this.appearanceStyle, this.options.style) });
366
+ this.post(Object.assign({ type: 'OZ_INIT', elementType: this.elementType, options: sanitizeOptions(mergedOptions), frameId: this.frameId }, (this.fonts.length > 0 ? { fonts: this.fonts } : {})));
367
+ this.pendingMessages.forEach(m => this.send(m));
368
+ this.pendingMessages = [];
369
+ this.emit('ready', undefined);
370
+ break;
371
+ }
372
+ case 'OZ_CHANGE':
373
+ this.emit('change', {
374
+ empty: msg.empty,
375
+ complete: msg.complete,
376
+ valid: msg.valid,
377
+ cardBrand: msg.cardBrand,
378
+ month: msg.month,
379
+ year: msg.year,
380
+ error: msg.error,
381
+ });
382
+ break;
383
+ case 'OZ_FOCUS':
384
+ this.emit('focus', undefined);
385
+ break;
386
+ case 'OZ_BLUR':
387
+ this.emit('blur', {
388
+ empty: msg.empty,
389
+ complete: msg.complete,
390
+ valid: msg.valid,
391
+ error: msg.error,
392
+ });
393
+ break;
394
+ case 'OZ_RESIZE':
395
+ if (this.iframe) {
396
+ this.iframe.style.height = `${msg.height}px`;
397
+ }
398
+ break;
399
+ }
400
+ }
401
+ // ─── Internal ────────────────────────────────────────────────────────────
402
+ emit(event, payload) {
403
+ const list = this.handlers.get(event);
404
+ if (list)
405
+ [...list].forEach(fn => fn(payload));
406
+ }
407
+ post(data) {
408
+ const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
409
+ if (!this._ready) {
410
+ this.pendingMessages.push(msg);
411
+ }
412
+ else {
413
+ this.send(msg);
414
+ }
415
+ }
416
+ send(msg) {
417
+ var _a;
418
+ (_a = this._frameWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * errors.ts — error types and normalisation for OzElements.
424
+ *
425
+ * Two normalisation functions:
426
+ * - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages
427
+ * - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
428
+ *
429
+ * Error keys in normalizeCardSaleError are taken directly from checkout's
430
+ * errorMapping.ts so the same error strings produce the same user-facing copy.
431
+ */
432
+ class OzError extends Error {
433
+ constructor(message, raw, errorCode) {
434
+ super(message);
435
+ this.name = 'OzError';
436
+ this.raw = raw !== null && raw !== void 0 ? raw : message;
437
+ this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
438
+ this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
439
+ }
440
+ }
441
+ /** Shared patterns that apply to both card and bank vault errors. */
442
+ function normalizeCommonVaultError(msg) {
443
+ if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
444
+ return 'Authentication failed. Check your vault API key configuration.';
445
+ }
446
+ if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('fetch')) {
447
+ return 'A network error occurred. Please check your connection and try again.';
448
+ }
449
+ if (msg.includes('timeout') || msg.includes('timed out')) {
450
+ return 'The request timed out. Please try again.';
451
+ }
452
+ if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
453
+ return 'A server error occurred. Please try again shortly.';
454
+ }
455
+ return null;
456
+ }
457
+ /**
458
+ * Maps a raw vault /tokenize error string to a user-facing message for card flows.
459
+ * Falls back to the original string if no pattern matches.
460
+ */
461
+ function normalizeVaultError(raw) {
462
+ const msg = raw.toLowerCase();
463
+ const common = normalizeCommonVaultError(msg);
464
+ if (common)
465
+ return common;
466
+ if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
467
+ return 'The card number is invalid. Please check and try again.';
468
+ }
469
+ if (msg.includes('expir')) {
470
+ return 'The card expiration date is invalid or the card has expired.';
471
+ }
472
+ if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
473
+ return 'The CVV code is invalid. Please check and try again.';
474
+ }
475
+ if (msg.includes('insufficient') || msg.includes('funds')) {
476
+ return 'Your card has insufficient funds. Please use a different card.';
477
+ }
478
+ if (msg.includes('declined') || msg.includes('do not honor')) {
479
+ return 'Your card was declined. Please try a different card or contact your bank.';
480
+ }
481
+ return raw;
482
+ }
483
+ /**
484
+ * Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
485
+ * Uses bank-specific pattern matching so card-specific messages are never shown for
486
+ * bank errors. Falls back to the original string if no pattern matches.
487
+ */
488
+ function normalizeBankVaultError(raw) {
489
+ const msg = raw.toLowerCase();
490
+ const common = normalizeCommonVaultError(msg);
491
+ if (common)
492
+ return common;
493
+ if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
494
+ return 'The bank account number is invalid. Please check and try again.';
495
+ }
496
+ if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || msg.includes('aba')) {
497
+ return 'The routing number is invalid. Please check and try again.';
498
+ }
499
+ return raw;
500
+ }
501
+ // ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
502
+ const CARD_SALE_ERROR_MAP = [
503
+ ['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
504
+ ['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
505
+ ['Card expired', 'Your card has expired. Please use a different card.'],
506
+ ['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
507
+ ['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
508
+ ['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
509
+ ['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
510
+ ['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
511
+ ['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
512
+ ['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
513
+ ['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
514
+ ['API key not found', 'Authentication failed. Please refresh the page.'],
515
+ ['Access token expired', 'Your session has expired. Please refresh the page.'],
516
+ ['Access token is invalid', 'Authentication failed. Please refresh the page.'],
517
+ ['Unauthorized', 'Authentication failed. Please refresh the page.'],
518
+ ['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
519
+ ['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
520
+ ['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
521
+ ['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
522
+ ['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
523
+ ['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
524
+ ['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
525
+ ];
526
+ /**
527
+ * Maps a raw Ozura Pay API cardSale error string to a user-facing message.
528
+ *
529
+ * Uses the exact same error key table as checkout's `getUserFriendlyError()` in
530
+ * errorMapping.ts so both surfaces produce identical copy for the same errors.
531
+ *
532
+ * Falls back to the original string when it's under 100 characters, or to a
533
+ * generic message for long/opaque server errors — matching checkout's fallback
534
+ * behaviour exactly.
535
+ */
536
+ function normalizeCardSaleError(raw) {
537
+ if (!raw)
538
+ return 'Payment processing failed. Please try again.';
539
+ for (const [key, message] of CARD_SALE_ERROR_MAP) {
540
+ if (raw.toLowerCase().includes(key.toLowerCase())) {
541
+ return message;
542
+ }
543
+ }
544
+ // Checkout fallback: pass through short errors, genericise long ones
545
+ if (raw.length < 100)
546
+ return raw;
547
+ return 'Payment processing failed. Please try again or contact support.';
548
+ }
549
+
550
+ /**
551
+ * billingUtils.ts — billing detail validation and normalization.
552
+ *
553
+ * Mirrors the validation in checkout/page.tsx (pre-flight checks before cardSale)
554
+ * so that billing data passed to createToken() is guaranteed schema-compliant and
555
+ * ready to forward directly to the Ozura Pay API cardSale endpoint.
556
+ *
557
+ * All string fields enforced to 1–50 characters (cardSale schema constraint).
558
+ * State is normalized to 2-letter abbreviation for US and CA.
559
+ * Phone must be E.164 format (matches checkout's formatPhoneForAPI output).
560
+ */
561
+ // ─── Email ────────────────────────────────────────────────────────────────────
562
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
563
+ /** Returns true when the email is syntactically valid and ≤50 characters. */
564
+ function validateEmail(email) {
565
+ return EMAIL_RE.test(email) && email.length <= 50;
566
+ }
567
+ // ─── Phone ───────────────────────────────────────────────────────────────────
568
+ /**
569
+ * Validates E.164 phone format: starts with +, 1–3 digit country code,
570
+ * followed by 7–12 digits, total ≤50 characters.
571
+ *
572
+ * Matches the output of checkout's formatPhoneForAPI() function.
573
+ * Examples: "+15551234567", "+447911123456", "+61412345678"
574
+ */
575
+ function validateE164Phone(phone) {
576
+ return /^\+[1-9]\d{6,49}$/.test(phone) && phone.length <= 50;
577
+ }
578
+ // ─── Field length ─────────────────────────────────────────────────────────────
579
+ /** Returns true when the string is non-empty and ≤50 characters (cardSale schema). */
580
+ function isValidBillingField(value) {
581
+ return value.length > 0 && value.length <= 50;
582
+ }
583
+ // ─── US state normalization ───────────────────────────────────────────────────
584
+ // Mirrors checkout's convertStateToAbbreviation() so the same input variants work.
585
+ const US_STATES = {
586
+ alabama: 'AL', alaska: 'AK', arizona: 'AZ', arkansas: 'AR',
587
+ california: 'CA', colorado: 'CO', connecticut: 'CT', delaware: 'DE',
588
+ 'district of columbia': 'DC', florida: 'FL', georgia: 'GA', hawaii: 'HI',
589
+ idaho: 'ID', illinois: 'IL', indiana: 'IN', iowa: 'IA', kansas: 'KS',
590
+ kentucky: 'KY', louisiana: 'LA', maine: 'ME', maryland: 'MD',
591
+ massachusetts: 'MA', michigan: 'MI', minnesota: 'MN', mississippi: 'MS',
592
+ missouri: 'MO', montana: 'MT', nebraska: 'NE', nevada: 'NV',
593
+ 'new hampshire': 'NH', 'new jersey': 'NJ', 'new mexico': 'NM', 'new york': 'NY',
594
+ 'north carolina': 'NC', 'north dakota': 'ND', ohio: 'OH', oklahoma: 'OK',
595
+ oregon: 'OR', pennsylvania: 'PA', 'rhode island': 'RI', 'south carolina': 'SC',
596
+ 'south dakota': 'SD', tennessee: 'TN', texas: 'TX', utah: 'UT',
597
+ vermont: 'VT', virginia: 'VA', washington: 'WA', 'west virginia': 'WV',
598
+ wisconsin: 'WI', wyoming: 'WY',
599
+ };
600
+ const US_ABBREVS = new Set(Object.values(US_STATES));
601
+ const CA_PROVINCES = {
602
+ alberta: 'AB', 'british columbia': 'BC', manitoba: 'MB', 'new brunswick': 'NB',
603
+ 'newfoundland and labrador': 'NL', 'nova scotia': 'NS', ontario: 'ON',
604
+ 'prince edward island': 'PE', quebec: 'QC', saskatchewan: 'SK',
605
+ 'northwest territories': 'NT', nunavut: 'NU', yukon: 'YT',
606
+ };
607
+ const CA_ABBREVS = new Set(Object.values(CA_PROVINCES));
608
+ /**
609
+ * Converts a full US state or Canadian province name to its 2-letter abbreviation.
610
+ * If already a valid abbreviation (case-insensitive), returns it uppercased.
611
+ * For non-US/CA countries, returns the input uppercased unchanged.
612
+ *
613
+ * Matches checkout's convertStateToAbbreviation() behaviour exactly.
614
+ */
615
+ function normalizeState(state, country) {
616
+ var _a, _b;
617
+ const upper = state.trim().toUpperCase();
618
+ const lower = state.trim().toLowerCase();
619
+ if (country === 'US') {
620
+ if (US_ABBREVS.has(upper))
621
+ return upper;
622
+ return (_a = US_STATES[lower]) !== null && _a !== void 0 ? _a : upper;
623
+ }
624
+ if (country === 'CA') {
625
+ if (CA_ABBREVS.has(upper))
626
+ return upper;
627
+ return (_b = CA_PROVINCES[lower]) !== null && _b !== void 0 ? _b : upper;
628
+ }
629
+ return upper;
630
+ }
631
+ // ─── Postal code validation ───────────────────────────────────────────────────
632
+ const POSTAL_PATTERNS = {
633
+ US: /^\d{5}(-?\d{4})?$/, // 5-digit or ZIP+4 (with or without hyphen)
634
+ CA: /^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/, // A1A 1A1
635
+ GB: /^[A-Za-z]{1,2}\d[A-Za-z\d]?\s?\d[A-Za-z]{2}$/,
636
+ DE: /^\d{5}$/,
637
+ FR: /^\d{5}$/,
638
+ ES: /^\d{5}$/,
639
+ IT: /^\d{5}$/,
640
+ AU: /^\d{4}$/,
641
+ NL: /^\d{4}\s?[A-Za-z]{2}$/,
642
+ BR: /^\d{5}-?\d{3}$/,
643
+ JP: /^\d{3}-?\d{4}$/,
644
+ IN: /^\d{6}$/,
645
+ };
646
+ /**
647
+ * Validates a postal/ZIP code against country-specific format rules.
648
+ * For countries without a specific pattern, falls back to generic 1–50 char check.
649
+ */
650
+ function validatePostalCode(zip, country) {
651
+ if (!zip || zip.length === 0)
652
+ return { valid: false, error: 'Postal code is required' };
653
+ if (zip.length > 50)
654
+ return { valid: false, error: 'Postal code must be 50 characters or fewer' };
655
+ const pattern = POSTAL_PATTERNS[country.toUpperCase()];
656
+ if (pattern && !pattern.test(zip)) {
657
+ return { valid: false, error: `Invalid postal code format for ${country.toUpperCase()}` };
658
+ }
659
+ return { valid: true };
660
+ }
661
+ /**
662
+ * Validates and normalizes billing details against the Ozura cardSale API schema.
663
+ *
664
+ * Rules applied (same as checkout's pre-flight validation in page.tsx):
665
+ * - firstName, lastName: required, 1–50 chars
666
+ * - email: optional; if provided, must be valid format and ≤50 chars
667
+ * - phone: optional; if provided, must be E.164 and ≤50 chars
668
+ * - address fields: if address is provided, line1/city/state/zip/country are
669
+ * required (1–50 chars each); line2 is optional and omitted from normalized
670
+ * output if blank (cardSale schema: minLength 1 if present)
671
+ * - state: normalized to 2-letter abbreviation for US and CA
672
+ */
673
+ function validateBilling(billing) {
674
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
675
+ const errors = [];
676
+ const firstName = (_b = (_a = billing.firstName) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
677
+ const lastName = (_d = (_c = billing.lastName) === null || _c === void 0 ? void 0 : _c.trim()) !== null && _d !== void 0 ? _d : '';
678
+ const email = (_f = (_e = billing.email) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : '';
679
+ const phone = (_h = (_g = billing.phone) === null || _g === void 0 ? void 0 : _g.trim()) !== null && _h !== void 0 ? _h : '';
680
+ if (!isValidBillingField(firstName)) {
681
+ errors.push('billing.firstName must be 1–50 characters');
682
+ }
683
+ if (!isValidBillingField(lastName)) {
684
+ errors.push('billing.lastName must be 1–50 characters');
685
+ }
686
+ if (email && !validateEmail(email)) {
687
+ errors.push('billing.email must be a valid address (max 50 characters)');
688
+ }
689
+ if (phone && !validateE164Phone(phone)) {
690
+ errors.push('billing.phone must be E.164 format, e.g. "+15551234567" (max 50 characters)');
691
+ }
692
+ let normalizedAddress;
693
+ if (billing.address) {
694
+ const a = billing.address;
695
+ const country = (_k = (_j = a.country) === null || _j === void 0 ? void 0 : _j.trim().toUpperCase()) !== null && _k !== void 0 ? _k : '';
696
+ const line1 = (_m = (_l = a.line1) === null || _l === void 0 ? void 0 : _l.trim()) !== null && _m !== void 0 ? _m : '';
697
+ const line2 = (_p = (_o = a.line2) === null || _o === void 0 ? void 0 : _o.trim()) !== null && _p !== void 0 ? _p : '';
698
+ const city = (_r = (_q = a.city) === null || _q === void 0 ? void 0 : _q.trim()) !== null && _r !== void 0 ? _r : '';
699
+ const zip = (_t = (_s = a.zip) === null || _s === void 0 ? void 0 : _s.trim()) !== null && _t !== void 0 ? _t : '';
700
+ const state = normalizeState((_v = (_u = a.state) === null || _u === void 0 ? void 0 : _u.trim()) !== null && _v !== void 0 ? _v : '', country);
701
+ if (!isValidBillingField(line1))
702
+ errors.push('billing.address.line1 must be 1–50 characters');
703
+ if (line2 && !isValidBillingField(line2))
704
+ errors.push('billing.address.line2 must be 1–50 characters if provided');
705
+ if (!isValidBillingField(city))
706
+ errors.push('billing.address.city must be 1–50 characters');
707
+ if (!isValidBillingField(state))
708
+ errors.push('billing.address.state must be 1–50 characters');
709
+ // cardSale backend uses strict enum validation on country — must be exactly 2 uppercase letters
710
+ if (!/^[A-Z]{2}$/.test(country)) {
711
+ errors.push('billing.address.country must be a 2-letter ISO 3166-1 alpha-2 code (e.g. "US", "CA", "GB")');
712
+ }
713
+ if (!isValidBillingField(zip)) {
714
+ errors.push('billing.address.zip must be 1–50 characters');
715
+ }
716
+ else if (/^[A-Z]{2}$/.test(country)) {
717
+ const postalResult = validatePostalCode(zip, country);
718
+ if (!postalResult.valid) {
719
+ errors.push(`billing.address.zip: ${postalResult.error}`);
720
+ }
721
+ }
722
+ normalizedAddress = Object.assign(Object.assign({ line1 }, (line2 ? { line2 } : {})), { city,
723
+ state,
724
+ zip,
725
+ country });
726
+ }
727
+ const normalized = Object.assign(Object.assign(Object.assign({ firstName,
728
+ lastName }, (email ? { email } : {})), (phone ? { phone } : {})), (normalizedAddress ? { address: normalizedAddress } : {}));
729
+ return { valid: errors.length === 0, errors, normalized };
730
+ }
731
+
732
+ const DEFAULT_API_URL = "https://pci-vault-staging-drc0duhcakf4g4fr.eastus-01.azurewebsites.net";
733
+ const DEFAULT_FRAME_BASE_URL = "https://staging.elements.ozura.com";
734
+ /**
735
+ * The main entry point for OzElements. Creates and manages iframe-based
736
+ * card input elements that keep raw card data isolated from the merchant page.
737
+ *
738
+ * @example
739
+ * const vault = new OzVault('your_vault_api_key');
740
+ * const cardNum = vault.createElement('cardNumber');
741
+ * cardNum.mount('#card-number');
742
+ * const { token, cvcSession } = await vault.createToken({
743
+ * billing: { firstName: 'Jane', lastName: 'Doe' },
744
+ * });
745
+ */
746
+ class OzVault {
747
+ constructor(apiKey, options) {
748
+ var _a, _b;
749
+ this.elements = new Map();
750
+ this.elementsByType = new Map();
751
+ this.bankElementsByType = new Map();
752
+ this.tokenizeResolvers = new Map();
753
+ this.bankTokenizeResolvers = new Map();
754
+ // Track completion state per element for auto-advance (only fire on transition)
755
+ this.completionState = new Map();
756
+ this.tokenizerFrame = null;
757
+ this.tokenizerWindow = null;
758
+ this.tokenizerReady = false;
759
+ this._tokenizing = null;
760
+ this._destroyed = false;
761
+ this._pendingMount = null;
762
+ this.loadErrorTimeoutId = null;
763
+ if (!apiKey || !apiKey.trim()) {
764
+ throw new OzError('A non-empty vault API key is required. Pass your key as the first argument to new OzVault().');
765
+ }
766
+ this.apiKey = apiKey;
767
+ this.pubKey = options.pubKey;
768
+ this.apiUrl = options.apiUrl || DEFAULT_API_URL;
769
+ this.frameBaseUrl = options.frameBaseUrl || DEFAULT_FRAME_BASE_URL;
770
+ this.frameOrigin = new URL(this.frameBaseUrl).origin;
771
+ this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
772
+ this.resolvedAppearance = resolveAppearance(options.appearance);
773
+ this.vaultId = `vault-${Math.random().toString(36).slice(2, 12)}`;
774
+ this.tokenizerName = `__oz_tok_${this.vaultId}`;
775
+ this.boundHandleMessage = this.handleMessage.bind(this);
776
+ window.addEventListener('message', this.boundHandleMessage);
777
+ this.mountTokenizerFrame();
778
+ if (options.onLoadError) {
779
+ const timeout = (_b = options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
780
+ this.loadErrorTimeoutId = setTimeout(() => {
781
+ this.loadErrorTimeoutId = null;
782
+ if (!this._destroyed && !this.tokenizerReady) {
783
+ options.onLoadError();
784
+ }
785
+ }, timeout);
786
+ }
787
+ }
788
+ /**
789
+ * True once the hidden tokenizer iframe has loaded and signalled ready.
790
+ * Use this to gate the pay button when building custom UIs without React.
791
+ * React consumers should use the `ready` value returned by `useOzElements()`.
792
+ */
793
+ get isReady() {
794
+ return this.tokenizerReady;
795
+ }
796
+ /**
797
+ * Creates a new OzElement of the given type. Call `.mount(selector)` on the
798
+ * returned element to attach it to the DOM.
799
+ */
800
+ createElement(type, options = {}) {
801
+ if (this._destroyed) {
802
+ throw new OzError('Cannot create elements on a destroyed vault. Create a new OzVault instance.');
803
+ }
804
+ const existing = this.elementsByType.get(type);
805
+ if (existing) {
806
+ this.elements.delete(existing.frameId);
807
+ this.completionState.delete(existing.frameId);
808
+ existing.destroy();
809
+ }
810
+ const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance);
811
+ this.elements.set(el.frameId, el);
812
+ this.elementsByType.set(type, el);
813
+ return el;
814
+ }
815
+ /** Returns the existing element of the given type, or null if none has been created. */
816
+ getElement(type) {
817
+ var _a;
818
+ return (_a = this.elementsByType.get(type)) !== null && _a !== void 0 ? _a : null;
819
+ }
820
+ /**
821
+ * Creates a bank account input element (accountNumber or routingNumber).
822
+ * Call `.mount(selector)` on the returned element to attach it to the DOM.
823
+ *
824
+ * @example
825
+ * const accountEl = vault.createBankElement('accountNumber');
826
+ * const routingEl = vault.createBankElement('routingNumber');
827
+ * accountEl.mount('#account-number');
828
+ * routingEl.mount('#routing-number');
829
+ */
830
+ createBankElement(type, options = {}) {
831
+ if (this._destroyed) {
832
+ throw new OzError('Cannot create elements on a destroyed vault. Create a new OzVault instance.');
833
+ }
834
+ const existing = this.bankElementsByType.get(type);
835
+ if (existing) {
836
+ this.elements.delete(existing.frameId);
837
+ this.completionState.delete(existing.frameId);
838
+ existing.destroy();
839
+ }
840
+ const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance);
841
+ this.elements.set(el.frameId, el);
842
+ this.bankElementsByType.set(type, el);
843
+ return el;
844
+ }
845
+ /** Returns the existing bank element of the given type, or null if none has been created. */
846
+ getBankElement(type) {
847
+ var _a;
848
+ return (_a = this.bankElementsByType.get(type)) !== null && _a !== void 0 ? _a : null;
849
+ }
850
+ /**
851
+ * Tokenizes mounted bank account elements. Raw account and routing numbers
852
+ * never leave the Ozura-origin iframes — the tokenizer iframe POSTs directly
853
+ * to the vault API.
854
+ *
855
+ * Returns a token that can be used with any ACH-capable payment processor.
856
+ *
857
+ * **Note:** OzuraPay does not currently support bank account payments.
858
+ * Use this token with your own ACH processor backend.
859
+ *
860
+ * @example
861
+ * const { token, bank } = await vault.createBankToken({
862
+ * firstName: 'Jane',
863
+ * lastName: 'Smith',
864
+ * });
865
+ */
866
+ async createBankToken(options) {
867
+ var _a, _b;
868
+ if (this._destroyed) {
869
+ throw new OzError('Cannot tokenize on a destroyed vault. Create a new OzVault instance.');
870
+ }
871
+ if (!this.tokenizerReady) {
872
+ throw new OzError('Vault not ready. Ensure the page is fully loaded before calling createBankToken.');
873
+ }
874
+ if (this._tokenizing) {
875
+ throw new OzError(this._tokenizing === 'card'
876
+ ? 'A card tokenization is already in progress. Wait for it to complete before calling createBankToken().'
877
+ : 'A bank tokenization is already in progress. Wait for it to complete before calling createBankToken() again.');
878
+ }
879
+ if (!((_a = options.firstName) === null || _a === void 0 ? void 0 : _a.trim())) {
880
+ throw new OzError('firstName is required for bank account tokenization.');
881
+ }
882
+ if (!((_b = options.lastName) === null || _b === void 0 ? void 0 : _b.trim())) {
883
+ throw new OzError('lastName is required for bank account tokenization.');
884
+ }
885
+ const accountEl = this.bankElementsByType.get('accountNumber');
886
+ const routingEl = this.bankElementsByType.get('routingNumber');
887
+ const accountReady = !!accountEl && this.elements.has(accountEl.frameId) && accountEl.isReady;
888
+ const routingReady = !!routingEl && this.elements.has(routingEl.frameId) && routingEl.isReady;
889
+ if (!accountReady && !routingReady) {
890
+ throw new OzError('No bank elements are mounted and ready. Mount accountNumber and routingNumber elements before calling createBankToken.');
891
+ }
892
+ if (!accountReady) {
893
+ throw new OzError('accountNumber element is not mounted or not ready. Mount both accountNumber and routingNumber elements before calling createBankToken.');
894
+ }
895
+ if (!routingReady) {
896
+ throw new OzError('routingNumber element is not mounted or not ready. Mount both accountNumber and routingNumber elements before calling createBankToken.');
897
+ }
898
+ const readyBankElements = [accountEl, routingEl];
899
+ this._tokenizing = 'bank';
900
+ const requestId = `req-${Math.random().toString(36).slice(2, 10)}`;
901
+ return new Promise((resolve, reject) => {
902
+ const cleanup = () => { this._tokenizing = null; };
903
+ this.bankTokenizeResolvers.set(requestId, {
904
+ resolve: (v) => { cleanup(); resolve(v); },
905
+ reject: (e) => { cleanup(); reject(e); },
906
+ });
907
+ this.sendToTokenizer({
908
+ type: 'OZ_BANK_TOKENIZE',
909
+ requestId,
910
+ apiUrl: this.apiUrl,
911
+ apiKey: this.apiKey,
912
+ pubKey: this.pubKey,
913
+ firstName: options.firstName.trim(),
914
+ lastName: options.lastName.trim(),
915
+ fieldCount: readyBankElements.length,
916
+ });
917
+ readyBankElements.forEach(el => el.beginCollect(requestId));
918
+ setTimeout(() => {
919
+ if (this.bankTokenizeResolvers.has(requestId)) {
920
+ this.bankTokenizeResolvers.delete(requestId);
921
+ cleanup();
922
+ reject(new OzError('Bank tokenization timed out after 30 seconds', undefined, 'timeout'));
923
+ }
924
+ }, 30000);
925
+ });
926
+ }
927
+ /**
928
+ * Tokenizes all mounted elements. Raw card data never leaves the Ozura-origin
929
+ * iframes — the tokenizer iframe POSTs directly to the vault API.
930
+ *
931
+ * Returns a token and cvcSession that can be passed to the Ozura Pay API.
932
+ */
933
+ async createToken(options = {}) {
934
+ var _a, _b;
935
+ if (this._destroyed) {
936
+ throw new OzError('Cannot tokenize on a destroyed vault. Create a new OzVault instance.');
937
+ }
938
+ if (!this.tokenizerReady) {
939
+ throw new OzError('Vault not ready. Ensure the page is fully loaded before calling createToken.');
940
+ }
941
+ if (this._tokenizing) {
942
+ throw new OzError(this._tokenizing === 'bank'
943
+ ? 'A bank tokenization is already in progress. Wait for it to complete before calling createToken().'
944
+ : 'A card tokenization is already in progress. Wait for it to complete before calling createToken() again.');
945
+ }
946
+ this._tokenizing = 'card';
947
+ const requestId = `req-${Math.random().toString(36).slice(2, 10)}`;
948
+ // Only include card elements whose iframes have actually loaded — an element
949
+ // created but never mounted will never send OZ_FIELD_VALUE, which would
950
+ // cause the tokenizer to hang waiting for a value that never arrives.
951
+ // Explicitly exclude bank elements (accountNumber/routingNumber) that share
952
+ // the same this.elements map; including them would inflate fieldCount and
953
+ // allow the accountNumber iframe's last4 to overwrite cardMeta.last4.
954
+ const cardElements = new Set(this.elementsByType.values());
955
+ const readyElements = [...this.elements.values()].filter(el => el.isReady && cardElements.has(el));
956
+ if (readyElements.length === 0) {
957
+ this._tokenizing = null;
958
+ throw new OzError('No elements are mounted and ready. Mount at least one element before calling createToken.');
959
+ }
960
+ // Validate billing details if provided and extract firstName/lastName for the vault payload.
961
+ // billing.firstName/lastName take precedence over the deprecated top-level params.
962
+ let normalizedBilling;
963
+ let firstName = (_a = options.firstName) !== null && _a !== void 0 ? _a : '';
964
+ let lastName = (_b = options.lastName) !== null && _b !== void 0 ? _b : '';
965
+ if (options.billing) {
966
+ const result = validateBilling(options.billing);
967
+ if (!result.valid) {
968
+ this._tokenizing = null;
969
+ throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
970
+ }
971
+ normalizedBilling = result.normalized;
972
+ firstName = normalizedBilling.firstName;
973
+ lastName = normalizedBilling.lastName;
974
+ }
975
+ else {
976
+ if (firstName.length > 50) {
977
+ this._tokenizing = null;
978
+ throw new OzError('firstName must be 50 characters or fewer');
979
+ }
980
+ if (lastName.length > 50) {
981
+ this._tokenizing = null;
982
+ throw new OzError('lastName must be 50 characters or fewer');
983
+ }
984
+ }
985
+ return new Promise((resolve, reject) => {
986
+ const cleanup = () => { this._tokenizing = null; };
987
+ this.tokenizeResolvers.set(requestId, {
988
+ resolve: (v) => { cleanup(); resolve(v); },
989
+ reject: (e) => { cleanup(); reject(e); },
990
+ billing: normalizedBilling,
991
+ });
992
+ // Tell tokenizer frame to expect N field values, then tokenize
993
+ this.sendToTokenizer({
994
+ type: 'OZ_TOKENIZE',
995
+ requestId,
996
+ apiUrl: this.apiUrl,
997
+ apiKey: this.apiKey,
998
+ pubKey: this.pubKey,
999
+ firstName,
1000
+ lastName,
1001
+ fieldCount: readyElements.length,
1002
+ });
1003
+ // Tell each ready element frame to send its raw value to the tokenizer
1004
+ readyElements.forEach(el => el.beginCollect(requestId));
1005
+ setTimeout(() => {
1006
+ if (this.tokenizeResolvers.has(requestId)) {
1007
+ this.tokenizeResolvers.delete(requestId);
1008
+ cleanup();
1009
+ reject(new OzError('Tokenization timed out after 30 seconds', undefined, 'timeout'));
1010
+ }
1011
+ }, 30000);
1012
+ });
1013
+ }
1014
+ /**
1015
+ * Tears down the vault: removes all element iframes, the tokenizer iframe,
1016
+ * and the global message listener. Call this when the checkout component
1017
+ * unmounts (e.g. in React's useEffect cleanup or a SPA route change).
1018
+ */
1019
+ destroy() {
1020
+ var _a;
1021
+ if (this._destroyed)
1022
+ return;
1023
+ this._destroyed = true;
1024
+ window.removeEventListener('message', this.boundHandleMessage);
1025
+ if (this._pendingMount) {
1026
+ document.removeEventListener('DOMContentLoaded', this._pendingMount);
1027
+ this._pendingMount = null;
1028
+ }
1029
+ if (this.loadErrorTimeoutId != null) {
1030
+ clearTimeout(this.loadErrorTimeoutId);
1031
+ this.loadErrorTimeoutId = null;
1032
+ }
1033
+ // Reject any pending tokenize promises so callers aren't left hanging
1034
+ this._tokenizing = null;
1035
+ this.tokenizeResolvers.forEach(({ reject }) => {
1036
+ reject(new OzError('Vault destroyed before tokenization completed.'));
1037
+ });
1038
+ this.tokenizeResolvers.clear();
1039
+ this.bankTokenizeResolvers.forEach(({ reject }) => {
1040
+ reject(new OzError('Vault destroyed before bank tokenization completed.'));
1041
+ });
1042
+ this.bankTokenizeResolvers.clear();
1043
+ this.elements.forEach(el => el.destroy());
1044
+ this.elements.clear();
1045
+ this.elementsByType.clear();
1046
+ this.bankElementsByType.clear();
1047
+ this.completionState.clear();
1048
+ (_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.remove();
1049
+ this.tokenizerFrame = null;
1050
+ this.tokenizerWindow = null;
1051
+ this.tokenizerReady = false;
1052
+ }
1053
+ // ─── Private ─────────────────────────────────────────────────────────────
1054
+ mountTokenizerFrame() {
1055
+ const mount = () => {
1056
+ this._pendingMount = null;
1057
+ const iframe = document.createElement('iframe');
1058
+ iframe.name = this.tokenizerName;
1059
+ iframe.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;';
1060
+ iframe.setAttribute('aria-hidden', 'true');
1061
+ iframe.tabIndex = -1;
1062
+ const parentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
1063
+ iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1064
+ document.body.appendChild(iframe);
1065
+ this.tokenizerFrame = iframe;
1066
+ };
1067
+ if (document.readyState === 'loading') {
1068
+ this._pendingMount = mount;
1069
+ document.addEventListener('DOMContentLoaded', mount);
1070
+ }
1071
+ else {
1072
+ mount();
1073
+ }
1074
+ }
1075
+ handleMessage(event) {
1076
+ var _a;
1077
+ // Only accept messages from our frame origin (defense in depth; prevents
1078
+ // arbitrary pages from injecting OZ_TOKEN_RESULT etc. with a guessed vaultId).
1079
+ if (event.origin !== this.frameOrigin)
1080
+ return;
1081
+ const msg = event.data;
1082
+ if (!msg || msg.__oz !== true || msg.vaultId !== this.vaultId)
1083
+ return;
1084
+ // Route tokenizer messages
1085
+ if (event.source === ((_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.contentWindow)) {
1086
+ this.handleTokenizerMessage(msg);
1087
+ return;
1088
+ }
1089
+ // Route to the matching element
1090
+ const frameId = msg.frameId;
1091
+ if (frameId) {
1092
+ const el = this.elements.get(frameId);
1093
+ if (el) {
1094
+ // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1095
+ if (msg.type === 'OZ_CHANGE') {
1096
+ this.handleElementChange(msg, el);
1097
+ }
1098
+ el.handleMessage(msg);
1099
+ if (msg.type === 'OZ_FRAME_READY' && this.tokenizerReady) {
1100
+ el.setTokenizerName(this.tokenizerName);
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ /**
1106
+ * Handles side-effects that the SDK manages internally when a field changes:
1107
+ * - CVV length sync when card brand changes
1108
+ * - Auto-advance focus when a field completes
1109
+ */
1110
+ handleElementChange(msg, el) {
1111
+ var _a, _b, _c;
1112
+ const complete = msg.complete;
1113
+ const valid = msg.valid;
1114
+ const wasComplete = (_a = this.completionState.get(el.frameId)) !== null && _a !== void 0 ? _a : false;
1115
+ this.completionState.set(el.frameId, complete);
1116
+ // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1117
+ // where isComplete() fires before the user has finished typing.
1118
+ const justCompleted = complete && valid && !wasComplete;
1119
+ // Sync CVV length when card brand changes
1120
+ if (el.type === 'cardNumber') {
1121
+ const brand = msg.cardBrand;
1122
+ const cvvEl = this.elementsByType.get('cvv');
1123
+ if (cvvEl && brand) {
1124
+ cvvEl.setCvvLength(brand === 'amex' ? 4 : 3);
1125
+ }
1126
+ }
1127
+ // Auto-advance focus on completion
1128
+ if (justCompleted) {
1129
+ if (el.type === 'cardNumber') {
1130
+ (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1131
+ }
1132
+ else if (el.type === 'expirationDate') {
1133
+ (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1134
+ }
1135
+ }
1136
+ }
1137
+ handleTokenizerMessage(msg) {
1138
+ var _a, _b;
1139
+ switch (msg.type) {
1140
+ case 'OZ_FRAME_READY':
1141
+ this.tokenizerReady = true;
1142
+ if (this.loadErrorTimeoutId != null) {
1143
+ clearTimeout(this.loadErrorTimeoutId);
1144
+ this.loadErrorTimeoutId = null;
1145
+ }
1146
+ this.tokenizerWindow = (_b = (_a = this.tokenizerFrame) === null || _a === void 0 ? void 0 : _a.contentWindow) !== null && _b !== void 0 ? _b : null;
1147
+ this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__' });
1148
+ this.elements.forEach(el => el.setTokenizerName(this.tokenizerName));
1149
+ break;
1150
+ case 'OZ_TOKEN_RESULT': {
1151
+ const pending = this.tokenizeResolvers.get(msg.requestId);
1152
+ if (pending) {
1153
+ this.tokenizeResolvers.delete(msg.requestId);
1154
+ const card = msg.card;
1155
+ pending.resolve(Object.assign(Object.assign({ token: msg.token, cvcSession: msg.cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1156
+ }
1157
+ break;
1158
+ }
1159
+ case 'OZ_TOKEN_ERROR': {
1160
+ const pending = this.tokenizeResolvers.get(msg.requestId);
1161
+ if (pending) {
1162
+ this.tokenizeResolvers.delete(msg.requestId);
1163
+ const raw = msg.error;
1164
+ const errorCode = (msg.errorCode || 'unknown');
1165
+ pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
1166
+ }
1167
+ // Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR
1168
+ const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
1169
+ if (bankPending) {
1170
+ this.bankTokenizeResolvers.delete(msg.requestId);
1171
+ const raw = msg.error;
1172
+ const errorCode = (msg.errorCode || 'unknown');
1173
+ bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
1174
+ }
1175
+ break;
1176
+ }
1177
+ case 'OZ_BANK_TOKEN_RESULT': {
1178
+ const pending = this.bankTokenizeResolvers.get(msg.requestId);
1179
+ if (pending) {
1180
+ this.bankTokenizeResolvers.delete(msg.requestId);
1181
+ const bank = msg.bank;
1182
+ pending.resolve(Object.assign({ token: msg.token }, (bank ? { bank } : {})));
1183
+ }
1184
+ break;
1185
+ }
1186
+ }
1187
+ }
1188
+ sendToTokenizer(data) {
1189
+ var _a;
1190
+ const msg = Object.assign({ __oz: true, vaultId: this.vaultId }, data);
1191
+ (_a = this.tokenizerWindow) === null || _a === void 0 ? void 0 : _a.postMessage(msg, this.frameOrigin);
1192
+ }
1193
+ }
1194
+
1195
+ exports.OzElement = OzElement;
1196
+ exports.OzError = OzError;
1197
+ exports.OzVault = OzVault;
1198
+ exports.normalizeCardSaleError = normalizeCardSaleError;
1199
+ exports.normalizeVaultError = normalizeVaultError;
1200
+
1201
+ }));
1202
+ //# sourceMappingURL=oz-elements.umd.js.map