@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.
Files changed (34) hide show
  1. package/package.json +13 -5
  2. package/src/card/stripe-token.ts +3 -0
  3. package/src/card/tokenize.ts +17 -3
  4. package/src/components/checkout/card-instruments.ts +222 -0
  5. package/src/components/checkout/card-view.tsx +56 -6
  6. package/src/components/checkout/checkout-flow.tsx +1 -0
  7. package/src/components/checkout/checkout-steps.tsx +13 -54
  8. package/src/components/checkout/client.ts +21 -3
  9. package/src/components/checkout/failure-codes.ts +17 -0
  10. package/src/components/checkout/method-capability.ts +83 -9
  11. package/src/components/checkout/payment-error-panel.tsx +97 -0
  12. package/src/components/checkout/types.ts +39 -4
  13. package/src/components/checkout/use-card-checkout.ts +77 -91
  14. package/src/components/checkout/use-checkout-controller.ts +33 -6
  15. package/src/result.ts +15 -2
  16. package/eslint.config.js +0 -34
  17. package/src/__tests__/checkout-confirmation.test.tsx +0 -177
  18. package/src/__tests__/connection-state.test.tsx +0 -84
  19. package/src/__tests__/context.test.tsx +0 -88
  20. package/src/__tests__/controlled-provider.test.tsx +0 -116
  21. package/src/__tests__/credential-confirm.test.tsx +0 -193
  22. package/src/__tests__/initial-provider.test.tsx +0 -81
  23. package/src/__tests__/provider-priority-list.test.tsx +0 -159
  24. package/src/__tests__/provider-status-bar.test.tsx +0 -152
  25. package/src/__tests__/slugged-provider.test.tsx +0 -108
  26. package/src/__tests__/verification-slot.test.tsx +0 -125
  27. package/src/card/tokenize.test.ts +0 -194
  28. package/src/components/checkout/__tests__/card-3ds-handover.test.tsx +0 -147
  29. package/src/components/checkout/__tests__/clear-cart-on-paid.test.tsx +0 -64
  30. package/src/components/checkout/__tests__/hosted-return.test.ts +0 -109
  31. package/src/components/checkout/__tests__/method-capability.test.tsx +0 -120
  32. package/src/components/checkout/__tests__/payments-unavailable.test.tsx +0 -53
  33. package/src/components/checkout/__tests__/save-on-continue.test.tsx +0 -165
  34. 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
- });