@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.
@@ -413,7 +413,7 @@
413
413
  * (useful when integrating with React refs).
414
414
  */
415
415
  mount(target) {
416
- var _a;
416
+ var _a, _b;
417
417
  if (this._destroyed)
418
418
  throw new OzError('Cannot mount a destroyed element.');
419
419
  if (this.iframe)
@@ -430,7 +430,13 @@
430
430
  iframe.setAttribute('scrolling', 'no');
431
431
  iframe.setAttribute('allowtransparency', 'true');
432
432
  iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
433
- iframe.title = `Secure ${this.elementType} input`;
433
+ iframe.title = `Secure ${(_a = {
434
+ cardNumber: 'card number',
435
+ expirationDate: 'expiration date',
436
+ cvv: 'CVV',
437
+ accountNumber: 'account number',
438
+ routingNumber: 'routing number',
439
+ }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
434
440
  // Note: the `sandbox` attribute is intentionally NOT set. Field values are
435
441
  // delivered to the tokenizer iframe via a MessageChannel port (transferred
436
442
  // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
@@ -444,7 +450,7 @@
444
450
  container.appendChild(iframe);
445
451
  this.iframe = iframe;
446
452
  this._frameWindow = iframe.contentWindow;
447
- const timeout = (_a = this.options.loadTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
453
+ const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
448
454
  this._loadTimer = setTimeout(() => {
449
455
  if (!this._ready && !this._destroyed) {
450
456
  this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
@@ -929,7 +935,7 @@
929
935
  * @internal
930
936
  */
931
937
  constructor(options, waxKey, tokenizationSessionId) {
932
- var _a, _b, _c;
938
+ var _a, _b, _c, _d;
933
939
  this.elements = new Map();
934
940
  this.elementsByType = new Map();
935
941
  this.bankElementsByType = new Map();
@@ -942,6 +948,9 @@
942
948
  this.tokenizerReady = false;
943
949
  this._tokenizing = null;
944
950
  this._destroyed = false;
951
+ // Incremented every time reset() cancels an active tokenization so that
952
+ // any in-flight wax-key refresh retry can detect it was superseded.
953
+ this._resetCount = 0;
945
954
  // Tracks successful tokenizations against the per-key call budget so the SDK
946
955
  // can proactively refresh the wax key after it has been consumed rather than
947
956
  // waiting for the next createToken() call to fail.
@@ -961,13 +970,14 @@
961
970
  this.resolvedAppearance = resolveAppearance(options.appearance);
962
971
  this.vaultId = `vault-${uuid()}`;
963
972
  this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
973
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
964
974
  this.boundHandleMessage = this.handleMessage.bind(this);
965
975
  window.addEventListener('message', this.boundHandleMessage);
966
976
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
967
977
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
968
978
  this.mountTokenizerFrame();
969
979
  if (options.onLoadError) {
970
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
980
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
971
981
  this.loadErrorTimeoutId = setTimeout(() => {
972
982
  this.loadErrorTimeoutId = null;
973
983
  if (!this._destroyed && !this.tokenizerReady) {
@@ -977,6 +987,7 @@
977
987
  }
978
988
  this._onWaxRefresh = options.onWaxRefresh;
979
989
  this._onReady = options.onReady;
990
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
980
991
  }
981
992
  /**
982
993
  * Creates and returns a ready `OzVault` instance.
@@ -1046,6 +1057,7 @@
1046
1057
  if (vault.tokenizerReady) {
1047
1058
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
1048
1059
  }
1060
+ vault.log('wax key received — vault ready');
1049
1061
  return vault;
1050
1062
  }
1051
1063
  /**
@@ -1187,8 +1199,13 @@
1187
1199
  const readyBankElements = [accountEl, routingEl];
1188
1200
  this._tokenizing = 'bank';
1189
1201
  const requestId = `req-${uuid()}`;
1202
+ this.log('createBankToken() called');
1190
1203
  return new Promise((resolve, reject) => {
1191
- const cleanup = () => { this._tokenizing = null; };
1204
+ const resetCountAtStart = this._resetCount;
1205
+ const cleanup = () => {
1206
+ if (this._resetCount === resetCountAtStart)
1207
+ this._tokenizing = null;
1208
+ };
1192
1209
  this.bankTokenizeResolvers.set(requestId, {
1193
1210
  resolve: (v) => { cleanup(); resolve(v); },
1194
1211
  reject: (e) => { cleanup(); reject(e); },
@@ -1199,6 +1216,7 @@
1199
1216
  });
1200
1217
  try {
1201
1218
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1219
+ const bankTokenizeStartMs = Date.now();
1202
1220
  this.sendToTokenizer({
1203
1221
  type: 'OZ_BANK_TOKENIZE',
1204
1222
  requestId,
@@ -1209,6 +1227,7 @@
1209
1227
  lastName: options.lastName.trim(),
1210
1228
  fieldCount: readyBankElements.length,
1211
1229
  }, bankChannels.map(ch => ch.port1));
1230
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1212
1231
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1213
1232
  const bankTimeoutId = setTimeout(() => {
1214
1233
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1219,8 +1238,10 @@
1219
1238
  }
1220
1239
  }, 30000);
1221
1240
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1222
- if (bankPendingEntry)
1241
+ if (bankPendingEntry) {
1223
1242
  bankPendingEntry.timeoutId = bankTimeoutId;
1243
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1244
+ }
1224
1245
  }
1225
1246
  catch (err) {
1226
1247
  this.bankTokenizeResolvers.delete(requestId);
@@ -1291,8 +1312,15 @@
1291
1312
  }
1292
1313
  this._tokenizing = 'card';
1293
1314
  const requestId = `req-${uuid()}`;
1315
+ this.log('createToken() called');
1294
1316
  return new Promise((resolve, reject) => {
1295
- const cleanup = () => { this._tokenizing = null; };
1317
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1318
+ // still belongs to this invocation — not a newer one that started after a reset.
1319
+ const resetCountAtStart = this._resetCount;
1320
+ const cleanup = () => {
1321
+ if (this._resetCount === resetCountAtStart)
1322
+ this._tokenizing = null;
1323
+ };
1296
1324
  this.tokenizeResolvers.set(requestId, {
1297
1325
  resolve: (v) => { cleanup(); resolve(v); },
1298
1326
  reject: (e) => { cleanup(); reject(e); },
@@ -1305,6 +1333,7 @@
1305
1333
  try {
1306
1334
  // Tell tokenizer frame to expect N field values, then tokenize
1307
1335
  const cardChannels = readyElements.map(() => new MessageChannel());
1336
+ const tokenizeStartMs = Date.now();
1308
1337
  this.sendToTokenizer({
1309
1338
  type: 'OZ_TOKENIZE',
1310
1339
  requestId,
@@ -1315,6 +1344,11 @@
1315
1344
  lastName,
1316
1345
  fieldCount: readyElements.length,
1317
1346
  }, cardChannels.map(ch => ch.port1));
1347
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1348
+ // Store start time for elapsed-ms logging on result
1349
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1350
+ if (cardEntry)
1351
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1318
1352
  // Tell each ready element frame to send its raw value to the tokenizer
1319
1353
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1320
1354
  const cardTimeoutId = setTimeout(() => {
@@ -1336,6 +1370,63 @@
1336
1370
  }
1337
1371
  });
1338
1372
  }
1373
+ /**
1374
+ * Clears all mounted element fields without tearing down the vault.
1375
+ *
1376
+ * Call this after a failed payment (e.g. card declined) to let the customer
1377
+ * re-enter their details. The vault instance, tokenizer iframe, wax key, and
1378
+ * tokenization budget counter are all preserved — no network calls are made.
1379
+ *
1380
+ * **Wax key session model:** by design, one wax key covers the full checkout
1381
+ * session. The default `max_tokenize_calls: 3` supports two declined attempts
1382
+ * and one final attempt on the same key. Do not call `vault.destroy()` and
1383
+ * recreate the vault between declines — that unnecessarily re-mints the key
1384
+ * and discards the remaining budget.
1385
+ *
1386
+ * @example
1387
+ * try {
1388
+ * const { token, cvcSession } = await vault.createToken({ billing });
1389
+ * await chargeCard(token, cvcSession);
1390
+ * } catch (err) {
1391
+ * vault.reset(); // clear fields; let customer re-enter
1392
+ * showError(err.message);
1393
+ * }
1394
+ */
1395
+ reset() {
1396
+ if (this._destroyed)
1397
+ return;
1398
+ const cancelling = Boolean(this._tokenizing);
1399
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1400
+ if (this._tokenizing) {
1401
+ this._tokenizing = null;
1402
+ this._resetCount++;
1403
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1404
+ if (timeoutId != null)
1405
+ clearTimeout(timeoutId);
1406
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1407
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1408
+ });
1409
+ this.tokenizeResolvers.clear();
1410
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1411
+ if (timeoutId != null)
1412
+ clearTimeout(timeoutId);
1413
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1414
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1415
+ });
1416
+ this.bankTokenizeResolvers.clear();
1417
+ }
1418
+ // Clear field values in all mounted element iframes
1419
+ this.elementsByType.forEach(el => el.clear());
1420
+ this.bankElementsByType.forEach(el => el.clear());
1421
+ // Reset per-element completion state so auto-advance starts fresh on re-entry
1422
+ for (const frameId of this.completionState.keys()) {
1423
+ this.completionState.set(frameId, false);
1424
+ }
1425
+ // NOTE: _tokenizeSuccessCount is intentionally NOT reset.
1426
+ // It reflects real server-side wax key budget consumption. Zeroing it
1427
+ // would desync the proactive refresh logic from the vault's state and
1428
+ // risk triggering a mid-session re-mint on what should be a clean retry.
1429
+ }
1339
1430
  /**
1340
1431
  * Tears down the vault: removes all element iframes, the tokenizer iframe,
1341
1432
  * and the global message listener. Call this when the checkout component
@@ -1346,6 +1437,7 @@
1346
1437
  if (this._destroyed)
1347
1438
  return;
1348
1439
  this._destroyed = true;
1440
+ this.log('destroy() called');
1349
1441
  window.removeEventListener('message', this.boundHandleMessage);
1350
1442
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1351
1443
  if (this._pendingMount) {
@@ -1400,13 +1492,17 @@
1400
1492
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1401
1493
  if (document.hidden) {
1402
1494
  this._hiddenAt = Date.now();
1495
+ this.log('tab hidden');
1403
1496
  }
1404
1497
  else {
1405
- if (this._hiddenAt !== null &&
1406
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1407
- this._storedFetchWaxKey &&
1498
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1499
+ const willRefresh = (this._hiddenAt !== null &&
1500
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1501
+ Boolean(this._storedFetchWaxKey) &&
1408
1502
  !this._tokenizing &&
1409
- !this._waxRefreshing) {
1503
+ !this._waxRefreshing);
1504
+ this.log('tab visible', { hiddenMs, willRefresh });
1505
+ if (willRefresh) {
1410
1506
  this.refreshWaxKey().catch((err) => {
1411
1507
  // Proactive refresh failure is non-fatal — the reactive path on the
1412
1508
  // next createToken() call will handle it, including the auth retry.
@@ -1416,6 +1512,56 @@
1416
1512
  this._hiddenAt = null;
1417
1513
  }
1418
1514
  }
1515
+ // ─── Debug ───────────────────────────────────────────────────────────────
1516
+ /**
1517
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1518
+ * not set. Never called with sensitive values — callers use presence flags only.
1519
+ */
1520
+ log(message, data) {
1521
+ if (!this._debug)
1522
+ return;
1523
+ if (data !== undefined) {
1524
+ console.log(`[OzVault] ${message}`, data);
1525
+ }
1526
+ else {
1527
+ console.log(`[OzVault] ${message}`);
1528
+ }
1529
+ }
1530
+ /**
1531
+ * Returns a plain-object snapshot of the vault's current internal state.
1532
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1533
+ *
1534
+ * Available on all vault instances regardless of whether `debug` was enabled.
1535
+ *
1536
+ * @example
1537
+ * console.log(vault.debugState());
1538
+ * // {
1539
+ * // vaultId: 'vault-abc123',
1540
+ * // isReady: true,
1541
+ * // tokenizing: null,
1542
+ * // destroyed: false,
1543
+ * // waxKeyPresent: true,
1544
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1545
+ * // ...
1546
+ * // }
1547
+ */
1548
+ debugState() {
1549
+ return {
1550
+ vaultId: this.vaultId,
1551
+ isReady: this.tokenizerReady,
1552
+ tokenizing: this._tokenizing,
1553
+ destroyed: this._destroyed,
1554
+ waxKeyPresent: Boolean(this.waxKey),
1555
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1556
+ maxTokenizeCalls: this._maxTokenizeCalls,
1557
+ resetCount: this._resetCount,
1558
+ elements: [...this.elementsByType.keys()],
1559
+ bankElements: [...this.bankElementsByType.keys()],
1560
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1561
+ pendingTokenizations: this.tokenizeResolvers.size,
1562
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1563
+ };
1564
+ }
1419
1565
  mountTokenizerFrame() {
1420
1566
  const mount = () => {
1421
1567
  this._pendingMount = null;
@@ -1427,6 +1573,7 @@
1427
1573
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1428
1574
  document.body.appendChild(iframe);
1429
1575
  this.tokenizerFrame = iframe;
1576
+ this.log('mounting tokenizer iframe');
1430
1577
  };
1431
1578
  if (document.readyState === 'loading') {
1432
1579
  this._pendingMount = mount;
@@ -1467,6 +1614,7 @@
1467
1614
  `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1468
1615
  'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1469
1616
  }
