@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
@@ -138,6 +138,41 @@ const TEMPLATE_FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
138
138
  credential: { name: 'Introduction to Biology' },
139
139
  },
140
140
  'account-sign-in-changed': {},
141
+
142
+ 'escrow-hold-started': {
143
+ requestedAt: 'September 25, 2026, 3:00 PM',
144
+ releaseAfter: 'October 2, 2026, 3:00 PM',
145
+ cancelUrl: 'https://learncard.app/recovery/cancel?token=abc123',
146
+ deviceHint: 'Chrome on Windows',
147
+ },
148
+
149
+ 'escrow-hold-reminder': {
150
+ releaseAfter: 'October 2, 2026, 3:00 PM',
151
+ cancelUrl: 'https://learncard.app/recovery/cancel?token=abc123',
152
+ },
153
+
154
+ 'escrow-hold-released': {
155
+ completedAt: 'October 2, 2026, 3:00 PM',
156
+ supportUrl: 'https://learncard.app/support',
157
+ },
158
+
159
+ 'escrow-hold-cancelled': {
160
+ cancelledAt: 'September 26, 2026, 9:15 AM',
161
+ reason: 'user',
162
+ },
163
+
164
+ 'escrow-pin-locked': {
165
+ lockedAt: 'September 25, 2026, 3:00 PM',
166
+ releaseAfter: 'October 2, 2026, 3:00 PM',
167
+ cancelUrl: 'https://learncard.app/recovery/cancel?token=abc123',
168
+ },
169
+ 'escrow-pin-sign-in': {
170
+ signedInAt: 'October 2, 2026, 3:00 PM',
171
+ secureUrl: 'https://learncard.app/account/secure',
172
+ },
173
+ 'account-secured': {
174
+ securedAt: 'October 2, 2026, 3:00 PM',
175
+ },
141
176
  };
142
177
 
143
178
  const ALL_TEMPLATE_IDS = Object.keys(TEMPLATE_FIXTURES) as TemplateId[];
@@ -386,6 +421,277 @@ describe('renderEmail — credential-updated', () => {
386
421
  });
387
422
  });
388
423
 
