@ozura/elements 1.0.2 → 1.1.0-next.21

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.
@@ -357,7 +357,7 @@ class OzElement {
357
357
  * (useful when integrating with React refs).
358
358
  */
359
359
  mount(target) {
360
- var _a;
360
+ var _a, _b;
361
361
  if (this._destroyed)
362
362
  throw new OzError('Cannot mount a destroyed element.');
363
363
  if (this.iframe)
@@ -374,7 +374,13 @@ class OzElement {
374
374
  iframe.setAttribute('scrolling', 'no');
375
375
  iframe.setAttribute('allowtransparency', 'true');
376
376
  iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
377
- iframe.title = `Secure ${this.elementType} input`;
377
+ iframe.title = `Secure ${(_a = {
378
+ cardNumber: 'card number',
379
+ expirationDate: 'expiration date',
380
+ cvv: 'CVV',
381
+ accountNumber: 'account number',
382
+ routingNumber: 'routing number',
383
+ }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
378
384
  // Note: the `sandbox` attribute is intentionally NOT set. Field values are
379
385
  // delivered to the tokenizer iframe via a MessageChannel port (transferred
380
386
  // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
@@ -388,7 +394,7 @@ class OzElement {
388
394
  container.appendChild(iframe);
389
395
  this.iframe = iframe;
390
396
  this._frameWindow = iframe.contentWindow;
391
- const timeout = (_a = this.options.loadTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
397
+ const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
392
398
  this._loadTimer = setTimeout(() => {
393
399
  if (!this._ready && !this._destroyed) {
394
400
  this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
@@ -821,13 +827,26 @@ function validateBilling(billing) {
821
827
  return { valid: errors.length === 0, errors, normalized };
822
828
  }
823
829
 
830
+ /**
831
+ * Shared postMessage protocol constants.
832
+ *
833
+ * PROTOCOL_VERSION must be incremented any time a breaking change is made to
834
+ * the postMessage message shape (new required fields, renamed types, removed
835
+ * fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
836
+ * messages and warns when the frame and SDK are out of sync.
837
+ *
838
+ * Non-breaking additions (new optional fields, new message types that old
839
+ * frames can safely ignore) do NOT require a version bump.
840
+ */
841
+ const PROTOCOL_VERSION = 1;
842
+
824
843
  function isCardMetadata(v) {
825
844
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
826
845
  }
827
846
  function isBankAccountMetadata(v) {
828
847
  return !!v && typeof v === 'object' && typeof v.last4 === 'string';
829
848
  }
830
- const DEFAULT_FRAME_BASE_URL = "https://elements.ozura.com";
849
+ const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
831
850
  /**
832
851
  * The main entry point for OzElements. Creates and manages iframe-based
833
852
  * card input elements that keep raw card data isolated from the merchant page.
@@ -860,7 +879,7 @@ class OzVault {
860
879
  * @internal
861
880
  */
862
881
  constructor(options, waxKey, tokenizationSessionId) {
863
- var _a, _b, _c;
882
+ var _a, _b, _c, _d;
864
883
  this.elements = new Map();
865
884
  this.elementsByType = new Map();
866
885
  this.bankElementsByType = new Map();
@@ -873,6 +892,9 @@ class OzVault {
873
892
  this.tokenizerReady = false;
874
893
  this._tokenizing = null;
875
894
  this._destroyed = false;
895
+ // Incremented every time reset() cancels an active tokenization so that
896
+ // any in-flight wax-key refresh retry can detect it was superseded.
897
+ this._resetCount = 0;
876
898
  // Tracks successful tokenizations against the per-key call budget so the SDK
877
899
  // can proactively refresh the wax key after it has been consumed rather than
878
900
  // waiting for the next createToken() call to fail.
@@ -892,13 +914,14 @@ class OzVault {
892
914
  this.resolvedAppearance = resolveAppearance(options.appearance);
893
915
  this.vaultId = `vault-${uuid()}`;
894
916
  this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
917
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
895
918
  this.boundHandleMessage = this.handleMessage.bind(this);
896
919
  window.addEventListener('message', this.boundHandleMessage);
897
920
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
898
921
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
899
922
  this.mountTokenizerFrame();
900
923
  if (options.onLoadError) {
901
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
924
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
902
925
  this.loadErrorTimeoutId = setTimeout(() => {
903
926
  this.loadErrorTimeoutId = null;
904
927
  if (!this._destroyed && !this.tokenizerReady) {
@@ -908,6 +931,7 @@ class OzVault {
908
931
  }
909
932
  this._onWaxRefresh = options.onWaxRefresh;
910
933
  this._onReady = options.onReady;
934
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
911
935
  }
912
936
  /**
913
937
  * Creates and returns a ready `OzVault` instance.
@@ -977,6 +1001,7 @@ class OzVault {
977
1001
  if (vault.tokenizerReady) {
978
1002
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
979
1003
  }
1004
+ vault.log('wax key received — vault ready');
980
1005
  return vault;
981
1006
  }
982
1007
  /**
@@ -1118,8 +1143,13 @@ class OzVault {
1118
1143
  const readyBankElements = [accountEl, routingEl];
1119
1144
  this._tokenizing = 'bank';
1120
1145
  const requestId = `req-${uuid()}`;
1146
+ this.log('createBankToken() called');
1121
1147
  return new Promise((resolve, reject) => {
1122
- const cleanup = () => { this._tokenizing = null; };
1148
+ const resetCountAtStart = this._resetCount;
1149
+ const cleanup = () => {
1150
+ if (this._resetCount === resetCountAtStart)
1151
+ this._tokenizing = null;
1152
+ };
1123
1153
  this.bankTokenizeResolvers.set(requestId, {
1124
1154
  resolve: (v) => { cleanup(); resolve(v); },
1125
1155
  reject: (e) => { cleanup(); reject(e); },
@@ -1130,6 +1160,7 @@ class OzVault {
1130
1160
  });
1131
1161
  try {
1132
1162
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1163
+ const bankTokenizeStartMs = Date.now();
1133
1164
  this.sendToTokenizer({
1134
1165
  type: 'OZ_BANK_TOKENIZE',
1135
1166
  requestId,
@@ -1140,6 +1171,7 @@ class OzVault {
1140
1171
  lastName: options.lastName.trim(),
1141
1172
  fieldCount: readyBankElements.length,
1142
1173
  }, bankChannels.map(ch => ch.port1));
1174
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1143
1175
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1144
1176
  const bankTimeoutId = setTimeout(() => {
1145
1177
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1150,8 +1182,10 @@ class OzVault {
1150
1182
  }
1151
1183
  }, 30000);
1152
1184
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1153
- if (bankPendingEntry)
1185
+ if (bankPendingEntry) {
1154
1186
  bankPendingEntry.timeoutId = bankTimeoutId;
1187
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1188
+ }
1155
1189
  }
1156
1190
  catch (err) {
1157
1191
  this.bankTokenizeResolvers.delete(requestId);
@@ -1222,8 +1256,15 @@ class OzVault {
1222
1256
  }
1223
1257
  this._tokenizing = 'card';
1224
1258
  const requestId = `req-${uuid()}`;
1259
+ this.log('createToken() called');
1225
1260
  return new Promise((resolve, reject) => {
1226
- const cleanup = () => { this._tokenizing = null; };
1261
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1262
+ // still belongs to this invocation — not a newer one that started after a reset.
1263
+ const resetCountAtStart = this._resetCount;
1264
+ const cleanup = () => {
1265
+ if (this._resetCount === resetCountAtStart)
1266
+ this._tokenizing = null;
1267
+ };
1227
1268
  this.tokenizeResolvers.set(requestId, {
1228
1269
  resolve: (v) => { cleanup(); resolve(v); },
1229
1270
  reject: (e) => { cleanup(); reject(e); },
@@ -1236,6 +1277,7 @@ class OzVault {
1236
1277
  try {
1237
1278
  // Tell tokenizer frame to expect N field values, then tokenize
1238
1279
  const cardChannels = readyElements.map(() => new MessageChannel());
1280
+ const tokenizeStartMs = Date.now();
1239
1281
  this.sendToTokenizer({
1240
1282
  type: 'OZ_TOKENIZE',
1241
1283
  requestId,
@@ -1246,6 +1288,11 @@ class OzVault {
1246
1288
  lastName,
1247
1289
  fieldCount: readyElements.length,
1248
1290
  }, cardChannels.map(ch => ch.port1));
1291
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1292
+ // Store start time for elapsed-ms logging on result
1293
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1294
+ if (cardEntry)
1295
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1249
1296
  // Tell each ready element frame to send its raw value to the tokenizer
1250
1297
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1251
1298
  const cardTimeoutId = setTimeout(() => {
@@ -1267,6 +1314,63 @@ class OzVault {
1267
1314
  }
1268
1315
  });
1269
1316
  }
1317
+ /**
1318
+ * Clears all mounted element fields without tearing down the vault.
1319
+ *
1320
+ * Call this after a failed payment (e.g. card declined) to let the customer
1321
+ * re-enter their details. The vault instance, tokenizer iframe, wax key, and
1322
+ * tokenization budget counter are all preserved — no network calls are made.
1323
+ *
1324
+ * **Wax key session model:** by design, one wax key covers the full checkout
1325
+ * session. The default `max_tokenize_calls: 3` supports two declined attempts
1326
+ * and one final attempt on the same key. Do not call `vault.destroy()` and
1327
+ * recreate the vault between declines — that unnecessarily re-mints the key
1328
+ * and discards the remaining budget.
1329
+ *
1330
+ * @example
1331
+ * try {
1332
+ * const { token, cvcSession } = await vault.createToken({ billing });
1333
+ * await chargeCard(token, cvcSession);
1334
+ * } catch (err) {
1335
+ * vault.reset(); // clear fields; let customer re-enter
1336
+ * showError(err.message);
1337
+ * }
1338
+ */
1339
+ reset() {
1340
+ if (this._destroyed)
1341
+ return;
1342
+ const cancelling = Boolean(this._tokenizing);
1343
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1344
+ if (this._tokenizing) {
1345
+ this._tokenizing = null;
1346
+ this._resetCount++;
1347
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1348
+ if (timeoutId != null)
1349
+ clearTimeout(timeoutId);
1350
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1351
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1352
+ });
1353
+ this.tokenizeResolvers.clear();
1354
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1355
+ if (timeoutId != null)
1356
+ clearTimeout(timeoutId);
1357
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1358
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1359
+ });
1360
+ this.bankTokenizeResolvers.clear();
1361
+ }
1362
+ // Clear field values in all mounted element iframes
1363
+ this.elementsByType.forEach(el => el.clear());
1364
+ this.bankElementsByType.forEach(el => el.clear());
1365
+ // Reset per-element completion state so auto-advance starts fresh on re-entry
1366
+ for (const frameId of this.completionState.keys()) {
1367
+ this.completionState.set(frameId, false);
1368
+ }
1369
+ // NOTE: _tokenizeSuccessCount is intentionally NOT reset.
1370
+ // It reflects real server-side wax key budget consumption. Zeroing it
1371
+ // would desync the proactive refresh logic from the vault's state and
1372
+ // risk triggering a mid-session re-mint on what should be a clean retry.
1373
+ }
1270
1374
  /**
1271
1375
  * Tears down the vault: removes all element iframes, the tokenizer iframe,
1272
1376
  * and the global message listener. Call this when the checkout component
@@ -1277,6 +1381,7 @@ class OzVault {
1277
1381
  if (this._destroyed)
1278
1382
  return;
1279
1383
  this._destroyed = true;
1384
+ this.log('destroy() called');
1280
1385
  window.removeEventListener('message', this.boundHandleMessage);
1281
1386
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1282
1387
  if (this._pendingMount) {
@@ -1331,13 +1436,17 @@ class OzVault {
1331
1436
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1332
1437
  if (document.hidden) {
1333
1438
  this._hiddenAt = Date.now();
1439
+ this.log('tab hidden');
1334
1440
  }
1335
1441
  else {
1336
- if (this._hiddenAt !== null &&
1337
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1338
- this._storedFetchWaxKey &&
1442
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1443
+ const willRefresh = (this._hiddenAt !== null &&
1444
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1445
+ Boolean(this._storedFetchWaxKey) &&
1339
1446
  !this._tokenizing &&
1340
- !this._waxRefreshing) {
1447
+ !this._waxRefreshing);
1448
+ this.log('tab visible', { hiddenMs, willRefresh });
1449
+ if (willRefresh) {
1341
1450
  this.refreshWaxKey().catch((err) => {
1342
1451
  // Proactive refresh failure is non-fatal — the reactive path on the
1343
1452
  // next createToken() call will handle it, including the auth retry.
@@ -1347,6 +1456,56 @@ class OzVault {
1347
1456
  this._hiddenAt = null;
1348
1457
  }
1349
1458
  }
1459
+ // ─── Debug ───────────────────────────────────────────────────────────────
1460
+ /**
1461
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1462
+ * not set. Never called with sensitive values — callers use presence flags only.
1463
+ */
1464
+ log(message, data) {
1465
+ if (!this._debug)
1466
+ return;
1467
+ if (data !== undefined) {
1468
+ console.log(`[OzVault] ${message}`, data);
1469
+ }
1470
+ else {
1471
+ console.log(`[OzVault] ${message}`);
1472
+ }
1473
+ }
1474
+ /**
1475
+ * Returns a plain-object snapshot of the vault's current internal state.
1476
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1477
+ *
1478
+ * Available on all vault instances regardless of whether `debug` was enabled.
1479
+ *
1480
+ * @example
1481
+ * console.log(vault.debugState());
1482
+ * // {
1483
+ * // vaultId: 'vault-abc123',
1484
+ * // isReady: true,
1485
+ * // tokenizing: null,
1486
+ * // destroyed: false,
1487
+ * // waxKeyPresent: true,
1488
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1489
+ * // ...
1490
+ * // }
1491
+ */
1492
+ debugState() {
1493
+ return {
1494
+ vaultId: this.vaultId,
1495
+ isReady: this.tokenizerReady,
1496
+ tokenizing: this._tokenizing,
1497
+ destroyed: this._destroyed,
1498
+ waxKeyPresent: Boolean(this.waxKey),
1499
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1500
+ maxTokenizeCalls: this._maxTokenizeCalls,
1501
+ resetCount: this._resetCount,
1502
+ elements: [...this.elementsByType.keys()],
1503
+ bankElements: [...this.bankElementsByType.keys()],
1504
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1505
+ pendingTokenizations: this.tokenizeResolvers.size,
1506
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1507
+ };
1508
+ }
1350
1509
  mountTokenizerFrame() {
1351
1510
  const mount = () => {
1352
1511
  this._pendingMount = null;
@@ -1358,6 +1517,7 @@ class OzVault {
1358
1517
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1359
1518
  document.body.appendChild(iframe);
1360
1519
  this.tokenizerFrame = iframe;
1520
+ this.log('mounting tokenizer iframe');
1361
1521
  };
1362
1522
  if (document.readyState === 'loading') {
1363
1523
  this._pendingMount = mount;
@@ -1393,6 +1553,12 @@ class OzVault {
1393
1553
  // the previous session and justCompleted never fires, breaking auto-advance.
1394
1554
  if (msg.type === 'OZ_FRAME_READY') {
1395
1555
  this.completionState.set(frameId, false);
1556
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1557
+ console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
1558
+ `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1559
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1560
+ }
1561
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1396
1562
  }
1397
1563
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1398
1564
  if (msg.type === 'OZ_CHANGE') {
@@ -1416,6 +1582,7 @@ class OzVault {
1416
1582
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1417
1583
  // where isComplete() fires before the user has finished typing.
1418
1584
  const justCompleted = complete && valid && !wasComplete;
1585
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1419
1586
  // Sync CVV length when card brand changes
1420
1587
  if (el.type === 'cardNumber') {
1421
1588
  const brand = msg.cardBrand;
@@ -1427,17 +1594,25 @@ class OzVault {
1427
1594
  // Auto-advance focus on completion
1428
1595
  if (justCompleted) {
1429
1596
  if (el.type === 'cardNumber') {
1597
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1430
1598
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1431
1599
  }
1432
1600
  else if (el.type === 'expirationDate') {
1601
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1433
1602
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1434
1603
  }
1435
1604
  }
1436
1605
  }
1437
1606
  handleTokenizerMessage(msg) {
1438
- var _a, _b, _c;
1607
+ var _a, _b, _c, _d;
1439
1608
  switch (msg.type) {
1440
1609
  case 'OZ_FRAME_READY':
1610
+ if (msg.__ozVersion !== PROTOCOL_VERSION) {
1611
+ console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
1612
+ `tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1613
+ 'This usually means the deployed frame files are stale. ' +
1614
+ 'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1615
+ }
1441
1616
  this.tokenizerReady = true;
1442
1617
  if (this.loadErrorTimeoutId != null) {
1443
1618
  clearTimeout(this.loadErrorTimeoutId);
@@ -1449,6 +1624,7 @@ class OzVault {
1449
1624
  // sent again from create() once the key is available.
1450
1625
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1451
1626
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1627
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1452
1628
  break;
1453
1629
  case 'OZ_TOKEN_RESULT': {
1454
1630
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1473,11 +1649,18 @@ class OzVault {
1473
1649
  }
1474
1650
  pending.resolve(Object.assign(Object.assign({ token,
1475
1651
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1652
+ this.log('token received', {
1653
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1654
+ tokenPresent: true,
1655
+ cvcSessionPresent: true,
1656
+ cardMetadataPresent: Boolean(card),
1657
+ });
1476
1658
  // Increment the per-key success counter and proactively refresh once
1477
1659
  // the budget is exhausted so the next createToken() call uses a fresh
1478
1660
  // key without waiting for a vault rejection.
1479
1661
  this._tokenizeSuccessCount++;
1480
1662
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1663
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1481
1664
  this.refreshWaxKey().catch((err) => {
1482
1665
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1483
1666
  });
@@ -1497,14 +1680,25 @@ class OzVault {
1497
1680
  this.tokenizeResolvers.delete(msg.requestId);
1498
1681
  if (pending.timeoutId != null)
1499
1682
  clearTimeout(pending.timeoutId);
1683
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1684
+ this.log('token error', { errorCode, willRefresh });
1500
1685
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1501
1686
  // already retried for this request, transparently re-mint and retry.
1502
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1687
+ if (willRefresh) {
1688
+ const resetCountAtRetry = this._resetCount;
1503
1689
  this.refreshWaxKey().then(() => {
1504
1690
  if (this._destroyed) {
1505
1691
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1506
1692
  return;
1507
1693
  }
1694
+ if (this._resetCount !== resetCountAtRetry) {
1695
+ // reset() was called while the wax key was refreshing — the fields
1696
+ // have been cleared and _tokenizing was zeroed. Reject the original
1697
+ // promise so it doesn't stay pending, and bail out without starting
1698
+ // a new retry (which would tokenize against empty fields).
1699
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1700
+ return;
1701
+ }
1508
1702
  const newRequestId = `req-${uuid()}`;
1509
1703
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1510
1704
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1551,11 +1745,16 @@ class OzVault {
1551
1745
  if (bankPending.timeoutId != null)
1552
1746
  clearTimeout(bankPending.timeoutId);
1553
1747
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1748
+ const resetCountAtRetry = this._resetCount;
1554
1749
  this.refreshWaxKey().then(() => {
1555
1750
  if (this._destroyed) {
1556
1751
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1557
1752
  return;
1558
1753
  }
1754
+ if (this._resetCount !== resetCountAtRetry) {
1755
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1756
+ return;
1757
+ }
1559
1758
  const newRequestId = `req-${uuid()}`;
1560
1759
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1561
1760
  try {
@@ -1613,9 +1812,15 @@ class OzVault {
1613
1812
  }
1614
1813
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1615
1814
  pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
1815
+ this.log('bank token received', {
1816
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1817
+ tokenPresent: true,
1818
+ bankMetadataPresent: Boolean(bank),
1819
+ });
1616
1820
  // Same proactive refresh logic as card tokenization.
1617
1821
  this._tokenizeSuccessCount++;
1618
1822
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1823
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1619
1824
  this.refreshWaxKey().catch((err) => {
1620
1825
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1621
1826
  });
@@ -1671,6 +1876,7 @@ class OzVault {
1671
1876
  }
1672
1877
  const newSessionId = uuid();
1673
1878
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
1879
+ this.log('wax key refresh started');
1674
1880
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1675
1881
  .then(newWaxKey => {
1676
1882
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1684,6 +1890,11 @@ class OzVault {
1684
1890
  if (!this._destroyed && this.tokenizerReady) {
1685
1891
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1686
1892
  }
1893
+ this.log('wax key refresh succeeded');
1894
+ })
1895
+ .catch((err) => {
1896
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
1897
+ throw err;
1687
1898
  })
1688
1899
  .finally(() => {
1689
1900
  this._waxRefreshing = null;
@@ -1791,7 +2002,7 @@ const OzContext = react.createContext({
1791
2002
  * All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
1792
2003
  * rendered inside this provider.
1793
2004
  */
1794
- function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, children }) {
2005
+ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, debug, children }) {
1795
2006
  const [vault, setVault] = react.useState(null);
1796
2007
  const [initError, setInitError] = react.useState(null);
1797
2008
  const [mountedCount, setMountedCount] = react.useState(0);
@@ -1858,7 +2069,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
1858
2069
  var _a;
1859
2070
  Promise.resolve().then(() => setTokenizeCount(0));
1860
2071
  (_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
1861
- } }), (onReadyRef.current ? { onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } } : {})), (maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), abortController.signal).then(v => {
2072
+ }, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
1862
2073
  if (cancelled) {
1863
2074
  v.destroy();
1864
2075
  return;
@@ -1891,7 +2102,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
1891
2102
  setVault(null);
1892
2103
  setInitError(null);
1893
2104
  };
1894
- }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls]);
2105
+ }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls, debug]);
1895
2106
  const notifyMount = react.useCallback(() => setMountedCount(n => n + 1), []);
1896
2107
  const notifyReady = react.useCallback(() => setReadyCount(n => n + 1), []);
1897
2108
  const notifyUnmount = react.useCallback(() => {
@@ -1924,8 +2135,11 @@ function useOzElements() {
1924
2135
  notifyTokenize();
1925
2136
  return result;
1926
2137
  }, [vault, notifyTokenize]);
2138
+ const reset = react.useCallback(() => {
2139
+ vault === null || vault === void 0 ? void 0 : vault.reset();
2140
+ }, [vault]);
1927
2141
  const ready = vault !== null && vault.isReady && mountedCount > 0 && readyCount >= mountedCount;
1928
- return { createToken, createBankToken, ready, initError, tokenizeCount };
2142
+ return { createToken, createBankToken, reset, ready, initError, tokenizeCount };
1929
2143
  }
1930
2144
  const SKELETON_STYLE = {
1931
2145
  height: 46,
@@ -2020,6 +2234,71 @@ const OzCardNumber = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type
2020
2234
  const OzExpiry = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "expirationDate", variant: "card" }, props));
2021
2235
  /** Renders a PCI-isolated CVV input inside an Ozura iframe. */
2022
2236
  const OzCvv = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "cvv", variant: "card" }, props));
2237
+ // ─── Shared composite-component hook ─────────────────────────────────────────
2238
+ /**
2239
+ * Shared plumbing for OzCard and OzBankCard.
2240
+ *
2241
+ * Manages:
2242
+ * - Callback refs (onChange, onReady, onFocus, onBlur) kept in sync on every render
2243
+ * - Vault-change detection: resets `readyFieldTypes` and `onReadyFiredRef` when the
2244
+ * vault instance is replaced (e.g. after fetchWaxKey changes or the provider remounts)
2245
+ * - Per-field ready tracking: creates one stable handler per named field; fires the
2246
+ * `onReady` callback once all `fieldNames.length` fields have reported ready
2247
+ * - Error state
2248
+ * - Layout helpers: `gapStr`, `renderLabel`
2249
+ *
2250
+ * @internal — not exported; used only by OzCard and OzBankCard.
2251
+ */
2252
+ function useCardBase({ vault, fieldNames, onChange, onReady, onFocus, onBlur, gap = 8, labelStyle, labelClassName, }) {
2253
+ const totalFields = fieldNames.length;
2254
+ const readyFieldTypes = react.useRef(new Set());
2255
+ const onReadyFiredRef = react.useRef(false);
2256
+ const vaultRef = react.useRef(vault);
2257
+ const onChangeRef = react.useRef(onChange);
2258
+ const onReadyRef = react.useRef(onReady);
2259
+ const onFocusRef = react.useRef(onFocus);
2260
+ const onBlurRef = react.useRef(onBlur);
2261
+ react.useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
2262
+ react.useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
2263
+ react.useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
2264
+ react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
2265
+ react.useEffect(() => {
2266
+ if (vault !== vaultRef.current) {
2267
+ vaultRef.current = vault;
2268
+ readyFieldTypes.current = new Set();
2269
+ onReadyFiredRef.current = false;
2270
+ }
2271
+ return () => {
2272
+ readyFieldTypes.current = new Set();
2273
+ onReadyFiredRef.current = false;
2274
+ };
2275
+ }, [vault]);
2276
+ // One stable handler per named field — recreated only when total field count changes.
2277
+ // Field names are static (card = 3 fields, bank = 2 fields) so `totalFields` alone
2278
+ // is a sufficient dependency; a JSON dep would create a new map on every render.
2279
+ // CONTRACT: `fieldNames` must be a static literal — callers must not pass a dynamic
2280
+ // array that changes length without also changing field count.
2281
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2282
+ const readyHandlers = react.useMemo(() => {
2283
+ const handlers = {};
2284
+ for (const name of fieldNames) {
2285
+ handlers[name] = () => {
2286
+ var _a;
2287
+ readyFieldTypes.current.add(name);
2288
+ if (readyFieldTypes.current.size >= totalFields && !onReadyFiredRef.current) {
2289
+ onReadyFiredRef.current = true;
2290
+ (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
2291
+ }
2292
+ };
2293
+ }
2294
+ return handlers;
2295
+ }, [totalFields]); // totalFields captures fieldNames.length; field names are static
2296
+ const [error, setError] = react.useState();
2297
+ const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
2298
+ const resolvedLabelStyle = react.useMemo(() => (labelStyle ? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE), [labelStyle]);
2299
+ const renderLabel = react.useCallback((text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle), [labelClassName, resolvedLabelStyle]);
2300
+ return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel };
2301
+ }
2023
2302
  const DEFAULT_ERROR_STYLE = {
2024
2303
  color: '#dc2626',
2025
2304
  fontSize: 13,
@@ -2062,62 +2341,22 @@ function mergeStyles(base, override) {
2062
2341
  function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName, layout = 'default', gap = 8, hideErrors = false, errorStyle, errorClassName, renderError, onChange, onReady, onFocus, onBlur, disabled, className, placeholders, }) {
2063
2342
  var _a, _b, _c;
2064
2343
  const { vault } = react.useContext(OzContext);
2344
+ const { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel, } = useCardBase({
2345
+ vault,
2346
+ fieldNames: ['cardNumber', 'expiry', 'cvv'],
2347
+ onChange,
2348
+ onReady,
2349
+ onFocus,
2350
+ onBlur,
2351
+ gap,
2352
+ labelStyle,
2353
+ labelClassName,
2354
+ });
2065
2355
  const fieldState = react.useRef({
2066
2356
  cardNumber: null,
2067
2357
  expiry: null,
2068
2358
  cvv: null,
2069
2359
  });
2070
- const readyFieldTypes = react.useRef(new Set());
2071
- const onReadyFiredRef = react.useRef(false);
2072
- const vaultRef = react.useRef(vault);
2073
- const onChangeRef = react.useRef(onChange);
2074
- const onReadyRef = react.useRef(onReady);
2075
- const onFocusRef = react.useRef(onFocus);
2076
- const onBlurRef = react.useRef(onBlur);
2077
- react.useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
2078
- react.useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
2079
- react.useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
2080
- react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
2081
- // When the vault is recreated (e.g. appearance/fonts props change on OzElements),
2082
- // context readyCount is reset but this ref is not. Reset so onReady fires once when all 3 are ready.
2083
- // The cleanup resets readyFieldTypes when the component unmounts (covers React StrictMode double-invoke
2084
- // and SPA scenarios where the parent re-mounts this component).
2085
- react.useEffect(() => {
2086
- if (vault !== vaultRef.current) {
2087
- vaultRef.current = vault;
2088
- readyFieldTypes.current = new Set();
2089
- onReadyFiredRef.current = false;
2090
- }
2091
- return () => {
2092
- readyFieldTypes.current = new Set();
2093
- onReadyFiredRef.current = false;
2094
- };
2095
- }, [vault]);
2096
- const [error, setError] = react.useState();
2097
- const handleCardNumberReady = react.useCallback(() => {
2098
- var _a;
2099
- readyFieldTypes.current.add('cardNumber');
2100
- if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
2101
- onReadyFiredRef.current = true;
2102
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
2103
- }
2104
- }, []);
2105
- const handleExpiryReady = react.useCallback(() => {
2106
- var _a;
2107
- readyFieldTypes.current.add('expiry');
2108
- if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
2109
- onReadyFiredRef.current = true;
2110
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
2111
- }
2112
- }, []);
2113
- const handleCvvReady = react.useCallback(() => {
2114
- var _a;
2115
- readyFieldTypes.current.add('cvv');
2116
- if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
2117
- onReadyFiredRef.current = true;
2118
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
2119
- }
2120
- }, []);
2121
2360
  const emitChange = react.useCallback(() => {
2122
2361
  var _a;
2123
2362
  const { cardNumber, expiry, cvv } = fieldState.current;
@@ -2132,20 +2371,16 @@ function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName,
2132
2371
  error: err,
2133
2372
  fields: Object.assign({}, fieldState.current),
2134
2373
  });
2135
- }, []);
2136
- const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
2137
- const resolvedLabelStyle = labelStyle
2138
- ? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
2139
- const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
2374
+ }, [setError, onChangeRef]);
2140
2375
  const showError = !hideErrors && error;
2141
2376
  const errorNode = showError
2142
2377
  ? renderError
2143
2378
  ? renderError(error)
2144
2379
  : (jsxRuntime.jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
2145
2380
  : null;
2146
- const cardNumberField = (jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cardNumber), jsxRuntime.jsx(OzCardNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cardNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.cardNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cardNumber) !== null && _a !== void 0 ? _a : 'Card number', disabled: disabled, onChange: (e) => { fieldState.current.cardNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cardNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cardNumber'); }, onReady: handleCardNumberReady })] }));
2147
- const expiryField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.expiry), jsxRuntime.jsx(OzExpiry, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.expiry), className: classNames === null || classNames === void 0 ? void 0 : classNames.expiry, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.expiry) !== null && _b !== void 0 ? _b : 'MM / YY', disabled: disabled, onChange: (e) => { fieldState.current.expiry = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'expiry'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'expiry'); }, onReady: handleExpiryReady })] }));
2148
- const cvvField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cvv), jsxRuntime.jsx(OzCvv, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cvv), className: classNames === null || classNames === void 0 ? void 0 : classNames.cvv, placeholder: (_c = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cvv) !== null && _c !== void 0 ? _c : 'CVV', disabled: disabled, onChange: (e) => { fieldState.current.cvv = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cvv'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cvv'); }, onReady: handleCvvReady })] }));
2381
+ 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: readyHandlers['cardNumber'] })] }));
2382
+ 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: readyHandlers['expiry'] })] }));
2383
+ 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: readyHandlers['cvv'] })] }));
2149
2384
  if (layout === 'rows') {
2150
2385
  return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [cardNumberField, expiryField, cvvField, errorNode] }));
2151
2386
  }
@@ -2166,49 +2401,21 @@ const OzBankRoutingNumber = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign
2166
2401
  function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassName, gap = 8, hideErrors = false, errorStyle, errorClassName, renderError, onChange, onReady, onFocus, onBlur, disabled, className, placeholders, }) {
2167
2402
  var _a, _b;
2168
2403
  const { vault } = react.useContext(OzContext);
2404
+ const { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel, } = useCardBase({
2405
+ vault,
2406
+ fieldNames: ['accountNumber', 'routingNumber'],
2407
+ onChange,
2408
+ onReady,
2409
+ onFocus,
2410
+ onBlur,
2411
+ gap,
2412
+ labelStyle,
2413
+ labelClassName,
2414
+ });
2169
2415
  const fieldState = react.useRef({
2170
2416
  accountNumber: null,
2171
2417
  routingNumber: null,
2172
2418
  });
2173
- const readyFieldTypes = react.useRef(new Set());
2174
- const onReadyFiredRef = react.useRef(false);
2175
- const vaultRef = react.useRef(vault);
2176
- const onChangeRef = react.useRef(onChange);
2177
- const onReadyRef = react.useRef(onReady);
2178
- const onFocusRef = react.useRef(onFocus);
2179
- const onBlurRef = react.useRef(onBlur);
2180
- react.useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
2181
- react.useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
2182
- react.useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
2183
- react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
2184
- react.useEffect(() => {
2185
- if (vault !== vaultRef.current) {
2186
- vaultRef.current = vault;
2187
- readyFieldTypes.current = new Set();
2188
- onReadyFiredRef.current = false;
2189
- }
2190
- return () => {
2191
- readyFieldTypes.current = new Set();
2192
- onReadyFiredRef.current = false;
2193
- };
2194
- }, [vault]);
2195
- const [error, setError] = react.useState();
2196
- const handleAccountReady = react.useCallback(() => {
2197
- var _a;
2198
- readyFieldTypes.current.add('accountNumber');
2199
- if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
2200
- onReadyFiredRef.current = true;
2201
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
2202
- }
2203
- }, []);
2204
- const handleRoutingReady = react.useCallback(() => {
2205
- var _a;
2206
- readyFieldTypes.current.add('routingNumber');
2207
- if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
2208
- onReadyFiredRef.current = true;
2209
- (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
2210
- }
2211
- }, []);
2212
2419
  const emitChange = react.useCallback(() => {
2213
2420
  var _a;
2214
2421
  const { accountNumber, routingNumber } = fieldState.current;
@@ -2221,18 +2428,14 @@ function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassN
2221
2428
  error: err,
2222
2429
  fields: Object.assign({}, fieldState.current),
2223
2430
  });
2224
- }, []);
2225
- const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
2226
- const resolvedLabelStyle = labelStyle
2227
- ? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
2228
- const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
2431
+ }, [setError, onChangeRef]);
2229
2432
  const showError = !hideErrors && error;
2230
2433
  const errorNode = showError
2231
2434
  ? renderError
2232
2435
  ? renderError(error)
2233
2436
  : (jsxRuntime.jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
2234
2437
  : null;
2235
- return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsxRuntime.jsx(OzBankAccountNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.accountNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.accountNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.accountNumber) !== null && _a !== void 0 ? _a : 'Account number', disabled: disabled, onChange: (e) => { fieldState.current.accountNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'accountNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'accountNumber'); }, onReady: handleAccountReady })] }), jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.routingNumber), jsxRuntime.jsx(OzBankRoutingNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.routingNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.routingNumber, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.routingNumber) !== null && _b !== void 0 ? _b : 'Routing number', disabled: disabled, onChange: (e) => { fieldState.current.routingNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'routingNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'routingNumber'); }, onReady: handleRoutingReady })] }), errorNode] }));
2438
+ return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsxRuntime.jsx(OzBankAccountNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.accountNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.accountNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.accountNumber) !== null && _a !== void 0 ? _a : 'Account number', disabled: disabled, onChange: (e) => { fieldState.current.accountNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'accountNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'accountNumber'); }, onReady: readyHandlers['accountNumber'] })] }), jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.routingNumber), jsxRuntime.jsx(OzBankRoutingNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.routingNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.routingNumber, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.routingNumber) !== null && _b !== void 0 ? _b : 'Routing number', disabled: disabled, onChange: (e) => { fieldState.current.routingNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'routingNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'routingNumber'); }, onReady: readyHandlers['routingNumber'] })] }), errorNode] }));
2236
2439
  }
2237
2440
 
2238
2441
  exports.OzBankAccountNumber = OzBankAccountNumber;