@learncard/email-templates 1.0.6 → 1.2.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.
Files changed (41) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/previews/credential-updated-scenarios.d.ts +9 -0
  5. package/dist/previews/credential-updated-scenarios.d.ts.map +1 -0
  6. package/dist/previews/credential-updated-scenarios.js +7 -0
  7. package/dist/previews/credential-updated-scenarios.js.map +1 -0
  8. package/dist/previews/recovery-key-scenarios.d.ts.map +1 -1
  9. package/dist/previews/recovery-key-scenarios.js +1 -1
  10. package/dist/previews/recovery-key-scenarios.js.map +1 -1
  11. package/dist/render.d.ts +16 -0
  12. package/dist/render.d.ts.map +1 -1
  13. package/dist/render.js +16 -1
  14. package/dist/render.js.map +1 -1
  15. package/dist/templates/account-sign-in-changed.d.ts +11 -0
  16. package/dist/templates/account-sign-in-changed.d.ts.map +1 -0
  17. package/dist/templates/account-sign-in-changed.js +70 -0
  18. package/dist/templates/account-sign-in-changed.js.map +1 -0
  19. package/dist/templates/credential-updated.d.ts +33 -0
  20. package/dist/templates/credential-updated.d.ts.map +1 -0
  21. package/dist/templates/credential-updated.js +104 -0
  22. package/dist/templates/credential-updated.js.map +1 -0
  23. package/dist/templates/index.d.ts +7 -3
  24. package/dist/templates/index.d.ts.map +1 -1
  25. package/dist/templates/index.js +5 -3
  26. package/dist/templates/index.js.map +1 -1
  27. package/dist/templates/recovery-key.d.ts +1 -0
  28. package/dist/templates/recovery-key.d.ts.map +1 -1
  29. package/dist/templates/recovery-key.js +11 -3
  30. package/dist/templates/recovery-key.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/__tests__/i18n.test.ts +9 -2
  33. package/src/__tests__/render.test.ts +151 -26
  34. package/src/index.ts +2 -0
  35. package/src/previews/credential-updated-scenarios.tsx +61 -0
  36. package/src/previews/recovery-key-scenarios.tsx +3 -0
  37. package/src/render.ts +39 -0
  38. package/src/templates/account-sign-in-changed.tsx +113 -0
  39. package/src/templates/credential-updated.tsx +309 -0
  40. package/src/templates/index.ts +17 -3
  41. package/src/templates/recovery-key.tsx +26 -1
@@ -1 +1 @@
1
- {"version":3,"file":"recovery-key.js","sourceRoot":"","sources":["../../src/templates/recovery-key.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"recovery-key.js","sourceRoot":"","sources":["../../src/templates/recovery-key.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/email-templates",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "Git-managed, tenant-branded email and SMS templates for LearnCard services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  "@types/react": "^18.3.0",
27
27
  "react-email": "^3.0.0",
28
28
  "typescript": "^5.0.0",
29
- "vitest": "4.1.10"
29
+ "vitest": "4.1.11"
30
30
  },
31
31
  "author": "Learning Economy Foundation (www.learningeconomy.io)",
32
32
  "license": "MIT",
@@ -49,8 +49,14 @@ const FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
49
49
  },
50
50
  'account-approved': { user: { displayName: 'New User' } },
51
51
  'account-approved-email': { user: { displayName: 'New User' } },
52
- 'recovery-key': { recoveryKey: 'mango-delta-fox-echo-bravo-seven-lima-niner' },
53
- 'recovery-key-backup': { recoveryKey: 'mango-delta-fox-echo-bravo-seven-lima-niner' },
52
+ 'recovery-key': {
53
+ recoveryKey: 'mango-delta-fox-echo-bravo-seven-lima-niner',
54
+ confirmationCode: '123456',
55
+ },
56
+ 'recovery-key-backup': {
57
+ recoveryKey: 'mango-delta-fox-echo-bravo-seven-lima-niner',
58
+ confirmationCode: '123456',
59
+ },
54
60
  'endorsement-request': {
55
61
  shareLink: 'https://learncard.app/share/xyz',
56
62
  recipient: { name: 'Dr. Emily Chen' },
@@ -85,6 +91,7 @@ const FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
85
91
  credential: { name: 'Perfect Attendance Award' },
86
92
  recipient: { email: 'student@example.com' },
87
93
  },
