@ozura/elements 1.3.1 → 1.4.0

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.
@@ -24,7 +24,7 @@
24
24
  * ```
25
25
  */
26
26
  import { type Ref, type PropType, type ComputedRef } from 'vue';
27
- import type { TokenizeOptions, TokenResponse, BankTokenizeOptions, BankTokenResponse, FontSource, Appearance } from '../types';
27
+ import type { ElementOptions, TokenizeOptions, TokenResponse, BankTokenizeOptions, BankTokenResponse, FontSource, Appearance } from '../types';
28
28
  export interface OzElementsProps {
29
29
  /** Omit when using a test vault key from a Test project at ozuravault.com.
30
30
  * Required for production vault keys. */
@@ -40,6 +40,14 @@ export interface OzElementsProps {
40
40
  onSessionRefresh?: () => void;
41
41
  /** Called once when the vault tokenizer and all mounted field iframes are ready. */
42
42
  onReady?: () => void;
43
+ /**
44
+ * Called if the tokenizer iframe fails to signal ready within `loadTimeoutMs`.
45
+ * Use this to show a fallback UI (e.g. "Payment fields failed to load").
46
+ * Receives an optional info object with `source: 'tokenizer'`.
47
+ */
48
+ onLoadError?: (info?: {
49
+ source: 'tokenizer';
50
+ }) => void;
43
51
  /**
44
52
  * Maximum number of tokenize calls before the vault proactively refreshes the session.
45
53
  * Must match the `sessionLimit` passed to `createSession()` on your server.
@@ -94,6 +102,12 @@ export declare const OzElements: import("vue").DefineComponent<import("vue").Ext
94
102
  type: PropType<() => void>;
95
103
  default: undefined;
96
104
  };
105
+ onLoadError: {
106
+ type: PropType<(info?: {
107
+ source: "tokenizer";
108
+ }) => void>;
109
+ default: undefined;
110
+ };
97
111
  sessionLimit: {
98
112
  type: PropType<number | null>;
99
113
  default: undefined;
@@ -145,6 +159,12 @@ export declare const OzElements: import("vue").DefineComponent<import("vue").Ext
145
159
  type: PropType<() => void>;
146
160
  default: undefined;
147
161
  };
162
+ onLoadError: {
163
+ type: PropType<(info?: {
164
+ source: "tokenizer";
165
+ }) => void>;
166
+ default: undefined;
167
+ };
148
168
  sessionLimit: {
149
169
  type: PropType<number | null>;
150
170
  default: undefined;
@@ -168,6 +188,9 @@ export declare const OzElements: import("vue").DefineComponent<import("vue").Ext
168
188
  appearance: Appearance;
169
189
  onSessionRefresh: () => void;
170
190
  onReady: () => void;
191
+ onLoadError: (info?: {
192
+ source: "tokenizer";
193
+ }) => void;
171
194
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
172
195
  export interface UseOzElementsReturn {
173
196
  /**
@@ -196,6 +219,32 @@ export interface UseOzElementsReturn {
196
219
  * Clears all mounted element fields without destroying the vault.
197
220
  */
198
221
  reset: () => void;
222
+ /**
223
+ * `true` when every mounted field has reported `complete && valid`.
224
+ * Use this to gate the pay button without wiring individual `@change`
225
+ * listeners — reacts in real time as the customer fills in fields.
226
+ *
227
+ * @example
228
+ * const { ready, isComplete, createToken } = useOzElements();
229
+ * // <button :disabled="!ready || !isComplete" @click="createToken()">Pay</button>
230
+ */
231
+ isComplete: ComputedRef<boolean>;
232
+ /**
233
+ * Like `isComplete`, but for bank-account fields (`createBankElement`). Card
234
+ * and bank completion are tracked independently, so a card-only form isn't
235
+ * gated on bank fields and vice versa.
236
+ */
237
+ isBankComplete: ComputedRef<boolean>;
238
+ /**
239
+ * `true` while `createToken()` or `createBankToken()` is in progress,
240
+ * including the transparent wax-key refresh phase. Use this to keep the
241
+ * pay button disabled and prevent double-submission.
242
+ *
243
+ * @example
244
+ * const { isTokenizing, createToken } = useOzElements();
245
+ * // <button :disabled="isTokenizing" @click="createToken()">Pay</button>
246
+ */
247
+ isTokenizing: ComputedRef<boolean>;
199
248
  }
200
249
  /**
201
250
  * Returns createToken, createBankToken, ready, initError, tokenizeCount, and reset.
@@ -204,6 +253,12 @@ export interface UseOzElementsReturn {
204
253
  * @throws {Error} if called outside an <OzElements> provider
205
254
  */
206
255
  export declare function useOzElements(): UseOzElementsReturn;
256
+ /** Props accepted by all individual field components (OzCardNumber, OzExpiry, OzCvv, etc.). */
257
+ export interface OzFieldProps {
258
+ placeholder?: string;
259
+ disabled?: boolean;
260
+ style?: ElementOptions['style'];
261
+ }
207
262
  /** Card number field. Emits `change` (ElementChangeEvent), `focus`, `blur`. */
208
263
  export declare const OzCardNumber: import("vue").DefineComponent<{
209
264
  placeholder?: string | undefined;
@@ -107,6 +107,39 @@ export declare class OzVault {
107
107
  * payButton.textContent = remaining > 0 ? `Pay (${remaining} attempts left)` : 'Pay';
108
108
  */
109
109
  get tokenizeCount(): number;
110
+ /**
111
+ * `true` when every mounted field has reported `complete && valid` via its
112
+ * last `change` event. `false` if no fields have been created, or if any
113
+ * field is incomplete or invalid.
114
+ *
115
+ * Use this to gate the pay button in vanilla JS integrations without having
116
+ * to wire up individual `change` event listeners:
117
+ *
118
+ * @example
119
+ * vault.getElement('cardNumber')!.on('change', () => {
120
+ * payBtn.disabled = !vault.isComplete;
121
+ * });
122
+ */
123
+ get isComplete(): boolean;
124
+ /**
125
+ * Like {@link isComplete}, but for bank-account elements created via
126
+ * {@link createBankElement}. Card and bank fields are tracked separately so a
127
+ * card-only checkout is never gated on bank fields (and vice versa), matching
128
+ * the `createToken()` / `createBankToken()` split. A vault with both card and
129
+ * bank elements exposes each completion state independently.
130
+ */
131
+ get isBankComplete(): boolean;
132
+ /** True iff the set is non-empty and every element has reported complete-and-valid. */
133
+ private allComplete;
134
+ /**
135
+ * `true` while a `createToken()` or `createBankToken()` call is in progress
136
+ * (including the transparent wax-key refresh phase). Use this to keep the pay
137
+ * button disabled during tokenization to prevent double-submission.
138
+ *
139
+ * @example
140
+ * payBtn.disabled = vault.isTokenizing;
141
+ */
142
+ get isTokenizing(): boolean;
110
143
  /**
111
144
  * Creates a new OzElement of the given type. Call `.mount(selector)` on the
112
145
  * returned element to attach it to the DOM.
@@ -223,8 +223,14 @@ export interface VaultOptions {
223
223
  /**
224
224
  * Called if the tokenizer iframe fails to signal ready within `loadTimeoutMs`.
225
225
  * Use this to show a fallback UI (e.g. "Unable to load payment fields").
226
+ *
227
+ * Receives an optional `info` object with a `source` field identifying which
228
+ * iframe timed out. Currently always `'tokenizer'` — element-level load errors
229
+ * fire the `loaderror` event on the individual element instead.
226
230
  */
227
- onLoadError?: () => void;
231
+ onLoadError?: (info?: {
232
+ source: 'tokenizer';
233
+ }) => void;
228
234
  /** How long to wait (ms) for the tokenizer iframe before calling `onLoadError`. Default: 10 000.
229
235
  * Only takes effect when `onLoadError` is also provided — setting this without `onLoadError` has no effect. */
230
236
  loadTimeoutMs?: number;
@@ -350,10 +356,24 @@ export interface TokenizeOptions {
350
356
  }
351
357
  /** Options for `vault.createBankToken()`. */
352
358
  export interface BankTokenizeOptions {
353
- /** Account holder first name. Required. */
354
- firstName: string;
355
- /** Account holder last name. Required. */
356
- lastName: string;
359
+ /**
360
+ * Account holder first name.
361
+ * Required when `billing` is not provided.
362
+ * @deprecated Pass firstName inside `billing` instead.
363
+ */
364
+ firstName?: string;
365
+ /**
366
+ * Account holder last name.
367
+ * Required when `billing` is not provided.
368
+ * @deprecated Pass lastName inside `billing` instead.
369
+ */
370
+ lastName?: string;
371
+ /**
372
+ * Billing details for the account holder. When provided, `billing.firstName`
373
+ * and `billing.lastName` take precedence over the top-level fields.
374
+ * The normalized details are echoed back in `BankTokenResponse.billing`.
375
+ */
376
+ billing?: BillingDetails;
357
377
  }
358
378
  /** Non-sensitive bank account metadata returned alongside the token. */
359
379
  export interface BankAccountMetadata {
@@ -406,6 +426,11 @@ export interface BankTokenResponse {
406
426
  token: string;
407
427
  /** Non-sensitive bank account metadata. */
408
428
  bank?: BankAccountMetadata;
429
+ /**
430
+ * Validated, normalized billing details — echoed back when billing was
431
+ * passed to createBankToken(). Ready to spread into your ACH processor request.
432
+ */
433
+ billing?: BillingDetails;
409
434
  }
410
435
  /**
411
436
  * Full request body for the Ozura Pay API cardSale endpoint.
@@ -24,7 +24,7 @@
24
24
  * ```
