@12-apps/payments-frontend 1.15.2 → 1.16.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": "1.15.2",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
5
  "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.",
6
6
  "exports": {
@@ -17,7 +17,7 @@
17
17
  "storybook:build": "storybook build"
18
18
  },
19
19
  "dependencies": {
20
- "@12-apps/payments-backend": "^1.15.2",
20
+ "@12-apps/payments-backend": "^1.16.0",
21
21
  "react-qr-code": "^2.2.0"
22
22
  },
23
23
  "peerDependencies": {
@@ -28,8 +28,8 @@
28
28
  "react-dom": ">=19.0.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@12-apps/eslint-config": "^1.17.2",
32
- "@12-apps/typescript-config": "^1.17.2",
31
+ "@12-apps/eslint-config": "^1.18.0",
32
+ "@12-apps/typescript-config": "^1.18.0",
33
33
  "@emotion/react": "^11.14.0",
34
34
  "@emotion/styled": "^11.14.0",
35
35
  "@mui/material": "^6.5.0",
@@ -22,7 +22,7 @@ import type {
22
22
  } from '@12-apps/payments-backend';
23
23
 
24
24
  import type { PaymentsSettingsClient } from '../client';
25
- import { isConnected } from './connection-state';
25
+ import { expiryProximity, isConnected } from './connection-state';
26
26
 
27
27
  /**
28
28
  * The `authMode: 'oauth'` half of the settings page: a provider whose
@@ -49,10 +49,35 @@ export interface ProviderConnectionProps {
49
49
  onChanged: () => void;
50
50
  }
51
51
 
52
- function expiryNote(expiresAt: string | null): string | null {
53
- if (!expiresAt) return null;
54
- const when = new Date(expiresAt);
55
- return `Autorização válida até ${when.toLocaleString('pt-BR')}`;
52
+ /**
53
+ * The expiry caption, with PROXIMITY emphasis (FUT-683): the renewal sweep
54
+ * normally refreshes a grant long before it lapses, so an expiry that is
55
+ * actually close means renewal has been failing quietly — and this caption is
56
+ * the only warning an owner gets before checkout starts refusing. A neutral
57
+ * gray line read the same on the eve of an outage as a year out.
58
+ */
59
+ function ExpiryNote(props: { expiresAt: string }) {
60
+ const when = new Date(props.expiresAt).toLocaleString('pt-BR');
61
+ const proximity = expiryProximity(props.expiresAt);
62
+ if (proximity === 'SAFE' || proximity === null) {
63
+ return (
64
+ <Typography variant="caption" color="text.secondary">
65
+ {`Autorização válida até ${when}`}
66
+ </Typography>
67
+ );
68
+ }
69
+ return (
70
+ <Typography
71
+ variant="caption"
72
+ color={proximity === 'PAST' ? 'error.main' : 'warning.main'}
73
+ sx={{ fontWeight: 600 }}
74
+ data-testid="payments-expiry-warning"
75
+ >
76
+ {proximity === 'PAST'
77
+ ? `A autorização expirou em ${when}. Reconecte para voltar a receber pagamentos.`
78
+ : `A autorização expira em ${when}. Se o aviso continuar, reconecte a conta.`}
79
+ </Typography>
80
+ );
56
81
  }
57
82
 
58
83
  function connectLabel(displayName: string, connected: boolean, busy: string | null) {
@@ -98,11 +123,7 @@ function ConnectionSummary(props: {
98
123
  A autorização expirou ou foi revogada. Reconecte para voltar a receber pagamentos.
99
124
  </Alert>
100
125
  ) : null}
101
- {props.expiresAt ? (
102
- <Typography variant="caption" color="text.secondary">
103
- {expiryNote(props.expiresAt)}
104
- </Typography>
105
- ) : null}
126
+ {props.expiresAt ? <ExpiryNote expiresAt={props.expiresAt} /> : null}
106
127
  </>
107
128
  );
108
129
  }
@@ -46,6 +46,37 @@ export function canAttemptCharge(config: MaskedProviderConfig | null | undefined
46
46
  return false;
47
47
  }
48
48
 
49
+ /**
50
+ * How close `expiresAt` is to becoming an outage, in buckets the UI can
51
+ * style (FUT-683).
52
+ *
53
+ * The host's renewal sweep normally refreshes a grant long before this ever
54
+ * says `NEAR` — so `NEAR` is not "working as intended", it is the sweep having
55
+ * failed quietly for days, and the caption is the only place an owner can see
56
+ * it coming before checkout starts refusing. `PAST` means the grant is already
57
+ * dead even if the status has not caught up yet.
58
+ */
59
+ type ExpiryProximity = 'SAFE' | 'NEAR' | 'PAST';
60
+
61
+ /**
62
+ * A week: several sweep cycles' worth of margin, so a single missed run never
63
+ * alarms anyone, while a renewal that keeps failing surfaces well before the
64
+ * grant actually lapses.
65
+ */
66
+ const EXPIRY_WARNING_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
67
+
68
+ /** Where `expiresAt` stands relative to `now`; null when it cannot expire. */
69
+ export function expiryProximity(
70
+ expiresAt: string | null | undefined,
71
+ now: Date = new Date(),
72
+ ): ExpiryProximity | null {
73
+ if (!expiresAt) return null;
74
+ const at = new Date(expiresAt).getTime();
75
+ if (Number.isNaN(at)) return null;
76
+ if (at <= now.getTime()) return 'PAST';
77
+ return at - now.getTime() <= EXPIRY_WARNING_WINDOW_MS ? 'NEAR' : 'SAFE';
78
+ }
79
+
49
80
  /** What a provider card reports at a glance. */
50
81
  type ConnectionBadge =
51
82
  | { label: 'Ativo'; color: 'success' }