94
+ 'account-sign-in-changed': {},
88
95
  };
89
96
 
90
97
  const ALL_TEMPLATE_IDS = Object.keys(FIXTURES) as TemplateId[];
@@ -83,10 +83,12 @@ const TEMPLATE_FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
83
83
 
84
84
  'recovery-key': {
85
85
  recoveryKey: 'ABCD-EFGH-1234-5678',
86
+ confirmationCode: '123456',
86
87
  },
87
88
 
88
89
  'recovery-key-backup': {
89
90
  recoveryKey: 'WXYZ-9876-LMNO-4321',
91
+ confirmationCode: '654321',
90
92
  },
91
93
 
92
94
  'endorsement-request': {
@@ -130,6 +132,12 @@ const TEMPLATE_FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
130
132
  credential: { name: 'Diploma' },
131
133
  recipient: { email: 'student@example.com' },
132
134
  },
135
+
136
+ 'credential-updated': {
137
+ issuer: { name: 'Inbox Demo School' },
138
+ credential: { name: 'Introduction to Biology' },
139
+ },
140
+ 'account-sign-in-changed': {},
133
141
  };
134
142
 
135
143
  const ALL_TEMPLATE_IDS = Object.keys(TEMPLATE_FIXTURES) as TemplateId[];
@@ -139,38 +147,40 @@ const ALL_TEMPLATE_IDS = Object.keys(TEMPLATE_FIXTURES) as TemplateId[];
139
147
  // ---------------------------------------------------------------------------
140
148
 
141
149
  describe('renderEmail — smoke tests', () => {
142
- it.each(ALL_TEMPLATE_IDS)(
143
- '"%s" renders with default branding',
144
- async (templateId) => {
145
- const result = await renderEmail(templateId, DEFAULT_BRANDING, TEMPLATE_FIXTURES[templateId]);
146
-
147
- expect(result.html).toBeTruthy();
148
- expect(result.html).toContain('<!DOCTYPE');
149
- expect(result.text).toBeTruthy();
150
- expect(result.subject).toBeTruthy();
151
- expect(result.subject.length).toBeGreaterThan(0);
152
- },
153
- );
150
+ it.each(ALL_TEMPLATE_IDS)('"%s" renders with default branding', async templateId => {
151
+ const result = await renderEmail(
152
+ templateId,
153
+ DEFAULT_BRANDING,
154
+ TEMPLATE_FIXTURES[templateId]
155
+ );
156
+
157
+ expect(result.html).toBeTruthy();
158
+ expect(result.html).toContain('<!DOCTYPE');
159
+ expect(result.text).toBeTruthy();
160
+ expect(result.subject).toBeTruthy();
161
+ expect(result.subject.length).toBeGreaterThan(0);
162
+ });
154
163
 
155
- it.each(ALL_TEMPLATE_IDS)(
156
- '"%s" renders with custom (VetPass) branding',
157
- async (templateId) => {
158
- const result = await renderEmail(templateId, VETPASS_BRANDING, TEMPLATE_FIXTURES[templateId]);
164
+ it.each(ALL_TEMPLATE_IDS)('"%s" renders with custom (VetPass) branding', async templateId => {
165
+ const result = await renderEmail(
166
+ templateId,
167
+ VETPASS_BRANDING,
168
+ TEMPLATE_FIXTURES[templateId]
169
+ );
159
170
 
160
- expect(result.html).toBeTruthy();
161
- expect(result.text).toBeTruthy();
162
- expect(result.subject).toBeTruthy();
163
- },
164
- );
171
+ expect(result.html).toBeTruthy();
172
+ expect(result.text).toBeTruthy();
173
+ expect(result.subject).toBeTruthy();
174
+ });
165
175
 
166
176
  it.each(ALL_TEMPLATE_IDS)(
167
177
  '"%s" renders with empty branding (falls back to defaults)',
168
- async (templateId) => {
178
+ async templateId => {
169
179
  const result = await renderEmail(templateId, {}, TEMPLATE_FIXTURES[templateId]);
170
180
 
171
181
  expect(result.html).toBeTruthy();
172
182
  expect(result.subject).toBeTruthy();
173
- },
183
+ }
174
184
  );
175
185
  });