1617
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1470
1618
  }
1471
1619
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1472
1620
  if (msg.type === 'OZ_CHANGE') {
@@ -1490,6 +1638,7 @@
1490
1638
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1491
1639
  // where isComplete() fires before the user has finished typing.
1492
1640
  const justCompleted = complete && valid && !wasComplete;
1641
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1493
1642
  // Sync CVV length when card brand changes
1494
1643
  if (el.type === 'cardNumber') {
1495
1644
  const brand = msg.cardBrand;
@@ -1501,15 +1650,17 @@
1501
1650
  // Auto-advance focus on completion
1502
1651
  if (justCompleted) {
1503
1652
  if (el.type === 'cardNumber') {
1653
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1504
1654
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1505
1655
  }
1506
1656
  else if (el.type === 'expirationDate') {
1657
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1507
1658
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1508
1659
  }
1509
1660
  }
1510
1661
  }
1511
1662
  handleTokenizerMessage(msg) {
1512
- var _a, _b, _c;
1663
+ var _a, _b, _c, _d;
1513
1664
  switch (msg.type) {
1514
1665
  case 'OZ_FRAME_READY':
1515
1666
  if (msg.__ozVersion !== PROTOCOL_VERSION) {
@@ -1529,6 +1680,7 @@
1529
1680
  // sent again from create() once the key is available.
1530
1681
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1531
1682
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1683
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1532
1684
  break;
1533
1685
  case 'OZ_TOKEN_RESULT': {
1534
1686
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1553,11 +1705,18 @@
1553
1705
  }
1554
1706
  pending.resolve(Object.assign(Object.assign({ token,
1555
1707
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1708
+ this.log('token received', {
1709
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1710
+ tokenPresent: true,
1711
+ cvcSessionPresent: true,
1712
+ cardMetadataPresent: Boolean(card),
1713
+ });
1556
1714
  // Increment the per-key success counter and proactively refresh once
1557
1715
  // the budget is exhausted so the next createToken() call uses a fresh
1558
1716
  // key without waiting for a vault rejection.
1559
1717
  this._tokenizeSuccessCount++;
1560
1718
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1719
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1561
1720
  this.refreshWaxKey().catch((err) => {
1562
1721
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1563
1722
  });
@@ -1577,14 +1736,25 @@
1577
1736
  this.tokenizeResolvers.delete(msg.requestId);
1578
1737
  if (pending.timeoutId != null)
1579
1738
  clearTimeout(pending.timeoutId);
1739
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1740
+ this.log('token error', { errorCode, willRefresh });
1580
1741
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1581
1742
  // already retried for this request, transparently re-mint and retry.
1582
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1743
+ if (willRefresh) {
1744
+ const resetCountAtRetry = this._resetCount;
1583
1745
  this.refreshWaxKey().then(() => {
1584
1746
  if (this._destroyed) {
1585
1747
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1586
1748
  return;
1587
1749
  }
1750
+ if (this._resetCount !== resetCountAtRetry) {
1751
+ // reset() was called while the wax key was refreshing — the fields
1752
+ // have been cleared and _tokenizing was zeroed. Reject the original
1753
+ // promise so it doesn't stay pending, and bail out without starting
1754
+ // a new retry (which would tokenize against empty fields).
1755
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1756
+ return;
1757
+ }
1588
1758
  const newRequestId = `req-${uuid()}`;
1589
1759
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1590
1760
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1631,11 +1801,16 @@
1631
1801
  if (bankPending.timeoutId != null)
1632
1802
  clearTimeout(bankPending.timeoutId);
1633
1803
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1804
+ const resetCountAtRetry = this._resetCount;
1634
1805
  this.refreshWaxKey().then(() => {
1635
1806
  if (this._destroyed) {
1636
1807
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1637
1808
  return;
1638
1809
  }
1810
+ if (this._resetCount !== resetCountAtRetry) {
1811
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1812
+ return;
1813
+ }
1639
1814
  const newRequestId = `req-${uuid()}`;
1640
1815
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1641
1816
  try {
@@ -1693,9 +1868,15 @@
1693
1868
  }
1694
1869
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1695
1870
  pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
1871
+ this.log('bank token received', {
1872
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1873
+ tokenPresent: true,
1874
+ bankMetadataPresent: Boolean(bank),
1875
+ });
1696
1876
  // Same proactive refresh logic as card tokenization.
1697
1877
  this._tokenizeSuccessCount++;
1698
1878
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1879
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1699
1880
  this.refreshWaxKey().catch((err) => {
1700
1881
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1701
1882
  });
@@ -1751,6 +1932,7 @@
1751
1932
  }
1752
1933
  const newSessionId = uuid();
1753
1934
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
1935
+ this.log('wax key refresh started');
1754
1936
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1755
1937
  .then(newWaxKey => {
1756
1938
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1764,6 +1946,11 @@
1764
1946
  if (!this._destroyed && this.tokenizerReady) {
1765
1947
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1766
1948
  }
1949
+ this.log('wax key refresh succeeded');
1950
+ })
1951
+ .catch((err) => {
1952
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
1953
+ throw err;
1767
1954
  })
1768
1955
  .finally(() => {
1769
1956
  this._waxRefreshing = null;