@12-apps/payments-frontend 3.28.0 → 3.29.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 +2 -2
- package/src/components/ProviderSetupGuide.tsx +4 -1
- package/src/components/ProviderStatusBar.tsx +54 -1
- package/src/components/connection-state.ts +23 -3
- package/src/components/settings-copy.ts +8 -0
- package/src/components/settings-en-US.ts +6 -0
- package/src/components/settings-pt-BR.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.29.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"storybook:build": "storybook build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@12-apps/payments-backend": "^4.
|
|
26
|
+
"@12-apps/payments-backend": "^4.29.0",
|
|
27
27
|
"react-qr-code": "^2.2.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -240,7 +240,10 @@ function StepRow({ step, actions }: StepRowProps) {
|
|
|
240
240
|
href={step.button.url}
|
|
241
241
|
target="_blank"
|
|
242
242
|
rel="noreferrer"
|
|
243
|
-
|
|
243
|
+
// Never narrower than its label beside the text, but never wider than
|
|
244
|
+
// the row once it has wrapped under it: at 320px "Abrir o portal do
|
|
245
|
+
// desenvolvedor ↗" was wider than the step and spilled out of it.
|
|
246
|
+
sx={{ ...BTN_SECONDARY_SX, px: '12px', py: '7px', fontSize: '12px', flexShrink: 0, maxWidth: '100%' }}
|
|
244
247
|
// The mark is the promise: this leaves the store and opens the
|
|
245
248
|
// provider's site. A button that reads the same as the in-page ones
|
|
246
249
|
// and then navigates away is a small betrayal, and here it lands on
|
|
@@ -4,7 +4,7 @@ import { Box, Stack, Switch, Tooltip, Typography } from '@mui/material';
|
|
|
4
4
|
|
|
5
5
|
import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments-backend';
|
|
6
6
|
|
|
7
|
-
import { isConnected } from './connection-state';
|
|
7
|
+
import { certificateExpiryProximity, isConnected } from './connection-state';
|
|
8
8
|
import { T } from './panel-tokens';
|
|
9
9
|
import type { ConnectionStatusCopy } from './settings-copy';
|
|
10
10
|
import { usePaymentsSettingsCopy } from './settings-copy-context';
|
|
@@ -218,6 +218,58 @@ function ProviderName({
|
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* The saved production certificate's renewal warning, under the headline.
|
|
223
|
+
*
|
|
224
|
+
* Only for a store on PRODUCTION: that is the one whose PIX stops when the
|
|
225
|
+
* certificate lapses, and a sandbox store testing the form has nothing to
|
|
226
|
+
* renew yet. Silent outside the 30-day window (`certificateExpiryProximity`) —
|
|
227
|
+
* a date a year out is not news, and it is on the provider's portal anyway.
|
|
228
|
+
* A `status`, lapsed or not: it is state on load, and the probe's own error is
|
|
229
|
+
* already the alert — a second would be read out twice.
|
|
230
|
+
*/
|
|
231
|
+
function CertificateExpiryNote({ copy, config }: { copy: ConnectionStatusCopy; config: MaskedProviderConfig | null }) {
|
|
232
|
+
const note = certificateNote(copy, config);
|
|
233
|
+
if (!note) return null;
|
|
234
|
+
const tone = note.past
|
|
235
|
+
? { border: T.badLine, background: T.badSoft, color: T.badInk }
|
|
236
|
+
: { border: T.warnLine, background: T.warnSoft, color: T.warnInk };
|
|
237
|
+
return (
|
|
238
|
+
<Typography
|
|
239
|
+
role="status"
|
|
240
|
+
data-testid="payments-certificate-expiry"
|
|
241
|
+
sx={{
|
|
242
|
+
fontSize: '12.5px',
|
|
243
|
+
fontWeight: 600,
|
|
244
|
+
lineHeight: 1.5,
|
|
245
|
+
maxWidth: '52ch',
|
|
246
|
+
mt: '8px',
|
|
247
|
+
px: '10px',
|
|
248
|
+
py: '7px',
|
|
249
|
+
borderRadius: '8px',
|
|
250
|
+
border: `1px solid ${tone.border}`,
|
|
251
|
+
background: tone.background,
|
|
252
|
+
color: tone.color,
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{note.text}
|
|
256
|
+
</Typography>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** What the certificate note says, or null when it has nothing to say. */
|
|
261
|
+
function certificateNote(
|
|
262
|
+
copy: ConnectionStatusCopy,
|
|
263
|
+
config: MaskedProviderConfig | null,
|
|
264
|
+
): { text: string; past: boolean } | null {
|
|
265
|
+
const expiresAt = config?.environment === 'PRODUCTION' ? config.credentialExpiresAt : null;
|
|
266
|
+
if (!expiresAt) return null;
|
|
267
|
+
const proximity = certificateExpiryProximity(expiresAt);
|
|
268
|
+
if (proximity === 'PAST') return { text: copy.certificateExpired(expiresAt), past: true };
|
|
269
|
+
if (proximity === 'NEAR') return { text: copy.certificateExpires(expiresAt), past: false };
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
221
273
|
export function ProviderStatusBar({
|
|
222
274
|
descriptor,
|
|
223
275
|
config,
|
|
@@ -248,6 +300,7 @@ export function ProviderStatusBar({
|
|
|
248
300
|
>
|
|
249
301
|
{sub}
|
|
250
302
|
</Typography>
|
|
303
|
+
<CertificateExpiryNote copy={copy} config={config} />
|
|
251
304
|
</Box>
|
|
252
305
|
{/* The switch belongs at the far edge: it is the one control here that
|
|
253
306
|
changes what buyers experience, and crowding it against the status
|
|
@@ -71,11 +71,31 @@ export function expiryProximity(
|
|
|
71
71
|
expiresAt: string | null | undefined,
|
|
72
72
|
now: Date = new Date(),
|
|
73
73
|
): ExpiryProximity | null {
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
return proximityWithin(EXPIRY_WARNING_WINDOW_MS, expiresAt, now);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A saved CERTIFICATE's proximity (`credentialExpiresAt`) — a different
|
|
79
|
+
* window from the OAuth grant's on purpose. No sweep renews a certificate:
|
|
80
|
+
* the owner generates a new one by hand, and Itaú opens that renewal 30 days
|
|
81
|
+
* before expiry, so the warning starts the day there is something to do about
|
|
82
|
+
* it rather than a week before the store's PIX stops.
|
|
83
|
+
*/
|
|
84
|
+
const CERTIFICATE_WARNING_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
85
|
+
|
|
86
|
+
export function certificateExpiryProximity(
|
|
87
|
+
credentialExpiresAt: string | null | undefined,
|
|
88
|
+
now: Date = new Date(),
|
|
89
|
+
): ExpiryProximity | null {
|
|
90
|
+
return proximityWithin(CERTIFICATE_WARNING_WINDOW_MS, credentialExpiresAt, now);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function proximityWithin(windowMs: number, iso: string | null | undefined, now: Date): ExpiryProximity | null {
|
|
94
|
+
if (!iso) return null;
|
|
95
|
+
const at = new Date(iso).getTime();
|
|
76
96
|
if (Number.isNaN(at)) return null;
|
|
77
97
|
if (at <= now.getTime()) return 'PAST';
|
|
78
|
-
return at - now.getTime() <=
|
|
98
|
+
return at - now.getTime() <= windowMs ? 'NEAR' : 'SAFE';
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/**
|
|
@@ -49,6 +49,14 @@ export interface ConnectionStatusCopy {
|
|
|
49
49
|
pausedChip: string;
|
|
50
50
|
/** Proven, not switched on yet. */
|
|
51
51
|
readyNotReceiving: { state: string; sub: string };
|
|
52
|
+
/**
|
|
53
|
+
* The saved production certificate lapses inside the renewal window, or has
|
|
54
|
+
* lapsed. Given the ISO `validTo`; the pack formats the date in its own
|
|
55
|
+
* locale. Provider-neutral wording — the certificate is the store's, the
|
|
56
|
+
* portal is the provider's.
|
|
57
|
+
*/
|
|
58
|
+
certificateExpires: (iso: string) => string;
|
|
59
|
+
certificateExpired: (iso: string) => string;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
@@ -39,6 +39,12 @@ export const EN_US_PAYMENTS_SETTINGS_COPY: PaymentsSettingsCopy = {
|
|
|
39
39
|
state: 'Not taking sales yet',
|
|
40
40
|
sub: 'Everything is ready — flip the switch to start taking payments.',
|
|
41
41
|
},
|
|
42
|
+
certificateExpires: (iso) =>
|
|
43
|
+
`The production certificate expires on ${new Date(iso).toLocaleDateString('en-US')}. ` +
|
|
44
|
+
'Before then, generate a new certificate and private key in the provider’s portal and paste both on the Production tab — with an expired certificate, Pix through this provider does not work.',
|
|
45
|
+
certificateExpired: (iso) =>
|
|
46
|
+
`The production certificate expired on ${new Date(iso).toLocaleDateString('en-US')}. ` +
|
|
47
|
+
'Until it is replaced, Pix through this provider does not work. Generate a new certificate and private key in the provider’s portal and paste both on the Production tab.',
|
|
42
48
|
},
|
|
43
49
|
listBadge: {
|
|
44
50
|
active: 'Active',
|
|
@@ -31,6 +31,12 @@ export const PT_BR_PAYMENTS_SETTINGS_COPY: PaymentsSettingsCopy = {
|
|
|
31
31
|
state: 'Ainda não está recebendo',
|
|
32
32
|
sub: 'Tudo pronto — ligue a chave para começar a receber.',
|
|
33
33
|
},
|
|
34
|
+
certificateExpires: (iso) =>
|
|
35
|
+
`O certificado de produção vence em ${new Date(iso).toLocaleDateString('pt-BR')}. ` +
|
|
36
|
+
'Antes disso, gere um novo certificado e uma nova chave privada no portal do provedor e cole os dois na aba Produção — com o certificado vencido, o Pix por este provedor não funciona.',
|
|
37
|
+
certificateExpired: (iso) =>
|
|
38
|
+
`O certificado de produção venceu em ${new Date(iso).toLocaleDateString('pt-BR')}. ` +
|
|
39
|
+
'Enquanto ele não for trocado, o Pix por este provedor não funciona. Gere um novo certificado e uma nova chave privada no portal do provedor e cole os dois na aba Produção.',
|
|
34
40
|
},
|
|
35
41
|
listBadge: {
|
|
36
42
|
active: 'Ativo',
|