@ozura/elements 1.0.2 → 1.1.0-next.22
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 +234 -86
- 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 +477 -251
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +478 -251
- 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 +447 -225
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +448 -226
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/react/index.d.ts +70 -26
- package/dist/react/sdk/OzVault.d.ts +55 -5
- package/dist/react/sdk/createSessionFetcher.d.ts +29 -0
- package/dist/react/sdk/index.d.ts +6 -26
- package/dist/react/server/index.d.ts +126 -74
- package/dist/react/types/index.d.ts +72 -31
- package/dist/server/frame/elementFrame.d.ts +70 -1
- package/dist/server/frame/protocol.d.ts +12 -0
- package/dist/server/index.cjs.js +167 -78
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +166 -79
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/sdk/OzVault.d.ts +55 -5
- package/dist/server/sdk/createSessionFetcher.d.ts +29 -0
- package/dist/server/sdk/index.d.ts +6 -26
- package/dist/server/server/index.d.ts +126 -74
- package/dist/server/types/index.d.ts +72 -31
- 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 +55 -5
- package/dist/types/sdk/createSessionFetcher.d.ts +29 -0
- package/dist/types/sdk/index.d.ts +6 -26
- package/dist/types/server/index.d.ts +126 -74
- package/dist/types/types/index.d.ts +72 -31
- package/package.json +1 -1
package/dist/react/index.cjs.js
CHANGED
|
@@ -357,7 +357,7 @@ class OzElement {
|
|
|
357
357
|
* (useful when integrating with React refs).
|
|
358
358
|
*/
|
|
359
359
|
mount(target) {
|
|
360
|
-
var _a;
|
|
360
|
+
var _a, _b;
|
|
361
361
|
if (this._destroyed)
|
|
362
362
|
throw new OzError('Cannot mount a destroyed element.');
|
|
363
363
|
if (this.iframe)
|
|
@@ -374,7 +374,13 @@ class OzElement {
|
|
|
374
374
|
iframe.setAttribute('scrolling', 'no');
|
|
375
375
|
iframe.setAttribute('allowtransparency', 'true');
|
|
376
376
|
iframe.style.cssText = 'border:none;width:100%;height:46px;display:block;overflow:hidden;';
|
|
377
|
-
iframe.title = `Secure ${
|
|
377
|
+
iframe.title = `Secure ${(_a = {
|
|
378
|
+
cardNumber: 'card number',
|
|
379
|
+
expirationDate: 'expiration date',
|
|
380
|
+
cvv: 'CVV',
|
|
381
|
+
accountNumber: 'account number',
|
|
382
|
+
routingNumber: 'routing number',
|
|
383
|
+
}[this.elementType]) !== null && _a !== void 0 ? _a : this.elementType} input`;
|
|
378
384
|
// Note: the `sandbox` attribute is intentionally NOT set. Field values are
|
|
379
385
|
// delivered to the tokenizer iframe via a MessageChannel port (transferred
|
|
380
386
|
// in OZ_BEGIN_COLLECT), so no window.parent named-frame lookup is needed.
|
|
@@ -388,7 +394,7 @@ class OzElement {
|
|
|
388
394
|
container.appendChild(iframe);
|
|
389
395
|
this.iframe = iframe;
|
|
390
396
|
this._frameWindow = iframe.contentWindow;
|
|
391
|
-
const timeout = (
|
|
397
|
+
const timeout = (_b = this.options.loadTimeoutMs) !== null && _b !== void 0 ? _b : 10000;
|
|
392
398
|
this._loadTimer = setTimeout(() => {
|
|
393
399
|
if (!this._ready && !this._destroyed) {
|
|
394
400
|
this.emit('loaderror', { elementType: this.elementType, error: `${this.elementType} iframe failed to load within ${timeout}ms` });
|
|
@@ -821,13 +827,105 @@ function validateBilling(billing) {
|
|
|
821
827
|
return { valid: errors.length === 0, errors, normalized };
|
|
822
828
|
}
|
|
823
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Shared postMessage protocol constants.
|
|
832
|
+
*
|
|
833
|
+
* PROTOCOL_VERSION must be incremented any time a breaking change is made to
|
|
834
|
+
* the postMessage message shape (new required fields, renamed types, removed
|
|
835
|
+
* fields, changed semantics). The SDK reads this value from OZ_FRAME_READY
|
|
836
|
+
* messages and warns when the frame and SDK are out of sync.
|
|
837
|
+
*
|
|
838
|
+
* Non-breaking additions (new optional fields, new message types that old
|
|
839
|
+
* frames can safely ignore) do NOT require a version bump.
|
|
840
|
+
*/
|
|
841
|
+
const PROTOCOL_VERSION = 1;
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
|
|
845
|
+
*
|
|
846
|
+
* This is the recommended way to wire the SDK to your backend session endpoint.
|
|
847
|
+
* If you don't need custom headers or auth logic, pass `sessionUrl` directly to
|
|
848
|
+
* `OzVault.create()` or `<OzElements>` — it calls this helper internally.
|
|
849
|
+
*
|
|
850
|
+
* The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
|
|
851
|
+
* legacy `waxKey`) from the JSON response, so it is compatible with both the
|
|
852
|
+
* new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
|
|
853
|
+
*
|
|
854
|
+
* Each call enforces a **10-second timeout**. On pure network failures
|
|
855
|
+
* (offline, DNS, connection refused) the request is retried **once after 750ms**.
|
|
856
|
+
* HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
|
|
857
|
+
*
|
|
858
|
+
* @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* // Simplest — just pass sessionUrl, no need to call this directly
|
|
862
|
+
* const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* // Manual — use when you need custom headers
|
|
866
|
+
* const vault = await OzVault.create({
|
|
867
|
+
* pubKey: 'pk_live_...',
|
|
868
|
+
* getSessionKey: createSessionFetcher('/api/oz-session'),
|
|
869
|
+
* });
|
|
870
|
+
*/
|
|
871
|
+
function createSessionFetcher(url) {
|
|
872
|
+
const TIMEOUT_MS = 10000;
|
|
873
|
+
// Each attempt gets its own AbortController so a timeout on attempt 1 does
|
|
874
|
+
// not bleed into the retry.
|
|
875
|
+
const attemptFetch = (sessionId) => {
|
|
876
|
+
const controller = new AbortController();
|
|
877
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
878
|
+
return fetch(url, {
|
|
879
|
+
method: 'POST',
|
|
880
|
+
headers: { 'Content-Type': 'application/json' },
|
|
881
|
+
body: JSON.stringify({ sessionId }),
|
|
882
|
+
signal: controller.signal,
|
|
883
|
+
}).finally(() => clearTimeout(timer));
|
|
884
|
+
};
|
|
885
|
+
return async (sessionId) => {
|
|
886
|
+
let res;
|
|
887
|
+
try {
|
|
888
|
+
res = await attemptFetch(sessionId);
|
|
889
|
+
}
|
|
890
|
+
catch (firstErr) {
|
|
891
|
+
// Timeout/abort — don't retry, we already waited the full duration.
|
|
892
|
+
if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
|
|
893
|
+
throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
|
|
894
|
+
}
|
|
895
|
+
// Pure network error — retry once after a short pause.
|
|
896
|
+
await new Promise(resolve => setTimeout(resolve, 750));
|
|
897
|
+
try {
|
|
898
|
+
res = await attemptFetch(sessionId);
|
|
899
|
+
}
|
|
900
|
+
catch (retryErr) {
|
|
901
|
+
const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
|
|
902
|
+
throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const data = await res.json().catch(() => ({}));
|
|
906
|
+
if (!res.ok) {
|
|
907
|
+
throw new OzError(typeof data.error === 'string' && data.error
|
|
908
|
+
? data.error
|
|
909
|
+
: `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
910
|
+
}
|
|
911
|
+
// Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
|
|
912
|
+
// with backends that haven't migrated to createSessionMiddleware yet.
|
|
913
|
+
const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
|
|
914
|
+
(typeof data.waxKey === 'string' ? data.waxKey : '');
|
|
915
|
+
if (!key.trim()) {
|
|
916
|
+
throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
917
|
+
}
|
|
918
|
+
return key;
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
824
922
|
function isCardMetadata(v) {
|
|
825
923
|
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
826
924
|
}
|
|
827
925
|
function isBankAccountMetadata(v) {
|
|
828
926
|
return !!v && typeof v === 'object' && typeof v.last4 === 'string';
|
|
829
927
|
}
|
|
830
|
-
const DEFAULT_FRAME_BASE_URL = "https://
|
|
928
|
+
const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
|
|
831
929
|
/**
|
|
832
930
|
* The main entry point for OzElements. Creates and manages iframe-based
|
|
833
931
|
* card input elements that keep raw card data isolated from the merchant page.
|
|
@@ -860,7 +958,7 @@ class OzVault {
|
|
|
860
958
|
* @internal
|
|
861
959
|
*/
|
|
862
960
|
constructor(options, waxKey, tokenizationSessionId) {
|
|
863
|
-
var _a, _b, _c;
|
|
961
|
+
var _a, _b, _c, _d, _e, _f;
|
|
864
962
|
this.elements = new Map();
|
|
865
963
|
this.elementsByType = new Map();
|
|
866
964
|
this.bankElementsByType = new Map();
|
|
@@ -873,6 +971,9 @@ class OzVault {
|
|
|
873
971
|
this.tokenizerReady = false;
|
|
874
972
|
this._tokenizing = null;
|
|
875
973
|
this._destroyed = false;
|
|
974
|
+
// Incremented every time reset() cancels an active tokenization so that
|
|
975
|
+
// any in-flight wax-key refresh retry can detect it was superseded.
|
|
976
|
+
this._resetCount = 0;
|
|
876
977
|
// Tracks successful tokenizations against the per-key call budget so the SDK
|
|
877
978
|
// can proactively refresh the wax key after it has been consumed rather than
|
|
878
979
|
// waiting for the next createToken() call to fail.
|
|
@@ -891,14 +992,16 @@ class OzVault {
|
|
|
891
992
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
892
993
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
893
994
|
this.vaultId = `vault-${uuid()}`;
|
|
894
|
-
|
|
995
|
+
// sessionLimit takes precedence over legacy maxTokenizeCalls
|
|
996
|
+
this._maxTokenizeCalls = (_c = (_b = options.sessionLimit) !== null && _b !== void 0 ? _b : options.maxTokenizeCalls) !== null && _c !== void 0 ? _c : 3;
|
|
997
|
+
this._debug = (_d = options.debug) !== null && _d !== void 0 ? _d : false;
|
|
895
998
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
896
999
|
window.addEventListener('message', this.boundHandleMessage);
|
|
897
1000
|
this.boundHandleVisibility = this.handleVisibilityChange.bind(this);
|
|
898
1001
|
document.addEventListener('visibilitychange', this.boundHandleVisibility);
|
|
899
1002
|
this.mountTokenizerFrame();
|
|
900
1003
|
if (options.onLoadError) {
|
|
901
|
-
const timeout = (
|
|
1004
|
+
const timeout = (_e = options.loadTimeoutMs) !== null && _e !== void 0 ? _e : 10000;
|
|
902
1005
|
this.loadErrorTimeoutId = setTimeout(() => {
|
|
903
1006
|
this.loadErrorTimeoutId = null;
|
|
904
1007
|
if (!this._destroyed && !this.tokenizerReady) {
|
|
@@ -906,54 +1009,68 @@ class OzVault {
|
|
|
906
1009
|
}
|
|
907
1010
|
}, timeout);
|
|
908
1011
|
}
|
|
909
|
-
|
|
1012
|
+
// onSessionRefresh takes precedence over legacy onWaxRefresh
|
|
1013
|
+
this._onWaxRefresh = (_f = options.onSessionRefresh) !== null && _f !== void 0 ? _f : options.onWaxRefresh;
|
|
910
1014
|
this._onReady = options.onReady;
|
|
1015
|
+
this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
911
1016
|
}
|
|
912
1017
|
/**
|
|
913
1018
|
* Creates and returns a ready `OzVault` instance.
|
|
914
1019
|
*
|
|
915
1020
|
* Internally this:
|
|
916
|
-
* 1. Generates a
|
|
1021
|
+
* 1. Generates a session UUID.
|
|
917
1022
|
* 2. Starts loading the hidden tokenizer iframe immediately.
|
|
918
|
-
* 3.
|
|
919
|
-
*
|
|
920
|
-
*
|
|
1023
|
+
* 3. Fetches a session key from your backend concurrently — either via
|
|
1024
|
+
* `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
|
|
1025
|
+
* deprecated `fetchWaxKey` callback.
|
|
1026
|
+
* 4. Resolves with the vault instance once the session key is stored. The iframe
|
|
921
1027
|
* has been loading the whole time, so `isReady` may already be true or
|
|
922
1028
|
* will fire shortly after.
|
|
923
1029
|
*
|
|
924
1030
|
* The returned vault is ready to create elements immediately. `createToken()`
|
|
925
1031
|
* additionally requires `vault.isReady` (tokenizer iframe loaded).
|
|
926
1032
|
*
|
|
927
|
-
* @throws {OzError} if
|
|
1033
|
+
* @throws {OzError} if the session fetch fails, times out, or returns an empty string.
|
|
928
1034
|
*/
|
|
929
1035
|
static async create(options, signal) {
|
|
930
1036
|
if (!options.pubKey || !options.pubKey.trim()) {
|
|
931
1037
|
throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
|
|
932
1038
|
}
|
|
933
|
-
|
|
934
|
-
|
|
1039
|
+
// Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
|
|
1040
|
+
// This allows merchants to use the clean new API without touching legacy code.
|
|
1041
|
+
let resolvedFetchKey;
|
|
1042
|
+
if (options.sessionUrl) {
|
|
1043
|
+
resolvedFetchKey = createSessionFetcher(options.sessionUrl);
|
|
1044
|
+
}
|
|
1045
|
+
else if (typeof options.getSessionKey === 'function') {
|
|
1046
|
+
resolvedFetchKey = options.getSessionKey;
|
|
1047
|
+
}
|
|
1048
|
+
else if (typeof options.fetchWaxKey === 'function') {
|
|
1049
|
+
resolvedFetchKey = options.fetchWaxKey;
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
|
|
935
1053
|
}
|
|
936
1054
|
const tokenizationSessionId = uuid();
|
|
937
1055
|
// Construct the vault immediately — this mounts the tokenizer iframe so it
|
|
938
|
-
// starts loading while
|
|
939
|
-
// empty and is set below before create() returns.
|
|
1056
|
+
// starts loading while the session fetch is in flight.
|
|
940
1057
|
const vault = new OzVault(options, '', tokenizationSessionId);
|
|
941
1058
|
// If the caller provides an AbortSignal (e.g. React useEffect cleanup),
|
|
942
1059
|
// destroy the vault immediately on abort so the tokenizer iframe and message
|
|
943
|
-
// listener are removed synchronously rather than waiting for
|
|
944
|
-
// settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
1060
|
+
// listener are removed synchronously rather than waiting for the session fetch
|
|
1061
|
+
// to settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
945
1062
|
const onAbort = () => vault.destroy();
|
|
946
1063
|
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
|
|
947
1064
|
let waxKey;
|
|
948
1065
|
try {
|
|
949
|
-
waxKey = await
|
|
1066
|
+
waxKey = await resolvedFetchKey(tokenizationSessionId);
|
|
950
1067
|
}
|
|
951
1068
|
catch (err) {
|
|
952
1069
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
953
1070
|
vault.destroy();
|
|
954
1071
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
955
1072
|
throw new OzError('OzVault.create() was cancelled.');
|
|
956
|
-
// Preserve errorCode/retryable from OzError (e.g. timeout/network from
|
|
1073
|
+
// Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
|
|
957
1074
|
// so callers can distinguish transient failures from config errors.
|
|
958
1075
|
const originalCode = err instanceof OzError ? err.errorCode : undefined;
|
|
959
1076
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
@@ -970,13 +1087,14 @@ class OzVault {
|
|
|
970
1087
|
}
|
|
971
1088
|
// Static methods can access private fields of instances of the same class.
|
|
972
1089
|
vault.waxKey = waxKey;
|
|
973
|
-
vault._storedFetchWaxKey =
|
|
1090
|
+
vault._storedFetchWaxKey = resolvedFetchKey;
|
|
974
1091
|
// If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
|
|
975
1092
|
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
976
1093
|
// so the tokenizer has the key stored before any createToken() call.
|
|
977
1094
|
if (vault.tokenizerReady) {
|
|
978
1095
|
vault.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey });
|
|
979
1096
|
}
|
|
1097
|
+
vault.log('wax key received — vault ready');
|
|
980
1098
|
return vault;
|
|
981
1099
|
}
|
|
982
1100
|
/**
|
|
@@ -1118,8 +1236,13 @@ class OzVault {
|
|
|
1118
1236
|
const readyBankElements = [accountEl, routingEl];
|
|
1119
1237
|
this._tokenizing = 'bank';
|
|
1120
1238
|
const requestId = `req-${uuid()}`;
|
|
1239
|
+
this.log('createBankToken() called');
|
|
1121
1240
|
return new Promise((resolve, reject) => {
|
|
1122
|
-
const
|
|
1241
|
+
const resetCountAtStart = this._resetCount;
|
|
1242
|
+
const cleanup = () => {
|
|
1243
|
+
if (this._resetCount === resetCountAtStart)
|
|
1244
|
+
this._tokenizing = null;
|
|
1245
|
+
};
|
|
1123
1246
|
this.bankTokenizeResolvers.set(requestId, {
|
|
1124
1247
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
1125
1248
|
reject: (e) => { cleanup(); reject(e); },
|
|
@@ -1130,6 +1253,7 @@ class OzVault {
|
|
|
1130
1253
|
});
|
|
1131
1254
|
try {
|
|
1132
1255
|
const bankChannels = readyBankElements.map(() => new MessageChannel());
|
|
1256
|
+
const bankTokenizeStartMs = Date.now();
|
|
1133
1257
|
this.sendToTokenizer({
|
|
1134
1258
|
type: 'OZ_BANK_TOKENIZE',
|
|
1135
1259
|
requestId,
|
|
@@ -1140,6 +1264,7 @@ class OzVault {
|
|
|
1140
1264
|
lastName: options.lastName.trim(),
|
|
1141
1265
|
fieldCount: readyBankElements.length,
|
|
1142
1266
|
}, bankChannels.map(ch => ch.port1));
|
|
1267
|
+
this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
|
|
1143
1268
|
readyBankElements.forEach((el, i) => el.beginCollect(requestId, bankChannels[i].port2));
|
|
1144
1269
|
const bankTimeoutId = setTimeout(() => {
|
|
1145
1270
|
if (this.bankTokenizeResolvers.has(requestId)) {
|
|
@@ -1150,8 +1275,10 @@ class OzVault {
|
|
|
1150
1275
|
}
|
|
1151
1276
|
}, 30000);
|
|
1152
1277
|
const bankPendingEntry = this.bankTokenizeResolvers.get(requestId);
|
|
1153
|
-
if (bankPendingEntry)
|
|
1278
|
+
if (bankPendingEntry) {
|
|
1154
1279
|
bankPendingEntry.timeoutId = bankTimeoutId;
|
|
1280
|
+
bankPendingEntry.tokenizeStartMs = bankTokenizeStartMs;
|
|
1281
|
+
}
|
|
1155
1282
|
}
|
|
1156
1283
|
catch (err) {
|
|
1157
1284
|
this.bankTokenizeResolvers.delete(requestId);
|
|
@@ -1222,8 +1349,15 @@ class OzVault {
|
|
|
1222
1349
|
}
|
|
1223
1350
|
this._tokenizing = 'card';
|
|
1224
1351
|
const requestId = `req-${uuid()}`;
|
|
1352
|
+
this.log('createToken() called');
|
|
1225
1353
|
return new Promise((resolve, reject) => {
|
|
1226
|
-
|
|
1354
|
+
// Capture the reset generation so cleanup() only zeros _tokenizing when it
|
|
1355
|
+
// still belongs to this invocation — not a newer one that started after a reset.
|
|
1356
|
+
const resetCountAtStart = this._resetCount;
|
|
1357
|
+
const cleanup = () => {
|
|
1358
|
+
if (this._resetCount === resetCountAtStart)
|
|
1359
|
+
this._tokenizing = null;
|
|
1360
|
+
};
|
|
1227
1361
|
this.tokenizeResolvers.set(requestId, {
|
|
1228
1362
|
resolve: (v) => { cleanup(); resolve(v); },
|
|
1229
1363
|
reject: (e) => { cleanup(); reject(e); },
|
|
@@ -1236,6 +1370,7 @@ class OzVault {
|
|
|
1236
1370
|
try {
|
|
1237
1371
|
// Tell tokenizer frame to expect N field values, then tokenize
|
|
1238
1372
|
const cardChannels = readyElements.map(() => new MessageChannel());
|
|
1373
|
+
const tokenizeStartMs = Date.now();
|
|
1239
1374
|
this.sendToTokenizer({
|
|
1240
1375
|
type: 'OZ_TOKENIZE',
|
|
1241
1376
|
requestId,
|
|
@@ -1246,6 +1381,11 @@ class OzVault {
|
|
|
1246
1381
|
lastName,
|
|
1247
1382
|
fieldCount: readyElements.length,
|
|
1248
1383
|
}, cardChannels.map(ch => ch.port1));
|
|
1384
|
+
this.log('OZ_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyElements.length });
|
|
1385
|
+
// Store start time for elapsed-ms logging on result
|
|
1386
|
+
const cardEntry = this.tokenizeResolvers.get(requestId);
|
|
1387
|
+
if (cardEntry)
|
|
1388
|
+
cardEntry.tokenizeStartMs = tokenizeStartMs;
|
|
1249
1389
|
// Tell each ready element frame to send its raw value to the tokenizer
|
|
1250
1390
|
readyElements.forEach((el, i) => el.beginCollect(requestId, cardChannels[i].port2));
|
|
1251
1391
|
const cardTimeoutId = setTimeout(() => {
|
|
@@ -1267,6 +1407,63 @@ class OzVault {
|
|
|
1267
1407
|
}
|
|
1268
1408
|
});
|
|
1269
1409
|
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Clears all mounted element fields without tearing down the vault.
|
|
1412
|
+
*
|
|
1413
|
+
* Call this after a failed payment (e.g. card declined) to let the customer
|
|
1414
|
+
* re-enter their details. The vault instance, tokenizer iframe, wax key, and
|
|
1415
|
+
* tokenization budget counter are all preserved — no network calls are made.
|
|
1416
|
+
*
|
|
1417
|
+
* **Wax key session model:** by design, one wax key covers the full checkout
|
|
1418
|
+
* session. The default `max_tokenize_calls: 3` supports two declined attempts
|
|
1419
|
+
* and one final attempt on the same key. Do not call `vault.destroy()` and
|
|
1420
|
+
* recreate the vault between declines — that unnecessarily re-mints the key
|
|
1421
|
+
* and discards the remaining budget.
|
|
1422
|
+
*
|
|
1423
|
+
* @example
|
|
1424
|
+
* try {
|
|
1425
|
+
* const { token, cvcSession } = await vault.createToken({ billing });
|
|
1426
|
+
* await chargeCard(token, cvcSession);
|
|
1427
|
+
* } catch (err) {
|
|
1428
|
+
* vault.reset(); // clear fields; let customer re-enter
|
|
1429
|
+
* showError(err.message);
|
|
1430
|
+
* }
|
|
1431
|
+
*/
|
|
1432
|
+
reset() {
|
|
1433
|
+
if (this._destroyed)
|
|
1434
|
+
return;
|
|
1435
|
+
const cancelling = Boolean(this._tokenizing);
|
|
1436
|
+
this.log('reset() called', { tokenizing: this._tokenizing, cancelling });
|
|
1437
|
+
if (this._tokenizing) {
|
|
1438
|
+
this._tokenizing = null;
|
|
1439
|
+
this._resetCount++;
|
|
1440
|
+
this.tokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1441
|
+
if (timeoutId != null)
|
|
1442
|
+
clearTimeout(timeoutId);
|
|
1443
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1444
|
+
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1445
|
+
});
|
|
1446
|
+
this.tokenizeResolvers.clear();
|
|
1447
|
+
this.bankTokenizeResolvers.forEach(({ reject, timeoutId }, requestId) => {
|
|
1448
|
+
if (timeoutId != null)
|
|
1449
|
+
clearTimeout(timeoutId);
|
|
1450
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId });
|
|
1451
|
+
reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1452
|
+
});
|
|
1453
|
+
this.bankTokenizeResolvers.clear();
|
|
1454
|
+
}
|
|
1455
|
+
// Clear field values in all mounted element iframes
|
|
1456
|
+
this.elementsByType.forEach(el => el.clear());
|
|
1457
|
+
this.bankElementsByType.forEach(el => el.clear());
|
|
1458
|
+
// Reset per-element completion state so auto-advance starts fresh on re-entry
|
|
1459
|
+
for (const frameId of this.completionState.keys()) {
|
|
1460
|
+
this.completionState.set(frameId, false);
|
|
1461
|
+
}
|
|
1462
|
+
// NOTE: _tokenizeSuccessCount is intentionally NOT reset.
|
|
1463
|
+
// It reflects real server-side wax key budget consumption. Zeroing it
|
|
1464
|
+
// would desync the proactive refresh logic from the vault's state and
|
|
1465
|
+
// risk triggering a mid-session re-mint on what should be a clean retry.
|
|
1466
|
+
}
|
|
1270
1467
|
/**
|
|
1271
1468
|
* Tears down the vault: removes all element iframes, the tokenizer iframe,
|
|
1272
1469
|
* and the global message listener. Call this when the checkout component
|
|
@@ -1277,6 +1474,7 @@ class OzVault {
|
|
|
1277
1474
|
if (this._destroyed)
|
|
1278
1475
|
return;
|
|
1279
1476
|
this._destroyed = true;
|
|
1477
|
+
this.log('destroy() called');
|
|
1280
1478
|
window.removeEventListener('message', this.boundHandleMessage);
|
|
1281
1479
|
document.removeEventListener('visibilitychange', this.boundHandleVisibility);
|
|
1282
1480
|
if (this._pendingMount) {
|
|
@@ -1331,13 +1529,17 @@ class OzVault {
|
|
|
1331
1529
|
const REFRESH_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
|
|
1332
1530
|
if (document.hidden) {
|
|
1333
1531
|
this._hiddenAt = Date.now();
|
|
1532
|
+
this.log('tab hidden');
|
|
1334
1533
|
}
|
|
1335
1534
|
else {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1535
|
+
const hiddenMs = this._hiddenAt !== null ? Date.now() - this._hiddenAt : 0;
|
|
1536
|
+
const willRefresh = (this._hiddenAt !== null &&
|
|
1537
|
+
hiddenMs >= REFRESH_THRESHOLD_MS &&
|
|
1538
|
+
Boolean(this._storedFetchWaxKey) &&
|
|
1339
1539
|
!this._tokenizing &&
|
|
1340
|
-
!this._waxRefreshing)
|
|
1540
|
+
!this._waxRefreshing);
|
|
1541
|
+
this.log('tab visible', { hiddenMs, willRefresh });
|
|
1542
|
+
if (willRefresh) {
|
|
1341
1543
|
this.refreshWaxKey().catch((err) => {
|
|
1342
1544
|
// Proactive refresh failure is non-fatal — the reactive path on the
|
|
1343
1545
|
// next createToken() call will handle it, including the auth retry.
|
|
@@ -1347,6 +1549,56 @@ class OzVault {
|
|
|
1347
1549
|
this._hiddenAt = null;
|
|
1348
1550
|
}
|
|
1349
1551
|
}
|
|
1552
|
+
// ─── Debug ───────────────────────────────────────────────────────────────
|
|
1553
|
+
/**
|
|
1554
|
+
* Emits a `[OzVault]`-prefixed entry to `console.log`. No-op when `debug` is
|
|
1555
|
+
* not set. Never called with sensitive values — callers use presence flags only.
|
|
1556
|
+
*/
|
|
1557
|
+
log(message, data) {
|
|
1558
|
+
if (!this._debug)
|
|
1559
|
+
return;
|
|
1560
|
+
if (data !== undefined) {
|
|
1561
|
+
console.log(`[OzVault] ${message}`, data);
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
1564
|
+
console.log(`[OzVault] ${message}`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Returns a plain-object snapshot of the vault's current internal state.
|
|
1569
|
+
* Safe to attach to bug reports — no wax keys, tokens, or billing data included.
|
|
1570
|
+
*
|
|
1571
|
+
* Available on all vault instances regardless of whether `debug` was enabled.
|
|
1572
|
+
*
|
|
1573
|
+
* @example
|
|
1574
|
+
* console.log(vault.debugState());
|
|
1575
|
+
* // {
|
|
1576
|
+
* // vaultId: 'vault-abc123',
|
|
1577
|
+
* // isReady: true,
|
|
1578
|
+
* // tokenizing: null,
|
|
1579
|
+
* // destroyed: false,
|
|
1580
|
+
* // waxKeyPresent: true,
|
|
1581
|
+
* // elements: ['cardNumber', 'expirationDate', 'cvv'],
|
|
1582
|
+
* // ...
|
|
1583
|
+
* // }
|
|
1584
|
+
*/
|
|
1585
|
+
debugState() {
|
|
1586
|
+
return {
|
|
1587
|
+
vaultId: this.vaultId,
|
|
1588
|
+
isReady: this.tokenizerReady,
|
|
1589
|
+
tokenizing: this._tokenizing,
|
|
1590
|
+
destroyed: this._destroyed,
|
|
1591
|
+
waxKeyPresent: Boolean(this.waxKey),
|
|
1592
|
+
tokenizeSuccessCount: this._tokenizeSuccessCount,
|
|
1593
|
+
maxTokenizeCalls: this._maxTokenizeCalls,
|
|
1594
|
+
resetCount: this._resetCount,
|
|
1595
|
+
elements: [...this.elementsByType.keys()],
|
|
1596
|
+
bankElements: [...this.bankElementsByType.keys()],
|
|
1597
|
+
completionState: Object.fromEntries([...this.completionState.entries()].map(([id, v]) => [id.slice(0, 8), v])),
|
|
1598
|
+
pendingTokenizations: this.tokenizeResolvers.size,
|
|
1599
|
+
pendingBankTokenizations: this.bankTokenizeResolvers.size,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1350
1602
|
mountTokenizerFrame() {
|
|
1351
1603
|
const mount = () => {
|
|
1352
1604
|
this._pendingMount = null;
|
|
@@ -1358,6 +1610,7 @@ class OzVault {
|
|
|
1358
1610
|
iframe.src = `${this.frameBaseUrl}/frame/tokenizer-frame.html#vaultId=${encodeURIComponent(this.vaultId)}${parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : ''}`;
|
|
1359
1611
|
document.body.appendChild(iframe);
|
|
1360
1612
|
this.tokenizerFrame = iframe;
|
|
1613
|
+
this.log('mounting tokenizer iframe');
|
|
1361
1614
|
};
|
|
1362
1615
|
if (document.readyState === 'loading') {
|
|
1363
1616
|
this._pendingMount = mount;
|
|
@@ -1393,6 +1646,12 @@ class OzVault {
|
|
|
1393
1646
|
// the previous session and justCompleted never fires, breaking auto-advance.
|
|
1394
1647
|
if (msg.type === 'OZ_FRAME_READY') {
|
|
1395
1648
|
this.completionState.set(frameId, false);
|
|
1649
|
+
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
1650
|
+
console.warn(`[OzVault] Protocol version mismatch on element frame "${frameId}" — ` +
|
|
1651
|
+
`SDK expects v${PROTOCOL_VERSION}, frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
|
|
1652
|
+
'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
|
|
1653
|
+
}
|
|
1654
|
+
this.log('element iframe ready', { type: el.type, frameIdPrefix: frameId.slice(0, 8) });
|
|
1396
1655
|
}
|
|
1397
1656
|
// Intercept OZ_CHANGE before forwarding — handle auto-advance and CVV sync
|
|
1398
1657
|
if (msg.type === 'OZ_CHANGE') {
|
|
@@ -1416,6 +1675,7 @@ class OzVault {
|
|
|
1416
1675
|
// Require valid too — avoids advancing at 13 digits for unknown-brand cards
|
|
1417
1676
|
// where isComplete() fires before the user has finished typing.
|
|
1418
1677
|
const justCompleted = complete && valid && !wasComplete;
|
|
1678
|
+
this.log('field changed', { type: el.type, complete, valid, justCompleted });
|
|
1419
1679
|
// Sync CVV length when card brand changes
|
|
1420
1680
|
if (el.type === 'cardNumber') {
|
|
1421
1681
|
const brand = msg.cardBrand;
|
|
@@ -1427,17 +1687,25 @@ class OzVault {
|
|
|
1427
1687
|
// Auto-advance focus on completion
|
|
1428
1688
|
if (justCompleted) {
|
|
1429
1689
|
if (el.type === 'cardNumber') {
|
|
1690
|
+
this.log('auto-advance', { from: 'cardNumber', to: 'expirationDate' });
|
|
1430
1691
|
(_b = this.elementsByType.get('expirationDate')) === null || _b === void 0 ? void 0 : _b.focus();
|
|
1431
1692
|
}
|
|
1432
1693
|
else if (el.type === 'expirationDate') {
|
|
1694
|
+
this.log('auto-advance', { from: 'expirationDate', to: 'cvv' });
|
|
1433
1695
|
(_c = this.elementsByType.get('cvv')) === null || _c === void 0 ? void 0 : _c.focus();
|
|
1434
1696
|
}
|
|
1435
1697
|
}
|
|
1436
1698
|
}
|
|
1437
1699
|
handleTokenizerMessage(msg) {
|
|
1438
|
-
var _a, _b, _c;
|
|
1700
|
+
var _a, _b, _c, _d;
|
|
1439
1701
|
switch (msg.type) {
|
|
1440
1702
|
case 'OZ_FRAME_READY':
|
|
1703
|
+
if (msg.__ozVersion !== PROTOCOL_VERSION) {
|
|
1704
|
+
console.warn(`[OzVault] Protocol version mismatch — SDK expects v${PROTOCOL_VERSION}, ` +
|
|
1705
|
+
`tokenizer frame reported v${typeof msg.__ozVersion === 'number' ? msg.__ozVersion : '(none)'}. ` +
|
|
1706
|
+
'This usually means the deployed frame files are stale. ' +
|
|
1707
|
+
'Deploy the matching frame assets to elements.ozura.com and purge the Azure CDN cache.');
|
|
1708
|
+
}
|
|
1441
1709
|
this.tokenizerReady = true;
|
|
1442
1710
|
if (this.loadErrorTimeoutId != null) {
|
|
1443
1711
|
clearTimeout(this.loadErrorTimeoutId);
|
|
@@ -1449,6 +1717,7 @@ class OzVault {
|
|
|
1449
1717
|
// sent again from create() once the key is available.
|
|
1450
1718
|
this.sendToTokenizer(Object.assign({ type: 'OZ_INIT', frameId: '__tokenizer__' }, (this.waxKey ? { waxKey: this.waxKey } : {})));
|
|
1451
1719
|
(_c = this._onReady) === null || _c === void 0 ? void 0 : _c.call(this);
|
|
1720
|
+
this.log('tokenizer iframe ready', { protocolVersion: (_d = msg.__ozVersion) !== null && _d !== void 0 ? _d : null });
|
|
1452
1721
|
break;
|
|
1453
1722
|
case 'OZ_TOKEN_RESULT': {
|
|
1454
1723
|
if (typeof msg.requestId !== 'string' || !msg.requestId) {
|
|
@@ -1473,11 +1742,18 @@ class OzVault {
|
|
|
1473
1742
|
}
|
|
1474
1743
|
pending.resolve(Object.assign(Object.assign({ token,
|
|
1475
1744
|
cvcSession }, (card ? { card } : {})), (pending.billing ? { billing: pending.billing } : {})));
|
|
1745
|
+
this.log('token received', {
|
|
1746
|
+
elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
|
|
1747
|
+
tokenPresent: true,
|
|
1748
|
+
cvcSessionPresent: true,
|
|
1749
|
+
cardMetadataPresent: Boolean(card),
|
|
1750
|
+
});
|
|
1476
1751
|
// Increment the per-key success counter and proactively refresh once
|
|
1477
1752
|
// the budget is exhausted so the next createToken() call uses a fresh
|
|
1478
1753
|
// key without waiting for a vault rejection.
|
|
1479
1754
|
this._tokenizeSuccessCount++;
|
|
1480
1755
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1756
|
+
this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1481
1757
|
this.refreshWaxKey().catch((err) => {
|
|
1482
1758
|
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1483
1759
|
});
|
|
@@ -1497,14 +1773,25 @@ class OzVault {
|
|
|
1497
1773
|
this.tokenizeResolvers.delete(msg.requestId);
|
|
1498
1774
|
if (pending.timeoutId != null)
|
|
1499
1775
|
clearTimeout(pending.timeoutId);
|
|
1776
|
+
const willRefresh = this.isRefreshableAuthError(errorCode, raw) && !pending.retried && Boolean(this._storedFetchWaxKey);
|
|
1777
|
+
this.log('token error', { errorCode, willRefresh });
|
|
1500
1778
|
// Auto-refresh: if the wax key expired or was consumed and we haven't
|
|
1501
1779
|
// already retried for this request, transparently re-mint and retry.
|
|
1502
|
-
if (
|
|
1780
|
+
if (willRefresh) {
|
|
1781
|
+
const resetCountAtRetry = this._resetCount;
|
|
1503
1782
|
this.refreshWaxKey().then(() => {
|
|
1504
1783
|
if (this._destroyed) {
|
|
1505
1784
|
pending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1506
1785
|
return;
|
|
1507
1786
|
}
|
|
1787
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1788
|
+
// reset() was called while the wax key was refreshing — the fields
|
|
1789
|
+
// have been cleared and _tokenizing was zeroed. Reject the original
|
|
1790
|
+
// promise so it doesn't stay pending, and bail out without starting
|
|
1791
|
+
// a new retry (which would tokenize against empty fields).
|
|
1792
|
+
pending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1508
1795
|
const newRequestId = `req-${uuid()}`;
|
|
1509
1796
|
// _tokenizing is still 'card' (cleanup() hasn't been called yet)
|
|
1510
1797
|
this.tokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, pending), { retried: true }));
|
|
@@ -1551,11 +1838,16 @@ class OzVault {
|
|
|
1551
1838
|
if (bankPending.timeoutId != null)
|
|
1552
1839
|
clearTimeout(bankPending.timeoutId);
|
|
1553
1840
|
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1841
|
+
const resetCountAtRetry = this._resetCount;
|
|
1554
1842
|
this.refreshWaxKey().then(() => {
|
|
1555
1843
|
if (this._destroyed) {
|
|
1556
1844
|
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1557
1845
|
return;
|
|
1558
1846
|
}
|
|
1847
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1848
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1559
1851
|
const newRequestId = `req-${uuid()}`;
|
|
1560
1852
|
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1561
1853
|
try {
|
|
@@ -1613,9 +1905,15 @@ class OzVault {
|
|
|
1613
1905
|
}
|
|
1614
1906
|
const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
|
|
1615
1907
|
pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
|
|
1908
|
+
this.log('bank token received', {
|
|
1909
|
+
elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
|
|
1910
|
+
tokenPresent: true,
|
|
1911
|
+
bankMetadataPresent: Boolean(bank),
|
|
1912
|
+
});
|
|
1616
1913
|
// Same proactive refresh logic as card tokenization.
|
|
1617
1914
|
this._tokenizeSuccessCount++;
|
|
1618
1915
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1916
|
+
this.log('proactive wax key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1619
1917
|
this.refreshWaxKey().catch((err) => {
|
|
1620
1918
|
console.warn('[OzVault] Post-budget wax key refresh failed:', err instanceof Error ? err.message : err);
|
|
1621
1919
|
});
|
|
@@ -1671,6 +1969,7 @@ class OzVault {
|
|
|
1671
1969
|
}
|
|
1672
1970
|
const newSessionId = uuid();
|
|
1673
1971
|
(_a = this._onWaxRefresh) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
1972
|
+
this.log('wax key refresh started');
|
|
1674
1973
|
this._waxRefreshing = this._storedFetchWaxKey(newSessionId)
|
|
1675
1974
|
.then(newWaxKey => {
|
|
1676
1975
|
if (typeof newWaxKey !== 'string' || !newWaxKey.trim()) {
|
|
@@ -1684,6 +1983,11 @@ class OzVault {
|
|
|
1684
1983
|
if (!this._destroyed && this.tokenizerReady) {
|
|
1685
1984
|
this.sendToTokenizer({ type: 'OZ_INIT', frameId: '__tokenizer__', waxKey: newWaxKey });
|
|
1686
1985
|
}
|
|
1986
|
+
this.log('wax key refresh succeeded');
|
|
1987
|
+
})
|
|
1988
|
+
.catch((err) => {
|
|
1989
|
+
this.log('wax key refresh failed', { error: err instanceof Error ? err.message : String(err) });
|
|
1990
|
+
throw err;
|
|
1687
1991
|
})
|
|
1688
1992
|
.finally(() => {
|
|
1689
1993
|
this._waxRefreshing = null;
|
|
@@ -1697,84 +2001,6 @@ class OzVault {
|
|
|
1697
2001
|
}
|
|
1698
2002
|
}
|
|
1699
2003
|
|
|
1700
|
-
/**
|
|
1701
|
-
* Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
|
|
1702
|
-
*
|
|
1703
|
-
* Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
|
|
1704
|
-
* Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
|
|
1705
|
-
* error through its normal error path.
|
|
1706
|
-
*
|
|
1707
|
-
* Each call enforces a 10-second per-attempt timeout. On a pure network-level
|
|
1708
|
-
* failure (connection refused, DNS failure, etc.) the call is retried once after
|
|
1709
|
-
* 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
|
|
1710
|
-
* an endpoint misconfiguration or an invalid key, not a transient failure.
|
|
1711
|
-
*
|
|
1712
|
-
* The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
|
|
1713
|
-
* from `@ozura/elements/server`.
|
|
1714
|
-
*
|
|
1715
|
-
* @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
|
|
1716
|
-
*
|
|
1717
|
-
* @example
|
|
1718
|
-
* // Vanilla JS
|
|
1719
|
-
* const vault = await OzVault.create({
|
|
1720
|
-
* pubKey: 'pk_live_...',
|
|
1721
|
-
* fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
|
|
1722
|
-
* });
|
|
1723
|
-
*
|
|
1724
|
-
* @example
|
|
1725
|
-
* // React
|
|
1726
|
-
* <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
|
|
1727
|
-
*/
|
|
1728
|
-
function createFetchWaxKey(mintUrl) {
|
|
1729
|
-
const TIMEOUT_MS = 10000;
|
|
1730
|
-
// Each attempt gets its own AbortController so a timeout on attempt 1 does
|
|
1731
|
-
// not bleed into the retry. Uses AbortController + setTimeout instead of
|
|
1732
|
-
// AbortSignal.timeout() to support environments without that API.
|
|
1733
|
-
const attemptFetch = (sessionId) => {
|
|
1734
|
-
const controller = new AbortController();
|
|
1735
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1736
|
-
return fetch(mintUrl, {
|
|
1737
|
-
method: 'POST',
|
|
1738
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1739
|
-
body: JSON.stringify({ sessionId }),
|
|
1740
|
-
signal: controller.signal,
|
|
1741
|
-
}).finally(() => clearTimeout(timer));
|
|
1742
|
-
};
|
|
1743
|
-
return async (sessionId) => {
|
|
1744
|
-
let res;
|
|
1745
|
-
try {
|
|
1746
|
-
res = await attemptFetch(sessionId);
|
|
1747
|
-
}
|
|
1748
|
-
catch (firstErr) {
|
|
1749
|
-
// Abort/timeout should not be retried — the server received nothing or
|
|
1750
|
-
// we already waited the full timeout duration.
|
|
1751
|
-
if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
|
|
1752
|
-
throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
|
|
1753
|
-
}
|
|
1754
|
-
// Pure network error (offline, DNS, connection refused) — retry once
|
|
1755
|
-
// after a short pause in case of a transient blip.
|
|
1756
|
-
await new Promise(resolve => setTimeout(resolve, 750));
|
|
1757
|
-
try {
|
|
1758
|
-
res = await attemptFetch(sessionId);
|
|
1759
|
-
}
|
|
1760
|
-
catch (retryErr) {
|
|
1761
|
-
const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
|
|
1762
|
-
throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
const data = await res.json().catch(() => ({}));
|
|
1766
|
-
if (!res.ok) {
|
|
1767
|
-
throw new OzError(typeof data.error === 'string' && data.error
|
|
1768
|
-
? data.error
|
|
1769
|
-
: `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
1770
|
-
}
|
|
1771
|
-
if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
|
|
1772
|
-
throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
|
|
1773
|
-
}
|
|
1774
|
-
return data.waxKey;
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
2004
|
const OzContext = react.createContext({
|
|
1779
2005
|
vault: null,
|
|
1780
2006
|
initError: null,
|
|
@@ -1791,7 +2017,7 @@ const OzContext = react.createContext({
|
|
|
1791
2017
|
* All `<OzCardNumber />`, `<OzExpiry />`, and `<OzCvv />` children must be
|
|
1792
2018
|
* rendered inside this provider.
|
|
1793
2019
|
*/
|
|
1794
|
-
function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onWaxRefresh, onReady, appearance, maxTokenizeCalls, children }) {
|
|
2020
|
+
function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loadTimeoutMs, onSessionRefresh, onWaxRefresh, onReady, appearance, sessionLimit, maxTokenizeCalls, debug, children }) {
|
|
1795
2021
|
const [vault, setVault] = react.useState(null);
|
|
1796
2022
|
const [initError, setInitError] = react.useState(null);
|
|
1797
2023
|
const [mountedCount, setMountedCount] = react.useState(0);
|
|
@@ -1799,13 +2025,14 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1799
2025
|
const [tokenizeCount, setTokenizeCount] = react.useState(0);
|
|
1800
2026
|
const onLoadErrorRef = react.useRef(onLoadError);
|
|
1801
2027
|
onLoadErrorRef.current = onLoadError;
|
|
1802
|
-
const onWaxRefreshRef = react.useRef(onWaxRefresh);
|
|
1803
|
-
onWaxRefreshRef.current = onWaxRefresh;
|
|
2028
|
+
const onWaxRefreshRef = react.useRef(onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh);
|
|
2029
|
+
onWaxRefreshRef.current = onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh;
|
|
1804
2030
|
const onReadyRef = react.useRef(onReady);
|
|
1805
2031
|
onReadyRef.current = onReady;
|
|
1806
|
-
// Keep a ref to
|
|
1807
|
-
|
|
1808
|
-
|
|
2032
|
+
// Keep a ref to the session callback so changes don't trigger vault recreation.
|
|
2033
|
+
// Priority mirrors OzVault.create(): sessionUrl > getSessionKey > fetchWaxKey.
|
|
2034
|
+
const getSessionKeyRef = react.useRef(getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey);
|
|
2035
|
+
getSessionKeyRef.current = getSessionKey !== null && getSessionKey !== void 0 ? getSessionKey : fetchWaxKey;
|
|
1809
2036
|
const appearanceKey = react.useMemo(() => appearance ? JSON.stringify(appearance) : '', [appearance]);
|
|
1810
2037
|
const fontsKey = react.useMemo(() => fonts ? JSON.stringify(fonts) : '', [fonts]);
|
|
1811
2038
|
react.useEffect(() => {
|
|
@@ -1832,7 +2059,9 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1832
2059
|
// synchronously rather than waiting for the promise to settle. Without this,
|
|
1833
2060
|
// two hidden iframes and two window listeners briefly coexist.
|
|
1834
2061
|
const abortController = new AbortController();
|
|
1835
|
-
OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(
|
|
2062
|
+
OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey }, (sessionUrl
|
|
2063
|
+
? { sessionUrl }
|
|
2064
|
+
: { getSessionKey: (sessionId) => getSessionKeyRef.current(sessionId) })), (frameBaseUrl ? { frameBaseUrl } : {})), (parsedFonts ? { fonts: parsedFonts } : {})), (parsedAppearance ? { appearance: parsedAppearance } : {})), (onLoadErrorRef.current ? { onLoadError: fireLoadError, loadTimeoutMs } : {})), {
|
|
1836
2065
|
// Always install onWaxRefresh internally so we can reset tokenizeCount
|
|
1837
2066
|
// when any wax key refresh occurs (reactive TTL expiry, post-budget
|
|
1838
2067
|
// proactive, or visibility-change proactive). Without this the React
|
|
@@ -1854,11 +2083,12 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1854
2083
|
// the retry tokenization completes (a full fetchWaxKey + tokenize round-
|
|
1855
2084
|
// trip separates them), so the count correctly resets to 0 then rises to
|
|
1856
2085
|
// 1 after the retry notifyTokenize fires.
|
|
1857
|
-
|
|
2086
|
+
onSessionRefresh: () => {
|
|
1858
2087
|
var _a;
|
|
1859
2088
|
Promise.resolve().then(() => setTokenizeCount(0));
|
|
1860
2089
|
(_a = onWaxRefreshRef.current) === null || _a === void 0 ? void 0 : _a.call(onWaxRefreshRef);
|
|
1861
|
-
}
|
|
2090
|
+
}, onReady: () => { var _a; return (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef); } }), (sessionLimit !== undefined ? { sessionLimit }
|
|
2091
|
+
: maxTokenizeCalls !== undefined ? { maxTokenizeCalls } : {})), (debug ? { debug: true } : {})), abortController.signal).then(v => {
|
|
1862
2092
|
if (cancelled) {
|
|
1863
2093
|
v.destroy();
|
|
1864
2094
|
return;
|
|
@@ -1891,7 +2121,7 @@ function OzElements({ fetchWaxKey, pubKey, frameBaseUrl, fonts, onLoadError, loa
|
|
|
1891
2121
|
setVault(null);
|
|
1892
2122
|
setInitError(null);
|
|
1893
2123
|
};
|
|
1894
|
-
}, [pubKey, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, maxTokenizeCalls]);
|
|
2124
|
+
}, [pubKey, sessionUrl, frameBaseUrl, loadTimeoutMs, appearanceKey, fontsKey, sessionLimit, maxTokenizeCalls, debug]);
|
|
1895
2125
|
const notifyMount = react.useCallback(() => setMountedCount(n => n + 1), []);
|
|
1896
2126
|
const notifyReady = react.useCallback(() => setReadyCount(n => n + 1), []);
|
|
1897
2127
|
const notifyUnmount = react.useCallback(() => {
|
|
@@ -1924,8 +2154,11 @@ function useOzElements() {
|
|
|
1924
2154
|
notifyTokenize();
|
|
1925
2155
|
return result;
|
|
1926
2156
|
}, [vault, notifyTokenize]);
|
|
2157
|
+
const reset = react.useCallback(() => {
|
|
2158
|
+
vault === null || vault === void 0 ? void 0 : vault.reset();
|
|
2159
|
+
}, [vault]);
|
|
1927
2160
|
const ready = vault !== null && vault.isReady && mountedCount > 0 && readyCount >= mountedCount;
|
|
1928
|
-
return { createToken, createBankToken, ready, initError, tokenizeCount };
|
|
2161
|
+
return { createToken, createBankToken, reset, ready, initError, tokenizeCount };
|
|
1929
2162
|
}
|
|
1930
2163
|
const SKELETON_STYLE = {
|
|
1931
2164
|
height: 46,
|
|
@@ -2020,6 +2253,71 @@ const OzCardNumber = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type
|
|
|
2020
2253
|
const OzExpiry = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "expirationDate", variant: "card" }, props));
|
|
2021
2254
|
/** Renders a PCI-isolated CVV input inside an Ozura iframe. */
|
|
2022
2255
|
const OzCvv = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign({ type: "cvv", variant: "card" }, props));
|
|
2256
|
+
// ─── Shared composite-component hook ─────────────────────────────────────────
|
|
2257
|
+
/**
|
|
2258
|
+
* Shared plumbing for OzCard and OzBankCard.
|
|
2259
|
+
*
|
|
2260
|
+
* Manages:
|
|
2261
|
+
* - Callback refs (onChange, onReady, onFocus, onBlur) kept in sync on every render
|
|
2262
|
+
* - Vault-change detection: resets `readyFieldTypes` and `onReadyFiredRef` when the
|
|
2263
|
+
* vault instance is replaced (e.g. after fetchWaxKey changes or the provider remounts)
|
|
2264
|
+
* - Per-field ready tracking: creates one stable handler per named field; fires the
|
|
2265
|
+
* `onReady` callback once all `fieldNames.length` fields have reported ready
|
|
2266
|
+
* - Error state
|
|
2267
|
+
* - Layout helpers: `gapStr`, `renderLabel`
|
|
2268
|
+
*
|
|
2269
|
+
* @internal — not exported; used only by OzCard and OzBankCard.
|
|
2270
|
+
*/
|
|
2271
|
+
function useCardBase({ vault, fieldNames, onChange, onReady, onFocus, onBlur, gap = 8, labelStyle, labelClassName, }) {
|
|
2272
|
+
const totalFields = fieldNames.length;
|
|
2273
|
+
const readyFieldTypes = react.useRef(new Set());
|
|
2274
|
+
const onReadyFiredRef = react.useRef(false);
|
|
2275
|
+
const vaultRef = react.useRef(vault);
|
|
2276
|
+
const onChangeRef = react.useRef(onChange);
|
|
2277
|
+
const onReadyRef = react.useRef(onReady);
|
|
2278
|
+
const onFocusRef = react.useRef(onFocus);
|
|
2279
|
+
const onBlurRef = react.useRef(onBlur);
|
|
2280
|
+
react.useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
|
|
2281
|
+
react.useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
|
|
2282
|
+
react.useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
|
|
2283
|
+
react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
2284
|
+
react.useEffect(() => {
|
|
2285
|
+
if (vault !== vaultRef.current) {
|
|
2286
|
+
vaultRef.current = vault;
|
|
2287
|
+
readyFieldTypes.current = new Set();
|
|
2288
|
+
onReadyFiredRef.current = false;
|
|
2289
|
+
}
|
|
2290
|
+
return () => {
|
|
2291
|
+
readyFieldTypes.current = new Set();
|
|
2292
|
+
onReadyFiredRef.current = false;
|
|
2293
|
+
};
|
|
2294
|
+
}, [vault]);
|
|
2295
|
+
// One stable handler per named field — recreated only when total field count changes.
|
|
2296
|
+
// Field names are static (card = 3 fields, bank = 2 fields) so `totalFields` alone
|
|
2297
|
+
// is a sufficient dependency; a JSON dep would create a new map on every render.
|
|
2298
|
+
// CONTRACT: `fieldNames` must be a static literal — callers must not pass a dynamic
|
|
2299
|
+
// array that changes length without also changing field count.
|
|
2300
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2301
|
+
const readyHandlers = react.useMemo(() => {
|
|
2302
|
+
const handlers = {};
|
|
2303
|
+
for (const name of fieldNames) {
|
|
2304
|
+
handlers[name] = () => {
|
|
2305
|
+
var _a;
|
|
2306
|
+
readyFieldTypes.current.add(name);
|
|
2307
|
+
if (readyFieldTypes.current.size >= totalFields && !onReadyFiredRef.current) {
|
|
2308
|
+
onReadyFiredRef.current = true;
|
|
2309
|
+
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
return handlers;
|
|
2314
|
+
}, [totalFields]); // totalFields captures fieldNames.length; field names are static
|
|
2315
|
+
const [error, setError] = react.useState();
|
|
2316
|
+
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
2317
|
+
const resolvedLabelStyle = react.useMemo(() => (labelStyle ? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE), [labelStyle]);
|
|
2318
|
+
const renderLabel = react.useCallback((text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle), [labelClassName, resolvedLabelStyle]);
|
|
2319
|
+
return { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel };
|
|
2320
|
+
}
|
|
2023
2321
|
const DEFAULT_ERROR_STYLE = {
|
|
2024
2322
|
color: '#dc2626',
|
|
2025
2323
|
fontSize: 13,
|
|
@@ -2062,62 +2360,22 @@ function mergeStyles(base, override) {
|
|
|
2062
2360
|
function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName, layout = 'default', gap = 8, hideErrors = false, errorStyle, errorClassName, renderError, onChange, onReady, onFocus, onBlur, disabled, className, placeholders, }) {
|
|
2063
2361
|
var _a, _b, _c;
|
|
2064
2362
|
const { vault } = react.useContext(OzContext);
|
|
2363
|
+
const { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel, } = useCardBase({
|
|
2364
|
+
vault,
|
|
2365
|
+
fieldNames: ['cardNumber', 'expiry', 'cvv'],
|
|
2366
|
+
onChange,
|
|
2367
|
+
onReady,
|
|
2368
|
+
onFocus,
|
|
2369
|
+
onBlur,
|
|
2370
|
+
gap,
|
|
2371
|
+
labelStyle,
|
|
2372
|
+
labelClassName,
|
|
2373
|
+
});
|
|
2065
2374
|
const fieldState = react.useRef({
|
|
2066
2375
|
cardNumber: null,
|
|
2067
2376
|
expiry: null,
|
|
2068
2377
|
cvv: null,
|
|
2069
2378
|
});
|
|
2070
|
-
const readyFieldTypes = react.useRef(new Set());
|
|
2071
|
-
const onReadyFiredRef = react.useRef(false);
|
|
2072
|
-
const vaultRef = react.useRef(vault);
|
|
2073
|
-
const onChangeRef = react.useRef(onChange);
|
|
2074
|
-
const onReadyRef = react.useRef(onReady);
|
|
2075
|
-
const onFocusRef = react.useRef(onFocus);
|
|
2076
|
-
const onBlurRef = react.useRef(onBlur);
|
|
2077
|
-
react.useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
|
|
2078
|
-
react.useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
|
|
2079
|
-
react.useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
|
|
2080
|
-
react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
2081
|
-
// When the vault is recreated (e.g. appearance/fonts props change on OzElements),
|
|
2082
|
-
// context readyCount is reset but this ref is not. Reset so onReady fires once when all 3 are ready.
|
|
2083
|
-
// The cleanup resets readyFieldTypes when the component unmounts (covers React StrictMode double-invoke
|
|
2084
|
-
// and SPA scenarios where the parent re-mounts this component).
|
|
2085
|
-
react.useEffect(() => {
|
|
2086
|
-
if (vault !== vaultRef.current) {
|
|
2087
|
-
vaultRef.current = vault;
|
|
2088
|
-
readyFieldTypes.current = new Set();
|
|
2089
|
-
onReadyFiredRef.current = false;
|
|
2090
|
-
}
|
|
2091
|
-
return () => {
|
|
2092
|
-
readyFieldTypes.current = new Set();
|
|
2093
|
-
onReadyFiredRef.current = false;
|
|
2094
|
-
};
|
|
2095
|
-
}, [vault]);
|
|
2096
|
-
const [error, setError] = react.useState();
|
|
2097
|
-
const handleCardNumberReady = react.useCallback(() => {
|
|
2098
|
-
var _a;
|
|
2099
|
-
readyFieldTypes.current.add('cardNumber');
|
|
2100
|
-
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2101
|
-
onReadyFiredRef.current = true;
|
|
2102
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2103
|
-
}
|
|
2104
|
-
}, []);
|
|
2105
|
-
const handleExpiryReady = react.useCallback(() => {
|
|
2106
|
-
var _a;
|
|
2107
|
-
readyFieldTypes.current.add('expiry');
|
|
2108
|
-
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2109
|
-
onReadyFiredRef.current = true;
|
|
2110
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2111
|
-
}
|
|
2112
|
-
}, []);
|
|
2113
|
-
const handleCvvReady = react.useCallback(() => {
|
|
2114
|
-
var _a;
|
|
2115
|
-
readyFieldTypes.current.add('cvv');
|
|
2116
|
-
if (readyFieldTypes.current.size >= 3 && !onReadyFiredRef.current) {
|
|
2117
|
-
onReadyFiredRef.current = true;
|
|
2118
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2119
|
-
}
|
|
2120
|
-
}, []);
|
|
2121
2379
|
const emitChange = react.useCallback(() => {
|
|
2122
2380
|
var _a;
|
|
2123
2381
|
const { cardNumber, expiry, cvv } = fieldState.current;
|
|
@@ -2132,20 +2390,16 @@ function OzCard({ style, styles, classNames, labels, labelStyle, labelClassName,
|
|
|
2132
2390
|
error: err,
|
|
2133
2391
|
fields: Object.assign({}, fieldState.current),
|
|
2134
2392
|
});
|
|
2135
|
-
}, []);
|
|
2136
|
-
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
2137
|
-
const resolvedLabelStyle = labelStyle
|
|
2138
|
-
? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
|
|
2139
|
-
const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
|
|
2393
|
+
}, [setError, onChangeRef]);
|
|
2140
2394
|
const showError = !hideErrors && error;
|
|
2141
2395
|
const errorNode = showError
|
|
2142
2396
|
? renderError
|
|
2143
2397
|
? renderError(error)
|
|
2144
2398
|
: (jsxRuntime.jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
|
|
2145
2399
|
: null;
|
|
2146
|
-
const cardNumberField = (jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cardNumber), jsxRuntime.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:
|
|
2147
|
-
const expiryField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.expiry), jsxRuntime.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:
|
|
2148
|
-
const cvvField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cvv), jsxRuntime.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:
|
|
2400
|
+
const cardNumberField = (jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cardNumber), jsxRuntime.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'] })] }));
|
|
2401
|
+
const expiryField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.expiry), jsxRuntime.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'] })] }));
|
|
2402
|
+
const cvvField = (jsxRuntime.jsxs("div", { style: layout === 'default' ? { flex: 1 } : undefined, children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.cvv), jsxRuntime.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'] })] }));
|
|
2149
2403
|
if (layout === 'rows') {
|
|
2150
2404
|
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [cardNumberField, expiryField, cvvField, errorNode] }));
|
|
2151
2405
|
}
|
|
@@ -2166,49 +2420,21 @@ const OzBankRoutingNumber = (props) => jsxRuntime.jsx(OzFieldBase, Object.assign
|
|
|
2166
2420
|
function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassName, gap = 8, hideErrors = false, errorStyle, errorClassName, renderError, onChange, onReady, onFocus, onBlur, disabled, className, placeholders, }) {
|
|
2167
2421
|
var _a, _b;
|
|
2168
2422
|
const { vault } = react.useContext(OzContext);
|
|
2423
|
+
const { onChangeRef, onFocusRef, onBlurRef, readyHandlers, error, setError, gapStr, renderLabel, } = useCardBase({
|
|
2424
|
+
vault,
|
|
2425
|
+
fieldNames: ['accountNumber', 'routingNumber'],
|
|
2426
|
+
onChange,
|
|
2427
|
+
onReady,
|
|
2428
|
+
onFocus,
|
|
2429
|
+
onBlur,
|
|
2430
|
+
gap,
|
|
2431
|
+
labelStyle,
|
|
2432
|
+
labelClassName,
|
|
2433
|
+
});
|
|
2169
2434
|
const fieldState = react.useRef({
|
|
2170
2435
|
accountNumber: null,
|
|
2171
2436
|
routingNumber: null,
|
|
2172
2437
|
});
|
|
2173
|
-
const readyFieldTypes = react.useRef(new Set());
|
|
2174
|
-
const onReadyFiredRef = react.useRef(false);
|
|
2175
|
-
const vaultRef = react.useRef(vault);
|
|
2176
|
-
const onChangeRef = react.useRef(onChange);
|
|
2177
|
-
const onReadyRef = react.useRef(onReady);
|
|
2178
|
-
const onFocusRef = react.useRef(onFocus);
|
|
2179
|
-
const onBlurRef = react.useRef(onBlur);
|
|
2180
|
-
react.useEffect(() => { onChangeRef.current = onChange; }, [onChange]);
|
|
2181
|
-
react.useEffect(() => { onReadyRef.current = onReady; }, [onReady]);
|
|
2182
|
-
react.useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]);
|
|
2183
|
-
react.useEffect(() => { onBlurRef.current = onBlur; }, [onBlur]);
|
|
2184
|
-
react.useEffect(() => {
|
|
2185
|
-
if (vault !== vaultRef.current) {
|
|
2186
|
-
vaultRef.current = vault;
|
|
2187
|
-
readyFieldTypes.current = new Set();
|
|
2188
|
-
onReadyFiredRef.current = false;
|
|
2189
|
-
}
|
|
2190
|
-
return () => {
|
|
2191
|
-
readyFieldTypes.current = new Set();
|
|
2192
|
-
onReadyFiredRef.current = false;
|
|
2193
|
-
};
|
|
2194
|
-
}, [vault]);
|
|
2195
|
-
const [error, setError] = react.useState();
|
|
2196
|
-
const handleAccountReady = react.useCallback(() => {
|
|
2197
|
-
var _a;
|
|
2198
|
-
readyFieldTypes.current.add('accountNumber');
|
|
2199
|
-
if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
|
|
2200
|
-
onReadyFiredRef.current = true;
|
|
2201
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2202
|
-
}
|
|
2203
|
-
}, []);
|
|
2204
|
-
const handleRoutingReady = react.useCallback(() => {
|
|
2205
|
-
var _a;
|
|
2206
|
-
readyFieldTypes.current.add('routingNumber');
|
|
2207
|
-
if (readyFieldTypes.current.size >= 2 && !onReadyFiredRef.current) {
|
|
2208
|
-
onReadyFiredRef.current = true;
|
|
2209
|
-
(_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
|
|
2210
|
-
}
|
|
2211
|
-
}, []);
|
|
2212
2438
|
const emitChange = react.useCallback(() => {
|
|
2213
2439
|
var _a;
|
|
2214
2440
|
const { accountNumber, routingNumber } = fieldState.current;
|
|
@@ -2221,18 +2447,14 @@ function OzBankCard({ style, styles, classNames, labels, labelStyle, labelClassN
|
|
|
2221
2447
|
error: err,
|
|
2222
2448
|
fields: Object.assign({}, fieldState.current),
|
|
2223
2449
|
});
|
|
2224
|
-
}, []);
|
|
2225
|
-
const gapStr = typeof gap === 'string' ? gap : `${gap}px`;
|
|
2226
|
-
const resolvedLabelStyle = labelStyle
|
|
2227
|
-
? Object.assign(Object.assign({}, DEFAULT_LABEL_STYLE), labelStyle) : DEFAULT_LABEL_STYLE;
|
|
2228
|
-
const renderLabel = (text) => renderFieldLabel(text, labelClassName, resolvedLabelStyle);
|
|
2450
|
+
}, [setError, onChangeRef]);
|
|
2229
2451
|
const showError = !hideErrors && error;
|
|
2230
2452
|
const errorNode = showError
|
|
2231
2453
|
? renderError
|
|
2232
2454
|
? renderError(error)
|
|
2233
2455
|
: (jsxRuntime.jsx("div", { role: "alert", className: errorClassName, style: errorStyle ? Object.assign(Object.assign({}, DEFAULT_ERROR_STYLE), errorStyle) : DEFAULT_ERROR_STYLE, children: error }))
|
|
2234
2456
|
: null;
|
|
2235
|
-
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsxRuntime.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:
|
|
2457
|
+
return (jsxRuntime.jsxs("div", { className: className, style: { width: '100%', display: 'flex', flexDirection: 'column', gap: gapStr }, children: [jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.accountNumber), jsxRuntime.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'] })] }), jsxRuntime.jsxs("div", { children: [renderLabel(labels === null || labels === void 0 ? void 0 : labels.routingNumber), jsxRuntime.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] }));
|
|
2236
2458
|
}
|
|
2237
2459
|
|
|
2238
2460
|
exports.OzBankAccountNumber = OzBankAccountNumber;
|
|
@@ -2243,6 +2465,6 @@ exports.OzCardNumber = OzCardNumber;
|
|
|
2243
2465
|
exports.OzCvv = OzCvv;
|
|
2244
2466
|
exports.OzElements = OzElements;
|
|
2245
2467
|
exports.OzExpiry = OzExpiry;
|
|
2246
|
-
exports.createFetchWaxKey =
|
|
2468
|
+
exports.createFetchWaxKey = createSessionFetcher;
|
|
2247
2469
|
exports.useOzElements = useOzElements;
|
|
2248
2470
|
//# sourceMappingURL=index.cjs.js.map
|