@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.
- package/README.md +167 -21
- package/dist/frame/element-frame.js +56 -15
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.js +4 -1
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/oz-elements.esm.js +201 -14
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +201 -14
- package/dist/oz-elements.umd.js.map +1 -1
- package/dist/react/frame/elementFrame.d.ts +9 -0
- package/dist/react/index.cjs.js +210 -20
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +210 -20
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/react/index.d.ts +28 -1
- package/dist/react/sdk/OzVault.d.ts +49 -0
- package/dist/react/types/index.d.ts +18 -0
- package/dist/server/frame/elementFrame.d.ts +9 -0
- package/dist/server/sdk/OzVault.d.ts +49 -0
- package/dist/server/types/index.d.ts +18 -0
- package/dist/types/frame/elementFrame.d.ts +9 -0
- package/dist/types/sdk/OzVault.d.ts +49 -0
- package/dist/types/types/index.d.ts +18 -0
- package/package.json +1 -1
package/dist/react/index.cjs.js
CHANGED
|
@@ -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 ${
|
|
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 = (
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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 (
|
|
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
|
-
}
|
|
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`, `
|
|
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,
|
|
2300
|
+
return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel };
|
|
2111
2301
|
}
|
|
2112
2302
|
const DEFAULT_ERROR_STYLE = {
|
|
2113
2303
|
color: '#dc2626',
|