@12-apps/payments-frontend 1.16.0 → 1.17.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/package.json +4 -4
- package/src/components/checkout/client-context.tsx +2 -0
- package/src/components/checkout/client.ts +10 -0
- package/src/components/checkout/google-pay-button.tsx +253 -0
- package/src/components/checkout/method-capability.ts +27 -0
- package/src/components/checkout/providers/pix-and-card.tsx +8 -23
- package/src/components/checkout/transport.ts +38 -17
- package/src/components/checkout/types.ts +41 -0
- package/src/components/checkout/use-wallet-charge.ts +123 -0
- package/src/components/checkout/wallet-pane.tsx +161 -0
- package/src/index.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"storybook:build": "storybook build"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@12-apps/payments-backend": "^1.
|
|
20
|
+
"@12-apps/payments-backend": "^1.17.0",
|
|
21
21
|
"react-qr-code": "^2.2.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"react-dom": ">=19.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@12-apps/eslint-config": "^1.
|
|
32
|
-
"@12-apps/typescript-config": "^1.
|
|
31
|
+
"@12-apps/eslint-config": "^1.19.0",
|
|
32
|
+
"@12-apps/typescript-config": "^1.19.0",
|
|
33
33
|
"@emotion/react": "^11.14.0",
|
|
34
34
|
"@emotion/styled": "^11.14.0",
|
|
35
35
|
"@mui/material": "^6.5.0",
|
|
@@ -20,6 +20,7 @@ import { createContext, useContext, type JSX, type ReactNode } from "react";
|
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
chargeCard,
|
|
23
|
+
chargeWallet,
|
|
23
24
|
fetchCheckoutConfig,
|
|
24
25
|
listSavedCards,
|
|
25
26
|
pollOrderStatus,
|
|
@@ -32,6 +33,7 @@ const DEFAULT_CLIENT: CheckoutClient = {
|
|
|
32
33
|
getConfig: (tenantSlug) => fetchCheckoutConfig(tenantSlug),
|
|
33
34
|
getStatus: (ref) => pollOrderStatus(ref),
|
|
34
35
|
charge: (input) => chargeCard(input),
|
|
36
|
+
chargeWallet: (input) => chargeWallet(input),
|
|
35
37
|
listInstruments: (tenantSlug) => listSavedCards(tenantSlug),
|
|
36
38
|
refreshBrowserKey: (input) => refreshCardPublicKey(input),
|
|
37
39
|
};
|
|
@@ -28,6 +28,7 @@ import { createCheckoutClient } from "./transport";
|
|
|
28
28
|
import type {
|
|
29
29
|
ChargeCardInput,
|
|
30
30
|
ChargeOutcome,
|
|
31
|
+
ChargeWalletInput,
|
|
31
32
|
CheckoutProviderConfig,
|
|
32
33
|
OrderStatus,
|
|
33
34
|
} from "./types";
|
|
@@ -77,6 +78,15 @@ export async function chargeCard(input: ChargeCardInput): Promise<Result<ChargeO
|
|
|
77
78
|
return defaultClient.charge(input);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Charge a wallet-minted instrument against an order (FUT-471/472) — the same
|
|
83
|
+
* `/charge` route as {@link chargeCard}, carrying `wallet: { type, key }` in
|
|
84
|
+
* place of a card token.
|
|
85
|
+
*/
|
|
86
|
+
export async function chargeWallet(input: ChargeWalletInput): Promise<Result<ChargeOutcome>> {
|
|
87
|
+
return defaultClient.chargeWallet(input);
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
/** List saved cards available for reuse (empty on any error — non-blocking). */
|
|
81
91
|
export async function listSavedCards(tenantSlug?: string): Promise<SavedCard[]> {
|
|
82
92
|
return defaultClient.listInstruments(tenantSlug);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import { useEffect, useRef, useState, type JSX } from "react";
|
|
3
|
+
|
|
4
|
+
import type { CheckoutOrder } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Google-branded pay button (FUT-471), per Google's four-step web guide —
|
|
8
|
+
* and ONLY the four steps. This component owns token ACQUISITION: load
|
|
9
|
+
* `pay.js`, construct a `PaymentsClient`, gate rendering on `isReadyToPay`,
|
|
10
|
+
* render the button Google's brand rules require via `createButton`, and turn
|
|
11
|
+
* `loadPaymentData` into the wallet key
|
|
12
|
+
* (`paymentData.paymentMethodData.tokenizationData.token`). What happens to
|
|
13
|
+
* the key — the charge, the polling, the outcome — belongs to the pane above
|
|
14
|
+
* (`wallet-pane.tsx`), so this file never talks to the wire.
|
|
15
|
+
*
|
|
16
|
+
* The `tokenizationSpecification` is `{ type: 'PAYMENT_GATEWAY', gateway,
|
|
17
|
+
* gatewayMerchantId }`, both parameters published by the store's chain head
|
|
18
|
+
* (`googlePayConfig`) — no vendor name is spelled here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The slice of Google's `PaymentsClient` this button drives. */
|
|
22
|
+
export interface GooglePaymentsClient {
|
|
23
|
+
isReadyToPay(request: Record<string, unknown>): Promise<{ result: boolean }>;
|
|
24
|
+
createButton(options: {
|
|
25
|
+
onClick: () => void;
|
|
26
|
+
buttonSizeMode?: string;
|
|
27
|
+
buttonLocale?: string;
|
|
28
|
+
}): HTMLElement;
|
|
29
|
+
loadPaymentData(request: Record<string, unknown>): Promise<GooglePaymentData>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The one path of the payment data this checkout reads. */
|
|
33
|
+
export interface GooglePaymentData {
|
|
34
|
+
paymentMethodData: { tokenizationData: { token: string } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The `google.payments.api` namespace, as far as this button needs it. */
|
|
38
|
+
export interface GooglePayApi {
|
|
39
|
+
PaymentsClient: new (options: { environment: "TEST" | "PRODUCTION" }) => GooglePaymentsClient;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the store's chain head published for the tokenizationSpecification. */
|
|
43
|
+
export interface GooglePayGatewayParams {
|
|
44
|
+
gateway: string;
|
|
45
|
+
gatewayMerchantId: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Google's script, loaded once per page. */
|
|
49
|
+
const PAY_JS_URL = "https://pay.google.com/gp/p/js/pay.js";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Card networks offered to Google: the intersection of Google's
|
|
53
|
+
* `allowedCardNetworks` enum and what PagBank's card acquiring processes.
|
|
54
|
+
* A network the gateway would refuse must not be offered on the sheet.
|
|
55
|
+
*/
|
|
56
|
+
const ALLOWED_CARD_NETWORKS = ["AMEX", "ELO", "MASTERCARD", "VISA"];
|
|
57
|
+
|
|
58
|
+
/** Both auth methods of Google's guide: tokenized device cards and PAN_ONLY. */
|
|
59
|
+
const ALLOWED_AUTH_METHODS = ["PAN_ONLY", "CRYPTOGRAM_3DS"];
|
|
60
|
+
|
|
61
|
+
/** The `google.payments.api` global, when a script (or a harness) installed it. */
|
|
62
|
+
function installedApi(): GooglePayApi | null {
|
|
63
|
+
if (typeof window === "undefined") return null;
|
|
64
|
+
const scope = window as unknown as { google?: { payments?: { api?: GooglePayApi } } };
|
|
65
|
+
return scope.google?.payments?.api ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The in-flight (or settled) pay.js load — one script tag per page, ever. */
|
|
69
|
+
const loader: { pending: Promise<GooglePayApi | null> | null } = { pending: null };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Step 1 of the guide: load `pay.js` and hand back the API namespace. Answers
|
|
73
|
+
* `null` — never throws — when the script cannot load: an offline CDN must
|
|
74
|
+
* degrade to "no button", not to a crashed checkout. A pre-installed global
|
|
75
|
+
* (another button on the page, or an e2e harness) is used without a network
|
|
76
|
+
* request.
|
|
77
|
+
*/
|
|
78
|
+
function loadGooglePayApi(): Promise<GooglePayApi | null> {
|
|
79
|
+
const installed = installedApi();
|
|
80
|
+
if (installed) return Promise.resolve(installed);
|
|
81
|
+
if (typeof document === "undefined") return Promise.resolve(null);
|
|
82
|
+
loader.pending ??= new Promise((resolve) => {
|
|
83
|
+
const script = document.createElement("script");
|
|
84
|
+
script.src = PAY_JS_URL;
|
|
85
|
+
script.async = true;
|
|
86
|
+
script.onload = () => resolve(installedApi());
|
|
87
|
+
script.onerror = () => resolve(null);
|
|
88
|
+
document.head.appendChild(script);
|
|
89
|
+
});
|
|
90
|
+
return loader.pending;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Step 2's probe: may this browser/device pay at all? */
|
|
94
|
+
function isReadyToPayRequest(): Record<string, unknown> {
|
|
95
|
+
return {
|
|
96
|
+
apiVersion: 2,
|
|
97
|
+
apiVersionMinor: 0,
|
|
98
|
+
allowedPaymentMethods: [
|
|
99
|
+
{
|
|
100
|
+
type: "CARD",
|
|
101
|
+
parameters: {
|
|
102
|
+
allowedAuthMethods: ALLOWED_AUTH_METHODS,
|
|
103
|
+
allowedCardNetworks: ALLOWED_CARD_NETWORKS,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Step 4's request: the same card method, now carrying gateway + price. */
|
|
111
|
+
function paymentDataRequest(
|
|
112
|
+
params: GooglePayGatewayParams,
|
|
113
|
+
order: CheckoutOrder,
|
|
114
|
+
): Record<string, unknown> {
|
|
115
|
+
return {
|
|
116
|
+
apiVersion: 2,
|
|
117
|
+
apiVersionMinor: 0,
|
|
118
|
+
allowedPaymentMethods: [
|
|
119
|
+
{
|
|
120
|
+
type: "CARD",
|
|
121
|
+
parameters: {
|
|
122
|
+
allowedAuthMethods: ALLOWED_AUTH_METHODS,
|
|
123
|
+
allowedCardNetworks: ALLOWED_CARD_NETWORKS,
|
|
124
|
+
},
|
|
125
|
+
tokenizationSpecification: {
|
|
126
|
+
type: "PAYMENT_GATEWAY",
|
|
127
|
+
parameters: {
|
|
128
|
+
gateway: params.gateway,
|
|
129
|
+
gatewayMerchantId: params.gatewayMerchantId,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
transactionInfo: {
|
|
135
|
+
totalPriceStatus: "FINAL",
|
|
136
|
+
// Integer cents to Google's decimal string — the one money conversion
|
|
137
|
+
// in this file, from the server-authoritative order total.
|
|
138
|
+
totalPrice: (order.totalCents / 100).toFixed(2),
|
|
139
|
+
currencyCode: "BRL",
|
|
140
|
+
countryCode: "BR",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The buyer closed the sheet — a choice, not a failure to report. */
|
|
146
|
+
function sheetDismissed(error: unknown): boolean {
|
|
147
|
+
return (
|
|
148
|
+
typeof error === "object" &&
|
|
149
|
+
error !== null &&
|
|
150
|
+
(error as { statusCode?: unknown }).statusCode === "CANCELED"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the client and ask `isReadyToPay` — the gate that decides whether
|
|
156
|
+
* the button exists at all. `api` is injectable for tests and harnesses;
|
|
157
|
+
* `undefined` means "load pay.js".
|
|
158
|
+
*/
|
|
159
|
+
function useGooglePayClient(
|
|
160
|
+
api: GooglePayApi | null | undefined,
|
|
161
|
+
environment: "TEST" | "PRODUCTION",
|
|
162
|
+
): GooglePaymentsClient | null {
|
|
163
|
+
const [client, setClient] = useState<GooglePaymentsClient | null>(null);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const alive = { current: true };
|
|
166
|
+
void (api === undefined ? loadGooglePayApi() : Promise.resolve(api)).then((resolved) => {
|
|
167
|
+
if (!alive.current || !resolved) return;
|
|
168
|
+
const paymentsClient = new resolved.PaymentsClient({ environment });
|
|
169
|
+
paymentsClient
|
|
170
|
+
.isReadyToPay(isReadyToPayRequest())
|
|
171
|
+
.then((answer) => {
|
|
172
|
+
if (alive.current && answer.result) setClient(paymentsClient);
|
|
173
|
+
})
|
|
174
|
+
.catch(() => undefined);
|
|
175
|
+
});
|
|
176
|
+
return () => {
|
|
177
|
+
alive.current = false;
|
|
178
|
+
};
|
|
179
|
+
}, [api, environment]);
|
|
180
|
+
return client;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface GooglePayButtonProps {
|
|
184
|
+
order: CheckoutOrder;
|
|
185
|
+
/** The chain head's published gateway parameters (`googlePayConfig`). */
|
|
186
|
+
params: GooglePayGatewayParams;
|
|
187
|
+
/** The sheet resolved — charge this key. */
|
|
188
|
+
onKey: (key: string) => void;
|
|
189
|
+
/** The sheet failed for a reason worth telling the buyer (not a dismissal). */
|
|
190
|
+
onError: (message: string) => void;
|
|
191
|
+
/**
|
|
192
|
+
* Google's environment. Defaults to TEST — production requires the external
|
|
193
|
+
* Google Pay registration (see the ticket), and TEST tokens exercise the
|
|
194
|
+
* whole path against PagBank's sandbox with fictitious instruments.
|
|
195
|
+
*/
|
|
196
|
+
environment?: "TEST" | "PRODUCTION";
|
|
197
|
+
/** Injectable API namespace for tests/harnesses; omit to load pay.js. */
|
|
198
|
+
api?: GooglePayApi | null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Renders NOTHING until `isReadyToPay` says this browser can pay — per the
|
|
203
|
+
* guide, the buyer must never see a Google Pay button that cannot work. The
|
|
204
|
+
* button element itself comes from `createButton` (brand rules); this
|
|
205
|
+
* component only gives it a mount point.
|
|
206
|
+
*/
|
|
207
|
+
export function GooglePayButton({
|
|
208
|
+
order,
|
|
209
|
+
params,
|
|
210
|
+
onKey,
|
|
211
|
+
onError,
|
|
212
|
+
environment = "TEST",
|
|
213
|
+
api,
|
|
214
|
+
}: GooglePayButtonProps): JSX.Element | null {
|
|
215
|
+
const client = useGooglePayClient(api, environment);
|
|
216
|
+
const container = useRef<HTMLDivElement | null>(null);
|
|
217
|
+
// The latest handlers/order, so the Google-rendered button — mounted once —
|
|
218
|
+
// never closes over a stale charge target.
|
|
219
|
+
const current = useRef({ order, params, onKey, onError });
|
|
220
|
+
current.current = { order, params, onKey, onError };
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
const mount = container.current;
|
|
224
|
+
if (!client || !mount) return undefined;
|
|
225
|
+
const button = client.createButton({
|
|
226
|
+
onClick: () => {
|
|
227
|
+
const { order: forOrder, params: forParams, onKey: emit, onError: fail } = current.current;
|
|
228
|
+
client
|
|
229
|
+
.loadPaymentData(paymentDataRequest(forParams, forOrder))
|
|
230
|
+
.then((data) => emit(data.paymentMethodData.tokenizationData.token))
|
|
231
|
+
.catch((error: unknown) => {
|
|
232
|
+
if (sheetDismissed(error)) return;
|
|
233
|
+
fail("Não foi possível concluir o pagamento com o Google Pay. Tente novamente ou pague com cartão.");
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
buttonSizeMode: "fill",
|
|
237
|
+
buttonLocale: "pt",
|
|
238
|
+
});
|
|
239
|
+
mount.replaceChildren(button);
|
|
240
|
+
return () => {
|
|
241
|
+
mount.replaceChildren();
|
|
242
|
+
};
|
|
243
|
+
}, [client]);
|
|
244
|
+
|
|
245
|
+
if (!client) return null;
|
|
246
|
+
return (
|
|
247
|
+
<Box
|
|
248
|
+
ref={container}
|
|
249
|
+
data-testid="google-pay-button"
|
|
250
|
+
sx={{ minHeight: 40, "& > *": { width: "100%" } }}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -140,6 +140,33 @@ function toCardLink(link: CheckoutChainLink): CardChainLink {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Everything the Google Pay button needs to mint a token (FUT-471), or `null`
|
|
145
|
+
* when the button must not render.
|
|
146
|
+
*
|
|
147
|
+
* Read off the chain HEAD only, like the checkout screen: the wallet token is
|
|
148
|
+
* minted against ONE gateway's `gatewayMerchantId`, so only the provider the
|
|
149
|
+
* walk tries first can charge it — a tail entry's declaration cannot be
|
|
150
|
+
* honoured from this browser.
|
|
151
|
+
*
|
|
152
|
+
* FAILS CLOSED, deliberately the opposite of `offeredMethods`' fail-open: a
|
|
153
|
+
* missing config only costs the buyer a button they still have the card form
|
|
154
|
+
* without, while rendering one on a store that cannot charge wallets sends a
|
|
155
|
+
* buyer through the wallet sheet into a guaranteed refusal. Every clause
|
|
156
|
+
* narrows toward NOT rendering: no config, no chain, a head that cannot CARD,
|
|
157
|
+
* no `GOOGLE_PAY` declaration, no gateway parameters, no merchant id.
|
|
158
|
+
*/
|
|
159
|
+
export function googlePayConfig(
|
|
160
|
+
config: CheckoutProviderConfig | null,
|
|
161
|
+
): { gateway: string; gatewayMerchantId: string } | null {
|
|
162
|
+
const head = config?.chain?.[0];
|
|
163
|
+
if (!head?.methods.includes("CARD")) return null;
|
|
164
|
+
if (!head.wallets?.includes("GOOGLE_PAY")) return null;
|
|
165
|
+
const params = head.googlePay;
|
|
166
|
+
if (!params?.gatewayMerchantId) return null;
|
|
167
|
+
return { gateway: params.gateway, gatewayMerchantId: params.gatewayMerchantId };
|
|
168
|
+
}
|
|
169
|
+
|
|
143
170
|
/**
|
|
144
171
|
* The methods the picker may offer, from the chain's declared capabilities
|
|
145
172
|
* (FUT-698). `null` config — still loading, or a fetch blip — fails OPEN like
|
|
@@ -12,37 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { JSX } from "react";
|
|
14
14
|
|
|
15
|
-
import { CardView } from "../card-view";
|
|
16
|
-
import { cardChain, cardTokenization } from "../method-capability";
|
|
17
15
|
import { PixView } from "../pix-view";
|
|
16
|
+
import { WalletCardPane } from "../wallet-pane";
|
|
18
17
|
|
|
19
18
|
import type { ProviderCheckoutScreenProps } from "./types";
|
|
20
19
|
|
|
21
|
-
export function PixAndCardScreen({
|
|
22
|
-
order,
|
|
23
|
-
buyer,
|
|
24
|
-
config,
|
|
25
|
-
tenantSlug,
|
|
26
|
-
onResolved,
|
|
27
|
-
pollIntervalMs,
|
|
28
|
-
}: ProviderCheckoutScreenProps): JSX.Element | null {
|
|
20
|
+
export function PixAndCardScreen(props: ProviderCheckoutScreenProps): JSX.Element | null {
|
|
21
|
+
const { order, onResolved, pollIntervalMs } = props;
|
|
29
22
|
if (order?.method === "PIX") {
|
|
30
23
|
return <PixView order={order} onResolved={onResolved} pollIntervalMs={pollIntervalMs} />;
|
|
31
24
|
}
|
|
32
25
|
if (order?.method === "CARD") {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// The whole chain (FUT-563): one instrument is minted per provider so
|
|
39
|
-
// the charge survives the first one failing, with nothing re-typed.
|
|
40
|
-
providerChain={cardChain(config)}
|
|
41
|
-
tenantSlug={tenantSlug}
|
|
42
|
-
onResolved={onResolved}
|
|
43
|
-
pollIntervalMs={pollIntervalMs}
|
|
44
|
-
/>
|
|
45
|
-
);
|
|
26
|
+
// The card pane, with its wallet fast lane above the form (FUT-471/472).
|
|
27
|
+
// The pane reads the chain itself (FUT-563: one instrument per provider,
|
|
28
|
+
// so the charge survives the first one failing with nothing re-typed) and
|
|
29
|
+
// renders exactly the old CardView for a store with no wallet.
|
|
30
|
+
return <WalletCardPane {...props} order={order} />;
|
|
46
31
|
}
|
|
47
32
|
// No order yet — the shell is still showing the picker, and raises one as
|
|
48
33
|
// soon as a method is chosen.
|
|
@@ -21,6 +21,7 @@ import { err, ok, type Result } from "../../result";
|
|
|
21
21
|
import type {
|
|
22
22
|
ChargeCardInput,
|
|
23
23
|
ChargeOutcome,
|
|
24
|
+
ChargeWalletInput,
|
|
24
25
|
CheckoutProviderConfig,
|
|
25
26
|
OrderStatus,
|
|
26
27
|
} from "./types";
|
|
@@ -45,11 +46,13 @@ export interface CheckoutTransport {
|
|
|
45
46
|
headers?: () => HeadersInit | Promise<HeadersInit>;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/** The
|
|
49
|
+
/** The six calls the buyer checkout makes, pre-bound to a {@link CheckoutTransport}. */
|
|
49
50
|
export interface CheckoutClient {
|
|
50
51
|
getConfig(tenantSlug: string): Promise<Result<CheckoutProviderConfig>>;
|
|
51
52
|
getStatus(ref: string): Promise<Result<OrderStatus>>;
|
|
52
53
|
charge(input: ChargeCardInput): Promise<Result<ChargeOutcome>>;
|
|
54
|
+
/** A wallet instrument against the same `/charge` route (FUT-471/472). */
|
|
55
|
+
chargeWallet(input: ChargeWalletInput): Promise<Result<ChargeOutcome>>;
|
|
53
56
|
listInstruments(tenantSlug?: string): Promise<SavedCard[]>;
|
|
54
57
|
refreshBrowserKey(input: { orderId: string }): Promise<Result<{ publicKey: string | null }>>;
|
|
55
58
|
}
|
|
@@ -112,6 +115,36 @@ function ambientFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Res
|
|
|
112
115
|
return globalThis.fetch(input, init);
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The FLAT card-charge body the shipped client has always sent. Pinned from
|
|
120
|
+
* both ends by `charge-wire.contract.test.ts`; nothing here may re-nest it.
|
|
121
|
+
*/
|
|
122
|
+
function flatChargeBody(input: ChargeCardInput): string {
|
|
123
|
+
return JSON.stringify({
|
|
124
|
+
orderId: input.orderId,
|
|
125
|
+
token: input.token,
|
|
126
|
+
// One instrument per provider (FUT-563) — the server hands each provider
|
|
127
|
+
// in the chain its own, which is what lets a card charge fail over.
|
|
128
|
+
...(input.tokensByProvider ? { tokensByProvider: input.tokensByProvider } : {}),
|
|
129
|
+
saveCard: input.saveCard,
|
|
130
|
+
cardMeta: input.cardMeta,
|
|
131
|
+
taxId: input.taxId,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The same flat wire with `wallet` in place of `token` (FUT-471): the mount's
|
|
137
|
+
* draft reader takes either, and a body naming both would carry two
|
|
138
|
+
* instruments for one charge. Pinned by `wallet-wire.contract.test.ts`.
|
|
139
|
+
*/
|
|
140
|
+
function flatWalletBody(input: ChargeWalletInput): string {
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
orderId: input.orderId,
|
|
143
|
+
wallet: input.wallet,
|
|
144
|
+
taxId: input.taxId,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
115
148
|
/**
|
|
116
149
|
* The five checkout calls, bound to one transport.
|
|
117
150
|
*
|
|
@@ -156,22 +189,10 @@ export function createCheckoutClient(transport: CheckoutTransport = {}): Checkou
|
|
|
156
189
|
),
|
|
157
190
|
|
|
158
191
|
charge: (input) =>
|
|
159
|
-
call<ChargeOutcome>("/charge", {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
body: JSON.stringify({
|
|
164
|
-
orderId: input.orderId,
|
|
165
|
-
token: input.token,
|
|
166
|
-
// One instrument per provider (FUT-563) — the server hands each
|
|
167
|
-
// provider in the chain its own, which is what lets a card charge
|
|
168
|
-
// fail over.
|
|
169
|
-
...(input.tokensByProvider ? { tokensByProvider: input.tokensByProvider } : {}),
|
|
170
|
-
saveCard: input.saveCard,
|
|
171
|
-
cardMeta: input.cardMeta,
|
|
172
|
-
taxId: input.taxId,
|
|
173
|
-
}),
|
|
174
|
-
}),
|
|
192
|
+
call<ChargeOutcome>("/charge", { method: "POST", body: flatChargeBody(input) }),
|
|
193
|
+
|
|
194
|
+
chargeWallet: (input) =>
|
|
195
|
+
call<ChargeOutcome>("/charge", { method: "POST", body: flatWalletBody(input) }),
|
|
175
196
|
|
|
176
197
|
listInstruments: async (tenantSlug) => {
|
|
177
198
|
// Scoped to the store when known (FUT-697): only cards the store's ACTIVE
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
/** Payment methods offered at checkout. Mirrors `Payment.method` (FUT-42). */
|
|
13
13
|
export type PaymentMethod = "PIX" | "CARD";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The digital wallets a chain entry can charge (FUT-471/472). Mirrors the
|
|
17
|
+
* backend's `WalletType` — a wallet is not a fourth method: it is another way
|
|
18
|
+
* of producing the CARD instrument, so it rides the CARD tile and the CARD
|
|
19
|
+
* charge.
|
|
20
|
+
*/
|
|
21
|
+
export type CheckoutWalletType = "GOOGLE_PAY" | "APPLE_PAY";
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* Order lifecycle, aligned with the FUT-43 backend:
|
|
17
25
|
* - `AWAITING_PAYMENT` — order created, charge raised, not yet reconciled (the
|
|
@@ -198,6 +206,20 @@ export interface ChargeCardInput {
|
|
|
198
206
|
taxId?: string;
|
|
199
207
|
}
|
|
200
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Input to charge a WALLET instrument against an order (FUT-471/472) —
|
|
211
|
+
* `ChargeCardInput`'s wallet sibling, posted to the same `/charge` route. One
|
|
212
|
+
* shape for both wallets because the wire is one shape: `key` is whatever the
|
|
213
|
+
* wallet handed the browser (Google's `tokenizationData.token`, Apple's
|
|
214
|
+
* serialized `token.paymentData`), forwarded verbatim and never persisted.
|
|
215
|
+
*/
|
|
216
|
+
export interface ChargeWalletInput {
|
|
217
|
+
orderId: string;
|
|
218
|
+
wallet: { type: CheckoutWalletType; key: string };
|
|
219
|
+
/** Buyer CPF for the provider charge (`customer.tax_id`); never persisted. */
|
|
220
|
+
taxId?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
201
223
|
/** A buyer field a provider asks for, as `/config` publishes it (FUT-595). */
|
|
202
224
|
export interface CheckoutCustomerField {
|
|
203
225
|
key: "name" | "email" | "taxId" | "phone";
|
|
@@ -236,6 +258,25 @@ export interface CheckoutChainLink {
|
|
|
236
258
|
* never shown a field for.
|
|
237
259
|
*/
|
|
238
260
|
customerSchema?: CheckoutCustomerField[];
|
|
261
|
+
/**
|
|
262
|
+
* The digital wallets THIS entry's card charge accepts (FUT-471/472).
|
|
263
|
+
*
|
|
264
|
+
* Optional, and the DEGRADE DIRECTION IS CLOSED — the opposite of
|
|
265
|
+
* `customerSchema`'s above, because the stakes invert: a chain that asks for
|
|
266
|
+
* nothing produces a refused charge, but a wallet button on a store whose
|
|
267
|
+
* server cannot charge wallets produces a buyer who authorized a payment on
|
|
268
|
+
* the wallet sheet and was then refused. Absent (an older host) means NO
|
|
269
|
+
* wallet buttons, which is exactly what that host's charge route supports.
|
|
270
|
+
*/
|
|
271
|
+
wallets?: CheckoutWalletType[];
|
|
272
|
+
/**
|
|
273
|
+
* Google Pay's `PAYMENT_GATEWAY` tokenizationSpecification parameters for
|
|
274
|
+
* THIS entry (FUT-471), or absent/`null` when it cannot run Google Pay.
|
|
275
|
+
* `gatewayMerchantId: null` means the connection carries no merchant id yet
|
|
276
|
+
* — the button must not render, because a token minted against a missing id
|
|
277
|
+
* charges nobody.
|
|
278
|
+
*/
|
|
279
|
+
googlePay?: { gateway: string; gatewayMerchantId: string | null } | null;
|
|
239
280
|
/**
|
|
240
281
|
* The buyer screen THIS provider's flow needs (FUT-596) — an opaque id the
|
|
241
282
|
* adapter declares and `providers/registry.ts` resolves to a component.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { useCheckoutClientApi } from "./client-context";
|
|
4
|
+
import { UNRESOLVED_CODE } from "./failure-codes";
|
|
5
|
+
import { rememberHostedOrder } from "./hosted-return";
|
|
6
|
+
import { useCheckoutNavigate } from "./navigate-context";
|
|
7
|
+
import type { BuyerInfo, CheckoutOrder, CheckoutWalletType, OrderStatus } from "./types";
|
|
8
|
+
import { usePaymentPolling } from "./use-payment-polling";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The submit half of a WALLET payment (FUT-471/472) — `useCardSubmit`'s
|
|
12
|
+
* sibling, minus everything a wallet does not have: no form to validate, no
|
|
13
|
+
* instrument to mint per provider (the wallet key is chain-head-bound, see the
|
|
14
|
+
* backend's `core/card-instrument.ts`), no vault opt-in. What remains is the
|
|
15
|
+
* same money path: charge → classify the outcome → poll for the async
|
|
16
|
+
* confirmation → bubble the terminal status up.
|
|
17
|
+
*
|
|
18
|
+
* One hook for BOTH wallets, because the wire is one shape — the buttons only
|
|
19
|
+
* differ in how they acquire the key.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Where the wallet payment stands. `idle` renders the buttons and the card
|
|
24
|
+
* form; anything else replaces them — a live "pay" control under a charge that
|
|
25
|
+
* may already be holding the buyer's money is the double-payment invitation
|
|
26
|
+
* the card view already refuses to render.
|
|
27
|
+
*/
|
|
28
|
+
export type WalletPhase = "idle" | "charging" | "polling";
|
|
29
|
+
|
|
30
|
+
/** Everything a wallet pane renders — state plus the one submit entry point. */
|
|
31
|
+
export interface WalletCharge {
|
|
32
|
+
phase: WalletPhase;
|
|
33
|
+
/** The refusal to show above the form, when the charge came back refused. */
|
|
34
|
+
error: string | null;
|
|
35
|
+
/** The refusal's machine code — an UNRESOLVED charge is not a decline. */
|
|
36
|
+
errorCode: string | null;
|
|
37
|
+
/** The charge is unresolved: no pay control may render (FUT-563). */
|
|
38
|
+
unresolved: boolean;
|
|
39
|
+
pollError: string | null;
|
|
40
|
+
/** The healthy-poll cap elapsed while still AWAITING (FUT-191). */
|
|
41
|
+
pollTimedOut: boolean;
|
|
42
|
+
/** Charge the wallet's key. The button calls this once the sheet resolves. */
|
|
43
|
+
payWithKey(type: CheckoutWalletType, key: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Healthy-poll cap for the wallet AWAITING wait — the card path's own cap
|
|
48
|
+
* (FUT-191): 36 polls ≈ 90 s at the 2500 ms default interval.
|
|
49
|
+
*/
|
|
50
|
+
const WALLET_AWAITING_POLL_CAP = 36;
|
|
51
|
+
|
|
52
|
+
/** The wallet charge state machine. See the module comment. */
|
|
53
|
+
export function useWalletCharge(
|
|
54
|
+
order: CheckoutOrder,
|
|
55
|
+
buyer: BuyerInfo,
|
|
56
|
+
onResolved: (status: OrderStatus) => void,
|
|
57
|
+
pollIntervalMs = 2500,
|
|
58
|
+
): WalletCharge {
|
|
59
|
+
const [phase, setPhase] = useState<WalletPhase>("idle");
|
|
60
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
|
+
const [errorCode, setErrorCode] = useState<string | null>(null);
|
|
62
|
+
const client = useCheckoutClientApi();
|
|
63
|
+
const navigate = useCheckoutNavigate();
|
|
64
|
+
|
|
65
|
+
const { status, error: pollError, timedOut: pollTimedOut } = usePaymentPolling(order.orderId, {
|
|
66
|
+
enabled: phase === "polling",
|
|
67
|
+
intervalMs: pollIntervalMs,
|
|
68
|
+
maxHealthyPolls: WALLET_AWAITING_POLL_CAP,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (status && status !== "AWAITING_PAYMENT") onResolved(status);
|
|
73
|
+
}, [status, onResolved]);
|
|
74
|
+
|
|
75
|
+
const payWithKey = async (type: CheckoutWalletType, key: string): Promise<void> => {
|
|
76
|
+
setError(null);
|
|
77
|
+
setErrorCode(null);
|
|
78
|
+
setPhase("charging");
|
|
79
|
+
|
|
80
|
+
const charged = await client.chargeWallet({
|
|
81
|
+
orderId: order.orderId,
|
|
82
|
+
wallet: { type, key },
|
|
83
|
+
// The CPF the Dados step collected — the provider's required-field gate
|
|
84
|
+
// reads it from the charge, the payable row has nowhere to keep it.
|
|
85
|
+
taxId: buyer.taxId,
|
|
86
|
+
});
|
|
87
|
+
if (!charged.ok) {
|
|
88
|
+
setError(charged.error);
|
|
89
|
+
setErrorCode(charged.code ?? null);
|
|
90
|
+
// Back to idle — but an UNRESOLVED code sets `unresolved`, and the pane
|
|
91
|
+
// reads that as "render NO pay control" (FUT-563): some provider may be
|
|
92
|
+
// holding the money, and a live button under "não pague de novo" is what
|
|
93
|
+
// the buyer's thumb reaches for.
|
|
94
|
+
setPhase("idle");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// A provider that demands its own page to finish (redirect 3-D Secure,
|
|
98
|
+
// FUT-698): park the order and hand the buyer over, exactly as the card
|
|
99
|
+
// path does. `phase` stays as-is — the tab is navigating away.
|
|
100
|
+
if (charged.data.hostedCheckoutUrl) {
|
|
101
|
+
rememberHostedOrder(order);
|
|
102
|
+
navigate(charged.data.hostedCheckoutUrl);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// A business outcome (declined → FAILED) shows the status screen; an
|
|
106
|
+
// accepted charge begins polling for the async confirmation.
|
|
107
|
+
if (charged.data.status !== "AWAITING_PAYMENT") {
|
|
108
|
+
onResolved(charged.data.status);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setPhase("polling");
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
phase,
|
|
116
|
+
error,
|
|
117
|
+
errorCode,
|
|
118
|
+
unresolved: errorCode === UNRESOLVED_CODE,
|
|
119
|
+
pollError,
|
|
120
|
+
pollTimedOut,
|
|
121
|
+
payWithKey,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Box, Divider } from "@mui/material";
|
|
2
|
+
import { useState, type JSX } from "react";
|
|
3
|
+
|
|
4
|
+
import { CardView } from "./card-view";
|
|
5
|
+
import { GooglePayButton } from "./google-pay-button";
|
|
6
|
+
import { cardChain, cardTokenization, googlePayConfig } from "./method-capability";
|
|
7
|
+
import type { ProviderCheckoutScreenProps } from "./providers/types";
|
|
8
|
+
import { useCheckoutComponents } from "./ui";
|
|
9
|
+
import { useWalletCharge, type WalletCharge } from "./use-wallet-charge";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The CARD pane with its wallet fast lane (FUT-471/472).
|
|
13
|
+
*
|
|
14
|
+
* A wallet is not a fourth method — it is another way of producing the CARD
|
|
15
|
+
* instrument — so it renders INSIDE the card pane, above the form, and only
|
|
16
|
+
* when the chain head both declared the wallet capability and published the
|
|
17
|
+
* parameters the browser needs (`googlePayConfig`, fail-closed). A store with
|
|
18
|
+
* no wallet renders exactly the card view it always did.
|
|
19
|
+
*
|
|
20
|
+
* One pane owns BOTH submit paths' visibility so they cannot invite a double
|
|
21
|
+
* payment: while a wallet charge is in flight or being confirmed, the card
|
|
22
|
+
* form and every wallet button are REPLACED by the processing state — the same
|
|
23
|
+
* rule `card-view.tsx` applies to its own pay bar, one level up.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Post-submit confirmation, error > timeout > spinner — the card view's order. */
|
|
27
|
+
function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
|
|
28
|
+
const { Alert, LoadingState } = useCheckoutComponents();
|
|
29
|
+
if (wallet.pollError) {
|
|
30
|
+
return (
|
|
31
|
+
<Alert
|
|
32
|
+
variant="danger"
|
|
33
|
+
title="Não foi possível confirmar o pagamento"
|
|
34
|
+
description={wallet.pollError}
|
|
35
|
+
showIcon
|
|
36
|
+
data-testid="wallet-poll-error"
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (wallet.pollTimedOut) {
|
|
41
|
+
return (
|
|
42
|
+
<Alert
|
|
43
|
+
variant="warning"
|
|
44
|
+
title="O pagamento está demorando mais que o esperado"
|
|
45
|
+
description="Você pode aguardar ou verificar seu pedido em instantes — não realize um novo pagamento."
|
|
46
|
+
showIcon
|
|
47
|
+
data-testid="wallet-poll-timeout"
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
<LoadingState
|
|
53
|
+
variant="spinner"
|
|
54
|
+
size="md"
|
|
55
|
+
message="Processando pagamento…"
|
|
56
|
+
dataTestId="wallet-processing"
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* An UNRESOLVED wallet charge (FUT-563): some provider may be holding the
|
|
63
|
+
* buyer's money, so the pane shows the warning and NO pay control of any kind
|
|
64
|
+
* — no wallet button, no card form. Same presentation rule as the card view's
|
|
65
|
+
* own unresolved state.
|
|
66
|
+
*/
|
|
67
|
+
function WalletUnresolved({ message }: { message: string }): JSX.Element {
|
|
68
|
+
const { Alert } = useCheckoutComponents();
|
|
69
|
+
return (
|
|
70
|
+
<Alert
|
|
71
|
+
variant="warning"
|
|
72
|
+
title="Estamos confirmando seu pagamento"
|
|
73
|
+
description={message}
|
|
74
|
+
showIcon
|
|
75
|
+
data-testid="wallet-unresolved"
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The wallet buttons the store's chain head supports, or null when there are
|
|
82
|
+
* none. Split out so the pane below stays under the function-size gate.
|
|
83
|
+
*/
|
|
84
|
+
function WalletButtons({
|
|
85
|
+
props,
|
|
86
|
+
wallet,
|
|
87
|
+
onSheetError,
|
|
88
|
+
}: {
|
|
89
|
+
props: ProviderCheckoutScreenProps & { order: NonNullable<ProviderCheckoutScreenProps["order"]> };
|
|
90
|
+
wallet: WalletCharge;
|
|
91
|
+
onSheetError: (message: string) => void;
|
|
92
|
+
}): JSX.Element | null {
|
|
93
|
+
const { Text } = useCheckoutComponents();
|
|
94
|
+
const googlePay = googlePayConfig(props.config);
|
|
95
|
+
if (!googlePay) return null;
|
|
96
|
+
return (
|
|
97
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
98
|
+
<GooglePayButton
|
|
99
|
+
order={props.order}
|
|
100
|
+
params={googlePay}
|
|
101
|
+
onKey={(key) => void wallet.payWithKey("GOOGLE_PAY", key)}
|
|
102
|
+
onError={onSheetError}
|
|
103
|
+
/>
|
|
104
|
+
<Divider>
|
|
105
|
+
<Text variant="caption" size="xs" color="secondary" as="span">
|
|
106
|
+
ou pague com cartão
|
|
107
|
+
</Text>
|
|
108
|
+
</Divider>
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The CARD pane: wallet fast lane above, the card form below. */
|
|
114
|
+
export function WalletCardPane(
|
|
115
|
+
props: ProviderCheckoutScreenProps & {
|
|
116
|
+
order: NonNullable<ProviderCheckoutScreenProps["order"]>;
|
|
117
|
+
},
|
|
118
|
+
): JSX.Element {
|
|
119
|
+
const { Alert } = useCheckoutComponents();
|
|
120
|
+
const { order, buyer, config, tenantSlug, onResolved, pollIntervalMs } = props;
|
|
121
|
+
const wallet = useWalletCharge(order, buyer, onResolved, pollIntervalMs);
|
|
122
|
+
// A sheet failure the wallet reported before any charge existed (pay.js
|
|
123
|
+
// refused, the sheet errored) — shown beside the form, which stays usable.
|
|
124
|
+
const [sheetError, setSheetError] = useState<string | null>(null);
|
|
125
|
+
|
|
126
|
+
if (wallet.phase !== "idle") return <WalletProcessing wallet={wallet} />;
|
|
127
|
+
if (wallet.unresolved) return <WalletUnresolved message={wallet.error ?? ""} />;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
|
131
|
+
<WalletButtons props={props} wallet={wallet} onSheetError={setSheetError} />
|
|
132
|
+
{wallet.error ? (
|
|
133
|
+
<Alert
|
|
134
|
+
variant="danger"
|
|
135
|
+
title="Não foi possível pagar"
|
|
136
|
+
description={wallet.error}
|
|
137
|
+
showIcon
|
|
138
|
+
data-testid="wallet-error"
|
|
139
|
+
/>
|
|
140
|
+
) : null}
|
|
141
|
+
{sheetError && !wallet.error ? (
|
|
142
|
+
<Alert
|
|
143
|
+
variant="danger"
|
|
144
|
+
title="Não foi possível pagar"
|
|
145
|
+
description={sheetError}
|
|
146
|
+
showIcon
|
|
147
|
+
data-testid="wallet-sheet-error"
|
|
148
|
+
/>
|
|
149
|
+
) : null}
|
|
150
|
+
<CardView
|
|
151
|
+
order={order}
|
|
152
|
+
buyer={buyer}
|
|
153
|
+
providerConfig={cardTokenization(config)}
|
|
154
|
+
providerChain={cardChain(config)}
|
|
155
|
+
tenantSlug={tenantSlug}
|
|
156
|
+
onResolved={onResolved}
|
|
157
|
+
pollIntervalMs={pollIntervalMs}
|
|
158
|
+
/>
|
|
159
|
+
</Box>
|
|
160
|
+
);
|
|
161
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -84,6 +84,20 @@ export {
|
|
|
84
84
|
export { type CheckoutHostPorts } from './components/checkout/use-checkout-controller';
|
|
85
85
|
export { PaymentsUnavailable } from './components/checkout/payments-unavailable';
|
|
86
86
|
export { fetchCheckoutConfig } from './components/checkout/client';
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Digital wallets (FUT-471/472) — the Google-branded button and the capability
|
|
89
|
+
// read it is gated on. `CheckoutFlow` wires these automatically; they are
|
|
90
|
+
// exported for hosts composing their own pixels.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export {
|
|
93
|
+
GooglePayButton,
|
|
94
|
+
type GooglePayApi,
|
|
95
|
+
type GooglePayButtonProps,
|
|
96
|
+
type GooglePaymentData,
|
|
97
|
+
type GooglePaymentsClient,
|
|
98
|
+
type GooglePayGatewayParams,
|
|
99
|
+
} from './components/checkout/google-pay-button';
|
|
100
|
+
export { googlePayConfig } from './components/checkout/method-capability';
|
|
87
101
|
export {
|
|
88
102
|
CheckoutComponentsProvider,
|
|
89
103
|
type CheckoutActionBarProps,
|
|
@@ -103,11 +117,13 @@ export {
|
|
|
103
117
|
type BuyerContact,
|
|
104
118
|
type BuyerField,
|
|
105
119
|
type BuyerInfo,
|
|
120
|
+
type ChargeWalletInput,
|
|
106
121
|
type CheckoutChainLink,
|
|
107
122
|
type CheckoutCustomerField,
|
|
108
123
|
type CheckoutError,
|
|
109
124
|
type CheckoutOrder,
|
|
110
125
|
type CheckoutProviderConfig,
|
|
126
|
+
type CheckoutWalletType,
|
|
111
127
|
type ComandaCheckout,
|
|
112
128
|
type CreateOrderRequest,
|
|
113
129
|
type CreateOrderResult,
|