@ozura/elements 1.1.0 → 1.2.0-next.27
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 +73 -71
- package/dist/frame/element-frame.js +48 -3
- package/dist/frame/element-frame.js.map +1 -1
- package/dist/frame/tokenizer-frame.html +1 -1
- package/dist/frame/tokenizer-frame.js +113 -52
- package/dist/frame/tokenizer-frame.js.map +1 -1
- package/dist/oz-elements.esm.js +363 -311
- package/dist/oz-elements.esm.js.map +1 -1
- package/dist/oz-elements.umd.js +364 -311
- package/dist/oz-elements.umd.js.map +1 -1
- package/dist/react/frame/elementFrame.d.ts +7 -0
- package/dist/react/frame/tokenizerFrame.d.ts +4 -1
- package/dist/react/index.cjs.js +239 -180
- package/dist/react/index.cjs.js.map +1 -1
- package/dist/react/index.esm.js +238 -180
- package/dist/react/index.esm.js.map +1 -1
- package/dist/react/react/index.d.ts +50 -29
- package/dist/react/sdk/OzElement.d.ts +3 -1
- package/dist/react/sdk/OzVault.d.ts +9 -14
- 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 +70 -41
- package/dist/server/frame/elementFrame.d.ts +7 -0
- package/dist/server/frame/tokenizerFrame.d.ts +4 -1
- package/dist/server/index.cjs.js +188 -90
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.esm.js +187 -91
- package/dist/server/index.esm.js.map +1 -1
- package/dist/server/sdk/OzElement.d.ts +3 -1
- package/dist/server/sdk/OzVault.d.ts +9 -14
- 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 +70 -41
- package/dist/types/frame/elementFrame.d.ts +7 -0
- package/dist/types/frame/tokenizerFrame.d.ts +4 -1
- package/dist/types/sdk/OzElement.d.ts +3 -1
- package/dist/types/sdk/OzVault.d.ts +9 -14
- 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 +70 -41
- package/package.json +1 -1
package/dist/oz-elements.umd.js
CHANGED
|
@@ -4,147 +4,6 @@
|
|
|
4
4
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.OzElements = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* errors.ts — error types and normalisation for OzElements.
|
|
9
|
-
*
|
|
10
|
-
* Three normalisation functions:
|
|
11
|
-
* - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
|
|
12
|
-
* - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
|
|
13
|
-
* - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
|
|
14
|
-
*
|
|
15
|
-
* Error keys in normalizeCardSaleError are taken directly from checkout's
|
|
16
|
-
* errorMapping.ts so the same error strings produce the same user-facing copy.
|
|
17
|
-
*/
|
|
18
|
-
const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
|
|
19
|
-
/** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
|
|
20
|
-
function isOzErrorCode(value) {
|
|
21
|
-
return typeof value === 'string' && OZ_ERROR_CODES.has(value);
|
|
22
|
-
}
|
|
23
|
-
class OzError extends Error {
|
|
24
|
-
constructor(message, raw, errorCode) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.name = 'OzError';
|
|
27
|
-
this.raw = raw !== null && raw !== void 0 ? raw : message;
|
|
28
|
-
this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
|
|
29
|
-
this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/** Shared patterns that apply to both card and bank vault errors. */
|
|
33
|
-
function normalizeCommonVaultError(msg) {
|
|
34
|
-
if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
|
|
35
|
-
return 'Authentication failed. Check your vault API key configuration.';
|
|
36
|
-
}
|
|
37
|
-
if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
|
|
38
|
-
return 'A network error occurred. Please check your connection and try again.';
|
|
39
|
-
}
|
|
40
|
-
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
41
|
-
return 'The request timed out. Please try again.';
|
|
42
|
-
}
|
|
43
|
-
if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
|
|
44
|
-
return 'A server error occurred. Please try again shortly.';
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Maps a raw vault /tokenize error string to a user-facing message for card flows.
|
|
50
|
-
* Falls back to the original string if no pattern matches.
|
|
51
|
-
*/
|
|
52
|
-
function normalizeVaultError(raw) {
|
|
53
|
-
const msg = raw.toLowerCase();
|
|
54
|
-
if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
|
|
55
|
-
return 'The card number is invalid. Please check and try again.';
|
|
56
|
-
}
|
|
57
|
-
if (msg.includes('expir')) {
|
|
58
|
-
return 'The card expiration date is invalid or the card has expired.';
|
|
59
|
-
}
|
|
60
|
-
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
61
|
-
return 'The CVV code is invalid. Please check and try again.';
|
|
62
|
-
}
|
|
63
|
-
if (msg.includes('insufficient') || msg.includes('funds')) {
|
|
64
|
-
return 'Your card has insufficient funds. Please use a different card.';
|
|
65
|
-
}
|
|
66
|
-
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
67
|
-
return 'Your card was declined. Please try a different card or contact your bank.';
|
|
68
|
-
}
|
|
69
|
-
const common = normalizeCommonVaultError(msg);
|
|
70
|
-
if (common)
|
|
71
|
-
return common;
|
|
72
|
-
return raw;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
|
|
76
|
-
* Uses bank-specific pattern matching so card-specific messages are never shown for
|
|
77
|
-
* bank errors. Falls back to the original string if no pattern matches.
|
|
78
|
-
*/
|
|
79
|
-
function normalizeBankVaultError(raw) {
|
|
80
|
-
const msg = raw.toLowerCase();
|
|
81
|
-
if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
|
|
82
|
-
return 'The bank account number is invalid. Please check and try again.';
|
|
83
|
-
}
|
|
84
|
-
if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
|
|
85
|
-
return 'The routing number is invalid. Please check and try again.';
|
|
86
|
-
}
|
|
87
|
-
const common = normalizeCommonVaultError(msg);
|
|
88
|
-
if (common)
|
|
89
|
-
return common;
|
|
90
|
-
return raw;
|
|
91
|
-
}
|
|
92
|
-
// ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
|
|
93
|
-
const CARD_SALE_ERROR_MAP = [
|
|
94
|
-
['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
|
|
95
|
-
['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
|
|
96
|
-
['Card expired', 'Your card has expired. Please use a different card.'],
|
|
97
|
-
['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
|
|
98
|
-
['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
|
|
99
|
-
['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
100
|
-
['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
101
|
-
['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
|
|
102
|
-
['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
|
|
103
|
-
['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
|
|
104
|
-
['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
|
|
105
|
-
['API key not found', 'Authentication failed. Please refresh the page.'],
|
|
106
|
-
['Access token expired', 'Your session has expired. Please refresh the page.'],
|
|
107
|
-
['Access token is invalid', 'Authentication failed. Please refresh the page.'],
|
|
108
|
-
['Unauthorized', 'Authentication failed. Please refresh the page.'],
|
|
109
|
-
['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
|
|
110
|
-
['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
|
|
111
|
-
['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
|
|
112
|
-
['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
|
|
113
|
-
['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
|
|
114
|
-
['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
|
|
115
|
-
['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
|
|
116
|
-
];
|
|
117
|
-
/**
|
|
118
|
-
* Maps a raw Ozura Pay API cardSale error string to a user-facing message.
|
|
119
|
-
*
|
|
120
|
-
* Uses the exact same error key table as checkout's `getUserFriendlyError()` in
|
|
121
|
-
* errorMapping.ts so both surfaces produce identical copy for the same errors.
|
|
122
|
-
*
|
|
123
|
-
* Falls back to the original string when it's under 100 characters, or to a
|
|
124
|
-
* generic message for long/opaque server errors — matching checkout's fallback
|
|
125
|
-
* behaviour exactly.
|
|
126
|
-
*
|
|
127
|
-
* **Trade-off:** Short unrecognised strings (e.g. processor codes like
|
|
128
|
-
* `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
|
|
129
|
-
* checkout so the same raw Pay API errors produce the same user-facing text on
|
|
130
|
-
* both surfaces. If the Pay API ever returns internal codes that should never
|
|
131
|
-
* reach the UI, the fix belongs in the Pay API error normalisation layer rather
|
|
132
|
-
* than here.
|
|
133
|
-
*/
|
|
134
|
-
function normalizeCardSaleError(raw) {
|
|
135
|
-
if (!raw)
|
|
136
|
-
return 'Payment processing failed. Please try again.';
|
|
137
|
-
for (const [key, message] of CARD_SALE_ERROR_MAP) {
|
|
138
|
-
if (raw.toLowerCase().includes(key.toLowerCase())) {
|
|
139
|
-
return message;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Checkout fallback: pass through short errors, genericise long ones
|
|
143
|
-
if (raw.length < 100)
|
|
144
|
-
return raw;
|
|
145
|
-
return 'Payment processing failed. Please try again or contact support.';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
7
|
const THEME_DEFAULT = {
|
|
149
8
|
base: {
|
|
150
9
|
color: '#1a1a2e',
|
|
@@ -309,6 +168,147 @@
|
|
|
309
168
|
return mergeStyleConfigs(appearance, elementStyle);
|
|
310
169
|
}
|
|
311
170
|
|
|
171
|
+
/**
|
|
172
|
+
* errors.ts — error types and normalisation for OzElements.
|
|
173
|
+
*
|
|
174
|
+
* Three normalisation functions:
|
|
175
|
+
* - normalizeVaultError — maps raw vault /tokenize errors to user-facing messages (card flows)
|
|
176
|
+
* - normalizeBankVaultError — maps raw vault /tokenize errors to user-facing messages (bank/ACH flows)
|
|
177
|
+
* - normalizeCardSaleError — maps raw cardSale API errors to user-facing messages
|
|
178
|
+
*
|
|
179
|
+
* Error keys in normalizeCardSaleError are taken directly from checkout's
|
|
180
|
+
* errorMapping.ts so the same error strings produce the same user-facing copy.
|
|
181
|
+
*/
|
|
182
|
+
const OZ_ERROR_CODES = new Set(['network', 'timeout', 'auth', 'validation', 'server', 'config', 'unknown']);
|
|
183
|
+
/** Returns true and narrows to OzErrorCode when `value` is a valid member of the union. */
|
|
184
|
+
function isOzErrorCode(value) {
|
|
185
|
+
return typeof value === 'string' && OZ_ERROR_CODES.has(value);
|
|
186
|
+
}
|
|
187
|
+
class OzError extends Error {
|
|
188
|
+
constructor(message, raw, errorCode) {
|
|
189
|
+
super(message);
|
|
190
|
+
this.name = 'OzError';
|
|
191
|
+
this.raw = raw !== null && raw !== void 0 ? raw : message;
|
|
192
|
+
this.errorCode = errorCode !== null && errorCode !== void 0 ? errorCode : 'unknown';
|
|
193
|
+
this.retryable = this.errorCode === 'network' || this.errorCode === 'timeout' || this.errorCode === 'server';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Shared patterns that apply to both card and bank vault errors. */
|
|
197
|
+
function normalizeCommonVaultError(msg) {
|
|
198
|
+
if (msg.includes('api key') || msg.includes('unauthorized') || msg.includes('authentication') || msg.includes('forbidden')) {
|
|
199
|
+
return 'Authentication failed. Check your vault API key configuration.';
|
|
200
|
+
}
|
|
201
|
+
if (msg.includes('network') || msg.includes('failed to fetch') || msg.includes('networkerror')) {
|
|
202
|
+
return 'A network error occurred. Please check your connection and try again.';
|
|
203
|
+
}
|
|
204
|
+
if (msg.includes('timeout') || msg.includes('timed out')) {
|
|
205
|
+
return 'The request timed out. Please try again.';
|
|
206
|
+
}
|
|
207
|
+
if (msg.includes('http 5') || msg.includes('500') || msg.includes('502') || msg.includes('503')) {
|
|
208
|
+
return 'A server error occurred. Please try again shortly.';
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Maps a raw vault /tokenize error string to a user-facing message for card flows.
|
|
214
|
+
* Falls back to the original string if no pattern matches.
|
|
215
|
+
*/
|
|
216
|
+
function normalizeVaultError(raw) {
|
|
217
|
+
const msg = raw.toLowerCase();
|
|
218
|
+
if (msg.includes('card number') || msg.includes('invalid card') || msg.includes('luhn')) {
|
|
219
|
+
return 'The card number is invalid. Please check and try again.';
|
|
220
|
+
}
|
|
221
|
+
if (msg.includes('expir')) {
|
|
222
|
+
return 'The card expiration date is invalid or the card has expired.';
|
|
223
|
+
}
|
|
224
|
+
if (msg.includes('cvv') || msg.includes('cvc') || msg.includes('security code')) {
|
|
225
|
+
return 'The CVV code is invalid. Please check and try again.';
|
|
226
|
+
}
|
|
227
|
+
if (msg.includes('insufficient') || msg.includes('funds')) {
|
|
228
|
+
return 'Your card has insufficient funds. Please use a different card.';
|
|
229
|
+
}
|
|
230
|
+
if (msg.includes('declined') || msg.includes('do not honor')) {
|
|
231
|
+
return 'Your card was declined. Please try a different card or contact your bank.';
|
|
232
|
+
}
|
|
233
|
+
const common = normalizeCommonVaultError(msg);
|
|
234
|
+
if (common)
|
|
235
|
+
return common;
|
|
236
|
+
return raw;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Maps a raw vault /tokenize error string to a user-facing message for bank (ACH) flows.
|
|
240
|
+
* Uses bank-specific pattern matching so card-specific messages are never shown for
|
|
241
|
+
* bank errors. Falls back to the original string if no pattern matches.
|
|
242
|
+
*/
|
|
243
|
+
function normalizeBankVaultError(raw) {
|
|
244
|
+
const msg = raw.toLowerCase();
|
|
245
|
+
if (msg.includes('account number') || msg.includes('account_number') || msg.includes('invalid account')) {
|
|
246
|
+
return 'The bank account number is invalid. Please check and try again.';
|
|
247
|
+
}
|
|
248
|
+
if (msg.includes('routing number') || msg.includes('routing_number') || msg.includes('invalid routing') || /\baba\b/.test(msg)) {
|
|
249
|
+
return 'The routing number is invalid. Please check and try again.';
|
|
250
|
+
}
|
|
251
|
+
const common = normalizeCommonVaultError(msg);
|
|
252
|
+
if (common)
|
|
253
|
+
return common;
|
|
254
|
+
return raw;
|
|
255
|
+
}
|
|
256
|
+
// ─── cardSale error map (mirrors checkout/src/utils/errorMapping.ts exactly) ─
|
|
257
|
+
const CARD_SALE_ERROR_MAP = [
|
|
258
|
+
['Insufficient Funds', 'Your card has insufficient funds. Please use a different payment method.'],
|
|
259
|
+
['Invalid card number', 'The card number you entered is invalid. Please check and try again.'],
|
|
260
|
+
['Card expired', 'Your card has expired. Please use a different card.'],
|
|
261
|
+
['CVV Verification Failed', 'The CVV code you entered is incorrect. Please check and try again.'],
|
|
262
|
+
['Address Verification Failed', 'The billing address does not match your card. Please verify your address.'],
|
|
263
|
+
['Do Not Honor', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
264
|
+
['Declined', 'Your card was declined. Please contact your bank or use a different payment method.'],
|
|
265
|
+
['Surcharge is currently not supported', 'Surcharge fees are not supported at this time.'],
|
|
266
|
+
['Surcharge percent must be between', 'Surcharge fees are not supported at this time.'],
|
|
267
|
+
['Forbidden - API key', 'Authentication failed. Please refresh the page.'],
|
|
268
|
+
['Api Key is invalid', 'Authentication failed. Please refresh the page.'],
|
|
269
|
+
['API key not found', 'Authentication failed. Please refresh the page.'],
|
|
270
|
+
['Access token expired', 'Your session has expired. Please refresh the page.'],
|
|
271
|
+
['Access token is invalid', 'Authentication failed. Please refresh the page.'],
|
|
272
|
+
['Unauthorized', 'Authentication failed. Please refresh the page.'],
|
|
273
|
+
['Too Many Requests', 'Too many requests. Please wait a moment and try again.'],
|
|
274
|
+
['Rate limit exceeded', 'System is busy. Please wait a moment and try again.'],
|
|
275
|
+
['No processor integrations found', 'Payment processing is not configured for this merchant. Please contact the merchant for assistance.'],
|
|
276
|
+
['processor integration', 'Payment processing is temporarily unavailable. Please try again later or contact the merchant.'],
|
|
277
|
+
['Invalid zipcode', 'The ZIP code you entered is invalid. Please check and try again.'],
|
|
278
|
+
['failed to save to database', 'Your payment was processed but we encountered an issue. Please contact support.'],
|
|
279
|
+
['CASHBACK UNAVAIL', 'This transaction type is not supported. Please try again or use a different payment method.'],
|
|
280
|
+
];
|
|
281
|
+
/**
|
|
282
|
+
* Maps a raw Ozura Pay API cardSale error string to a user-facing message.
|
|
283
|
+
*
|
|
284
|
+
* Uses the exact same error key table as checkout's `getUserFriendlyError()` in
|
|
285
|
+
* errorMapping.ts so both surfaces produce identical copy for the same errors.
|
|
286
|
+
*
|
|
287
|
+
* Falls back to the original string when it's under 100 characters, or to a
|
|
288
|
+
* generic message for long/opaque server errors — matching checkout's fallback
|
|
289
|
+
* behaviour exactly.
|
|
290
|
+
*
|
|
291
|
+
* **Trade-off:** Short unrecognised strings (e.g. processor codes like
|
|
292
|
+
* `"PROC_TIMEOUT"`) are passed through verbatim. This intentionally mirrors
|
|
293
|
+
* checkout so the same raw Pay API errors produce the same user-facing text on
|
|
294
|
+
* both surfaces. If the Pay API ever returns internal codes that should never
|
|
295
|
+
* reach the UI, the fix belongs in the Pay API error normalisation layer rather
|
|
296
|
+
* than here.
|
|
297
|
+
*/
|
|
298
|
+
function normalizeCardSaleError(raw) {
|
|
299
|
+
if (!raw)
|
|
300
|
+
return 'Payment processing failed. Please try again.';
|
|
301
|
+
for (const [key, message] of CARD_SALE_ERROR_MAP) {
|
|
302
|
+
if (raw.toLowerCase().includes(key.toLowerCase())) {
|
|
303
|
+
return message;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Checkout fallback: pass through short errors, genericise long ones
|
|
307
|
+
if (raw.length < 100)
|
|
308
|
+
return raw;
|
|
309
|
+
return 'Payment processing failed. Please try again or contact support.';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
312
|
/**
|
|
313
313
|
* Generates a RFC 4122 v4 UUID.
|
|
314
314
|
*
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
* it never holds raw card data — all sensitive values live in the iframe.
|
|
383
383
|
*/
|
|
384
384
|
class OzElement {
|
|
385
|
-
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle) {
|
|
385
|
+
constructor(elementType, options, vaultId, frameBaseUrl, fonts = [], appearanceStyle, onDestroy) {
|
|
386
386
|
this.iframe = null;
|
|
387
387
|
this._frameWindow = null;
|
|
388
388
|
this._ready = false;
|
|
@@ -398,6 +398,7 @@
|
|
|
398
398
|
this.fonts = fonts;
|
|
399
399
|
this.appearanceStyle = appearanceStyle;
|
|
400
400
|
this.frameId = `oz-${elementType}-${uuid()}`;
|
|
401
|
+
this._onDestroy = onDestroy;
|
|
401
402
|
}
|
|
402
403
|
/** The element type this proxy represents. */
|
|
403
404
|
get type() {
|
|
@@ -553,9 +554,14 @@
|
|
|
553
554
|
* and prevents future use. Distinct from `unmount()` which allows re-mounting.
|
|
554
555
|
*/
|
|
555
556
|
destroy() {
|
|
557
|
+
var _a;
|
|
556
558
|
this.unmount();
|
|
557
559
|
this.handlers.clear();
|
|
558
560
|
this._destroyed = true;
|
|
561
|
+
// Notify OzVault so it can prune the stale frameId entry from its elements
|
|
562
|
+
// and completionState maps. Without this, manually calling el.destroy() leaks
|
|
563
|
+
// map entries that grow unboundedly in SPA scenarios with repeated mount/unmount.
|
|
564
|
+
(_a = this._onDestroy) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
559
565
|
}
|
|
560
566
|
// ─── Called by OzVault ───────────────────────────────────────────────────
|
|
561
567
|
/**
|
|
@@ -896,13 +902,116 @@
|
|
|
896
902
|
*/
|
|
897
903
|
const PROTOCOL_VERSION = 1;
|
|
898
904
|
|
|
905
|
+
/**
|
|
906
|
+
* Creates a `getSessionKey` callback for `OzVault.create()` and `<OzElements>`.
|
|
907
|
+
*
|
|
908
|
+
* This is the recommended way to wire the SDK to your backend session endpoint.
|
|
909
|
+
* If you don't need custom headers or auth logic, pass `sessionUrl` directly to
|
|
910
|
+
* `OzVault.create()` or `<OzElements>` — it calls this helper internally.
|
|
911
|
+
*
|
|
912
|
+
* The callback POSTs `{ sessionId }` to `url` and reads `sessionKey` (or the
|
|
913
|
+
* legacy `waxKey`) from the JSON response, so it is compatible with both the
|
|
914
|
+
* new `createSessionMiddleware` and the old `createMintWaxMiddleware` backends.
|
|
915
|
+
*
|
|
916
|
+
* Each call enforces a **10-second timeout**. On pure network failures
|
|
917
|
+
* (offline, DNS, connection refused) the request is retried **once after 750ms**.
|
|
918
|
+
* HTTP 4xx/5xx errors are never retried — they indicate misconfiguration.
|
|
919
|
+
*
|
|
920
|
+
* @param url - Absolute or relative URL of your session endpoint, e.g. `'/api/oz-session'`.
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* // Simplest — just pass sessionUrl, no need to call this directly
|
|
924
|
+
* const vault = await OzVault.create({ pubKey: 'pk_live_...', sessionUrl: '/api/oz-session' });
|
|
925
|
+
*
|
|
926
|
+
* @example
|
|
927
|
+
* // Manual — use when you need custom headers
|
|
928
|
+
* const vault = await OzVault.create({
|
|
929
|
+
* pubKey: 'pk_live_...',
|
|
930
|
+
* getSessionKey: createSessionFetcher('/api/oz-session'),
|
|
931
|
+
* });
|
|
932
|
+
*/
|
|
933
|
+
function createSessionFetcher(url) {
|
|
934
|
+
const TIMEOUT_MS = 10000;
|
|
935
|
+
// Each attempt gets its own AbortController so a timeout on attempt 1 does
|
|
936
|
+
// not bleed into the retry.
|
|
937
|
+
const attemptFetch = (sessionId) => {
|
|
938
|
+
const controller = new AbortController();
|
|
939
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
940
|
+
return fetch(url, {
|
|
941
|
+
method: 'POST',
|
|
942
|
+
headers: { 'Content-Type': 'application/json' },
|
|
943
|
+
body: JSON.stringify({ sessionId }),
|
|
944
|
+
signal: controller.signal,
|
|
945
|
+
}).finally(() => clearTimeout(timer));
|
|
946
|
+
};
|
|
947
|
+
return async (sessionId) => {
|
|
948
|
+
let res;
|
|
949
|
+
try {
|
|
950
|
+
res = await attemptFetch(sessionId);
|
|
951
|
+
}
|
|
952
|
+
catch (firstErr) {
|
|
953
|
+
// Timeout/abort — don't retry, we already waited the full duration.
|
|
954
|
+
if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
|
|
955
|
+
throw new OzError(`Session endpoint timed out after ${TIMEOUT_MS / 1000}s (${url})`, undefined, 'timeout');
|
|
956
|
+
}
|
|
957
|
+
// Pure network error — retry once after a short pause.
|
|
958
|
+
await new Promise(resolve => setTimeout(resolve, 750));
|
|
959
|
+
try {
|
|
960
|
+
res = await attemptFetch(sessionId);
|
|
961
|
+
}
|
|
962
|
+
catch (retryErr) {
|
|
963
|
+
const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
|
|
964
|
+
throw new OzError(`Could not reach session endpoint (${url}): ${msg}`, undefined, 'network');
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// Parse JSON separately from the ok-check so that a non-JSON error body
|
|
968
|
+
// (HTML error page, WAF block, CDN 503) produces the right error code.
|
|
969
|
+
// Previously res.json() was attempted before res.ok was checked; a parse
|
|
970
|
+
// failure on a 5xx HTML body would fall through as {} and produce a
|
|
971
|
+
// misleading 'validation' code when the real cause is a server/network issue.
|
|
972
|
+
let data = {};
|
|
973
|
+
try {
|
|
974
|
+
data = await res.json();
|
|
975
|
+
}
|
|
976
|
+
catch (_a) {
|
|
977
|
+
if (!res.ok) {
|
|
978
|
+
throw new OzError(`Session endpoint returned HTTP ${res.status} with a non-JSON body`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
979
|
+
}
|
|
980
|
+
// HTTP 200 but body isn't JSON — this is a misconfigured session endpoint.
|
|
981
|
+
throw new OzError('Session endpoint returned HTTP 200 but the response body is not valid JSON. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
982
|
+
}
|
|
983
|
+
if (!res.ok) {
|
|
984
|
+
throw new OzError(typeof data.error === 'string' && data.error
|
|
985
|
+
? data.error
|
|
986
|
+
: `Session endpoint returned HTTP ${res.status}`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
987
|
+
}
|
|
988
|
+
// Accept both new `sessionKey` and legacy `waxKey` for backward compatibility
|
|
989
|
+
// with backends that haven't migrated to createSessionMiddleware yet.
|
|
990
|
+
const key = (typeof data.sessionKey === 'string' ? data.sessionKey : '') ||
|
|
991
|
+
(typeof data.waxKey === 'string' ? data.waxKey : '');
|
|
992
|
+
if (!key.trim()) {
|
|
993
|
+
throw new OzError('Session endpoint response is missing sessionKey. Check your /api/oz-session implementation.', undefined, 'validation');
|
|
994
|
+
}
|
|
995
|
+
return key;
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
899
999
|
function isCardMetadata(v) {
|
|
900
|
-
|
|
1000
|
+
if (!v || typeof v !== 'object')
|
|
1001
|
+
return false;
|
|
1002
|
+
const r = v;
|
|
1003
|
+
return (typeof r.last4 === 'string' &&
|
|
1004
|
+
typeof r.brand === 'string' &&
|
|
1005
|
+
typeof r.expMonth === 'string' &&
|
|
1006
|
+
typeof r.expYear === 'string');
|
|
901
1007
|
}
|
|
902
1008
|
function isBankAccountMetadata(v) {
|
|
903
|
-
|
|
1009
|
+
if (!v || typeof v !== 'object')
|
|
1010
|
+
return false;
|
|
1011
|
+
const r = v;
|
|
1012
|
+
return typeof r.last4 === 'string' && typeof r.routingNumberLast4 === 'string';
|
|
904
1013
|
}
|
|
905
|
-
const DEFAULT_FRAME_BASE_URL = "https://
|
|
1014
|
+
const DEFAULT_FRAME_BASE_URL = "https://lively-hill-097170c0f.4.azurestaticapps.net";
|
|
906
1015
|
/**
|
|
907
1016
|
* The main entry point for OzElements. Creates and manages iframe-based
|
|
908
1017
|
* card input elements that keep raw card data isolated from the merchant page.
|
|
@@ -910,16 +1019,10 @@
|
|
|
910
1019
|
* Use the static `OzVault.create()` factory — do not call `new OzVault()` directly.
|
|
911
1020
|
*
|
|
912
1021
|
* @example
|
|
1022
|
+
* // Recommended — pass sessionUrl and let the SDK call your backend automatically
|
|
913
1023
|
* const vault = await OzVault.create({
|
|
914
|
-
* pubKey: '
|
|
915
|
-
*
|
|
916
|
-
* // Call your backend — which calls ozura.mintWaxKey() from @ozura/elements/server
|
|
917
|
-
* const { waxKey } = await fetch('/api/mint-wax', {
|
|
918
|
-
* method: 'POST',
|
|
919
|
-
* body: JSON.stringify({ sessionId }),
|
|
920
|
-
* }).then(r => r.json());
|
|
921
|
-
* return waxKey;
|
|
922
|
-
* },
|
|
1024
|
+
* pubKey: 'pk_prod_...', // or 'pk_test_...' for test mode
|
|
1025
|
+
* sessionUrl: '/api/oz-session', // backend endpoint that calls ozura.createSession()
|
|
923
1026
|
* });
|
|
924
1027
|
* const cardNum = vault.createElement('cardNumber');
|
|
925
1028
|
* cardNum.mount('#card-number');
|
|
@@ -935,7 +1038,7 @@
|
|
|
935
1038
|
* @internal
|
|
936
1039
|
*/
|
|
937
1040
|
constructor(options, waxKey, tokenizationSessionId) {
|
|
938
|
-
var _a, _b, _c, _d;
|
|
1041
|
+
var _a, _b, _c, _d, _e;
|
|
939
1042
|
this.elements = new Map();
|
|
940
1043
|
this.elementsByType = new Map();
|
|
941
1044
|
this.bankElementsByType = new Map();
|
|
@@ -969,7 +1072,12 @@
|
|
|
969
1072
|
this.fonts = (_a = options.fonts) !== null && _a !== void 0 ? _a : [];
|
|
970
1073
|
this.resolvedAppearance = resolveAppearance(options.appearance);
|
|
971
1074
|
this.vaultId = `vault-${uuid()}`;
|
|
972
|
-
|
|
1075
|
+
// sessionLimit takes precedence over legacy maxTokenizeCalls.
|
|
1076
|
+
// null means unlimited — use Infinity so the ">=" check never triggers.
|
|
1077
|
+
const rawLimit = options.sessionLimit !== undefined
|
|
1078
|
+
? options.sessionLimit
|
|
1079
|
+
: ((_b = options.maxTokenizeCalls) !== null && _b !== void 0 ? _b : 3);
|
|
1080
|
+
this._maxTokenizeCalls = rawLimit === null ? Infinity : rawLimit;
|
|
973
1081
|
this._debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false;
|
|
974
1082
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
975
1083
|
window.addEventListener('message', this.boundHandleMessage);
|
|
@@ -985,7 +1093,8 @@
|
|
|
985
1093
|
}
|
|
986
1094
|
}, timeout);
|
|
987
1095
|
}
|
|
988
|
-
|
|
1096
|
+
// onSessionRefresh takes precedence over legacy onWaxRefresh
|
|
1097
|
+
this._onWaxRefresh = (_e = options.onSessionRefresh) !== null && _e !== void 0 ? _e : options.onWaxRefresh;
|
|
989
1098
|
this._onReady = options.onReady;
|
|
990
1099
|
this.log('vault created', { vaultId: this.vaultId, frameBaseUrl: this.frameBaseUrl, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
991
1100
|
}
|
|
@@ -993,51 +1102,63 @@
|
|
|
993
1102
|
* Creates and returns a ready `OzVault` instance.
|
|
994
1103
|
*
|
|
995
1104
|
* Internally this:
|
|
996
|
-
* 1. Generates a
|
|
1105
|
+
* 1. Generates a session UUID.
|
|
997
1106
|
* 2. Starts loading the hidden tokenizer iframe immediately.
|
|
998
|
-
* 3.
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1107
|
+
* 3. Fetches a session key from your backend concurrently — either via
|
|
1108
|
+
* `sessionUrl` (simplest), `getSessionKey` (custom headers/auth), or the
|
|
1109
|
+
* deprecated `fetchWaxKey` callback.
|
|
1110
|
+
* 4. Resolves with the vault instance once the session key is stored. The iframe
|
|
1001
1111
|
* has been loading the whole time, so `isReady` may already be true or
|
|
1002
1112
|
* will fire shortly after.
|
|
1003
1113
|
*
|
|
1004
1114
|
* The returned vault is ready to create elements immediately. `createToken()`
|
|
1005
1115
|
* additionally requires `vault.isReady` (tokenizer iframe loaded).
|
|
1006
1116
|
*
|
|
1007
|
-
* @throws {OzError} if
|
|
1117
|
+
* @throws {OzError} if the session fetch fails, times out, or returns an empty string.
|
|
1008
1118
|
*/
|
|
1009
1119
|
static async create(options, signal) {
|
|
1010
1120
|
if (!options.pubKey || !options.pubKey.trim()) {
|
|
1011
1121
|
throw new OzError('pubKey is required in options. Obtain your public key from the Ozura admin.');
|
|
1012
1122
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1123
|
+
// Normalize the session callback. Priority: sessionUrl > getSessionKey > fetchWaxKey (deprecated).
|
|
1124
|
+
// This allows merchants to use the clean new API without touching legacy code.
|
|
1125
|
+
let resolvedFetchKey;
|
|
1126
|
+
if (options.sessionUrl) {
|
|
1127
|
+
resolvedFetchKey = createSessionFetcher(options.sessionUrl);
|
|
1128
|
+
}
|
|
1129
|
+
else if (typeof options.getSessionKey === 'function') {
|
|
1130
|
+
resolvedFetchKey = options.getSessionKey;
|
|
1131
|
+
}
|
|
1132
|
+
else if (typeof options.fetchWaxKey === 'function') {
|
|
1133
|
+
resolvedFetchKey = options.fetchWaxKey;
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
throw new OzError('A session URL or callback is required. Pass sessionUrl, getSessionKey, or fetchWaxKey to OzVault.create().');
|
|
1015
1137
|
}
|
|
1016
1138
|
const tokenizationSessionId = uuid();
|
|
1017
1139
|
// Construct the vault immediately — this mounts the tokenizer iframe so it
|
|
1018
|
-
// starts loading while
|
|
1019
|
-
// empty and is set below before create() returns.
|
|
1140
|
+
// starts loading while the session fetch is in flight.
|
|
1020
1141
|
const vault = new OzVault(options, '', tokenizationSessionId);
|
|
1021
1142
|
// If the caller provides an AbortSignal (e.g. React useEffect cleanup),
|
|
1022
1143
|
// destroy the vault immediately on abort so the tokenizer iframe and message
|
|
1023
|
-
// listener are removed synchronously rather than waiting for
|
|
1024
|
-
// settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
1144
|
+
// listener are removed synchronously rather than waiting for the session fetch
|
|
1145
|
+
// to settle. This eliminates the brief double-iframe window in React StrictMode.
|
|
1025
1146
|
const onAbort = () => vault.destroy();
|
|
1026
1147
|
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
|
|
1027
1148
|
let waxKey;
|
|
1028
1149
|
try {
|
|
1029
|
-
waxKey = await
|
|
1150
|
+
waxKey = await resolvedFetchKey(tokenizationSessionId);
|
|
1030
1151
|
}
|
|
1031
1152
|
catch (err) {
|
|
1032
1153
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
1033
1154
|
vault.destroy();
|
|
1034
1155
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
1035
1156
|
throw new OzError('OzVault.create() was cancelled.');
|
|
1036
|
-
// Preserve errorCode/retryable from OzError (e.g. timeout/network from
|
|
1157
|
+
// Preserve errorCode/retryable from OzError (e.g. timeout/network from createSessionFetcher)
|
|
1037
1158
|
// so callers can distinguish transient failures from config errors.
|
|
1038
1159
|
const originalCode = err instanceof OzError ? err.errorCode : undefined;
|
|
1039
1160
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
1040
|
-
throw new OzError(`
|
|
1161
|
+
throw new OzError(`Session fetch threw an error: ${msg}`, undefined, originalCode);
|
|
1041
1162
|
}
|
|
1042
1163
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
1043
1164
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
@@ -1046,11 +1167,11 @@
|
|
|
1046
1167
|
}
|
|
1047
1168
|
if (typeof waxKey !== 'string' || !waxKey.trim()) {
|
|
1048
1169
|
vault.destroy();
|
|
1049
|
-
throw new OzError('
|
|
1170
|
+
throw new OzError('Session fetch returned an empty key. Check your session endpoint response — it must return { sessionKey: "..." }.');
|
|
1050
1171
|
}
|
|
1051
1172
|
// Static methods can access private fields of instances of the same class.
|
|
1052
1173
|
vault.waxKey = waxKey;
|
|
1053
|
-
vault._storedFetchWaxKey =
|
|
1174
|
+
vault._storedFetchWaxKey = resolvedFetchKey;
|
|
1054
1175
|
// If the tokenizer iframe fired OZ_FRAME_READY before fetchWaxKey resolved,
|
|
1055
1176
|
// the OZ_INIT sent at that point had an empty waxKey. Send a follow-up now
|
|
1056
1177
|
// so the tokenizer has the key stored before any createToken() call.
|
|
@@ -1137,7 +1258,12 @@
|
|
|
1137
1258
|
this.completionState.delete(existing.frameId);
|
|
1138
1259
|
existing.destroy();
|
|
1139
1260
|
}
|
|
1140
|
-
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance)
|
|
1261
|
+
const el = new OzElement(type, options, this.vaultId, this.frameBaseUrl, this.fonts, this.resolvedAppearance, () => {
|
|
1262
|
+
// Prune vault-level maps when the element is manually destroyed so they
|
|
1263
|
+
// don't grow unboundedly in SPA scenarios with repeated mount/unmount cycles.
|
|
1264
|
+
this.elements.delete(el.frameId);
|
|
1265
|
+
this.completionState.delete(el.frameId);
|
|
1266
|
+
});
|
|
1141
1267
|
this.elements.set(el.frameId, el);
|
|
1142
1268
|
typeMap.set(type, el);
|
|
1143
1269
|
return el;
|
|
@@ -1716,9 +1842,9 @@
|
|
|
1716
1842
|
// key without waiting for a vault rejection.
|
|
1717
1843
|
this._tokenizeSuccessCount++;
|
|
1718
1844
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1719
|
-
this.log('proactive
|
|
1845
|
+
this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1720
1846
|
this.refreshWaxKey().catch((err) => {
|
|
1721
|
-
console.warn('[OzVault] Post-budget
|
|
1847
|
+
console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
|
|
1722
1848
|
});
|
|
1723
1849
|
}
|
|
1724
1850
|
}
|
|
@@ -1794,61 +1920,65 @@
|
|
|
1794
1920
|
}
|
|
1795
1921
|
pending.reject(new OzError(normalizeVaultError(raw), raw, errorCode));
|
|
1796
1922
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
const
|
|
1818
|
-
this.
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1923
|
+
else {
|
|
1924
|
+
// Also check bank resolvers — both card and bank errors use OZ_TOKEN_ERROR.
|
|
1925
|
+
// Use else-if rather than sequential checks so a UUID collision (however
|
|
1926
|
+
// improbable) can never trigger double-rejection of two unrelated resolvers.
|
|
1927
|
+
const bankPending = this.bankTokenizeResolvers.get(msg.requestId);
|
|
1928
|
+
if (bankPending) {
|
|
1929
|
+
this.bankTokenizeResolvers.delete(msg.requestId);
|
|
1930
|
+
if (bankPending.timeoutId != null)
|
|
1931
|
+
clearTimeout(bankPending.timeoutId);
|
|
1932
|
+
if (this.isRefreshableAuthError(errorCode, raw) && !bankPending.retried && this._storedFetchWaxKey) {
|
|
1933
|
+
const resetCountAtRetry = this._resetCount;
|
|
1934
|
+
this.refreshWaxKey().then(() => {
|
|
1935
|
+
if (this._destroyed) {
|
|
1936
|
+
bankPending.reject(new OzError('Vault destroyed during wax key refresh.'));
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (this._resetCount !== resetCountAtRetry) {
|
|
1940
|
+
bankPending.reject(new OzError('Vault was reset while tokenization was in progress.'));
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const newRequestId = `req-${uuid()}`;
|
|
1944
|
+
this.bankTokenizeResolvers.set(newRequestId, Object.assign(Object.assign({}, bankPending), { retried: true }));
|
|
1945
|
+
try {
|
|
1946
|
+
const retryBankChannels = bankPending.readyElements.map(() => new MessageChannel());
|
|
1947
|
+
this.sendToTokenizer({
|
|
1948
|
+
type: 'OZ_BANK_TOKENIZE',
|
|
1949
|
+
requestId: newRequestId,
|
|
1950
|
+
waxKey: this.waxKey,
|
|
1951
|
+
tokenizationSessionId: this.tokenizationSessionId,
|
|
1952
|
+
pubKey: this.pubKey,
|
|
1953
|
+
firstName: bankPending.firstName,
|
|
1954
|
+
lastName: bankPending.lastName,
|
|
1955
|
+
fieldCount: bankPending.fieldCount,
|
|
1956
|
+
}, retryBankChannels.map(ch => ch.port1));
|
|
1957
|
+
bankPending.readyElements.forEach((el, i) => el.beginCollect(newRequestId, retryBankChannels[i].port2));
|
|
1958
|
+
const retryBankTimeoutId = setTimeout(() => {
|
|
1959
|
+
if (this.bankTokenizeResolvers.has(newRequestId)) {
|
|
1960
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1961
|
+
this.sendToTokenizer({ type: 'OZ_TOKENIZE_CANCEL', requestId: newRequestId });
|
|
1962
|
+
bankPending.reject(new OzError('Bank tokenization timed out after wax key refresh.', undefined, 'timeout'));
|
|
1963
|
+
}
|
|
1964
|
+
}, 30000);
|
|
1965
|
+
const retryBankEntry = this.bankTokenizeResolvers.get(newRequestId);
|
|
1966
|
+
if (retryBankEntry)
|
|
1967
|
+
retryBankEntry.timeoutId = retryBankTimeoutId;
|
|
1968
|
+
}
|
|
1969
|
+
catch (setupErr) {
|
|
1970
|
+
this.bankTokenizeResolvers.delete(newRequestId);
|
|
1971
|
+
bankPending.reject(setupErr instanceof OzError ? setupErr : new OzError('Retry bank tokenization failed to start'));
|
|
1972
|
+
}
|
|
1973
|
+
}).catch((refreshErr) => {
|
|
1974
|
+
const msg = refreshErr instanceof Error ? refreshErr.message : 'Wax key refresh failed';
|
|
1975
|
+
bankPending.reject(new OzError(msg, undefined, 'auth'));
|
|
1976
|
+
});
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
bankPending.reject(new OzError(normalizeBankVaultError(raw), raw, errorCode));
|
|
1849
1980
|
}
|
|
1850
|
-
|
|
1851
|
-
}
|
|
1981
|
+
} // end else (bank path)
|
|
1852
1982
|
break;
|
|
1853
1983
|
}
|
|
1854
1984
|
case 'OZ_BANK_TOKEN_RESULT': {
|
|
@@ -1876,9 +2006,9 @@
|
|
|
1876
2006
|
// Same proactive refresh logic as card tokenization.
|
|
1877
2007
|
this._tokenizeSuccessCount++;
|
|
1878
2008
|
if (this._tokenizeSuccessCount >= this._maxTokenizeCalls) {
|
|
1879
|
-
this.log('proactive
|
|
2009
|
+
this.log('proactive session key refresh triggered', { tokenizeSuccessCount: this._tokenizeSuccessCount, maxTokenizeCalls: this._maxTokenizeCalls });
|
|
1880
2010
|
this.refreshWaxKey().catch((err) => {
|
|
1881
|
-
console.warn('[OzVault] Post-budget
|
|
2011
|
+
console.warn('[OzVault] Post-budget session key refresh failed:', err instanceof Error ? err.message : err);
|
|
1882
2012
|
});
|
|
1883
2013
|
}
|
|
1884
2014
|
}
|
|
@@ -1964,88 +2094,11 @@
|
|
|
1964
2094
|
}
|
|
1965
2095
|
}
|
|
1966
2096
|
|
|
1967
|
-
/**
|
|
1968
|
-
* Creates a ready-to-use `fetchWaxKey` callback for `OzVault.create()` and `<OzElements>`.
|
|
1969
|
-
*
|
|
1970
|
-
* Calls your backend mint endpoint with `{ sessionId }` and returns the wax key string.
|
|
1971
|
-
* Throws on non-OK responses or a missing `waxKey` field so the vault can surface the
|
|
1972
|
-
* error through its normal error path.
|
|
1973
|
-
*
|
|
1974
|
-
* Each call enforces a 10-second per-attempt timeout. On a pure network-level
|
|
1975
|
-
* failure (connection refused, DNS failure, etc.) the call is retried once after
|
|
1976
|
-
* 750ms before throwing. HTTP errors (4xx/5xx) are never retried — they indicate
|
|
1977
|
-
* an endpoint misconfiguration or an invalid key, not a transient failure.
|
|
1978
|
-
*
|
|
1979
|
-
* The mint endpoint is typically the one-line `createMintWaxHandler` / `createMintWaxMiddleware`
|
|
1980
|
-
* from `@ozura/elements/server`.
|
|
1981
|
-
*
|
|
1982
|
-
* @param mintUrl - Absolute or relative URL of your wax-key mint endpoint, e.g. `'/api/mint-wax'`.
|
|
1983
|
-
*
|
|
1984
|
-
* @example
|
|
1985
|
-
* // Vanilla JS
|
|
1986
|
-
* const vault = await OzVault.create({
|
|
1987
|
-
* pubKey: 'pk_live_...',
|
|
1988
|
-
* fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
|
|
1989
|
-
* });
|
|
1990
|
-
*
|
|
1991
|
-
* @example
|
|
1992
|
-
* // React
|
|
1993
|
-
* <OzElements pubKey="pk_live_..." fetchWaxKey={createFetchWaxKey('/api/mint-wax')}>
|
|
1994
|
-
*/
|
|
1995
|
-
function createFetchWaxKey(mintUrl) {
|
|
1996
|
-
const TIMEOUT_MS = 10000;
|
|
1997
|
-
// Each attempt gets its own AbortController so a timeout on attempt 1 does
|
|
1998
|
-
// not bleed into the retry. Uses AbortController + setTimeout instead of
|
|
1999
|
-
// AbortSignal.timeout() to support environments without that API.
|
|
2000
|
-
const attemptFetch = (sessionId) => {
|
|
2001
|
-
const controller = new AbortController();
|
|
2002
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
2003
|
-
return fetch(mintUrl, {
|
|
2004
|
-
method: 'POST',
|
|
2005
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2006
|
-
body: JSON.stringify({ sessionId }),
|
|
2007
|
-
signal: controller.signal,
|
|
2008
|
-
}).finally(() => clearTimeout(timer));
|
|
2009
|
-
};
|
|
2010
|
-
return async (sessionId) => {
|
|
2011
|
-
let res;
|
|
2012
|
-
try {
|
|
2013
|
-
res = await attemptFetch(sessionId);
|
|
2014
|
-
}
|
|
2015
|
-
catch (firstErr) {
|
|
2016
|
-
// Abort/timeout should not be retried — the server received nothing or
|
|
2017
|
-
// we already waited the full timeout duration.
|
|
2018
|
-
if (firstErr instanceof Error && (firstErr.name === 'AbortError' || firstErr.name === 'TimeoutError')) {
|
|
2019
|
-
throw new OzError(`Wax key mint timed out after ${TIMEOUT_MS / 1000}s (${mintUrl})`, undefined, 'timeout');
|
|
2020
|
-
}
|
|
2021
|
-
// Pure network error (offline, DNS, connection refused) — retry once
|
|
2022
|
-
// after a short pause in case of a transient blip.
|
|
2023
|
-
await new Promise(resolve => setTimeout(resolve, 750));
|
|
2024
|
-
try {
|
|
2025
|
-
res = await attemptFetch(sessionId);
|
|
2026
|
-
}
|
|
2027
|
-
catch (retryErr) {
|
|
2028
|
-
const msg = retryErr instanceof Error ? retryErr.message : 'Network error';
|
|
2029
|
-
throw new OzError(`Could not reach wax key mint endpoint (${mintUrl}): ${msg}`, undefined, 'network');
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
const data = await res.json().catch(() => ({}));
|
|
2033
|
-
if (!res.ok) {
|
|
2034
|
-
throw new OzError(typeof data.error === 'string' && data.error
|
|
2035
|
-
? data.error
|
|
2036
|
-
: `Wax key mint failed (HTTP ${res.status})`, undefined, res.status >= 500 ? 'server' : res.status === 401 || res.status === 403 ? 'auth' : 'validation');
|
|
2037
|
-
}
|
|
2038
|
-
if (typeof data.waxKey !== 'string' || !data.waxKey.trim()) {
|
|
2039
|
-
throw new OzError('Mint endpoint response is missing waxKey. Check your /api/mint-wax implementation.', undefined, 'validation');
|
|
2040
|
-
}
|
|
2041
|
-
return data.waxKey;
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
2097
|
exports.OzElement = OzElement;
|
|
2046
2098
|
exports.OzError = OzError;
|
|
2047
2099
|
exports.OzVault = OzVault;
|
|
2048
|
-
exports.createFetchWaxKey =
|
|
2100
|
+
exports.createFetchWaxKey = createSessionFetcher;
|
|
2101
|
+
exports.createSessionFetcher = createSessionFetcher;
|
|
2049
2102
|
exports.normalizeBankVaultError = normalizeBankVaultError;
|
|
2050
2103
|
exports.normalizeCardSaleError = normalizeCardSaleError;
|
|
2051
2104
|
exports.normalizeVaultError = normalizeVaultError;
|