@ozura/elements 1.0.2 → 1.1.0

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