@12-apps/payments-frontend 3.28.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.28.1",
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.28.0",
26
+ "@12-apps/payments-backend": "^4.29.0",
27
27
  "react-qr-code": "^2.2.0"
28
28
  },
29
29
  "peerDependencies": {
@@ -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
- if (!expiresAt) return null;
75
- const at = new Date(expiresAt).getTime();
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() <= EXPIRY_WARNING_WINDOW_MS ? 'NEAR' : 'SAFE';
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',