@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.
@@ -17,6 +17,6 @@
17
17
  </style>
18
18
  </head>
19
19
  <body>
20
- <script src="./element-frame.js" integrity="sha384-xpV1wS1ZIxuDGHUg8WQAbQr94m8oGwI4CutCvD37d7sacy9sRmi4CA/EU62Mrd2I" crossorigin="anonymous"></script>
20
+ <script src="./element-frame.js" integrity="sha384-cIulRhFy1VmJOkAhGUL37LXgMI2dlGiy0XbrrqeZ/IvsXlcq7U+Z1+KLIKb4Ty1G" crossorigin="anonymous"></script>
21
21
  </body>
22
22
  </html>
@@ -588,10 +588,13 @@ var _OzElementFrame = (function (exports) {
588
588
  const msg = event.data;
589
589
  if (!msg || msg.__oz !== true || msg.vaultId !== this.vaultId)
590
590
  return;
591
- if (!this.hostOrigin) {
592
- this.hostOrigin = event.origin;
593
- }
594
- else if (event.origin !== this.hostOrigin) {
591
+ // Reject messages when parentOrigin was absent/invalid, and messages from
592
+ // any origin that doesn't match the validated parentOrigin. Never adopt an
593
+ // unknown sender as the host — that would let any page that guesses the
594
+ // vaultId hijack the frame's postMessage target. The SDK always supplies
595
+ // parentOrigin in the hash; if it's missing, the frame is non-functional
596
+ // by design rather than open to the first sender.
597
+ if (!this.hostOrigin || event.origin !== this.hostOrigin) {
595
598
  return;
596
599
  }
597
600
  switch (msg.type) {
@@ -6,6 +6,6 @@
6
6
  <title>Ozura Tokenizer</title>
7
7
  </head>
8
8
  <body>
9
- <script src="./tokenizer-frame.js" integrity="sha384-jKzyFta4CjrMb8YW6wX9/MzSUp55uqUytZeya/g55SgF9IvsJ39Jveh4AhwszApc" crossorigin="anonymous"></script>
9
+ <script src="./tokenizer-frame.js" integrity="sha384-n3WeN859wcdlVjR0zCrcbTWEtfZyxw682z21vn2OGkjLOtPfrDa9IqywSkLhNh3e" crossorigin="anonymous"></script>
10
10
  </body>
11
11
  </html>
@@ -143,15 +143,14 @@ var _OzTokenizerFrame = (function (exports) {
143
143
  return;
144
144
  switch (msg.type) {
145
145
  case 'OZ_INIT':
146
- if (!this.hostOrigin) {
147
- this.hostOrigin = event.origin;
148
- }
146
+ // Never adopt an unknown sender as the host. If parentOrigin was absent
147
+ // or failed safeParentOrigin() validation, hostOrigin is "" and this
148
+ // frame is non-functional by design — rejecting all messages is safer
149
+ // than trusting the first sender. The SDK always supplies parentOrigin.
149
150
  // Origin-validated wax key store. Accepts both the initial delivery and
150
151
  // subsequent refresh deliveries (sent by OzVault.refreshWaxKey() after
151
152
  // minting a new key). Origin validation is the security boundary —
152
- // messages from any origin other than the one that sent the first
153
- // OZ_INIT are rejected. The previous write-once guard blocked refreshes
154
- // and made the auto-refresh feature non-functional.
153
+ // messages from any origin other than hostOrigin are silently dropped.
155
154
  if (event.origin === this.hostOrigin && typeof msg.waxKey === 'string' && msg.waxKey) {
156
155
  this.waxKey = msg.waxKey;
157
156
  }
@@ -1167,7 +1167,7 @@ class OzVault {
1167
1167
  this.loadErrorTimeoutId = setTimeout(() => {
1168
1168
  this.loadErrorTimeoutId = null;
1169
1169
  if (!this._destroyed && !this.tokenizerReady) {
1170
- options.onLoadError();
1170
+ options.onLoadError({ source: 'tokenizer' });
1171
1171
  }
1172
1172
  }, timeout);
1173
1173
  }
@@ -1300,6 +1300,49 @@ class OzVault {
1300
1300
  get tokenizeCount() {
1301
1301
  return this._tokenizeSuccessCount;
1302
1302
  }
1303
+ /**
1304
+ * `true` when every mounted field has reported `complete && valid` via its
1305
+ * last `change` event. `false` if no fields have been created, or if any
1306
+ * field is incomplete or invalid.
1307
+ *
1308
+ * Use this to gate the pay button in vanilla JS integrations without having
1309
+ * to wire up individual `change` event listeners:
1310
+ *
1311
+ * @example
1312
+ * vault.getElement('cardNumber')!.on('change', () => {
1313
+ * payBtn.disabled = !vault.isComplete;
1314
+ * });
1315
+ */
1316
+ get isComplete() {
1317
+ return this.allComplete([...this.elementsByType.values()]);
1318
+ }
1319
+ /**
1320
+ * Like {@link isComplete}, but for bank-account elements created via
1321
+ * {@link createBankElement}. Card and bank fields are tracked separately so a
1322
+ * card-only checkout is never gated on bank fields (and vice versa), matching
1323
+ * the `createToken()` / `createBankToken()` split. A vault with both card and
1324
+ * bank elements exposes each completion state independently.
1325
+ */
1326
+ get isBankComplete() {
1327
+ return this.allComplete([...this.bankElementsByType.values()]);
1328
+ }
1329
+ /** True iff the set is non-empty and every element has reported complete-and-valid. */
1330
+ allComplete(els) {
1331
+ if (els.length === 0)
1332
+ return false;
1333
+ return els.every(el => this.completionState.get(el.frameId) === true);
1334
+ }
1335
+ /**
1336
+ * `true` while a `createToken()` or `createBankToken()` call is in progress
1337
+ * (including the transparent wax-key refresh phase). Use this to keep the pay
1338
+ * button disabled during tokenization to prevent double-submission.
1339
+ *
1340
+ * @example
1341
+ * payBtn.disabled = vault.isTokenizing;
1342
+ */
1343
+ get isTokenizing() {
1344
+ return this._tokenizing !== null;
1345
+ }
1303
1346
  /**
1304
1347
  * Creates a new OzElement of the given type. Call `.mount(selector)` on the
1305
1348
  * returned element to attach it to the DOM.
@@ -1382,17 +1425,29 @@ class OzVault {
1382
1425
  ? 'A card tokenization is already in progress. Wait for it to complete before calling createBankToken().'
1383
1426
  : 'A bank tokenization is already in progress. Wait for it to complete before calling createBankToken() again.');
1384
1427
  }
1385
- if (!((_a = options.firstName) === null || _a === void 0 ? void 0 : _a.trim())) {
1386
- throw new OzError('firstName is required for bank account tokenization.');
1387
- }
1388
- if (!((_b = options.lastName) === null || _b === void 0 ? void 0 : _b.trim())) {
1389
- throw new OzError('lastName is required for bank account tokenization.');
1390
- }
1391
- if (options.firstName.trim().length > 50) {
1392
- throw new OzError('firstName must be 50 characters or fewer.');
1428
+ // Validate billing details if provided billing.firstName/lastName take
1429
+ // precedence over the top-level params (mirrors createToken() behaviour).
1430
+ let normalizedBankBilling;
1431
+ let bankFirstName = ((_a = options.firstName) !== null && _a !== void 0 ? _a : '').trim();
1432
+ let bankLastName = ((_b = options.lastName) !== null && _b !== void 0 ? _b : '').trim();
1433
+ if (options.billing) {
1434
+ const result = validateBilling(options.billing);
1435
+ if (!result.valid) {
1436
+ throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
1437
+ }
1438
+ normalizedBankBilling = result.normalized;
1439
+ bankFirstName = normalizedBankBilling.firstName;
1440
+ bankLastName = normalizedBankBilling.lastName;
1393
1441
  }
1394
- if (options.lastName.trim().length > 50) {
1395
- throw new OzError('lastName must be 50 characters or fewer.');
1442
+ else {
1443
+ if (!bankFirstName)
1444
+ throw new OzError('firstName is required for bank account tokenization.');
1445
+ if (!bankLastName)
1446
+ throw new OzError('lastName is required for bank account tokenization.');
1447
+ if (bankFirstName.length > 50)
1448
+ throw new OzError('firstName must be 50 characters or fewer.');
1449
+ if (bankLastName.length > 50)
1450
+ throw new OzError('lastName must be 50 characters or fewer.');
1396
1451
  }
1397
1452
  const accountEl = this.bankElementsByType.get('accountNumber');
1398
1453
  const routingEl = this.bankElementsByType.get('routingNumber');
@@ -1418,14 +1473,7 @@ class OzVault {
1418
1473
  if (this._resetCount === resetCountAtStart)
1419
1474
  this._tokenizing = null;
1420
1475
  };
1421
- this.bankTokenizeResolvers.set(requestId, {
1422
- resolve: (v) => { cleanup(); resolve(v); },
1423
- reject: (e) => { cleanup(); reject(e); },
1424
- firstName: options.firstName.trim(),
1425
- lastName: options.lastName.trim(),
1426
- readyElements: readyBankElements,
1427
- fieldCount: readyBankElements.length,
1428
- });
1476
+ 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 }));
1429
1477
  try {
1430
1478
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1431
1479
  const bankTokenizeStartMs = Date.now();
@@ -1434,8 +1482,8 @@ class OzVault {
1434
1482
  requestId,
1435
1483
  tokenizationSessionId: this.tokenizationSessionId,
1436
1484
  pubKey: (_a = this.pubKey) !== null && _a !== void 0 ? _a : '',
1437
- firstName: options.firstName.trim(),
1438
- lastName: options.lastName.trim(),
1485
+ firstName: bankFirstName,
1486
+ lastName: bankLastName,
1439
1487
  fieldCount: readyBankElements.length,
1440
1488
  }, bankChannels.map(ch => ch.port1));
1441
1489
  this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
@@ -2157,7 +2205,7 @@ class OzVault {
2157
2205
  break;
2158
2206
  }
2159
2207
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
2160
- pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
2208
+ pending.resolve(Object.assign(Object.assign({ token }, (bank ? { bank } : {})), (pending.billing ? { billing: pending.billing } : {})));
2161
2209
  this.log('bank token received', {
2162
2210
  elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
2163
2211
  tokenPresent: true,
@@ -1173,7 +1173,7 @@
1173
1173
  this.loadErrorTimeoutId = setTimeout(() => {
1174
1174
  this.loadErrorTimeoutId = null;
1175
1175
  if (!this._destroyed && !this.tokenizerReady) {
1176
- options.onLoadError();
1176
+ options.onLoadError({ source: 'tokenizer' });
1177
1177
  }
1178
1178
  }, timeout);
1179
1179
  }
@@ -1306,6 +1306,49 @@
1306
1306
  get tokenizeCount() {
1307
1307
  return this._tokenizeSuccessCount;
1308
1308
  }
1309
+ /**
1310
+ * `true` when every mounted field has reported `complete && valid` via its
1311
+ * last `change` event. `false` if no fields have been created, or if any
1312
+ * field is incomplete or invalid.
1313
+ *
1314
+ * Use this to gate the pay button in vanilla JS integrations without having
1315
+ * to wire up individual `change` event listeners:
1316
+ *
1317
+ * @example
1318
+ * vault.getElement('cardNumber')!.on('change', () => {
1319
+ * payBtn.disabled = !vault.isComplete;
1320
+ * });
1321
+ */
1322
+ get isComplete() {
1323
+ return this.allComplete([...this.elementsByType.values()]);
1324
+ }
1325
+ /**
1326
+ * Like {@link isComplete}, but for bank-account elements created via
1327
+ * {@link createBankElement}. Card and bank fields are tracked separately so a
1328
+ * card-only checkout is never gated on bank fields (and vice versa), matching
1329
+ * the `createToken()` / `createBankToken()` split. A vault with both card and
1330
+ * bank elements exposes each completion state independently.
1331
+ */
1332
+ get isBankComplete() {
1333
+ return this.allComplete([...this.bankElementsByType.values()]);
1334
+ }
1335
+ /** True iff the set is non-empty and every element has reported complete-and-valid. */
1336
+ allComplete(els) {
1337
+ if (els.length === 0)
1338
+ return false;
1339
+ return els.every(el => this.completionState.get(el.frameId) === true);
1340
+ }
1341
+ /**
1342
+ * `true` while a `createToken()` or `createBankToken()` call is in progress
1343
+ * (including the transparent wax-key refresh phase). Use this to keep the pay
1344
+ * button disabled during tokenization to prevent double-submission.
1345
+ *
1346
+ * @example
1347
+ * payBtn.disabled = vault.isTokenizing;
1348
+ */
1349
+ get isTokenizing() {
1350
+ return this._tokenizing !== null;
1351
+ }
1309
1352
  /**
1310
1353
  * Creates a new OzElement of the given type. Call `.mount(selector)` on the
1311
1354
  * returned element to attach it to the DOM.
@@ -1388,17 +1431,29 @@
1388
1431
  ? 'A card tokenization is already in progress. Wait for it to complete before calling createBankToken().'
1389
1432
  : 'A bank tokenization is already in progress. Wait for it to complete before calling createBankToken() again.');
1390
1433
  }
1391
- if (!((_a = options.firstName) === null || _a === void 0 ? void 0 : _a.trim())) {
1392
- throw new OzError('firstName is required for bank account tokenization.');
1393
- }
1394
- if (!((_b = options.lastName) === null || _b === void 0 ? void 0 : _b.trim())) {
1395
- throw new OzError('lastName is required for bank account tokenization.');
1396
- }
1397
- if (options.firstName.trim().length > 50) {
1398
- throw new OzError('firstName must be 50 characters or fewer.');
1434
+ // Validate billing details if provided billing.firstName/lastName take
1435
+ // precedence over the top-level params (mirrors createToken() behaviour).
1436
+ let normalizedBankBilling;
1437
+ let bankFirstName = ((_a = options.firstName) !== null && _a !== void 0 ? _a : '').trim();
1438
+ let bankLastName = ((_b = options.lastName) !== null && _b !== void 0 ? _b : '').trim();
1439
+ if (options.billing) {
1440
+ const result = validateBilling(options.billing);
1441
+ if (!result.valid) {
1442
+ throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
1443
+ }
1444
+ normalizedBankBilling = result.normalized;
1445
+ bankFirstName = normalizedBankBilling.firstName;
1446
+ bankLastName = normalizedBankBilling.lastName;
1399
1447
  }
1400
- if (options.lastName.trim().length > 50) {
1401
- throw new OzError('lastName must be 50 characters or fewer.');
1448
+ else {
1449
+ if (!bankFirstName)
1450
+ throw new OzError('firstName is required for bank account tokenization.');
1451
+ if (!bankLastName)
1452
+ throw new OzError('lastName is required for bank account tokenization.');
1453
+ if (bankFirstName.length > 50)
1454
+ throw new OzError('firstName must be 50 characters or fewer.');
1455
+ if (bankLastName.length > 50)
1456
+ throw new OzError('lastName must be 50 characters or fewer.');
1402
1457
  }
1403
1458
  const accountEl = this.bankElementsByType.get('accountNumber');
1404
1459
  const routingEl = this.bankElementsByType.get('routingNumber');
@@ -1424,14 +1479,7 @@
1424
1479
  if (this._resetCount === resetCountAtStart)
1425
1480
  this._tokenizing = null;
1426
1481
  };
1427
- this.bankTokenizeResolvers.set(requestId, {
1428
- resolve: (v) => { cleanup(); resolve(v); },
1429
- reject: (e) => { cleanup(); reject(e); },
1430
- firstName: options.firstName.trim(),
1431
- lastName: options.lastName.trim(),
1432
- readyElements: readyBankElements,
1433
- fieldCount: readyBankElements.length,
1434
- });
1482
+ 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 }));
1435
1483
  try {
1436
1484
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1437
1485
  const bankTokenizeStartMs = Date.now();
@@ -1440,8 +1488,8 @@
1440
1488
  requestId,
1441
1489
  tokenizationSessionId: this.tokenizationSessionId,
1442
1490
  pubKey: (_a = this.pubKey) !== null && _a !== void 0 ? _a : '',
1443
- firstName: options.firstName.trim(),
1444
- lastName: options.lastName.trim(),
1491
+ firstName: bankFirstName,
1492
+ lastName: bankLastName,
1445
1493
  fieldCount: readyBankElements.length,
1446
1494
  }, bankChannels.map(ch => ch.port1));
1447
1495
  this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
@@ -2163,7 +2211,7 @@
2163
2211
  break;
2164
2212
  }
2165
2213
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
2166
- pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
2214
+ pending.resolve(Object.assign(Object.assign({ token }, (bank ? { bank } : {})), (pending.billing ? { billing: pending.billing } : {})));
2167
2215
  this.log('bank token received', {
2168
2216
  elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
2169
2217
  tokenPresent: true,
@@ -1117,7 +1117,7 @@ class OzVault {
1117
1117
  this.loadErrorTimeoutId = setTimeout(() => {
1118
1118
  this.loadErrorTimeoutId = null;
1119
1119
  if (!this._destroyed && !this.tokenizerReady) {
1120
- options.onLoadError();
1120
+ options.onLoadError({ source: 'tokenizer' });
1121
1121
  }
1122
1122
  }, timeout);
1123
1123
  }
@@ -1250,6 +1250,49 @@ class OzVault {
1250
1250
  get tokenizeCount() {
1251
1251
  return this._tokenizeSuccessCount;
1252
1252
  }
1253
+ /**
1254
+ * `true` when every mounted field has reported `complete && valid` via its
1255
+ * last `change` event. `false` if no fields have been created, or if any
1256
+ * field is incomplete or invalid.
1257
+ *
1258
+ * Use this to gate the pay button in vanilla JS integrations without having
1259
+ * to wire up individual `change` event listeners:
1260
+ *
1261
+ * @example
1262
+ * vault.getElement('cardNumber')!.on('change', () => {
1263
+ * payBtn.disabled = !vault.isComplete;
1264
+ * });
1265
+ */
1266
+ get isComplete() {
1267
+ return this.allComplete([...this.elementsByType.values()]);
1268
+ }
1269
+ /**
1270
+ * Like {@link isComplete}, but for bank-account elements created via
1271
+ * {@link createBankElement}. Card and bank fields are tracked separately so a
1272
+ * card-only checkout is never gated on bank fields (and vice versa), matching
1273
+ * the `createToken()` / `createBankToken()` split. A vault with both card and
1274
+ * bank elements exposes each completion state independently.
1275
+ */
1276
+ get isBankComplete() {
1277
+ return this.allComplete([...this.bankElementsByType.values()]);
1278
+ }
1279
+ /** True iff the set is non-empty and every element has reported complete-and-valid. */
1280
+ allComplete(els) {
1281
+ if (els.length === 0)
1282
+ return false;
1283
+ return els.every(el => this.completionState.get(el.frameId) === true);
1284
+ }
1285
+ /**
1286
+ * `true` while a `createToken()` or `createBankToken()` call is in progress
1287
+ * (including the transparent wax-key refresh phase). Use this to keep the pay
1288
+ * button disabled during tokenization to prevent double-submission.
1289
+ *
1290
+ * @example
1291
+ * payBtn.disabled = vault.isTokenizing;
1292
+ */
1293
+ get isTokenizing() {
1294
+ return this._tokenizing !== null;
1295
+ }
1253
1296
  /**
1254
1297
  * Creates a new OzElement of the given type. Call `.mount(selector)` on the
1255
1298
  * returned element to attach it to the DOM.
@@ -1332,17 +1375,29 @@ class OzVault {
1332
1375
  ? 'A card tokenization is already in progress. Wait for it to complete before calling createBankToken().'
1333
1376
  : 'A bank tokenization is already in progress. Wait for it to complete before calling createBankToken() again.');
1334
1377
  }
1335
- if (!((_a = options.firstName) === null || _a === void 0 ? void 0 : _a.trim())) {
1336
- throw new OzError('firstName is required for bank account tokenization.');
1337
- }
1338
- if (!((_b = options.lastName) === null || _b === void 0 ? void 0 : _b.trim())) {
1339
- throw new OzError('lastName is required for bank account tokenization.');
1340
- }
1341
- if (options.firstName.trim().length > 50) {
1342
- throw new OzError('firstName must be 50 characters or fewer.');
1378
+ // Validate billing details if provided billing.firstName/lastName take
1379
+ // precedence over the top-level params (mirrors createToken() behaviour).
1380
+ let normalizedBankBilling;
1381
+ let bankFirstName = ((_a = options.firstName) !== null && _a !== void 0 ? _a : '').trim();
1382
+ let bankLastName = ((_b = options.lastName) !== null && _b !== void 0 ? _b : '').trim();
1383
+ if (options.billing) {
1384
+ const result = validateBilling(options.billing);
1385
+ if (!result.valid) {
1386
+ throw new OzError(`Invalid billing details: ${result.errors.join('; ')}`);
1387
+ }
1388
+ normalizedBankBilling = result.normalized;
1389
+ bankFirstName = normalizedBankBilling.firstName;
1390
+ bankLastName = normalizedBankBilling.lastName;
1343
1391
  }
1344
- if (options.lastName.trim().length > 50) {
1345
- throw new OzError('lastName must be 50 characters or fewer.');
1392
+ else {
1393
+ if (!bankFirstName)
1394
+ throw new OzError('firstName is required for bank account tokenization.');
1395
+ if (!bankLastName)
1396
+ throw new OzError('lastName is required for bank account tokenization.');
1397
+ if (bankFirstName.length > 50)
1398
+ throw new OzError('firstName must be 50 characters or fewer.');
1399
+ if (bankLastName.length > 50)
1400
+ throw new OzError('lastName must be 50 characters or fewer.');
1346
1401
  }
1347
1402
  const accountEl = this.bankElementsByType.get('accountNumber');
1348
1403
  const routingEl = this.bankElementsByType.get('routingNumber');
@@ -1368,14 +1423,7 @@ class OzVault {
1368
1423
  if (this._resetCount === resetCountAtStart)
1369
1424
  this._tokenizing = null;
1370
1425
  };
1371
- this.bankTokenizeResolvers.set(requestId, {
1372
- resolve: (v) => { cleanup(); resolve(v); },
1373
- reject: (e) => { cleanup(); reject(e); },
1374
- firstName: options.firstName.trim(),
1375
- lastName: options.lastName.trim(),
1376
- readyElements: readyBankElements,
1377
- fieldCount: readyBankElements.length,
1378
- });
1426
+ 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 }));
1379
1427
  try {
1380
1428
  const bankChannels = readyBankElements.map(() => new MessageChannel());
1381
1429
  const bankTokenizeStartMs = Date.now();
@@ -1384,8 +1432,8 @@ class OzVault {
1384
1432
  requestId,
1385
1433
  tokenizationSessionId: this.tokenizationSessionId,
1386
1434
  pubKey: (_a = this.pubKey) !== null && _a !== void 0 ? _a : '',
1387
- firstName: options.firstName.trim(),
1388
- lastName: options.lastName.trim(),
1435
+ firstName: bankFirstName,
1436
+ lastName: bankLastName,
1389
1437
  fieldCount: readyBankElements.length,
1390
1438
  }, bankChannels.map(ch => ch.port1));
1391
1439
  this.log('OZ_BANK_TOKENIZE sent', { requestIdPrefix: `${requestId.slice(0, 12)}...`, fieldCount: readyBankElements.length });
@@ -2107,7 +2155,7 @@ class OzVault {
2107
2155
  break;
2108
2156
  }
2109
2157
  const bank = isBankAccountMetadata(msg.bank) ? msg.bank : undefined;
2110
- pending.resolve(Object.assign({ token }, (bank ? { bank } : {})));
2158
+ pending.resolve(Object.assign(Object.assign({ token }, (bank ? { bank } : {})), (pending.billing ? { billing: pending.billing } : {})));
2111
2159
  this.log('bank token received', {
2112
2160
  elapsedMs: pending.tokenizeStartMs != null ? Date.now() - pending.tokenizeStartMs : null,
2113
2161
  tokenPresent: true,
@@ -2237,9 +2285,11 @@ const OzContext = react.createContext({
2237
2285
  notifyUnmount: () => { },
2238
2286
  notifyMount: () => { },
2239
2287
  notifyTokenize: () => { },
2288
+ notifyChange: () => { },
2240
2289
  mountedCount: 0,
2241
2290
  readyCount: 0,
2242
2291
  tokenizeCount: 0,
2292
+ changeTick: 0,
2243
2293
  });
2244
2294
  /**
2245
2295
  * Creates and owns an OzVault instance for the lifetime of this component.
@@ -2252,6 +2302,7 @@ function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseU
2252
2302
  const [mountedCount, setMountedCount] = react.useState(0);
2253
2303
  const [readyCount, setReadyCount] = react.useState(0);
2254
2304
  const [tokenizeCount, setTokenizeCount] = react.useState(0);
2305
+ const [changeTick, setChangeTick] = react.useState(0);
2255
2306
  const onLoadErrorRef = react.useRef(onLoadError);
2256
2307
  onLoadErrorRef.current = onLoadError;
2257
2308
  const onWaxRefreshRef = react.useRef(onSessionRefresh !== null && onSessionRefresh !== void 0 ? onSessionRefresh : onWaxRefresh);
@@ -2290,7 +2341,7 @@ function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseU
2290
2341
  if (loadErrorFired)
2291
2342
  return;
2292
2343
  loadErrorFired = true;
2293
- (_a = onLoadErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onLoadErrorRef);
2344
+ (_a = onLoadErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onLoadErrorRef, { source: 'tokenizer' });
2294
2345
  };
2295
2346
  // AbortController passed to create() so that if this effect's cleanup runs
2296
2347
  // while fetchWaxKey is still in-flight (React StrictMode double-invoke or
@@ -2374,7 +2425,8 @@ function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseU
2374
2425
  setReadyCount(n => Math.max(0, n - 1));
2375
2426
  }, []);
2376
2427
  const notifyTokenize = react.useCallback(() => setTokenizeCount(n => n + 1), []);
2377
- const value = react.useMemo(() => ({ vault, initError, notifyMount, notifyReady, notifyUnmount, notifyTokenize, mountedCount, readyCount, tokenizeCount }), [vault, initError, notifyMount, notifyReady, notifyUnmount, notifyTokenize, mountedCount, readyCount, tokenizeCount]);
2428
+ const notifyChange = react.useCallback(() => setChangeTick(n => n + 1), []);
2429
+ const value = react.useMemo(() => ({ vault, initError, notifyMount, notifyReady, notifyUnmount, notifyTokenize, notifyChange, mountedCount, readyCount, tokenizeCount, changeTick }), [vault, initError, notifyMount, notifyReady, notifyUnmount, notifyTokenize, notifyChange, mountedCount, readyCount, tokenizeCount, changeTick]);
2378
2430
  return jsxRuntime.jsx(OzContext.Provider, { value: value, children: children });
2379
2431
  }
2380
2432
  /**
@@ -2382,28 +2434,53 @@ function OzElements({ sessionUrl, getSessionKey, fetchWaxKey, pubKey, frameBaseU
2382
2434
  * an `<OzElements>` provider tree.
2383
2435
  */
2384
2436
  function useOzElements() {
2385
- const { vault, initError, mountedCount, readyCount, notifyTokenize, tokenizeCount } = react.useContext(OzContext);
2437
+ var _a, _b, _c;
2438
+ const { vault, initError, mountedCount, readyCount, notifyTokenize, notifyChange, tokenizeCount } = react.useContext(OzContext);
2386
2439
  const createToken = react.useCallback(async (options) => {
2387
2440
  if (!vault) {
2388
2441
  return Promise.reject(new OzError('useOzElements must be called inside an <OzElements> provider.'));
2389
2442
  }
2390
- const result = await vault.createToken(options);
2391
- notifyTokenize();
2392
- return result;
2393
- }, [vault, notifyTokenize]);
2443
+ // Start the call so vault._tokenizing flips synchronously, then notify so
2444
+ // `isTokenizing` re-renders as `true`. Notify again on settle so it returns
2445
+ // to `false`. (vault.isTokenizing is a plain getter — nothing else would
2446
+ // trigger a render while the call is in flight.)
2447
+ const promise = vault.createToken(options);
2448
+ notifyChange();
2449
+ try {
2450
+ const result = await promise;
2451
+ notifyTokenize();
2452
+ return result;
2453
+ }
2454
+ finally {
2455
+ notifyChange();
2456
+ }
2457
+ }, [vault, notifyTokenize, notifyChange]);
2394
2458
  const createBankToken = react.useCallback(async (options) => {
2395
2459
  if (!vault) {
2396
2460
  return Promise.reject(new OzError('useOzElements must be called inside an <OzElements> provider.'));
2397
2461
  }
2398
- const result = await vault.createBankToken(options);
2399
- notifyTokenize();
2400
- return result;
2401
- }, [vault, notifyTokenize]);
2462
+ const promise = vault.createBankToken(options);
2463
+ notifyChange();
2464
+ try {
2465
+ const result = await promise;
2466
+ notifyTokenize();
2467
+ return result;
2468
+ }
2469
+ finally {
2470
+ notifyChange();
2471
+ }
2472
+ }, [vault, notifyTokenize, notifyChange]);
2402
2473
  const reset = react.useCallback(() => {
2403
2474
  vault === null || vault === void 0 ? void 0 : vault.reset();
2404
- }, [vault]);
2475
+ // reset() clears completion state and cancels any in-flight tokenization, so
2476
+ // re-render to refresh the derived isComplete / isTokenizing getters below.
2477
+ notifyChange();
2478
+ }, [vault, notifyChange]);
2405
2479
  const ready = vault !== null && vault.isReady && mountedCount > 0 && readyCount >= mountedCount;
2406
- return { createToken, createBankToken, reset, ready, initError, tokenizeCount };
2480
+ const isComplete = (_a = vault === null || vault === void 0 ? void 0 : vault.isComplete) !== null && _a !== void 0 ? _a : false;
2481
+ const isBankComplete = (_b = vault === null || vault === void 0 ? void 0 : vault.isBankComplete) !== null && _b !== void 0 ? _b : false;
2482
+ const isTokenizing = (_c = vault === null || vault === void 0 ? void 0 : vault.isTokenizing) !== null && _c !== void 0 ? _c : false;
2483
+ return { createToken, createBankToken, reset, ready, initError, tokenizeCount, isComplete, isBankComplete, isTokenizing };
2407
2484
  }
2408
2485
  const SKELETON_STYLE = {
2409
2486
  height: 46,
@@ -2438,7 +2515,7 @@ function OzFieldBase({ type, variant, style, placeholder, disabled, loadTimeoutM
2438
2515
  const elementRef = react.useRef(null);
2439
2516
  const [loaded, setLoaded] = react.useState(false);
2440
2517
  const [loadError, setLoadError] = react.useState(null);
2441
- const { vault, notifyMount, notifyReady, notifyUnmount } = react.useContext(OzContext);
2518
+ const { vault, notifyMount, notifyReady, notifyUnmount, notifyChange } = react.useContext(OzContext);
2442
2519
  const onChangeRef = react.useRef(onChange);
2443
2520
  const onFocusRef = react.useRef(onFocus);
2444
2521
  const onBlurRef = react.useRef(onBlur);
@@ -2470,7 +2547,7 @@ function OzFieldBase({ type, variant, style, placeholder, disabled, loadTimeoutM
2470
2547
  notifyReady();
2471
2548
  (_a = onReadyRef.current) === null || _a === void 0 ? void 0 : _a.call(onReadyRef);
2472
2549
  });
2473
- element.on('change', (e) => { var _a; return (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, e); });
2550
+ element.on('change', (e) => { var _a; (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, e); notifyChange(); });
2474
2551
  element.on('focus', () => { var _a; return (_a = onFocusRef.current) === null || _a === void 0 ? void 0 : _a.call(onFocusRef); });
2475
2552
  element.on('blur', () => { var _a; return (_a = onBlurRef.current) === null || _a === void 0 ? void 0 : _a.call(onBlurRef); });
2476
2553
  element.on('loaderror', (e) => {
@@ -2713,4 +2790,3 @@ exports.OzExpiry = OzExpiry;
2713
2790
  exports.createFetchWaxKey = createSessionFetcher;
2714
2791
  exports.createSessionFetcher = createSessionFetcher;
2715
2792
  exports.useOzElements = useOzElements;
2716
- //# sourceMappingURL=index.cjs.js.map