176
186
 
@@ -244,6 +254,7 @@ describe('renderEmail — content assertions', () => {
244
254
  const { html } = await renderEmail('recovery-key', DEFAULT_BRANDING, data);
245
255
 
246
256
  expect(html).toContain('ABCD-EFGH-1234-5678');
257
+ expect(html).toContain('123456');
247
258
  });
248
259
 
249
260
  it('endorsement-request includes the share link and message', async () => {
@@ -292,6 +303,89 @@ describe('renderEmail — content assertions', () => {
292
303
  });
293
304
  });
294
305
 
306
+ // ---------------------------------------------------------------------------
307
+ // credential-updated — minimal-context update email (LC-2198)
308
+ // ---------------------------------------------------------------------------
309
+
310
+ describe('renderEmail — credential-updated', () => {
311
+ it('includes the issuer display name and credential title', async () => {
312
+ const data = TEMPLATE_FIXTURES['credential-updated'];
313
+ const { html } = await renderEmail('credential-updated', DEFAULT_BRANDING, data);
314
+
315
+ expect(html).toContain('Inbox Demo School');
316
+ expect(html).toContain('Introduction to Biology');
317
+ });
318
+
319
+ it('links the CTA to the configured app notifications page, not a claim or token URL', async () => {
320
+ const { html } = await renderEmail('credential-updated', VETPASS_BRANDING, {
321
+ issuer: { name: 'Vet School' },
322
+ credential: { name: 'Veterinary Ethics' },
323
+ });
324
+
325
+ expect(html).toContain('https://vetpass.app/notifications');
326
+ expect(html).not.toContain('/claim');
327
+ expect(html).not.toContain('token=');
328
+ });
329
+
330
+ it('falls back to generic copy when no credential title is available', async () => {
331
+ const { html, text } = await renderEmail('credential-updated', DEFAULT_BRANDING, {
332
+ issuer: { name: 'Inbox Demo School' },
333
+ });
334
+
335
+ expect(html).toContain('Inbox Demo School');
336
+ // Generic copy confirms an update without inventing a title.
337
+ expect(html.toLowerCase()).toContain('updated');
338
+ expect(text.toLowerCase()).toContain('updated');
339
+ expect(html).not.toContain('undefined');
340
+ });
341
+
342
+ it('never renders issuer-authored summary or credential body content', async () => {
343
+ // The data shape only carries issuer + title; assert nothing else leaks.
344
+ const { html } = await renderEmail('credential-updated', DEFAULT_BRANDING, {
345
+ issuer: { name: 'Inbox Demo School' },
346
+ credential: { name: 'Introduction to Biology' },
347
+ } as unknown as (typeof TEMPLATE_FIXTURES)['credential-updated']);
348
+
349
+ expect(html).not.toContain('updateSummary');
350
+ expect(html).not.toContain('credentialSubject');
351
+ });
352
+
353
+ it.each([
354
+ { locale: 'es', expected: 'actualiz' },
355
+ { locale: 'fr', expected: 'mis' },
356
+ { locale: 'ar', expected: 'تم' },
357
+ ])('renders localized copy for $locale', async ({ locale, expected }) => {
358
+ const data = TEMPLATE_FIXTURES['credential-updated'];
359
+ const { html, subject } = await renderEmail(
360
+ 'credential-updated',
361
+ DEFAULT_BRANDING,
362
+ data,
363
+ locale
364
+ );
365
+
366
+ expect(subject).toBeTruthy();
367
+ expect(html).toContain(expected);
368
+ });
369
+
370
+ it('plain text output is readable and free of HTML tags', async () => {
371
+ const data = TEMPLATE_FIXTURES['credential-updated'];
372
+ const { text } = await renderEmail('credential-updated', DEFAULT_BRANDING, data);
373
+
374
+ expect(text).not.toMatch(/<[a-z][\s\S]*>/i);
375
+ expect(text).toContain('Inbox Demo School');
376
+ expect(text).toContain('Introduction to Biology');
377
+ });
378
+
379
+ it('flows tenant branding through to the rendered email', async () => {
380
+ const data = TEMPLATE_FIXTURES['credential-updated'];
381
+ const { html } = await renderEmail('credential-updated', VETPASS_BRANDING, data);
382
+
383
+ expect(html).toContain('VetPass');
384
+ expect(html).toContain('#1B5E20');
385
+ expect(html).toContain('support@vetpass.app');
386
+ });
387
+ });
388
+
295
389
  // ---------------------------------------------------------------------------
