flintn-checkout 0.0.16 → 0.0.17

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/README.md CHANGED
@@ -331,7 +331,11 @@ function CheckoutForm() {
331
331
  </div>
332
332
  <div style={{ flex: 1 }}>
333
333
  <label>CVV</label>
334
- <div ref={cvvRef} style={{ height: 40, marginBottom: 4 }} />
334
+ {/* position/zIndex let the CVV helper panel overlay content below */}
335
+ <div
336
+ ref={cvvRef}
337
+ style={{ height: 40, marginBottom: 4, position: 'relative', zIndex: 1 }}
338
+ />
335
339
  {fieldErrors['cvv'] && (
336
340
  <span style={{ color: 'red' }}>{fieldErrors['cvv']}</span>
337
341
  )}
@@ -379,7 +383,7 @@ const fields = createFlintNFields({
379
383
 
380
384
  const cardNumber = fields.createField('card-number', { placeholder: '4111 1111 1111 1111' });
381
385
  const expiry = fields.createField('expiry', { placeholder: 'MM/YY' });
382
- const cvv = fields.createField('cvv', { placeholder: 'CVC' });
386
+ const cvv = fields.createField('cvv');
383
387
 
384
388
  cardNumber.mount('#card-number');
385
389
  expiry.mount('#expiry');
@@ -435,7 +439,7 @@ Additional React-only options:
435
439
 
436
440
  | Property | Type | Default | Description |
437
441
  |----------|------|---------|-------------|
438
- | `placeholder` | `string` | Field-specific default | Input placeholder text |
442
+ | `placeholder` | `string` | `'1234 1234 1234 1234'` / `'MM/YY'` / `'***'` | Input placeholder text |
439
443
  | `disabled` | `boolean` | `false` | Disable the input |
440
444
 
441
445
  ### React Hook Return Values
@@ -469,6 +473,7 @@ Validation mirrors react-hook-form `mode: 'onSubmit'`:
469
473
  - After first submit: errors update on every field change (revalidation)
470
474
  - Card number changes trigger CVV revalidation (CVV length depends on card brand)
471
475
  - All three fields (card number, expiry, CVV) are required — submitting with a missing field returns a `MISSING_FIELDS` error
476
+ - The CVV field ships a built-in helper: tapping the card icon expands a “where to find CVV” panel below the input. The field iframe grows to fit it, so keep the CVV container at a fixed `height` with `position: relative; z-index: 1` — the open panel then overlays the content below (like a popover) instead of pushing your layout
472
477
 
473
478
  ### Field Types
474
479
 
@@ -476,7 +481,7 @@ Validation mirrors react-hook-form `mode: 'onSubmit'`:
476
481
  |-------|-------------|
477
482
  | `'card-number'` | Card number input with automatic formatting and brand detection |
478
483
  | `'expiry'` | Expiry date input (MM/YY format) |
479
- | `'cvv'` | CVV/CVC input (3 or 4 digits depending on card brand) |
484
+ | `'cvv'` | CVV/CVC input (3 or 4 digits depending on card brand). Includes a built-in “where to find CVV” helper: tapping the card icon expands an explainer panel below the input |
480
485
 
481
486
  ### Individual Field Methods (Vanilla JS)
482
487
 
@@ -632,7 +637,8 @@ Common `code` values:
632
637
  | `CONFIG_ERROR` | error | Session config failed to load and the API gave no code |
633
638
  | `NETWORK_ERROR` / `HTTP_<status>` / `INVALID_RESPONSE` | error or payment | Network failure / HTTP error without an API code / malformed response |
634
639
  | `VALIDATION_ERROR` | payment | `submit()` blocked by field validation (details in `fieldErrors`) |
635
- | `MISSING_FIELDS` / `NOT_INITIALIZED` / `SUBMIT_IN_PROGRESS` / `SUBMIT_TIMEOUT` | payment | Card `submit()` preconditions / duplicate call / no response in time (30s; 10 min during a 3DS challenge) |
640
+ | `MISSING_FIELDS` / `NOT_INITIALIZED` / `SUBMIT_IN_PROGRESS` / `SUBMIT_TIMEOUT` | payment | Card `submit()` preconditions / duplicate call / no response in time (30s) |
641
+ | `THREE_DS_TIMEOUT` | payment | A 3DS challenge stayed unresolved for 10 minutes |
636
642
  | `PAYMENT_DECLINED` / processor decline codes | payment | Declined; the processor's own code is passed through when present |
637
643
  | `PAYMENT_FAILED` | payment | Non-declined failure (including unexpected 3DS outcomes) |
638
644
  | `APPLE_PAY_SDK_ERROR` / `GOOGLE_PAY_SDK_ERROR` / `PAYPAL_ERROR` | payment | The wallet SDK failed mid-flow |
package/dist/index.js CHANGED
@@ -793,7 +793,7 @@ var injectAppleButtonStyle = () => {
793
793
  if (document.getElementById(APPLE_STYLE_ID)) return;
794
794
  const style = document.createElement("style");
795
795
  style.id = APPLE_STYLE_ID;
796
- style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out,transform .1s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.9;}.${APPLE_BUTTON_CLASS}:active{opacity:.8;transform:scale(.99);}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
796
+ style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.8;}.${APPLE_BUTTON_CLASS}:active{opacity:.7;}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
797
797
  document.head.appendChild(style);
798
798
  };
799
799
  var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
@@ -818,17 +818,23 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
818
818
  if (processing || !applePayConfig || version === null) return;
819
819
  if (!callbacks.canStart()) return;
820
820
  callbacks.onStart();
821
- const session = new window.ApplePaySession(version, {
822
- countryCode: applePayConfig.country_code,
823
- currencyCode: config.session.currency_code,
824
- supportedNetworks: applePayConfig.supported_networks,
825
- merchantCapabilities: applePayConfig.merchant_capabilities,
826
- total: {
827
- label: applePayConfig.display_name,
828
- amount: (config.session.amount / 100).toFixed(2),
829
- type: "final"
830
- }
831
- });
821
+ let session;
822
+ try {
823
+ session = new window.ApplePaySession(version, {
824
+ countryCode: applePayConfig.country_code,
825
+ currencyCode: config.session.currency_code,
826
+ supportedNetworks: applePayConfig.supported_networks,
827
+ merchantCapabilities: applePayConfig.merchant_capabilities,
828
+ total: {
829
+ label: applePayConfig.display_name,
830
+ amount: (config.session.amount / 100).toFixed(2),
831
+ type: "final"
832
+ }
833
+ });
834
+ } catch {
835
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
836
+ return;
837
+ }
832
838
  processing = true;
833
839
  let abortedDueToError = false;
834
840
  session.onvalidatemerchant = async (event) => {
@@ -871,7 +877,13 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
871
877
  }
872
878
  callbacks.onCancel();
873
879
  };
