@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.
Files changed (92) hide show
  1. package/dist/i18n.d.ts +15 -0
  2. package/dist/i18n.d.ts.map +1 -1
  3. package/dist/i18n.js +31 -0
  4. package/dist/i18n.js.map +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/previews/account-secured-scenarios.d.ts +6 -0
  9. package/dist/previews/account-secured-scenarios.d.ts.map +1 -0
  10. package/dist/previews/account-secured-scenarios.js +7 -0
  11. package/dist/previews/account-secured-scenarios.js.map +1 -0
  12. package/dist/previews/escrow-hold-cancelled-scenarios.d.ts +6 -0
  13. package/dist/previews/escrow-hold-cancelled-scenarios.d.ts.map +1 -0
  14. package/dist/previews/escrow-hold-cancelled-scenarios.js +7 -0
  15. package/dist/previews/escrow-hold-cancelled-scenarios.js.map +1 -0
  16. package/dist/previews/escrow-hold-released-scenarios.d.ts +6 -0
  17. package/dist/previews/escrow-hold-released-scenarios.d.ts.map +1 -0
  18. package/dist/previews/escrow-hold-released-scenarios.js +7 -0
  19. package/dist/previews/escrow-hold-released-scenarios.js.map +1 -0
  20. package/dist/previews/escrow-hold-reminder-scenarios.d.ts +6 -0
  21. package/dist/previews/escrow-hold-reminder-scenarios.d.ts.map +1 -0
  22. package/dist/previews/escrow-hold-reminder-scenarios.js +7 -0
  23. package/dist/previews/escrow-hold-reminder-scenarios.js.map +1 -0
  24. package/dist/previews/escrow-hold-started-scenarios.d.ts +6 -0
  25. package/dist/previews/escrow-hold-started-scenarios.d.ts.map +1 -0
  26. package/dist/previews/escrow-hold-started-scenarios.js +7 -0
  27. package/dist/previews/escrow-hold-started-scenarios.js.map +1 -0
  28. package/dist/previews/escrow-pin-locked-scenarios.d.ts +6 -0
  29. package/dist/previews/escrow-pin-locked-scenarios.d.ts.map +1 -0
  30. package/dist/previews/escrow-pin-locked-scenarios.js +7 -0
  31. package/dist/previews/escrow-pin-locked-scenarios.js.map +1 -0
  32. package/dist/previews/escrow-pin-sign-in-scenarios.d.ts +6 -0
  33. package/dist/previews/escrow-pin-sign-in-scenarios.d.ts.map +1 -0
  34. package/dist/previews/escrow-pin-sign-in-scenarios.js +7 -0
  35. package/dist/previews/escrow-pin-sign-in-scenarios.js.map +1 -0
  36. package/dist/render.d.ts +45 -0
  37. package/dist/render.d.ts.map +1 -1
  38. package/dist/render.js +67 -1
  39. package/dist/render.js.map +1 -1
  40. package/dist/templates/account-secured.d.ts +27 -0
  41. package/dist/templates/account-secured.d.ts.map +1 -0
  42. package/dist/templates/account-secured.js +118 -0
  43. package/dist/templates/account-secured.js.map +1 -0
  44. package/dist/templates/escrow-hold-cancelled.d.ts +23 -0
  45. package/dist/templates/escrow-hold-cancelled.d.ts.map +1 -0
  46. package/dist/templates/escrow-hold-cancelled.js +131 -0
  47. package/dist/templates/escrow-hold-cancelled.js.map +1 -0
  48. package/dist/templates/escrow-hold-released.d.ts +21 -0
  49. package/dist/templates/escrow-hold-released.d.ts.map +1 -0
  50. package/dist/templates/escrow-hold-released.js +92 -0
  51. package/dist/templates/escrow-hold-released.js.map +1 -0
  52. package/dist/templates/escrow-hold-reminder.d.ts +21 -0
  53. package/dist/templates/escrow-hold-reminder.d.ts.map +1 -0
  54. package/dist/templates/escrow-hold-reminder.js +92 -0
  55. package/dist/templates/escrow-hold-reminder.js.map +1 -0
  56. package/dist/templates/escrow-hold-started.d.ts +25 -0
  57. package/dist/templates/escrow-hold-started.d.ts.map +1 -0
  58. package/dist/templates/escrow-hold-started.js +96 -0
  59. package/dist/templates/escrow-hold-started.js.map +1 -0
  60. package/dist/templates/escrow-pin-locked.d.ts +24 -0
  61. package/dist/templates/escrow-pin-locked.d.ts.map +1 -0
  62. package/dist/templates/escrow-pin-locked.js +97 -0
  63. package/dist/templates/escrow-pin-locked.js.map +1 -0
  64. package/dist/templates/escrow-pin-sign-in.d.ts +24 -0
  65. package/dist/templates/escrow-pin-sign-in.d.ts.map +1 -0
  66. package/dist/templates/escrow-pin-sign-in.js +96 -0
  67. package/dist/templates/escrow-pin-sign-in.js.map +1 -0
  68. package/dist/templates/index.d.ts +21 -28
  69. package/dist/templates/index.d.ts.map +1 -1
  70. package/dist/templates/index.js +20 -9
  71. package/dist/templates/index.js.map +1 -1
  72. package/package.json +2 -2
  73. package/src/__tests__/i18n.test.ts +62 -1
  74. package/src/__tests__/render.test.ts +306 -0
  75. package/src/i18n.ts +33 -0
  76. package/src/index.ts +8 -0
  77. package/src/previews/account-secured-scenarios.tsx +41 -0
  78. package/src/previews/escrow-hold-cancelled-scenarios.tsx +79 -0
  79. package/src/previews/escrow-hold-released-scenarios.tsx +58 -0
  80. package/src/previews/escrow-hold-reminder-scenarios.tsx +52 -0
  81. package/src/previews/escrow-hold-started-scenarios.tsx +67 -0
  82. package/src/previews/escrow-pin-locked-scenarios.tsx +60 -0
  83. package/src/previews/escrow-pin-sign-in-scenarios.tsx +52 -0
  84. package/src/render.ts +162 -0
  85. package/src/templates/account-secured.tsx +236 -0
  86. package/src/templates/escrow-hold-cancelled.tsx +212 -0
  87. package/src/templates/escrow-hold-released.tsx +184 -0
  88. package/src/templates/escrow-hold-reminder.tsx +184 -0
  89. package/src/templates/escrow-hold-started.tsx +216 -0
  90. package/src/templates/escrow-pin-locked.tsx +233 -0
  91. package/src/templates/escrow-pin-sign-in.tsx +197 -0
  92. package/src/templates/index.ts +100 -25
