@12-apps/payments-frontend 1.4.0 → 1.4.2

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 (24) hide show
  1. package/package.json +13 -5
  2. package/eslint.config.js +0 -34
  3. package/src/__tests__/checkout-confirmation.test.tsx +0 -177
  4. package/src/__tests__/connection-state.test.tsx +0 -84
  5. package/src/__tests__/context.test.tsx +0 -88
  6. package/src/__tests__/controlled-provider.test.tsx +0 -116
  7. package/src/__tests__/credential-confirm.test.tsx +0 -193
  8. package/src/__tests__/initial-provider.test.tsx +0 -81
  9. package/src/__tests__/provider-priority-list.test.tsx +0 -159
  10. package/src/__tests__/provider-status-bar.test.tsx +0 -152
  11. package/src/__tests__/slugged-provider.test.tsx +0 -108
  12. package/src/__tests__/verification-slot.test.tsx +0 -125
  13. package/src/card/tokenize.test.ts +0 -194
  14. package/src/components/checkout/__tests__/card-3ds-handover.test.tsx +0 -147
  15. package/src/components/checkout/__tests__/chain-instruments.test.tsx +0 -244
  16. package/src/components/checkout/__tests__/clear-cart-on-paid.test.tsx +0 -64
  17. package/src/components/checkout/__tests__/hosted-return.test.ts +0 -109
  18. package/src/components/checkout/__tests__/method-capability.test.tsx +0 -120
  19. package/src/components/checkout/__tests__/mint-deadline.test.ts +0 -99
  20. package/src/components/checkout/__tests__/payments-unavailable.test.tsx +0 -53
  21. package/src/components/checkout/__tests__/provider-chain.test.ts +0 -161
  22. package/src/components/checkout/__tests__/save-on-continue.test.tsx +0 -165
  23. package/src/components/checkout/__tests__/second-host.test.tsx +0 -86
  24. package/src/components/checkout/__tests__/unresolved-charge.test.tsx +0 -155
