@ozura/elements 1.0.2-next.9 → 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.
@@ -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` });
@@ -871,7 +877,7 @@ class OzVault {
871
877
  * @internal
872
878
  */
873
879
  constructor(options, waxKey, tokenizationSessionId) {
874
- var _a, _b, _c;
880
+ var _a, _b, _c, _d;
875
881
  this.elements = new Map();
876
882
  this.elementsByType = new Map();
877
883
  this.bankElementsByType = new Map();
@@ -884,6 +890,9 @@ class OzVault {
884
890
  this.tokenizerReady = false;
885
891
  this._tokenizing = null;
886
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;
887
896
  // Tracks successful tokenizations against the per-key call budget so the SDK
888
897
  // can proactively refresh the wax key after it has been consumed rather than
889
898
  // waiting for the next createToken() call to fail.
@@ -903,13 +912,14 @@ class OzVault {
903
912
  this.resolvedAppearance = resolveAppearance(options.appearance);
904
913
  this.vaultId = `vault-${uuid()}`;
905
914
  this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
915
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
906
916
  this.boundHandleMessage = this.handleMessage.bind(this);
907
917
  window.addEventListener('message', this.boundHandleMessage);
908
918
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
909
919
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
910
920
  this.mountTokenizerFrame();
911
921
  if (options.onLoadError) {
912
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
922
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
913
923
  this.loadErrorTimeoutId = setTimeout(() => {
914
924
  this.loadErrorTimeoutId = null;
915
925
  if (!this._destroyed && !this.tokenizerReady) {
@@ -919,6 +929,7 @@ class OzVault {
919
929
  }
920
930
  this._onWaxRefresh = options.onWaxRefresh;
921
931
  this._onReady = options.onReady;
932
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
922
933
  }
923
934
  /**
924
935
  * Creates and returns a ready `OzVault` instance.
@@ -988,6 +999,7 @@ class OzVault {
988
999
  if (vault.tokenizerReady) {
989
1000
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
990
1001
  }
1002
+ vault.log('wax key received — vault ready');
991
1003
  return vault;
992
1004
  }
993
1005
  /**
@@ -1129,8 +1141,13 @@ class OzVault {
1129
1141
  const readyBankElements = [accountEl, routingEl];
1130
1142
  this._tokenizing = 'bank';
1131
1143
  const requestId = `req-${uuid()}`;
1144
+ this.log('createBankToken() called');
1132
1145
  return new Promise((resolve, reject) => {
1133
- const cleanup = () => { this._tokenizing = null; };
1146
+ const resetCountAtStart = this._resetCount;
1147
+ const cleanup = () => {
1148
+ if (this._resetCount === resetCountAtStart)
1149
+ this._tokenizing = null;
1150
+ };
1134
1151
  this.bankTokenizeResolvers.set(requestId, {
1135
1152
  resolve: (v) => { cleanup(); resolve(v); },
1136
1153
  reject: (e) => { cleanup(); reject(e); },
@@ -1141,6 +1158,7 @@ class OzVault {
1141
1158
  });
1142
1159
  try {
1143
1160
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1161
+ const bankTokenizeStartMs = Date.now();
1144
1162
  this.sendToTokenizer({
1145
1163
  type: 'OZ_BANK_TOKENIZE',
1146
1164
  requestId,
@@ -1151,6 +1169,7 @@ class OzVault {
1151
1169
  lastName: options.lastName.trim(),
1152
1170
  fieldCount: readyBankElements.length,
1153
1171
  }, bankChannels.map(ch => ch.port1));
1172
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1154
1173
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1155
1174
  const bankTimeoutId = setTimeout(() => {
1156
1175
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1161,8 +1180,10 @@ class OzVault {
1161
1180
  }
1162
1181
  }, 30000);
1163
1182
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1164
- if (bankPendingEntry)
1183
+ if (bankPendingEntry) {
1165
1184
  bankPendingEntry.timeoutId = bankTimeoutId;
1185
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1186
+ }
1166
1187
  }
1167
1188
  catch (err) {
1168
1189
  this.bankTokenizeResolvers.delete(requestId);
@@ -1233,8 +1254,15 @@ class OzVault {
1233
1254
  }
1234
1255
  this._tokenizing = 'card';
1235
1256
  const requestId = `req-${uuid()}`;
1257
+ this.log('createToken() called');
1236
1258
  return new Promise((resolve, reject) => {
1237
- 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
+ };
1238
1266
  this.tokenizeResolvers.set(requestId, {
1239
1267
  resolve: (v) => { cleanup(); resolve(v); },
1240
1268
  reject: (e) => { cleanup(); reject(e); },
@@ -1247,6 +1275,7 @@ class OzVault {
1247
1275
  try {
1248
1276
  // Tell tokenizer frame to expect N field values, then tokenize
1249
1277
  const cardChannels = readyElements.map(() => new MessageChannel());
1278
+ const tokenizeStartMs = Date.now();
1250
1279
  this.sendToTokenizer({
1251
1280
  type: 'OZ_TOKENIZE',
1252
1281
  requestId,
@@ -1257,6 +1286,11 @@ class OzVault {
1257
1286
  lastName,
1258
1287
  fieldCount: readyElements.length,
1259
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;
1260
1294
  // Tell each ready element frame to send its raw value to the tokenizer
1261
1295
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1262
1296
  const cardTimeoutId = setTimeout(() => {
@@ -1278,6 +1312,63 @@ class OzVault {
1278
1312
  }
1279
1313
  });
1280
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
+ }
1281
1372
  /**
1282
1373
  * Tears down the vault: removes all element iframes, the tokenizer iframe,
1283
1374
  * and the global message listener. Call this when the checkout component
@@ -1288,6 +1379,7 @@ class OzVault {
1288
1379
  if (this._destroyed)
1289
1380
  return;
1290
1381
  this._destroyed = true;
1382
+ this.log('destroy() called');
1291
1383
  window.removeEventListener('message', this.boundHandleMessage);
1292
1384
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1293
1385
  if (this._pendingMount) {
@@ -1342,13 +1434,17 @@ class OzVault {
1342
1434
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1343
1435
  if (document.hidden) {
1344
1436
  this._hiddenAt = Date.now();
1437
+ this.log('tab hidden');
1345
1438
  }
1346
1439
  else {
1347
- if (this._hiddenAt !== null &&
1348
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1349
- 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) &&
1350
1444
  !this._tokenizing &&
1351
- !this._waxRefreshing) {
1445
+ !this._waxRefreshing);
1446
+ this.log('tab visible', { hiddenMs, willRefresh });
1447
+ if (willRefresh) {
1352
1448
  this.refreshWaxKey().catch((err) => {
1353
1449
  // Proactive refresh failure is non-fatal — the reactive path on the
1354
1450
  // next createToken() call will handle it, including the auth retry.
@@ -1358,6 +1454,56 @@ class OzVault {
1358
1454
  this._hiddenAt = null;
1359
1455
  }
1360
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
+ }
1361
1507
  mountTokenizerFrame() {
1362
1508
  const mount = () => {
1363
1509
  this._pendingMount = null;
@@ -1369,6 +1515,7 @@ class OzVault {
1369
1515
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1370
1516
  document.body.appendChild(iframe);
1371
1517
  this.tokenizerFrame = iframe;
1518
+ this.log('mounting tokenizer iframe');
1372
1519
  };
1373
1520
  if (document.readyState === 'loading') {
1374
1521
  this._pendingMount = mount;
@@ -1409,6 +1556,7 @@ class OzVault {
1409
1556
  `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1410
