@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,81 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { render, screen, waitFor } from '@testing-library/react';
3
- import { 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
- * Landing after an OAuth connect.
12
- *
13
- * This shipped broken TWICE, and the second time only because the test was
14
- * shaped around the code instead of the failure. The host reads the connected
15
- * provider from the callback's query string in an EFFECT, so it is null on
16
- * first render and arrives a tick later — and `useState(initialProvider)` keeps
17
- * the null forever. A test that renders with the prop already set passes
18
- * happily and proves nothing; the prop has to ARRIVE.
19
- */
20
-
21
- const VIEW: MerchantSettingsView = {
22
- providers: [
23
- { name: 'pagbank', displayName: 'PagBank', authMode: 'oauth', credentialSchema: [] },
24
- { name: 'stripe', displayName: 'Stripe', authMode: 'credentials', credentialSchema: [] },
25
- ],
26
- configs: [],
27
- activeProvider: null,
28
- } as unknown as MerchantSettingsView;
29
-
30
- function fakeClient(): PaymentsSettingsClient {
31
- return {
32
- getSettings: vi.fn().mockResolvedValue(VIEW),
33
- getSetupGuide: vi.fn().mockResolvedValue(null),
34
- setEnabled: vi.fn(),
35
- saveCredentials: vi.fn(),
36
- } as unknown as PaymentsSettingsClient;
37
- }
38
-
39
- describe('PaymentProviderSettings — landing after a connect', () => {
40
- it('lands on the provider list when nothing was just connected', async () => {
41
- render(<PaymentProviderSettings client={fakeClient()} />);
42
- expect(await screen.findByTestId('payments-provider-picker')).toBeDefined();
43
- });
44
-
45
- /**
46
- * The regression: the prop is null at mount and supplied afterwards, exactly
47
- * as the host does it. Seeding `useState` with it passes the OTHER test and
48
- * fails this one.
49
- */
50
- it('opens the provider when the host supplies it AFTER mount', async () => {
51
- const client = fakeClient();
52
- const { rerender } = render(<PaymentProviderSettings client={client} initialProvider={null} />);
53
- await screen.findByTestId('payments-provider-picker');
54
-
55
- rerender(<PaymentProviderSettings client={client} initialProvider="pagbank" />);
56
-
57
- // The picker is replaced by that provider's own panel.
58
- await waitFor(() => expect(screen.queryByTestId('payments-provider-picker')).toBeNull());
59
- expect(screen.getByTestId('payments-provider-back')).toBeDefined();
60
- });
61
-
62
- /**
63
- * Applied once, not on every render: the host holds the outcome for the
64
- * page's lifetime, so re-applying would make "Voltar aos provedores" bounce
65
- * straight back into the provider and trap the owner on one screen.
66
- */
67
- it('does not re-open the provider after the owner goes back', async () => {
68
- const client = fakeClient();
69
- const { rerender } = render(<PaymentProviderSettings client={client} initialProvider={null} />);
70
- await screen.findByTestId('payments-provider-picker');
71
-
72
- rerender(<PaymentProviderSettings client={client} initialProvider="pagbank" />);
73
- const back = await screen.findByTestId('payments-provider-back');
74
-
75
- back.click();
76
-
77
- // A rerender with the SAME still-set prop must not drag them back in.
78
- rerender(<PaymentProviderSettings client={client} initialProvider="pagbank" />);
79
- expect(await screen.findByTestId('payments-provider-picker')).toBeDefined();
80
- });
81
- });
@@ -1,159 +0,0 @@
1
- // @vitest-environment jsdom
2
- // fireEvent/render from @testing-library/react (act()-wrapped) — same choice
3
- // and reasoning as the context tests: user-event is not a dependency here.
4
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
5
- import { describe, expect, it, vi } from 'vitest';
6
-
7
- import type { MaskedProviderConfig, MerchantSettingsView } from '@12-apps/payments-backend';
8
-
9
- import type { PaymentsSettingsClient } from '../client';
10
- import { ProviderPriorityList, chainOf } from '../components/ProviderPriorityList';
11
-
12
- /**
13
- * The failover-chain editor. What matters here is not the drag mechanics but
14
- * that the order the merchant sees is the order checkout will use, and that a
15
- * failed save never leaves the screen claiming an order the server rejected.
16
- */
17
-
18
- function config(provider: string, enabled: boolean, priority: number): MaskedProviderConfig {
19
- return {
20
- provider,
21
- enabled,
22
- priority,
23
- environment: 'SANDBOX',
24
- status: 'VERIFIED',
25
- lastVerifiedAt: null,
26
- // Every row in the chain editor is by definition already proven — only a
27
- // successful charge can put a provider in the chain at all.
28
- chargeVerifiedAt: '2026-07-30T12:00:00.000Z',
29
- expiresAt: null,
30
- stub: false,
31
- environments: { SANDBOX: {}, PRODUCTION: {} },
32
- };
33
- }
34
-
35
- function viewOf(configs: MaskedProviderConfig[]): MerchantSettingsView {
36
- const chain = configs.filter((c) => c.enabled).map((c) => c.provider);
37
- return {
38
- providers: [
39
- { name: 'stone', displayName: 'Stone', urlSlug: 'stone', authMode: 'credentials', capabilities: CAPS, credentialSchema: [] },
40
- { name: 'stripe', displayName: 'Stripe', urlSlug: 'stripe', authMode: 'oauth', capabilities: CAPS, credentialSchema: [] },
41
- ],
42
- configs,
43
- providerChain: chain,
44
- activeProvider: chain[0] ?? null,
45
- failoverPolicy: 'TECHNICAL',
46
- };
47
- }
48
-
49
- const CAPS = {
50
- methods: ['PIX'] as const,
51
- savedCards: false,
52
- refunds: false,
53
- partialRefunds: false,
54
- splits: false,
55
- webhooks: true,
56
- tokenization: 'NONE' as const,
57
- };
58
-
59
- function fakeClient(overrides: Partial<PaymentsSettingsClient> = {}): PaymentsSettingsClient {
60
- return {
61
- getSettings: vi.fn(),
62
- saveCredentials: vi.fn(),
63
- setEnabled: vi.fn(),
64
- setPriorities: vi.fn(),
65
- setFailoverPolicy: vi.fn(),
66
- verify: vi.fn(),
67
- getSetupGuide: vi.fn(),
68
- beginOAuth: vi.fn(),
69
- disconnectOAuth: vi.fn(),
70
- ...overrides,
71
- } as PaymentsSettingsClient;
72
- }
73
-
74
- /** The rendered chain order, read off the list rows. */
75
- function rowOrder(): string[] {
76
- return Array.from(document.querySelectorAll('[data-testid^="payments-priority-item-"]')).map(
77
- (el) => el.getAttribute('data-testid')!.replace('payments-priority-item-', ''),
78
- );
79
- }
80
-
81
- describe('chainOf', () => {
82
- it('derives the chain from configs, ordered by rank', () => {
83
- const view = viewOf([config('stripe', true, 1), config('stone', true, 0)]);
84
- expect(chainOf(view)).toEqual(['stone', 'stripe']);
85
- });
86
-
87
- it('ignores providers that are configured but out of rotation', () => {
88
- const view = viewOf([config('stone', true, 0), config('stripe', false, 0)]);
89
- expect(chainOf(view)).toEqual(['stone']);
90
- });
91
- });
92
-
93
- describe('ProviderPriorityList', () => {
94
- it('names the provider checkout will try first', () => {
95
- render(
96
- <ProviderPriorityList
97
- view={viewOf([config('stone', true, 0), config('stripe', true, 1)])}
98
- client={fakeClient()}
99
- />,
100
- );
101
- // The intro names it in prose AND the row is first in the list; assert on
102
- // the list, which is the thing that actually encodes routing order.
103
- expect(screen.getByTestId('payments-priority-first')).toBeTruthy();
104
- expect(rowOrder()).toEqual(['stone', 'stripe']);
105
- });
106
-
107
- it('warns when nothing is in rotation, because checkout cannot charge', () => {
108
- render(<ProviderPriorityList view={viewOf([config('stone', false, 0)])} client={fakeClient()} />);
109
- expect(screen.getByTestId('payments-priority-empty')).toBeTruthy();
110
- });
111
-
112
- it('saves the WHOLE reordered chain, not a per-provider delta', async () => {
113
- const reordered = viewOf([config('stripe', true, 0), config('stone', true, 1)]);
114
- const setPriorities = vi.fn().mockResolvedValue(reordered);
115
- render(
116
- <ProviderPriorityList
117
- view={viewOf([config('stone', true, 0), config('stripe', true, 1)])}
118
- client={fakeClient({ setPriorities })}
119
- />,
120
- );
121
-
122
- fireEvent.click(screen.getByLabelText('Mover Stone para baixo'));
123
-
124
- await waitFor(() => {
125
- expect(setPriorities).toHaveBeenCalledWith(['stripe', 'stone']);
126
- });
127
- });
128
-
129
- it('rolls the list back when the save fails, and says why', async () => {
130
- const setPriorities = vi.fn().mockRejectedValue(new Error('conflito de ordem'));
131
- render(
132
- <ProviderPriorityList
133
- view={viewOf([config('stone', true, 0), config('stripe', true, 1)])}
134
- client={fakeClient({ setPriorities })}
135
- />,
136
- );
137
-
138
- fireEvent.click(screen.getByLabelText('Mover Stone para baixo'));
139
-
140
- await waitFor(() => {
141
- expect(screen.getByTestId('payments-priority-error').textContent).toContain(
142
- 'conflito de ordem',
143
- );
144
- });
145
- // The screen must not keep showing an order the server refused.
146
- expect(rowOrder()).toEqual(['stone', 'stripe']);
147
- });
148
-
149
- it('cannot move the first provider up or the last one down', () => {
150
- render(
151
- <ProviderPriorityList
152
- view={viewOf([config('stone', true, 0), config('stripe', true, 1)])}
153
- client={fakeClient()}
154
- />,
155
- );
156
- expect(screen.getByLabelText('Mover Stone para cima').hasAttribute('disabled')).toBe(true);
157
- expect(screen.getByLabelText('Mover Stripe para baixo').hasAttribute('disabled')).toBe(true);
158
- });
159
- });
@@ -1,152 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { cleanup, render, screen } from '@testing-library/react';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
-
5
- import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments-backend';
6
-
7
- import { ProviderStatusBar } from '../components/ProviderStatusBar';
8
-
9
- /**
10
- * "Ativo" decides whether checkout routes real buyers to this provider, so it
11
- * must not be offerable before there is anything to route to. The screen shipped
12
- * showing `PagBank [UNVERIFIED] [on] Ativo` — a provider that had never proved
13
- * it could charge, presented as live.
14
- */
15
-
16
- /** A provider that CAN run the activation charge, so the strict rule applies. */
17
- const DESCRIPTOR = {
18
- name: 'pagbank',
19
- displayName: 'PagBank',
20
- capabilities: { activationCharge: true },
21
- } as unknown as ProviderDescriptor;
22
-
23
- /**
24
- * One that cannot: no browser tokenization is written for it, so there is no
25
- * way to earn `chargeVerifiedAt`. Holding it to the charge would not make
26
- * "Ativo" honest — it would make the provider permanently unactivatable.
27
- */
28
- const NO_CHARGE_PATH = {
29
- name: 'stripe',
30
- displayName: 'Stripe',
31
- capabilities: { activationCharge: false },
32
- } as unknown as ProviderDescriptor;
33
-
34
- function configWith(
35
- status: string,
36
- enabled = false,
37
- chargeVerifiedAt: string | null = null,
38
- ): MaskedProviderConfig {
39
- return {
40
- provider: 'pagbank',
41
- status,
42
- enabled,
43
- chargeVerifiedAt,
44
- } as unknown as MaskedProviderConfig;
45
- }
46
-
47
- const PROVEN = '2026-07-30T12:00:00.000Z';
48
-
49
- /**
50
- * jest-dom is not a dependency here, so assert the DOM property directly. The
51
- * testid lands on MUI's Switch ROOT; the thing that carries `disabled` is the
52
- * checkbox input inside it.
53
- */
54
- function toggle(): HTMLInputElement {
55
- const root = screen.getByTestId('payments-enabled-toggle');
56
- const input = root.querySelector('input[type="checkbox"]');
57
- if (!input) throw new Error('switch input not found');
58
- return input as HTMLInputElement;
59
- }
60
-
61
- afterEach(cleanup);
62
-
63
- describe('ProviderStatusBar — the Ativo switch', () => {
64
- it('cannot be switched on with no connection at all', () => {
65
- render(<ProviderStatusBar descriptor={DESCRIPTOR} config={null} busy={false} onToggle={vi.fn()} />);
66
- expect(toggle().disabled).toBe(true);
67
- });
68
-
69
- it('cannot be switched on while the provider is UNVERIFIED', () => {
70
- render(
71
- <ProviderStatusBar
72
- descriptor={DESCRIPTOR}
73
- config={configWith('UNVERIFIED')}
74
- busy={false}
75
- onToggle={vi.fn()}
76
- />,
77
- );
78
- expect(toggle().disabled).toBe(true);
79
- });
80
-
81
- /**
82
- * The one this exists for. `VERIFIED` comes from the credential probe, which
83
- * only asks whether the keys authenticate — PagBank answers yes to that and
84
- * still refuses every real charge until the integration is homologated. A
85
- * store sat at `PagBank [VERIFIED] [on] Ativo` while declining every shopper.
86
- */
87
- it('stays locked at VERIFIED — authenticating is not being able to charge', () => {
88
- render(
89
- <ProviderStatusBar
90
- descriptor={DESCRIPTOR}
91
- config={configWith('VERIFIED')}
92
- busy={false}
93
- onToggle={vi.fn()}
94
- />,
95
- );
96
- expect(toggle().disabled).toBe(true);
97
- });
98
-
99
- it('unlocks once a real charge has succeeded', () => {
100
- render(
101
- <ProviderStatusBar
102
- descriptor={DESCRIPTOR}
103
- config={configWith('VERIFIED', false, PROVEN)}
104
- busy={false}
105
- onToggle={vi.fn()}
106
- />,
107
- );
108
- expect(toggle().disabled).toBe(false);
109
- });
110
-
111
- /**
112
- * Turning OFF is never gated. A row enabled before this rule existed — or a
113
- * store in RECONNECT_REQUIRED that demonstrably worked — must be pullable out
114
- * of rotation at once, or the switch traps an owner with a broken provider
115
- * live in the chain.
116
- */
117
- it('lets an enabled-but-unproven provider be switched off', () => {
118
- render(
119
- <ProviderStatusBar
120
- descriptor={DESCRIPTOR}
121
- config={configWith('RECONNECT_REQUIRED', true)}
122
- busy={false}
123
- onToggle={vi.fn()}
124
- />,
125
- );
126
- expect(toggle().disabled).toBe(false);
127
- });
128
-
129
- it('falls back to VERIFIED for a provider that cannot run the charge', () => {
130
- render(
131
- <ProviderStatusBar
132
- descriptor={NO_CHARGE_PATH}
133
- config={configWith('VERIFIED')}
134
- busy={false}
135
- onToggle={vi.fn()}
136
- />,
137
- );
138
- expect(toggle().disabled).toBe(false);
139
- });
140
-
141
- it('is locked while another action is in flight', () => {
142
- render(
143
- <ProviderStatusBar
144
- descriptor={DESCRIPTOR}
145
- config={configWith('VERIFIED', false, PROVEN)}
146
- busy
147
- onToggle={vi.fn()}
148
- />,
149
- );
150
- expect(toggle().disabled).toBe(true);
151
- });
152
- });
@@ -1,108 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { render, screen, waitFor } from '@testing-library/react';
3
- import { 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
- * How a provider is spelled in a URL is the ADAPTER's declaration (FUT-557),
12
- * carried in the catalog as `urlSlug`. A controlled host passes its path
13
- * segment verbatim and writes back whatever `onProviderChange` reports, so
14
- * it holds no map of providers: resolution here must accept the slug AND the
15
- * raw name (old links must not 404), and report the slug on every change.
16
- */
17
-
18
- const VIEW: MerchantSettingsView = {
19
- providers: [
20
- {
21
- name: 'infinitepay',
22
- displayName: 'InfinitePay',
23
- urlSlug: 'infinite-pay',
24
- authMode: 'credentials',
25
- credentialSchema: [],
26
- },
27
- { name: 'pagbank', displayName: 'PagBank', urlSlug: 'pagbank', authMode: 'oauth', credentialSchema: [] },
28
- ],
29
- configs: [],
30
- activeProvider: null,
31
- } as unknown as MerchantSettingsView;
32
-
33
- function fakeClient(): PaymentsSettingsClient {
34
- return {
35
- getSettings: vi.fn().mockResolvedValue(VIEW),
36
- getSetupGuide: vi.fn().mockResolvedValue(null),
37
- setEnabled: vi.fn(),
38
- saveCredentials: vi.fn(),
39
- } as unknown as PaymentsSettingsClient;
40
- }
41
-
42
- describe('PaymentProviderSettings — adapter-declared URL slugs', () => {
43
- it('opens the provider a slug segment names', async () => {
44
- render(<PaymentProviderSettings client={fakeClient()} selectedProvider="infinite-pay" />);
45
-
46
- expect(await screen.findByTestId('payments-provider-back')).toBeDefined();
47
- await waitFor(() => expect(screen.queryByTestId('payments-provider-picker')).toBeNull());
48
- });
49
-
50
- it('still resolves the raw name, so a link minted before the slug existed lands', async () => {
51
- render(<PaymentProviderSettings client={fakeClient()} selectedProvider="infinitepay" />);
52
-
53
- expect(await screen.findByTestId('payments-provider-back')).toBeDefined();
54
- });
55
-
56
- it('reports the slug on selection, for the host to write into its URL verbatim', async () => {
57
- const onProviderChange = vi.fn();
58
- render(
59
- <PaymentProviderSettings
60
- client={fakeClient()}
61
- selectedProvider={null}
62
- onProviderChange={onProviderChange}
63
- />,
64
- );
65
- const card = await screen.findByTestId('payments-provider-card-infinitepay');
66
-
67
- card.click();
68
-
69
- await waitFor(() => expect(onProviderChange).toHaveBeenCalledWith('infinite-pay'));
70
- });
71
-
72
- /**
73
- * The OAuth callback's `?connected=` carries the raw NAME, and the host
74
- * writes it into the URL before the catalog exists — it holds no map. The
75
- * screen is right either way (the alias resolves); the ADDRESS BAR is what
76
- * would stay wrong, and it is what a reload or a shared link uses. So once
77
- * the catalog can spell the provider, the component asks the host to respell
78
- * the segment — as a `replace`, so Voltar never revisits the alias.
79
- */
80
- it('asks the host to respell an alias segment to the canonical slug, as a replace', async () => {
81
- const onProviderChange = vi.fn();
82
- render(
83
- <PaymentProviderSettings
84
- client={fakeClient()}
85
- selectedProvider="infinitepay"
86
- onProviderChange={onProviderChange}
87
- />,
88
- );
89
-
90
- await waitFor(() =>
91
- expect(onProviderChange).toHaveBeenCalledWith('infinite-pay', { replace: true }),
92
- );
93
- });
94
-
95
- it('leaves a segment already spelled canonically alone', async () => {
96
- const onProviderChange = vi.fn();
97
- render(
98
- <PaymentProviderSettings
99
- client={fakeClient()}
100
- selectedProvider="infinite-pay"
101
- onProviderChange={onProviderChange}
102
- />,
103
- );
104
-
105
- await screen.findByTestId('payments-provider-back');
106
- expect(onProviderChange).not.toHaveBeenCalled();
107
- });
108
- });
@@ -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
- });