@12-apps/payments-frontend 1.17.0 → 1.19.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,183 @@
1
+ /**
2
+ * The add-card state machine (FUT-183) — the buyer half of the FUT-478 vault
3
+ * surface: `POST /cards/begin` equips this browser, the shared card form and
4
+ * tokenizer mint the instrument, `POST /cards/complete` stores it and answers
5
+ * display metadata. Extracted from the view for the same reason
6
+ * `use-card-checkout.ts` is: the screen stays presentational, and a story can
7
+ * stage any phase by building a {@link AddCardController} literal.
8
+ *
9
+ * What never appears here is as deliberate as what does:
10
+ *
11
+ * - no ownership facts. `reference`/`customerRef` are the HOST's answer to
12
+ * the mount's vault port; the browser contributes only the session it was
13
+ * handed and the token it minted.
14
+ * - no vault token on the way back. `complete` answers display metadata
15
+ * only, and that is all the saved phase holds.
16
+ */
17
+ import {
18
+ useEffect,
19
+ useState,
20
+ type Dispatch,
21
+ type SetStateAction,
22
+ } from "react";
23
+
24
+ import {
25
+ detectBrand,
26
+ onlyDigits,
27
+ tokenizeForCheckout,
28
+ validateCardNumber,
29
+ validateCvv,
30
+ validateExpiry,
31
+ validateHolder,
32
+ type CardBrand,
33
+ type CardDetails,
34
+ type CardFieldErrors,
35
+ type CardTokenizationConfig,
36
+ } from "../card";
37
+ import type {
38
+ BuyerVaultSession,
39
+ VaultedCardDisplay,
40
+ } from "../components/checkout/transport";
41
+ import type { CheckoutProviderConfig } from "../components/checkout/types";
42
+ import type { Result } from "../result";
43
+
44
+ import { useResolvedConfig, type FlowsRuntime } from "./runtime";
45
+
46
+ const EMPTY_CARD: CardDetails = { number: "", holder: "", expiry: "", cvv: "" };
47
+
48
+ /** Where the add-card flow is, from first paint to a card on file. */
49
+ export type AddCardPhase =
50
+ | { kind: "preparing" }
51
+ /** `begin` refused — a state the buyer cannot fix, said plainly. */
52
+ | { kind: "unavailable"; message: string }
53
+ | { kind: "form"; session: BuyerVaultSession }
54
+ | { kind: "saved"; display: VaultedCardDisplay };
55
+
56
+ /** Everything the add-card view renders. A story stages one as a literal. */
57
+ export interface AddCardController {
58
+ phase: AddCardPhase;
59
+ card: CardDetails;
60
+ setCard: Dispatch<SetStateAction<CardDetails>>;
61
+ fieldErrors: CardFieldErrors;
62
+ setFieldErrors: Dispatch<SetStateAction<CardFieldErrors>>;
63
+ brand: CardBrand;
64
+ /** A tokenize + complete round trip is in flight. */
65
+ saving: boolean;
66
+ /** The refusal the buyer reads — the endpoint's own reason, form kept editable. */
67
+ error: string | null;
68
+ submit(): Promise<void>;
69
+ }
70
+
71
+ /**
72
+ * Word a refused `begin`. `VAULT_NOT_ENABLED` is the mount's machine-level
73
+ * convention (a deliberately English sentence — a host wiring gap no buyer can
74
+ * fix), so the factory's own pt-BR stands in for it; every other refusal
75
+ * (`PAYMENT_NOT_CONFIGURED`, a transport failure) already carries the pt-BR
76
+ * message the host's copy table worded.
77
+ */
78
+ function beginRefusalMessage(
79
+ runtime: FlowsRuntime,
80
+ refusal: { error: string; code?: string },
81
+ ): string {
82
+ return refusal.code === "VAULT_NOT_ENABLED" ? runtime.copy.addCardUnavailable : refusal.error;
83
+ }
84
+
85
+ /**
86
+ * The tokenization triple for THIS vault session. Provider and key come from
87
+ * the `begin` answer — the session's own facts. The stub grant does not travel
88
+ * on it: `GET /config` is the ONLY sanctioned source for `mockTokenization`
89
+ * (FUT-697), so it is read off the published chain entry for the session's
90
+ * provider, and absent that, off the config head. No config ⇒ no grant.
91
+ */
92
+ function sessionTokenization(
93
+ session: BuyerVaultSession,
94
+ config: CheckoutProviderConfig | null,
95
+ ): CardTokenizationConfig {
96
+ const link = config?.chain?.find((entry) => entry.provider === session.provider);
97
+ const mockTokenization =
98
+ link?.mockTokenization ??
99
+ (config?.provider === session.provider ? config.mockTokenization : false);
100
+ return { provider: session.provider, publicKey: session.publicKey, mockTokenization };
101
+ }
102
+
103
+ /** Fetch the vault session once on mount; the phases follow the answer. */
104
+ function useVaultSession(runtime: FlowsRuntime): {
105
+ phase: AddCardPhase;
106
+ setPhase: Dispatch<SetStateAction<AddCardPhase>>;
107
+ } {
108
+ const [phase, setPhase] = useState<AddCardPhase>({ kind: "preparing" });
109
+ useEffect(() => {
110
+ let active = true;
111
+ void runtime.client.beginVault().then((result: Result<BuyerVaultSession>) => {
112
+ if (!active) return;
113
+ if (!result.ok) {
114
+ setPhase({ kind: "unavailable", message: beginRefusalMessage(runtime, result) });
115
+ return;
116
+ }
117
+ setPhase({ kind: "form", session: result.data });
118
+ });
119
+ return () => {
120
+ active = false;
121
+ };
122
+ }, [runtime]);
123
+ return { phase, setPhase };
124
+ }
125
+
126
+ /**
127
+ * The add-card flow: begin → (buyer types) → tokenize → complete → saved.
128
+ *
129
+ * A refused `complete` sets {@link AddCardController.error} and stays on the
130
+ * form — the endpoint's reason is the buyer's cue to fix the card, and wiping
131
+ * their input to say it would be the screen working against them.
132
+ */
133
+ export function useAddCard(
134
+ runtime: FlowsRuntime,
135
+ onSaved?: (display: VaultedCardDisplay) => void,
136
+ ): AddCardController {
137
+ const { config } = useResolvedConfig(runtime);
138
+ const { phase, setPhase } = useVaultSession(runtime);
139
+ const [card, setCard] = useState<CardDetails>(EMPTY_CARD);
140
+ const [fieldErrors, setFieldErrors] = useState<CardFieldErrors>({});
141
+ const [saving, setSaving] = useState(false);
142
+ const [error, setError] = useState<string | null>(null);
143
+
144
+ const brand = detectBrand(onlyDigits(card.number));
145
+
146
+ const validate = (): CardFieldErrors => ({
147
+ number: validateCardNumber(card.number),
148
+ holder: validateHolder(card.holder),
149
+ expiry: validateExpiry(card.expiry),
150
+ cvv: validateCvv(card.cvv, brand),
151
+ });
152
+
153
+ const submit = async (): Promise<void> => {
154
+ if (phase.kind !== "form" || saving) return;
155
+ setError(null);
156
+ const errors = validate();
157
+ setFieldErrors(errors);
158
+ if (Object.values(errors).some(Boolean)) return;
159
+
160
+ setSaving(true);
161
+ const minted = await tokenizeForCheckout(card, sessionTokenization(phase.session, config));
162
+ if (!minted.ok) {
163
+ setError(minted.error);
164
+ setSaving(false);
165
+ return;
166
+ }
167
+ // The browser's two legitimate facts, and nothing else: the session it is
168
+ // completing and the instrument it minted. Ownership rides server-side.
169
+ const completed = await runtime.client.completeVault({
170
+ ...(phase.session.sessionId ? { sessionId: phase.session.sessionId } : {}),
171
+ token: minted.data.token,
172
+ });
173
+ setSaving(false);
174
+ if (!completed.ok) {
175
+ setError(completed.error);
176
+ return;
177
+ }
178
+ setPhase({ kind: "saved", display: completed.data });
179
+ onSaved?.(completed.data);
180
+ };
181
+
182
+ return { phase, card, setCard, fieldErrors, setFieldErrors, brand, saving, error, submit };
183
+ }
package/src/index.ts CHANGED
@@ -78,8 +78,11 @@ export {
78
78
  export {
79
79
  createCheckoutClient,
80
80
  DEFAULT_CHECKOUT_BASE_URL,
81
+ type BuyerVaultSession,
81
82
  type CheckoutClient,
82
83
  type CheckoutTransport,
84
+ type CompleteVaultInput,
85
+ type VaultedCardDisplay,
83
86
  } from './components/checkout/transport';
84
87
  export { type CheckoutHostPorts } from './components/checkout/use-checkout-controller';
85
88
  export { PaymentsUnavailable } from './components/checkout/payments-unavailable';
@@ -97,7 +100,16 @@ export {
97
100
  type GooglePaymentsClient,
98
101
  type GooglePayGatewayParams,
99
102
  } from './components/checkout/google-pay-button';
100
- export { googlePayConfig } from './components/checkout/method-capability';
103
+ export {
104
+ ApplePayButton,
105
+ applePaySupported,
106
+ APPLE_PAY_SUPPORTED_NETWORKS,
107
+ type ApplePayButtonProps,
108
+ type ApplePayPaymentRequest,
109
+ type ApplePaySessionClass,
110
+ type ApplePaySessionLike,
111
+ } from './components/checkout/apple-pay-button';
112
+ export { applePayDeclared, googlePayConfig } from './components/checkout/method-capability';
101
113
  export {
102
114
  CheckoutComponentsProvider,
103
115
  type CheckoutActionBarProps,
@@ -185,6 +197,28 @@ export {
185
197
  type PaymentProviderSettingsProps,
186
198
  } from './components/PaymentProviderSettings';
187
199
 
200
+ // ---------------------------------------------------------------------------
201
+ // The PLATFORM operations screens (FUT-479 / FUT-483, packaged by FUT-573) —
202
+ // the Connect-application consult and the homologação, as dumb components a
203
+ // host page mounts with data + callbacks from its own routes. Their backend
204
+ // halves live in `@12-apps/payments-backend` (`consultConnectApplications`,
205
+ // `platformHomologacaoGuide`, `createHomologationRecordService`,
206
+ // `buildPlatformHomologacaoAnexo`).
207
+ // ---------------------------------------------------------------------------
208
+ export {
209
+ ConnectApplicationPanel,
210
+ type ConnectApplicationPanelProps,
211
+ } from './components/platform/ConnectApplicationPanel';
212
+ export {
213
+ PlatformHomologacao,
214
+ type PlatformHomologacaoProps,
215
+ } from './components/platform/PlatformHomologacao';
216
+ export {
217
+ type HomologacaoSaveInput,
218
+ type HomologacaoSaveState,
219
+ type PlatformHomologationRecordView,
220
+ } from './components/platform/HomologacaoOutcomeCard';
221
+
188
222
  /**
189
223
  * Re-exported because it appears in the `prepareConnect` prop a host must
190
224
  * implement: without it the host could not type its own callback without