424
+ // ---------------------------------------------------------------------------
425
+ // Escrow hold lifecycle (nitro-escrow-enclave, P5.1) — plain-language recovery
426
+ // notifications. No user-facing jargon: never render "wallet", "DID",
427
+ // "share", "enclave", "escrow", "SSS", or "key derivation".
428
+ // ---------------------------------------------------------------------------
429
+
430
+ describe('renderEmail — escrow hold lifecycle', () => {
431
+ const JARGON = ['wallet', 'DID', 'share', 'enclave', 'escrow', 'SSS', 'key derivation'];
432
+
433
+ const assertNoJargon = (text: string) => {
434
+ const lower = text.toLowerCase();
435
+ for (const word of JARGON) {
436
+ expect(lower).not.toContain(word.toLowerCase());
437
+ }
438
+ };
439
+
440
+ it('escrow-hold-started includes the cancel URL, both dates, and the device hint', async () => {
441
+ const data = TEMPLATE_FIXTURES['escrow-hold-started'];
442
+ const { html, text, subject } = await renderEmail(
443
+ 'escrow-hold-started',
444
+ DEFAULT_BRANDING,
445
+ data
446
+ );
447
+
448
+ expect(html).toContain(data.cancelUrl);
449
+ expect(html).toContain(data.requestedAt);
450
+ expect(html).toContain(data.releaseAfter);
451
+ expect(html).toContain(data.deviceHint);
452
+ expect(subject).toBe('Account recovery started');
453
+ assertNoJargon(html);
454
+ assertNoJargon(text);
455
+ assertNoJargon(subject);
456
+ });
457
+
458
+ it('escrow-hold-started renders without the optional device hint', async () => {
459
+ const { html } = await renderEmail('escrow-hold-started', DEFAULT_BRANDING, {
460
+ requestedAt: 'September 25, 2026, 3:00 PM',
461
+ releaseAfter: 'October 2, 2026, 3:00 PM',
462
+ cancelUrl: 'https://learncard.app/recovery/cancel?token=xyz',
463
+ });
464
+
465
+ expect(html).toContain('https://learncard.app/recovery/cancel?token=xyz');
466
+ expect(html).not.toContain('undefined');
467
+ });
468
+
469
+ it('escrow-hold-reminder includes the cancel URL and the release date', async () => {
470
+ const data = TEMPLATE_FIXTURES['escrow-hold-reminder'];
471
+ const { html, text, subject } = await renderEmail(
472
+ 'escrow-hold-reminder',
473
+ DEFAULT_BRANDING,
474
+ data
475
+ );
476
+
477
+ expect(html).toContain(data.cancelUrl);
478
+ expect(html).toContain(data.releaseAfter);
479
+ expect(subject).toBe('Recovery completes tomorrow');
480
+ assertNoJargon(html);
481
+ assertNoJargon(text);
482
+ assertNoJargon(subject);
483
+ });
484
+
485
+ it('escrow-hold-released includes the completion date and the support link when provided', async () => {
486
+ const data = TEMPLATE_FIXTURES['escrow-hold-released'];
487
+ const { html, text, subject } = await renderEmail(
488
+ 'escrow-hold-released',
489
+ DEFAULT_BRANDING,
490
+ data
491
+ );
492
+
493
+ expect(html).toContain(data.completedAt);
494
+ expect(html).toContain(data.supportUrl);
495
+ expect(subject).toBe('Your account was recovered');
496
+ assertNoJargon(html);
497
+ assertNoJargon(text);
498
+ assertNoJargon(subject);
499
+ });
500
+
501
+ it('escrow-hold-released renders without the optional support link', async () => {
502
+ const { html } = await renderEmail('escrow-hold-released', DEFAULT_BRANDING, {
503
+ completedAt: 'October 2, 2026, 3:00 PM',
504
+ });
505
+
506
+ expect(html).toContain('October 2, 2026, 3:00 PM');
507
+ expect(html).not.toContain('undefined');
508
+ });
509
+
510
+ it.each([
511
+ ['user', 'cancelled this request'],
512
+ ['superseded', 'newer recovery request'],
513
+ ['pin-locked', 'incorrect PIN attempts'],
514
+ ['release-failed', 'could not be completed'],
515
+ ] as const)('escrow-hold-cancelled renders reason "%s"', async (reason, expectedSnippet) => {
516
+ const { html, subject } = await renderEmail('escrow-hold-cancelled', DEFAULT_BRANDING, {
517
+ cancelledAt: 'September 26, 2026, 9:15 AM',
518
+ reason,
519
+ });
520
+
521
+ expect(html).toContain('September 26, 2026, 9:15 AM');
522
+ expect(html).toContain(expectedSnippet);
523
+ expect(subject).toBe('Recovery request cancelled');
524
+ assertNoJargon(html);
525
+ });
526
+
527
+ it('escrow-hold-cancelled renders a generic message when reason is omitted', async () => {
528
+ const { html } = await renderEmail('escrow-hold-cancelled', DEFAULT_BRANDING, {
529
+ cancelledAt: 'September 26, 2026, 9:15 AM',
530
+ });
531
+
532
+ expect(html).toContain('No further action is needed');
533
+ });
534
+
535
+ it('escrow-pin-locked includes the locked date, release date, and cancel URL when a hold is pending', async () => {
536
+ const data = TEMPLATE_FIXTURES['escrow-pin-locked'];
537
+ const { html, text, subject } = await renderEmail(
538
+ 'escrow-pin-locked',
539
+ DEFAULT_BRANDING,
540
+ data
541
+ );
542
+
543
+ expect(html).toContain(data.lockedAt);
544
+ expect(html).toContain(data.releaseAfter);
545
+ expect(html).toContain(data.cancelUrl);
546
+ expect(subject).toBe('Recovery PIN locked');
547
+ assertNoJargon(html);
548
+ assertNoJargon(text);
549
+ assertNoJargon(subject);
550
+ });
551
+
552
+ it('escrow-pin-locked renders generic fallback copy with no button when no hold is pending', async () => {
553
+ const { html } = await renderEmail('escrow-pin-locked', DEFAULT_BRANDING, {
554
+ lockedAt: 'September 25, 2026, 3:00 PM',
555
+ });
556
+
557
+ expect(html).toContain('September 25, 2026, 3:00 PM');
558
+ expect(html).toContain('waiting-period recovery is still available');
559
+ expect(html).not.toContain('undefined');
560
+ });
561
+
562
+ it('escrow-pin-sign-in includes the sign-in date and the secure-account link', async () => {
563
+ const data = TEMPLATE_FIXTURES['escrow-pin-sign-in'];
564
+ const { html, text, subject } = await renderEmail(
565
+ 'escrow-pin-sign-in',
566
+ DEFAULT_BRANDING,
567
+ data
568
+ );
569
+
570
+ expect(html).toContain(data.signedInAt);
571
+ expect(html).toContain(data.secureUrl);
572
+ expect(text).toContain('Not you? Secure your account.');
573
+ expect(text).toContain('Secure my account');
574
+ expect(subject).toBe('New device signed in with your PIN');
575
+ assertNoJargon(html);
576
+ assertNoJargon(text);
577
+ assertNoJargon(subject);
578
+ });
579
+
580
+ it('escrow-pin-sign-in formats the ISO timestamp lca-api sends, per locale', async () => {
581
+ const iso = '2026-11-02T15:04:05.123Z';
582
+ const en = await renderEmail('escrow-pin-sign-in', DEFAULT_BRANDING, {
583
+ signedInAt: iso,
584
+ secureUrl: 'https://learncard.app/account/secure',
585
+ });
586
+ const fr = await renderEmail(
587
+ 'escrow-pin-sign-in',
588
+ DEFAULT_BRANDING,
589
+ { signedInAt: iso, secureUrl: 'https://learncard.app/account/secure' },
590
+ 'fr'
591
+ );
592
+
593
+ expect(en.text).not.toContain(iso);
594
+ expect(en.text).toContain('November 2, 2026');
595
+ expect(en.text).toContain('UTC');
596
+ expect(fr.text).toContain('2 novembre 2026');
597
+ });
598
+
599
+ it('escrow-pin-sign-in carries tenant branding', async () => {
600
+ const { html } = await renderEmail('escrow-pin-sign-in', VETPASS_BRANDING, {
601
+ signedInAt: 'October 2, 2026, 3:00 PM',
602
+ secureUrl: 'https://vetpass.app/account/secure',
603
+ });
604
+
605
+ expect(html).toContain('VetPass');
606
+ expect(html).toContain('https://vetpass.app/account/secure');
607
+ });
608
+
609
+ it('account-secured works for an owner who did not do it (no secure link, support first)', async () => {
610
+ const data = TEMPLATE_FIXTURES['account-secured'];
611
+ const { html, text, subject } = await renderEmail(
612
+ 'account-secured',
613
+ DEFAULT_BRANDING,
614
+ data
615
+ );
616
+
617
+ expect(html).toContain(data.securedAt);
618
+ expect(subject).toBe('Other devices were signed out of your account');
619
+ // The owner's login may be gone, so the notice must not depend on the in-app action.
620
+ expect(html).not.toContain('/account/secure');
621
+ expect(text).toContain('your own sign-in may have been removed');
622
+ // The revoke reset every recovery method and removed automatic recovery, so the
623
+ // lost-login path is not offered as the way back; support is.
624
+ expect(text).toContain('reset its recovery methods');
625
+ expect(text).not.toContain('I lost access to my school email or login');
626
+ expect(text).toContain(`Contact ${DEFAULT_BRANDING.supportEmail} right away`);
627
+ expect(html).toContain(`href="${DEFAULT_BRANDING.appUrl}"`);
628
+ assertNoJargon(html);
629
+ assertNoJargon(text);
630
+ assertNoJargon(subject);
631
+ });
632
+
633
+ it('account-secured formats the ISO timestamp lca-api sends, per locale', async () => {
634
+ const iso = '2026-11-02T15:04:05.123Z';
635
+ const en = await renderEmail('account-secured', DEFAULT_BRANDING, { securedAt: iso });
636
+ const es = await renderEmail('account-secured', DEFAULT_BRANDING, { securedAt: iso }, 'es');
637
+
638
+ expect(en.text).not.toContain(iso);
639
+ expect(en.text).toContain('November 2, 2026');
640
+ expect(en.text).toContain('UTC');
641
+ expect(es.text).toContain('2 de noviembre de 2026');
642
+ expect(es.subject).not.toBe(en.subject);
643
+ });
644
+
645
+ it('account-secured carries tenant branding and the tenant support address', async () => {
646
+ const { html, text } = await renderEmail('account-secured', VETPASS_BRANDING, {
647
+ securedAt: 'October 2, 2026, 3:00 PM',
648
+ });
649
+
650
+ expect(html).toContain('VetPass');
651
+ expect(text).toContain(`Contact ${VETPASS_BRANDING.supportEmail} right away`);
652
+ expect(html).toContain(`href="${VETPASS_BRANDING.appUrl}"`);
653
+ expect(text).not.toContain(DEFAULT_BRANDING.supportEmail);
654
+ });
655
+
656
+ it('account-secured points to the organization when the tenant has no support address', async () => {
657
+ const { text } = await renderEmail(
658
+ 'account-secured',
659
+ { ...DEFAULT_BRANDING, supportEmail: '' },
660
+ TEMPLATE_FIXTURES['account-secured']
661
+ );
662
+
663
+ expect(text).not.toContain('Contact right away');
664
+ expect(text).toContain(
665
+ `Contact the organization that gave you access to ${DEFAULT_BRANDING.brandName} right away`
666
+ );
667
+ expect(text).not.toContain('I lost access to my school email or login');
668
+ });
669
+
670
+ it('all escrow hold templates include the "This wasn\'t me" cancel CTA when a cancel URL is present', async () => {
671
+ const started = await renderEmail(
672
+ 'escrow-hold-started',
673
+ DEFAULT_BRANDING,
674
+ TEMPLATE_FIXTURES['escrow-hold-started']
675
+ );
676
+ const reminder = await renderEmail(
677
+ 'escrow-hold-reminder',
678
+ DEFAULT_BRANDING,
679
+ TEMPLATE_FIXTURES['escrow-hold-reminder']
680
+ );
681
+ const pinLocked = await renderEmail(
682
+ 'escrow-pin-locked',
683
+ DEFAULT_BRANDING,
684
+ TEMPLATE_FIXTURES['escrow-pin-locked']
685
+ );
686
+
687
+ // Plain text output has no HTML entity escaping, so the apostrophe is literal.
688
+ for (const { text } of [started, reminder, pinLocked]) {
689
+ expect(text).toContain("This wasn't me");
690
+ expect(text).toContain('Cancel recovery');
691
+ }
692
+ });
693
+ });
694
+
389
695
  // ---------------------------------------------------------------------------