296
390
  // Branding tests — custom branding flows through to rendered output
297
391
  // ---------------------------------------------------------------------------
@@ -366,10 +460,33 @@ describe('renderEmail — subjects', () => {
366
460
  expect(subject).toBe('Verify Your Email');
367
461
  });
368
462
 
463
+ it('credential-updated subject is localized and generic', async () => {
464
+ const [en, es] = await Promise.all([
465
+ renderEmail(
466
+ 'credential-updated',
467
+ DEFAULT_BRANDING,
468
+ TEMPLATE_FIXTURES['credential-updated']
469
+ ),
470
+ renderEmail(
471
+ 'credential-updated',
472
+ DEFAULT_BRANDING,
473
+ TEMPLATE_FIXTURES['credential-updated'],
474
+ 'es'
475
+ ),
476
+ ]);
477
+
478
+ expect(en.subject).toBe('Your credential was updated');
479
+ expect(es.subject).toBe('Tu credencial se actualizó');
480
+ });
481
+
369
482
  it('Postmark alias pairs produce the same subject', async () => {
370
483
  const [a, b] = await Promise.all([
371
484
  renderEmail('inbox-claim', DEFAULT_BRANDING, TEMPLATE_FIXTURES['inbox-claim']),
372
- renderEmail('universal-inbox-claim', DEFAULT_BRANDING, TEMPLATE_FIXTURES['inbox-claim']),
485
+ renderEmail(
486
+ 'universal-inbox-claim',
487
+ DEFAULT_BRANDING,
488
+ TEMPLATE_FIXTURES['inbox-claim']
489
+ ),
373
490
  ]);
374
491
 
375
492
  expect(a.subject).toBe(b.subject);
@@ -382,13 +499,21 @@ describe('renderEmail — subjects', () => {
382
499
 
383
500
  describe('renderEmail — plain text', () => {
384
501
  it('plain text output does not contain HTML tags', async () => {
385
- const { text } = await renderEmail('inbox-claim', DEFAULT_BRANDING, TEMPLATE_FIXTURES['inbox-claim']);
502
+ const { text } = await renderEmail(
503
+ 'inbox-claim',
504
+ DEFAULT_BRANDING,
505
+ TEMPLATE_FIXTURES['inbox-claim']
506
+ );
386
507
 
387
508
  expect(text).not.toMatch(/<[a-z][\s\S]*>/i);
388
509
  });
389
510
 
390
511
  it('plain text includes key content', async () => {
391
- const { text } = await renderEmail('recovery-key', DEFAULT_BRANDING, TEMPLATE_FIXTURES['recovery-key']);
512
+ const { text } = await renderEmail(
513
+ 'recovery-key',
514
+ DEFAULT_BRANDING,
515
+ TEMPLATE_FIXTURES['recovery-key']
516
+ );
392
517
 
393
518
  expect(text).toContain('ABCD-EFGH-1234-5678');
394
519
  });
package/src/index.ts CHANGED
@@ -37,6 +37,8 @@ export type {
37
37
  GuardianCredentialApprovalData,
38
38
  GuardianEmailOtpData,
39
39
  GuardianRejectedCredentialData,
40
+ CredentialUpdatedData,
41
+ AccountSignInChangedData,
40
42
  } from './render';
41
43
 
42
44
  // SMS rendering
@@ -0,0 +1,61 @@
1
+ /**
2
+ * credential-updated — minimal-context update notice scenarios.
3
+ *
4
+ * Shows the title/no-title fallback and locale variants so reviewers can verify
5
+ * the email never renders grades, subject details, or issuer summaries.
6
+ */
7
+
8
+ import * as React from 'react';
9
+
10
+ import { CredentialUpdated } from '../templates/credential-updated';
11
+ import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
12
+
13
+ export default function CredentialUpdatedScenarios() {
14
+ return (
15
+ <>
16
+ <ScenarioDivider label="English — with credential title" />
17
+ <CredentialUpdated
18
+ branding={LEARNCARD_BRANDING}
19
+ issuer={{ name: 'Inbox Demo School' }}
20
+ credential={{ name: 'Introduction to Biology' }}
21
+ />
22
+
23
+ <ScenarioDivider label="English — title unavailable (generic fallback)" />
24
+ <CredentialUpdated
25
+ branding={LEARNCARD_BRANDING}
26
+ issuer={{ name: 'Inbox Demo School' }}
27
+ />
28
+
29
+ <ScenarioDivider label="Spanish" />
30
+ <CredentialUpdated
31
+ branding={LEARNCARD_BRANDING}
32
+ issuer={{ name: 'Escuela Demo' }}
33
+ credential={{ name: 'Introducción a la Biología' }}
34
+ locale="es"
35
+ />
36
+
37
+ <ScenarioDivider label="French" />
38
+ <CredentialUpdated
39
+ branding={LEARNCARD_BRANDING}
40
+ issuer={{ name: 'École Démo' }}
41
+ credential={{ name: 'Introduction à la biologie' }}
42
+ locale="fr"
43
+ />
44
+
45
+ <ScenarioDivider label="Arabic (RTL)" />
46
+ <CredentialUpdated
47
+ branding={LEARNCARD_BRANDING}
48
+ issuer={{ name: 'مدرسة تجريبية' }}
49
+ credential={{ name: 'مقدمة في علم الأحياء' }}
50
+ locale="ar"
51
+ />
52
+
53
+ <ScenarioDivider label="VetPass branding" />
54
+ <CredentialUpdated
55
+ branding={VETPASS_BRANDING}
56
+ issuer={{ name: 'VetPass Academy' }}
57
+ credential={{ name: 'Veterinary Ethics' }}
58
+ />
59
+ </>
60
+ );
61
+ }
@@ -14,18 +14,21 @@ export default function RecoveryKeyScenarios() {
14
14
  <RecoveryKey
15
15
  branding={LEARNCARD_BRANDING}
16
16
  recoveryKey="mango-delta-fox-echo-bravo-seven-lima-niner"
17
+ confirmationCode="123456"
17
18
  />
18
19
 
19
20
  <ScenarioDivider label="Long recovery key (wrapping test)" />
20
21
  <RecoveryKey
21
22
  branding={LEARNCARD_BRANDING}
22
23
  recoveryKey="alpha-bravo-charlie-delta-echo-foxtrot-golf-hotel-india-juliet-kilo-lima"
24
+ confirmationCode="654321"
23
25
  />
24
26
 
25
27
  <ScenarioDivider label="VetPass tenant branding" />
26
28
  <RecoveryKey
27
29
  branding={VETPASS_BRANDING}
28
30
  recoveryKey="sierra-tango-uniform-victor-whiskey-xray"
31
+ confirmationCode="987654"
29
32
  />
30
33
  </>
31
34
  );
package/src/render.ts CHANGED
@@ -39,6 +39,10 @@ import {
39
39
  getGuardianRejectedCredentialSubject,
40
40
  EmailVerification,
41
41
  getEmailVerificationSubject,
42
+ CredentialUpdated,
43
+ getCredentialUpdatedSubject,
44
+ AccountSignInChanged,
45
+ getAccountSignInChangedSubject,
42
46
  } from './templates';
43
47
 
44
48
  import type {
@@ -53,6 +57,8 @@ import type {
53
57
  GuardianEmailOtpProps,
54
58
  GuardianRejectedCredentialProps,
55
59
  EmailVerificationProps,
60
+ CredentialUpdatedProps,
61
+ AccountSignInChangedProps,
56
62
  } from './templates';
57
63
 
58
64
  // ---------------------------------------------------------------------------
@@ -146,6 +152,11 @@ export interface TemplateDataMap {
146
152
 
147
153
  /** Sent to student: guardian rejected credential */
148
154
  'guardian-rejected-credential': GuardianRejectedCredentialData;
155
+
156
+ /** brain-service: managed credential refresh update notice (LC-2198) */
157
+ 'credential-updated': CredentialUpdatedData;
158
+ /** lca-api: lost-login identity rebind security notification */
159
+ 'account-sign-in-changed': AccountSignInChangedData;
149
160
  }
150
161
 
151
162
  export type TemplateId = keyof TemplateDataMap;
@@ -184,6 +195,7 @@ export interface AccountApprovedData {
184
195
 
185
196
  export interface RecoveryKeyData {
186
197
  recoveryKey: string;
198
+ confirmationCode: string;
187
199
  }
188
200
 
189
201
  export interface EndorsementRequestData {
@@ -223,6 +235,14 @@ export interface GuardianRejectedCredentialData {
223
235
  recipient?: { email?: string };
224
236
  }
225
237
 
238
+ export interface CredentialUpdatedData {
239
+ issuer?: { name?: string; logoUrl?: string };
240
+ /** Bounded credential display title. Omitted when unavailable. */
241
+ credential?: { name?: string };
242
+ }
243
+
244
+ export type AccountSignInChangedData = Record<string, never>;
245
+
226
246
  // ---------------------------------------------------------------------------
227
247
  // renderEmail()
228
248
  // ---------------------------------------------------------------------------
@@ -420,6 +440,25 @@ function buildElement(
420
440
  };
421
441
  }
422
442
 
443
+ case 'credential-updated': {
444
+ const d = data as CredentialUpdatedData;
445
+ const props: CredentialUpdatedProps = { branding, ...d, locale };
446
+
447
+ return {
448
+ element: React.createElement(CredentialUpdated, props),
449
+ subject: getCredentialUpdatedSubject(branding, locale),
450
+ };
451
+ }
452
+
453
+ case 'account-sign-in-changed': {
454
+ const props: AccountSignInChangedProps = { branding, locale };
455
+
456
+ return {
457
+ element: React.createElement(AccountSignInChanged, props),
458
+ subject: getAccountSignInChangedSubject(branding, locale),
459
+ };
460
+ }
461
+
423
462
  default: {
424
463
  const _exhaustive: never = templateId;
425
464
  throw new Error(`Unknown template: ${_exhaustive}`);
@@ -0,0 +1,113 @@
1
+ import { Text } from '@react-email/components';
2
+ import * as React from 'react';
3
+
4
+ import type { TenantBranding } from '../branding';
5
+ import { DEFAULT_BRANDING } from '../branding';
6
+ import { Layout } from '../components/Layout';
7
+ import type { NotificationLocale } from '../i18n';
8
+ import { interpolate, resolveCatalogLocale, SHARED } from '../i18n';
9
+
10
+ export interface AccountSignInChangedProps {
11
+ branding: TenantBranding;
12
+ locale?: string;
13
+ }
14
+
15
+ const STRINGS: Record<
16
+ NotificationLocale,
17
+ {
18
+ preview: string;
19
+ heading: string;
20
+ body: string;
21
+ warning: string;
22
+ subject: string;
23
+ sincerely: string;
24
+ }
25
+ > = {
26
+ en: {
27
+ preview: 'Your account sign-in was changed',
28
+ heading: 'Your Sign-In Was Changed',
29
+ body: 'Your {brandName} account was restored and connected to a new sign-in.',
30
+ warning: 'If this was not you, contact support immediately.',
31
+ subject: 'Your account sign-in was changed',
32
+ sincerely: 'Sincerely,',
33
+ },
34
+ es: {
35
+ preview: 'Se cambió el inicio de sesión de tu cuenta',
36
+ heading: 'Se cambió tu inicio de sesión',
37
+ body: 'Tu cuenta de {brandName} se restauró y se conectó a un nuevo inicio de sesión.',
38
+ warning: 'Si no fuiste tú, comunícate con soporte de inmediato.',
39
+ subject: 'Se cambió el inicio de sesión de tu cuenta',
40
+ sincerely: 'Atentamente,',
41
+ },
42
+ fr: {
43
+ preview: 'La connexion à votre compte a été modifiée',
44
+ heading: 'Votre connexion a été modifiée',
45
+ body: 'Votre compte {brandName} a été restauré et associé à une nouvelle connexion.',
46
+ warning:
47
+ 'Si vous n’êtes pas à l’origine de cette action, contactez immédiatement l’assistance.',
48
+ subject: 'La connexion à votre compte a été modifiée',
49
+ sincerely: 'Cordialement,',
50
+ },
51
+ ar: {
52
+ preview: 'تم تغيير تسجيل الدخول إلى حسابك',
53
+ heading: 'تم تغيير تسجيل الدخول',
54
+ body: 'تمت استعادة حسابك في {brandName} وربطه بتسجيل دخول جديد.',
55
+ warning: 'إذا لم تكن أنت من أجرى هذا التغيير، فتواصل مع الدعم فورًا.',
56
+ subject: 'تم تغيير تسجيل الدخول إلى حسابك',
57
+ sincerely: 'مع التحيات،',
58
+ },
59
+ };
60
+
61
+ export const AccountSignInChanged: React.FC<AccountSignInChangedProps> = ({ branding, locale }) => {
62
+ const strings = STRINGS[resolveCatalogLocale(locale)];
63
+
64
+ return (
65
+ <Layout branding={branding} locale={locale} preview={strings.preview}>
66
+ <Text style={heading}>{strings.heading}</Text>
67
+ <Text style={paragraph}>
68
+ {interpolate(strings.body, { brandName: branding.brandName })}
69
+ </Text>
70
+ <Text style={warning}>{strings.warning}</Text>
71
+ <Text style={signOff}>
72
+ {strings.sincerely}
73
+ <br />
74
+ {interpolate(SHARED[resolveCatalogLocale(locale)].teamSignature, {
75
+ brandName: branding.brandName,
76
+ })}
77
+ </Text>
78
+ </Layout>
79
+ );
80
+ };
81
+
82
+ /** Return the localized subject for an account sign-in change notification. */
83
+ export const getAccountSignInChangedSubject = (
84
+ _branding: TenantBranding,
85
+ locale?: string
86
+ ): string => STRINGS[resolveCatalogLocale(locale)].subject;
87
+
88
+ const heading: React.CSSProperties = {
89
+ color: '#111827',
90
+ fontSize: '24px',
91
+ fontWeight: 600,
92
+ lineHeight: '32px',
93
+ };
94
+
95
+ const paragraph: React.CSSProperties = {
96
+ color: '#374151',
97
+ fontSize: '16px',
98
+ lineHeight: '24px',
99
+ };
100
+
101
+ const warning: React.CSSProperties = {
102
+ ...paragraph,
103
+ fontWeight: 600,
104
+ };
105
+
106
+ const signOff: React.CSSProperties = {
107
+ ...paragraph,
108
+ marginTop: '24px',
109
+ };
110
+
111
+ export default function Preview(): React.ReactElement {
112
+ return <AccountSignInChanged branding={DEFAULT_BRANDING} />;
113
+ }