@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.
@@ -357,7 +357,7 @@ class OzElement {
357
357
  * (useful when integrating with React refs).
358
358
  */
359
359
  mount(target) {
360
- var _a;
360
+ var _a, _b;
361
361
  if (this._destroyed)
362
362
  throw new OzError('Cannot mount a destroyed element.');
363
363
  if (this.iframe)
@@ -374,7 +374,13 @@ class OzElement {
374
374
  iframe.setAttribute('scrolling', 'no');
375
375
  iframe.setAttribute('allowtransparency', 'true');
376
376
  iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
377
- iframe.title = `Secure ${this.elementType} input`;
377
+ iframe.title = `Secure ${(_a = {
378
+ cardNumber: 'card number',
379
+ expirationDate: 'expiration date',
380
+ cvv: 'CVV',
381
+ accountNumber: 'account number',
382
+ routingNumber: 'routing number',
383
+ }[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
378
384
  // Note: the `sandbox` attribute is intentionally NOT set. Field values are
379
385
  // delivered to the tokenizer iframe via a MessageChannel port (transferred
380
386
  // in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
@@ -388,7 +394,7 @@ class OzElement {
388
394
  container.appendChild(iframe);
389
395
  this.iframe = iframe;
390
396
  this._frameWindow = iframe.contentWindow;
391
- const timeout = (_a = this.options.loadTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
397
+ const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
392
398
  this._loadTimer = setTimeout(() => {
393
399
  if (!this._ready && !this._destroyed) {
394
400
  this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
@@ -873,7 +879,7 @@ class OzVault {
873
879
  * @internal
874
880
  */
875
881
  constructor(options, waxKey, tokenizationSessionId) {
876
- var _a, _b, _c;
882
+ var _a, _b, _c, _d;
877
883
  this.elements = new Map();
878
884
  this.elementsByType = new Map();
879
885
  this.bankElementsByType = new Map();
@@ -886,6 +892,9 @@ class OzVault {
886
892
  this.tokenizerReady = false;
887
893
  this._tokenizing = null;
888
894
  this._destroyed = false;
895
+ // Incremented every time reset() cancels an active tokenization so that
896
+ // any in-flight wax-key refresh retry can detect it was superseded.
897
+ this._resetCount = 0;
889
898
  // Tracks successful tokenizations against the per-key call budget so the SDK
890
899
  // can proactively refresh the wax key after it has been consumed rather than
891
900
  // waiting for the next createToken() call to fail.
@@ -905,13 +914,14 @@ class OzVault {
905
914
  this.resolvedAppearance = resolveAppearance(options.appearance);
906
915
  this.vaultId = `vault-${uuid()}`;
907
916
  this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
917
+ this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
908
918
  this.boundHandleMessage = this.handleMessage.bind(this);
909
919
  window.addEventListener('message', this.boundHandleMessage);
910
920
  this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
911
921
  document.addEventListener('visibilitychange', this.boundHandleVisibility);
912
922
  this.mountTokenizerFrame();
913
923
  if (options.onLoadError) {
914
- const timeout = (_c = options.loadTimeoutMs) !== null && _c !== void 0 ? _c : 10000;
924
+ const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
915
925
  this.loadErrorTimeoutId = setTimeout(() => {
916
926
  this.loadErrorTimeoutId = null;
917
927
  if (!this._destroyed && !this.tokenizerReady) {
@@ -921,6 +931,7 @@ class OzVault {
921
931
  }
922
932
  this._onWaxRefresh = options.onWaxRefresh;
923
933
  this._onReady = options.onReady;
934
+ this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
924
935
  }
925
936
  /**
926
937
  * Creates and returns a ready `OzVault` instance.
@@ -990,6 +1001,7 @@ class OzVault {
990
1001
  if (vault.tokenizerReady) {
991
1002
  vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
992
1003
  }
1004
+ vault.log('wax key received — vault ready');
993
1005
  return vault;
994
1006
  }
995
1007
  /**
@@ -1131,8 +1143,13 @@ class OzVault {
1131
1143
  const readyBankElements = [accountEl, routingEl];
1132
1144
  this._tokenizing = 'bank';
1133
1145
  const requestId = `req-${uuid()}`;
1146
+ this.log('createBankToken() called');
1134
1147
  return new Promise((resolve, reject) => {
1135
- const cleanup = () => { this._tokenizing = null; };
1148
+ const resetCountAtStart = this._resetCount;
1149
+ const cleanup = () => {
1150
+ if (this._resetCount === resetCountAtStart)
1151
+ this._tokenizing = null;
1152
+ };
1136
1153
  this.bankTokenizeResolvers.set(requestId, {
1137
1154
  resolve: (v) => { cleanup(); resolve(v); },
1138
1155
  reject: (e) => { cleanup(); reject(e); },
@@ -1143,6 +1160,7 @@ class OzVault {
1143
1160
  });
1144
1161
  try {
1145
1162
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1163
+ const bankTokenizeStartMs = Date.now();
1146
1164
  this.sendToTokenizer({
1147
1165
  type: 'OZ_BANK_TOKENIZE',
1148
1166
  requestId,
@@ -1153,6 +1171,7 @@ class OzVault {
1153
1171
  lastName: options.lastName.trim(),
1154
1172
  fieldCount: readyBankElements.length,
1155
1173
  }, bankChannels.map(ch => ch.port1));
1174
+ this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
1156
1175
  readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
1157
1176
  const bankTimeoutId = setTimeout(() => {
1158
1177
  if (this.bankTokenizeResolvers.has(requestId)) {
@@ -1163,8 +1182,10 @@ class OzVault {
1163
1182
  }
1164
1183
  }, 30000);
1165
1184
  const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
1166
- if (bankPendingEntry)
1185
+ if (bankPendingEntry) {
1167
1186
  bankPendingEntry.timeoutId = bankTimeoutId;
1187
+ bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
1188
+ }
1168
1189
  }
1169
1190
  catch (err) {
1170
1191
  this.bankTokenizeResolvers.delete(requestId);
@@ -1235,8 +1256,15 @@ class OzVault {
1235
1256
  }
1236
1257
  this._tokenizing = 'card';
1237
1258
  const requestId = `req-${uuid()}`;
1259
+ this.log('createToken() called');
1238
1260
  return new Promise((resolve, reject) => {
1239
- const cleanup = () => { this._tokenizing = null; };
1261
+ // Capture the reset generation so cleanup() only zeros _tokenizing when it
1262
+ // still belongs to this invocation — not a newer one that started after a reset.
1263
+ const resetCountAtStart = this._resetCount;
1264
+ const cleanup = () => {
1265
+ if (this._resetCount === resetCountAtStart)
1266
+ this._tokenizing = null;
1267
+ };
1240
1268
  this.tokenizeResolvers.set(requestId, {
1241
1269
  resolve: (v) => { cleanup(); resolve(v); },
1242
1270
  reject: (e) => { cleanup(); reject(e); },
@@ -1249,6 +1277,7 @@ class OzVault {
1249
1277
  try {
1250
1278
  // Tell tokenizer frame to expect N field values, then tokenize
1251
1279
  const cardChannels = readyElements.map(() => new MessageChannel());
1280
+ const tokenizeStartMs = Date.now();
1252
1281
  this.sendToTokenizer({
1253
1282
  type: 'OZ_TOKENIZE',
1254
1283
  requestId,
@@ -1259,6 +1288,11 @@ class OzVault {
1259
1288
  lastName,
1260
1289
  fieldCount: readyElements.length,
1261
1290
  }, cardChannels.map(ch => ch.port1));
1291
+ this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
1292
+ // Store start time for elapsed-ms logging on result
1293
+ const cardEntry = this.tokenizeResolvers.get(requestId);
1294
+ if (cardEntry)
1295
+ cardEntry.tokenizeStartMs = tokenizeStartMs;
1262
1296
  // Tell each ready element frame to send its raw value to the tokenizer
1263
1297
  readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
1264
1298
  const cardTimeoutId = setTimeout(() => {
@@ -1280,6 +1314,63 @@ class OzVault {
1280
1314
  }
1281
1315
  });
1282
1316
  }
1317
+ /**
1318
+ * Clears all mounted element fields without tearing down the vault.
1319
+ *
1320
+ * Call this after a failed payment (e.g. card declined) to let the customer
1321
+ * re-enter their details. The vault instance, tokenizer iframe, wax key, and
1322
+ * tokenization budget counter are all preserved — no network calls are made.
1323
+ *
1324
+ * **Wax key session model:** by design, one wax key covers the full checkout
1325
+ * session. The default `max_tokenize_calls: 3` supports two declined attempts
1326
+ * and one final attempt on the same key. Do not call `vault.destroy()` and
1327
+ * recreate the vault between declines — that unnecessarily re-mints the key
1328
+ * and discards the remaining budget.
1329
+ *
1330
+ * @example
1331
+ * try {
1332
+ * const { token, cvcSession } = await vault.createToken({ billing });
1333
+ * await chargeCard(token, cvcSession);
1334
+ * } catch (err) {
1335
+ * vault.reset(); // clear fields; let customer re-enter
1336
+ * showError(err.message);
1337
+ * }
1338
+ */
1339
+ reset() {
1340
+ if (this._destroyed)
1341
+ return;
1342
+ const cancelling = Boolean(this._tokenizing);
1343
+ this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
1344
+ if (this._tokenizing) {
1345
+ this._tokenizing = null;
1346
+ this._resetCount++;
1347
+ this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1348
+ if (timeoutId != null)
1349
+ clearTimeout(timeoutId);
1350
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1351
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1352
+ });
1353
+ this.tokenizeResolvers.clear();
1354
+ this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
1355
+ if (timeoutId != null)
1356
+ clearTimeout(timeoutId);
1357
+ this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
1358
+ reject(new OzError('Vault was reset while tokenization was in progress.'));
1359
+ });
1360
+ this.bankTokenizeResolvers.clear();
1361
+ }
1362
+ // Clear field values in all mounted element iframes
1363
+ this.elementsByType.forEach(el => el.clear());
1364
+ this.bankElementsByType.forEach(el => el.clear());
1365
+ // Reset per-element completion state so auto-advance starts fresh on re-entry
1366
+ for (const frameId of this.completionState.keys()) {
1367
+ this.completionState.set(frameId, false);
1368
+ }
1369
+ // NOTE: _tokenizeSuccessCount is intentionally NOT reset.
1370
+ // It reflects real server-side wax key budget consumption. Zeroing it
1371
+ // would desync the proactive refresh logic from the vault's state and
1372
+ // risk triggering a mid-session re-mint on what should be a clean retry.
1373
+ }
1283
1374
  /**
1284
1375
  * Tears down the vault: removes all element iframes, the tokenizer iframe,
1285
1376
  * and the global message listener. Call this when the checkout component
@@ -1290,6 +1381,7 @@ class OzVault {
1290
1381
  if (this._destroyed)
1291
1382
  return;
1292
1383
  this._destroyed = true;
1384
+ this.log('destroy() called');
1293
1385
  window.removeEventListener('message', this.boundHandleMessage);
1294
1386
  document.removeEventListener('visibilitychange', this.boundHandleVisibility);
1295
1387
  if (this._pendingMount) {
@@ -1344,13 +1436,17 @@ class OzVault {
1344
1436
  const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
1345
1437
  if (document.hidden) {
1346
1438
  this._hiddenAt = Date.now();
1439
+ this.log('tab hidden');
1347
1440
  }
1348
1441
  else {
1349
- if (this._hiddenAt !== null &&
1350
- Date.now() - this._hiddenAt >= REFRESH_THRESHOLD_MS &&
1351
- this._storedFetchWaxKey &&
1442
+ const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
1443
+ const willRefresh = (this._hiddenAt !== null &&
1444
+ hiddenMs >= REFRESH_THRESHOLD_MS &&
1445
+ Boolean(this._storedFetchWaxKey) &&
1352
1446
  !this._tokenizing &&
1353
- !this._waxRefreshing) {
1447
+ !this._waxRefreshing);
1448
+ this.log('tab visible', { hiddenMs, willRefresh });
1449
+ if (willRefresh) {
1354
1450
  this.refreshWaxKey().catch((err) => {
1355
1451
  // Proactive refresh failure is non-fatal — the reactive path on the
1356
1452
  // next createToken() call will handle it, including the auth retry.
@@ -1360,6 +1456,56 @@ class OzVault {
1360
1456
  this._hiddenAt = null;
1361
1457
  }
1362
1458
  }
1459
+ // ─── Debug ───────────────────────────────────────────────────────────────
1460
+ /**
1461
+ * Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
1462
+ * not set. Never called with sensitive values — callers use presence flags only.
1463
+ */
1464
+ log(message, data) {
1465
+ if (!this._debug)
1466
+ return;
1467
+ if (data !== undefined) {
1468
+ console.log(`[OzVault] ${message}`, data);
1469
+ }
1470
+ else {
1471
+ console.log(`[OzVault] ${message}`);
1472
+ }
1473
+ }
1474
+ /**
1475
+ * Returns a plain-object snapshot of the vault's current internal state.
1476
+ * Safe to attach to bug reports — no wax keys, tokens, or billing data included.
1477
+ *
1478
+ * Available on all vault instances regardless of whether `debug` was enabled.
1479
+ *
1480
+ * @example
1481
+ * console.log(vault.debugState());
1482
+ * // {
1483
+ * // vaultId: 'vault-abc123',
1484
+ * // isReady: true,
1485
+ * // tokenizing: null,
1486
+ * // destroyed: false,
1487
+ * // waxKeyPresent: true,
1488
+ * // elements: ['cardNumber', 'expirationDate', 'cvv'],
1489
+ * // ...
1490
+ * // }
1491
+ */
1492
+ debugState() {
1493
+ return {
1494
+ vaultId: this.vaultId,
1495
+ isReady: this.tokenizerReady,
1496
+ tokenizing: this._tokenizing,
1497
+ destroyed: this._destroyed,
1498
+ waxKeyPresent: Boolean(this.waxKey),
1499
+ tokenizeSuccessCount: this._tokenizeSuccessCount,
1500
+ maxTokenizeCalls: this._maxTokenizeCalls,
1501
+ resetCount: this._resetCount,
1502
+ elements: [...this.elementsByType.keys()],
1503
+ bankElements: [...this.bankElementsByType.keys()],
1504
+ completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
1505
+ pendingTokenizations: this.tokenizeResolvers.size,
1506
+ pendingBankTokenizations: this.bankTokenizeResolvers.size,
1507
+ };
1508
+ }
1363
1509
  mountTokenizerFrame() {
1364
1510
  const mount = () => {
1365
1511
  this._pendingMount = null;
@@ -1371,6 +1517,7 @@ class OzVault {
1371
1517
  iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
1372
1518
  document.body.appendChild(iframe);
1373
1519
  this.tokenizerFrame = iframe;
1520
+ this.log('mounting tokenizer iframe');
1374
1521
  };
1375
1522
  if (document.readyState === 'loading') {
1376
1523
  this._pendingMount = mount;
@@ -1411,6 +1558,7 @@ class OzVault {
1411
1558
  `SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
1412
1559
  'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
1413
1560
  }
1561
+ this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
1414
1562
  }
1415
1563
  // Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
1416
1564
  if (msg.type === 'OZ_CHANGE') {
@@ -1434,6 +1582,7 @@ class OzVault {
1434
1582
  // Require valid too — avoids advancing at 13 digits for unknown-brand cards
1435
1583
  // where isComplete() fires before the user has finished typing.
1436
1584
  const justCompleted = complete && valid && !wasComplete;
1585
+ this.log('field changed', { type: el.type, complete, valid, justCompleted });
1437
1586
  // Sync CVV length when card brand changes
1438
1587
  if (el.type === 'cardNumber') {
1439
1588
  const brand = msg.cardBrand;
@@ -1445,15 +1594,17 @@ class OzVault {
1445
1594
  // Auto-advance focus on completion
1446
1595
  if (justCompleted) {
1447
1596
  if (el.type === 'cardNumber') {
1597
+ this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
1448
1598
  (_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
1449
1599
  }
1450
1600
  else if (el.type === 'expirationDate') {
1601
+ this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
1451
1602
  (_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
1452
1603
  }
1453
1604
  }
1454
1605
  }
1455
1606
  handleTokenizerMessage(msg) {
1456
- var _a, _b, _c;
1607
+ var _a, _b, _c, _d;
1457
1608
  switch (msg.type) {
1458
1609
  case 'OZ_FRAME_READY':
1459
1610
  if (msg.__ozVersion !== PROTOCOL_VERSION) {
@@ -1473,6 +1624,7 @@ class OzVault {
1473
1624
  // sent again from create() once the key is available.
1474
1625
  this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
1475
1626
  (_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
1627
+ this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
1476
1628
  break;
1477
1629
  case 'OZ_TOKEN_RESULT': {
1478
1630
  if (typeof msg.requestId !== 'string' || !msg.requestId) {
@@ -1497,11 +1649,18 @@ class OzVault {
1497
1649
  }
1498
1650
  pending.resolve(Object.assign(Object.assign({ token,
1499
1651
  cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
1652
+ this.log('token received', {
1653
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1654
+ tokenPresent: true,
1655
+ cvcSessionPresent: true,
1656
+ cardMetadataPresent: Boolean(card),
1657
+ });
1500
1658
  // Increment the per-key success counter and proactively refresh once
1501
1659
  // the budget is exhausted so the next createToken() call uses a fresh
1502
1660
  // key without waiting for a vault rejection.
1503
1661
  this._tokenizeSuccessCount++;
1504
1662
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1663
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1505
1664
  this.refreshWaxKey().catch((err) => {
1506
1665
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1507
1666
  });
@@ -1521,14 +1680,25 @@ class OzVault {
1521
1680
  this.tokenizeResolvers.delete(msg.requestId);
1522
1681
  if (pending.timeoutId != null)
1523
1682
  clearTimeout(pending.timeoutId);
1683
+ const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
1684
+ this.log('token error', { errorCode, willRefresh });
1524
1685
  // Auto-refresh: if the wax key expired or was consumed and we haven't
1525
1686
  // already retried for this request, transparently re-mint and retry.
1526
- if (this.isRefreshableAuthError(errorCode, raw) && !pending.retried && this._storedFetchWaxKey) {
1687
+ if (willRefresh) {
1688
+ const resetCountAtRetry = this._resetCount;
1527
1689
  this.refreshWaxKey().then(() => {
1528
1690
  if (this._destroyed) {
1529
1691
  pending.reject(new OzError('Vault destroyed during wax key refresh.'));
1530
1692
  return;
1531
1693
  }
1694
+ if (this._resetCount !== resetCountAtRetry) {
1695
+ // reset() was called while the wax key was refreshing — the fields
1696
+ // have been cleared and _tokenizing was zeroed. Reject the original
1697
+ // promise so it doesn't stay pending, and bail out without starting
1698
+ // a new retry (which would tokenize against empty fields).
1699
+ pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1700
+ return;
1701
+ }
1532
1702
  const newRequestId = `req-${uuid()}`;
1533
1703
  // _tokenizing is still 'card' (cleanup() hasn't been called yet)
1534
1704
  this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
@@ -1575,11 +1745,16 @@ class OzVault {
1575
1745
  if (bankPending.timeoutId != null)
1576
1746
  clearTimeout(bankPending.timeoutId);
1577
1747
  if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
1748
+ const resetCountAtRetry = this._resetCount;
1578
1749
  this.refreshWaxKey().then(() => {
1579
1750
  if (this._destroyed) {
1580
1751
  bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
1581
1752
  return;
1582
1753
  }
1754
+ if (this._resetCount !== resetCountAtRetry) {
1755
+ bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
1756
+ return;
1757
+ }
1583
1758
  const newRequestId = `req-${uuid()}`;
1584
1759
  this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
1585
1760
  try {
@@ -1637,9 +1812,15 @@ class OzVault {
1637
1812
  }
1638
1813
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
1639
1814
  pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
1815
+ this.log('bank token received', {
1816
+ elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
1817
+ tokenPresent: true,
1818
+ bankMetadataPresent: Boolean(bank),
1819
+ });
1640
1820
  // Same proactive refresh logic as card tokenization.
1641
1821
  this._tokenizeSuccessCount++;
1642
1822
  if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
1823
+ this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
1643
1824
  this.refreshWaxKey().catch((err) => {
1644
1825
  console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
1645
1826
  });
@@ -1695,6 +1876,7 @@ class OzVault {
1695
1876
  }
1696
1877
  const newSessionId = uuid();
1697
1878
  (_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
1879
+ this.log('wax key refresh started');
1698
1880
  this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
1699
1881
  .then(newWaxKey => {
1700
1882
  if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
@@ -1708,6 +1890,11 @@ class OzVault {
1708
1890
  if (!this._destroyed && this.tokenizerReady) {
1709
1891
  this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
1710
1892
  }
1893
+ this.log('wax key refresh succeeded');
1894
+ })
1895
+ .catch((err) => {
1896
+ this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
1897
+ throw err;
1711
1898
  })
1712
1899
  .finally(() => {
1713
1900
  this._waxRefreshing = null;
@@ -1815,7 +2002,7 @@ const OzContext = react.createContext({
1815
2002
  * All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
1816
2003
  * rendered inside this provider.
1817
2004
  */
1818
- function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, children }) {
2005
+ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, debug, children }) {
1819
2006
  const [vault, setVault] = react.useState(null);
1820
2007
  const [initError, setInitError] = react.useState(null);
1821
2008
  const [mountedCount, setMountedCount] = react.useState(0);
@@ -1882,7 +2069,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
1882
2069
  var _a;
1883
2070
  Promise.resolve().then(() => setTokenizeCount(0));
1884
2071
  (_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
1885
- } }), (onReadyRef.current ? { onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } } : {})), (maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), abortController.signal).then(v => {
2072
+ }, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
1886
2073
  if (cancelled) {
1887
2074
  v.destroy();
1888
2075
  return;
@@ -1915,7 +2102,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
1915
2102
  setVault(null);
1916
2103
  setInitError(null);
1917
2104
  };
1918
- }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls]);
2105
+ }, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls, debug]);
1919
2106
  const notifyMount = react.useCallback(() => setMountedCount(n => n + 1), []);
1920
2107
  const notifyReady = react.useCallback(() => setReadyCount(n => n + 1), []);
1921
2108
  const notifyUnmount = react.useCallback(() => {
@@ -1948,8 +2135,11 @@ function useOzElements() {
1948
2135
  notifyTokenize();
1949
2136
  return result;
1950
2137
  }, [vault, notifyTokenize]);
2138
+ const reset = react.useCallback(() => {
2139
+ vault === null || vault === void 0 ? void 0 : vault.reset();
2140
+ }, [vault]);
1951
2141
  const ready = vault !== null && vault.isReady && mountedCount > 0 && readyCount >= mountedCount;
1952
- return { createToken, createBankToken, ready, initError, tokenizeCount };
2142
+ return { createToken, createBankToken, reset, ready, initError, tokenizeCount };
1953
2143
  }
1954
2144
  const SKELETON_STYLE = {
1955
2145
  height: 46,
@@ -2055,7 +2245,7 @@ const OzCvv = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "cvv"
2055
2245
  * - Per-field ready tracking: creates one stable handler per named field; fires the
2056
2246
  * `onReady` callback once all `fieldNames.length` fields have reported ready
2057
2247
  * - Error state
2058
- * - Layout helpers: `gapStr`, `resolvedLabelStyle`, `renderLabel`
2248
+ * - Layout helpers: `gapStr`, `renderLabel`
2059
2249
  *
2060
2250
  * @internal — not exported; used only by OzCard and OzBankCard.
2061
2251
  */
@@ -2107,7 +2297,7 @@ function useCardBase({ vault, fieldNames, onChange, onReady, onFocus, onBlur, ga
2107
2297
  const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
2108
2298
  const resolvedLabelStyle = react.useMemo(() => (labelStyle ? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE), [labelStyle]);
2109
2299
  const renderLabel = react.useCallback((text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle), [labelClassName, resolvedLabelStyle]);
2110
- return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, resolvedLabelStyle, renderLabel };
2300
+ return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel };
2111
2301
  }
2112
2302
  const DEFAULT_ERROR_STYLE = {
2113
2303
  color: '#dc2626',