390
696
  // Branding tests — custom branding flows through to rendered output
391
697
  // ---------------------------------------------------------------------------
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
@@ -39,7 +39,15 @@ export type {
39
39
  GuardianRejectedCredentialData,
40
40
  CredentialUpdatedData,
41
41
  AccountSignInChangedData,
42
+ EscrowHoldStartedData,
43
+ EscrowHoldReminderData,
44
+ EscrowHoldReleasedData,
45
+ EscrowHoldCancelledData,
46
+ EscrowPinLockedData,
47
+ EscrowPinSignInData,
48
+ AccountSecuredData,
42
49
  } from './render';
50
+ export type { EscrowHoldCancelledReason } from './templates/escrow-hold-cancelled';
43
51
 
44
52
  // SMS rendering
45
53
  export { renderSms } from './sms';
@@ -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,79 @@
1
+ /**
2
+ * escrow-hold-cancelled — all preview scenarios stacked (one per reason).
3
+ */
4
+
5
+ import * as React from 'react';
6
+
7
+ import { EscrowHoldCancelled } from '../templates/escrow-hold-cancelled';
8
+ import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
9
+
10
+ export default function EscrowHoldCancelledScenarios() {
11
+ return (
12
+ <>
13
+ <ScenarioDivider label="Reason: user" />
14
+ <EscrowHoldCancelled
15
+ branding={LEARNCARD_BRANDING}
16
+ cancelledAt="September 26, 2026, 9:15 AM"
17
+ reason="user"
18
+ />
19
+
20
+ <ScenarioDivider label="Reason: superseded" />
21
+ <EscrowHoldCancelled
22
+ branding={LEARNCARD_BRANDING}
23
+ cancelledAt="September 26, 2026, 9:15 AM"
24
+ reason="superseded"
25
+ />
26
+
27
+ <ScenarioDivider label="Reason: pin-locked" />
28
+ <EscrowHoldCancelled
29
+ branding={LEARNCARD_BRANDING}
30
+ cancelledAt="September 26, 2026, 9:15 AM"
31
+ reason="pin-locked"
32
+ />
33
+
34
+ <ScenarioDivider label="Reason: release-failed" />
35
+ <EscrowHoldCancelled
36
+ branding={LEARNCARD_BRANDING}
37
+ cancelledAt="September 26, 2026, 9:15 AM"
38
+ reason="release-failed"
39
+ />
40
+
41
+ <ScenarioDivider label="No reason (generic message)" />
42
+ <EscrowHoldCancelled
43
+ branding={LEARNCARD_BRANDING}
44
+ cancelledAt="September 26, 2026, 9:15 AM"
45
+ />
46
+
47
+ <ScenarioDivider label="Spanish" />
48
+ <EscrowHoldCancelled
49
+ branding={LEARNCARD_BRANDING}
50
+ cancelledAt="26 de septiembre de 2026, 9:15 a.m."
51
+ reason="user"
52
+ locale="es"
53
+ />
54
+
55
+ <ScenarioDivider label="French" />
56
+ <EscrowHoldCancelled
57
+ branding={LEARNCARD_BRANDING}
58
+ cancelledAt="26 septembre 2026, 9h15"
59
+ reason="user"
60
+ locale="fr"
61
+ />
62
+
63
+ <ScenarioDivider label="Arabic (RTL)" />
64
+ <EscrowHoldCancelled
65
+ branding={LEARNCARD_BRANDING}
66
+ cancelledAt="26 سبتمبر 2026، 9:15 ص"
67
+ reason="user"
68
+ locale="ar"
69
+ />
70
+
71
+ <ScenarioDivider label="VetPass tenant branding" />
72
+ <EscrowHoldCancelled
73
+ branding={VETPASS_BRANDING}
74
+ cancelledAt="September 26, 2026, 9:15 AM"
75
+ reason="user"
76
+ />
77
+ </>
78
+ );
79
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * escrow-hold-released — all preview scenarios stacked.
3
+ */
4
+
5
+ import * as React from 'react';
6
+
7
+ import { EscrowHoldReleased } from '../templates/escrow-hold-released';
8
+ import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
9
+
10
+ export default function EscrowHoldReleasedScenarios() {
11
+ return (
12
+ <>
13
+ <ScenarioDivider label="With support link" />
14
+ <EscrowHoldReleased
15
+ branding={LEARNCARD_BRANDING}
16
+ completedAt="October 2, 2026, 3:00 PM"
17
+ supportUrl="https://learncard.app/support"
18
+ />
19
+
20
+ <ScenarioDivider label="No support link (footer contact only)" />
21
+ <EscrowHoldReleased
22
+ branding={LEARNCARD_BRANDING}
23
+ completedAt="October 2, 2026, 3:00 PM"
24
+ />
25
+
26
+ <ScenarioDivider label="Spanish" />
27
+ <EscrowHoldReleased
28
+ branding={LEARNCARD_BRANDING}
29
+ completedAt="2 de octubre de 2026, 3:00 p.m."
30
+ supportUrl="https://learncard.app/support"
31
+ locale="es"
32
+ />
33
+
34
+ <ScenarioDivider label="French" />
35
+ <EscrowHoldReleased
36
+ branding={LEARNCARD_BRANDING}
37
+ completedAt="2 octobre 2026, 15h00"
38
+ supportUrl="https://learncard.app/support"
39
+ locale="fr"
40
+ />
41
+
42
+ <ScenarioDivider label="Arabic (RTL)" />
43
+ <EscrowHoldReleased
44
+ branding={LEARNCARD_BRANDING}
45
+ completedAt="2 أكتوبر 2026، 3:00 م"
46
+ supportUrl="https://learncard.app/support"
47
+ locale="ar"
48
+ />
49
+
50
+ <ScenarioDivider label="VetPass tenant branding" />
51
+ <EscrowHoldReleased
52
+ branding={VETPASS_BRANDING}
53
+ completedAt="October 2, 2026, 3:00 PM"
54
+ supportUrl="https://vetpass.app/support"
55
+ />
56
+ </>
57
+ );
58
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * escrow-hold-reminder — all preview scenarios stacked.
3
+ */
4
+
5
+ import * as React from 'react';
6
+
7
+ import { EscrowHoldReminder } from '../templates/escrow-hold-reminder';
8
+ import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
9
+
10
+ export default function EscrowHoldReminderScenarios() {
11
+ return (
12
+ <>
13
+ <ScenarioDivider label="Default — LearnCard branding" />
14
+ <EscrowHoldReminder
15
+ branding={LEARNCARD_BRANDING}
16
+ releaseAfter="October 2, 2026, 3:00 PM"
17
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
18
+ />
19
+
20
+ <ScenarioDivider label="Spanish" />
21
+ <EscrowHoldReminder
22
+ branding={LEARNCARD_BRANDING}
23
+ releaseAfter="2 de octubre de 2026, 3:00 p.m."
24
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
25
+ locale="es"
26
+ />
27
+
28
+ <ScenarioDivider label="French" />
29
+ <EscrowHoldReminder
30
+ branding={LEARNCARD_BRANDING}
31
+ releaseAfter="2 octobre 2026, 15h00"
32
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
33
+ locale="fr"
34
+ />
35
+
36
+ <ScenarioDivider label="Arabic (RTL)" />
37
+ <EscrowHoldReminder
38
+ branding={LEARNCARD_BRANDING}
39
+ releaseAfter="2 أكتوبر 2026، 3:00 م"
40
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
41
+ locale="ar"
42
+ />
43
+
44
+ <ScenarioDivider label="VetPass tenant branding" />
45
+ <EscrowHoldReminder
46
+ branding={VETPASS_BRANDING}
47
+ releaseAfter="October 2, 2026, 3:00 PM"
48
+ cancelUrl="https://vetpass.app/recovery/cancel?token=abc123"
49
+ />
50
+ </>
51
+ );
52
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * escrow-hold-started — all preview scenarios stacked.
3
+ */
4
+
5
+ import * as React from 'react';
6
+
7
+ import { EscrowHoldStarted } from '../templates/escrow-hold-started';
8
+ import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
9
+
10
+ export default function EscrowHoldStartedScenarios() {
11
+ return (
12
+ <>
13
+ <ScenarioDivider label="Default — with device hint" />
14
+ <EscrowHoldStarted
15
+ branding={LEARNCARD_BRANDING}
16
+ requestedAt="September 25, 2026, 3:00 PM"
17
+ releaseAfter="October 2, 2026, 3:00 PM"
18
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
19
+ deviceHint="Chrome on Windows"
20
+ />
21
+
22
+ <ScenarioDivider label="No device hint" />
23
+ <EscrowHoldStarted
24
+ branding={LEARNCARD_BRANDING}
25
+ requestedAt="September 25, 2026, 3:00 PM"
26
+ releaseAfter="October 2, 2026, 3:00 PM"
27
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
28
+ />
29
+
30
+ <ScenarioDivider label="Spanish" />
31
+ <EscrowHoldStarted
32
+ branding={LEARNCARD_BRANDING}
33
+ requestedAt="25 de septiembre de 2026, 3:00 p.m."
34
+ releaseAfter="2 de octubre de 2026, 3:00 p.m."
35
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
36
+ locale="es"
37
+ />
38
+
39
+ <ScenarioDivider label="French" />
40
+ <EscrowHoldStarted
41
+ branding={LEARNCARD_BRANDING}
42
+ requestedAt="25 septembre 2026, 15h00"
43
+ releaseAfter="2 octobre 2026, 15h00"
44
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
45
+ locale="fr"
46
+ />
47
+
48
+ <ScenarioDivider label="Arabic (RTL)" />
49
+ <EscrowHoldStarted
50
+ branding={LEARNCARD_BRANDING}
51
+ requestedAt="25 سبتمبر 2026، 3:00 م"
52
+ releaseAfter="2 أكتوبر 2026، 3:00 م"
53
+ cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
54
+ locale="ar"
55
+ />
56
+
57
+ <ScenarioDivider label="VetPass tenant branding" />
58
+ <EscrowHoldStarted
59
+ branding={VETPASS_BRANDING}
60
+ requestedAt="September 25, 2026, 3:00 PM"
61
+ releaseAfter="October 2, 2026, 3:00 PM"
62
+ cancelUrl="https://vetpass.app/recovery/cancel?token=abc123"
63
+ deviceHint="Safari on iPhone"
64
+ />
65
+ </>
66
+ );
67
+ }