@usethrottle/checkout-react 1.4.1 → 2.0.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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { CSSProperties, RefObject } from 'react';
2
+ import { CSSProperties, RefObject, ReactNode } from 'react';
3
3
  import { CartSessionCart, AddLineItemInput, UpdateLineItemInput, SelectShippingInput, CheckoutHandoffInput, CheckoutHandoff, SelectedShipping } from '@usethrottle/cart';
4
4
 
5
5
  /**
@@ -84,7 +84,12 @@ type ThrottleParentMessage = ThrottleParentCommand & ThrottleParentEnvelope;
84
84
  */
85
85
  interface RecurringIntent {
86
86
  plan: string;
87
- interval: 'weekly' | 'biweekly' | 'monthly' | 'quarterly' | 'yearly';
87
+ /** A named interval. Or send intervalUnit + intervalCount for any cadence from 1 hour to 1 year. */
88
+ interval?: 'weekly' | 'biweekly' | 'monthly' | 'quarterly' | 'yearly' | 'custom';
89
+ intervalUnit?: 'hour' | 'day' | 'week' | 'month';
90
+ intervalCount?: number;
91
+ /** Makes the subscription an installment plan of exactly this many payments (2–60). Not combinable with trialDays. */
92
+ totalPayments?: number;
88
93
  amount?: number;
89
94
  trialDays?: number;
90
95
  planName?: string;
@@ -361,4 +366,215 @@ interface UseThrottleCheckoutResult {
361
366
  */
362
367
  declare function useThrottleCheckout(options: UseThrottleCheckoutOptions): UseThrottleCheckoutResult;
363
368
 
364
- export { CheckoutEmbed, type CheckoutEmbedExtraProps, type CheckoutTotals, PaymentEmbed, type RecurringIntent, CheckoutEmbed as ThrottleCheckout, type ThrottleCheckoutStatus, type ThrottleCompletedLineItem, type ThrottleEmbedProps, type ThrottleEnvelope, type ThrottleEvent, type ThrottleMessage, type ThrottleParentCommand, type ThrottleParentEnvelope, type ThrottleParentMessage, type UseCartSessionOptions, type UseCartSessionResult, type UseThrottleCheckoutOptions, type UseThrottleCheckoutResult, useCartSession, useThrottleCheckout, useThrottleEvents };
369
+ /**
370
+ * Subset of the Secure Fields instance this module needs — real or mocked.
371
+ *
372
+ * Declared structurally rather than as `Pick<SecureFields, …>`: any reference
373
+ * to the SDK's own type from an exported declaration makes tsup emit
374
+ * `import { SecureFields } from '@gr4vy/secure-fields'` into
375
+ * `dist/index.d.ts`, and that package's `exports` map carries no `types`
376
+ * condition — so a consumer on `moduleResolution: bundler` gets TS7016 under
377
+ * `skipLibCheck: false`, or silently degrades the member to `any` under
378
+ * `skipLibCheck: true` (the Next default). The `paths` override in this
379
+ * package's tsconfig repairs our own compile only; it does not ship.
380
+ *
381
+ * Members use method syntax deliberately: TypeScript checks method parameters
382
+ * bivariantly, so the real class's narrower `CombinedEvents` event parameter
383
+ * stays assignable to the `string` declared here.
384
+ */
385
+ interface ThreeDSecureInstance {
386
+ addThreeDSecure(element: string | HTMLElement, options?: {
387
+ challengeWindowSize?: '01' | '02' | '03' | '04' | '05';
388
+ }): void;
389
+ addEventListener(event: string, callback: (...args: any[]) => void): void;
390
+ removeEventListener?(event: string, callback: (...args: any[]) => void): void;
391
+ }
392
+ interface ResolveChallengeArgs {
393
+ /** The `SecureFields` instance already constructed for this session. */
394
+ instance: ThreeDSecureInstance;
395
+ /** Mount point for Gr4vy's own 3DS challenge iframe. */
396
+ element: HTMLElement | string;
397
+ challengeWindowSize?: '01' | '02' | '03' | '04' | '05';
398
+ }
399
+
400
+ /**
401
+ * Lifecycle status for the headless Secure Fields state machine.
402
+ * - `idle` — provider mounted, fields not yet registered
403
+ * - `mounting` — Secure Fields instance constructed, fields registering
404
+ * - `ready` — all required fields registered, submit() may be called
405
+ * - `vaulting` — submit() called, waiting on CARD_VAULT_SUCCESS/FAILURE
406
+ * - `authorizing` — card vaulted, awaiting the merchant's complete() call
407
+ * - `challenging` — complete() returned `requires_action`; Gr4vy's own 3DS
408
+ * iframe (addThreeDSecure) is mounted and running
409
+ * - `complete` — order created
410
+ * - `error` — vault, challenge, or completion failed (see `error`)
411
+ */
412
+ type SecureCardStatus = 'idle' | 'mounting' | 'ready' | 'vaulting' | 'authorizing' | 'challenging' | 'complete' | 'error';
413
+ /** The Task 2 `POST /checkout-sessions/:id/card-session` response shape. */
414
+ interface SecureCardSession {
415
+ sessionId: string;
416
+ /**
417
+ * The payment provider instance this session belongs to, straight from
418
+ * `POST /checkout-sessions/:id/card-session`.
419
+ *
420
+ * Either name works. `providerId` is the documented one; `gr4vyId` is the
421
+ * original and is still accepted, because the package shipped with it and
422
+ * dropping it would break consumers already passing the response through.
423
+ */
424
+ providerId?: string;
425
+ /** @deprecated Use `providerId`. Still honoured. */
426
+ gr4vyId?: string;
427
+ environment: 'sandbox' | 'production';
428
+ }
429
+ interface SecureCardError {
430
+ code: string;
431
+ message: string;
432
+ }
433
+ type SecureCardFieldKind = 'number' | 'expiry' | 'cvv' | 'postalCode';
434
+ /**
435
+ * Everything this package uses off a `SecureFields` instance, declared
436
+ * structurally instead of re-exporting the SDK's own class type.
437
+ *
438
+ * `@gr4vy/secure-fields`'s `exports` map has no `types` condition, so naming
439
+ * its type in an exported declaration makes tsup emit
440
+ * `import { SecureFields } from '@gr4vy/secure-fields'` into
441
+ * `dist/index.d.ts` — which is a hard TS7016 for any consumer on
442
+ * `moduleResolution: bundler` with `skipLibCheck: false`, and a silent
443
+ * degradation of `instance` to `any` with `skipLibCheck: true` (the Next
444
+ * default). That hits every installer of this package, not only Secure Fields
445
+ * users. The `paths` override in this package's tsconfig repairs our own
446
+ * compile; it is monorepo-only and does not ship. Same approach as
447
+ * `ThreeDSecureInstance` in `threeDSecure.ts`.
448
+ *
449
+ * Members use method syntax deliberately: TypeScript checks method parameters
450
+ * bivariantly, so the real class's narrower option and event types stay
451
+ * assignable to the looser ones declared here.
452
+ */
453
+ interface SecureFieldsInstance {
454
+ /** Gr4vy's hidden controller iframe, removed when the provider unmounts. */
455
+ controller?: HTMLIFrameElement;
456
+ addCardNumberField(element: string | HTMLElement, options?: Record<string, unknown>): unknown;
457
+ addExpiryDateField(element: string | HTMLElement, options?: Record<string, unknown>): unknown;
458
+ addSecurityCodeField(element: string | HTMLElement, options?: Record<string, unknown>): unknown;
459
+ addField(element: string | HTMLElement, options: Record<string, unknown>): unknown;
460
+ addThreeDSecure(element: string | HTMLElement, options?: {
461
+ challengeWindowSize?: '01' | '02' | '03' | '04' | '05';
462
+ }): void;
463
+ addEventListener(event: string, callback: (...args: any[]) => void): void;
464
+ removeEventListener?(event: string, callback: (...args: any[]) => void): void;
465
+ submit(): void;
466
+ }
467
+ interface SecureCardContextValue {
468
+ /**
469
+ * The underlying Secure Fields instance — Task 5's field primitives register
470
+ * against it.
471
+ *
472
+ * `null` until the provider's mount effect has constructed it. It cannot be
473
+ * built during render: `new SecureFields()` mutates the document (see the
474
+ * effect in `SecureCardProvider`), and React does not promise a render-phase
475
+ * factory runs exactly once. Consumers that reach for it directly must
476
+ * null-check; the field primitives already wait for it.
477
+ */
478
+ instance: SecureFieldsInstance | null;
479
+ registerField: (kind: SecureCardFieldKind, element: HTMLElement, options?: Record<string, unknown>) => void;
480
+ /**
481
+ * Mount point for Gr4vy's own 3DS challenge iframe (`addThreeDSecure`).
482
+ * Register this once, alongside the card fields; it is only actually
483
+ * mounted if `complete()` gets `requires_action`.
484
+ */
485
+ registerThreeDSecure: (element: HTMLElement | string, options?: {
486
+ challengeWindowSize?: '01' | '02' | '03' | '04' | '05';
487
+ }) => void;
488
+ /**
489
+ * Abandons an in-progress challenge (e.g. the buyer closed the challenge
490
+ * UI). Only meaningful while `status === 'challenging'`; settles
491
+ * `complete()`'s promise with `error.code === 'three_ds_abandoned'`, kept
492
+ * distinct from a processor decline code.
493
+ */
494
+ cancelChallenge: () => void;
495
+ status: SecureCardStatus;
496
+ error: SecureCardError | null;
497
+ /** True once all required fields are registered and submit() may be called. */
498
+ ready: boolean;
499
+ /** vault() then complete(). */
500
+ pay: () => Promise<unknown>;
501
+ vault: () => Promise<void>;
502
+ complete: () => Promise<unknown>;
503
+ }
504
+ interface SecureCardProviderProps {
505
+ session: SecureCardSession;
506
+ /**
507
+ * Throttle's own checkout session id — the `:id` already used to mint
508
+ * `session` via `POST /checkout-sessions/:id/card-session`. NOT the same
509
+ * value as `session.sessionId`: that field is Gr4vy's own checkout session
510
+ * id (required to construct the `SecureFields` instance — see
511
+ * `packages/platform-api/src/routes/card-session.ts`, which returns
512
+ * `{ sessionId: checkoutSessionId, ... }` from `adapter.createCardSession()`,
513
+ * a Gr4vy-minted id). The public `POST /checkout-sessions/:id/complete`
514
+ * route looks `:id` up directly against Throttle's own `checkoutSessions`
515
+ * table (`checkout-sessions.ts`), so `defaultComplete` needs this id, not
516
+ * `session.sessionId`, to call the right URL.
517
+ */
518
+ checkoutSessionId: string;
519
+ children: ReactNode;
520
+ /** Injected for tests / custom completion flows; defaults to a real POST /complete call. */
521
+ completeFn?: (args: {
522
+ sessionId: string;
523
+ gr4vySessionId: string;
524
+ }) => Promise<any>;
525
+ /** Injected for tests / custom 3DS UI; defaults to mounting Gr4vy's own 3DS iframe via addThreeDSecure. */
526
+ resolveChallengeFn?: (args: ResolveChallengeArgs) => Promise<void>;
527
+ /**
528
+ * Bounds how long `complete()` waits on an unfinished challenge before
529
+ * settling to a terminal `error` (code `three_ds_timeout`) on its own — the
530
+ * only other exits are Gr4vy's own `THREE_DS_FINISH` and an explicit
531
+ * `cancelChallenge()` call, neither of which is guaranteed to ever fire if
532
+ * the buyer just abandons the tab. Not sourced from Gr4vy documentation
533
+ * (no challenge-duration limit is published in the SDK); 5 minutes is a
534
+ * conservative, overridable default.
535
+ */
536
+ challengeTimeoutMs?: number;
537
+ /**
538
+ * Base URL of the Throttle API (e.g. `https://api.usethrottle.dev`).
539
+ * Required — not optional-with-a-same-origin-default. Secure Fields is the
540
+ * headless surface for merchants who mount the card form on THEIR OWN
541
+ * origin, which by definition is not same-origin with the Throttle API, so
542
+ * `defaultComplete`'s `fetch()` can never resolve a relative path (it 404s
543
+ * against the merchant's own domain at payment time instead). Making this
544
+ * required turns that into a compile error instead of a silent runtime
545
+ * failure. Joined onto the request path regardless of a trailing slash.
546
+ */
547
+ apiBaseUrl: string;
548
+ onSuccess?: (result: SecureCardCompleteResult) => void;
549
+ }
550
+ /**
551
+ * The real `POST /checkout-sessions/:id/complete` success body for the
552
+ * Secure Fields flow (`checkout-session-service.ts` `complete()`,
553
+ * proxy-mode-embedded return, ~line 2338 and its idempotent-retry twin at
554
+ * ~line 1849). There is no `order` key.
555
+ */
556
+ interface SecureCardCompleteResult {
557
+ orderId: string;
558
+ orderNumber: string;
559
+ paymentId: string;
560
+ /** Minor units (cents). */
561
+ total: number;
562
+ currency: string;
563
+ status: string;
564
+ paymentStatus: string;
565
+ }
566
+ declare function SecureCardProvider({ session, checkoutSessionId, children, completeFn, resolveChallengeFn, challengeTimeoutMs, apiBaseUrl, onSuccess, }: SecureCardProviderProps): react_jsx_runtime.JSX.Element;
567
+ declare function useSecureCard(): SecureCardContextValue;
568
+
569
+ interface SecureCardFieldProps {
570
+ className?: string;
571
+ style?: CSSProperties;
572
+ /** Gr4vy field options (styles, autoFocus, maskInput, pattern…) passed through unchanged. */
573
+ options?: Record<string, unknown>;
574
+ }
575
+ declare function CardNumberField({ className, style, options }: SecureCardFieldProps): react_jsx_runtime.JSX.Element;
576
+ declare function ExpiryField({ className, style, options }: SecureCardFieldProps): react_jsx_runtime.JSX.Element;
577
+ declare function SecurityCodeField({ className, style, options }: SecureCardFieldProps): react_jsx_runtime.JSX.Element;
578
+ declare function PostalCodeField({ className, style, options }: SecureCardFieldProps): react_jsx_runtime.JSX.Element;
579
+
580
+ export { CardNumberField, CheckoutEmbed, type CheckoutEmbedExtraProps, type CheckoutTotals, ExpiryField, PaymentEmbed, PostalCodeField, type RecurringIntent, type ResolveChallengeArgs, type SecureCardCompleteResult, type SecureCardContextValue, type SecureCardError, type SecureCardFieldKind, type SecureCardFieldProps, SecureCardProvider, type SecureCardProviderProps, type SecureCardSession, type SecureCardStatus, type SecureFieldsInstance, SecurityCodeField, type ThreeDSecureInstance, CheckoutEmbed as ThrottleCheckout, type ThrottleCheckoutStatus, type ThrottleCompletedLineItem, type ThrottleEmbedProps, type ThrottleEnvelope, type ThrottleEvent, type ThrottleMessage, type ThrottleParentCommand, type ThrottleParentEnvelope, type ThrottleParentMessage, type UseCartSessionOptions, type UseCartSessionResult, type UseThrottleCheckoutOptions, type UseThrottleCheckoutResult, useCartSession, useSecureCard, useThrottleCheckout, useThrottleEvents };
package/dist/index.js CHANGED
@@ -638,11 +638,336 @@ function useThrottleCheckout(options) {
638
638
  reset
639
639
  };
640
640
  }
641
+
642
+ // src/useSecureCard.tsx
643
+ import {
644
+ createContext,
645
+ useCallback as useCallback5,
646
+ useContext,
647
+ useEffect as useEffect5,
648
+ useRef as useRef4,
649
+ useState as useState6
650
+ } from "react";
651
+ import { SecureFields as SecureFields2 } from "@gr4vy/secure-fields";
652
+
653
+ // src/threeDSecure.ts
654
+ import { SecureFields } from "@gr4vy/secure-fields";
655
+ function resolveChallenge({ instance, element, challengeWindowSize }) {
656
+ return new Promise((resolve) => {
657
+ const handleFinish = () => {
658
+ instance.removeEventListener?.(SecureFields.Events.THREE_DS_FINISH, handleFinish);
659
+ resolve();
660
+ };
661
+ instance.addEventListener(SecureFields.Events.THREE_DS_FINISH, handleFinish);
662
+ instance.addThreeDSecure(element, challengeWindowSize ? { challengeWindowSize } : void 0);
663
+ });
664
+ }
665
+
666
+ // src/useSecureCard.tsx
667
+ import { jsx as jsx3 } from "react/jsx-runtime";
668
+ var SecureCardContext = createContext(null);
669
+ var FIELDS_REQUIRED_TO_BE_READY = 3;
670
+ var DEFAULT_CHALLENGE_TIMEOUT_MS = 5 * 60 * 1e3;
671
+ function SecureCardProvider({
672
+ session,
673
+ checkoutSessionId,
674
+ children,
675
+ completeFn,
676
+ resolveChallengeFn,
677
+ challengeTimeoutMs = DEFAULT_CHALLENGE_TIMEOUT_MS,
678
+ apiBaseUrl,
679
+ onSuccess
680
+ }) {
681
+ const [status, setStatus] = useState6("idle");
682
+ const [error, setError] = useState6(null);
683
+ const [mountedCount, setMountedCount] = useState6(0);
684
+ const vaultWaiter = useRef4(null);
685
+ const threeDSElement = useRef4(null);
686
+ const threeDSOptions = useRef4(void 0);
687
+ const challengeWaiter = useRef4(null);
688
+ const { environment, sessionId } = session;
689
+ const providerInstanceId = session.providerId ?? session.gr4vyId ?? "";
690
+ const [instance, setInstance] = useState6(null);
691
+ const [threeDSMount, setThreeDSMount] = useState6(null);
692
+ useEffect5(() => {
693
+ const created = new SecureFields2({ gr4vyId: providerInstanceId, environment, sessionId });
694
+ setInstance(created);
695
+ setMountedCount(0);
696
+ setStatus((current) => current === "idle" || current === "ready" ? "mounting" : current);
697
+ return () => {
698
+ created.controller?.remove();
699
+ setInstance(null);
700
+ };
701
+ }, [providerInstanceId, environment, sessionId]);
702
+ useEffect5(() => {
703
+ if (!instance) return;
704
+ const handleSuccess = () => {
705
+ vaultWaiter.current?.resolve();
706
+ };
707
+ const handleFailure = (data) => {
708
+ vaultWaiter.current?.reject({
709
+ code: data?.code ?? "card_vault_failed",
710
+ message: data?.message ?? "The card could not be saved."
711
+ });
712
+ };
713
+ const handleThreeDSStart = () => setStatus("challenging");
714
+ const handleThreeDSFinish = () => {
715
+ if (!challengeWaiter.current) return;
716
+ setStatus((current) => current === "challenging" ? "vaulting" : current);
717
+ };
718
+ instance.addEventListener(SecureFields2.Events.CARD_VAULT_SUCCESS, handleSuccess);
719
+ instance.addEventListener(SecureFields2.Events.CARD_VAULT_FAILURE, handleFailure);
720
+ instance.addEventListener(SecureFields2.Events.THREE_DS_START, handleThreeDSStart);
721
+ instance.addEventListener(SecureFields2.Events.THREE_DS_FINISH, handleThreeDSFinish);
722
+ return () => {
723
+ instance.removeEventListener?.(SecureFields2.Events.CARD_VAULT_SUCCESS, handleSuccess);
724
+ instance.removeEventListener?.(SecureFields2.Events.CARD_VAULT_FAILURE, handleFailure);
725
+ instance.removeEventListener?.(SecureFields2.Events.THREE_DS_START, handleThreeDSStart);
726
+ instance.removeEventListener?.(SecureFields2.Events.THREE_DS_FINISH, handleThreeDSFinish);
727
+ };
728
+ }, [instance]);
729
+ const registerField = useCallback5(
730
+ (kind, element, options = {}) => {
731
+ if (!instance) return;
732
+ if (kind === "number") instance.addCardNumberField(element, options);
733
+ else if (kind === "expiry") instance.addExpiryDateField(element, options);
734
+ else if (kind === "cvv") instance.addSecurityCodeField(element, options);
735
+ else instance.addField(element, { type: "postalCode", ...options });
736
+ setMountedCount((n) => n + 1);
737
+ },
738
+ [instance]
739
+ );
740
+ useEffect5(() => {
741
+ if (mountedCount >= FIELDS_REQUIRED_TO_BE_READY) setStatus("ready");
742
+ }, [mountedCount]);
743
+ const registerThreeDSecure = useCallback5(
744
+ (element, options) => {
745
+ threeDSElement.current = element;
746
+ threeDSOptions.current = options;
747
+ setThreeDSMount(element);
748
+ },
749
+ []
750
+ );
751
+ useEffect5(() => {
752
+ if (!instance || !threeDSMount) return;
753
+ try {
754
+ instance.addThreeDSecure(
755
+ threeDSMount,
756
+ threeDSOptions.current?.challengeWindowSize ? { challengeWindowSize: threeDSOptions.current.challengeWindowSize } : void 0
757
+ );
758
+ } catch (err) {
759
+ setError({
760
+ code: "three_ds_arm_failed",
761
+ message: err?.message ?? "Could not arm 3-D Secure."
762
+ });
763
+ }
764
+ }, [instance, threeDSMount]);
765
+ const cancelChallenge = useCallback5(() => {
766
+ challengeWaiter.current?.reject({
767
+ code: "three_ds_abandoned",
768
+ message: "The 3-D Secure challenge was abandoned."
769
+ });
770
+ }, []);
771
+ const vault = useCallback5(() => {
772
+ if (!instance) {
773
+ return Promise.reject({
774
+ code: "not_ready",
775
+ message: "The card fields are not ready yet."
776
+ });
777
+ }
778
+ if (vaultWaiter.current) {
779
+ return Promise.reject({
780
+ code: "vault_in_progress",
781
+ message: "A card vault is already in progress."
782
+ });
783
+ }
784
+ setError(null);
785
+ setStatus("vaulting");
786
+ return new Promise((resolve, reject) => {
787
+ vaultWaiter.current = {
788
+ resolve: () => {
789
+ vaultWaiter.current = null;
790
+ resolve();
791
+ },
792
+ reject: (e) => {
793
+ vaultWaiter.current = null;
794
+ setError(e);
795
+ setStatus("error");
796
+ reject(e);
797
+ }
798
+ };
799
+ instance.submit();
800
+ });
801
+ }, [instance]);
802
+ const complete = useCallback5(async () => {
803
+ setStatus("authorizing");
804
+ const doComplete = completeFn ?? ((args) => defaultComplete({ ...args, apiBaseUrl }));
805
+ const doResolveChallenge = resolveChallengeFn ?? resolveChallenge;
806
+ try {
807
+ let res = await doComplete({ sessionId: checkoutSessionId, gr4vySessionId: session.sessionId });
808
+ if (res?.status === "requires_action") {
809
+ if (!threeDSElement.current) {
810
+ throw Object.assign(new Error("No 3-D Secure mount element registered (call registerThreeDSecure)."), {
811
+ code: "three_ds_element_missing"
812
+ });
813
+ }
814
+ if (!instance) {
815
+ throw Object.assign(new Error("The card fields are not ready yet."), { code: "not_ready" });
816
+ }
817
+ setStatus("challenging");
818
+ await new Promise((resolve, reject) => {
819
+ const timeoutId = setTimeout(() => {
820
+ challengeWaiter.current?.reject({
821
+ code: "three_ds_timeout",
822
+ message: "The 3-D Secure challenge timed out."
823
+ });
824
+ }, challengeTimeoutMs);
825
+ challengeWaiter.current = {
826
+ resolve: () => {
827
+ clearTimeout(timeoutId);
828
+ challengeWaiter.current = null;
829
+ resolve();
830
+ },
831
+ reject: (e) => {
832
+ clearTimeout(timeoutId);
833
+ challengeWaiter.current = null;
834
+ reject(Object.assign(new Error(e.message), { code: e.code }));
835
+ }
836
+ };
837
+ doResolveChallenge({
838
+ instance,
839
+ element: threeDSElement.current,
840
+ ...threeDSOptions.current ?? {}
841
+ }).then(
842
+ () => challengeWaiter.current?.resolve(),
843
+ // `resolveChallenge` calls `instance.addThreeDSecure()` synchronously
844
+ // inside its Promise executor, so an SDK throw rejects here; a
845
+ // merchant-supplied `resolveChallengeFn` can reject too. Without this
846
+ // arm the only exit was `challengeTimeoutMs` — five minutes stuck at
847
+ // `status: 'challenging'`, plus an unhandled rejection. Same
848
+ // code-passthrough convention as `complete()`'s catch: keep a `code`
849
+ // the resolver supplied, otherwise report `three_ds_failed`.
850
+ (err) => challengeWaiter.current?.reject({
851
+ code: err?.code ?? "three_ds_failed",
852
+ message: err?.message ?? "The 3-D Secure challenge could not be completed."
853
+ })
854
+ );
855
+ });
856
+ setStatus("authorizing");
857
+ res = await doComplete({ sessionId: checkoutSessionId, gr4vySessionId: session.sessionId });
858
+ if (res?.status === "requires_action") {
859
+ throw Object.assign(new Error("The payment still requires authentication after the 3-D Secure challenge."), {
860
+ code: "three_ds_unresolved"
861
+ });
862
+ }
863
+ }
864
+ setStatus("complete");
865
+ onSuccess?.(res);
866
+ return res;
867
+ } catch (err) {
868
+ setError({
869
+ code: err?.code ?? "complete_failed",
870
+ message: err?.message ?? "Payment could not be completed."
871
+ });
872
+ setStatus("error");
873
+ throw err;
874
+ }
875
+ }, [completeFn, resolveChallengeFn, challengeTimeoutMs, apiBaseUrl, instance, checkoutSessionId, session.sessionId, onSuccess]);
876
+ const pay = useCallback5(async () => {
877
+ await vault();
878
+ return complete();
879
+ }, [vault, complete]);
880
+ const value = {
881
+ instance,
882
+ registerField,
883
+ registerThreeDSecure,
884
+ cancelChallenge,
885
+ status,
886
+ error,
887
+ // "The fields are mounted and nothing is in flight" — NOT `status ===
888
+ // 'ready'`. Deriving it from the status alone made a decline terminal: the
889
+ // only thing that ever sets `ready` is the mount-count effect, which does
890
+ // not re-run after `setStatus('error')`, so `ready` stayed false and the
891
+ // merchant's pay button stayed disabled. A declined buyer could not try
892
+ // another card without reloading the page — and a decline is the single
893
+ // most common failure at a checkout.
894
+ ready: mountedCount >= FIELDS_REQUIRED_TO_BE_READY && status !== "vaulting" && status !== "authorizing" && status !== "challenging" && status !== "complete",
895
+ pay,
896
+ vault,
897
+ complete
898
+ };
899
+ return /* @__PURE__ */ jsx3(SecureCardContext.Provider, { value, children });
900
+ }
901
+ async function defaultComplete({
902
+ sessionId,
903
+ gr4vySessionId,
904
+ apiBaseUrl
905
+ }) {
906
+ const res = await fetch(joinApiUrl(apiBaseUrl, `/api/v1/checkout-sessions/${sessionId}/complete`), {
907
+ method: "POST",
908
+ headers: { "Content-Type": "application/json" },
909
+ body: JSON.stringify({ paymentMethod: "embedded", processorCheckoutSessionId: gr4vySessionId })
910
+ });
911
+ const body = await res.json();
912
+ if (!res.ok) {
913
+ throw Object.assign(new Error(body?.error?.message ?? "Payment could not be completed."), {
914
+ code: body?.error?.code ?? "payment_failed"
915
+ });
916
+ }
917
+ return body?.data ?? body;
918
+ }
919
+ function joinApiUrl(base, path) {
920
+ return `${base.replace(/\/+$/, "")}${path}`;
921
+ }
922
+ function useSecureCard() {
923
+ const ctx = useContext(SecureCardContext);
924
+ if (!ctx) throw new Error("useSecureCard must be used inside <SecureCardProvider>");
925
+ return ctx;
926
+ }
927
+
928
+ // src/SecureCardFields.tsx
929
+ import { useContext as useContext2, useEffect as useEffect6, useRef as useRef5 } from "react";
930
+ import { jsx as jsx4 } from "react/jsx-runtime";
931
+ function useField(kind, options) {
932
+ const ctx = useContext2(SecureCardContext);
933
+ const hostRef = useRef5(null);
934
+ const instance = ctx?.instance ?? null;
935
+ useEffect6(() => {
936
+ const host = hostRef.current;
937
+ if (!host || !ctx || !instance) return;
938
+ const target = document.createElement("div");
939
+ target.style.height = "100%";
940
+ host.appendChild(target);
941
+ ctx.registerField(kind, target, options ?? {});
942
+ return () => {
943
+ while (host.firstChild) host.removeChild(host.firstChild);
944
+ };
945
+ }, [instance]);
946
+ return hostRef;
947
+ }
948
+ function CardNumberField({ className, style, options }) {
949
+ return /* @__PURE__ */ jsx4("div", { ref: useField("number", options), className, style });
950
+ }
951
+ function ExpiryField({ className, style, options }) {
952
+ return /* @__PURE__ */ jsx4("div", { ref: useField("expiry", options), className, style });
953
+ }
954
+ function SecurityCodeField({ className, style, options }) {
955
+ return /* @__PURE__ */ jsx4("div", { ref: useField("cvv", options), className, style });
956
+ }
957
+ function PostalCodeField({ className, style, options }) {
958
+ return /* @__PURE__ */ jsx4("div", { ref: useField("postalCode", options), className, style });
959
+ }
641
960
  export {
961
+ CardNumberField,
642
962
  CheckoutEmbed,
963
+ ExpiryField,
643
964
  PaymentEmbed,
965
+ PostalCodeField,
966
+ SecureCardProvider,
967
+ SecurityCodeField,
644
968
  CheckoutEmbed as ThrottleCheckout,
645
969
  useCartSession,
970
+ useSecureCard,
646
971
  useThrottleCheckout,
647
972
  useThrottleEvents
648
973
  };