@learncard/email-templates 1.2.1 → 1.3.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 (59) hide show
  1. package/dist/components/Layout.d.ts +1 -0
  2. package/dist/components/Layout.d.ts.map +1 -1
  3. package/dist/components/Layout.js +2 -2
  4. package/dist/components/Layout.js.map +1 -1
  5. package/dist/i18n.d.ts +15 -0
  6. package/dist/i18n.d.ts.map +1 -1
  7. package/dist/i18n.js +31 -0
  8. package/dist/i18n.js.map +1 -1
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +0 -5
  12. package/dist/index.js.map +1 -1
  13. package/dist/previews/account-secured-scenarios.d.ts +6 -0
  14. package/dist/previews/account-secured-scenarios.d.ts.map +1 -0
  15. package/dist/previews/account-secured-scenarios.js +7 -0
  16. package/dist/previews/account-secured-scenarios.js.map +1 -0
  17. package/dist/previews/escrow-pin-sign-in-scenarios.d.ts +6 -0
  18. package/dist/previews/escrow-pin-sign-in-scenarios.d.ts.map +1 -0
  19. package/dist/previews/escrow-pin-sign-in-scenarios.js +7 -0
  20. package/dist/previews/escrow-pin-sign-in-scenarios.js.map +1 -0
  21. package/dist/render.d.ts +20 -9
  22. package/dist/render.d.ts.map +1 -1
  23. package/dist/render.js +39 -1
  24. package/dist/render.js.map +1 -1
  25. package/dist/templates/account-secured.d.ts +27 -0
  26. package/dist/templates/account-secured.d.ts.map +1 -0
  27. package/dist/templates/account-secured.js +118 -0
  28. package/dist/templates/account-secured.js.map +1 -0
  29. package/dist/templates/announcement.d.ts +13 -0
  30. package/dist/templates/announcement.d.ts.map +1 -0
  31. package/dist/templates/announcement.js +34 -0
  32. package/dist/templates/announcement.js.map +1 -0
  33. package/dist/templates/escrow-pin-sign-in.d.ts +24 -0
  34. package/dist/templates/escrow-pin-sign-in.d.ts.map +1 -0
  35. package/dist/templates/escrow-pin-sign-in.js +96 -0
  36. package/dist/templates/escrow-pin-sign-in.js.map +1 -0
  37. package/dist/templates/index.d.ts +22 -38
  38. package/dist/templates/index.d.ts.map +1 -1
  39. package/dist/templates/index.js +21 -14
  40. package/dist/templates/index.js.map +1 -1
  41. package/dist/tenant-registry.d.ts +2 -2
  42. package/dist/tenant-registry.d.ts.map +1 -1
  43. package/dist/tenant-registry.js +2 -3
  44. package/dist/tenant-registry.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/__tests__/i18n.test.ts +39 -1
  47. package/src/__tests__/render.test.ts +154 -0
  48. package/src/__tests__/tenant-registry.test.ts +2 -2
  49. package/src/components/Layout.tsx +4 -1
  50. package/src/i18n.ts +33 -0
  51. package/src/index.ts +4 -0
  52. package/src/previews/account-secured-scenarios.tsx +41 -0
  53. package/src/previews/escrow-pin-sign-in-scenarios.tsx +52 -0
  54. package/src/render.ts +73 -0
  55. package/src/templates/account-secured.tsx +236 -0
  56. package/src/templates/announcement.tsx +100 -0
  57. package/src/templates/escrow-pin-sign-in.tsx +197 -0
  58. package/src/templates/index.ts +97 -35
  59. package/src/tenant-registry.ts +3 -4