874
- session.begin();
880
+ try {
881
+ session.begin();
882
+ } catch {
883
+ processing = false;
884
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
885
+ return;
886
+ }
875
887
  styleApplePayModalWhenReady();
876
888
  };
877
889
  button.addEventListener("click", handleClick);
@@ -962,13 +974,15 @@ var detectGooglePay = async (config) => {
962
974
  };
963
975
  var GPAY_WRAP_CLASS = "flintn-gpay";
964
976
  var GPAY_STYLE_ID = "flintn-gpay-style";
977
+ var gpaySurfaceSelectors = [".gpay-button", ".gpay-card-info-container"].flatMap((s) => [s, `${s}:hover`, `${s}:active`, `${s}.hover`, `${s}.active`]).map((s) => `.${GPAY_WRAP_CLASS} ${s}`).join(",");
965
978
  var injectGpayStyle = () => {
966
979
  if (document.getElementById(GPAY_STYLE_ID)) return;
967
980
  const style = document.createElement("style");
968
981
  style.id = GPAY_STYLE_ID;
969
- style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
982
+ style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${GPAY_WRAP_CLASS}:hover{opacity:.8;}.${GPAY_WRAP_CLASS}:active{opacity:.7;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}${gpaySurfaceSelectors}{border:none!important;outline:none!important;box-shadow:none!important;background-color:#000!important;}.${GPAY_WRAP_CLASS} .gpay-card-info-container:focus-visible,.${GPAY_WRAP_CLASS} .gpay-button:focus-visible{outline:2px solid #0071e3!important;outline-offset:2px;}.${GPAY_WRAP_CLASS} .gpay-card-info-iframe{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button-fill,.${GPAY_WRAP_CLASS} .gpay-card-info-animation-container-fade-out{opacity:1!important;}.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::before,.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::after{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button.gpay-button-size-match-height{height:50%!important;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
970
983
  document.head.appendChild(style);
971
984
  };
985
+ var SHEET_SETTLE_GRACE_MS = 3e3;
972
986
  var mountGooglePayButton = (container, client, config, borderRadius, callbacks) => {
973
987
  let processing = false;
974
988
  const handleClick = () => {
@@ -977,7 +991,31 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
977
991
  if (!callbacks.canStart()) return;
978
992
  processing = true;
979
993
  callbacks.onStart();
994
+ let settled = false;
995
+ let graceTimer = null;
996
+ const disarmWatchdog = () => {
997
+ document.removeEventListener("visibilitychange", onVisibleAgain);
998
+ if (graceTimer !== null) clearTimeout(graceTimer);
999
+ };
1000
+ const trySettle = () => {
1001
+ if (settled) return false;
1002
+ settled = true;
1003
+ disarmWatchdog();
1004
+ return true;
1005
+ };
1006
+ const onVisibleAgain = () => {
1007
+ if (document.visibilityState !== "visible" || graceTimer !== null) {
1008
+ return;
1009
+ }
1010
+ graceTimer = setTimeout(() => {
1011
+ if (!trySettle()) return;
1012
+ processing = false;
1013
+ callbacks.onCancel();
1014
+ }, SHEET_SETTLE_GRACE_MS);
1015
+ };
1016
+ document.addEventListener("visibilitychange", onVisibleAgain);
980
1017
  client.loadPaymentData(request2).then((paymentData) => {
1018
+ if (!trySettle()) return;
981
1019
  const complete = (_success, errorMessage) => {
982
1020
  processing = false;
983
1021
  if (errorMessage) callbacks.onSdkError(errorMessage);
@@ -987,6 +1025,7 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
987
1025
  callbacks.handlers(complete)
988
1026
  );
989
1027
  }).catch((err) => {
1028
+ if (!trySettle()) return;
990
1029
  processing = false;
991
1030
  if (err?.statusCode === "CANCELED") callbacks.onCancel();
992
1031
  else callbacks.onSdkError("Google Pay payment failed");
@@ -1462,6 +1501,9 @@ var openThreeDsDialog = (options) => {
1462
1501
  overlay.id = OVERLAY_ID;
1463
1502
  overlay.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center;";
1464
1503
  const panel = document.createElement("div");
1504
+ panel.setAttribute("role", "dialog");
1505
+ panel.setAttribute("aria-modal", "true");
1506
+ panel.setAttribute("aria-label", "3DS verification");
1465
1507
  panel.style.cssText = `position: relative; background: #fff; border-radius: 8px; width: min(${PANEL_WIDTH}px, calc(100vw - 32px)); height: min(${PANEL_HEIGHT}px, calc(100vh - 32px)); box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25); overflow: hidden;`;
1466
1508
  const closeButton = document.createElement("button");
1467
1509
  closeButton.type = "button";
@@ -1475,13 +1517,28 @@ var openThreeDsDialog = (options) => {
1475
1517
  iframe.style.cssText = "width: 100%; height: 100%; border: none; display: block;";
1476
1518
  panel.append(iframe, closeButton);
1477
1519
  overlay.appendChild(panel);
1520
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1521
+ const trapFocus = (event) => {
1522
+ if (event.key !== "Tab") return;
1523
+ const active = document.activeElement;
1524
+ const inside = active !== null && overlay.contains(active);
1525
+ if (event.shiftKey && (!inside || active === iframe)) {
1526
+ event.preventDefault();
1527
+ closeButton.focus();
1528
+ } else if (!event.shiftKey && (!inside || active === closeButton)) {
1529
+ event.preventDefault();
1530
+ iframe.focus();
1531
+ }
1532
+ };
1478
1533
  let finished = false;
1479
1534
  const finish = (status) => {
1480
1535
  if (finished) return;
1481
1536
  finished = true;
1482
1537
  window.removeEventListener("message", handleMessage);
1538
+ window.removeEventListener("keydown", trapFocus);
1483
1539
  overlay.remove();
1484
1540
  closeCurrent = null;
1541
+ previouslyFocused?.focus();
1485
1542
  options.onFinish(status);
1486
1543
  };
1487
1544
  const handleMessage = (event) => {
@@ -1494,7 +1551,9 @@ var openThreeDsDialog = (options) => {
1494
1551
  };
1495
1552
  closeButton.addEventListener("click", () => finish("CANCELLED"));
1496
1553
  window.addEventListener("message", handleMessage);
1554
+ window.addEventListener("keydown", trapFocus);
1497
1555
  document.body.appendChild(overlay);
1556
+ closeButton.focus();
1498
1557
  closeCurrent = () => finish("CANCELLED");
1499
1558
  };
1500
1559
 
@@ -1607,7 +1666,15 @@ function createFlintNFields(options) {
1607
1666
  const pending = pendingSubmit;
1608
1667
  pending.timer = setTimeout(() => {
1609
1668
  if (pendingSubmit === pending) {
1610
- pendingSubmit.reject(new Error("3DS challenge timed out"));
1669
+ const timeoutResult = {
1670
+ status: "PAYMENT_ERROR",
1671
+ error: {
1672
+ code: "THREE_DS_TIMEOUT",
1673
+ message: "3DS challenge timed out"
1674
+ }
1675
+ };
1676
+ pendingSubmit.resolve(timeoutResult);
1677
+ options.onPayment?.(timeoutResult);
1611
1678
  pendingSubmit = null;
1612
1679
  }
1613
1680
  }, THREE_DS_TIMEOUT);
package/dist/index.mjs CHANGED
@@ -745,7 +745,7 @@ var injectAppleButtonStyle = () => {
745
745
  if (document.getElementById(APPLE_STYLE_ID)) return;
746
746
  const style = document.createElement("style");
747
747
  style.id = APPLE_STYLE_ID;
748
- style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out,transform .1s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.9;}.${APPLE_BUTTON_CLASS}:active{opacity:.8;transform:scale(.99);}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
748
+ style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.8;}.${APPLE_BUTTON_CLASS}:active{opacity:.7;}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
749
749
  document.head.appendChild(style);
750
750
  };
751
751
  var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
@@ -770,17 +770,23 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
770
770
  if (processing || !applePayConfig || version === null) return;
771
771
  if (!callbacks.canStart()) return;
772
772
  callbacks.onStart();
773
- const session = new window.ApplePaySession(version, {
774
- countryCode: applePayConfig.country_code,
775
- currencyCode: config.session.currency_code,
776
- supportedNetworks: applePayConfig.supported_networks,
777
- merchantCapabilities: applePayConfig.merchant_capabilities,
778
- total: {
779
- label: applePayConfig.display_name,
780
- amount: (config.session.amount / 100).toFixed(2),
781
- type: "final"
782
- }
783
- });
773
+ let session;
774
+ try {
775
+ session = new window.ApplePaySession(version, {
776
+ countryCode: applePayConfig.country_code,
777
+ currencyCode: config.session.currency_code,
778
+ supportedNetworks: applePayConfig.supported_networks,
779
+ merchantCapabilities: applePayConfig.merchant_capabilities,
780
+ total: {
781
+ label: applePayConfig.display_name,
782
+ amount: (config.session.amount / 100).toFixed(2),
783
+ type: "final"
784
+ }
785
+ });
786
+ } catch {
787
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
788
+ return;
789
+ }
784
790
  processing = true;
785
791
  let abortedDueToError = false;
786
792
  session.onvalidatemerchant = async (event) => {
@@ -823,7 +829,13 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
823
829
  }
824
830
  callbacks.onCancel();
825
831
  };
826
- session.begin();
832
+ try {
833
+ session.begin();
834
+ } catch {
835
+ processing = false;
836
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
837
+ return;
838
+ }
827
839
  styleApplePayModalWhenReady();
828
840
  };
829
841
  button.addEventListener("click", handleClick);
@@ -914,13 +926,15 @@ var detectGooglePay = async (config) => {
914
926
  };
915
927
  var GPAY_WRAP_CLASS = "flintn-gpay";
916
928
  var GPAY_STYLE_ID = "flintn-gpay-style";
929
+ var gpaySurfaceSelectors = [".gpay-button", ".gpay-card-info-container"].flatMap((s) => [s, `${s}:hover`, `${s}:active`, `${s}.hover`, `${s}.active`]).map((s) => `.${GPAY_WRAP_CLASS} ${s}`).join(",");
917
930
  var injectGpayStyle = () => {
918
931
  if (document.getElementById(GPAY_STYLE_ID)) return;
919
932
  const style = document.createElement("style");
920
933
  style.id = GPAY_STYLE_ID;
921
- style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
934
+ style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${GPAY_WRAP_CLASS}:hover{opacity:.8;}.${GPAY_WRAP_CLASS}:active{opacity:.7;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}${gpaySurfaceSelectors}{border:none!important;outline:none!important;box-shadow:none!important;background-color:#000!important;}.${GPAY_WRAP_CLASS} .gpay-card-info-container:focus-visible,.${GPAY_WRAP_CLASS} .gpay-button:focus-visible{outline:2px solid #0071e3!important;outline-offset:2px;}.${GPAY_WRAP_CLASS} .gpay-card-info-iframe{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button-fill,.${GPAY_WRAP_CLASS} .gpay-card-info-animation-container-fade-out{opacity:1!important;}.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::before,.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::after{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button.gpay-button-size-match-height{height:50%!important;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
922
935
  document.head.appendChild(style);
923
936
  };
937
+ var SHEET_SETTLE_GRACE_MS = 3e3;
924
938
  var mountGooglePayButton = (container, client, config, borderRadius, callbacks) => {
925
939
  let processing = false;
926
940
  const handleClick = () => {
@@ -929,7 +943,31 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
929
943
  if (!callbacks.canStart()) return;
930
944
  processing = true;
931
945
  callbacks.onStart();
946
+ let settled = false;
947
+ let graceTimer = null;
948
+ const disarmWatchdog = () => {
949
+ document.removeEventListener("visibilitychange", onVisibleAgain);
950
+ if (graceTimer !== null) clearTimeout(graceTimer);
951
+ };
952
+ const trySettle = () => {
953
+ if (settled) return false;
954
+ settled = true;
955
+ disarmWatchdog();
956
+ return true;
957
+ };
958
+ const onVisibleAgain = () => {
959
+ if (document.visibilityState !== "visible" || graceTimer !== null) {
960
+ return;
961
+ }
962
+ graceTimer = setTimeout(() => {
963
+ if (!trySettle()) return;
964
+ processing = false;
965
+ callbacks.onCancel();
966
+ }, SHEET_SETTLE_GRACE_MS);
967
+ };
968
+ document.addEventListener("visibilitychange", onVisibleAgain);
932
969
  client.loadPaymentData(request2).then((paymentData) => {
970
+ if (!trySettle()) return;
933
971
  const complete = (_success, errorMessage) => {
934
972
  processing = false;
935
973
  if (errorMessage) callbacks.onSdkError(errorMessage);
@@ -939,6 +977,7 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
939
977
  callbacks.handlers(complete)
940
978
  );
941
979
  }).catch((err) => {
980
+ if (!trySettle()) return;
942
981
  processing = false;
943
982
  if (err?.statusCode === "CANCELED") callbacks.onCancel();
944
983
  else callbacks.onSdkError("Google Pay payment failed");
@@ -1414,6 +1453,9 @@ var openThreeDsDialog = (options) => {
1414
1453
  overlay.id = OVERLAY_ID;
1415
1454
  overlay.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center;";
1416
1455
  const panel = document.createElement("div");
1456
+ panel.setAttribute("role", "dialog");
1457
+ panel.setAttribute("aria-modal", "true");
1458
+ panel.setAttribute("aria-label", "3DS verification");
1417
1459
  panel.style.cssText = `position: relative; background: #fff; border-radius: 8px; width: min(${PANEL_WIDTH}px, calc(100vw - 32px)); height: min(${PANEL_HEIGHT}px, calc(100vh - 32px)); box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25); overflow: hidden;`;
1418
1460
  const closeButton = document.createElement("button");
1419
1461
  closeButton.type = "button";
@@ -1427,13 +1469,28 @@ var openThreeDsDialog = (options) => {
1427
1469
  iframe.style.cssText = "width: 100%; height: 100%; border: none; display: block;";
1428
1470
  panel.append(iframe, closeButton);
1429
1471
  overlay.appendChild(panel);
1472
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1473
+ const trapFocus = (event) => {
1474
+ if (event.key !== "Tab") return;
1475
+ const active = document.activeElement;
1476
+ const inside = active !== null && overlay.contains(active);
1477
+ if (event.shiftKey && (!inside || active === iframe)) {
1478
+ event.preventDefault();
1479
+ closeButton.focus();
1480
+ } else if (!event.shiftKey && (!inside || active === closeButton)) {
1481
+ event.preventDefault();
1482
+ iframe.focus();
1483
+ }
1484
+ };
1430
1485
  let finished = false;
1431
1486
  const finish = (status) => {
1432
1487
  if (finished) return;
1433
1488
  finished = true;
1434
1489
  window.removeEventListener("message", handleMessage);
1490
+ window.removeEventListener("keydown", trapFocus);
1435
1491
  overlay.remove();
1436
1492
  closeCurrent = null;
1493
+ previouslyFocused?.focus();
1437
1494
  options.onFinish(status);
1438
1495
  };
1439
1496
  const handleMessage = (event) => {
@@ -1446,7 +1503,9 @@ var openThreeDsDialog = (options) => {
1446
1503
  };
1447
1504
  closeButton.addEventListener("click", () => finish("CANCELLED"));
1448
1505
  window.addEventListener("message", handleMessage);
1506
+ window.addEventListener("keydown", trapFocus);
1449
1507
  document.body.appendChild(overlay);
1508
+ closeButton.focus();
1450
1509
  closeCurrent = () => finish("CANCELLED");
1451
1510
  };
1452
1511
 
@@ -1559,7 +1618,15 @@ function createFlintNFields(options) {
1559
1618
  const pending = pendingSubmit;
1560
1619
  pending.timer = setTimeout(() => {
1561
1620
  if (pendingSubmit === pending) {
1562
- pendingSubmit.reject(new Error("3DS challenge timed out"));
1621
+ const timeoutResult = {
1622
+ status: "PAYMENT_ERROR",
1623
+ error: {
1624
+ code: "THREE_DS_TIMEOUT",
1625
+ message: "3DS challenge timed out"
1626
+ }
1627
+ };
1628
+ pendingSubmit.resolve(timeoutResult);
1629
+ options.onPayment?.(timeoutResult);
1563
1630
  pendingSubmit = null;
1564
1631
  }
1565
1632
  }, THREE_DS_TIMEOUT);
package/dist/react.js CHANGED
@@ -810,7 +810,7 @@ var injectAppleButtonStyle = () => {
810
810
  if (document.getElementById(APPLE_STYLE_ID)) return;
811
811
  const style = document.createElement("style");
812
812
  style.id = APPLE_STYLE_ID;
813
- style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out,transform .1s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.9;}.${APPLE_BUTTON_CLASS}:active{opacity:.8;transform:scale(.99);}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
813
+ style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.8;}.${APPLE_BUTTON_CLASS}:active{opacity:.7;}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
814
814
  document.head.appendChild(style);
815
815
  };
816
816
  var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
@@ -835,17 +835,23 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
835
835
  if (processing || !applePayConfig || version === null) return;
836
836
  if (!callbacks.canStart()) return;
837
837
  callbacks.onStart();
838
- const session = new window.ApplePaySession(version, {
839
- countryCode: applePayConfig.country_code,
840
- currencyCode: config.session.currency_code,
841
- supportedNetworks: applePayConfig.supported_networks,
842
- merchantCapabilities: applePayConfig.merchant_capabilities,
843
- total: {
844
- label: applePayConfig.display_name,
845
- amount: (config.session.amount / 100).toFixed(2),
846
- type: "final"
847
- }
848
- });
838
+ let session;
839
+ try {
840
+ session = new window.ApplePaySession(version, {
841
+ countryCode: applePayConfig.country_code,
842
+ currencyCode: config.session.currency_code,
843
+ supportedNetworks: applePayConfig.supported_networks,
844
+ merchantCapabilities: applePayConfig.merchant_capabilities,
845
+ total: {
846
+ label: applePayConfig.display_name,
847
+ amount: (config.session.amount / 100).toFixed(2),
848
+ type: "final"
849
+ }
850
+ });
851
+ } catch {
852
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
853
+ return;
854
+ }
849
855
  processing = true;
850
856
  let abortedDueToError = false;
851
857
  session.onvalidatemerchant = async (event) => {
@@ -888,7 +894,13 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
888
894
  }
889
895
  callbacks.onCancel();
890
896
  };
891
- session.begin();
897
+ try {
898
+ session.begin();
899
+ } catch {
900
+ processing = false;
901
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
902
+ return;
903
+ }
892
904
  styleApplePayModalWhenReady();
893
905
  };
894
906
  button.addEventListener("click", handleClick);
@@ -979,13 +991,15 @@ var detectGooglePay = async (config) => {
979
991
  };
980
992
  var GPAY_WRAP_CLASS = "flintn-gpay";
981
993
  var GPAY_STYLE_ID = "flintn-gpay-style";
994
+ var gpaySurfaceSelectors = [".gpay-button", ".gpay-card-info-container"].flatMap((s) => [s, `${s}:hover`, `${s}:active`, `${s}.hover`, `${s}.active`]).map((s) => `.${GPAY_WRAP_CLASS} ${s}`).join(",");
982
995
  var injectGpayStyle = () => {
983
996
  if (document.getElementById(GPAY_STYLE_ID)) return;
984
997
  const style = document.createElement("style");
985
998
  style.id = GPAY_STYLE_ID;
986
- style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
999
+ style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${GPAY_WRAP_CLASS}:hover{opacity:.8;}.${GPAY_WRAP_CLASS}:active{opacity:.7;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}${gpaySurfaceSelectors}{border:none!important;outline:none!important;box-shadow:none!important;background-color:#000!important;}.${GPAY_WRAP_CLASS} .gpay-card-info-container:focus-visible,.${GPAY_WRAP_CLASS} .gpay-button:focus-visible{outline:2px solid #0071e3!important;outline-offset:2px;}.${GPAY_WRAP_CLASS} .gpay-card-info-iframe{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button-fill,.${GPAY_WRAP_CLASS} .gpay-card-info-animation-container-fade-out{opacity:1!important;}.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::before,.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::after{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button.gpay-button-size-match-height{height:50%!important;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
987
1000
  document.head.appendChild(style);
988
1001
  };
1002
+ var SHEET_SETTLE_GRACE_MS = 3e3;
989
1003
  var mountGooglePayButton = (container, client, config, borderRadius, callbacks) => {
990
1004
  let processing = false;
991
1005
  const handleClick = () => {
@@ -994,7 +1008,31 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
994
1008
  if (!callbacks.canStart()) return;
995
1009
  processing = true;
996
1010
  callbacks.onStart();
1011
+ let settled = false;
1012
+ let graceTimer = null;
1013
+ const disarmWatchdog = () => {
1014
+ document.removeEventListener("visibilitychange", onVisibleAgain);
1015
+ if (graceTimer !== null) clearTimeout(graceTimer);
1016
+ };
1017
+ const trySettle = () => {
1018
+ if (settled) return false;
1019
+ settled = true;
1020
+ disarmWatchdog();
1021
+ return true;
1022
+ };
1023
+ const onVisibleAgain = () => {
1024
+ if (document.visibilityState !== "visible" || graceTimer !== null) {
1025
+ return;
1026
+ }
1027
+ graceTimer = setTimeout(() => {
1028
+ if (!trySettle()) return;
1029
+ processing = false;
1030
+ callbacks.onCancel();
1031
+ }, SHEET_SETTLE_GRACE_MS);
1032
+ };
1033
+ document.addEventListener("visibilitychange", onVisibleAgain);
997
1034
  client.loadPaymentData(request2).then((paymentData) => {
1035
+ if (!trySettle()) return;
998
1036
  const complete = (_success, errorMessage) => {
999
1037
  processing = false;
1000
1038
  if (errorMessage) callbacks.onSdkError(errorMessage);
@@ -1004,6 +1042,7 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
1004
1042
  callbacks.handlers(complete)
1005
1043
  );
1006
1044
  }).catch((err) => {
1045
+ if (!trySettle()) return;
1007
1046
  processing = false;
1008
1047
  if (err?.statusCode === "CANCELED") callbacks.onCancel();
1009
1048
  else callbacks.onSdkError("Google Pay payment failed");
@@ -1479,6 +1518,9 @@ var openThreeDsDialog = (options) => {
1479
1518
  overlay.id = OVERLAY_ID;
1480
1519
  overlay.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center;";
1481
1520
  const panel = document.createElement("div");
1521
+ panel.setAttribute("role", "dialog");
1522
+ panel.setAttribute("aria-modal", "true");
1523
+ panel.setAttribute("aria-label", "3DS verification");
1482
1524
  panel.style.cssText = `position: relative; background: #fff; border-radius: 8px; width: min(${PANEL_WIDTH}px, calc(100vw - 32px)); height: min(${PANEL_HEIGHT}px, calc(100vh - 32px)); box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25); overflow: hidden;`;
1483
1525
  const closeButton = document.createElement("button");
1484
1526
  closeButton.type = "button";
@@ -1492,13 +1534,28 @@ var openThreeDsDialog = (options) => {
1492
1534
  iframe.style.cssText = "width: 100%; height: 100%; border: none; display: block;";
1493
1535
  panel.append(iframe, closeButton);
1494
1536
  overlay.appendChild(panel);
1537
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1538
+ const trapFocus = (event) => {
1539
+ if (event.key !== "Tab") return;
1540
+ const active = document.activeElement;
1541
+ const inside = active !== null && overlay.contains(active);
1542
+ if (event.shiftKey && (!inside || active === iframe)) {
1543
+ event.preventDefault();
1544
+ closeButton.focus();
1545
+ } else if (!event.shiftKey && (!inside || active === closeButton)) {
1546
+ event.preventDefault();
1547
+ iframe.focus();
1548
+ }
1549
+ };
1495
1550
  let finished = false;
1496
1551
  const finish = (status) => {
1497
1552
  if (finished) return;
1498
1553
  finished = true;
1499
1554
  window.removeEventListener("message", handleMessage);
1555
+ window.removeEventListener("keydown", trapFocus);
1500
1556
  overlay.remove();
1501
1557
  closeCurrent = null;
1558
+ previouslyFocused?.focus();
1502
1559
  options.onFinish(status);
1503
1560
  };
1504
1561
  const handleMessage = (event) => {
@@ -1511,7 +1568,9 @@ var openThreeDsDialog = (options) => {
1511
1568
  };
1512
1569
  closeButton.addEventListener("click", () => finish("CANCELLED"));
1513
1570
  window.addEventListener("message", handleMessage);
1571
+ window.addEventListener("keydown", trapFocus);
1514
1572
  document.body.appendChild(overlay);
1573
+ closeButton.focus();
1515
1574
  closeCurrent = () => finish("CANCELLED");
1516
1575
  };
1517
1576
 
@@ -1624,7 +1683,15 @@ function createFlintNFields(options) {
1624
1683
  const pending = pendingSubmit;
1625
1684
  pending.timer = setTimeout(() => {
1626
1685
  if (pendingSubmit === pending) {
1627
- pendingSubmit.reject(new Error("3DS challenge timed out"));
1686
+ const timeoutResult = {
1687
+ status: "PAYMENT_ERROR",
1688
+ error: {
1689
+ code: "THREE_DS_TIMEOUT",
1690
+ message: "3DS challenge timed out"
1691
+ }
1692
+ };
1693
+ pendingSubmit.resolve(timeoutResult);
1694
+ options.onPayment?.(timeoutResult);
1628
1695
  pendingSubmit = null;
1629
1696
  }
1630
1697
  }, THREE_DS_TIMEOUT);
@@ -1867,7 +1934,7 @@ var DEFAULT_CARD_OPTIONS = {
1867
1934
  placeholder: "1234 1234 1234 1234"
1868
1935
  };
1869
1936
  var DEFAULT_EXPIRY_OPTIONS = { placeholder: "MM/YY" };
1870
- var DEFAULT_CVV_OPTIONS = { placeholder: "CVC" };
1937
+ var DEFAULT_CVV_OPTIONS = { placeholder: "***" };
1871
1938
  function useFlintNFields(options) {
1872
1939
  const cardNumberRef = (0, import_react2.useRef)(null);
1873
1940
  const expiryRef = (0, import_react2.useRef)(null);
package/dist/react.mjs CHANGED
@@ -783,7 +783,7 @@ var injectAppleButtonStyle = () => {
783
783
  if (document.getElementById(APPLE_STYLE_ID)) return;
784
784
  const style = document.createElement("style");
785
785
  style.id = APPLE_STYLE_ID;
786
- style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out,transform .1s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.9;}.${APPLE_BUTTON_CLASS}:active{opacity:.8;transform:scale(.99);}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
786
+ style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.8;}.${APPLE_BUTTON_CLASS}:active{opacity:.7;}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
787
787
  document.head.appendChild(style);
788
788
  };
789
789
  var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
@@ -808,17 +808,23 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
808
808
  if (processing || !applePayConfig || version === null) return;
809
809
  if (!callbacks.canStart()) return;
810
810
  callbacks.onStart();
811
- const session = new window.ApplePaySession(version, {
812
- countryCode: applePayConfig.country_code,
813
- currencyCode: config.session.currency_code,
814
- supportedNetworks: applePayConfig.supported_networks,
815
- merchantCapabilities: applePayConfig.merchant_capabilities,
816
- total: {
817
- label: applePayConfig.display_name,
818
- amount: (config.session.amount / 100).toFixed(2),
819
- type: "final"
820
- }
821
- });
811
+ let session;
812
+ try {
813
+ session = new window.ApplePaySession(version, {
814
+ countryCode: applePayConfig.country_code,
815
+ currencyCode: config.session.currency_code,
816
+ supportedNetworks: applePayConfig.supported_networks,
817
+ merchantCapabilities: applePayConfig.merchant_capabilities,
818
+ total: {
819
+ label: applePayConfig.display_name,
820
+ amount: (config.session.amount / 100).toFixed(2),
821
+ type: "final"
822
+ }
823
+ });
824
+ } catch {
825
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
826
+ return;
827
+ }
822
828
  processing = true;
823
829
  let abortedDueToError = false;
824
830
  session.onvalidatemerchant = async (event) => {
@@ -861,7 +867,13 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
861
867
  }
862
868
  callbacks.onCancel();
863
869
  };
864
- session.begin();
870
+ try {
871
+ session.begin();
872
+ } catch {
873
+ processing = false;
874
+ callbacks.onSdkError("Apple Pay is not available in this browser.");
875
+ return;
876
+ }
865
877
  styleApplePayModalWhenReady();
866
878
  };
867
879
  button.addEventListener("click", handleClick);
@@ -952,13 +964,15 @@ var detectGooglePay = async (config) => {
952
964
  };
953
965
  var GPAY_WRAP_CLASS = "flintn-gpay";
954
966
  var GPAY_STYLE_ID = "flintn-gpay-style";
967
+ var gpaySurfaceSelectors = [".gpay-button", ".gpay-card-info-container"].flatMap((s) => [s, `${s}:hover`, `${s}:active`, `${s}.hover`, `${s}.active`]).map((s) => `.${GPAY_WRAP_CLASS} ${s}`).join(",");
955
968
  var injectGpayStyle = () => {
956
969
  if (document.getElementById(GPAY_STYLE_ID)) return;
957
970
  const style = document.createElement("style");
958
971
  style.id = GPAY_STYLE_ID;
959
- style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
972
+ style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${GPAY_WRAP_CLASS}:hover{opacity:.8;}.${GPAY_WRAP_CLASS}:active{opacity:.7;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}${gpaySurfaceSelectors}{border:none!important;outline:none!important;box-shadow:none!important;background-color:#000!important;}.${GPAY_WRAP_CLASS} .gpay-card-info-container:focus-visible,.${GPAY_WRAP_CLASS} .gpay-button:focus-visible{outline:2px solid #0071e3!important;outline-offset:2px;}.${GPAY_WRAP_CLASS} .gpay-card-info-iframe{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button-fill,.${GPAY_WRAP_CLASS} .gpay-card-info-animation-container-fade-out{opacity:1!important;}.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::before,.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::after{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button.gpay-button-size-match-height{height:50%!important;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
960
973
  document.head.appendChild(style);
961
974
  };
975
+ var SHEET_SETTLE_GRACE_MS = 3e3;
962
976
  var mountGooglePayButton = (container, client, config, borderRadius, callbacks) => {
963
977
  let processing = false;
964
978
  const handleClick = () => {
@@ -967,7 +981,31 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
967
981
  if (!callbacks.canStart()) return;
968
982
  processing = true;
969
983
  callbacks.onStart();
984
+ let settled = false;
985
+ let graceTimer = null;
986
+ const disarmWatchdog = () => {
987
+ document.removeEventListener("visibilitychange", onVisibleAgain);
988
+ if (graceTimer !== null) clearTimeout(graceTimer);
989
+ };
990
+ const trySettle = () => {
991
+ if (settled) return false;
992
+ settled = true;
993
+ disarmWatchdog();
994
+ return true;
995
+ };
996
+ const onVisibleAgain = () => {
997
+ if (document.visibilityState !== "visible" || graceTimer !== null) {
998
+ return;
999
+ }
1000
+ graceTimer = setTimeout(() => {
1001
+ if (!trySettle()) return;
1002
+ processing = false;
1003
+ callbacks.onCancel();
1004
+ }, SHEET_SETTLE_GRACE_MS);
1005
+ };
1006
+ document.addEventListener("visibilitychange", onVisibleAgain);
970
1007
  client.loadPaymentData(request2).then((paymentData) => {
1008
+ if (!trySettle()) return;
971
1009
  const complete = (_success, errorMessage) => {
972
1010
  processing = false;
973
1011
  if (errorMessage) callbacks.onSdkError(errorMessage);
@@ -977,6 +1015,7 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
977
1015
  callbacks.handlers(complete)
978
1016
  );
979
1017
  }).catch((err) => {
1018
+ if (!trySettle()) return;
980
1019
  processing = false;
981
1020
  if (err?.statusCode === "CANCELED") callbacks.onCancel();
982
1021
  else callbacks.onSdkError("Google Pay payment failed");
@@ -1452,6 +1491,9 @@ var openThreeDsDialog = (options) => {
1452
1491
  overlay.id = OVERLAY_ID;
1453
1492
  overlay.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center;";
1454
1493
  const panel = document.createElement("div");
1494
+ panel.setAttribute("role", "dialog");
1495
+ panel.setAttribute("aria-modal", "true");
1496
+ panel.setAttribute("aria-label", "3DS verification");
1455
1497
  panel.style.cssText = `position: relative; background: #fff; border-radius: 8px; width: min(${PANEL_WIDTH}px, calc(100vw - 32px)); height: min(${PANEL_HEIGHT}px, calc(100vh - 32px)); box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25); overflow: hidden;`;
1456
1498
  const closeButton = document.createElement("button");
1457
1499
  closeButton.type = "button";
@@ -1465,13 +1507,28 @@ var openThreeDsDialog = (options) => {
1465
1507
  iframe.style.cssText = "width: 100%; height: 100%; border: none; display: block;";
1466
1508
  panel.append(iframe, closeButton);
1467
1509
  overlay.appendChild(panel);
1510
+ const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1511
+ const trapFocus = (event) => {
1512
+ if (event.key !== "Tab") return;
1513
+ const active = document.activeElement;
1514
+ const inside = active !== null && overlay.contains(active);
1515
+ if (event.shiftKey && (!inside || active === iframe)) {
1516
+ event.preventDefault();
1517
+ closeButton.focus();
1518
+ } else if (!event.shiftKey && (!inside || active === closeButton)) {
1519
+ event.preventDefault();
1520
+ iframe.focus();
1521
+ }
1522
+ };
1468
1523
  let finished = false;
1469
1524
  const finish = (status) => {
1470
1525
  if (finished) return;
1471
1526
  finished = true;
1472
1527
  window.removeEventListener("message", handleMessage);
1528
+ window.removeEventListener("keydown", trapFocus);
1473
1529
  overlay.remove();
1474
1530
  closeCurrent = null;
1531
+ previouslyFocused?.focus();
1475
1532
  options.onFinish(status);
1476
1533
  };
1477
1534
  const handleMessage = (event) => {
@@ -1484,7 +1541,9 @@ var openThreeDsDialog = (options) => {
1484
1541
  };
1485
1542
  closeButton.addEventListener("click", () => finish("CANCELLED"));
1486
1543
  window.addEventListener("message", handleMessage);
1544
+ window.addEventListener("keydown", trapFocus);
1487
1545
  document.body.appendChild(overlay);
1546
+ closeButton.focus();
1488
1547
  closeCurrent = () => finish("CANCELLED");
1489
1548
  };
1490
1549
 
@@ -1597,7 +1656,15 @@ function createFlintNFields(options) {
1597
1656
  const pending = pendingSubmit;
1598
1657
  pending.timer = setTimeout(() => {
1599
1658
  if (pendingSubmit === pending) {
1600
- pendingSubmit.reject(new Error("3DS challenge timed out"));
1659
+ const timeoutResult = {
1660
+ status: "PAYMENT_ERROR",
1661
+ error: {
1662
+ code: "THREE_DS_TIMEOUT",
1663
+ message: "3DS challenge timed out"
1664
+ }
1665
+ };
1666
+ pendingSubmit.resolve(timeoutResult);
1667
+ options.onPayment?.(timeoutResult);
1601
1668
  pendingSubmit = null;
1602
1669
  }
1603
1670
  }, THREE_DS_TIMEOUT);
@@ -1840,7 +1907,7 @@ var DEFAULT_CARD_OPTIONS = {
1840
1907
  placeholder: "1234 1234 1234 1234"
1841
1908
  };
1842
1909
  var DEFAULT_EXPIRY_OPTIONS = { placeholder: "MM/YY" };
1843
- var DEFAULT_CVV_OPTIONS = { placeholder: "CVC" };
1910
+ var DEFAULT_CVV_OPTIONS = { placeholder: "***" };
1844
1911
  function useFlintNFields(options) {
1845
1912
  const cardNumberRef = useRef2(null);
1846
1913
  const expiryRef = useRef2(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flintn-checkout",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "FlintN Payment SDK — drop-in iframe checkout for card payments and wallets with localization and theming.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",