@@ -1,125 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { cleanup, render, screen, waitFor } from '@testing-library/react';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
-
5
- import type { MerchantSettingsView } from '@12-apps/payments-backend';
6
-
7
- import type { PaymentsSettingsClient } from '../client';
8
- import { PaymentProviderSettings } from '../components/PaymentProviderSettings';
9
-
10
- /**
11
- * The activation step's slot.
12
- *
13
- * "Connected" and "can charge" are different facts, and only the HOST can prove
14
- * the second — it owns the endpoint that puts a real cent through the store's
15
- * account. This package's job is narrower and worth pinning: place that step
16
- * where an owner will actually find it, and tell the host which provider is
17
- * open and whether it is already live.
18
- */
19
-
20
- const PROVIDERS = [
21
- { name: 'pagbank', displayName: 'PagBank', authMode: 'oauth', credentialSchema: [] },
22
- ];
23
-
24
- function viewWith(config: Record<string, unknown> | null): MerchantSettingsView {
25
- return {
26
- providers: PROVIDERS,
27
- configs: config ? [config] : [],
28
- activeProvider: null,
29
- } as unknown as MerchantSettingsView;
30
- }
31
-
32
- function fakeClient(view: MerchantSettingsView): PaymentsSettingsClient {
33
- return {
34
- getSettings: vi.fn().mockResolvedValue(view),
35
- getSetupGuide: vi.fn().mockResolvedValue(null),
36
- setEnabled: vi.fn(),
37
- saveCredentials: vi.fn(),
38
- } as unknown as PaymentsSettingsClient;
39
- }
40
-
41
- afterEach(cleanup);
42
-
43
- describe('PaymentProviderSettings — the verification slot', () => {
44
- it('is not rendered while the owner is still on the provider list', async () => {
45
- const renderVerification = vi.fn(() => <div data-testid="slot" />);
46
- render(
47
- <PaymentProviderSettings
48
- client={fakeClient(viewWith(null))}
49
- renderVerification={renderVerification}
50
- />,
51
- );
52
-
53
- await screen.findByTestId('payments-provider-picker');
54
- expect(renderVerification).not.toHaveBeenCalled();
55
- });
56
-
57
- it('hands the host the open provider and its connection state', async () => {
58
- const renderVerification = vi.fn(() => <div data-testid="slot" />);
59
- render(
60
- <PaymentProviderSettings
61
- client={fakeClient(
62
- viewWith({ provider: 'pagbank', status: 'VERIFIED', enabled: true, environments: {} }),
63
- )}
64
- initialProvider="pagbank"
65
- renderVerification={renderVerification}
66
- />,
67
- );
68
-
69
- await screen.findByTestId('slot');
70
- expect(renderVerification).toHaveBeenCalledWith(
71
- expect.objectContaining({ provider: 'pagbank', connected: true }),
72
- );
73
- /**
74
- * `enabled` is deliberately withheld. The step used it to render a
75
- * standing "Provedor ativo" banner, which went on reassuring a store whose
76
- * every charge was refused — the banner and the refusal were on screen
77
- * together. The step reports what it observes, nothing else.
78
- */
79
- expect(renderVerification).not.toHaveBeenCalledWith(
80
- expect.objectContaining({ enabled: expect.anything() }),
81
- );
82
- });
83
-
84
- /**
85
- * A provider with no stored row yet still renders the slot — the host decides
86
- * to show nothing. Reporting `connected: false` is what lets it.
87
- */
88
- it('reports a provider with no stored connection as not connected', async () => {
89
- const renderVerification = vi.fn(() => <div data-testid="slot" />);
90
- render(
91
- <PaymentProviderSettings
92
- client={fakeClient(viewWith(null))}
93
- initialProvider="pagbank"
94
- renderVerification={renderVerification}
95
- />,
96
- );
97
-
98
- await screen.findByTestId('slot');
99
- expect(renderVerification).toHaveBeenCalledWith(
100
- expect.objectContaining({ connected: false }),
101
- );
102
- });
103
-
104
- /**
105
- * Placement, not just presence: the step must sit OUTSIDE the manual
106
- * credentials disclosure. An owner who connected by OAuth is told no key
107
- * needs copying, so they have no reason to open it — and the step that
108
- * actually turns the store on would be hidden in there.
109
- */
110
- it('places the step outside the manual-credentials disclosure', async () => {
111
- render(
112
- <PaymentProviderSettings
113
- client={fakeClient(
114
- viewWith({ provider: 'pagbank', status: 'VERIFIED', enabled: false, environments: {} }),
115
- )}
116
- initialProvider="pagbank"
117
- renderVerification={() => <div data-testid="slot" />}
118
- />,
119
- );
120
-
121
- const slot = await screen.findByTestId('slot');
122
- await waitFor(() => expect(screen.queryByTestId('payments-manual-fallback')).toBeNull());
123
- expect(slot.closest('[data-testid="payments-manual-fallback"]')).toBeNull();
124
- });
125
- });
@@ -1,194 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { afterEach, describe, expect, it, vi } from "vitest";
3
-
4
- import { tokenizeForCheckout, tokenizerFor } from "./tokenize";
5
- import type { CardDetails } from "./types";
6
-
7
- /** A Luhn-valid Visa test PAN (the mock path validates before minting). */
8
- const VALID_CARD: CardDetails = {
9
- number: "4111 1111 1111 1111",
10
- holder: "Ana Compradora",
11
- expiry: "12/39",
12
- cvv: "123",
13
- };
14
-
15
- /** The always-declines test PAN the mock encodes as `tok_declined…`. */
16
- const DECLINE_CARD: CardDetails = { ...VALID_CARD, number: "4000 0000 0000 0002" };
17
-
18
- describe("tokenizeForCheckout — the mock gate (FUT-697)", () => {
19
- it("NEVER mints a mock token without server-granted stub permission", async () => {
20
- // The exact production shape of the bug: a Stone/Stripe store resolved to
21
- // a null PagBank key, and the old fallback minted a fake `tok_…` that
22
- // entered a real charge. With no `mockTokenization` grant, the answer is
23
- // a clear bail BEFORE any money moves.
24
- const result = await tokenizeForCheckout(VALID_CARD, {
25
- provider: "stripe",
26
- publicKey: null,
27
- mockTokenization: false,
28
- });
29
- expect(result.ok).toBe(false);
30
- if (!result.ok) expect(result.error).toContain("indisponível");
31
- });
32
-
33
- it("bails clearly for a provider with no browser scheme, even WITH a key", async () => {
34
- const result = await tokenizeForCheckout(VALID_CARD, {
35
- provider: "infinitepay",
36
- publicKey: "pk_live_123",
37
- mockTokenization: false,
38
- });
39
- expect(result.ok).toBe(false);
40
- if (!result.ok) expect(result.error).toContain("indisponível");
41
- });
42
-
43
- it("bails clearly when the store has no provider at all", async () => {
44
- const result = await tokenizeForCheckout(VALID_CARD, {
45
- provider: null,
46
- publicKey: null,
47
- mockTokenization: false,
48
- });
49
- expect(result.ok).toBe(false);
50
- if (!result.ok) expect(result.error).toContain("indisponível");
51
- });
52
-
53
- it("mints the mock token under stub permission (dev / e2e path)", async () => {
54
- const result = await tokenizeForCheckout(VALID_CARD, {
55
- provider: "pagbank",
56
- publicKey: null,
57
- mockTokenization: true,
58
- });
59
- expect(result.ok).toBe(true);
60
- if (result.ok) {
61
- expect(result.data.token).toMatch(/^tok/);
62
- expect(result.data.token).not.toMatch(/^tok_declined/);
63
- expect(result.data.last4).toBe("1111");
64
- }
65
- });
66
-
67
- it("encodes the decline scenario in the mock token", async () => {
68
- const result = await tokenizeForCheckout(DECLINE_CARD, {
69
- provider: "pagbank",
70
- publicKey: null,
71
- mockTokenization: true,
72
- });
73
- expect(result.ok).toBe(true);
74
- if (result.ok) expect(result.data.token).toMatch(/^tok_declined/);
75
- });
76
-
77
- it("still validates the card before minting a mock", async () => {
78
- const result = await tokenizeForCheckout(
79
- { ...VALID_CARD, number: "1234" },
80
- { provider: "pagbank", publicKey: null, mockTokenization: true },
81
- );
82
- expect(result.ok).toBe(false);
83
- });
84
-
85
- it("routes a keyed PagBank store to the REAL tokenizer — a key beats stub permission", async () => {
86
- // Stub the SDK so `ensurePagBankSdk` short-circuits (no script injection in
87
- // the test env). The encrypted blob coming back — never a `tok_…` mock —
88
- // is the proof that a present key routes to REAL tokenization even when
89
- // stub permission was granted.
90
- const pagSeguro = {
91
- encryptCard: () => ({ hasErrors: false, encryptedCard: "enc-blob-123" }),
92
- };
93
- (window as typeof window & { PagSeguro?: typeof pagSeguro }).PagSeguro = pagSeguro;
94
- try {
95
- const result = await tokenizeForCheckout(VALID_CARD, {
96
- provider: "pagbank",
97
- publicKey: "PUB-KEY",
98
- mockTokenization: true, // even with stub permission, a key means REAL tokenization
99
- });
100
- expect(result.ok).toBe(true);
101
- if (result.ok) {
102
- expect(result.data.token).toBe("enc-blob-123");
103
- expect(result.data.token).not.toMatch(/^tok/);
104
- }
105
- } finally {
106
- delete (window as typeof window & { PagSeguro?: typeof pagSeguro }).PagSeguro;
107
- }
108
- });
109
- });
110
-
111
- describe("tokenizerFor — scheme is chosen by provider, never by capability", () => {
112
- it("names each implemented provider's own scheme", () => {
113
- expect(tokenizerFor("pagbank")).toBe("pagbank-sdk");
114
- expect(tokenizerFor("stone")).toBe("pagarme-token");
115
- expect(tokenizerFor("stripe")).toBe("stripe-pm");
116
- });
117
-
118
- it("answers null for providers with no browser card path", () => {
119
- expect(tokenizerFor("infinitepay")).toBeNull();
120
- expect(tokenizerFor("unknown-vendor")).toBeNull();
121
- });
122
- });
123
-
124
- describe("tokenizeForCheckout — the stripe-pm scheme (FUT-698)", () => {
125
- afterEach(() => {
126
- vi.unstubAllGlobals();
127
- });
128
-
129
- /** Stub fetch once and capture the single call the tokenizer makes. */
130
- function stubStripe(status: number, body: unknown): { url?: string; init?: RequestInit }[] {
131
- const calls: { url?: string; init?: RequestInit }[] = [];
132
- vi.stubGlobal(
133
- "fetch",
134
- vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
135
- calls.push({ url: String(url), init });
136
- return new Response(JSON.stringify(body), { status });
137
- }),
138
- );
139
- return calls;
140
- }
141
-
142
- it("mints a PaymentMethod at Stripe with the publishable key — the PAN never reaches us", async () => {
143
- const calls = stubStripe(200, { id: "pm_123", card: { brand: "visa", last4: "1111" } });
144
-
145
- const result = await tokenizeForCheckout(VALID_CARD, {
146
- provider: "stripe",
147
- publicKey: "pk_test_1",
148
- mockTokenization: false,
149
- });
150
-
151
- expect(result.ok).toBe(true);
152
- if (result.ok) {
153
- expect(result.data.token).toBe("pm_123");
154
- expect(result.data.last4).toBe("1111");
155
- }
156
- expect(calls[0]?.url).toBe("https://api.stripe.com/v1/payment_methods");
157
- const headers = calls[0]?.init?.headers as Record<string, string>;
158
- expect(headers["Authorization"]).toBe("Bearer pk_test_1");
159
- const form = new URLSearchParams(String(calls[0]?.init?.body));
160
- expect(form.get("type")).toBe("card");
161
- expect(form.get("card[number]")).toBe("4111111111111111");
162
- expect(form.get("card[exp_month]")).toBe("12");
163
- expect(form.get("card[exp_year]")).toBe("2039");
164
- });
165
-
166
- it("carries Stripe's refusal verbatim — the provider's answer must reach a human", async () => {
167
- stubStripe(402, { error: { message: "Your card number is incorrect." } });
168
-
169
- const result = await tokenizeForCheckout(VALID_CARD, {
170
- provider: "stripe",
171
- publicKey: "pk_test_1",
172
- mockTokenization: false,
173
- });
174
-
175
- expect(result.ok).toBe(false);
176
- if (!result.ok) {
177
- expect(result.error).toContain("HTTP 402");
178
- expect(result.error).toContain("Your card number is incorrect.");
179
- }
180
- });
181
-
182
- it("a keyed Stripe store routes to the REAL tokenizer — a key beats stub permission", async () => {
183
- stubStripe(200, { id: "pm_real" });
184
-
185
- const result = await tokenizeForCheckout(VALID_CARD, {
186
- provider: "stripe",
187
- publicKey: "pk_test_1",
188
- mockTokenization: true, // even with stub permission, a key means REAL tokenization
189
- });
190
-
191
- expect(result.ok).toBe(true);
192
- if (result.ok) expect(result.data.token).toBe("pm_real");
193
- });
194
- });
@@ -1,147 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * FUT-698 — "Dado uma loja Stripe, quando o comprador paga com um cartão que
4
- * exige 3DS, então ele é levado ao desafio" — the browser-unreachable half of
5
- * that scenario (the stub adapter settles cards inline, so no e2e journey can
6
- * produce a real challenge). The return half — "e volta com o pedido pago
7
- * exatamente uma vez" — is the hosted-return machinery, pinned in
8
- * hosted-return.test.ts and settled server-side by the idempotent webhook path.
9
- *
10
- * The charge answers `hostedCheckoutUrl` and the card hook must hand the buyer
11
- * over exactly as a redirect provider's link does (FUT-556): park the order,
12
- * navigate, and start NO poll in a tab that is being torn down.
13
- */
14
- import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
15
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
-
17
- import type { JSX } from "react";
18
-
19
- const client = vi.hoisted(() => ({
20
- chargeCard: vi.fn(),
21
- listSavedCards: vi.fn(),
22
- refreshCardPublicKey: vi.fn(),
23
- pollOrderStatus: vi.fn(),
24
- fetchCheckoutConfig: vi.fn(),
25
- }));
26
-
27
- vi.mock("../client", () => client);
28
-
29
- import type { CardTokenizationConfig } from "../../../card";
30
- import type { CheckoutOrder, OrderStatus } from "../types";
31
- import { useCardCheckout } from "../use-card-checkout";
32
-
33
- const ORDER: CheckoutOrder = {
34
- orderId: "o1",
35
- status: "AWAITING_PAYMENT",
36
- method: "CARD",
37
- totalCents: 1250,
38
- subtotalCents: 1250,
39
- discountTotalCents: 0,
40
- appliedDiscounts: [],
41
- totalLabel: "R$ 12,50",
42
- };
43
-
44
- const STRIPE_CONFIG: CardTokenizationConfig = {
45
- provider: "stripe",
46
- publicKey: "pk_test_1",
47
- mockTokenization: false,
48
- };
49
-
50
- /** A saved card, so paying skips tokenization and goes straight to the charge. */
51
- const SAVED_CARD = {
52
- id: "card-1",
53
- brand: "visa",
54
- last4: "4242",
55
- expMonth: 12,
56
- expYear: 2033,
57
- holder: "OLGA STONE",
58
- };
59
-
60
- function Harness({ onResolved }: { onResolved: (status: OrderStatus) => void }): JSX.Element {
61
- const cc = useCardCheckout(ORDER, {}, STRIPE_CONFIG, onResolved, 10, "acme");
62
- return (
63
- <div>
64
- <span data-testid="submitted">{String(cc.submitted)}</span>
65
- <span data-testid="selection">{cc.selection}</span>
66
- <button type="button" data-testid="pay" onClick={() => void cc.handlePay()}>
67
- Pagar
68
- </button>
69
- </div>
70
- );
71
- }
72
-
73
- /** Render, wait for the saved card to be SELECTED, and tap Pagar. */
74
- async function pay(onResolved = vi.fn()): Promise<void> {
75
- render(<Harness onResolved={onResolved} />);
76
- // Selection — not merely the fetch — is what routes handlePay past the
77
- // empty new-card form; clicking earlier validates that form and stops.
78
- await waitFor(() => {
79
- expect(screen.getByTestId("selection").textContent).toBe("card-1");
80
- });
81
- fireEvent.click(screen.getByTestId("pay"));
82
- }
83
-
84
- const assign = vi.fn();
85
-
86
- beforeEach(() => {
87
- vi.clearAllMocks();
88
- window.sessionStorage.clear();
89
- client.listSavedCards.mockResolvedValue([SAVED_CARD]);
90
- client.pollOrderStatus.mockResolvedValue({ ok: true, data: "AWAITING_PAYMENT" });
91
- vi.stubGlobal("location", { search: "", assign });
92
- });
93
-
94
- afterEach(() => {
95
- cleanup();
96
- vi.unstubAllGlobals();
97
- window.sessionStorage.clear();
98
- });
99
-
100
- describe("card charge → 3DS handover (FUT-698)", () => {
101
- it("hands the buyer to the challenge page with the order parked for the return trip", async () => {
102
- client.chargeCard.mockResolvedValue({
103
- ok: true,
104
- data: { status: "AWAITING_PAYMENT", hostedCheckoutUrl: "https://hooks.stripe.com/3ds/x" },
105
- });
106
- const onResolved = vi.fn();
107
-
108
- await pay(onResolved);
109
-
110
- await waitFor(() => {
111
- expect(assign).toHaveBeenCalledWith("https://hooks.stripe.com/3ds/x");
112
- });
113
- // Parked exactly as the redirect-provider handoff parks (FUT-556), so the
114
- // return trip resumes THIS order's confirmation screen.
115
- const parked = window.sessionStorage.getItem("futurepay.checkout.hostedOrder");
116
- expect(parked).not.toBeNull();
117
- expect((JSON.parse(parked as string) as { orderId: string }).orderId).toBe("o1");
118
- // The tab is navigating away: nothing resolved, no poll started here.
119
- expect(onResolved).not.toHaveBeenCalled();
120
- expect(client.pollOrderStatus).not.toHaveBeenCalled();
121
- expect(screen.getByTestId("submitted").textContent).toBe("false");
122
- });
123
-
124
- it("an accepted charge WITHOUT a challenge begins polling instead of navigating", async () => {
125
- client.chargeCard.mockResolvedValue({ ok: true, data: { status: "AWAITING_PAYMENT" } });
126
-
127
- await pay();
128
-
129
- await waitFor(() => {
130
- expect(screen.getByTestId("submitted").textContent).toBe("true");
131
- });
132
- expect(assign).not.toHaveBeenCalled();
133
- expect(window.sessionStorage.getItem("futurepay.checkout.hostedOrder")).toBeNull();
134
- });
135
-
136
- it("a decline resolves to the status screen, never a handover", async () => {
137
- client.chargeCard.mockResolvedValue({ ok: true, data: { status: "FAILED" } });
138
- const onResolved = vi.fn();
139
-
140
- await pay(onResolved);
141
-
142
- await waitFor(() => {
143
- expect(onResolved).toHaveBeenCalledWith("FAILED");
144
- });
145
- expect(assign).not.toHaveBeenCalled();
146
- });
147
- });