@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.esm.js
CHANGED
|
@@ -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 ${
|
|
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 = (
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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 (
|
|
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
|
-
}
|
|
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`, `
|
|
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,
|
|
2298
|
+
return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel };
|
|
2109
2299
|
}
|
|
2110
2300
|
const DEFAULT_ERROR_STYLE = {
|
|
2111
2301
|
color: '#dc2626',
|