@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/react/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import { createContext, useContext, useRef,
|
|
2
|
+
import { createContext, useContext, useRef, useCallback, useState, useMemo, useEffect } from 'react';
|
|
3
3
|
|
|
4
4
|
const THEME_DEFAULT = {
|
|
5
5
|
base: {
|
|
@@ -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` });
|
|
@@ -819,13 +825,26 @@ function validateBilling(billing) {
|
|
|
819
825
|
return { valid: errors.length === 0, errors, normalized };
|
|
820
826
|
}
|
|
821
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Shared postMessage protocol constants.
|
|
830
|
+
*
|
|
831
|
+
* PROTOCOL_VERSION must be incremented any time a breaking change is made to
|
|
832
|
+
* the postMessage message shape (new required fields, renamed types, removed
|
|
833
|
+
* fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
|
|
834
|
+
* messages and warns when the frame and SDK are out of sync.
|
|
835
|
+
*
|
|
836
|
+
* Non-breaking additions (new optional fields, new message types that old
|
|
837
|
+
* frames can safely ignore) do NOT require a version bump.
|
|
838
|
+
*/
|
|
839
|
+
const PROTOCOL_VERSION = 1;
|
|
840
|
+
|
|
822
841
|
function isCardMetadata(v) {
|
|
823
842
|
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
824
843
|
}
|
|
825
844
|
function isBankAccountMetadata(v) {
|
|
826
845
|
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
827
846
|
}
|
|
828
|
-
const DEFAULT_FRAME_BASE_URL = "https://
|
|
847
|
+
const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
|
|
829
848
|
/**
|
|
830
849
|
* The main entry point for OzElements. Creates and manages iframe-based
|
|
831
850
|
* card input elements that keep raw card data isolated from the merchant page.
|
|
@@ -858,7 +877,7 @@ class OzVault {
|
|
|
858
877
|
* @internal
|
|
859
878
|
*/
|
|
860
879
|
constructor(options, waxKey, tokenizationSessionId) {
|
|
861
|
-
var _a, _b, _c;
|
|
880
|
+
var _a, _b, _c, _d;
|
|
862
881
|
this.elements = new Map();
|
|
863
882
|
this.elementsByType = new Map();
|
|
864
883
|
this.bankElementsByType = new Map();
|
|
@@ -871,6 +890,9 @@ class OzVault {
|
|
|
871
890
|
this.tokenizerReady = false;
|
|
872
891
|
this._tokenizing = null;
|
|
873
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;
|
|
874
896
|
// Tracks successful tokenizations against the per-key call budget so the SDK
|
|
875
897
|
// can proactively refresh the wax key after it has been consumed rather than
|
|
876
898
|
// waiting for the next createToken() call to fail.
|
|
@@ -890,13 +912,14 @@ class OzVault {
|
|
|
890
912
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
891
913
|
this.vaultId = `vault-${uuid()}`;
|
|
892
914
|
this._maxTokenizeCalls = (_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3;
|
|
915
|
+
this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
|
|
893
916
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
894
917
|
window.addEventListener('message', this.boundHandleMessage);
|
|
895
918
|
this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
|
|
896
919
|
document.addEventListener('visibilitychange', this.boundHandleVisibility);
|
|
897
920
|
this.mountTokenizerFrame();
|
|
898
921
|
if (options.onLoadError) {
|
|
899
|
-
const timeout = (
|
|
922
|
+
const timeout = (_d = options.loadTimeoutMs) !== null && _d !== void 0 ? _d : 10000;
|
|
900
923
|
this.loadErrorTimeoutId = setTimeout(() => {
|
|
901
924
|
this.loadErrorTimeoutId = null;
|
|
902
925
|
if (!this._destroyed && !this.tokenizerReady) {
|
|
@@ -906,6 +929,7 @@ class OzVault {
|
|
|
906
929
|
}
|
|
907
930
|
this._onWaxRefresh = options.onWaxRefresh;
|
|
908
931
|
this._onReady = options.onReady;
|
|
932
|
+
this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
909
933
|
}
|
|
910
934
|
/**
|
|
911
935
|
* Creates and returns a ready `OzVault` instance.
|
|
@@ -975,6 +999,7 @@ class OzVault {
|
|
|
975
999
|
if (vault.tokenizerReady) {
|
|
976
1000
|
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
977
1001
|
}
|
|
1002
|
+
vault.log('wax key received — vault ready');
|
|
978
1003
|
return vault;
|
|
979
1004
|
}
|
|
980
1005
|
/**
|
|
@@ -1116,8 +1141,13 @@ class OzVault {
|
|
|
1116
1141
|
const readyBankElements = [accountEl, routingEl];
|
|
1117
1142
|
this._tokenizing = 'bank';
|
|
1118
1143
|
const requestId = `req-${uuid()}`;
|
|
1144
|
+
this.log('createBankToken() called');
|
|
1119
1145
|
return new Promise((resolve, reject) => {
|
|
1120
|
-
const
|
|
1146
|
+
const resetCountAtStart = this._resetCount;
|
|
1147
|
+
const cleanup = () => {
|
|
1148
|
+
if (this._resetCount === resetCountAtStart)
|
|
1149
|
+
this._tokenizing = null;
|
|
1150
|
+
};
|
|
1121
1151
|
this.bankTokenizeResolvers.set(requestId, {
|
|
1122
1152
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
1123
1153
|
reject: (e) => { cleanup(); reject(e); },
|
|
@@ -1128,6 +1158,7 @@ class OzVault {
|
|
|
1128
1158
|
});
|
|
1129
1159
|
try {
|
|
1130
1160
|
const bankChannels = readyBankElements.map(() => new MessageChannel());
|
|
1161
|
+
const bankTokenizeStartMs = Date.now();
|
|
1131
1162
|
this.sendToTokenizer({
|
|
1132
1163
|
type: 'OZ_BANK_TOKENIZE',
|
|
1133
1164
|
requestId,
|
|
@@ -1138,6 +1169,7 @@ class OzVault {
|
|
|
1138
1169
|
lastName: options.lastName.trim(),
|
|
1139
1170
|
fieldCount: readyBankElements.length,
|
|
1140
1171
|
}, bankChannels.map(ch => ch.port1));
|
|
1172
|
+
this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
|
|
1141
1173
|
readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
|
|
1142
1174
|
const bankTimeoutId = setTimeout(() => {
|
|
1143
1175
|
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
@@ -1148,8 +1180,10 @@ class OzVault {
|
|
|
1148
1180
|
}
|
|
1149
1181
|
}, 30000);
|
|
1150
1182
|
const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
|
|
1151
|
-
if (bankPendingEntry)
|
|
1183
|
+
if (bankPendingEntry) {
|
|
1152
1184
|
bankPendingEntry.timeoutId = bankTimeoutId;
|
|
1185
|
+
bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
|
|
1186
|
+
}
|
|
1153
1187
|
}
|
|
1154
1188
|
catch (err) {
|
|
1155
1189
|
this.bankTokenizeResolvers.delete(requestId);
|
|
@@ -1220,8 +1254,15 @@ class OzVault {
|
|
|
1220
1254
|
}
|
|
1221
1255
|
this._tokenizing = 'card';
|
|
1222
1256
|
const requestId = `req-${uuid()}`;
|
|
1257
|
+
this.log('createToken() called');
|
|
1223
1258
|
return new Promise((resolve, reject) => {
|
|
1224
|
-
|
|
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
|
+
};
|
|
1225
1266
|
this.tokenizeResolvers.set(requestId, {
|
|
1226
1267
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
1227
1268
|
reject: (e) => { cleanup(); reject(e); },
|
|
@@ -1234,6 +1275,7 @@ class OzVault {
|
|
|
1234
1275
|
try {
|
|
1235
1276
|
// Tell tokenizer frame to expect N field values, then tokenize
|
|
1236
1277
|
const cardChannels = readyElements.map(() => new MessageChannel());
|
|
1278
|
+
const tokenizeStartMs = Date.now();
|
|
1237
1279
|
this.sendToTokenizer({
|
|
1238
1280
|
type: 'OZ_TOKENIZE',
|
|
1239
1281
|
requestId,
|
|
@@ -1244,6 +1286,11 @@ class OzVault {
|
|
|
1244
1286
|
lastName,
|
|
1245
1287
|
fieldCount: readyElements.length,
|
|
1246
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;
|
|
1247
1294
|
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1248
1295
|
readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
|
|
1249
1296
|
const cardTimeoutId = setTimeout(() => {
|
|
@@ -1265,6 +1312,63 @@ class OzVault {
|
|
|
1265
1312
|
}
|
|
1266
1313
|
});
|
|
1267
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
|
+
}
|
|
1268
1372
|
/**
|
|
1269
1373
|
* Tears down the vault: removes all element iframes, the tokenizer iframe,
|
|
1270
1374
|
* and the global message listener. Call this when the checkout component
|
|
@@ -1275,6 +1379,7 @@ class OzVault {
|
|
|
1275
1379
|
if (this._destroyed)
|
|
1276
1380
|
return;
|
|
1277
1381
|
this._destroyed = true;
|
|
1382
|
+
this.log('destroy() called');
|
|
1278
1383
|
window.removeEventListener('message', this.boundHandleMessage);
|
|
1279
1384
|
document.removeEventListener('visibilitychange', this.boundHandleVisibility);
|
|
1280
1385
|
if (this._pendingMount) {
|
|
@@ -1329,13 +1434,17 @@ class OzVault {
|
|
|
1329
1434
|
const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
|
|
1330
1435
|
if (document.hidden) {
|
|
1331
1436
|
this._hiddenAt = Date.now();
|
|
1437
|
+
this.log('tab hidden');
|
|
1332
1438
|
}
|
|
1333
1439
|
else {
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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) &&
|
|
1337
1444
|
!this._tokenizing &&
|
|
1338
|
-
!this._waxRefreshing)
|
|
1445
|
+
!this._waxRefreshing);
|
|
1446
|
+
this.log('tab visible', { hiddenMs, willRefresh });
|
|
1447
|
+
if (willRefresh) {
|
|
1339
1448
|
this.refreshWaxKey().catch((err) => {
|
|
1340
1449
|
// Proactive refresh failure is non-fatal — the reactive path on the
|
|
1341
1450
|
// next createToken() call will handle it, including the auth retry.
|
|
@@ -1345,6 +1454,56 @@ class OzVault {
|
|
|
1345
1454
|
this._hiddenAt = null;
|
|
1346
1455
|
}
|
|
1347
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
|
+
}
|
|
1348
1507
|
mountTokenizerFrame() {
|
|
1349
1508
|
const mount = () => {
|
|
1350
1509
|
this._pendingMount = null;
|
|
@@ -1356,6 +1515,7 @@ class OzVault {
|
|
|
1356
1515
|
iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
|
|
1357
1516
|
document.body.appendChild(iframe);
|
|
1358
1517
|
this.tokenizerFrame = iframe;
|
|
1518
|
+
this.log('mounting tokenizer iframe');
|
|
1359
1519
|
};
|
|
1360
1520
|
if (document.readyState === 'loading') {
|
|
1361
1521
|
this._pendingMount = mount;
|
|
@@ -1391,6 +1551,12 @@ class OzVault {
|
|
|
1391
1551
|
// the previous session and justCompleted never fires, breaking auto-advance.
|
|
1392
1552
|
if (msg.type === 'OZ_FRAME_READY') {
|
|
1393
1553
|
this.completionState.set(frameId, false);
|
|
1554
|
+
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
1555
|
+
console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
|
|
1556
|
+
`SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
|
|
1557
|
+
'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
|
|
1558
|
+
}
|
|
1559
|
+
this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
|
|
1394
1560
|
}
|
|
1395
1561
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1396
1562
|
if (msg.type === 'OZ_CHANGE') {
|
|
@@ -1414,6 +1580,7 @@ class OzVault {
|
|
|
1414
1580
|
// Require valid too — avoids advancing at 13 digits for unknown-brand cards
|
|
1415
1581
|
// where isComplete() fires before the user has finished typing.
|
|
1416
1582
|
const justCompleted = complete && valid && !wasComplete;
|
|
1583
|
+
this.log('field changed', { type: el.type, complete, valid, justCompleted });
|
|
1417
1584
|
// Sync CVV length when card brand changes
|
|
1418
1585
|
if (el.type === 'cardNumber') {
|
|
1419
1586
|
const brand = msg.cardBrand;
|
|
@@ -1425,17 +1592,25 @@ class OzVault {
|
|
|
1425
1592
|
// Auto-advance focus on completion
|
|
1426
1593
|
if (justCompleted) {
|
|
1427
1594
|
if (el.type === 'cardNumber') {
|
|
1595
|
+
this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
|
|
1428
1596
|
(_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
|
|
1429
1597
|
}
|
|
1430
1598
|
else if (el.type === 'expirationDate') {
|
|
1599
|
+
this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
|
|
1431
1600
|
(_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
|
|
1432
1601
|
}
|
|
1433
1602
|
}
|
|
1434
1603
|
}
|
|
1435
1604
|
handleTokenizerMessage(msg) {
|
|
1436
|
-
var _a, _b, _c;
|
|
1605
|
+
var _a, _b, _c, _d;
|
|
1437
1606
|
switch (msg.type) {
|
|
1438
1607
|
case 'OZ_FRAME_READY':
|
|
1608
|
+
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
1609
|
+
console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
|
|
1610
|
+
`tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
|
|
1611
|
+
'This usually means the deployed frame files are stale. ' +
|
|
1612
|
+
'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
|
|
1613
|
+
}
|
|
1439
1614
|
this.tokenizerReady = true;
|
|
1440
1615
|
if (this.loadErrorTimeoutId != null) {
|
|
1441
1616
|
clearTimeout(this.loadErrorTimeoutId);
|
|
@@ -1447,6 +1622,7 @@ class OzVault {
|
|
|
1447
1622
|
// sent again from create() once the key is available.
|
|
1448
1623
|
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1449
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 });
|
|
1450
1626
|
break;
|
|
1451
1627
|
case 'OZ_TOKEN_RESULT': {
|
|
1452
1628
|
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
@@ -1471,11 +1647,18 @@ class OzVault {
|
|
|
1471
1647
|
}
|
|
1472
1648
|
pending.resolve(Object.assign(Object.assign({ token,
|
|
1473
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
|
+
});
|
|
1474
1656
|
// Increment the per-key success counter and proactively refresh once
|
|
1475
1657
|
// the budget is exhausted so the next createToken() call uses a fresh
|
|
1476
1658
|
// key without waiting for a vault rejection.
|
|
1477
1659
|
this._tokenizeSuccessCount++;
|
|
1478
1660
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1661
|
+
this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1479
1662
|
this.refreshWaxKey().catch((err) => {
|
|
1480
1663
|
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1481
1664
|
});
|
|
@@ -1495,14 +1678,25 @@ class OzVault {
|
|
|
1495
1678
|
this.tokenizeResolvers.delete(msg.requestId);
|
|
1496
1679
|
if (pending.timeoutId != null)
|
|
1497
1680
|
clearTimeout(pending.timeoutId);
|
|
1681
|
+
const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
|
|
1682
|
+
this.log('token error', { errorCode, willRefresh });
|
|
1498
1683
|
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1499
1684
|
// already retried for this request, transparently re-mint and retry.
|
|
1500
|
-
if (
|
|
1685
|
+
if (willRefresh) {
|
|
1686
|
+
const resetCountAtRetry = this._resetCount;
|
|
1501
1687
|
this.refreshWaxKey().then(() => {
|
|
1502
1688
|
if (this._destroyed) {
|
|
1503
1689
|
pending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1504
1690
|
return;
|
|
1505
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
|
+
}
|
|
1506
1700
|
const newRequestId = `req-${uuid()}`;
|
|
1507
1701
|
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1508
1702
|
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
@@ -1549,11 +1743,16 @@ class OzVault {
|
|
|
1549
1743
|
if (bankPending.timeoutId != null)
|
|
1550
1744
|
clearTimeout(bankPending.timeoutId);
|
|
1551
1745
|
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1746
|
+
const resetCountAtRetry = this._resetCount;
|
|
1552
1747
|
this.refreshWaxKey().then(() => {
|
|
1553
1748
|
if (this._destroyed) {
|
|
1554
1749
|
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1555
1750
|
return;
|
|
1556
1751
|
}
|
|
1752
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1753
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1557
1756
|
const newRequestId = `req-${uuid()}`;
|
|
1558
1757
|
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1559
1758
|
try {
|
|
@@ -1611,9 +1810,15 @@ class OzVault {
|
|
|
1611
1810
|
}
|
|
1612
1811
|
const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
|
|
1613
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
|
+
});
|
|
1614
1818
|
// Same proactive refresh logic as card tokenization.
|
|
1615
1819
|
this._tokenizeSuccessCount++;
|
|
1616
1820
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1821
|
+
this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1617
1822
|
this.refreshWaxKey().catch((err) => {
|
|
1618
1823
|
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1619
1824
|
});
|
|
@@ -1669,6 +1874,7 @@ class OzVault {
|
|
|
1669
1874
|
}
|
|
1670
1875
|
const newSessionId = uuid();
|
|
1671
1876
|
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
1877
|
+
this.log('wax key refresh started');
|
|
1672
1878
|
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
1673
1879
|
.then(newWaxKey => {
|
|
1674
1880
|
if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
|
|
@@ -1682,6 +1888,11 @@ class OzVault {
|
|
|
1682
1888
|
if (!this._destroyed && this.tokenizerReady) {
|
|
1683
1889
|
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
1684
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;
|
|
1685
1896
|
})
|
|
1686
1897
|
.finally(() => {
|
|
1687
1898
|
this._waxRefreshing = null;
|
|
@@ -1789,7 +2000,7 @@ const OzContext = createContext({
|
|
|
1789
2000
|
* All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
|
|
1790
2001
|
* rendered inside this provider.
|
|
1791
2002
|
*/
|
|
1792
|
-
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 }) {
|
|
1793
2004
|
const [vault, setVault] = useState(null);
|
|
1794
2005
|
const [initError, setInitError] = useState(null);
|
|
1795
2006
|
const [mountedCount, setMountedCount] = useState(0);
|
|
@@ -1856,7 +2067,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1856
2067
|
var _a;
|
|
1857
2068
|
Promise.resolve().then(() => setTokenizeCount(0));
|
|
1858
2069
|
(_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
|
|
1859
|
-
}
|
|
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 => {
|
|
1860
2071
|
if (cancelled) {
|
|
1861
2072
|
v.destroy();
|
|
1862
2073
|
return;
|
|
@@ -1889,7 +2100,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1889
2100
|
setVault(null);
|
|
1890
2101
|
setInitError(null);
|
|
1891
2102
|
};
|
|
1892
|
-
}, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls]);
|
|
2103
|
+
}, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls, debug]);
|
|
1893
2104
|
const notifyMount = useCallback(() => setMountedCount(n => n + 1), []);
|
|
1894
2105
|
const notifyReady = useCallback(() => setReadyCount(n => n + 1), []);
|
|
1895
2106
|
const notifyUnmount = useCallback(() => {
|
|
@@ -1922,8 +2133,11 @@ function useOzElements() {
|
|
|
1922
2133
|
notifyTokenize();
|
|
1923
2134
|
return result;
|
|
1924
2135
|
}, [vault, notifyTokenize]);
|
|
2136
|
+
const reset = useCallback(() => {
|
|
2137
|
+
vault === null || vault === void 0 ? void 0 : vault.reset();
|
|
2138
|
+
}, [vault]);
|
|
1925
2139
|
const ready = vault !== null && vault.isReady && mountedCount > 0 && readyCount >= mountedCount;
|
|
1926
|
-
return { createToken, createBankToken, ready, initError, tokenizeCount };
|
|
2140
|
+
return { createToken, createBankToken, reset, ready, initError, tokenizeCount };
|
|
1927
2141
|
}
|
|
1928
2142
|
const SKELETON_STYLE = {
|
|
1929
2143
|
height: 46,
|
|
@@ -2018,6 +2232,71 @@ const OzCardNumber = (props) => jsx(OzFieldBase, Object.assign({ type: "cardNumb
|
|
|
2018
2232
|
const OzExpiry = (props) => jsx(OzFieldBase, Object.assign({ type: "expirationDate", variant: "card" }, props));
|
|
2019
2233
|
/** Renders a PCI-isolated CVV input inside an Ozura iframe. */
|
|
2020
2234
|
const OzCvv = (props) => jsx(OzFieldBase, Object.assign({ type: "cvv", variant: "card" }, props));
|
|
2235
|
+
// ─── Shared composite-component hook ─────────────────────────────────────────
|
|
2236
|
+
/**
|
|
2237
|
+
* Shared plumbing for OzCard and OzBankCard.
|
|
2238
|
+
*
|
|
2239
|
+
* Manages:
|
|
2240
|
+
* - Callback refs (onChange, onReady, onFocus, onBlur) kept in sync on every render
|
|
2241
|
+
* - Vault-change detection: resets `readyFieldTypes` and `onReadyFiredRef` when the
|
|
2242
|
+
* vault instance is replaced (e.g. after fetchWaxKey changes or the provider remounts)
|
|
2243
|
+
* - Per-field ready tracking: creates one stable handler per named field; fires the
|
|
2244
|
+
* `onReady` callback once all `fieldNames.length` fields have reported ready
|
|
2245
|
+
* - Error state
|
|
2246
|
+
* - Layout helpers: `gapStr`, `renderLabel`
|
|
2247
|
+
*
|
|
2248
|
+
* @internal — not exported; used only by OzCard and OzBankCard.
|
|
2249
|
+
*/
|
|
2250
|
+
function useCardBase({ vault, fieldNames, onChange, onReady, onFocus, onBlur, gap = 8, labelStyle, labelClassName, }) {
|
|
2251
|
+
const totalFields = fieldNames.length;
|
|
2252
|
+
const readyFieldTypes = useRef(new Set());
|
|
2253
|
+
const onReadyFiredRef = useRef(false);
|
|
2254
|
+
const vaultRef = useRef(vault);
|
|
2255
|
+
const onChangeRef = useRef(onChange);
|
|
2256
|
+
const onReadyRef = useRef(onReady);
|
|
2257
|
+
const onFocusRef = useRef(onFocus);
|
|
2258
|
+
const onBlurRef = useRef(onBlur);
|
|
2259
|
+
useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
|
|
2260
|
+
useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
|
|
2261
|
+
useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
|
|
2262
|
+
useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
2263
|
+
useEffect(() => {
|
|
2264
|
+
if (vault !== vaultRef.current) {
|
|
2265
|
+
vaultRef.current = vault;
|
|
2266
|
+
readyFieldTypes.current = new Set();
|
|
2267
|
+
onReadyFiredRef.current = false;
|
|
2268
|
+
}
|
|
2269
|
+
return () => {
|
|
2270
|
+
readyFieldTypes.current = new Set();
|
|
2271
|
+
onReadyFiredRef.current = false;
|
|
2272
|
+
};
|
|
2273
|
+
}, [vault]);
|
|
2274
|
+
// One stable handler per named field — recreated only when total field count changes.
|
|
2275
|
+
// Field names are static (card = 3 fields, bank = 2 fields) so `totalFields` alone
|
|
2276
|
+
// is a sufficient dependency; a JSON dep would create a new map on every render.
|
|
2277
|
+
// CONTRACT: `fieldNames` must be a static literal — callers must not pass a dynamic
|
|
2278
|
+
// array that changes length without also changing field count.
|
|
2279
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2280
|
+
const readyHandlers = useMemo(() => {
|
|
2281
|
+
const handlers = {};
|
|
2282
|
+
for (const name of fieldNames) {
|
|
2283
|
+
handlers[name] = () => {
|
|
2284
|
+
var _a;
|
|
2285
|
+
readyFieldTypes.current.add(name);
|
|
2286
|
+
if (readyFieldTypes.current.size >= totalFields && !onReadyFiredRef.current) {
|
|
2287
|
+
onReadyFiredRef.current = true;
|
|
2288
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
return handlers;
|
|
2293
|
+
}, [totalFields]); // totalFields captures fieldNames.length; field names are static
|
|
2294
|
+
const [error, setError] = useState();
|
|
2295
|
+
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
2296
|
+
const resolvedLabelStyle = useMemo(() => (labelStyle ? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE), [labelStyle]);
|
|
2297
|
+
const renderLabel = useCallback((text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle), [labelClassName, resolvedLabelStyle]);
|
|
2298
|
+
return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel };
|
|
2299
|
+
}
|
|
2021
2300
|
const DEFAULT_ERROR_STYLE = {
|
|
2022
2301
|
color: '#dc2626',
|
|
2023
2302
|
fontSize: 13,
|
|
@@ -2060,62 +2339,22 @@ function mergeStyles(base, override) {
|
|
|
2060
2339
|
function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName, layout = 'default', gap = 8, hideErrors = false, errorStyle, errorClassName, renderError, onChange, onReady, onFocus, onBlur, disabled, className, placeholders, }) {
|
|
2061
2340
|
var _a, _b, _c;
|
|
2062
2341
|
const { vault } = useContext(OzContext);
|
|
2342
|
+
const { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel, } = useCardBase({
|
|
2343
|
+
vault,
|
|
2344
|
+
fieldNames: ['cardNumber', 'expiry', 'cvv'],
|
|
2345
|
+
onChange,
|
|
2346
|
+
onReady,
|
|
2347
|
+
onFocus,
|
|
2348
|
+
onBlur,
|
|
2349
|
+
gap,
|
|
2350
|
+
labelStyle,
|
|
2351
|
+
labelClassName,
|
|
2352
|
+
});
|
|
2063
2353
|
const fieldState = useRef({
|
|
2064
2354
|
cardNumber: null,
|
|
2065
2355
|
expiry: null,
|
|
2066
2356
|
cvv: null,
|
|
2067
2357
|
});
|
|
2068
|
-
const readyFieldTypes = useRef(new Set());
|
|
2069
|
-
const onReadyFiredRef = useRef(false);
|
|
2070
|
-
const vaultRef = useRef(vault);
|
|
2071
|
-
const onChangeRef = useRef(onChange);
|
|
2072
|
-
const onReadyRef = useRef(onReady);
|
|
2073
|
-
const onFocusRef = useRef(onFocus);
|
|
2074
|
-
const onBlurRef = useRef(onBlur);
|
|
2075
|
-
useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
|
|
2076
|
-
useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
|
|
2077
|
-
useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
|
|
2078
|
-
useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
2079
|
-
// When the vault is recreated (e.g. appearance/fonts props change on OzElements),
|
|
2080
|
-
// context readyCount is reset but this ref is not. Reset so onReady fires once when all 3 are ready.
|
|
2081
|
-
// The cleanup resets readyFieldTypes when the component unmounts (covers React StrictMode double-invoke
|
|
2082
|
-
// and SPA scenarios where the parent re-mounts this component).
|
|
2083
|
-
useEffect(() => {
|
|
2084
|
-
if (vault !== vaultRef.current) {
|
|
2085
|
-
vaultRef.current = vault;
|
|
2086
|
-
readyFieldTypes.current = new Set();
|
|
2087
|
-
onReadyFiredRef.current = false;
|
|
2088
|
-
}
|
|
2089
|
-
return () => {
|
|
2090
|
-
readyFieldTypes.current = new Set();
|
|
2091
|
-
onReadyFiredRef.current = false;
|
|
2092
|
-
};
|
|
2093
|
-
}, [vault]);
|
|
2094
|
-
const [error, setError] = useState();
|
|
2095
|
-
const handleCardNumberReady = useCallback(() => {
|
|
2096
|
-
var _a;
|
|
2097
|
-
readyFieldTypes.current.add('cardNumber');
|
|
2098
|
-
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2099
|
-
onReadyFiredRef.current = true;
|
|
2100
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2101
|
-
}
|
|
2102
|
-
}, []);
|
|
2103
|
-
const handleExpiryReady = useCallback(() => {
|
|
2104
|
-
var _a;
|
|
2105
|
-
readyFieldTypes.current.add('expiry');
|
|
2106
|
-
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2107
|
-
onReadyFiredRef.current = true;
|
|
2108
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2109
|
-
}
|
|
2110
|
-
}, []);
|
|
2111
|
-
const handleCvvReady = useCallback(() => {
|
|
2112
|
-
var _a;
|
|
2113
|
-
readyFieldTypes.current.add('cvv');
|
|
2114
|
-
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2115
|
-
onReadyFiredRef.current = true;
|
|
2116
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2117
|
-
}
|
|
2118
|
-
}, []);
|
|
2119
2358
|
const emitChange = useCallback(() => {
|
|
2120
2359
|
var _a;
|
|
2121
2360
|
const { cardNumber, expiry, cvv } = fieldState.current;
|
|
@@ -2130,20 +2369,16 @@ function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName,
|
|
|
2130
2369
|
error: err,
|
|
2131
2370
|
fields: Object.assign({}, fieldState.current),
|
|
2132
2371
|
});
|
|
2133
|
-
}, []);
|
|
2134
|
-
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
2135
|
-
const resolvedLabelStyle = labelStyle
|
|
2136
|
-
? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
|
|
2137
|
-
const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
|
|
2372
|
+
}, [setError, onChangeRef]);
|
|
2138
2373
|
const showError = !hideErrors && error;
|
|
2139
2374
|
const errorNode = showError
|
|
2140
2375
|
? renderError
|
|
2141
2376
|
? renderError(error)
|
|
2142
2377
|
: (jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
|
|
2143
2378
|
: null;
|
|
2144
|
-
const cardNumberField = (jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cardNumber), jsx(OzCardNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cardNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.cardNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cardNumber) !== null && _a !== void 0 ? _a : 'Card number', disabled: disabled, onChange: (e) => { fieldState.current.cardNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cardNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cardNumber'); }, onReady:
|
|
2145
|
-
const expiryField = (jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.expiry), jsx(OzExpiry, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.expiry), className: classNames === null || classNames === void 0 ? void 0 : classNames.expiry, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.expiry) !== null && _b !== void 0 ? _b : 'MM / YY', disabled: disabled, onChange: (e) => { fieldState.current.expiry = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'expiry'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'expiry'); }, onReady:
|
|
2146
|
-
const cvvField = (jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cvv), jsx(OzCvv, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cvv), className: classNames === null || classNames === void 0 ? void 0 : classNames.cvv, placeholder: (_c = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cvv) !== null && _c !== void 0 ? _c : 'CVV', disabled: disabled, onChange: (e) => { fieldState.current.cvv = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cvv'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cvv'); }, onReady:
|
|
2379
|
+
const cardNumberField = (jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cardNumber), jsx(OzCardNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cardNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.cardNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cardNumber) !== null && _a !== void 0 ? _a : 'Card number', disabled: disabled, onChange: (e) => { fieldState.current.cardNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cardNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cardNumber'); }, onReady: readyHandlers['cardNumber'] })] }));
|
|
2380
|
+
const expiryField = (jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.expiry), jsx(OzExpiry, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.expiry), className: classNames === null || classNames === void 0 ? void 0 : classNames.expiry, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.expiry) !== null && _b !== void 0 ? _b : 'MM / YY', disabled: disabled, onChange: (e) => { fieldState.current.expiry = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'expiry'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'expiry'); }, onReady: readyHandlers['expiry'] })] }));
|
|
2381
|
+
const cvvField = (jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cvv), jsx(OzCvv, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.cvv), className: classNames === null || classNames === void 0 ? void 0 : classNames.cvv, placeholder: (_c = placeholders === null || placeholders === void 0 ? void 0 : placeholders.cvv) !== null && _c !== void 0 ? _c : 'CVV', disabled: disabled, onChange: (e) => { fieldState.current.cvv = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'cvv'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'cvv'); }, onReady: readyHandlers['cvv'] })] }));
|
|
2147
2382
|
if (layout === 'rows') {
|
|
2148
2383
|
return (jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [cardNumberField, expiryField, cvvField, errorNode] }));
|
|
2149
2384
|
}
|
|
@@ -2164,49 +2399,21 @@ const OzBankRoutingNumber = (props) => jsx(OzFieldBase, Object.assign({ type: "r
|
|
|
2164
2399
|
function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassName, gap = 8, hideErrors = false, errorStyle, errorClassName, renderError, onChange, onReady, onFocus, onBlur, disabled, className, placeholders, }) {
|
|
2165
2400
|
var _a, _b;
|
|
2166
2401
|
const { vault } = useContext(OzContext);
|
|
2402
|
+
const { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel, } = useCardBase({
|
|
2403
|
+
vault,
|
|
2404
|
+
fieldNames: ['accountNumber', 'routingNumber'],
|
|
2405
|
+
onChange,
|
|
2406
|
+
onReady,
|
|
2407
|
+
onFocus,
|
|
2408
|
+
onBlur,
|
|
2409
|
+
gap,
|
|
2410
|
+
labelStyle,
|
|
2411
|
+
labelClassName,
|
|
2412
|
+
});
|
|
2167
2413
|
const fieldState = useRef({
|
|
2168
2414
|
accountNumber: null,
|
|
2169
2415
|
routingNumber: null,
|
|
2170
2416
|
});
|
|
2171
|
-
const readyFieldTypes = useRef(new Set());
|
|
2172
|
-
const onReadyFiredRef = useRef(false);
|
|
2173
|
-
const vaultRef = useRef(vault);
|
|
2174
|
-
const onChangeRef = useRef(onChange);
|
|
2175
|
-
const onReadyRef = useRef(onReady);
|
|
2176
|
-
const onFocusRef = useRef(onFocus);
|
|
2177
|
-
const onBlurRef = useRef(onBlur);
|
|
2178
|
-
useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
|
|
2179
|
-
useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
|
|
2180
|
-
useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
|
|
2181
|
-
useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
2182
|
-
useEffect(() => {
|
|
2183
|
-
if (vault !== vaultRef.current) {
|
|
2184
|
-
vaultRef.current = vault;
|
|
2185
|
-
readyFieldTypes.current = new Set();
|
|
2186
|
-
onReadyFiredRef.current = false;
|
|
2187
|
-
}
|
|
2188
|
-
return () => {
|
|
2189
|
-
readyFieldTypes.current = new Set();
|
|
2190
|
-
onReadyFiredRef.current = false;
|
|
2191
|
-
};
|
|
2192
|
-
}, [vault]);
|
|
2193
|
-
const [error, setError] = useState();
|
|
2194
|
-
const handleAccountReady = useCallback(() => {
|
|
2195
|
-
var _a;
|
|
2196
|
-
readyFieldTypes.current.add('accountNumber');
|
|
2197
|
-
if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
|
|
2198
|
-
onReadyFiredRef.current = true;
|
|
2199
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2200
|
-
}
|
|
2201
|
-
}, []);
|
|
2202
|
-
const handleRoutingReady = useCallback(() => {
|
|
2203
|
-
var _a;
|
|
2204
|
-
readyFieldTypes.current.add('routingNumber');
|
|
2205
|
-
if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
|
|
2206
|
-
onReadyFiredRef.current = true;
|
|
2207
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2208
|
-
}
|
|
2209
|
-
}, []);
|
|
2210
2417
|
const emitChange = useCallback(() => {
|
|
2211
2418
|
var _a;
|
|
2212
2419
|
const { accountNumber, routingNumber } = fieldState.current;
|
|
@@ -2219,18 +2426,14 @@ function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassN
|
|
|
2219
2426
|
error: err,
|
|
2220
2427
|
fields: Object.assign({}, fieldState.current),
|
|
2221
2428
|
});
|
|
2222
|
-
}, []);
|
|
2223
|
-
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
2224
|
-
const resolvedLabelStyle = labelStyle
|
|
2225
|
-
? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
|
|
2226
|
-
const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
|
|
2429
|
+
}, [setError, onChangeRef]);
|
|
2227
2430
|
const showError = !hideErrors && error;
|
|
2228
2431
|
const errorNode = showError
|
|
2229
2432
|
? renderError
|
|
2230
2433
|
? renderError(error)
|
|
2231
2434
|
: (jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
|
|
2232
2435
|
: null;
|
|
2233
|
-
return (jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsx(OzBankAccountNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.accountNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.accountNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.accountNumber) !== null && _a !== void 0 ? _a : 'Account number', disabled: disabled, onChange: (e) => { fieldState.current.accountNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'accountNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'accountNumber'); }, onReady:
|
|
2436
|
+
return (jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsx(OzBankAccountNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.accountNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.accountNumber, placeholder: (_a = placeholders === null || placeholders === void 0 ? void 0 : placeholders.accountNumber) !== null && _a !== void 0 ? _a : 'Account number', disabled: disabled, onChange: (e) => { fieldState.current.accountNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'accountNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'accountNumber'); }, onReady: readyHandlers['accountNumber'] })] }), jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.routingNumber), jsx(OzBankRoutingNumber, { style: mergeStyles(style, styles === null || styles === void 0 ? void 0 : styles.routingNumber), className: classNames === null || classNames === void 0 ? void 0 : classNames.routingNumber, placeholder: (_b = placeholders === null || placeholders === void 0 ? void 0 : placeholders.routingNumber) !== null && _b !== void 0 ? _b : 'Routing number', disabled: disabled, onChange: (e) => { fieldState.current.routingNumber = e; emitChange(); }, onFocus: () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef, 'routingNumber'); }, onBlur: () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef, 'routingNumber'); }, onReady: readyHandlers['routingNumber'] })] }), errorNode] }));
|
|
2234
2437
|
}
|
|
2235
2438
|
|
|
2236
2439
|
export { OzBankAccountNumber, OzBankCard, OzBankRoutingNumber, OzCard, OzCardNumber, OzCvv, OzElements, OzExpiry, createFetchWaxKey, useOzElements };
|