@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.cjs +324 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +219 -3
- package/dist/index.d.ts +219 -3
- package/dist/index.js +325 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
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
|
|
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
|
-
|
|
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
|
};
|