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