1557
  'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1411
1558
  }
1559
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1412
1560
  }
1413
1561
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1414
1562
  if (msg.type === 'OZ_CHANGE') {
@@ -1432,6 +1580,7 @@ class OzVault {
1432
1580
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1433
1581
  // where isComplete() fires before the user has finished typing.
1434
1582
  const justCompleted = complete && valid && !wasComplete;
1583
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1435
1584
  // Sync CVV length when card brand changes
1436
1585
  if (el.type === 'cardNumber') {
1437
1586
  const brand = msg.cardBrand;
@@ -1443,15 +1592,17 @@ class OzVault {
1443
1592
  // Auto-advance focus on completion
1444
1593
  if (justCompleted) {
1445
1594
  if (el.type === 'cardNumber') {
1595
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1446
1596
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1447
1597
  }
1448
1598
  else if (el.type === 'expirationDate') {
1599
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1449
1600
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1450
1601
  }
1451
1602
  }
1452
1603
  }
1453
1604
  handleTokenizerMessage(msg) {
1454
- var _a, _b, _c;
1605
+ var _a, _b, _c, _d;
1455
1606
  switch (msg.type) {
1456
1607
  case 'OZ_FRAME_READY':
1457
1608
  if (msg.__ozVersion !== PROTOCOL_VERSION) {
@@ -1471,6 +1622,7 @@ class OzVault {
1471
1622
  // sent again from create() once the key is available.
1472
1623
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1473
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 });
1474
1626
  break;
1475
1627
  case 'OZ_TOKEN_RESULT': {
1476
1628
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1495,11 +1647,18 @@ class OzVault {
1495
1647
  }
1496
1648
  pending.resolve(Object.assign(Object.assign({ token,
1497
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
+ });
1498
1656
  // Increment the per-key success counter and proactively refresh once
1499
1657
  // the budget is exhausted so the next createToken() call uses a fresh
1500
1658
  // key without waiting for a vault rejection.
1501
1659
  this._tokenizeSuccessCount++;
1502
1660
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1661
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1503
1662
  this.refreshWaxKey().catch((err) => {
1504
1663
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1505
1664
  });
@@ -1519,14 +1678,25 @@ class OzVault {
1519
1678
  this.tokenizeResolvers.delete(msg.requestId);
1520
1679
  if (pending.timeoutId != null)
1521
1680
  clearTimeout(pending.timeoutId);
1681
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1682
+ this.log('token error', { errorCode, willRefresh });
1522
1683
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1523
1684
  // already retried for this request, transparently re-mint and retry.
1524
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1685
+ if (willRefresh) {
1686
+ const resetCountAtRetry = this._resetCount;
1525
1687
  this.refreshWaxKey().then(() => {
1526
1688
  if (this._destroyed) {
1527
1689
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1528
1690
  return;
1529
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
+ }
1530
1700
  const newRequestId = `req-${uuid()}`;
1531
1701
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1532
1702
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1573,11 +1743,16 @@ class OzVault {
1573
1743
  if (bankPending.timeoutId != null)
1574
1744
  clearTimeout(bankPending.timeoutId);
1575
1745
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1746
+ const resetCountAtRetry = this._resetCount;
1576
1747
  this.refreshWaxKey().then(() => {
1577
1748
  if (this._destroyed) {
1578
1749
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1579
1750
  return;
1580
1751
  }
1752
+ if (this._resetCount !== resetCountAtRetry) {
1753
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1754
+ return;
1755
+ }
1581
1756
  const newRequestId = `req-${uuid()}`;
1582
1757
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1583
1758
  try {
@@ -1635,9 +1810,15 @@ class OzVault {
1635
1810
  }
1636
1811
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1637
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
+ });
1638
1818
  // Same proactive refresh logic as card tokenization.
1639
1819
  this._tokenizeSuccessCount++;
1640
1820
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1821
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1641
1822
  this.refreshWaxKey().catch((err) => {
1642
1823
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1643
1824
  });
@@ -1693,6 +1874,7 @@ class OzVault {
1693
1874
  }
1694
1875
  const newSessionId = uuid();
1695
1876
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
1877
+ this.log('wax key refresh started');
1696
1878
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1697
1879
  .then(newWaxKey => {
1698
1880
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1706,6 +1888,11 @@ class OzVault {
1706
1888
  if (!this._destroyed && this.tokenizerReady) {
1707
1889
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1708
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;
1709
1896
  })
1710
1897
  .finally(() => {
1711
1898
  this._waxRefreshing = null;
@@ -1813,7 +2000,7 @@ const OzContext = createContext({
1813
2000
  * All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
1814
2001
  * rendered inside this provider.
1815
2002
  */
1816
- 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 }) {
1817
2004
  const [vault, setVault] = useState(null);
1818
2005
  const [initError, setInitError] = useState(null);
1819
2006
  const [mountedCount, setMountedCount] = useState(0);
@@ -1880,7 +2067,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
1880
2067
  var _a;
1881
2068
  Promise.resolve().then(() => setTokenizeCount(0));
1882
2069
  (_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
1883
- } }), (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 => {
1884
2071
  if (cancelled) {
1885
2072
  v.destroy();
1886
2073
  return;
@@ -1913,7 +2100,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
1913
2100
  setVault(null);
1914
2101
  setInitError(null);
1915
2102
  };
1916
- }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls]);
2103
+ }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls, debug]);
1917
2104
  const notifyMount = useCallback(() => setMountedCount(n => n + 1), []);
1918
2105
  const notifyReady = useCallback(() => setReadyCount(n => n + 1), []);
1919
2106
  const notifyUnmount = useCallback(() => {
@@ -1946,8 +2133,11 @@ function useOzElements() {
1946
2133
  notifyTokenize();
1947
2134
  return result;
1948
2135
  }, [vault, notifyTokenize]);
2136
+ const reset = useCallback(() => {
2137
+ vault === null || vault === void 0 ? void 0 : vault.reset();
2138
+ }, [vault]);
1949
2139
  const ready = vault !== null && vault.isReady && mountedCount > 0 && readyCount >= mountedCount;
1950
- return { createToken, createBankToken, ready, initError, tokenizeCount };
2140
+ return { createToken, createBankToken, reset, ready, initError, tokenizeCount };
1951
2141
  }
1952
2142
  const SKELETON_STYLE = {
1953
2143
  height: 46,
@@ -2053,7 +2243,7 @@ const OzCvv = (props) => jsx(OzFieldBase, Object.assign({ type: "cvv", variant:
2053
2243
  * - Per-field ready tracking: creates one stable handler per named field; fires the
2054
2244
  * `onReady` callback once all `fieldNames.length` fields have reported ready
2055
2245
  * - Error state
2056
- * - Layout helpers: `gapStr`, `resolvedLabelStyle`, `renderLabel`
2246
+ * - Layout helpers: `gapStr`, `renderLabel`
2057
2247
  *
2058
2248
  * @internal — not exported; used only by OzCard and OzBankCard.
2059
2249
  */
@@ -2105,7 +2295,7 @@ function useCardBase({ vault, fieldNames, onChange, onReady, onFocus, onBlur, ga
2105
2295
  const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
2106
2296
  const resolvedLabelStyle = useMemo(() => (labelStyle ? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE), [labelStyle]);
2107
2297
  const renderLabel = useCallback((text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle), [labelClassName, resolvedLabelStyle]);
2108
- return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, resolvedLabelStyle, renderLabel };
2298
+ return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel };
2109
2299
  }
2110
2300
  const DEFAULT_ERROR_STYLE = {
2111
2301
  color: '#dc2626',