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