@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,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
|
-
});
|