25
25
  */
26
26
  import { type Ref, type PropType, type ComputedRef } from 'vue';
27
- import type { TokenizeOptions, TokenResponse, BankTokenizeOptions, BankTokenResponse, FontSource, Appearance } from '../types';
27
+ import type { ElementOptions, TokenizeOptions, TokenResponse, BankTokenizeOptions, BankTokenResponse, FontSource, Appearance } from '../types';
28
28
  export interface OzElementsProps {
29
29
  /** Omit when using a test vault key from a Test project at ozuravault.com.
30
30
  * Required for production vault keys. */
@@ -40,6 +40,14 @@ export interface OzElementsProps {
40
40
  onSessionRefresh?: () => void;
41
41
  /** Called once when the vault tokenizer and all mounted field iframes are ready. */
42
42
  onReady?: () => void;
43
+ /**
44
+ * Called if the tokenizer iframe fails to signal ready within `loadTimeoutMs`.
45
+ * Use this to show a fallback UI (e.g. "Payment fields failed to load").
46
+ * Receives an optional info object with `source: 'tokenizer'`.
47
+ */
48
+ onLoadError?: (info?: {
49
+ source: 'tokenizer';
50
+ }) => void;
43
51
  /**
44
52
  * Maximum number of tokenize calls before the vault proactively refreshes the session.
45
53
  * Must match the `sessionLimit` passed to `createSession()` on your server.
@@ -94,6 +102,12 @@ export declare const OzElements: import("vue").DefineComponent<import("vue").Ext
94
102
  type: PropType<() => void>;
95
103
  default: undefined;
96
104
  };
105
+ onLoadError: {
106
+ type: PropType<(info?: {
107
+ source: "tokenizer";
108
+ }) => void>;
109
+ default: undefined;
110
+ };
97
111
  sessionLimit: {
98
112
  type: PropType<number | null>;
99
113
  default: undefined;
@@ -145,6 +159,12 @@ export declare const OzElements: import("vue").DefineComponent<import("vue").Ext
145
159
  type: PropType<() => void>;
146
160
  default: undefined;
147
161
  };
162
+ onLoadError: {
163
+ type: PropType<(info?: {
164
+ source: "tokenizer";
165
+ }) => void>;
166
+ default: undefined;
167
+ };
148
168
  sessionLimit: {
149
169
  type: PropType<number | null>;
150
170
  default: undefined;
@@ -168,6 +188,9 @@ export declare const OzElements: import("vue").DefineComponent<import("vue").Ext
168
188
  appearance: Appearance;
169
189
  onSessionRefresh: () => void;
170
190
  onReady: () => void;
191
+ onLoadError: (info?: {
192
+ source: "tokenizer";
193
+ }) => void;
171
194
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
172
195
  export interface UseOzElementsReturn {
173
196
  /**
@@ -196,6 +219,32 @@ export interface UseOzElementsReturn {
196
219
  * Clears all mounted element fields without destroying the vault.
197
220
  */
198
221
  reset: () => void;
222
+ /**
223
+ * `true` when every mounted field has reported `complete && valid`.
224
+ * Use this to gate the pay button without wiring individual `@change`
225
+ * listeners — reacts in real time as the customer fills in fields.
226
+ *
227
+ * @example
228
+ * const { ready, isComplete, createToken } = useOzElements();
229
+ * // <button :disabled="!ready || !isComplete" @click="createToken()">Pay</button>
230
+ */
231
+ isComplete: ComputedRef<boolean>;
232
+ /**
233
+ * Like `isComplete`, but for bank-account fields (`createBankElement`). Card
234
+ * and bank completion are tracked independently, so a card-only form isn't
235
+ * gated on bank fields and vice versa.
236
+ */
237
+ isBankComplete: ComputedRef<boolean>;
238
+ /**
239
+ * `true` while `createToken()` or `createBankToken()` is in progress,
240
+ * including the transparent wax-key refresh phase. Use this to keep the
241
+ * pay button disabled and prevent double-submission.
242
+ *
243
+ * @example
244
+ * const { isTokenizing, createToken } = useOzElements();
245
+ * // <button :disabled="isTokenizing" @click="createToken()">Pay</button>
246
+ */
247
+ isTokenizing: ComputedRef<boolean>;
199
248
  }
200
249
  /**
201
250
  * Returns createToken, createBankToken, ready, initError, tokenizeCount, and reset.
@@ -204,6 +253,12 @@ export interface UseOzElementsReturn {
204
253
  * @throws {Error} if called outside an <OzElements> provider
205
254
  */
206
255
  export declare function useOzElements(): UseOzElementsReturn;
256
+ /** Props accepted by all individual field components (OzCardNumber, OzExpiry, OzCvv, etc.). */
257
+ export interface OzFieldProps {
258
+ placeholder?: string;
259
+ disabled?: boolean;
260
+ style?: ElementOptions['style'];
261
+ }
207
262
  /** Card number field. Emits `change` (ElementChangeEvent), `focus`, `blur`. */
208
263
  export declare const OzCardNumber: import("vue").DefineComponent<{
209
264
  placeholder?: string | undefined;
@@ -1116,7 +1116,7 @@ class OzVault {
1116
1116
  this.loadErrorTimeoutId = setTimeout(() => {
1117
1117
  this.loadErrorTimeoutId = null;
1118
1118
  if (!this._destroyed && !this.tokenizerReady) {
1119
- options.onLoadError();
1119
+ options.onLoadError({ source: 'tokenizer' });
1120
1120
  }
1121
1121
  }, timeout);
1122
1122
  }
@@ -1249,6 +1249,49 @@ class OzVault {
1249
1249
  get tokenizeCount() {
1250
1250
  return this._tokenizeSuccessCount;
1251
1251
  }
1252
+ /**
1253
+ * `true` when every mounted field has reported `complete && valid` via its
1254
+ * last `change` event. `false` if no fields have been created, or if any
1255
+ * field is incomplete or invalid.
1256
+ *
1257
+ * Use this to gate the pay button in vanilla JS integrations without having
1258
+ * to wire up individual `change` event listeners:
1259
+ *
1260
+ * @example
1261
+ * vault.getElement('cardNumber')!.on('change', () => {
1262
+ * payBtn.disabled = !vault.isComplete;
1263
+ * });
1264
+ */
1265
+ get isComplete() {
1266
+ return this.allComplete([...this.elementsByType.values()]);
1267
+ }
1268
+ /**
1269
+ * Like {@link isComplete}, but for bank-account elements created via
1270
+ * {@link createBankElement}. Card and bank fields are tracked separately so a
1271
+ * card-only checkout is never gated on bank fields (and vice versa), matching
1272
+ * the `createToken()` / `createBankToken()` split. A vault with both card and
1273
+ * bank elements exposes each completion state independently.
1274
+ */
1275
+ get isBankComplete() {
1276
+ return this.allComplete([...this.bankElementsByType.values()]);
1277
+ }
1278
+ /** True iff the set is non-empty and every element has reported complete-and-valid. */
1279
+ allComplete(els) {
1280
+ if (els.length === 0)
1281
+ return false;
1282
+ return els.every(el => this.completionState.get(el.frameId) === true);
1283
+ }
1284
+ /**
1285
+ * `true` while a `createToken()` or `createBankToken()` call is in progress
1286
+ * (including the transparent wax-key refresh phase). Use this to keep the pay
1287
+ * button disabled during tokenization to prevent double-submission.
1288
+ *
1289
+ * @example
1290
+ * payBtn.disabled = vault.isTokenizing;
1291
+ */
1292
+ get isTokenizing() {
1293
+ return this._tokenizing !== null;
1294
+ }
1252
1295
  /**
1253
1296
  * Creates a new OzElement of the given type. Call `.mount(selector)` on the
1254
1297
  * returned element to attach it to the DOM.
@@ -1331,17 +1374,29 @@ class OzVault {
1331
1374
  ? 'A card tokenization is already in progress. Wait for it to complete before calling createBankToken().'
1332
1375
  : 'A bank tokenization is already in progress. Wait for it to complete before calling createBankToken() again.');
1333
1376
  }
1334
- if (!((_a = options.firstName) === null || _a === void 0 ? void 0 : _a.trim())) {
1335
- throw new OzError('firstName is required for bank account tokenization.');
1336
- }
1337
- if (!((_b = options.lastName) === null || _b === void 0 ? void 0 : _b.trim())) {
1338
- throw new OzError('lastName is required for bank account tokenization.');
1339
- }
1340
- if (options.firstName.trim().length > 50) {
1341
- throw new OzError('firstName must be 50 characters or fewer.');
1377
+ // Validate billing details if provided billing.firstName/lastName take
1378
+ // precedence over the top-level params (mirrors createToken() behaviour).
1379
+ let normalizedBankBilling;
1380
+ let bankFirstName = ((_a = options.firstName) !== null && _a !== void 0 ? _a : '').trim();
1381
+ let bankLastName = ((_b = options.lastName) !== null && _b !== void 0 ? _b : '').trim();
1382
+ if (options.billing) {
1383
+ const result = validateBilling(options.billing);
1384
+ if (!result.valid) {
1385
+ throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
1386
+ }
1387
+ normalizedBankBilling = result.normalized;
1388
+ bankFirstName = normalizedBankBilling.firstName;
1389
+ bankLastName = normalizedBankBilling.lastName;
1342
1390
  }
1343
- if (options.lastName.trim().length > 50) {
1344
- throw new OzError('lastName must be 50 characters or fewer.');
1391
+ else {
1392
+ if (!bankFirstName)
1393
+ throw new OzError('firstName is required for bank account tokenization.');
1394
+ if (!bankLastName)
1395
+ throw new OzError('lastName is required for bank account tokenization.');
1396
+ if (bankFirstName.length > 50)
1397
+ throw new OzError('firstName must be 50 characters or fewer.');
1398
+ if (bankLastName.length > 50)
1399
+ throw new OzError('lastName must be 50 characters or fewer.');
1345
1400
  }
1346
1401
  const accountEl = this.bankElementsByType.get('accountNumber');
1347
1402
  const routingEl = this.bankElementsByType.get('routingNumber');
@@ -1367,14 +1422,7 @@ class OzVault {
1367
1422
  if (this._resetCount === resetCountAtStart)
1368
1423
  this._tokenizing = null;
1369
1424
  };
1370
- this.bankTokenizeResolvers.set(requestId, {
1371
- resolve: (v) => { cleanup(); resolve(v); },
1372
- reject: (e) => { cleanup(); reject(e); },
1373
- firstName: options.firstName.trim(),
1374
- lastName: options.lastName.trim(),
1375
- readyElements: readyBankElements,
1376
- fieldCount: readyBankElements.length,
1377
- });
1425
+ this.bankTokenizeResolvers.set(requestId, Object.assign(Object.assign({ resolve: (v) => { cleanup(); resolve(v); }, reject: (e) => { cleanup(); reject(e); }, firstName: bankFirstName, lastName: bankLastName }, (normalizedBankBilling ? { billing: normalizedBankBilling } : {})), { readyElements: readyBankElements, fieldCount: readyBankElements.length }));
1378
1426
  try {
1379
1427
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1380
1428
  const bankTokenizeStartMs = Date.now();
@@ -1383,8 +1431,8 @@ class OzVault {
1383
1431
  requestId,
1384
1432
  tokenizationSessionId: this.tokenizationSessionId,
1385
1433
  pubKey: (_a = this.pubKey) !== null && _a !== void 0 ? _a : '',
1386
- firstName: options.firstName.trim(),
1387
- lastName: options.lastName.trim(),
1434
+ firstName: bankFirstName,
1435
+ lastName: bankLastName,
1388
1436
  fieldCount: readyBankElements.length,
1389
1437
  }, bankChannels.map(ch => ch.port1));
1390
1438
  this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
@@ -2106,7 +2154,7 @@ class OzVault {
2106
2154
  break;
2107
2155
  }
2108
2156
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
2109
- pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
2157
+ pending.resolve(Object.assign(Object.assign({ token }, (bank ? { bank } : {})), (pending.billing ? { billing: pending.billing } : {})));
2110
2158
  this.log('bank token received', {
2111
2159
  elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
2112
2160
  tokenPresent: true,
@@ -2273,6 +2321,7 @@ const OzElements = vue.defineComponent({
2273
2321
  debug: { type: Boolean, default: undefined },
2274
2322
  onSessionRefresh: { type: Function, default: undefined },
2275
2323
  onReady: { type: Function, default: undefined },
2324
+ onLoadError: { type: Function, default: undefined },
2276
2325
  sessionLimit: { type: Number, default: undefined },
2277
2326
  maxTokenizeCalls: { type: Number, default: undefined },
2278
2327
  },
@@ -2283,6 +2332,8 @@ const OzElements = vue.defineComponent({
2283
2332
  const mountedCount = vue.ref(0);
2284
2333
  const readyCount = vue.ref(0);
2285
2334
  const tokenizeCount = vue.ref(0);
2335
+ const changeTick = vue.ref(0);
2336
+ const notifyChange = () => { changeTick.value++; };
2286
2337
  const notifyMount = () => { mountedCount.value++; };
2287
2338
  let readyEmitted = false;
2288
2339
  const notifyReady = () => {
@@ -2308,17 +2359,32 @@ const OzElements = vue.defineComponent({
2308
2359
  mountedCount,
2309
2360
  readyCount,
2310
2361
  tokenizeCount,
2362
+ changeTick,
2311
2363
  notifyMount,
2312
2364
  notifyReady,
2313
2365
  notifyUnmount,
2314
2366
  notifyTokenize,
2367
+ notifyChange,
2315
2368
  });
2316
2369
  let createdVault = null;
2317
2370
  let abortController = null;
2318
2371
  vue.onMounted(() => {
2319
2372
  const ac = new AbortController();
2320
2373
  abortController = ac;
2321
- OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey: props.pubKey }, (props.sessionUrl ? { sessionUrl: props.sessionUrl } : {})), (props.getSessionKey ? { getSessionKey: props.getSessionKey } : {})), (props.frameBaseUrl ? { frameBaseUrl: props.frameBaseUrl } : {})), (props.fonts ? { fonts: props.fonts } : {})), (props.appearance ? { appearance: props.appearance } : {})), (props.loadTimeoutMs !== undefined ? { loadTimeoutMs: props.loadTimeoutMs } : {})), (props.debug ? { debug: props.debug } : {})), {
2374
+ // Guard: onLoadError must fire at most once per mount cycle. It can be
2375
+ // triggered by two independent paths — the vault's iframe load timeout
2376
+ // (inside OzVault constructor) and the .catch below when create() rejects
2377
+ // after the timeout has already fired. Without this flag both paths would
2378
+ // call the callback, mirroring the same guard used in the React provider.
2379
+ let loadErrorFired = false;
2380
+ const fireLoadError = () => {
2381
+ var _a;
2382
+ if (loadErrorFired)
2383
+ return;
2384
+ loadErrorFired = true;
2385
+ (_a = props.onLoadError) === null || _a === void 0 ? void 0 : _a.call(props, { source: 'tokenizer' });
2386
+ };
2387
+ OzVault.create(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ pubKey: props.pubKey }, (props.sessionUrl ? { sessionUrl: props.sessionUrl } : {})), (props.getSessionKey ? { getSessionKey: props.getSessionKey } : {})), (props.frameBaseUrl ? { frameBaseUrl: props.frameBaseUrl } : {})), (props.fonts ? { fonts: props.fonts } : {})), (props.appearance ? { appearance: props.appearance } : {})), (props.loadTimeoutMs !== undefined ? { loadTimeoutMs: props.loadTimeoutMs } : {})), (props.onLoadError ? { onLoadError: fireLoadError } : {})), (props.debug ? { debug: props.debug } : {})), {
2322
2388
  // Session lifecycle — wire refresh callback and reset tokenizeCount so the
2323
2389
  // counter stays accurate across proactive key refreshes (mirrors React provider).
2324
2390
  // Deferred by one microtask for the same reason as React: notifyTokenize fires
@@ -2340,6 +2406,12 @@ const OzElements = vue.defineComponent({
2340
2406
  if (ac.signal.aborted)
2341
2407
  return;
2342
2408
  initError.value = err instanceof Error ? err : new Error('OzVault.create() failed.');
2409
+ if (props.onLoadError) {
2410
+ fireLoadError();
2411
+ }
2412
+ else {
2413
+ console.error('[OzElements] OzVault.create() failed. Provide an `onLoadError` prop to handle this gracefully.', err);
2414
+ }
2343
2415
  });
2344
2416
  });
2345
2417
  vue.onUnmounted(() => {
@@ -2361,7 +2433,7 @@ function useOzElements() {
2361
2433
  if (!ctx) {
2362
2434
  throw new Error('[OzVault] useOzElements() must be called inside <OzElements>');
2363
2435
  }
2364
- const { vault, initError, mountedCount, readyCount, tokenizeCount, notifyTokenize } = ctx;
2436
+ const { vault, initError, mountedCount, readyCount, tokenizeCount, changeTick, notifyTokenize, notifyChange } = ctx;
2365
2437
  const ready = vue.computed(() => vault.value !== null &&
2366
2438
  vault.value.isReady &&
2367
2439
  mountedCount.value > 0 &&
@@ -2370,20 +2442,49 @@ function useOzElements() {
2370
2442
  if (!vault.value) {
2371
2443
  throw new Error('[OzVault] vault is not ready — wait for ready before calling createToken()');
2372
2444
  }
2373
- const result = await vault.value.createToken(options);
2374
- notifyTokenize();
2375
- return result;
2445
+ // Start the call so vault._tokenizing flips synchronously, then bump changeTick
2446
+ // so `isTokenizing` recomputes as `true`; bump again on settle. (vault.isTokenizing
2447
+ // is a plain getter — nothing else would recompute the computed while in flight.)
2448
+ const promise = vault.value.createToken(options);
2449
+ notifyChange();
2450
+ try {
2451
+ const result = await promise;
2452
+ notifyTokenize();
2453
+ return result;
2454
+ }
2455
+ finally {
2456
+ notifyChange();
2457
+ }
2376
2458
  };
2377
2459
  const createBankToken = async (options) => {
2378
2460
  if (!vault.value) {
2379
2461
  throw new Error('[OzVault] vault is not ready — wait for ready before calling createBankToken()');
2380
2462
  }
2381
- const result = await vault.value.createBankToken(options);
2382
- notifyTokenize();
2383
- return result;
2463
+ const promise = vault.value.createBankToken(options);
2464
+ notifyChange();
2465
+ try {
2466
+ const result = await promise;
2467
+ notifyTokenize();
2468
+ return result;
2469
+ }
2470
+ finally {
2471
+ notifyChange();
2472
+ }
2473
+ };
2474
+ const reset = () => {
2475
+ var _a;
2476
+ (_a = vault.value) === null || _a === void 0 ? void 0 : _a.reset();
2477
+ // reset() clears completion state and cancels any in-flight tokenization;
2478
+ // bump changeTick so the derived computeds below recompute.
2479
+ notifyChange();
2384
2480
  };
2385
- const reset = () => { var _a; (_a = vault.value) === null || _a === void 0 ? void 0 : _a.reset(); };
2386
- return { createToken, createBankToken, ready, initError, tokenizeCount, reset };
2481
+ // `void changeTick.value` registers a reactive dependency so these recompute when
2482
+ // notifyChange() fires (field change / tokenize start-stop). vault.isComplete and
2483
+ // vault.isTokenizing read non-reactive internal Maps/flags that Vue cannot track.
2484
+ const isComplete = vue.computed(() => { var _a, _b; void changeTick.value; return (_b = (_a = vault.value) === null || _a === void 0 ? void 0 : _a.isComplete) !== null && _b !== void 0 ? _b : false; });
2485
+ const isBankComplete = vue.computed(() => { var _a, _b; void changeTick.value; return (_b = (_a = vault.value) === null || _a === void 0 ? void 0 : _a.isBankComplete) !== null && _b !== void 0 ? _b : false; });
2486
+ const isTokenizing = vue.computed(() => { var _a, _b; void changeTick.value; return (_b = (_a = vault.value) === null || _a === void 0 ? void 0 : _a.isTokenizing) !== null && _b !== void 0 ? _b : false; });
2487
+ return { createToken, createBankToken, ready, initError, tokenizeCount, reset, isComplete, isBankComplete, isTokenizing };
2387
2488
  }
2388
2489
  function createFieldComponent(displayName, mountElement) {
2389
2490
  return vue.defineComponent({
@@ -2399,7 +2500,7 @@ function createFieldComponent(displayName, mountElement) {
2399
2500
  if (!ctx) {
2400
2501
  throw new Error('[OzVault] useOzElements() must be called inside <OzElements>');
2401
2502
  }
2402
- const { vault, notifyMount, notifyReady, notifyUnmount } = ctx;
2503
+ const { vault, notifyMount, notifyReady, notifyUnmount, notifyChange } = ctx;
2403
2504
  const containerRef = vue.ref(null);
2404
2505
  let element = null;
2405
2506
  let notifyMountCalled = false;
@@ -2414,7 +2515,7 @@ function createFieldComponent(displayName, mountElement) {
2414
2515
  notifyMountCalled = true;
2415
2516
  notifyMount();
2416
2517
  element.on('ready', () => notifyReady());
2417
- element.on('change', (e) => emit('change', e));
2518
+ element.on('change', (e) => { emit('change', e); notifyChange(); });
2418
2519
  element.on('focus', () => emit('focus'));
2419
2520
  element.on('blur', () => emit('blur'));
2420
2521
  element.mount(containerRef.value);
@@ -2452,4 +2553,3 @@ exports.OzCvv = OzCvv;
2452
2553
  exports.OzElements = OzElements;
2453
2554
  exports.OzExpiry = OzExpiry;
2454
2555
  exports.useOzElements = useOzElements;
2455
- //# sourceMappingURL=index.cjs.js.map