@@ -28,6 +28,13 @@ const VETPASS_BRANDING: Partial<TenantBranding> = {
28
28
  * produce output without throwing.
29
29
  */
30
30
  const TEMPLATE_FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
31
+ announcement: {
32
+ title: 'New learning opportunities',
33
+ body: 'Explore the latest programs.',
34
+ ctaLabel: 'Explore',
35
+ ctaUrl: 'https://learncard.app/m/programs?utm_source=email',
36
+ unsubscribeUrl: 'https://learncard.app/unsubscribe?token=example',
37
+ },
31
38
  'embed-email-verification': {
32
39
  verificationCode: '123456',
33
40
  verificationEmail: 'test@example.com',
@@ -166,6 +173,13 @@ const TEMPLATE_FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
166
173
  releaseAfter: 'October 2, 2026, 3:00 PM',
167
174
  cancelUrl: 'https://learncard.app/recovery/cancel?token=abc123',
168
175
  },
176
+ 'escrow-pin-sign-in': {
177
+ signedInAt: 'October 2, 2026, 3:00 PM',
178
+ secureUrl: 'https://learncard.app/account/secure',
179
+ },
180
+ 'account-secured': {
181
+ securedAt: 'October 2, 2026, 3:00 PM',
182
+ },
169
183
  };
170
184
 
171
185
  const ALL_TEMPLATE_IDS = Object.keys(TEMPLATE_FIXTURES) as TemplateId[];
@@ -552,6 +566,114 @@ describe('renderEmail — escrow hold lifecycle', () => {
552
566
  expect(html).not.toContain('undefined');
553
567
  });
554
568
 
