@12-apps/payments-frontend 1.3.1 → 1.4.1
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 +13 -5
- package/src/card/stripe-token.ts +3 -0
- package/src/card/tokenize.ts +17 -3
- package/src/components/checkout/card-instruments.ts +222 -0
- package/src/components/checkout/card-view.tsx +56 -6
- package/src/components/checkout/checkout-flow.tsx +1 -0
- package/src/components/checkout/checkout-steps.tsx +13 -54
- package/src/components/checkout/client.ts +21 -3
- package/src/components/checkout/failure-codes.ts +17 -0
- package/src/components/checkout/method-capability.ts +83 -9
- package/src/components/checkout/payment-error-panel.tsx +97 -0
- package/src/components/checkout/types.ts +39 -4
- package/src/components/checkout/use-card-checkout.ts +77 -91
- package/src/components/checkout/use-checkout-controller.ts +33 -6
- package/src/result.ts +15 -2
- package/eslint.config.js +0 -34
- package/src/__tests__/checkout-confirmation.test.tsx +0 -177
- package/src/__tests__/connection-state.test.tsx +0 -84
- package/src/__tests__/context.test.tsx +0 -88
- package/src/__tests__/controlled-provider.test.tsx +0 -116
- package/src/__tests__/credential-confirm.test.tsx +0 -193
- package/src/__tests__/initial-provider.test.tsx +0 -81
- package/src/__tests__/provider-priority-list.test.tsx +0 -159
- package/src/__tests__/provider-status-bar.test.tsx +0 -152
- package/src/__tests__/slugged-provider.test.tsx +0 -108
- package/src/__tests__/verification-slot.test.tsx +0 -125
- package/src/card/tokenize.test.ts +0 -194
- package/src/components/checkout/__tests__/card-3ds-handover.test.tsx +0 -147
- package/src/components/checkout/__tests__/clear-cart-on-paid.test.tsx +0 -64
- package/src/components/checkout/__tests__/hosted-return.test.ts +0 -109
- package/src/components/checkout/__tests__/method-capability.test.tsx +0 -120
- package/src/components/checkout/__tests__/payments-unavailable.test.tsx +0 -53
- package/src/components/checkout/__tests__/save-on-continue.test.tsx +0 -165
- package/src/components/checkout/__tests__/second-host.test.tsx +0 -86
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* The buyer's details must be persisted when they press "Continuar" on the
|
|
4
|
-
* "Dados" step — NOT when a payment is raised.
|
|
5
|
-
*
|
|
6
|
-
* This is a regression test with a history: the save used to live only in the
|
|
7
|
-
* order-creation path, which runs a whole step later, only once a payment
|
|
8
|
-
* method is chosen, and which deliberately never writes the CPF. A buyer who
|
|
9
|
-
* filled the form and then hit anything at all on the payment screen — no
|
|
10
|
-
* provider configured, a declined card, an abandoned PIX, a closed tab — had
|
|
11
|
-
* nothing stored.
|
|
12
|
-
*
|
|
13
|
-
* Since FUT-564 the write itself belongs to the host (`saveBuyerContact` port;
|
|
14
|
-
* the storefront's implementation owns the wire shape and the blank-CPF-never-
|
|
15
|
-
* clears rule, pinned in its own suite). What THIS suite pins is the flow's
|
|
16
|
-
* side of the contract — TIMING, CONSENT and INDEPENDENCE: the port fires on
|
|
17
|
-
* "Continuar", carries the CPF, respects the LGPD checkbox, and the step
|
|
18
|
-
* advances no matter what the host does with the call.
|
|
19
|
-
*/
|
|
20
|
-
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
21
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
22
|
-
|
|
23
|
-
import { useCheckoutController, type CheckoutHostPorts } from "../use-checkout-controller";
|
|
24
|
-
import type { BuyerContact } from "../types";
|
|
25
|
-
|
|
26
|
-
/** A valid CPF (passes the client-side check digits). */
|
|
27
|
-
const VALID_CPF = "52998224725";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* One test's own port set + recorder. Everything is a local — no module-level
|
|
31
|
-
* mutable state, so the tests carry no order dependency between them
|
|
32
|
-
* (`test-flakiness/no-test-isolation`). The stub only ever PUSHES onto its
|
|
33
|
-
* container, never reassigns a closed-over binding (`no-global-state-mutation`).
|
|
34
|
-
*/
|
|
35
|
-
function makePorts(): {
|
|
36
|
-
ports: CheckoutHostPorts;
|
|
37
|
-
savedContacts: () => BuyerContact[];
|
|
38
|
-
orderRequested: () => boolean;
|
|
39
|
-
} {
|
|
40
|
-
const recorded: BuyerContact[] = [];
|
|
41
|
-
const createOrder = vi.fn();
|
|
42
|
-
return {
|
|
43
|
-
ports: {
|
|
44
|
-
createOrder,
|
|
45
|
-
saveBuyerContact: (contact) => {
|
|
46
|
-
recorded.push(contact);
|
|
47
|
-
},
|
|
48
|
-
onExitToMenu: vi.fn(),
|
|
49
|
-
},
|
|
50
|
-
savedContacts: () => recorded,
|
|
51
|
-
orderRequested: () => createOrder.mock.calls.length > 0,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
vi.restoreAllMocks();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('"Dados" step persists the buyer on Continuar', () => {
|
|
60
|
-
it("saves name, phone and CPF the moment Continuar is pressed", async () => {
|
|
61
|
-
const { ports, savedContacts, orderRequested } = makePorts();
|
|
62
|
-
const { result } = renderHook(() => useCheckoutController(ports));
|
|
63
|
-
|
|
64
|
-
act(() => {
|
|
65
|
-
result.current.setBuyer({ name: "Ana", phone: "11999999999", taxId: VALID_CPF });
|
|
66
|
-
});
|
|
67
|
-
act(() => {
|
|
68
|
-
result.current.goToPayment();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
await waitFor(() => expect(savedContacts()).toHaveLength(1));
|
|
72
|
-
expect(savedContacts()[0]).toEqual({
|
|
73
|
-
name: "Ana",
|
|
74
|
-
phone: "11999999999",
|
|
75
|
-
taxId: VALID_CPF,
|
|
76
|
-
});
|
|
77
|
-
// The whole point: no order was created, and none needed to be.
|
|
78
|
-
expect(orderRequested()).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("advances to Pagamento without waiting on the save", async () => {
|
|
82
|
-
const { ports, savedContacts } = makePorts();
|
|
83
|
-
// A host whose save blows up must not trap the buyer on the Dados step —
|
|
84
|
-
// the port is fire-and-forget, so the flow never sees the failure at all.
|
|
85
|
-
const { result } = renderHook(() => useCheckoutController(ports));
|
|
86
|
-
|
|
87
|
-
act(() => {
|
|
88
|
-
result.current.setBuyer({ name: "Ana", taxId: VALID_CPF });
|
|
89
|
-
});
|
|
90
|
-
act(() => {
|
|
91
|
-
result.current.goToPayment();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
await waitFor(() => expect(savedContacts()).toHaveLength(1));
|
|
95
|
-
expect(result.current.step).toBe("payment");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("does not save when the buyer declined the consent checkbox", async () => {
|
|
99
|
-
const { ports, savedContacts } = makePorts();
|
|
100
|
-
const { result } = renderHook(() => useCheckoutController(ports));
|
|
101
|
-
|
|
102
|
-
act(() => {
|
|
103
|
-
result.current.setBuyer({ name: "Ana", taxId: VALID_CPF });
|
|
104
|
-
result.current.setSaveProfile(false);
|
|
105
|
-
});
|
|
106
|
-
act(() => {
|
|
107
|
-
result.current.goToPayment();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await waitFor(() => expect(result.current.step).toBe("payment"));
|
|
111
|
-
expect(savedContacts()).toHaveLength(0);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("does not save — or advance — when the CPF is invalid", async () => {
|
|
115
|
-
const { ports, savedContacts } = makePorts();
|
|
116
|
-
const { result } = renderHook(() => useCheckoutController(ports));
|
|
117
|
-
|
|
118
|
-
act(() => {
|
|
119
|
-
result.current.setBuyer({ name: "Ana", taxId: "11111111111" });
|
|
120
|
-
});
|
|
121
|
-
act(() => {
|
|
122
|
-
result.current.goToPayment();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
await waitFor(() => expect(result.current.errorField).toBe("cpf"));
|
|
126
|
-
expect(result.current.step).toBe("dados");
|
|
127
|
-
expect(savedContacts()).toHaveLength(0);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("lets a buyer with a CPF on file continue without retyping it", async () => {
|
|
131
|
-
const { ports, savedContacts } = makePorts();
|
|
132
|
-
// Third argument: the buyer already gave the store a CPF, so Dados was
|
|
133
|
-
// skipped and they reopened it through "Alterar". The field starts empty
|
|
134
|
-
// (the client never receives the saved value) — requiring one here stranded
|
|
135
|
-
// them on a form they had no way to satisfy.
|
|
136
|
-
const { result } = renderHook(() => useCheckoutController(ports, undefined, true));
|
|
137
|
-
|
|
138
|
-
act(() => {
|
|
139
|
-
result.current.setStep("dados");
|
|
140
|
-
});
|
|
141
|
-
act(() => {
|
|
142
|
-
result.current.goToPayment();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
await waitFor(() => expect(result.current.step).toBe("payment"));
|
|
146
|
-
expect(result.current.errorField).toBeNull();
|
|
147
|
-
// Nothing typed ⇒ the port carries no values at all. What the HOST must do
|
|
148
|
-
// with a blank CPF (omit it so it can never clear the stored one) is the
|
|
149
|
-
// host's contract, pinned beside its `saveBuyerContact` implementation.
|
|
150
|
-
expect(savedContacts()[0]).toEqual({ name: undefined, phone: undefined, taxId: undefined });
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("still demands a CPF from a buyer who has none on file", async () => {
|
|
154
|
-
const { ports, savedContacts } = makePorts();
|
|
155
|
-
const { result } = renderHook(() => useCheckoutController(ports));
|
|
156
|
-
|
|
157
|
-
act(() => {
|
|
158
|
-
result.current.goToPayment();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await waitFor(() => expect(result.current.errorField).toBe("cpf"));
|
|
162
|
-
expect(result.current.step).toBe("dados");
|
|
163
|
-
expect(savedContacts()).toHaveLength(0);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* The second-host proof (FUT-564 acceptance).
|
|
4
|
-
*
|
|
5
|
-
* This package's dependency graph contains NO `@12-apps/ui` — it is not even
|
|
6
|
-
* resolvable from here — so a green run of this suite IS the proof that a host
|
|
7
|
-
* without this repo's design system can mount the buyer checkout: every pixel
|
|
8
|
-
* below renders through the raw-MUI default slots (`mui-defaults.tsx`),
|
|
9
|
-
* because no `components` prop is passed and no provider sits above the flow.
|
|
10
|
-
*
|
|
11
|
-
* The walk is the buyer's own: Dados (CPF form, consent checkbox, pay bar) →
|
|
12
|
-
* Pagamento (method picker) → the card form, with the order raised through the
|
|
13
|
-
* host port. The load-bearing test ids must survive the default slots too —
|
|
14
|
-
* they are the same hooks the storefront journeys click.
|
|
15
|
-
*/
|
|
16
|
-
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
17
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
18
|
-
|
|
19
|
-
import { CheckoutFlow } from "../checkout-flow";
|
|
20
|
-
import type { CheckoutOrder, CreateOrderRequest, CreateOrderResult } from "../types";
|
|
21
|
-
|
|
22
|
-
afterEach(cleanup);
|
|
23
|
-
|
|
24
|
-
const CARD_ORDER: CheckoutOrder = {
|
|
25
|
-
orderId: "o-2nd-host",
|
|
26
|
-
status: "AWAITING_PAYMENT",
|
|
27
|
-
method: "CARD",
|
|
28
|
-
totalCents: 1000,
|
|
29
|
-
subtotalCents: 1000,
|
|
30
|
-
discountTotalCents: 0,
|
|
31
|
-
appliedDiscounts: [],
|
|
32
|
-
totalLabel: "R$ 10,00",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** A valid CPF (passes the client-side check digits). */
|
|
36
|
-
const VALID_CPF = "52998224725";
|
|
37
|
-
|
|
38
|
-
describe("CheckoutFlow with no design system present (raw-MUI default slots)", () => {
|
|
39
|
-
it("walks Dados → Pagamento → card form through the default slots", async () => {
|
|
40
|
-
const createOrder = vi.fn<(input: CreateOrderRequest) => Promise<CreateOrderResult>>(
|
|
41
|
-
async () => ({ ok: true, data: CARD_ORDER }),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
render(
|
|
45
|
-
<CheckoutFlow
|
|
46
|
-
cart={{ empty: false, totalLabel: "R$ 10,00", totalItems: 1 }}
|
|
47
|
-
createOrder={createOrder}
|
|
48
|
-
onExitToMenu={vi.fn()}
|
|
49
|
-
/>,
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
// Dados renders through the defaults: stepper, CPF field, consent, pay bar.
|
|
53
|
-
expect(screen.getByTestId("checkout-stepper")).toBeTruthy();
|
|
54
|
-
expect(screen.getByTestId("buyer-save-profile")).toBeTruthy();
|
|
55
|
-
expect(screen.getByTestId("checkout-pay-bar")).toBeTruthy();
|
|
56
|
-
|
|
57
|
-
fireEvent.change(screen.getByTestId("buyer-cpf"), { target: { value: VALID_CPF } });
|
|
58
|
-
fireEvent.click(screen.getByTestId("checkout-continue"));
|
|
59
|
-
|
|
60
|
-
// Pagamento: the method picker, with both in-browser methods on offer.
|
|
61
|
-
await waitFor(() => expect(screen.getByTestId("checkout-method")).toBeTruthy());
|
|
62
|
-
expect(screen.getByTestId("checkout-method-PIX")).toBeTruthy();
|
|
63
|
-
|
|
64
|
-
// Choosing card auto-raises the order through the HOST port…
|
|
65
|
-
fireEvent.click(screen.getByTestId("checkout-method-CARD"));
|
|
66
|
-
await waitFor(() => expect(createOrder).toHaveBeenCalledTimes(1));
|
|
67
|
-
expect(createOrder.mock.calls[0]?.[0]).toMatchObject({ method: "CARD", saveProfile: true });
|
|
68
|
-
|
|
69
|
-
// …and the card form renders, again through the default slots.
|
|
70
|
-
await waitFor(() => expect(screen.getByTestId("card-number")).toBeTruthy());
|
|
71
|
-
expect(screen.getByTestId("card-holder")).toBeTruthy();
|
|
72
|
-
expect(screen.getByTestId("card-pay")).toBeTruthy();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("shows the empty-cart state for a cart with nothing to pay", () => {
|
|
76
|
-
render(
|
|
77
|
-
<CheckoutFlow
|
|
78
|
-
cart={{ empty: true, totalLabel: "R$ 0,00", totalItems: 0 }}
|
|
79
|
-
createOrder={vi.fn()}
|
|
80
|
-
onExitToMenu={vi.fn()}
|
|
81
|
-
/>,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
expect(screen.getByTestId("checkout-empty")).toBeTruthy();
|
|
85
|
-
});
|
|
86
|
-
});
|