@learncard/email-templates 1.2.0 → 1.2.2
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/dist/i18n.d.ts +15 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +31 -0
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/previews/account-secured-scenarios.d.ts +6 -0
- package/dist/previews/account-secured-scenarios.d.ts.map +1 -0
- package/dist/previews/account-secured-scenarios.js +7 -0
- package/dist/previews/account-secured-scenarios.js.map +1 -0
- package/dist/previews/escrow-hold-cancelled-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-cancelled-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-cancelled-scenarios.js +7 -0
- package/dist/previews/escrow-hold-cancelled-scenarios.js.map +1 -0
- package/dist/previews/escrow-hold-released-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-released-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-released-scenarios.js +7 -0
- package/dist/previews/escrow-hold-released-scenarios.js.map +1 -0
- package/dist/previews/escrow-hold-reminder-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-reminder-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-reminder-scenarios.js +7 -0
- package/dist/previews/escrow-hold-reminder-scenarios.js.map +1 -0
- package/dist/previews/escrow-hold-started-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-started-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-started-scenarios.js +7 -0
- package/dist/previews/escrow-hold-started-scenarios.js.map +1 -0
- package/dist/previews/escrow-pin-locked-scenarios.d.ts +6 -0
- package/dist/previews/escrow-pin-locked-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-pin-locked-scenarios.js +7 -0
- package/dist/previews/escrow-pin-locked-scenarios.js.map +1 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.d.ts +6 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.js +7 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.js.map +1 -0
- package/dist/render.d.ts +45 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +67 -1
- package/dist/render.js.map +1 -1
- package/dist/templates/account-secured.d.ts +27 -0
- package/dist/templates/account-secured.d.ts.map +1 -0
- package/dist/templates/account-secured.js +118 -0
- package/dist/templates/account-secured.js.map +1 -0
- package/dist/templates/escrow-hold-cancelled.d.ts +23 -0
- package/dist/templates/escrow-hold-cancelled.d.ts.map +1 -0
- package/dist/templates/escrow-hold-cancelled.js +131 -0
- package/dist/templates/escrow-hold-cancelled.js.map +1 -0
- package/dist/templates/escrow-hold-released.d.ts +21 -0
- package/dist/templates/escrow-hold-released.d.ts.map +1 -0
- package/dist/templates/escrow-hold-released.js +92 -0
- package/dist/templates/escrow-hold-released.js.map +1 -0
- package/dist/templates/escrow-hold-reminder.d.ts +21 -0
- package/dist/templates/escrow-hold-reminder.d.ts.map +1 -0
- package/dist/templates/escrow-hold-reminder.js +92 -0
- package/dist/templates/escrow-hold-reminder.js.map +1 -0
- package/dist/templates/escrow-hold-started.d.ts +25 -0
- package/dist/templates/escrow-hold-started.d.ts.map +1 -0
- package/dist/templates/escrow-hold-started.js +96 -0
- package/dist/templates/escrow-hold-started.js.map +1 -0
- package/dist/templates/escrow-pin-locked.d.ts +24 -0
- package/dist/templates/escrow-pin-locked.d.ts.map +1 -0
- package/dist/templates/escrow-pin-locked.js +97 -0
- package/dist/templates/escrow-pin-locked.js.map +1 -0
- package/dist/templates/escrow-pin-sign-in.d.ts +24 -0
- package/dist/templates/escrow-pin-sign-in.d.ts.map +1 -0
- package/dist/templates/escrow-pin-sign-in.js +96 -0
- package/dist/templates/escrow-pin-sign-in.js.map +1 -0
- package/dist/templates/index.d.ts +21 -28
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +20 -9
- package/dist/templates/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/i18n.test.ts +62 -1
- package/src/__tests__/render.test.ts +306 -0
- package/src/i18n.ts +33 -0
- package/src/index.ts +8 -0
- package/src/previews/account-secured-scenarios.tsx +41 -0
- package/src/previews/escrow-hold-cancelled-scenarios.tsx +79 -0
- package/src/previews/escrow-hold-released-scenarios.tsx +58 -0
- package/src/previews/escrow-hold-reminder-scenarios.tsx +52 -0
- package/src/previews/escrow-hold-started-scenarios.tsx +67 -0
- package/src/previews/escrow-pin-locked-scenarios.tsx +60 -0
- package/src/previews/escrow-pin-sign-in-scenarios.tsx +52 -0
- package/src/render.ts +162 -0
- package/src/templates/account-secured.tsx +236 -0
- package/src/templates/escrow-hold-cancelled.tsx +212 -0
- package/src/templates/escrow-hold-released.tsx +184 -0
- package/src/templates/escrow-hold-reminder.tsx +184 -0
- package/src/templates/escrow-hold-started.tsx +216 -0
- package/src/templates/escrow-pin-locked.tsx +233 -0
- package/src/templates/escrow-pin-sign-in.tsx +197 -0
- package/src/templates/index.ts +100 -25
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escrow-pin-locked — all preview scenarios stacked.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
import { EscrowPinLocked } from '../templates/escrow-pin-locked';
|
|
8
|
+
import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
|
|
9
|
+
|
|
10
|
+
export default function EscrowPinLockedScenarios() {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<ScenarioDivider label="With pending waiting-period recovery (cancel link)" />
|
|
14
|
+
<EscrowPinLocked
|
|
15
|
+
branding={LEARNCARD_BRANDING}
|
|
16
|
+
lockedAt="September 25, 2026, 3:00 PM"
|
|
17
|
+
releaseAfter="October 2, 2026, 3:00 PM"
|
|
18
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<ScenarioDivider label="No pending recovery" />
|
|
22
|
+
<EscrowPinLocked branding={LEARNCARD_BRANDING} lockedAt="September 25, 2026, 3:00 PM" />
|
|
23
|
+
|
|
24
|
+
<ScenarioDivider label="Spanish" />
|
|
25
|
+
<EscrowPinLocked
|
|
26
|
+
branding={LEARNCARD_BRANDING}
|
|
27
|
+
lockedAt="25 de septiembre de 2026, 3:00 p.m."
|
|
28
|
+
releaseAfter="2 de octubre de 2026, 3:00 p.m."
|
|
29
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
30
|
+
locale="es"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<ScenarioDivider label="French" />
|
|
34
|
+
<EscrowPinLocked
|
|
35
|
+
branding={LEARNCARD_BRANDING}
|
|
36
|
+
lockedAt="25 septembre 2026, 15h00"
|
|
37
|
+
releaseAfter="2 octobre 2026, 15h00"
|
|
38
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
39
|
+
locale="fr"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<ScenarioDivider label="Arabic (RTL)" />
|
|
43
|
+
<EscrowPinLocked
|
|
44
|
+
branding={LEARNCARD_BRANDING}
|
|
45
|
+
lockedAt="25 سبتمبر 2026، 3:00 م"
|
|
46
|
+
releaseAfter="2 أكتوبر 2026، 3:00 م"
|
|
47
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
48
|
+
locale="ar"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<ScenarioDivider label="VetPass tenant branding" />
|
|
52
|
+
<EscrowPinLocked
|
|
53
|
+
branding={VETPASS_BRANDING}
|
|
54
|
+
lockedAt="September 25, 2026, 3:00 PM"
|
|
55
|
+
releaseAfter="October 2, 2026, 3:00 PM"
|
|
56
|
+
cancelUrl="https://vetpass.app/recovery/cancel?token=abc123"
|
|
57
|
+
/>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escrow-pin-sign-in — all preview scenarios stacked.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
import { EscrowPinSignIn } from '../templates/escrow-pin-sign-in';
|
|
8
|
+
import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
|
|
9
|
+
|
|
10
|
+
export default function EscrowPinSignInScenarios() {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<ScenarioDivider label="Default (LearnCard)" />
|
|
14
|
+
<EscrowPinSignIn
|
|
15
|
+
branding={LEARNCARD_BRANDING}
|
|
16
|
+
signedInAt="October 2, 2026, 3:00 PM"
|
|
17
|
+
secureUrl="https://learncard.app/account/secure"
|
|
18
|
+
/>
|
|
19
|
+
|
|
20
|
+
<ScenarioDivider label="Spanish" />
|
|
21
|
+
<EscrowPinSignIn
|
|
22
|
+
branding={LEARNCARD_BRANDING}
|
|
23
|
+
signedInAt="2 de octubre de 2026, 3:00 p.m."
|
|
24
|
+
secureUrl="https://learncard.app/account/secure"
|
|
25
|
+
locale="es"
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<ScenarioDivider label="French" />
|
|
29
|
+
<EscrowPinSignIn
|
|
30
|
+
branding={LEARNCARD_BRANDING}
|
|
31
|
+
signedInAt="2 octobre 2026, 15h00"
|
|
32
|
+
secureUrl="https://learncard.app/account/secure"
|
|
33
|
+
locale="fr"
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<ScenarioDivider label="Arabic (RTL)" />
|
|
37
|
+
<EscrowPinSignIn
|
|
38
|
+
branding={LEARNCARD_BRANDING}
|
|
39
|
+
signedInAt="2 أكتوبر 2026، 3:00 م"
|
|
40
|
+
secureUrl="https://learncard.app/account/secure"
|
|
41
|
+
locale="ar"
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<ScenarioDivider label="VetPass tenant branding" />
|
|
45
|
+
<EscrowPinSignIn
|
|
46
|
+
branding={VETPASS_BRANDING}
|
|
47
|
+
signedInAt="October 2, 2026, 3:00 PM"
|
|
48
|
+
secureUrl="https://vetpass.app/account/secure"
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -43,6 +43,20 @@ import {
|
|
|
43
43
|
getCredentialUpdatedSubject,
|
|
44
44
|
AccountSignInChanged,
|
|
45
45
|
getAccountSignInChangedSubject,
|
|
46
|
+
EscrowHoldStarted,
|
|
47
|
+
getEscrowHoldStartedSubject,
|
|
48
|
+
EscrowHoldReminder,
|
|
49
|
+
getEscrowHoldReminderSubject,
|
|
50
|
+
EscrowHoldReleased,
|
|
51
|
+
getEscrowHoldReleasedSubject,
|
|
52
|
+
EscrowHoldCancelled,
|
|
53
|
+
getEscrowHoldCancelledSubject,
|
|
54
|
+
EscrowPinLocked,
|
|
55
|
+
getEscrowPinLockedSubject,
|
|
56
|
+
EscrowPinSignIn,
|
|
57
|
+
getEscrowPinSignInSubject,
|
|
58
|
+
AccountSecured,
|
|
59
|
+
getAccountSecuredSubject,
|
|
46
60
|
} from './templates';
|
|
47
61
|
|
|
48
62
|
import type {
|
|
@@ -59,7 +73,16 @@ import type {
|
|
|
59
73
|
EmailVerificationProps,
|
|
60
74
|
CredentialUpdatedProps,
|
|
61
75
|
AccountSignInChangedProps,
|
|
76
|
+
EscrowHoldStartedProps,
|
|
77
|
+
EscrowHoldReminderProps,
|
|
78
|
+
EscrowHoldReleasedProps,
|
|
79
|
+
EscrowHoldCancelledProps,
|
|
80
|
+
EscrowHoldCancelledReason,
|
|
81
|
+
EscrowPinLockedProps,
|
|
82
|
+
EscrowPinSignInProps,
|
|
83
|
+
AccountSecuredProps,
|
|
62
84
|
} from './templates';
|
|
85
|
+
import { localeProp } from './i18n';
|
|
63
86
|
|
|
64
87
|
// ---------------------------------------------------------------------------
|
|
65
88
|
// Public types
|
|
@@ -157,6 +180,29 @@ export interface TemplateDataMap {
|
|
|
157
180
|
'credential-updated': CredentialUpdatedData;
|
|
158
181
|
/** lca-api: lost-login identity rebind security notification */
|
|
159
182
|
'account-sign-in-changed': AccountSignInChangedData;
|
|
183
|
+
|
|
184
|
+
// -- Recovery hold lifecycle (nitro-escrow-enclave) ----------------------
|
|
185
|
+
|
|
186
|
+
/** lca-api: recovery hold started — includes a cancel link */
|
|
187
|
+
'escrow-hold-started': EscrowHoldStartedData;
|
|
188
|
+
|
|
189
|
+
/** lca-api: recovery hold reminder (T-24h) — includes a cancel link */
|
|
190
|
+
'escrow-hold-reminder': EscrowHoldReminderData;
|
|
191
|
+
|
|
192
|
+
/** lca-api: recovery hold completed and access was restored */
|
|
193
|
+
'escrow-hold-released': EscrowHoldReleasedData;
|
|
194
|
+
|
|
195
|
+
/** lca-api: recovery hold cancelled (user, superseded, PIN lock, or failure) */
|
|
196
|
+
'escrow-hold-cancelled': EscrowHoldCancelledData;
|
|
197
|
+
|
|
198
|
+
/** lca-api: recovery PIN locked after too many failed attempts */
|
|
199
|
+
'escrow-pin-locked': EscrowPinLockedData;
|
|
200
|
+
|
|
201
|
+
/** lca-api: a new device signed in with the recovery PIN — links to "Secure my account" */
|
|
202
|
+
'escrow-pin-sign-in': EscrowPinSignInData;
|
|
203
|
+
|
|
204
|
+
/** lca-api: "Secure my account" signed out every other device and login */
|
|
205
|
+
'account-secured': AccountSecuredData;
|
|
160
206
|
}
|
|
161
207
|
|
|
162
208
|
export type TemplateId = keyof TemplateDataMap;
|
|
@@ -243,6 +289,43 @@ export interface CredentialUpdatedData {
|
|
|
243
289
|
|
|
244
290
|
export type AccountSignInChangedData = Record<string, never>;
|
|
245
291
|
|
|
292
|
+
export interface EscrowHoldStartedData {
|
|
293
|
+
requestedAt: string;
|
|
294
|
+
releaseAfter: string;
|
|
295
|
+
cancelUrl: string;
|
|
296
|
+
deviceHint?: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface EscrowHoldReminderData {
|
|
300
|
+
releaseAfter: string;
|
|
301
|
+
cancelUrl: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface EscrowHoldReleasedData {
|
|
305
|
+
completedAt: string;
|
|
306
|
+
supportUrl?: string;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface EscrowHoldCancelledData {
|
|
310
|
+
cancelledAt: string;
|
|
311
|
+
reason?: EscrowHoldCancelledReason;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface EscrowPinLockedData {
|
|
315
|
+
lockedAt: string;
|
|
316
|
+
releaseAfter?: string;
|
|
317
|
+
cancelUrl?: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface EscrowPinSignInData {
|
|
321
|
+
signedInAt: string;
|
|
322
|
+
secureUrl: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface AccountSecuredData {
|
|
326
|
+
securedAt: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
246
329
|
// ---------------------------------------------------------------------------
|
|
247
330
|
// renderEmail()
|
|
248
331
|
// ---------------------------------------------------------------------------
|
|
@@ -459,6 +542,85 @@ function buildElement(
|
|
|
459
542
|
};
|
|
460
543
|
}
|
|
461
544
|
|
|
545
|
+
case 'escrow-hold-started': {
|
|
546
|
+
const d = data as EscrowHoldStartedData;
|
|
547
|
+
const props: EscrowHoldStartedProps = { branding, ...d, locale };
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
element: React.createElement(EscrowHoldStarted, props),
|
|
551
|
+
subject: getEscrowHoldStartedSubject(branding, locale),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
case 'escrow-hold-reminder': {
|
|
556
|
+
const d = data as EscrowHoldReminderData;
|
|
557
|
+
const props: EscrowHoldReminderProps = { branding, ...d, locale };
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
element: React.createElement(EscrowHoldReminder, props),
|
|
561
|
+
subject: getEscrowHoldReminderSubject(branding, locale),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
case 'escrow-hold-released': {
|
|
566
|
+
const d = data as EscrowHoldReleasedData;
|
|
567
|
+
const props: EscrowHoldReleasedProps = { branding, ...d, locale };
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
element: React.createElement(EscrowHoldReleased, props),
|
|
571
|
+
subject: getEscrowHoldReleasedSubject(branding, locale),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case 'escrow-hold-cancelled': {
|
|
576
|
+
const d = data as EscrowHoldCancelledData;
|
|
577
|
+
const props: EscrowHoldCancelledProps = { branding, ...d, locale };
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
element: React.createElement(EscrowHoldCancelled, props),
|
|
581
|
+
subject: getEscrowHoldCancelledSubject(branding, locale),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
case 'escrow-pin-locked': {
|
|
586
|
+
const d = data as EscrowPinLockedData;
|
|
587
|
+
const props: EscrowPinLockedProps = { branding, ...d, locale };
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
element: React.createElement(EscrowPinLocked, props),
|
|
591
|
+
subject: getEscrowPinLockedSubject(branding, locale),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
case 'escrow-pin-sign-in': {
|
|
596
|
+
const d = data as EscrowPinSignInData;
|
|
597
|
+
const props: EscrowPinSignInProps = {
|
|
598
|
+
branding,
|
|
599
|
+
signedInAt: d.signedInAt,
|
|
600
|
+
secureUrl: d.secureUrl,
|
|
601
|
+
...localeProp(locale),
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
element: React.createElement(EscrowPinSignIn, props),
|
|
606
|
+
subject: getEscrowPinSignInSubject(branding, locale),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
case 'account-secured': {
|
|
611
|
+
const d = data as AccountSecuredData;
|
|
612
|
+
const props: AccountSecuredProps = {
|
|
613
|
+
branding,
|
|
614
|
+
securedAt: d.securedAt,
|
|
615
|
+
...localeProp(locale),
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
element: React.createElement(AccountSecured, props),
|
|
620
|
+
subject: getAccountSecuredSubject(branding, locale),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
462
624
|
default: {
|
|
463
625
|
const _exhaustive: never = templateId;
|
|
464
626
|
throw new Error(`Unknown template: ${_exhaustive}`);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccountSecured — tells the account owner that "Secure my account" ran: every other
|
|
3
|
+
* device and login was signed out and the recovery methods have to be set up again.
|
|
4
|
+
*
|
|
5
|
+
* The recipient may be an owner who did NOT do this (someone with their login and
|
|
6
|
+
* PIN ran it first) and whose own login was unlinked. So the copy never relies on the
|
|
7
|
+
* in-app "Secure my account" link. It also does not offer the app's lost-login path
|
|
8
|
+
* as the way back: the revoke itself returned every recovery method to pending and
|
|
9
|
+
* removed automatic recovery, so that path usually has nothing left to use. It sends
|
|
10
|
+
* the owner to the tenant's support address, or, when the tenant has none, to the
|
|
11
|
+
* organization that gave them access (no address is made up).
|
|
12
|
+
*
|
|
13
|
+
* Used by: lca-api `POST /keys/revoke-other-devices` (templateId: 'account-secured')
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Text, Section } from '@react-email/components';
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
|
|
19
|
+
import type { TenantBranding } from '../branding';
|
|
20
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
21
|
+
import { Layout } from '../components/Layout';
|
|
22
|
+
import { EmailButton } from '../components/EmailButton';
|
|
23
|
+
import { LinkFallback } from '../components/LinkFallback';
|
|
24
|
+
import type { NotificationLocale } from '../i18n';
|
|
25
|
+
import {
|
|
26
|
+
resolveCatalogLocale,
|
|
27
|
+
interpolate,
|
|
28
|
+
SHARED,
|
|
29
|
+
formatNoticeTimestamp,
|
|
30
|
+
localeProp,
|
|
31
|
+
} from '../i18n';
|
|
32
|
+
|
|
33
|
+
export interface AccountSecuredProps {
|
|
34
|
+
branding: TenantBranding;
|
|
35
|
+
/** ISO timestamp of the revoke (formatted per locale), or display text. */
|
|
36
|
+
securedAt: string;
|
|
37
|
+
/** Recipient locale (BCP-47). Defaults to English. */
|
|
38
|
+
locale?: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Strings = {
|
|
42
|
+
preview: string;
|
|
43
|
+
heading: string;
|
|
44
|
+
body: (securedAt: string, brandName: string) => React.ReactNode;
|
|
45
|
+
ifYou: string;
|
|
46
|
+
notYouWarning: string;
|
|
47
|
+
resetWarning: string;
|
|
48
|
+
support: string;
|
|
49
|
+
supportFallback: string;
|
|
50
|
+
button: string;
|
|
51
|
+
sincerely: string;
|
|
52
|
+
subject: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const EN: Strings = {
|
|
56
|
+
preview: 'Other devices were signed out of your account',
|
|
57
|
+
heading: 'Your Account Was Secured',
|
|
58
|
+
body: (securedAt, brandName) => (
|
|
59
|
+
<>
|
|
60
|
+
On <strong>{securedAt}</strong>, someone signed in to your {brandName} account signed
|
|
61
|
+
out every other device and login. The account’s recovery methods now need to be set up
|
|
62
|
+
again.
|
|
63
|
+
</>
|
|
64
|
+
),
|
|
65
|
+
ifYou: 'If this was you, there is nothing else to do here. Finish setting up your recovery methods in the app.',
|
|
66
|
+
notYouWarning:
|
|
67
|
+
'If this wasn’t you, someone else may have access to your account, and your own sign-in may have been removed.',
|
|
68
|
+
resetWarning:
|
|
69
|
+
'Securing the account also reset its recovery methods, so your recovery phrase, backup, passkey or recovery PIN may no longer get you back in.',
|
|
70
|
+
support: 'Contact {supportEmail} right away for help getting back into your account.',
|
|
71
|
+
supportFallback:
|
|
72
|
+
'Contact the organization that gave you access to {brandName} right away for help getting back into your account.',
|
|
73
|
+
button: 'Open {brandName}',
|
|
74
|
+
sincerely: 'Sincerely,',
|
|
75
|
+
subject: 'Other devices were signed out of your account',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const ES: Strings = {
|
|
79
|
+
preview: 'Se cerró la sesión de otros dispositivos en tu cuenta',
|
|
80
|
+
heading: 'Tu Cuenta Fue Protegida',
|
|
81
|
+
body: (securedAt, brandName) => (
|
|
82
|
+
<>
|
|
83
|
+
El <strong>{securedAt}</strong>, alguien con sesión iniciada en tu cuenta de {brandName}{' '}
|
|
84
|
+
cerró la sesión de todos los demás dispositivos e inicios de sesión. Ahora hay que
|
|
85
|
+
volver a configurar los métodos de recuperación de la cuenta.
|
|
86
|
+
</>
|
|
87
|
+
),
|
|
88
|
+
ifYou: 'Si fuiste tú, no tienes que hacer nada más aquí. Termina de configurar tus métodos de recuperación en la aplicación.',
|
|
89
|
+
notYouWarning:
|
|
90
|
+
'Si no fuiste tú, es posible que otra persona tenga acceso a tu cuenta y que se haya eliminado tu propio inicio de sesión.',
|
|
91
|
+
resetWarning:
|
|
92
|
+
'Al proteger la cuenta también se restablecieron sus métodos de recuperación, así que es posible que tu frase de recuperación, copia de seguridad, llave de acceso o PIN de recuperación ya no te permitan volver a entrar.',
|
|
93
|
+
support:
|
|
94
|
+
'Escribe cuanto antes a {supportEmail} para que te ayuden a recuperar el acceso a tu cuenta.',
|
|
95
|
+
supportFallback:
|
|
96
|
+
'Comunícate cuanto antes con la organización que te dio acceso a {brandName} para que te ayuden a recuperar el acceso a tu cuenta.',
|
|
97
|
+
button: 'Abrir {brandName}',
|
|
98
|
+
sincerely: 'Atentamente,',
|
|
99
|
+
subject: 'Se cerró la sesión de otros dispositivos en tu cuenta',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const FR: Strings = {
|
|
103
|
+
preview: 'Les autres appareils ont été déconnectés de votre compte',
|
|
104
|
+
heading: 'Votre Compte a Été Sécurisé',
|
|
105
|
+
body: (securedAt, brandName) => (
|
|
106
|
+
<>
|
|
107
|
+
Le <strong>{securedAt}</strong>, une personne connectée à votre compte {brandName} a
|
|
108
|
+
déconnecté tous les autres appareils et connexions. Les méthodes de récupération du
|
|
109
|
+
compte doivent maintenant être configurées à nouveau.
|
|
110
|
+
</>
|
|
111
|
+
),
|
|
112
|
+
ifYou: 'Si c’était vous, vous n’avez rien d’autre à faire ici. Terminez la configuration de vos méthodes de récupération dans l’application.',
|
|
113
|
+
notYouWarning:
|
|
114
|
+
'Si ce n’était pas vous, quelqu’un d’autre a peut-être accès à votre compte, et votre propre connexion a peut-être été supprimée.',
|
|
115
|
+
resetWarning:
|
|
116
|
+
'La sécurisation du compte a aussi réinitialisé ses méthodes de récupération : votre phrase de récupération, votre sauvegarde, votre clé d’accès ou votre code PIN de récupération ne vous permettront peut-être plus de vous reconnecter.',
|
|
117
|
+
support:
|
|
118
|
+
'Écrivez sans attendre à {supportEmail} pour obtenir de l’aide afin de récupérer l’accès à votre compte.',
|
|
119
|
+
supportFallback:
|
|
120
|
+
'Contactez sans attendre l’organisation qui vous a donné accès à {brandName} pour obtenir de l’aide afin de récupérer l’accès à votre compte.',
|
|
121
|
+
button: 'Ouvrir {brandName}',
|
|
122
|
+
sincerely: 'Cordialement,',
|
|
123
|
+
subject: 'Les autres appareils ont été déconnectés de votre compte',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const AR: Strings = {
|
|
127
|
+
preview: 'تم تسجيل خروج الأجهزة الأخرى من حسابك',
|
|
128
|
+
heading: 'تم تأمين حسابك',
|
|
129
|
+
body: (securedAt, brandName) => (
|
|
130
|
+
<>
|
|
131
|
+
في <strong>{securedAt}</strong>، قام شخص مسجّل الدخول إلى حسابك في {brandName} بتسجيل
|
|
132
|
+
خروج جميع الأجهزة وعمليات تسجيل الدخول الأخرى. يجب الآن إعداد طرق استعادة الحساب من
|
|
133
|
+
جديد.
|
|
134
|
+
</>
|
|
135
|
+
),
|
|
136
|
+
ifYou: 'إذا كنت أنت، فلا داعي لفعل أي شيء آخر هنا. أكمل إعداد طرق الاستعادة في التطبيق.',
|
|
137
|
+
notYouWarning:
|
|
138
|
+
'إذا لم تكن أنت، فقد يكون لدى شخص آخر حق الوصول إلى حسابك، وربما تمت إزالة تسجيل الدخول الخاص بك.',
|
|
139
|
+
resetWarning:
|
|
140
|
+
'أدى تأمين الحساب أيضًا إلى إعادة تعيين طرق استعادته، لذلك قد لا تتمكن بعد الآن من العودة إلى حسابك باستخدام عبارة الاستعادة أو النسخة الاحتياطية أو مفتاح المرور أو رمز PIN للاستعادة.',
|
|
141
|
+
support: 'راسل {supportEmail} فورًا للحصول على مساعدة في استعادة الوصول إلى حسابك.',
|
|
142
|
+
supportFallback:
|
|
143
|
+
'تواصل فورًا مع الجهة التي منحتك الوصول إلى {brandName} للحصول على مساعدة في استعادة الوصول إلى حسابك.',
|
|
144
|
+
button: 'فتح {brandName}',
|
|
145
|
+
sincerely: 'مع التحيات،',
|
|
146
|
+
subject: 'تم تسجيل خروج الأجهزة الأخرى من حسابك',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const STRINGS: Record<NotificationLocale, Strings> = { en: EN, es: ES, fr: FR, ar: AR };
|
|
150
|
+
|
|
151
|
+
export const AccountSecured: React.FC<AccountSecuredProps> = ({ branding, securedAt, locale }) => {
|
|
152
|
+
const catalogLocale = resolveCatalogLocale(locale);
|
|
153
|
+
const s = STRINGS[catalogLocale];
|
|
154
|
+
const { brandName, supportEmail, appUrl } = branding;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Layout branding={branding} preview={s.preview} {...localeProp(locale)}>
|
|
158
|
+
<Text style={heading}>{s.heading}</Text>
|
|
159
|
+
|
|
160
|
+
<Text style={paragraph}>
|
|
161
|
+
{s.body(formatNoticeTimestamp(securedAt, catalogLocale), brandName)}
|
|
162
|
+
</Text>
|
|
163
|
+
|
|
164
|
+
<Text style={paragraph}>{s.ifYou}</Text>
|
|
165
|
+
|
|
166
|
+
<Text style={warning}>{s.notYouWarning}</Text>
|
|
167
|
+
|
|
168
|
+
<Text style={paragraph}>{s.resetWarning}</Text>
|
|
169
|
+
|
|
170
|
+
<Text style={paragraph}>
|
|
171
|
+
{supportEmail
|
|
172
|
+
? interpolate(s.support, { supportEmail })
|
|
173
|
+
: interpolate(s.supportFallback, { brandName })}
|
|
174
|
+
</Text>
|
|
175
|
+
|
|
176
|
+
<Section style={buttonWrapper}>
|
|
177
|
+
<EmailButton href={appUrl} branding={branding}>
|
|
178
|
+
{interpolate(s.button, { brandName })}
|
|
179
|
+
</EmailButton>
|
|
180
|
+
</Section>
|
|
181
|
+
|
|
182
|
+
<Text style={signOff}>
|
|
183
|
+
{s.sincerely}
|
|
184
|
+
<br />
|
|
185
|
+
{interpolate(SHARED[catalogLocale].teamSignature, { brandName })}
|
|
186
|
+
</Text>
|
|
187
|
+
|
|
188
|
+
<LinkFallback href={appUrl} {...localeProp(locale)} />
|
|
189
|
+
</Layout>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const getAccountSecuredSubject = (_branding: TenantBranding, locale?: string): string =>
|
|
194
|
+
STRINGS[resolveCatalogLocale(locale)].subject;
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Styles
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
const heading: React.CSSProperties = {
|
|
201
|
+
fontSize: 24,
|
|
202
|
+
fontWeight: 600,
|
|
203
|
+
color: '#111827',
|
|
204
|
+
margin: '0 0 24px',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const paragraph: React.CSSProperties = {
|
|
208
|
+
fontSize: 16,
|
|
209
|
+
color: '#374151',
|
|
210
|
+
lineHeight: '24px',
|
|
211
|
+
margin: '0 0 24px',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const warning: React.CSSProperties = {
|
|
215
|
+
...paragraph,
|
|
216
|
+
fontWeight: 600,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const buttonWrapper: React.CSSProperties = {
|
|
220
|
+
margin: '0 0 24px',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const signOff: React.CSSProperties = {
|
|
224
|
+
fontSize: 14,
|
|
225
|
+
color: '#374151',
|
|
226
|
+
lineHeight: '20px',
|
|
227
|
+
margin: '24px 0 0',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Preview
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
export default function Preview() {
|
|
235
|
+
return <AccountSecured branding={DEFAULT_BRANDING} securedAt="October 2, 2026, 3:00 PM" />;
|
|
236
|
+
}
|