569
+ it('escrow-pin-sign-in includes the sign-in date and the secure-account link', async () => {
570
+ const data = TEMPLATE_FIXTURES['escrow-pin-sign-in'];
571
+ const { html, text, subject } = await renderEmail(
572
+ 'escrow-pin-sign-in',
573
+ DEFAULT_BRANDING,
574
+ data
575
+ );
576
+
577
+ expect(html).toContain(data.signedInAt);
578
+ expect(html).toContain(data.secureUrl);
579
+ expect(text).toContain('Not you? Secure your account.');
580
+ expect(text).toContain('Secure my account');
581
+ expect(subject).toBe('New device signed in with your PIN');
582
+ assertNoJargon(html);
583
+ assertNoJargon(text);
584
+ assertNoJargon(subject);
585
+ });
586
+
587
+ it('escrow-pin-sign-in formats the ISO timestamp lca-api sends, per locale', async () => {
588
+ const iso = '2026-11-02T15:04:05.123Z';
589
+ const en = await renderEmail('escrow-pin-sign-in', DEFAULT_BRANDING, {
590
+ signedInAt: iso,
591
+ secureUrl: 'https://learncard.app/account/secure',
592
+ });
593
+ const fr = await renderEmail(
594
+ 'escrow-pin-sign-in',
595
+ DEFAULT_BRANDING,
596
+ { signedInAt: iso, secureUrl: 'https://learncard.app/account/secure' },
597
+ 'fr'
598
+ );
599
+
600
+ expect(en.text).not.toContain(iso);
601
+ expect(en.text).toContain('November 2, 2026');
602
+ expect(en.text).toContain('UTC');
603
+ expect(fr.text).toContain('2 novembre 2026');
604
+ });
605
+
606
+ it('escrow-pin-sign-in carries tenant branding', async () => {
607
+ const { html } = await renderEmail('escrow-pin-sign-in', VETPASS_BRANDING, {
608
+ signedInAt: 'October 2, 2026, 3:00 PM',
609
+ secureUrl: 'https://vetpass.app/account/secure',
610
+ });
611
+
612
+ expect(html).toContain('VetPass');
613
+ expect(html).toContain('https://vetpass.app/account/secure');
614
+ });
615
+
616
+ it('account-secured works for an owner who did not do it (no secure link, support first)', async () => {
617
+ const data = TEMPLATE_FIXTURES['account-secured'];
618
+ const { html, text, subject } = await renderEmail(
619
+ 'account-secured',
620
+ DEFAULT_BRANDING,
621
+ data
622
+ );
623
+
624
+ expect(html).toContain(data.securedAt);
625
+ expect(subject).toBe('Other devices were signed out of your account');
626
+ // The owner's login may be gone, so the notice must not depend on the in-app action.
627
+ expect(html).not.toContain('/account/secure');
628
+ expect(text).toContain('your own sign-in may have been removed');
629
+ // The revoke reset every recovery method and removed automatic recovery, so the
630
+ // lost-login path is not offered as the way back; support is.
631
+ expect(text).toContain('reset its recovery methods');
632
+ expect(text).not.toContain('I lost access to my school email or login');
633
+ expect(text).toContain(`Contact ${DEFAULT_BRANDING.supportEmail} right away`);
634
+ expect(html).toContain(`href="${DEFAULT_BRANDING.appUrl}"`);
635
+ assertNoJargon(html);
636
+ assertNoJargon(text);
637
+ assertNoJargon(subject);
638
+ });
639
+
640
+ it('account-secured formats the ISO timestamp lca-api sends, per locale', async () => {
641
+ const iso = '2026-11-02T15:04:05.123Z';
642
+ const en = await renderEmail('account-secured', DEFAULT_BRANDING, { securedAt: iso });
643
+ const es = await renderEmail('account-secured', DEFAULT_BRANDING, { securedAt: iso }, 'es');
644
+
645
+ expect(en.text).not.toContain(iso);
646
+ expect(en.text).toContain('November 2, 2026');
647
+ expect(en.text).toContain('UTC');
648
+ expect(es.text).toContain('2 de noviembre de 2026');
649
+ expect(es.subject).not.toBe(en.subject);
650
+ });
651
+
652
+ it('account-secured carries tenant branding and the tenant support address', async () => {
653
+ const { html, text } = await renderEmail('account-secured', VETPASS_BRANDING, {
654
+ securedAt: 'October 2, 2026, 3:00 PM',
655
+ });
656
+
657
+ expect(html).toContain('VetPass');
658
+ expect(text).toContain(`Contact ${VETPASS_BRANDING.supportEmail} right away`);
659
+ expect(html).toContain(`href="${VETPASS_BRANDING.appUrl}"`);
660
+ expect(text).not.toContain(DEFAULT_BRANDING.supportEmail);
661
+ });
662
+
663
+ it('account-secured points to the organization when the tenant has no support address', async () => {
664
+ const { text } = await renderEmail(
665
+ 'account-secured',
666
+ { ...DEFAULT_BRANDING, supportEmail: '' },
667
+ TEMPLATE_FIXTURES['account-secured']
668
+ );
669
+
670
+ expect(text).not.toContain('Contact right away');
671
+ expect(text).toContain(
672
+ `Contact the organization that gave you access to ${DEFAULT_BRANDING.brandName} right away`
673
+ );
674
+ expect(text).not.toContain('I lost access to my school email or login');
675
+ });
676
+
555
677
  it('all escrow hold templates include the "This wasn\'t me" cancel CTA when a cancel URL is present', async () => {
556
678
  const started = await renderEmail(
557
679
  'escrow-hold-started',
@@ -746,3 +868,35 @@ describe('renderEmail — edge cases', () => {
746
868
  expect(html).toContain('no-recipient-token');
747
869
  });
748
870
  });
871
+
872
+ describe('announcement', () => {
873
+ it('omits the button and fallback in HTML and text when no campaign destination exists', async () => {
874
+ const { ctaUrl, ...content } = TEMPLATE_FIXTURES.announcement;
875
+ const data = { ...content, ctaLabel: 'Unique campaign action' };
876
+ const { html, text } = await renderEmail('announcement', VETPASS_BRANDING, data);
877
+ for (const output of [html, text]) {
878
+ expect(output).not.toContain(ctaUrl);
879
+ expect(output).not.toContain(data.ctaLabel);
880
+ expect(output).not.toContain('/m/');
881
+ expect(output).toContain(data.body);
882
+ expect(output).toContain(data.unsubscribeUrl);
883
+ }
884
+ });
885
+
886
+ it('renders escaped authored copy, CTA and unsubscribe in HTML and text', async () => {
887
+ const data = {
888
+ ...TEMPLATE_FIXTURES.announcement,
889
+ body: '<script>alert(1)</script>\nSecond line',
890
+ };
891
+ const { html, text, subject } = await renderEmail('announcement', VETPASS_BRANDING, data);
892
+ expect(subject).toBe(data.title);
893
+ expect(html).toContain('VetPass');
894
+ expect(html).not.toContain('<script>alert');
895
+ expect(html).toContain('&lt;script&gt;');
896
+ for (const output of [html, text]) {
897
+ expect(output).toContain(data.ctaUrl);
898
+ expect(output).toContain(data.unsubscribeUrl);
899
+ expect(output).toContain('Second line');
900
+ }
901
+ });
902
+ });
@@ -70,8 +70,8 @@ describe('resolveTenantFromRequest', () => {
70
70
  expect(result.resolvedVia).toBe('origin');
71
71
  });
72
72
 
73
- it('resolves vetpass from alpha.vetpass.app', () => {
74
- const headers: RequestHeaders = { origin: 'https://alpha.vetpass.app' };
73
+ it('resolves an unlisted vetpass subdomain via its parent domain', () => {
74
+ const headers: RequestHeaders = { origin: 'https://preview.vetpass.app' };
75
75
  const result = resolveTenantFromRequest(headers);
76
76
 
77
77
  expect(result.id).toBe('vetpass');
@@ -24,6 +24,7 @@ import { SHARED, htmlLang, htmlDir, interpolate, resolveCatalogLocale } from '..
24
24
  interface LayoutProps {
25
25
  branding: TenantBranding;
26
26
  preview: string;
27
+ disclaimer?: string;
27
28
  children: React.ReactNode;
28
29
  /** Show the brand icon at the top of the card. Defaults to true.
29
30
  * Set to false when the template already has an IssuerLogo. */
@@ -35,12 +36,14 @@ interface LayoutProps {
35
36
  export const Layout: React.FC<LayoutProps> = ({
36
37
  branding,
37
38
  preview,
39
+ disclaimer,
38
40
  children,
39
41
  showHeaderLogo = true,
40
42
  locale,
41
43
  }) => {
42
44
  const chrome = SHARED[resolveCatalogLocale(locale)];
43
- const disclaimerText = interpolate(chrome.footerDisclaimer, { brandName: branding.brandName });
45
+ const disclaimerText =
46
+ disclaimer ?? interpolate(chrome.footerDisclaimer, { brandName: branding.brandName });
44
47
 
45
48
  return (
46
49
  <Html lang={htmlLang(locale)} dir={htmlDir(locale)}>
package/src/i18n.ts CHANGED
@@ -108,3 +108,36 @@ export const interpolate = (template: string, params: Record<string, string | un
108
108
  const value = params[key];
109
109
  return value === undefined ? match : value;
110
110
  });
111
+
112
+ /**
113
+ * `{ locale }` when set, `{}` otherwise — spread into props whose `locale` is optional
114
+ * so an absent locale is omitted rather than passed as `undefined`
115
+ * (safe under `exactOptionalPropertyTypes`).
116
+ */
117
+ export const localeProp = (locale: string | null | undefined): { locale?: string } =>
118
+ locale === undefined || locale === null ? {} : { locale };
119
+
120
+ /**
121
+ * lca-api passes ISO timestamps for security notices; show them as a readable date in
122
+ * the recipient's language. The recipient's time zone is unknown, so the time is shown
123
+ * in UTC and labelled as such. Anything that is not an ISO timestamp (previews,
124
+ * pre-formatted text) is shown as is.
125
+ */
126
+ export const formatNoticeTimestamp = (value: string, locale: NotificationLocale): string => {
127
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) return value;
128
+
129
+ const date = new Date(value);
130
+ if (Number.isNaN(date.getTime())) return value;
131
+
132
+ try {
133
+ return new Intl.DateTimeFormat(locale, {
134
+ dateStyle: 'long',
135
+ timeStyle: 'short',
136
+ timeZone: 'UTC',
137
+ })
138
+ .format(date)
139
+ .concat(' UTC');
140
+ } catch {
141
+ return value;
142
+ }
143
+ };
package/src/index.ts CHANGED
@@ -44,6 +44,8 @@ export type {
44
44
  EscrowHoldReleasedData,
45
45
  EscrowHoldCancelledData,
46
46
  EscrowPinLockedData,
47
+ EscrowPinSignInData,
48
+ AccountSecuredData,
47
49
  } from './render';
48
50
  export type { EscrowHoldCancelledReason } from './templates/escrow-hold-cancelled';
49
51
 
@@ -69,3 +71,5 @@ export type { ResolvedTenant, RequestHeaders } from './tenant-registry';
69
71
  // configure `jsx` in their tsconfig. Use renderEmail() / renderSms() instead.
70
72
  // For direct component access (testing / preview), import from
71
73
  // '@learncard/email-templates/src/templates' explicitly.
74
+
75
+ export type { AnnouncementData } from './render';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * account-secured — all preview scenarios stacked.
3
+ */
4
+
5
+ import * as React from 'react';
6
+
7
+ import { AccountSecured } from '../templates/account-secured';
8
+ import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
9
+
10
+ export default function AccountSecuredScenarios() {
11
+ return (
12
+ <>
13
+ <ScenarioDivider label="Default (LearnCard)" />
14
+ <AccountSecured branding={LEARNCARD_BRANDING} securedAt="October 2, 2026, 3:00 PM" />
15
+
16
+ <ScenarioDivider label="Spanish" />
17
+ <AccountSecured
18
+ branding={LEARNCARD_BRANDING}
19
+ securedAt="2 de octubre de 2026, 3:00 p.m."
20
+ locale="es"
21
+ />
22
+
23
+ <ScenarioDivider label="French" />
24
+ <AccountSecured
25
+ branding={LEARNCARD_BRANDING}
26
+ securedAt="2 octobre 2026, 15h00"
27
+ locale="fr"
28
+ />
29
+
30
+ <ScenarioDivider label="Arabic (RTL)" />
31
+ <AccountSecured
32
+ branding={LEARNCARD_BRANDING}
33
+ securedAt="2 أكتوبر 2026، 3:00 م"
34
+ locale="ar"
35
+ />
36
+
37
+ <ScenarioDivider label="VetPass tenant branding" />
38
+ <AccountSecured branding={VETPASS_BRANDING} securedAt="October 2, 2026, 3:00 PM" />
39
+ </>
40
+ );
41
+ }
@@ -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
@@ -1,3 +1,4 @@
1
+ import { Announcement, getAnnouncementSubject } from './templates/announcement';
1
2
  /**
2
3
  * render.ts — the main rendering entry point.
3
4
  *
@@ -53,6 +54,10 @@ import {
53
54
  getEscrowHoldCancelledSubject,
54
55
  EscrowPinLocked,
55
56
  getEscrowPinLockedSubject,
57
+ EscrowPinSignIn,
58
+ getEscrowPinSignInSubject,
59
+ AccountSecured,
60
+ getAccountSecuredSubject,
56
61
  } from './templates';
57
62
 
58
63
  import type {
@@ -75,7 +80,10 @@ import type {
75
80
  EscrowHoldCancelledProps,
76
81
  EscrowHoldCancelledReason,
77
82
  EscrowPinLockedProps,
83
+ EscrowPinSignInProps,
84
+ AccountSecuredProps,
78
85
  } from './templates';
86
+ import { localeProp } from './i18n';
79
87
 
80
88
  // ---------------------------------------------------------------------------
81
89
  // Public types
@@ -94,7 +102,17 @@ export interface RenderedEmail {
94
102
  * This provides full type safety: `renderEmail('inbox-claim', branding, data)` will
95
103
  * require `data` to match `InboxClaimData`.
96
104
  */
105
+ export interface AnnouncementData {
106
+ title: string;
107
+ body: string;
108
+ heroImageUrl?: string;
109
+ ctaLabel: string;
110
+ ctaUrl?: string;
111
+ unsubscribeUrl: string;
112
+ }
113
+
97
114
  export interface TemplateDataMap {
115
+ announcement: AnnouncementData;
98
116
  // -- Verification code variants ------------------------------------------
99
117
 
100
118
  /** brain-service: contact-methods.ts, embed flow */
@@ -190,6 +208,12 @@ export interface TemplateDataMap {
190
208
 
191
209
  /** lca-api: recovery PIN locked after too many failed attempts */
192
210
  'escrow-pin-locked': EscrowPinLockedData;
211
+
212
+ /** lca-api: a new device signed in with the recovery PIN — links to "Secure my account" */
213
+ 'escrow-pin-sign-in': EscrowPinSignInData;
214
+
215
+ /** lca-api: "Secure my account" signed out every other device and login */
216
+ 'account-secured': AccountSecuredData;
193
217
  }
194
218
 
195
219
  export type TemplateId = keyof TemplateDataMap;
@@ -304,6 +328,15 @@ export interface EscrowPinLockedData {
304
328
  cancelUrl?: string;
305
329
  }
306
330
 
331
+ export interface EscrowPinSignInData {
332
+ signedInAt: string;
333
+ secureUrl: string;
334
+ }
335
+
336
+ export interface AccountSecuredData {
337
+ securedAt: string;
338
+ }
339
+
307
340
  // ---------------------------------------------------------------------------
308
341
  // renderEmail()
309
342
  // ---------------------------------------------------------------------------
@@ -501,6 +534,17 @@ function buildElement(
501
534
  };
502
535
  }
503
536
 
537
+ case 'announcement': {
538
+ const d = data as AnnouncementData;
539
+ return {
540
+ element: React.createElement(Announcement, {
541
+ branding,
542
+ ...d,
543
+ ...(locale === undefined ? {} : { locale }),
544
+ }),
545
+ subject: getAnnouncementSubject(d),
546
+ };
547
+ }
504
548
  case 'credential-updated': {
505
549
  const d = data as CredentialUpdatedData;
506
550
  const props: CredentialUpdatedProps = { branding, ...d, locale };
@@ -570,6 +614,35 @@ function buildElement(
570
614
  };
571
615
  }
572
616
 
617
+ case 'escrow-pin-sign-in': {
618
+ const d = data as EscrowPinSignInData;
619
+ const props: EscrowPinSignInProps = {
620
+ branding,
621
+ signedInAt: d.signedInAt,
622
+ secureUrl: d.secureUrl,
623
+ ...localeProp(locale),
624
+ };
625
+
626
+ return {
627
+ element: React.createElement(EscrowPinSignIn, props),
628
+ subject: getEscrowPinSignInSubject(branding, locale),
629
+ };
630
+ }
631
+
632
+ case 'account-secured': {
633
+ const d = data as AccountSecuredData;
634
+ const props: AccountSecuredProps = {
635
+ branding,
636
+ securedAt: d.securedAt,
637
+ ...localeProp(locale),
638
+ };
639
+
640
+ return {
641
+ element: React.createElement(AccountSecured, props),
642
+ subject: getAccountSecuredSubject(branding, locale),
643
+ };
644
+ }
645
+
573
646
  default: {
574
647
  const _exhaustive: never = templateId;
575
648
  throw new Error(`Unknown template: ${_exhaustive}`);