@ozura/elements 1.0.2 → 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/LICENSE +21 -21
- package/README.md +167 -21
- package/dist/frame/element-frame.js +92 -20
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.html +1 -1
- package/dist/frame/tokenizer-frame.js +20 -4
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/oz-elements.esm.js +226 -15
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +226 -15
- package/dist/oz-elements.umd.js.map +1 -1
- package/dist/react/frame/elementFrame.d.ts +70 -1
- package/dist/react/frame/protocol.d.ts +12 -0
- package/dist/react/index.cjs.js +326 -123
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +327 -124
- 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 +20 -0
- package/dist/server/frame/elementFrame.d.ts +70 -1
- package/dist/server/frame/protocol.d.ts +12 -0
- package/dist/server/index.cjs.js +2 -2
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +2 -2
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/sdk/OzVault.d.ts +49 -0
- package/dist/server/types/index.d.ts +20 -0
- package/dist/types/frame/elementFrame.d.ts +70 -1
- package/dist/types/frame/protocol.d.ts +12 -0
- package/dist/types/sdk/OzVault.d.ts +49 -0
- package/dist/types/types/index.d.ts +20 -0
- package/package.json +1 -1
package/dist/oz-elements.umd.js
CHANGED
|
@@ -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 ${
|
|
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 = (
|
|
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` });
|
|
@@ -877,13 +883,26 @@
|
|
|
877
883
|
return { valid: errors.length === 0, errors, normalized };
|
|
878
884
|
}
|
|
879
885
|
|
|
886
|
+
/**
|
|
887
|
+
* Shared postMessage protocol constants.
|
|
888
|
+
*
|
|
889
|
+
* PROTOCOL_VERSION must be incremented any time a breaking change is made to
|
|
890
|
+
* the postMessage message shape (new required fields, renamed types, removed
|
|
891
|
+
* fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
|
|
892
|
+
* messages and warns when the frame and SDK are out of sync.
|
|
893
|
+
*
|
|
894
|
+
* Non-breaking additions (new optional fields, new message types that old
|
|
895
|
+
* frames can safely ignore) do NOT require a version bump.
|
|
896
|
+
*/
|
|
897
|
+
const PROTOCOL_VERSION = 1;
|
|
898
|
+
|
|
880
899
|
function isCardMetadata(v) {
|
|
881
900
|
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
882
901
|
}
|
|
883
902
|
function isBankAccountMetadata(v) {
|
|
884
903
|
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
885
904
|
}
|
|
886
|
-
const DEFAULT_FRAME_BASE_URL = "https://
|
|
905
|
+
const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
|
|
887
906
|
/**
|
|
888
907
|
* The main entry point for OzElements. Creates and manages iframe-based
|
|
889
908
|
* card input elements that keep raw card data isolated from the merchant page.
|
|
@@ -916,7 +935,7 @@
|
|
|
916
935
|
* @internal
|
|
917
936
|
*/
|
|
918
937
|
constructor(options, waxKey, tokenizationSessionId) {
|
|
919
|
-
var _a, _b, _c;
|
|
938
|
+
var _a, _b, _c, _d;
|
|
920
939
|
this.elements = new Map();
|
|
921
940
|
this.elementsByType = new Map();
|
|
922
941
|
this.bankElementsByType = new Map();
|
|
@@ -929,6 +948,9 @@
|
|
|
929
948
|
this.tokenizerReady = false;
|
|
930
949
|
this._tokenizing = null;
|
|
931
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;
|
|
932
954
|
// Tracks successful tokenizations against the per-key call budget so the SDK
|
|
933
955
|
// can proactively refresh the wax key after it has been consumed rather than
|
|
934
956
|
// waiting for the next createToken() call to fail.
|
|
@@ -948,13 +970,14 @@
|
|
|
948
970
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
949
971
|
this.vaultId = `vault-${uuid()}`;
|
|
950
972
|
this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
|
|
973
|
+
this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
|
|
951
974
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
952
975
|
window.addEventListener('message', this.boundHandleMessage);
|
|
953
976
|
this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
|
|
954
977
|
document.addEventListener('visibilitychange', this.boundHandleVisibility);
|
|
955
978
|
this.mountTokenizerFrame();
|
|
956
979
|
if (options.onLoadError) {
|
|
957
|
-
const timeout = (
|
|
980
|
+
const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
|
|
958
981
|
this.loadErrorTimeoutId = setTimeout(() => {
|
|
959
982
|
this.loadErrorTimeoutId = null;
|
|
960
983
|
if (!this._destroyed && !this.tokenizerReady) {
|
|
@@ -964,6 +987,7 @@
|
|
|
964
987
|
}
|
|
965
988
|
this._onWaxRefresh = options.onWaxRefresh;
|
|
966
989
|
this._onReady = options.onReady;
|
|
990
|
+
this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
967
991
|
}
|
|
968
992
|
/**
|
|
969
993
|
* Creates and returns a ready `OzVault` instance.
|
|
@@ -1033,6 +1057,7 @@
|
|
|
1033
1057
|
if (vault.tokenizerReady) {
|
|
1034
1058
|
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
1035
1059
|
}
|
|
1060
|
+
vault.log('wax key received — vault ready');
|
|
1036
1061
|
return vault;
|
|
1037
1062
|
}
|
|
1038
1063
|
/**
|
|
@@ -1174,8 +1199,13 @@
|
|
|
1174
1199
|
const readyBankElements = [accountEl, routingEl];
|
|
1175
1200
|
this._tokenizing = 'bank';
|
|
1176
1201
|
const requestId = `req-${uuid()}`;
|
|
1202
|
+
this.log('createBankToken() called');
|
|
1177
1203
|
return new Promise((resolve, reject) => {
|
|
1178
|
-
const
|
|
1204
|
+
const resetCountAtStart = this._resetCount;
|
|
1205
|
+
const cleanup = () => {
|
|
1206
|
+
if (this._resetCount === resetCountAtStart)
|
|
1207
|
+
this._tokenizing = null;
|
|
1208
|
+
};
|
|
1179
1209
|
this.bankTokenizeResolvers.set(requestId, {
|
|
1180
1210
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
1181
1211
|
reject: (e) => { cleanup(); reject(e); },
|
|
@@ -1186,6 +1216,7 @@
|
|
|
1186
1216
|
});
|
|
1187
1217
|
try {
|
|
1188
1218
|
const bankChannels = readyBankElements.map(() => new MessageChannel());
|
|
1219
|
+
const bankTokenizeStartMs = Date.now();
|
|
1189
1220
|
this.sendToTokenizer({
|
|
1190
1221
|
type: 'OZ_BANK_TOKENIZE',
|
|
1191
1222
|
requestId,
|
|
@@ -1196,6 +1227,7 @@
|
|
|
1196
1227
|
lastName: options.lastName.trim(),
|
|
1197
1228
|
fieldCount: readyBankElements.length,
|
|
1198
1229
|
}, bankChannels.map(ch => ch.port1));
|
|
1230
|
+
this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
|
|
1199
1231
|
readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
|
|
1200
1232
|
const bankTimeoutId = setTimeout(() => {
|
|
1201
1233
|
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
@@ -1206,8 +1238,10 @@
|
|
|
1206
1238
|
}
|
|
1207
1239
|
}, 30000);
|
|
1208
1240
|
const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
|
|
1209
|
-
if (bankPendingEntry)
|
|
1241
|
+
if (bankPendingEntry) {
|
|
1210
1242
|
bankPendingEntry.timeoutId = bankTimeoutId;
|
|
1243
|
+
bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
|
|
1244
|
+
}
|
|
1211
1245
|
}
|
|
1212
1246
|
catch (err) {
|
|
1213
1247
|
this.bankTokenizeResolvers.delete(requestId);
|
|
@@ -1278,8 +1312,15 @@
|
|
|
1278
1312
|
}
|
|
1279
1313
|
this._tokenizing = 'card';
|
|
1280
1314
|
const requestId = `req-${uuid()}`;
|
|
1315
|
+
this.log('createToken() called');
|
|
1281
1316
|
return new Promise((resolve, reject) => {
|
|
1282
|
-
|
|
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
|
+
};
|
|
1283
1324
|
this.tokenizeResolvers.set(requestId, {
|
|
1284
1325
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
1285
1326
|
reject: (e) => { cleanup(); reject(e); },
|
|
@@ -1292,6 +1333,7 @@
|
|
|
1292
1333
|
try {
|
|
1293
1334
|
// Tell tokenizer frame to expect N field values, then tokenize
|
|
1294
1335
|
const cardChannels = readyElements.map(() => new MessageChannel());
|
|
1336
|
+
const tokenizeStartMs = Date.now();
|
|
1295
1337
|
this.sendToTokenizer({
|
|
1296
1338
|
type: 'OZ_TOKENIZE',
|
|
1297
1339
|
requestId,
|
|
@@ -1302,6 +1344,11 @@
|
|
|
1302
1344
|
lastName,
|
|
1303
1345
|
fieldCount: readyElements.length,
|
|
1304
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;
|
|
1305
1352
|
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1306
1353
|
readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
|
|
1307
1354
|
const cardTimeoutId = setTimeout(() => {
|
|
@@ -1323,6 +1370,63 @@
|
|
|
1323
1370
|
}
|
|
1324
1371
|
});
|
|
1325
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
|
+
}
|
|
1326
1430
|
/**
|
|
1327
1431
|
* Tears down the vault: removes all element iframes, the tokenizer iframe,
|
|
1328
1432
|
* and the global message listener. Call this when the checkout component
|
|
@@ -1333,6 +1437,7 @@
|
|
|
1333
1437
|
if (this._destroyed)
|
|
1334
1438
|
return;
|
|
1335
1439
|
this._destroyed = true;
|
|
1440
|
+
this.log('destroy() called');
|
|
1336
1441
|
window.removeEventListener('message', this.boundHandleMessage);
|
|
1337
1442
|
document.removeEventListener('visibilitychange', this.boundHandleVisibility);
|
|
1338
1443
|
if (this._pendingMount) {
|
|
@@ -1387,13 +1492,17 @@
|
|
|
1387
1492
|
const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
|
|
1388
1493
|
if (document.hidden) {
|
|
1389
1494
|
this._hiddenAt = Date.now();
|
|
1495
|
+
this.log('tab hidden');
|
|
1390
1496
|
}
|
|
1391
1497
|
else {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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) &&
|
|
1395
1502
|
!this._tokenizing &&
|
|
1396
|
-
!this._waxRefreshing)
|
|
1503
|
+
!this._waxRefreshing);
|
|
1504
|
+
this.log('tab visible', { hiddenMs, willRefresh });
|
|
1505
|
+
if (willRefresh) {
|
|
1397
1506
|
this.refreshWaxKey().catch((err) => {
|
|
1398
1507
|
// Proactive refresh failure is non-fatal — the reactive path on the
|
|
1399
1508
|
// next createToken() call will handle it, including the auth retry.
|
|
@@ -1403,6 +1512,56 @@
|
|
|
1403
1512
|
this._hiddenAt = null;
|
|
1404
1513
|
}
|
|
1405
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
|
+
}
|
|
1406
1565
|
mountTokenizerFrame() {
|
|
1407
1566
|
const mount = () => {
|
|
1408
1567
|
this._pendingMount = null;
|
|
@@ -1414,6 +1573,7 @@
|
|
|
1414
1573
|
iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
|
|
1415
1574
|
document.body.appendChild(iframe);
|
|
1416
1575
|
this.tokenizerFrame = iframe;
|
|
1576
|
+
this.log('mounting tokenizer iframe');
|
|
1417
1577
|
};
|
|
1418
1578
|
if (document.readyState === 'loading') {
|
|
1419
1579
|
this._pendingMount = mount;
|
|
@@ -1449,6 +1609,12 @@
|
|
|
1449
1609
|
// the previous session and justCompleted never fires, breaking auto-advance.
|
|
1450
1610
|
if (msg.type === 'OZ_FRAME_READY') {
|
|
1451
1611
|
this.completionState.set(frameId, false);
|
|
1612
|
+
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
1613
|
+
console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
|
|
1614
|
+
`SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
|
|
1615
|
+
'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
|
|
1616
|
+
}
|
|
1617
|
+
this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
|
|
1452
1618
|
}
|
|
1453
1619
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1454
1620
|
if (msg.type === 'OZ_CHANGE') {
|
|
@@ -1472,6 +1638,7 @@
|
|
|
1472
1638
|
// Require valid too — avoids advancing at 13 digits for unknown-brand cards
|
|
1473
1639
|
// where isComplete() fires before the user has finished typing.
|
|
1474
1640
|
const justCompleted = complete && valid && !wasComplete;
|
|
1641
|
+
this.log('field changed', { type: el.type, complete, valid, justCompleted });
|
|
1475
1642
|
// Sync CVV length when card brand changes
|
|
1476
1643
|
if (el.type === 'cardNumber') {
|
|
1477
1644
|
const brand = msg.cardBrand;
|
|
@@ -1483,17 +1650,25 @@
|
|
|
1483
1650
|
// Auto-advance focus on completion
|
|
1484
1651
|
if (justCompleted) {
|
|
1485
1652
|
if (el.type === 'cardNumber') {
|
|
1653
|
+
this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
|
|
1486
1654
|
(_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
|
|
1487
1655
|
}
|
|
1488
1656
|
else if (el.type === 'expirationDate') {
|
|
1657
|
+
this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
|
|
1489
1658
|
(_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
|
|
1490
1659
|
}
|
|
1491
1660
|
}
|
|
1492
1661
|
}
|
|
1493
1662
|
handleTokenizerMessage(msg) {
|
|
1494
|
-
var _a, _b, _c;
|
|
1663
|
+
var _a, _b, _c, _d;
|
|
1495
1664
|
switch (msg.type) {
|
|
1496
1665
|
case 'OZ_FRAME_READY':
|
|
1666
|
+
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
1667
|
+
console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
|
|
1668
|
+
`tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
|
|
1669
|
+
'This usually means the deployed frame files are stale. ' +
|
|
1670
|
+
'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
|
|
1671
|
+
}
|
|
1497
1672
|
this.tokenizerReady = true;
|
|
1498
1673
|
if (this.loadErrorTimeoutId != null) {
|
|
1499
1674
|
clearTimeout(this.loadErrorTimeoutId);
|
|
@@ -1505,6 +1680,7 @@
|
|
|
1505
1680
|
// sent again from create() once the key is available.
|
|
1506
1681
|
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1507
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 });
|
|
1508
1684
|
break;
|
|
1509
1685
|
case 'OZ_TOKEN_RESULT': {
|
|
1510
1686
|
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
@@ -1529,11 +1705,18 @@
|
|
|
1529
1705
|
}
|
|
1530
1706
|
pending.resolve(Object.assign(Object.assign({ token,
|
|
1531
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
|
+
});
|
|
1532
1714
|
// Increment the per-key success counter and proactively refresh once
|
|
1533
1715
|
// the budget is exhausted so the next createToken() call uses a fresh
|
|
1534
1716
|
// key without waiting for a vault rejection.
|
|
1535
1717
|
this._tokenizeSuccessCount++;
|
|
1536
1718
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1719
|
+
this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1537
1720
|
this.refreshWaxKey().catch((err) => {
|
|
1538
1721
|
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1539
1722
|
});
|
|
@@ -1553,14 +1736,25 @@
|
|
|
1553
1736
|
this.tokenizeResolvers.delete(msg.requestId);
|
|
1554
1737
|
if (pending.timeoutId != null)
|
|
1555
1738
|
clearTimeout(pending.timeoutId);
|
|
1739
|
+
const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
|
|
1740
|
+
this.log('token error', { errorCode, willRefresh });
|
|
1556
1741
|
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1557
1742
|
// already retried for this request, transparently re-mint and retry.
|
|
1558
|
-
if (
|
|
1743
|
+
if (willRefresh) {
|
|
1744
|
+
const resetCountAtRetry = this._resetCount;
|
|
1559
1745
|
this.refreshWaxKey().then(() => {
|
|
1560
1746
|
if (this._destroyed) {
|
|
1561
1747
|
pending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1562
1748
|
return;
|
|
1563
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
|
+
}
|
|
1564
1758
|
const newRequestId = `req-${uuid()}`;
|
|
1565
1759
|
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1566
1760
|
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
@@ -1607,11 +1801,16 @@
|
|
|
1607
1801
|
if (bankPending.timeoutId != null)
|
|
1608
1802
|
clearTimeout(bankPending.timeoutId);
|
|
1609
1803
|
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1804
|
+
const resetCountAtRetry = this._resetCount;
|
|
1610
1805
|
this.refreshWaxKey().then(() => {
|
|
1611
1806
|
if (this._destroyed) {
|
|
1612
1807
|
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1613
1808
|
return;
|
|
1614
1809
|
}
|
|
1810
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1811
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1615
1814
|
const newRequestId = `req-${uuid()}`;
|
|
1616
1815
|
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1617
1816
|
try {
|
|
@@ -1669,9 +1868,15 @@
|
|
|
1669
1868
|
}
|
|
1670
1869
|
const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
|
|
1671
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
|
+
});
|
|
1672
1876
|
// Same proactive refresh logic as card tokenization.
|
|
1673
1877
|
this._tokenizeSuccessCount++;
|
|
1674
1878
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1879
|
+
this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1675
1880
|
this.refreshWaxKey().catch((err) => {
|
|
1676
1881
|
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1677
1882
|
});
|
|
@@ -1727,6 +1932,7 @@
|
|
|
1727
1932
|
}
|
|
1728
1933
|
const newSessionId = uuid();
|
|
1729
1934
|
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
1935
|
+
this.log('wax key refresh started');
|
|
1730
1936
|
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
1731
1937
|
.then(newWaxKey => {
|
|
1732
1938
|
if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
|
|
@@ -1740,6 +1946,11 @@
|
|
|
1740
1946
|
if (!this._destroyed && this.tokenizerReady) {
|
|
1741
1947
|
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
1742
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;
|
|
1743
1954
|
})
|
|
1744
1955
|
.finally(() => {
|
|
1745
1956
|
this._waxRefreshing = null;
|