@12-apps/payments-frontend 1.16.0 → 1.18.0
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 +4 -4
- package/src/components/ProviderConnection.tsx +54 -0
- package/src/components/checkout/apple-pay-button.tsx +189 -0
- package/src/components/checkout/checkout-flow.tsx +9 -1
- package/src/components/checkout/checkout-steps.tsx +7 -0
- package/src/components/checkout/client-context.tsx +2 -0
- package/src/components/checkout/client.ts +10 -0
- package/src/components/checkout/google-pay-button.tsx +263 -0
- package/src/components/checkout/method-capability.ts +45 -0
- package/src/components/checkout/providers/pix-and-card.tsx +8 -23
- package/src/components/checkout/providers/types.ts +8 -0
- package/src/components/checkout/transport.ts +38 -17
- package/src/components/checkout/types.ts +41 -0
- package/src/components/checkout/use-wallet-charge.ts +129 -0
- package/src/components/checkout/wallet-pane.tsx +187 -0
- package/src/components/platform/ConnectApplicationPanel.tsx +77 -0
- package/src/components/platform/ConnectEnvironmentCard.tsx +175 -0
- package/src/components/platform/HomologacaoGuideCard.tsx +86 -0
- package/src/components/platform/HomologacaoOutcomeCard.tsx +170 -0
- package/src/components/platform/PlatformHomologacao.tsx +107 -0
- package/src/flows/create-payment-flows.tsx +1 -0
- package/src/flows/types.ts +7 -0
- package/src/index.ts +47 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Alert, Box, Button, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useState, type ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { ConnectApplicationStatus, PaymentEnvironment } from '@12-apps/payments-backend';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One environment's Connect application (FUT-479, packaged by FUT-573).
|
|
10
|
+
* Sandbox and produção are SEPARATE applications with separate id/secret
|
|
11
|
+
* pairs, so each card stands on its own: configured or not, what PagBank says
|
|
12
|
+
* is registered, and whether the registered redirect URI matches the
|
|
13
|
+
* deployment's callback.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const ENV_LABEL: Record<PaymentEnvironment, string> = {
|
|
17
|
+
SANDBOX: 'Sandbox',
|
|
18
|
+
PRODUCTION: 'Produção',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The bordered-card look every block of these screens shares. */
|
|
22
|
+
export const CARD_SX = {
|
|
23
|
+
border: 1,
|
|
24
|
+
borderColor: 'divider',
|
|
25
|
+
borderRadius: 2,
|
|
26
|
+
p: 2,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
function Field({ label, children }: { label: string; children: ReactNode }): ReactNode {
|
|
30
|
+
return (
|
|
31
|
+
<Box
|
|
32
|
+
sx={{ display: 'flex', flexDirection: 'column', gap: 0.25, minWidth: 180, wordBreak: 'break-all' }}
|
|
33
|
+
>
|
|
34
|
+
<Typography variant="caption" color="text.secondary" fontWeight={600}>
|
|
35
|
+
{label}
|
|
36
|
+
</Typography>
|
|
37
|
+
<Typography component="span" variant="body2">
|
|
38
|
+
{children}
|
|
39
|
+
</Typography>
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The verdict the screen exists for: does the registered callback match ours? */
|
|
45
|
+
function MismatchAlert({ status }: { status: ConnectApplicationStatus }): ReactNode {
|
|
46
|
+
if (status.application === null) return null;
|
|
47
|
+
if (status.redirectUriMismatch === true) {
|
|
48
|
+
return (
|
|
49
|
+
<Alert severity="error" data-testid={`connect-mismatch-${status.environment}`}>
|
|
50
|
+
A redirect_uri registrada no PagBank é diferente do callback desta instalação. O fluxo
|
|
51
|
+
de autorização OAuth falha silenciosamente até o cadastro ser corrigido no PagBank.
|
|
52
|
+
</Alert>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (status.redirectUriMismatch === false) {
|
|
56
|
+
return (
|
|
57
|
+
<Alert severity="success" data-testid={`connect-match-${status.environment}`}>
|
|
58
|
+
A redirect_uri registrada confere com o callback desta instalação.
|
|
59
|
+
</Alert>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<Alert severity="warning" data-testid={`connect-unknown-${status.environment}`}>
|
|
64
|
+
A resposta do PagBank não informou a redirect_uri — não foi possível comparar com o
|
|
65
|
+
callback desta instalação.
|
|
66
|
+
</Alert>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** What PagBank reports as registered, plus whatever extra keys came back. */
|
|
71
|
+
function ApplicationFields({ status }: { status: ConnectApplicationStatus }): ReactNode {
|
|
72
|
+
const app = status.application;
|
|
73
|
+
if (app === null) return null;
|
|
74
|
+
const extraKeys = Object.keys(app.extra);
|
|
75
|
+
return (
|
|
76
|
+
<Stack spacing={1.5}>
|
|
77
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2.5 }}>
|
|
78
|
+
<Field label="Nome (exibido ao lojista)">{app.name ?? '—'}</Field>
|
|
79
|
+
<Field label="Site">{app.site ?? '—'}</Field>
|
|
80
|
+
<Field label="Descrição">{app.description ?? '—'}</Field>
|
|
81
|
+
<Field label="Logo">{app.logo ?? '—'}</Field>
|
|
82
|
+
<Field label="redirect_uri registrada">{app.redirectUri ?? 'não informada'}</Field>
|
|
83
|
+
</Box>
|
|
84
|
+
{extraKeys.length > 0 ? (
|
|
85
|
+
<Box data-testid={`connect-extra-${status.environment}`}>
|
|
86
|
+
<Typography variant="caption" color="text.secondary" fontWeight={600}>
|
|
87
|
+
Outros campos retornados (schema não documentado)
|
|
88
|
+
</Typography>
|
|
89
|
+
<Box component="pre" sx={{ m: 0, fontSize: 12, overflowX: 'auto' }}>
|
|
90
|
+
{JSON.stringify(app.extra, null, 2)}
|
|
91
|
+
</Box>
|
|
92
|
+
</Box>
|
|
93
|
+
) : null}
|
|
94
|
+
</Stack>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The collapsible "what feeds this environment" help. The variable names are
|
|
100
|
+
* the HOST's own configuration surface, so they arrive via `configVars`; with
|
|
101
|
+
* none provided the toggle is omitted entirely rather than opening on nothing.
|
|
102
|
+
*/
|
|
103
|
+
function ConfigHelp({
|
|
104
|
+
environment,
|
|
105
|
+
configVars,
|
|
106
|
+
}: {
|
|
107
|
+
environment: PaymentEnvironment;
|
|
108
|
+
configVars?: string[];
|
|
109
|
+
}): ReactNode {
|
|
110
|
+
const [open, setOpen] = useState(false);
|
|
111
|
+
if (!configVars || configVars.length === 0) return null;
|
|
112
|
+
return (
|
|
113
|
+
<Stack spacing={1} alignItems="flex-start">
|
|
114
|
+
<Button
|
|
115
|
+
variant="outlined"
|
|
116
|
+
size="small"
|
|
117
|
+
onClick={() => setOpen((value) => !value)}
|
|
118
|
+
data-testid={`connect-config-toggle-${environment}`}
|
|
119
|
+
>
|
|
120
|
+
{open ? 'Ocultar variáveis de ambiente' : 'Ver variáveis de ambiente'}
|
|
121
|
+
</Button>
|
|
122
|
+
{open ? (
|
|
123
|
+
<Box data-testid={`connect-config-details-${environment}`}>
|
|
124
|
+
<Typography variant="caption" color="text.secondary" component="p">
|
|
125
|
+
A aplicação deste ambiente é resolvida estritamente por estas variáveis (sem
|
|
126
|
+
fallback entre ambientes):
|
|
127
|
+
</Typography>
|
|
128
|
+
<Box component="ul" sx={{ m: 0, pl: 2.5 }}>
|
|
129
|
+
{configVars.map((name) => (
|
|
130
|
+
<Box component="li" key={name}>
|
|
131
|
+
<Box component="code" sx={{ fontFamily: 'monospace', fontSize: 12 }}>
|
|
132
|
+
{name}
|
|
133
|
+
</Box>
|
|
134
|
+
</Box>
|
|
135
|
+
))}
|
|
136
|
+
</Box>
|
|
137
|
+
</Box>
|
|
138
|
+
) : null}
|
|
139
|
+
</Stack>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function ConnectEnvironmentCard({
|
|
144
|
+
status,
|
|
145
|
+
configVars,
|
|
146
|
+
}: {
|
|
147
|
+
status: ConnectApplicationStatus;
|
|
148
|
+
configVars?: string[];
|
|
149
|
+
}): ReactNode {
|
|
150
|
+
return (
|
|
151
|
+
<Stack spacing={1.5} data-testid={`connect-env-${status.environment}`} sx={CARD_SX}>
|
|
152
|
+
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1.5, wordBreak: 'break-all' }}>
|
|
153
|
+
<Typography variant="body2" fontWeight={600}>
|
|
154
|
+
{ENV_LABEL[status.environment]}
|
|
155
|
+
</Typography>
|
|
156
|
+
<Typography component="span" variant="caption" color="text.secondary">
|
|
157
|
+
{status.clientId !== null ? `client_id: ${status.clientId}` : ''}
|
|
158
|
+
</Typography>
|
|
159
|
+
</Box>
|
|
160
|
+
{!status.configured ? (
|
|
161
|
+
<Typography variant="body2" color="text.secondary">
|
|
162
|
+
Nenhuma aplicação configurada neste ambiente.
|
|
163
|
+
</Typography>
|
|
164
|
+
) : null}
|
|
165
|
+
{status.error !== null ? (
|
|
166
|
+
<Alert severity="warning" data-testid={`connect-error-${status.environment}`}>
|
|
167
|
+
{status.error}
|
|
168
|
+
</Alert>
|
|
169
|
+
) : null}
|
|
170
|
+
<MismatchAlert status={status} />
|
|
171
|
+
<ApplicationFields status={status} />
|
|
172
|
+
<ConfigHelp environment={status.environment} configVars={configVars} />
|
|
173
|
+
</Stack>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Box, Link, Stack, Typography } from '@mui/material';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { HomologacaoGuide } from '@12-apps/payments-backend';
|
|
7
|
+
|
|
8
|
+
import { CARD_SX } from './ConnectEnvironmentCard';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The paste-ready homologação answers (FUT-483, packaged by FUT-573) — the
|
|
12
|
+
* Pipefy form with every deployment-specific value already computed, and the
|
|
13
|
+
* services list naming BOTH Order and Connect.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One paste-ready value, in a copyable code block. */
|
|
17
|
+
function Answer({ label, children }: { label: string; children: ReactNode }): ReactNode {
|
|
18
|
+
return (
|
|
19
|
+
<Stack spacing={0.25}>
|
|
20
|
+
<Typography variant="caption" color="text.secondary" fontWeight={600}>
|
|
21
|
+
{label}
|
|
22
|
+
</Typography>
|
|
23
|
+
<Box
|
|
24
|
+
component="code"
|
|
25
|
+
sx={{
|
|
26
|
+
fontFamily: 'monospace',
|
|
27
|
+
fontSize: 12,
|
|
28
|
+
p: 1,
|
|
29
|
+
borderRadius: 1,
|
|
30
|
+
bgcolor: 'action.hover',
|
|
31
|
+
wordBreak: 'break-word',
|
|
32
|
+
whiteSpace: 'pre-wrap',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</Box>
|
|
37
|
+
</Stack>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function HomologacaoGuideCard({ guide }: { guide: HomologacaoGuide }): ReactNode {
|
|
42
|
+
return (
|
|
43
|
+
<Stack spacing={1.5} data-testid="homologacao-guide-card" sx={CARD_SX}>
|
|
44
|
+
<Typography variant="body2" fontWeight={600}>
|
|
45
|
+
Formulário de homologação — respostas prontas
|
|
46
|
+
</Typography>
|
|
47
|
+
<Typography variant="body2" color="text.secondary" component="p">
|
|
48
|
+
Abra o{' '}
|
|
49
|
+
<Link
|
|
50
|
+
href={guide.formUrl}
|
|
51
|
+
target="_blank"
|
|
52
|
+
rel="noreferrer"
|
|
53
|
+
data-testid="homologacao-form-link"
|
|
54
|
+
>
|
|
55
|
+
formulário oficial de homologação
|
|
56
|
+
</Link>{' '}
|
|
57
|
+
e preencha com os valores abaixo. Em paralelo, abra um chamado no{' '}
|
|
58
|
+
<Link href={guide.supportFormUrl} target="_blank" rel="noreferrer">
|
|
59
|
+
SIP — Suporte Integração PagBank
|
|
60
|
+
</Link>{' '}
|
|
61
|
+
citando o 403 ACCESS_DENIED — o que responder primeiro resolve a dúvida de o
|
|
62
|
+
formulário cobrir ou não o Connect. Documentação:{' '}
|
|
63
|
+
<Link href={guide.docsUrl} target="_blank" rel="noreferrer">
|
|
64
|
+
solicitar homologação
|
|
65
|
+
</Link>
|
|
66
|
+
.
|
|
67
|
+
</Typography>
|
|
68
|
+
<Answer label="Selecione o tipo de integração">{guide.integrationType}</Answer>
|
|
69
|
+
<Box data-testid="homologacao-services">
|
|
70
|
+
<Answer label="Selecione qual serviço você integrou (marque OS DOIS)">
|
|
71
|
+
{guide.services.join('\n')}
|
|
72
|
+
</Answer>
|
|
73
|
+
</Box>
|
|
74
|
+
<Answer label="Instruções de acesso ao seu ambiente (limite de 255 caracteres)">
|
|
75
|
+
{guide.accessInstructions}
|
|
76
|
+
</Answer>
|
|
77
|
+
<Answer label="URL do site">{guide.siteUrl}</Answer>
|
|
78
|
+
<Answer label="Detalhe quais produtos/serviços serão comercializados">
|
|
79
|
+
{guide.productsDescription}
|
|
80
|
+
</Answer>
|
|
81
|
+
<Typography variant="caption" color="text.secondary" component="p">
|
|
82
|
+
{guide.slaText}
|
|
83
|
+
</Typography>
|
|
84
|
+
</Stack>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Alert, Box, Button, Chip, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
+
import { useState, type ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { PlatformHomologationStatus } from '@12-apps/payments-backend';
|
|
7
|
+
|
|
8
|
+
import { CARD_SX } from './ConnectEnvironmentCard';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The homologação outcome record (FUT-483, packaged by FUT-573) — the durable
|
|
12
|
+
* answer to "is the platform homologated?". Absence of the record is the
|
|
13
|
+
* honest fourth state ("não solicitada"), which is why it is displayed but
|
|
14
|
+
* never offered as a choice.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One provider's recorded outcome as the wire carries it — the backend's
|
|
19
|
+
* `PlatformHomologationRecord` after JSON serialization (dates as ISO
|
|
20
|
+
* strings).
|
|
21
|
+
*/
|
|
22
|
+
export interface PlatformHomologationRecordView {
|
|
23
|
+
provider: string;
|
|
24
|
+
status: PlatformHomologationStatus;
|
|
25
|
+
protocol: string | null;
|
|
26
|
+
notes: string | null;
|
|
27
|
+
submittedAt: string | null;
|
|
28
|
+
decidedAt: string | null;
|
|
29
|
+
updatedBy: string | null;
|
|
30
|
+
updatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the outcome form submits — the whole record, replaced deliberately. */
|
|
34
|
+
export interface HomologacaoSaveInput {
|
|
35
|
+
status: PlatformHomologationStatus;
|
|
36
|
+
protocol: string;
|
|
37
|
+
notes: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The host's save-mutation state, whatever machinery produces it. */
|
|
41
|
+
export interface HomologacaoSaveState {
|
|
42
|
+
pending: boolean;
|
|
43
|
+
error: string | null;
|
|
44
|
+
success: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const STATUS_LABEL: Record<PlatformHomologationStatus, string> = {
|
|
48
|
+
SUBMITTED: 'Solicitada',
|
|
49
|
+
APPROVED: 'Aprovada',
|
|
50
|
+
REJECTED: 'Recusada',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const STATUS_COLOR: Record<PlatformHomologationStatus, 'warning' | 'success' | 'error'> = {
|
|
54
|
+
SUBMITTED: 'warning',
|
|
55
|
+
APPROVED: 'success',
|
|
56
|
+
REJECTED: 'error',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function StatusChip({ record }: { record: PlatformHomologationRecordView | null }): ReactNode {
|
|
60
|
+
if (record === null) {
|
|
61
|
+
return (
|
|
62
|
+
<Chip
|
|
63
|
+
label="Não solicitada"
|
|
64
|
+
size="small"
|
|
65
|
+
variant="outlined"
|
|
66
|
+
data-testid="homologacao-status-chip"
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return (
|
|
71
|
+
<Chip
|
|
72
|
+
label={STATUS_LABEL[record.status]}
|
|
73
|
+
size="small"
|
|
74
|
+
color={STATUS_COLOR[record.status]}
|
|
75
|
+
data-testid="homologacao-status-chip"
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const formatDateTime = (iso: string): string => new Date(iso).toLocaleString('pt-BR');
|
|
81
|
+
|
|
82
|
+
function RecordTrail({ record }: { record: PlatformHomologationRecordView }): ReactNode {
|
|
83
|
+
return (
|
|
84
|
+
<Typography variant="caption" color="text.secondary" component="p">
|
|
85
|
+
{record.submittedAt ? `Solicitada em ${formatDateTime(record.submittedAt)}. ` : ''}
|
|
86
|
+
{record.decidedAt ? `Decidida em ${formatDateTime(record.decidedAt)}. ` : ''}
|
|
87
|
+
{record.updatedBy ? `Registrado por ${record.updatedBy}.` : ''}
|
|
88
|
+
</Typography>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface HomologacaoOutcomeCardProps {
|
|
93
|
+
record: PlatformHomologationRecordView | null;
|
|
94
|
+
/** Record the outcome — the host PUTs it and refreshes `record`. */
|
|
95
|
+
onSave: (input: HomologacaoSaveInput) => void;
|
|
96
|
+
save: HomologacaoSaveState;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function HomologacaoOutcomeCard(props: HomologacaoOutcomeCardProps): ReactNode {
|
|
100
|
+
const { record, onSave, save } = props;
|
|
101
|
+
const [status, setStatus] = useState<PlatformHomologationStatus>(record?.status ?? 'SUBMITTED');
|
|
102
|
+
const [protocol, setProtocol] = useState(record?.protocol ?? '');
|
|
103
|
+
const [notes, setNotes] = useState(record?.notes ?? '');
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Stack spacing={1.5} data-testid="homologacao-outcome-card" sx={CARD_SX}>
|
|
107
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
108
|
+
<Typography variant="body2" fontWeight={600}>
|
|
109
|
+
Situação da homologação
|
|
110
|
+
</Typography>
|
|
111
|
+
<StatusChip record={record} />
|
|
112
|
+
</Box>
|
|
113
|
+
{record ? <RecordTrail record={record} /> : null}
|
|
114
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5, alignItems: 'flex-start' }}>
|
|
115
|
+
<TextField
|
|
116
|
+
select
|
|
117
|
+
size="small"
|
|
118
|
+
label="Situação"
|
|
119
|
+
value={status}
|
|
120
|
+
onChange={(event) => setStatus(event.target.value as PlatformHomologationStatus)}
|
|
121
|
+
slotProps={{
|
|
122
|
+
select: { native: true },
|
|
123
|
+
htmlInput: { 'data-testid': 'homologacao-status-select' },
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{(Object.keys(STATUS_LABEL) as PlatformHomologationStatus[]).map((key) => (
|
|
127
|
+
<option key={key} value={key}>
|
|
128
|
+
{STATUS_LABEL[key]}
|
|
129
|
+
</option>
|
|
130
|
+
))}
|
|
131
|
+
</TextField>
|
|
132
|
+
<TextField
|
|
133
|
+
size="small"
|
|
134
|
+
aria-label="Protocolo"
|
|
135
|
+
placeholder="Protocolo (cartão do Pipefy / chamado)"
|
|
136
|
+
value={protocol}
|
|
137
|
+
onChange={(event) => setProtocol(event.target.value)}
|
|
138
|
+
slotProps={{ htmlInput: { 'data-testid': 'homologacao-protocol' } }}
|
|
139
|
+
/>
|
|
140
|
+
<TextField
|
|
141
|
+
size="small"
|
|
142
|
+
aria-label="Observações"
|
|
143
|
+
placeholder="Observações (resposta do PagBank, contexto…)"
|
|
144
|
+
value={notes}
|
|
145
|
+
onChange={(event) => setNotes(event.target.value)}
|
|
146
|
+
slotProps={{ htmlInput: { 'data-testid': 'homologacao-notes' } }}
|
|
147
|
+
/>
|
|
148
|
+
<Button
|
|
149
|
+
variant="contained"
|
|
150
|
+
size="small"
|
|
151
|
+
disabled={save.pending}
|
|
152
|
+
onClick={() => onSave({ status, protocol, notes })}
|
|
153
|
+
data-testid="homologacao-save"
|
|
154
|
+
>
|
|
155
|
+
Registrar
|
|
156
|
+
</Button>
|
|
157
|
+
</Box>
|
|
158
|
+
{save.error !== null ? (
|
|
159
|
+
<Alert severity="error" data-testid="homologacao-save-error">
|
|
160
|
+
{save.error}
|
|
161
|
+
</Alert>
|
|
162
|
+
) : null}
|
|
163
|
+
{save.success ? (
|
|
164
|
+
<Alert severity="success" data-testid="homologacao-save-ok">
|
|
165
|
+
Registro atualizado.
|
|
166
|
+
</Alert>
|
|
167
|
+
) : null}
|
|
168
|
+
</Stack>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Alert, Box, Button, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useState, type ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { HomologacaoGuide } from '@12-apps/payments-backend';
|
|
7
|
+
|
|
8
|
+
import { CARD_SX } from './ConnectEnvironmentCard';
|
|
9
|
+
import { HomologacaoGuideCard } from './HomologacaoGuideCard';
|
|
10
|
+
import {
|
|
11
|
+
HomologacaoOutcomeCard,
|
|
12
|
+
type HomologacaoSaveInput,
|
|
13
|
+
type HomologacaoSaveState,
|
|
14
|
+
type PlatformHomologationRecordView,
|
|
15
|
+
} from './HomologacaoOutcomeCard';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The PLATFORM's PagBank homologação screen (FUT-483, packaged by FUT-573).
|
|
19
|
+
*
|
|
20
|
+
* The platform is the direct integrator, so the homologação is the
|
|
21
|
+
* platform's, once — store owners are platform users and are exempt. The
|
|
22
|
+
* screen carries the three halves: the recorded outcome (so "is the platform
|
|
23
|
+
* homologated?" stops being a question for a person), the Pipefy form with
|
|
24
|
+
* paste-ready answers (BOTH services — Order and Connect), and the evidence
|
|
25
|
+
* generator running on the platform's own sandbox credentials.
|
|
26
|
+
*
|
|
27
|
+
* Dumb by design: data and mutations arrive via props from the host's own
|
|
28
|
+
* mounted routes (`platformHomologacaoGuide`, `createHomologationRecordService`
|
|
29
|
+
* and `buildPlatformHomologacaoAnexo` in `@12-apps/payments-backend`), so the
|
|
30
|
+
* host page is a thin mount.
|
|
31
|
+
*/
|
|
32
|
+
export interface PlatformHomologacaoProps {
|
|
33
|
+
/** The recorded outcome; null renders the honest "não solicitada". */
|
|
34
|
+
record: PlatformHomologationRecordView | null;
|
|
35
|
+
/** The paste-ready answers, computed by the host's backend. */
|
|
36
|
+
guide: HomologacaoGuide;
|
|
37
|
+
/** Record the outcome — the host PUTs it and refreshes `record`. */
|
|
38
|
+
onSaveRecord: (input: HomologacaoSaveInput) => void;
|
|
39
|
+
/** The host's save-mutation state. */
|
|
40
|
+
save: HomologacaoSaveState;
|
|
41
|
+
/**
|
|
42
|
+
* Generate AND deliver the evidence file (the host downloads what its anexo
|
|
43
|
+
* route answers). Reject with an Error whose message names the reason —
|
|
44
|
+
* e.g. the missing platform sandbox token and where to fix it — and the
|
|
45
|
+
* card shows it verbatim.
|
|
46
|
+
*/
|
|
47
|
+
onGenerateAnexo: () => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The evidence-file half: real sandbox calls, downloaded as a text file. */
|
|
51
|
+
function AnexoCard({ onGenerate }: { onGenerate: () => Promise<void> }): ReactNode {
|
|
52
|
+
const [error, setError] = useState<string | null>(null);
|
|
53
|
+
const [busy, setBusy] = useState(false);
|
|
54
|
+
|
|
55
|
+
const generate = async (): Promise<void> => {
|
|
56
|
+
setBusy(true);
|
|
57
|
+
setError(null);
|
|
58
|
+
try {
|
|
59
|
+
await onGenerate();
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
setError(cause instanceof Error ? cause.message : 'Não foi possível gerar o anexo.');
|
|
62
|
+
} finally {
|
|
63
|
+
setBusy(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Stack spacing={1.5} data-testid="homologacao-anexo-card" sx={CARD_SX}>
|
|
69
|
+
<Typography variant="body2" fontWeight={600}>
|
|
70
|
+
Anexo de evidências
|
|
71
|
+
</Typography>
|
|
72
|
+
<Typography variant="body2" color="text.secondary" component="p">
|
|
73
|
+
O formulário exige os requests e responses das requisições enviadas às APIs do
|
|
74
|
+
PagBank. O botão abaixo faz as chamadas reais no ambiente de testes (Sandbox) com o
|
|
75
|
+
token da própria plataforma — nada é cobrado de verdade — e baixa o arquivo pronto
|
|
76
|
+
para anexar, com o token redigido.
|
|
77
|
+
</Typography>
|
|
78
|
+
<Box>
|
|
79
|
+
<Button
|
|
80
|
+
variant="outlined"
|
|
81
|
+
size="small"
|
|
82
|
+
disabled={busy}
|
|
83
|
+
onClick={() => void generate()}
|
|
84
|
+
data-testid="homologacao-anexo-button"
|
|
85
|
+
>
|
|
86
|
+
Gerar anexo
|
|
87
|
+
</Button>
|
|
88
|
+
</Box>
|
|
89
|
+
{error !== null ? (
|
|
90
|
+
<Alert severity="error" data-testid="homologacao-anexo-error">
|
|
91
|
+
{error}
|
|
92
|
+
</Alert>
|
|
93
|
+
) : null}
|
|
94
|
+
</Stack>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function PlatformHomologacao(props: PlatformHomologacaoProps): ReactNode {
|
|
99
|
+
const { record, guide, onSaveRecord, save, onGenerateAnexo } = props;
|
|
100
|
+
return (
|
|
101
|
+
<Stack spacing={2} data-testid="platform-homologacao">
|
|
102
|
+
<HomologacaoOutcomeCard record={record} onSave={onSaveRecord} save={save} />
|
|
103
|
+
<HomologacaoGuideCard guide={guide} />
|
|
104
|
+
<AnexoCard onGenerate={onGenerateAnexo} />
|
|
105
|
+
</Stack>
|
|
106
|
+
);
|
|
107
|
+
}
|
package/src/flows/types.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface CheckoutPorts {
|
|
|
74
74
|
* some hosts must log or confirm the departure.
|
|
75
75
|
*/
|
|
76
76
|
navigate?(url: string): void;
|
|
77
|
+
/**
|
|
78
|
+
* Apple Pay merchant validation (FUT-472): exchange the session's
|
|
79
|
+
* `validationURL` for an Apple merchant session, SERVER-SIDE — the merchant
|
|
80
|
+
* identity certificate must never reach a browser. Optional; without it the
|
|
81
|
+
* Apple Pay sheet cannot start and the card form remains the way to pay.
|
|
82
|
+
*/
|
|
83
|
+
validateApplePayMerchant?(validationURL: string): Promise<unknown>;
|
|
77
84
|
/** The remedy shown on the no-provider screen, AND the host's veto. */
|
|
78
85
|
useAvailability?(): CheckoutAvailability;
|
|
79
86
|
}
|
package/src/index.ts
CHANGED
|
@@ -84,6 +84,29 @@ export {
|
|
|
84
84
|
export { type CheckoutHostPorts } from './components/checkout/use-checkout-controller';
|
|
85
85
|
export { PaymentsUnavailable } from './components/checkout/payments-unavailable';
|
|
86
86
|
export { fetchCheckoutConfig } from './components/checkout/client';
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Digital wallets (FUT-471/472) — the Google-branded button and the capability
|
|
89
|
+
// read it is gated on. `CheckoutFlow` wires these automatically; they are
|
|
90
|
+
// exported for hosts composing their own pixels.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
export {
|
|
93
|
+
GooglePayButton,
|
|
94
|
+
type GooglePayApi,
|
|
95
|
+
type GooglePayButtonProps,
|
|
96
|
+
type GooglePaymentData,
|
|
97
|
+
type GooglePaymentsClient,
|
|
98
|
+
type GooglePayGatewayParams,
|
|
99
|
+
} from './components/checkout/google-pay-button';
|
|
100
|
+
export {
|
|
101
|
+
ApplePayButton,
|
|
102
|
+
applePaySupported,
|
|
103
|
+
APPLE_PAY_SUPPORTED_NETWORKS,
|
|
104
|
+
type ApplePayButtonProps,
|
|
105
|
+
type ApplePayPaymentRequest,
|
|
106
|
+
type ApplePaySessionClass,
|
|
107
|
+
type ApplePaySessionLike,
|
|
108
|
+
} from './components/checkout/apple-pay-button';
|
|
109
|
+
export { applePayDeclared, googlePayConfig } from './components/checkout/method-capability';
|
|
87
110
|
export {
|
|
88
111
|
CheckoutComponentsProvider,
|
|
89
112
|
type CheckoutActionBarProps,
|
|
@@ -103,11 +126,13 @@ export {
|
|
|
103
126
|
type BuyerContact,
|
|
104
127
|
type BuyerField,
|
|
105
128
|
type BuyerInfo,
|
|
129
|
+
type ChargeWalletInput,
|
|
106
130
|
type CheckoutChainLink,
|
|
107
131
|
type CheckoutCustomerField,
|
|
108
132
|
type CheckoutError,
|
|
109
133
|
type CheckoutOrder,
|
|
110
134
|
type CheckoutProviderConfig,
|
|
135
|
+
type CheckoutWalletType,
|
|
111
136
|
type ComandaCheckout,
|
|
112
137
|
type CreateOrderRequest,
|
|
113
138
|
type CreateOrderResult,
|
|
@@ -169,6 +194,28 @@ export {
|
|
|
169
194
|
type PaymentProviderSettingsProps,
|
|
170
195
|
} from './components/PaymentProviderSettings';
|
|
171
196
|
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// The PLATFORM operations screens (FUT-479 / FUT-483, packaged by FUT-573) —
|
|
199
|
+
// the Connect-application consult and the homologação, as dumb components a
|
|
200
|
+
// host page mounts with data + callbacks from its own routes. Their backend
|
|
201
|
+
// halves live in `@12-apps/payments-backend` (`consultConnectApplications`,
|
|
202
|
+
// `platformHomologacaoGuide`, `createHomologationRecordService`,
|
|
203
|
+
// `buildPlatformHomologacaoAnexo`).
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
export {
|
|
206
|
+
ConnectApplicationPanel,
|
|
207
|
+
type ConnectApplicationPanelProps,
|
|
208
|
+
} from './components/platform/ConnectApplicationPanel';
|
|
209
|
+
export {
|
|
210
|
+
PlatformHomologacao,
|
|
211
|
+
type PlatformHomologacaoProps,
|
|
212
|
+
} from './components/platform/PlatformHomologacao';
|
|
213
|
+
export {
|
|
214
|
+
type HomologacaoSaveInput,
|
|
215
|
+
type HomologacaoSaveState,
|
|
216
|
+
type PlatformHomologationRecordView,
|
|
217
|
+
} from './components/platform/HomologacaoOutcomeCard';
|
|
218
|
+
|
|
172
219
|
/**
|
|
173
220
|
* Re-exported because it appears in the `prepareConnect` prop a host must
|
|
174
221
|
* implement: without it the host could not type its own callback without
|