@@ -0,0 +1,212 @@
1
+ /**
2
+ * EscrowHoldCancelled — confirms that a pending account recovery request was
3
+ * cancelled and no further action will be taken.
4
+ *
5
+ * Used by: lca-api recovery hold routes (templateId: 'escrow-hold-cancelled')
6
+ */
7
+
8
+ import { Text } from '@react-email/components';
9
+ import * as React from 'react';
10
+
11
+ import type { TenantBranding } from '../branding';
12
+ import { DEFAULT_BRANDING } from '../branding';
13
+ import { Layout } from '../components/Layout';
14
+ import type { NotificationLocale } from '../i18n';
15
+ import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
16
+
17
+ /** Why the pending recovery request was cancelled. */
18
+ export type EscrowHoldCancelledReason = 'user' | 'superseded' | 'pin-locked' | 'release-failed';
19
+
20
+ export interface EscrowHoldCancelledProps {
21
+ branding: TenantBranding;
22
+ /** Human-readable timestamp the request was cancelled. */
23
+ cancelledAt: string;
24
+ /** Why the request was cancelled. Omit for a generic message. */
25
+ reason?: EscrowHoldCancelledReason;
26
+ /** Recipient locale (BCP-47). Defaults to English. */
27
+ locale?: string;
28
+ }
29
+
30
+ type Strings = {
31
+ preview: string;
32
+ heading: string;
33
+ body: (cancelledAt: string) => React.ReactNode;
34
+ reasonText: (reason?: EscrowHoldCancelledReason) => string;
35
+ sincerely: string;
36
+ subject: string;
37
+ };
38
+
39
+ const EN: Strings = {
40
+ preview: 'Your recovery request was cancelled',
41
+ heading: 'Recovery Request Cancelled',
42
+ body: cancelledAt => (
43
+ <>
44
+ Your account recovery request was cancelled on <strong>{cancelledAt}</strong>.
45
+ </>
46
+ ),
47
+ reasonText: reason => {
48
+ switch (reason) {
49
+ case 'user':
50
+ return 'You cancelled this request. No further action is needed.';
51
+ case 'superseded':
52
+ return 'A newer recovery request replaced this one.';
53
+ case 'pin-locked':
54
+ return 'This request was cancelled after too many incorrect PIN attempts.';
55
+ case 'release-failed':
56
+ return 'This request could not be completed and was cancelled automatically.';
57
+ default:
58
+ return 'No further action is needed.';
59
+ }
60
+ },
61
+ sincerely: 'Sincerely,',
62
+ subject: 'Recovery request cancelled',
63
+ };
64
+
65
+ const ES: Strings = {
66
+ preview: 'Se canceló tu solicitud de recuperación',
67
+ heading: 'Solicitud de Recuperación Cancelada',
68
+ body: cancelledAt => (
69
+ <>
70
+ Tu solicitud de recuperación de cuenta se canceló el <strong>{cancelledAt}</strong>.
71
+ </>
72
+ ),
73
+ reasonText: reason => {
74
+ switch (reason) {
75
+ case 'user':
76
+ return 'Cancelaste esta solicitud. No se requiere ninguna otra acción.';
77
+ case 'superseded':
78
+ return 'Una solicitud de recuperación más reciente reemplazó a esta.';
79
+ case 'pin-locked':
80
+ return 'Esta solicitud se canceló tras demasiados intentos incorrectos de PIN.';
81
+ case 'release-failed':
82
+ return 'Esta solicitud no pudo completarse y se canceló automáticamente.';
83
+ default:
84
+ return 'No se requiere ninguna otra acción.';
85
+ }
86
+ },
87
+ sincerely: 'Atentamente,',
88
+ subject: 'Solicitud de recuperación cancelada',
89
+ };
90
+
91
+ const FR: Strings = {
92
+ preview: 'Votre demande de récupération a été annulée',
93
+ heading: 'Demande de Récupération Annulée',
94
+ body: cancelledAt => (
95
+ <>
96
+ Votre demande de récupération de compte a été annulée le <strong>{cancelledAt}</strong>.
97
+ </>
98
+ ),
99
+ reasonText: reason => {
100
+ switch (reason) {
101
+ case 'user':
102
+ return 'Vous avez annulé cette demande. Aucune autre action n\u2019est nécessaire.';
103
+ case 'superseded':
104
+ return 'Une demande de récupération plus récente a remplacé celle-ci.';
105
+ case 'pin-locked':
106
+ return 'Cette demande a été annulée après trop de tentatives de code PIN incorrectes.';
107
+ case 'release-failed':
108
+ return 'Cette demande n\u2019a pas pu aboutir et a été annulée automatiquement.';
109
+ default:
110
+ return 'Aucune autre action n\u2019est nécessaire.';
111
+ }
112
+ },
113
+ sincerely: 'Cordialement,',
114
+ subject: 'Demande de récupération annulée',
115
+ };
116
+
117
+ const AR: Strings = {
118
+ preview: 'تم إلغاء طلب الاستعادة الخاص بك',
119
+ heading: 'تم إلغاء طلب الاستعادة',
120
+ body: cancelledAt => (
121
+ <>
122
+ تم إلغاء طلب استعادة حسابك في <strong>{cancelledAt}</strong>.
123
+ </>
124
+ ),
125
+ reasonText: reason => {
126
+ switch (reason) {
127
+ case 'user':
128
+ return 'لقد ألغيت هذا الطلب. لا حاجة لأي إجراء آخر.';
129
+ case 'superseded':
130
+ return 'حلّ طلب استعادة أحدث محل هذا الطلب.';
131
+ case 'pin-locked':
132
+ return 'تم إلغاء هذا الطلب بعد عدة محاولات خاطئة لإدخال رمز PIN.';
133
+ case 'release-failed':
134
+ return 'تعذّر إكمال هذا الطلب وتم إلغاؤه تلقائيًا.';
135
+ default:
136
+ return 'لا حاجة لأي إجراء آخر.';
137
+ }
138
+ },
139
+ sincerely: 'مع التحيات،',
140
+ subject: 'تم إلغاء طلب الاستعادة',
141
+ };
142
+
143
+ const STRINGS: Record<NotificationLocale, Strings> = { en: EN, es: ES, fr: FR, ar: AR };
144
+
145
+ export const EscrowHoldCancelled: React.FC<EscrowHoldCancelledProps> = ({
146
+ branding,
147
+ cancelledAt,
148
+ reason,
149
+ locale,
150
+ }) => {
151
+ const s = STRINGS[resolveCatalogLocale(locale)];
152
+
153
+ return (
154
+ <Layout branding={branding} locale={locale} preview={s.preview}>
155
+ <Text style={heading}>{s.heading}</Text>
156
+
157
+ <Text style={paragraph}>{s.body(cancelledAt)}</Text>
158
+
159
+ <Text style={paragraph}>{s.reasonText(reason)}</Text>
160
+
161
+ <Text style={signOff}>
162
+ {s.sincerely}
163
+ <br />
164
+ {interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
165
+ brandName: branding.brandName,
166
+ })}
167
+ </Text>
168
+ </Layout>
169
+ );
170
+ };
171
+
172
+ export const getEscrowHoldCancelledSubject = (_branding: TenantBranding, locale?: string): string =>
173
+ STRINGS[resolveCatalogLocale(locale)].subject;
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Styles
177
+ // ---------------------------------------------------------------------------
178
+
179
+ const heading: React.CSSProperties = {
180
+ fontSize: 24,
181
+ fontWeight: 600,
182
+ color: '#111827',
183
+ margin: '0 0 24px',
184
+ };
185
+
186
+ const paragraph: React.CSSProperties = {
187
+ fontSize: 16,
188
+ color: '#374151',
189
+ lineHeight: '24px',
190
+ margin: '0 0 24px',
191
+ };
192
+
193
+ const signOff: React.CSSProperties = {
194
+ fontSize: 14,
195
+ color: '#374151',
196
+ lineHeight: '20px',
197
+ margin: '24px 0 0',
198
+ };
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Preview
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export default function Preview() {
205
+ return (
206
+ <EscrowHoldCancelled
207
+ branding={DEFAULT_BRANDING}
208
+ cancelledAt="September 26, 2026, 9:15 AM"
209
+ reason="user"
210
+ />
211
+ );
212
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * EscrowHoldReleased — confirms that a previously requested account recovery
3
+ * has completed and access has been restored.
4
+ *
5
+ * Used by: lca-api recovery hold routes (templateId: 'escrow-hold-released')
6
+ */
7
+
8
+ import { Text, Section } from '@react-email/components';
9
+ import * as React from 'react';
10
+
11
+ import type { TenantBranding } from '../branding';
12
+ import { DEFAULT_BRANDING } from '../branding';
13
+ import { Layout } from '../components/Layout';
14
+ import { EmailButton } from '../components/EmailButton';
15
+ import { LinkFallback } from '../components/LinkFallback';
16
+ import type { NotificationLocale } from '../i18n';
17
+ import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
18
+
19
+ export interface EscrowHoldReleasedProps {
20
+ branding: TenantBranding;
21
+ /** Human-readable timestamp access was restored. */
22
+ completedAt: string;
23
+ /** Optional support link shown as a CTA (falls back to the footer's mailto link). */
24
+ supportUrl?: string;
25
+ /** Recipient locale (BCP-47). Defaults to English. */
26
+ locale?: string;
27
+ }
28
+
29
+ type Strings = {
30
+ preview: string;
31
+ heading: string;
32
+ body: (completedAt: string) => React.ReactNode;
33
+ warning: string;
34
+ button: string;
35
+ sincerely: string;
36
+ subject: string;
37
+ };
38
+
39
+ const EN: Strings = {
40
+ preview: 'Your account was recovered',
41
+ heading: 'Your Account Was Recovered',
42
+ body: completedAt => (
43
+ <>
44
+ Access to your account was restored on <strong>{completedAt}</strong>.
45
+ </>
46
+ ),
47
+ warning: "If this wasn't you, contact support immediately.",
48
+ button: 'Contact Support',
49
+ sincerely: 'Sincerely,',
50
+ subject: 'Your account was recovered',
51
+ };
52
+
53
+ const ES: Strings = {
54
+ preview: 'Tu cuenta fue recuperada',
55
+ heading: 'Tu Cuenta Fue Recuperada',
56
+ body: completedAt => (
57
+ <>
58
+ Se restableció el acceso a tu cuenta el <strong>{completedAt}</strong>.
59
+ </>
60
+ ),
61
+ warning: 'Si no fuiste tú, comunícate con soporte de inmediato.',
62
+ button: 'Contactar con soporte',
63
+ sincerely: 'Atentamente,',
64
+ subject: 'Tu cuenta fue recuperada',
65
+ };
66
+
67
+ const FR: Strings = {
68
+ preview: 'Votre compte a été récupéré',
69
+ heading: 'Votre Compte a Été Récupéré',
70
+ body: completedAt => (
71
+ <>
72
+ L\u2019accès à votre compte a été rétabli le <strong>{completedAt}</strong>.
73
+ </>
74
+ ),
75
+ warning:
76
+ 'Si vous n\u2019êtes pas à l\u2019origine de cette action, contactez immédiatement l\u2019assistance.',
77
+ button: 'Contacter le support',
78
+ sincerely: 'Cordialement,',
79
+ subject: 'Votre compte a été récupéré',
80
+ };
81
+
82
+ const AR: Strings = {
83
+ preview: 'تمت استعادة حسابك',
84
+ heading: 'تمت استعادة حسابك',
85
+ body: completedAt => (
86
+ <>
87
+ تمت استعادة الوصول إلى حسابك في <strong>{completedAt}</strong>.
88
+ </>
89
+ ),
90
+ warning: 'إذا لم يكن هذا أنت، فتواصل مع الدعم فورًا.',
91
+ button: 'تواصل مع الدعم',
92
+ sincerely: 'مع التحيات،',
93
+ subject: 'تمت استعادة حسابك',
94
+ };
95
+
96
+ const STRINGS: Record<NotificationLocale, Strings> = { en: EN, es: ES, fr: FR, ar: AR };
97
+
98
+ export const EscrowHoldReleased: React.FC<EscrowHoldReleasedProps> = ({
99
+ branding,
100
+ completedAt,
101
+ supportUrl,
102
+ locale,
103
+ }) => {
104
+ const s = STRINGS[resolveCatalogLocale(locale)];
105
+
106
+ return (
107
+ <Layout branding={branding} locale={locale} preview={s.preview}>
108
+ <Text style={heading}>{s.heading}</Text>
109
+
110
+ <Text style={paragraph}>{s.body(completedAt)}</Text>
111
+
112
+ <Text style={warning}>{s.warning}</Text>
113
+
114
+ {supportUrl && (
115
+ <Section style={buttonWrapper}>
116
+ <EmailButton href={supportUrl} branding={branding}>
117
+ {s.button}
118
+ </EmailButton>
119
+ </Section>
120
+ )}
121
+
122
+ <Text style={signOff}>
123
+ {s.sincerely}
124
+ <br />
125
+ {interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
126
+ brandName: branding.brandName,
127
+ })}
128
+ </Text>
129
+
130
+ {supportUrl && <LinkFallback href={supportUrl} locale={locale} />}
131
+ </Layout>
132
+ );
133
+ };
134
+
135
+ export const getEscrowHoldReleasedSubject = (_branding: TenantBranding, locale?: string): string =>
136
+ STRINGS[resolveCatalogLocale(locale)].subject;
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Styles
140
+ // ---------------------------------------------------------------------------
141
+
142
+ const heading: React.CSSProperties = {
143
+ fontSize: 24,
144
+ fontWeight: 600,
145
+ color: '#111827',
146
+ margin: '0 0 24px',
147
+ };
148
+
149
+ const paragraph: React.CSSProperties = {
150
+ fontSize: 16,
151
+ color: '#374151',
152
+ lineHeight: '24px',
153
+ margin: '0 0 24px',
154
+ };
155
+
156
+ const warning: React.CSSProperties = {
157
+ ...paragraph,
158
+ fontWeight: 600,
159
+ };
160
+
161
+ const buttonWrapper: React.CSSProperties = {
162
+ margin: '0 0 24px',
163
+ };
164
+
165
+ const signOff: React.CSSProperties = {
166
+ fontSize: 14,
167
+ color: '#374151',
168
+ lineHeight: '20px',
169
+ margin: '24px 0 0',
170
+ };
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Preview
174
+ // ---------------------------------------------------------------------------
175
+
176
+ export default function Preview() {
177
+ return (
178
+ <EscrowHoldReleased
179
+ branding={DEFAULT_BRANDING}
180
+ completedAt="October 2, 2026, 3:00 PM"
181
+ supportUrl="https://learncard.app/support"
182
+ />
183
+ );
184
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * EscrowHoldReminder — reminds a user that a pending account recovery request
3
+ * is about to complete, and gives them one more chance to cancel it.
4
+ *
5
+ * Used by: lca-api recovery hold routes (templateId: 'escrow-hold-reminder')
6
+ */
7
+
8
+ import { Text, Section } from '@react-email/components';
9
+ import * as React from 'react';
10
+
11
+ import type { TenantBranding } from '../branding';
12
+ import { DEFAULT_BRANDING } from '../branding';
13
+ import { Layout } from '../components/Layout';
14
+ import { EmailButton } from '../components/EmailButton';
15
+ import { LinkFallback } from '../components/LinkFallback';
16
+ import type { NotificationLocale } from '../i18n';
17
+ import { resolveCatalogLocale, interpolate, SHARED } from '../i18n';
18
+
19
+ export interface EscrowHoldReminderProps {
20
+ branding: TenantBranding;
21
+ /** Human-readable timestamp access will be restored, unless cancelled. */
22
+ releaseAfter: string;
23
+ /** Link that cancels the pending recovery request. */
24
+ cancelUrl: string;
25
+ /** Recipient locale (BCP-47). Defaults to English. */
26
+ locale?: string;
27
+ }
28
+
29
+ type Strings = {
30
+ preview: string;
31
+ heading: string;
32
+ body: (releaseAfter: string) => React.ReactNode;
33
+ warning: string;
34
+ button: string;
35
+ sincerely: string;
36
+ subject: string;
37
+ };
38
+
39
+ const EN: Strings = {
40
+ preview: 'Recovery completes tomorrow',
41
+ heading: 'Recovery Completes Tomorrow',
42
+ body: releaseAfter => (
43
+ <>
44
+ Your account recovery will complete on <strong>{releaseAfter}</strong> unless you cancel
45
+ it.
46
+ </>
47
+ ),
48
+ warning: "If this wasn't you, cancel now.",
49
+ button: "This wasn't me \u2014 Cancel recovery",
50
+ sincerely: 'Sincerely,',
51
+ subject: 'Recovery completes tomorrow',
52
+ };
53
+
54
+ const ES: Strings = {
55
+ preview: 'La recuperación se completa mañana',
56
+ heading: 'La Recuperación se Completa Mañana',
57
+ body: releaseAfter => (
58
+ <>
59
+ La recuperación de tu cuenta se completará el <strong>{releaseAfter}</strong> a menos
60
+ que la canceles.
61
+ </>
62
+ ),
63
+ warning: 'Si no fuiste tú, cancélalo ahora.',
64
+ button: 'No fui yo \u2014 Cancelar recuperación',
65
+ sincerely: 'Atentamente,',
66
+ subject: 'La recuperación se completa mañana',
67
+ };
68
+
69
+ const FR: Strings = {
70
+ preview: 'La récupération se termine demain',
71
+ heading: 'La Récupération se Termine Demain',
72
+ body: releaseAfter => (
73
+ <>
74
+ La récupération de votre compte sera terminée le <strong>{releaseAfter}</strong> sauf si
75
+ vous l\u2019annulez.
76
+ </>
77
+ ),
78
+ warning: 'Si ce n\u2019était pas vous, annulez dès maintenant.',
79
+ button: 'Ce n\u2019était pas moi \u2014 Annuler la récupération',
80
+ sincerely: 'Cordialement,',
81
+ subject: 'La récupération se termine demain',
82
+ };
83
+
84
+ const AR: Strings = {
85
+ preview: 'تكتمل الاستعادة غدًا',
86
+ heading: 'تكتمل الاستعادة غدًا',
87
+ body: releaseAfter => (
88
+ <>
89
+ ستكتمل استعادة حسابك في <strong>{releaseAfter}</strong> ما لم تقم بإلغائها.
90
+ </>
91
+ ),
92
+ warning: 'إذا لم يكن هذا أنت، ألغِ الطلب الآن.',
93
+ button: 'لم يكن أنا \u2014 إلغاء الاستعادة',
94
+ sincerely: 'مع التحيات،',
95
+ subject: 'تكتمل الاستعادة غدًا',
96
+ };
97
+
98
+ const STRINGS: Record<NotificationLocale, Strings> = { en: EN, es: ES, fr: FR, ar: AR };
99
+
100
+ export const EscrowHoldReminder: React.FC<EscrowHoldReminderProps> = ({
101
+ branding,
102
+ releaseAfter,
103
+ cancelUrl,
104
+ locale,
105
+ }) => {
106
+ const s = STRINGS[resolveCatalogLocale(locale)];
107
+
108
+ return (
109
+ <Layout branding={branding} locale={locale} preview={s.preview}>
110
+ <Text style={heading}>{s.heading}</Text>
111
+
112
+ <Text style={paragraph}>{s.body(releaseAfter)}</Text>
113
+
114
+ <Text style={warning}>{s.warning}</Text>
115
+
116
+ <Section style={buttonWrapper}>
117
+ <EmailButton href={cancelUrl} branding={branding}>
118
+ {s.button}
119
+ </EmailButton>
120
+ </Section>
121
+
122
+ <Text style={signOff}>
123
+ {s.sincerely}
124
+ <br />
125
+ {interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
126
+ brandName: branding.brandName,
127
+ })}
128
+ </Text>
129
+
130
+ <LinkFallback href={cancelUrl} locale={locale} />
131
+ </Layout>
132
+ );
133
+ };
134
+
135
+ export const getEscrowHoldReminderSubject = (_branding: TenantBranding, locale?: string): string =>
136
+ STRINGS[resolveCatalogLocale(locale)].subject;
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Styles
140
+ // ---------------------------------------------------------------------------
141
+
142
+ const heading: React.CSSProperties = {
143
+ fontSize: 24,
144
+ fontWeight: 600,
145
+ color: '#111827',
146
+ margin: '0 0 24px',
147
+ };
148
+
149
+ const paragraph: React.CSSProperties = {
150
+ fontSize: 16,
151
+ color: '#374151',
152
+ lineHeight: '24px',
153
+ margin: '0 0 24px',
154
+ };
155
+
156
+ const warning: React.CSSProperties = {
157
+ ...paragraph,
158
+ fontWeight: 600,
159
+ };
160
+
161
+ const buttonWrapper: React.CSSProperties = {
162
+ margin: '0 0 24px',
163
+ };
164
+
165
+ const signOff: React.CSSProperties = {
166
+ fontSize: 14,
167
+ color: '#374151',
168
+ lineHeight: '20px',
169
+ margin: '24px 0 0',
170
+ };
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Preview
174
+ // ---------------------------------------------------------------------------
175
+
176
+ export default function Preview() {
177
+ return (
178
+ <EscrowHoldReminder
179
+ branding={DEFAULT_BRANDING}
180
+ releaseAfter="October 2, 2026, 3:00 PM"
181
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
182
+ />
183
+ );
184
+ }