@12-apps/payments-frontend 1.7.0 → 1.8.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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The factory's shared runtime (FUT-741): the fetched store protocol, and the
3
+ * providers that stop every nested screen re-fetching it.
4
+ *
5
+ * Two layers, on purpose:
6
+ *
7
+ * - {@link FlowsShell} supplies the things a screen cannot work WITHOUT —
8
+ * the design-system slots, the bound transport, the navigate port. Every
9
+ * returned screen wraps itself in one, which is what makes "every
10
+ * `screens.*` member already works standalone" true rather than
11
+ * aspirational. Re-supplying them under a `Provider` is a no-op.
12
+ * - {@link FlowsProvider} adds the one thing that is worth NOT repeating: the
13
+ * fetched `/config`. A screen under it reads that answer; a screen standing
14
+ * alone fetches its own.
15
+ */
16
+ import {
17
+ createContext,
18
+ useContext,
19
+ useEffect,
20
+ useMemo,
21
+ useState,
22
+ type JSX,
23
+ type ReactNode,
24
+ } from "react";
25
+
26
+ import { CheckoutClientProvider } from "../components/checkout/client-context";
27
+ import { CheckoutNavigateProvider } from "../components/checkout/navigate-context";
28
+ import type { CheckoutClient } from "../components/checkout/transport";
29
+ import type { CheckoutProviderConfig } from "../components/checkout/types";
30
+ import { CheckoutComponentsProvider } from "../components/checkout/ui";
31
+
32
+ import type { CheckoutCopyFE } from "./copy";
33
+ import type { CheckoutAvailability, CheckoutConfigState, PaymentFlowsConfig } from "./types";
34
+
35
+ /** Supplied by a `Provider`; `null` means "nobody above me fetched it". */
36
+ const FetchedConfigContext = createContext<CheckoutConfigState | null>(null);
37
+
38
+ /** Everything one factory hands the screens it built. */
39
+ export interface FlowsRuntime {
40
+ config: PaymentFlowsConfig;
41
+ client: CheckoutClient;
42
+ copy: CheckoutCopyFE;
43
+ navigate: (url: string) => void;
44
+ /** The store slug for THIS render — read from the host's hook, never frozen. */
45
+ useTenantSlug(): string | undefined;
46
+ /** The host's availability vote for THIS render. Defaults to "payable". */
47
+ useAvailability(): CheckoutAvailability;
48
+ }
49
+
50
+ /**
51
+ * Fetch the store protocol for one slug.
52
+ *
53
+ * A failure is NOT retried into a loop and NOT surfaced as an error state: the
54
+ * config read fails OPEN for the UI (`null` renders both method tiles) and the
55
+ * server still fails the charge CLOSED. Nothing about a blip here may hide a
56
+ * working checkout, and nothing about it may invent permission to mock.
57
+ */
58
+ function useFetchedConfig(
59
+ runtime: FlowsRuntime,
60
+ enabled: boolean,
61
+ ): CheckoutConfigState {
62
+ const { client } = runtime;
63
+ const onWarning = runtime.config.onWarning;
64
+ const tenantSlug = runtime.useTenantSlug();
65
+ const [state, setState] = useState<CheckoutConfigState>({ config: null, pending: enabled });
66
+
67
+ useEffect(() => {
68
+ if (!enabled || !tenantSlug) {
69
+ setState({ config: null, pending: false });
70
+ return undefined;
71
+ }
72
+ let active = true;
73
+ setState({ config: null, pending: true });
74
+ void client.getConfig(tenantSlug).then((result) => {
75
+ if (!active) return;
76
+ if (!result.ok) {
77
+ // Reported, never logged: a `console.error` in a buyer's browser tells
78
+ // nobody. Silent by default is the honest alternative.
79
+ onWarning?.("checkout config read failed", { tenantSlug, error: result.error });
80
+ setState({ config: null, pending: false });
81
+ return;
82
+ }
83
+ setState({ config: result.data, pending: false });
84
+ });
85
+ return () => {
86
+ active = false;
87
+ };
88
+ }, [client, tenantSlug, enabled, onWarning]);
89
+
90
+ return state;
91
+ }
92
+
93
+ /**
94
+ * The store protocol, from the nearest `Provider` or fetched here.
95
+ *
96
+ * Both branches always run their hooks; the self-fetch is merely disabled when
97
+ * somebody above already has the answer, which keeps the rule of hooks honest.
98
+ */
99
+ export function useResolvedConfig(runtime: FlowsRuntime): CheckoutConfigState {
100
+ const provided = useContext(FetchedConfigContext);
101
+ const own = useFetchedConfig(runtime, provided === null);
102
+ return provided ?? own;
103
+ }
104
+
105
+ /** Slots, transport and the navigate port — what a screen cannot work without. */
106
+ export function FlowsShell({
107
+ runtime,
108
+ children,
109
+ }: {
110
+ runtime: FlowsRuntime;
111
+ children: ReactNode;
112
+ }): JSX.Element {
113
+ return (
114
+ <CheckoutComponentsProvider components={runtime.config.components}>
115
+ <CheckoutClientProvider client={runtime.client}>
116
+ <CheckoutNavigateProvider navigate={runtime.navigate}>{children}</CheckoutNavigateProvider>
117
+ </CheckoutClientProvider>
118
+ </CheckoutComponentsProvider>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * The shell plus the fetched `/config`, supplied once for a host that nests
124
+ * individual screens in its own layout.
125
+ *
126
+ * A host that passes its own `config` skips the fetch entirely — that is the
127
+ * seam a story, a harness page or a server-rendered host uses to STATE the
128
+ * protocol rather than round-trip for it.
129
+ */
130
+ export function FlowsProvider({
131
+ runtime,
132
+ config,
133
+ children,
134
+ }: {
135
+ runtime: FlowsRuntime;
136
+ config?: CheckoutProviderConfig | null;
137
+ children: ReactNode;
138
+ }): JSX.Element {
139
+ const fetched = useFetchedConfig(runtime, config === undefined);
140
+ const value = useMemo<CheckoutConfigState>(
141
+ () => (config === undefined ? fetched : { config, pending: false }),
142
+ [config, fetched],
143
+ );
144
+ return (
145
+ <FlowsShell runtime={runtime}>
146
+ <FetchedConfigContext.Provider value={value}>{children}</FetchedConfigContext.Provider>
147
+ </FlowsShell>
148
+ );
149
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The buyer-facing screens the factory binds (FUT-741), part one: choosing a
3
+ * method, stating who is paying, reusing a card, and the empty cart.
4
+ *
5
+ * Every one of them is a THIN binding. The behaviour still lives in the
6
+ * components this package already shipped; what is new is that the config,
7
+ * the transport, the scope and the slots arrive from the factory instead of
8
+ * from six props the host had to thread itself.
9
+ */
10
+ import { Box } from "@mui/material";
11
+ import { useEffect, useMemo, useState, type JSX } from "react";
12
+
13
+ import { SavedCardsPicker, type SavedCard } from "../card";
14
+ import { buyerFieldsFor } from "../components/checkout/buyer-fields";
15
+ import { BuyerInfoForm } from "../components/checkout/buyer-info-form";
16
+ import { EmptyCart as EmptyCartView } from "../components/checkout/checkout-steps";
17
+ import {
18
+ cardPathAvailable,
19
+ offeredMethods,
20
+ selectableMethods,
21
+ usePreselectSoleMethod,
22
+ } from "../components/checkout/method-capability";
23
+ import { MethodPicker } from "../components/checkout/method-picker";
24
+ import { PayerSummary as PayerSummaryView } from "../components/checkout/payer-summary";
25
+ import type { BuyerInfo, PaymentMethod } from "../components/checkout/types";
26
+ import { useCheckoutComponents } from "../components/checkout/ui";
27
+
28
+ import { FlowsShell, useResolvedConfig, type FlowsRuntime } from "./runtime";
29
+ import type { BuyerDetailsProps, CheckoutScreens } from "./types";
30
+
31
+ function buildMethodChoice(runtime: FlowsRuntime): CheckoutScreens["MethodChoice"] {
32
+ function MethodChoiceBody({
33
+ value,
34
+ onChange,
35
+ }: {
36
+ value: PaymentMethod | null;
37
+ onChange: (method: PaymentMethod) => void;
38
+ }): JSX.Element {
39
+ const { config } = useResolvedConfig(runtime);
40
+ const offered = offeredMethods(config);
41
+ const cardUnavailable = !cardPathAvailable(config);
42
+ usePreselectSoleMethod(selectableMethods(offered, cardUnavailable), value, onChange);
43
+ return (
44
+ <MethodPicker
45
+ value={value}
46
+ onChange={onChange}
47
+ cardUnavailable={cardUnavailable}
48
+ offered={offered}
49
+ />
50
+ );
51
+ }
52
+ return function MethodChoice(props) {
53
+ return (
54
+ <FlowsShell runtime={runtime}>
55
+ <MethodChoiceBody {...props} />
56
+ </FlowsShell>
57
+ );
58
+ };
59
+ }
60
+
61
+ function buildBuyerDetails(runtime: FlowsRuntime): CheckoutScreens["BuyerDetails"] {
62
+ function BuyerDetailsBody({
63
+ value,
64
+ onChange,
65
+ method,
66
+ onContinue,
67
+ error,
68
+ }: BuyerDetailsProps): JSX.Element {
69
+ const { Button, Text } = useCheckoutComponents();
70
+ const { config } = useResolvedConfig(runtime);
71
+ // ∩ method (FUT-595). An absent declaration degrades to CPF-required —
72
+ // never to "ask nothing", which is a 400 the buyer only meets after
73
+ // finishing the form.
74
+ const fields = useMemo(() => buyerFieldsFor(config?.chain, method), [config, method]);
75
+ return (
76
+ <Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
77
+ <BuyerInfoForm value={value} onChange={onChange} fields={fields} fieldError={error} />
78
+ <Box>
79
+ <Button
80
+ variant="solid"
81
+ color="primary"
82
+ size="lg"
83
+ fullWidth
84
+ onClick={onContinue}
85
+ dataTestId="checkout-continue"
86
+ >
87
+ {runtime.copy.continueAction}
88
+ </Button>
89
+ </Box>
90
+ <Text variant="caption" size="xs" color="secondary" as="p">
91
+ Pagamento seguro
92
+ </Text>
93
+ </Box>
94
+ );
95
+ }
96
+ return function BuyerDetails(props) {
97
+ return (
98
+ <FlowsShell runtime={runtime}>
99
+ <BuyerDetailsBody {...props} />
100
+ </FlowsShell>
101
+ );
102
+ };
103
+ }
104
+
105
+ function buildPayerSummary(runtime: FlowsRuntime): CheckoutScreens["PayerSummary"] {
106
+ return function PayerSummary({ buyer, onEdit }: { buyer: BuyerInfo; onEdit?: () => void }) {
107
+ return (
108
+ <FlowsShell runtime={runtime}>
109
+ <PayerSummaryView name={buyer.name} taxId={buyer.taxId} onEdit={onEdit} />
110
+ </FlowsShell>
111
+ );
112
+ };
113
+ }
114
+
115
+ /** The vaulted instruments this store can actually charge (FUT-697 scoping). */
116
+ function useScopedInstruments(runtime: FlowsRuntime): SavedCard[] {
117
+ const tenantSlug = runtime.useTenantSlug();
118
+ const [cards, setCards] = useState<SavedCard[]>([]);
119
+ useEffect(() => {
120
+ let active = true;
121
+ void runtime.client.listInstruments(tenantSlug).then((list) => {
122
+ if (active) setCards(list);
123
+ });
124
+ return () => {
125
+ active = false;
126
+ };
127
+ }, [runtime, tenantSlug]);
128
+ return cards;
129
+ }
130
+
131
+ function buildSavedCards(runtime: FlowsRuntime): CheckoutScreens["SavedCards"] {
132
+ function SavedCardsBody({
133
+ selection,
134
+ onSelect,
135
+ }: {
136
+ selection: string;
137
+ onSelect: (id: string) => void;
138
+ }): JSX.Element | null {
139
+ const savedCards = useScopedInstruments(runtime);
140
+ // Nothing to reuse renders nothing at all — an empty picker is a heading
141
+ // over a void, and the new-card form below it already says what to do.
142
+ if (savedCards.length === 0) return null;
143
+ return <SavedCardsPicker savedCards={savedCards} selection={selection} onSelect={onSelect} />;
144
+ }
145
+ return function SavedCards(props) {
146
+ return (
147
+ <FlowsShell runtime={runtime}>
148
+ <SavedCardsBody {...props} />
149
+ </FlowsShell>
150
+ );
151
+ };
152
+ }
153
+
154
+ function buildEmptyCart(runtime: FlowsRuntime): CheckoutScreens["EmptyCart"] {
155
+ return function EmptyCart() {
156
+ return (
157
+ <FlowsShell runtime={runtime}>
158
+ <EmptyCartView onBack={runtime.config.ports.exitToCatalog} />
159
+ </FlowsShell>
160
+ );
161
+ };
162
+ }
163
+
164
+ export const buyerScreens = {
165
+ buildMethodChoice,
166
+ buildBuyerDetails,
167
+ buildPayerSummary,
168
+ buildSavedCards,
169
+ buildEmptyCart,
170
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The hosted handover, as two SCREENS (FUT-741) — the leg of checkout that had
3
+ * none.
4
+ *
5
+ * Today a redirect provider's link (FUT-556) and a 3-DS challenge (FUT-698) are
6
+ * both a bare `window.location.assign` buried in a hook. When the navigation
7
+ * works, nobody notices. When it does not — a popup/redirect blocker, a slow
8
+ * DNS, an in-app webview that refuses cross-origin navigations, a buyer who
9
+ * taps back — the buyer is left looking at a page that says nothing, offers
10
+ * nothing, and has already raised a charge.
11
+ *
12
+ * So: an interstitial that PARKS the order first, then navigates, and renders
13
+ * an explicit link as the fallback the assign never had. And the return leg as
14
+ * a screen of its own, so "rehydrate the parked order and poll it to a terminal
15
+ * state" is something a host can mount at its return route rather than
16
+ * something that only happens if the buyer lands back on the exact component
17
+ * that left.
18
+ *
19
+ * ORDERING IS LOAD-BEARING: park, then navigate. The navigation may tear this
20
+ * SPA down at any point after it starts, and a return trip that finds nothing
21
+ * parked drops the buyer on a blank confirmation after they have paid.
22
+ */
23
+ import { Box } from "@mui/material";
24
+ import { useEffect, useState, type JSX } from "react";
25
+
26
+ import { rememberHostedOrder, takeHostedOrder } from "../components/checkout/hosted-return";
27
+ import type { CheckoutOrder, OrderStatus } from "../components/checkout/types";
28
+ import { useCheckoutComponents } from "../components/checkout/ui";
29
+ import { usePaymentPolling } from "../components/checkout/use-payment-polling";
30
+
31
+ import { FlowsShell, type FlowsRuntime } from "./runtime";
32
+ import type { CheckoutScreens } from "./types";
33
+
34
+ /** Park the order, then navigate — exactly once, even under StrictMode. */
35
+ function useHandover(payable: CheckoutOrder, url: string, navigate: (url: string) => void): void {
36
+ const [done, setDone] = useState(false);
37
+ useEffect(() => {
38
+ if (done) return;
39
+ setDone(true);
40
+ rememberHostedOrder(payable);
41
+ navigate(url);
42
+ }, [done, payable, url, navigate]);
43
+ }
44
+
45
+ function buildHostedHandoff(runtime: FlowsRuntime): CheckoutScreens["HostedHandoff"] {
46
+ function HostedHandoffBody({
47
+ url,
48
+ payable,
49
+ onCancel,
50
+ }: {
51
+ url: string;
52
+ payable: CheckoutOrder;
53
+ onCancel?: () => void;
54
+ }): JSX.Element {
55
+ const { Button, LoadingState, Text } = useCheckoutComponents();
56
+ const copy = runtime.copy;
57
+ useHandover(payable, url, runtime.navigate);
58
+ return (
59
+ <Box
60
+ data-testid="checkout-hosted-handoff"
61
+ sx={{ display: "flex", flexDirection: "column", gap: 2, alignItems: "center", py: 4 }}
62
+ >
63
+ <Text variant="heading" size="md" weight="bold" as="h2">
64
+ {copy.handoffTitle}
65
+ </Text>
66
+ <Text variant="body" size="sm" color="secondary" as="p">
67
+ {copy.handoffBody}
68
+ </Text>
69
+ <LoadingState variant="spinner" size="md" message="" dataTestId="checkout-hosted-waiting" />
70
+ {/* A real anchor, not a second scripted navigation: when the scripted
71
+ one was blocked, another one will be too. This is the affordance a
72
+ bare `location.assign` never had. */}
73
+ <a href={url} data-testid="checkout-hosted-link" rel="noreferrer">
74
+ {copy.handoffLink}
75
+ </a>
76
+ {onCancel ? (
77
+ <Button
78
+ variant="text"
79
+ color="neutral"
80
+ size="sm"
81
+ onClick={onCancel}
82
+ dataTestId="checkout-hosted-cancel"
83
+ >
84
+ {copy.handoffCancel}
85
+ </Button>
86
+ ) : null}
87
+ </Box>
88
+ );
89
+ }
90
+ return function HostedHandoff(props) {
91
+ return (
92
+ <FlowsShell runtime={runtime}>
93
+ <HostedHandoffBody {...props} />
94
+ </FlowsShell>
95
+ );
96
+ };
97
+ }
98
+
99
+ function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn"] {
100
+ function HostedReturnBody({
101
+ onResolved,
102
+ }: {
103
+ onResolved: (status: OrderStatus) => void;
104
+ }): JSX.Element {
105
+ const { Alert, LoadingState } = useCheckoutComponents();
106
+ // Read-and-clear, once, on first render: the resumed view belongs to
107
+ // exactly one return trip.
108
+ const [parked] = useState(takeHostedOrder);
109
+ const { status } = usePaymentPolling(parked?.orderId ?? null, {
110
+ enabled: Boolean(parked),
111
+ intervalMs: runtime.config.polling?.intervalMs,
112
+ });
113
+
114
+ useEffect(() => {
115
+ if (status && status !== "AWAITING_PAYMENT") onResolved(status);
116
+ }, [status, onResolved]);
117
+
118
+ if (!parked) {
119
+ return (
120
+ <Alert
121
+ variant="info"
122
+ title={runtime.copy.returnPending}
123
+ description={runtime.copy.returnUnknown}
124
+ showIcon
125
+ data-testid="checkout-hosted-return-unknown"
126
+ />
127
+ );
128
+ }
129
+ return (
130
+ <Box data-testid="checkout-hosted-return" sx={{ py: 4 }}>
131
+ <LoadingState
132
+ variant="spinner"
133
+ size="md"
134
+ message={runtime.copy.returnPending}
135
+ dataTestId="checkout-hosted-return-waiting"
136
+ />
137
+ </Box>
138
+ );
139
+ }
140
+ return function HostedReturn(props) {
141
+ return (
142
+ <FlowsShell runtime={runtime}>
143
+ <HostedReturnBody {...props} />
144
+ </FlowsShell>
145
+ );
146
+ };
147
+ }
148
+
149
+ export const hostedScreens = { buildHostedHandoff, buildHostedReturn };
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The factory's payment screens (FUT-741), part two: taking the money, saying
3
+ * how it went, and saying plainly when the store cannot take it at all.
4
+ */
5
+ import { Box } from "@mui/material";
6
+ import type { JSX } from "react";
7
+
8
+ import { CardView } from "../components/checkout/card-view";
9
+ import {
10
+ cardChain,
11
+ cardTokenization,
12
+ } from "../components/checkout/method-capability";
13
+ import { PaymentStatus as PaymentStatusView } from "../components/checkout/payment-status";
14
+ import { PixView } from "../components/checkout/pix-view";
15
+ import type { CheckoutOrder, OrderStatus } from "../components/checkout/types";
16
+ import { useCheckoutComponents } from "../components/checkout/ui";
17
+
18
+ import { FlowsShell, useResolvedConfig, type FlowsRuntime } from "./runtime";
19
+ import type { CheckoutScreens } from "./types";
20
+
21
+ function buildCardEntry(runtime: FlowsRuntime): CheckoutScreens["CardEntry"] {
22
+ function CardEntryBody({
23
+ payable,
24
+ onResolved,
25
+ }: {
26
+ payable: CheckoutOrder;
27
+ onResolved: (status: OrderStatus) => void;
28
+ }): JSX.Element {
29
+ const { config } = useResolvedConfig(runtime);
30
+ return (
31
+ <CardView
32
+ order={payable}
33
+ providerConfig={cardTokenization(config)}
34
+ // The chain VERBATIM (FUT-563). Nothing here filters, sorts or
35
+ // de-duplicates it: `tokensByProvider` is sent iff the SERVER-published
36
+ // chain has more than one entry, so any tidying done at this layer
37
+ // silently disables failover for the store it exists for.
38
+ providerChain={cardChain(config)}
39
+ tenantSlug={runtime.useTenantSlug()}
40
+ onResolved={onResolved}
41
+ pollIntervalMs={runtime.config.polling?.intervalMs}
42
+ />
43
+ );
44
+ }
45
+ return function CardEntry(props) {
46
+ return (
47
+ <FlowsShell runtime={runtime}>
48
+ <CardEntryBody {...props} />
49
+ </FlowsShell>
50
+ );
51
+ };
52
+ }
53
+
54
+ function buildPixPayment(runtime: FlowsRuntime): CheckoutScreens["PixPayment"] {
55
+ return function PixPayment({
56
+ payable,
57
+ onResolved,
58
+ }: {
59
+ payable: CheckoutOrder;
60
+ onResolved: (status: OrderStatus) => void;
61
+ }) {
62
+ return (
63
+ <FlowsShell runtime={runtime}>
64
+ <PixView
65
+ order={payable}
66
+ onResolved={onResolved}
67
+ pollIntervalMs={runtime.config.polling?.intervalMs}
68
+ />
69
+ </FlowsShell>
70
+ );
71
+ };
72
+ }
73
+
74
+ function buildPaymentStatus(runtime: FlowsRuntime): CheckoutScreens["PaymentStatus"] {
75
+ return function PaymentStatus({
76
+ status,
77
+ payable,
78
+ }: {
79
+ status: OrderStatus | null;
80
+ payable?: CheckoutOrder | null;
81
+ }) {
82
+ return (
83
+ <FlowsShell runtime={runtime}>
84
+ <PaymentStatusView
85
+ status={status}
86
+ totalLabel={payable?.totalLabel ?? ""}
87
+ orderId={payable?.orderId}
88
+ onBackToMenu={runtime.config.ports.exitToCatalog}
89
+ paidExtra={runtime.config.confirmation?.extra}
90
+ />
91
+ </FlowsShell>
92
+ );
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Whether this store can take money at all.
98
+ *
99
+ * An OR, never a swap. `chain.length === 0` is the library's own fact — the
100
+ * server published no enabled provider. The host's veto is a DIFFERENT fact: a
101
+ * store with a perfectly good chain that has switched online payments off. Move
102
+ * the decision entirely to the chain and that store starts offering a checkout
103
+ * it will not honour; move it entirely to the host and a store that simply
104
+ * never connected a provider gets the payment step it cannot serve.
105
+ *
106
+ * While the config is still in flight, neither fact is known — so nothing is
107
+ * refused. The unavailable screen is a statement about the store, and stating
108
+ * it early would be a lie a spinner never tells.
109
+ */
110
+ export function storeCannotCharge(
111
+ config: { chain?: unknown[] } | null,
112
+ pending: boolean,
113
+ hostSaysPayable: boolean,
114
+ ): boolean {
115
+ if (pending) return false;
116
+ if (!hostSaysPayable) return true;
117
+ return config !== null && (config.chain?.length ?? 0) === 0;
118
+ }
119
+
120
+ function buildPaymentsUnavailable(
121
+ runtime: FlowsRuntime,
122
+ ): CheckoutScreens["PaymentsUnavailable"] {
123
+ function PaymentsUnavailableBody(): JSX.Element {
124
+ const { Alert, Button } = useCheckoutComponents();
125
+ const { remedy } = runtime.useAvailability();
126
+ const copy = runtime.copy;
127
+ if (!remedy) {
128
+ return (
129
+ <Box
130
+ sx={{ display: "flex", flexDirection: "column", gap: 2 }}
131
+ data-testid="checkout-payments-disabled"
132
+ >
133
+ <Alert
134
+ variant="info"
135
+ title={copy.unavailableTitle}
136
+ description={copy.unavailableBody}
137
+ showIcon
138
+ />
139
+ </Box>
140
+ );
141
+ }
142
+ return (
143
+ <Box
144
+ sx={{ display: "flex", flexDirection: "column", gap: 2 }}
145
+ data-testid="checkout-payments-remedy"
146
+ >
147
+ <Alert
148
+ variant="info"
149
+ title={copy.unavailableWithRemedyTitle}
150
+ description={copy.unavailableWithRemedyBody}
151
+ showIcon
152
+ />
153
+ <Button
154
+ variant="solid"
155
+ size="lg"
156
+ onClick={remedy.onSelect}
157
+ dataTestId="checkout-payments-remedy-action"
158
+ >
159
+ {remedy.label}
160
+ </Button>
161
+ </Box>
162
+ );
163
+ }
164
+ return function PaymentsUnavailable() {
165
+ return (
166
+ <FlowsShell runtime={runtime}>
167
+ <PaymentsUnavailableBody />
168
+ </FlowsShell>
169
+ );
170
+ };
171
+ }
172
+
173
+ export const payScreens = {
174
+ buildCardEntry,
175
+ buildPixPayment,
176
+ buildPaymentStatus,
177
+ buildPaymentsUnavailable,
178
+ };