@learncard/email-templates 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/previews/escrow-hold-cancelled-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-cancelled-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-cancelled-scenarios.js +7 -0
- package/dist/previews/escrow-hold-cancelled-scenarios.js.map +1 -0
- package/dist/previews/escrow-hold-released-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-released-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-released-scenarios.js +7 -0
- package/dist/previews/escrow-hold-released-scenarios.js.map +1 -0
- package/dist/previews/escrow-hold-reminder-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-reminder-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-reminder-scenarios.js +7 -0
- package/dist/previews/escrow-hold-reminder-scenarios.js.map +1 -0
- package/dist/previews/escrow-hold-started-scenarios.d.ts +6 -0
- package/dist/previews/escrow-hold-started-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-hold-started-scenarios.js +7 -0
- package/dist/previews/escrow-hold-started-scenarios.js.map +1 -0
- package/dist/previews/escrow-pin-locked-scenarios.d.ts +6 -0
- package/dist/previews/escrow-pin-locked-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-pin-locked-scenarios.js +7 -0
- package/dist/previews/escrow-pin-locked-scenarios.js.map +1 -0
- package/dist/render.d.ts +34 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +41 -1
- package/dist/render.js.map +1 -1
- package/dist/templates/escrow-hold-cancelled.d.ts +23 -0
- package/dist/templates/escrow-hold-cancelled.d.ts.map +1 -0
- package/dist/templates/escrow-hold-cancelled.js +131 -0
- package/dist/templates/escrow-hold-cancelled.js.map +1 -0
- package/dist/templates/escrow-hold-released.d.ts +21 -0
- package/dist/templates/escrow-hold-released.d.ts.map +1 -0
- package/dist/templates/escrow-hold-released.js +92 -0
- package/dist/templates/escrow-hold-released.js.map +1 -0
- package/dist/templates/escrow-hold-reminder.d.ts +21 -0
- package/dist/templates/escrow-hold-reminder.d.ts.map +1 -0
- package/dist/templates/escrow-hold-reminder.js +92 -0
- package/dist/templates/escrow-hold-reminder.js.map +1 -0
- package/dist/templates/escrow-hold-started.d.ts +25 -0
- package/dist/templates/escrow-hold-started.d.ts.map +1 -0
- package/dist/templates/escrow-hold-started.js +96 -0
- package/dist/templates/escrow-hold-started.js.map +1 -0
- package/dist/templates/escrow-pin-locked.d.ts +24 -0
- package/dist/templates/escrow-pin-locked.d.ts.map +1 -0
- package/dist/templates/escrow-pin-locked.js +97 -0
- package/dist/templates/escrow-pin-locked.js.map +1 -0
- package/dist/templates/index.d.ts +10 -0
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +5 -0
- package/dist/templates/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/i18n.test.ts +30 -0
- package/src/__tests__/render.test.ts +191 -0
- package/src/index.ts +6 -0
- package/src/previews/escrow-hold-cancelled-scenarios.tsx +79 -0
- package/src/previews/escrow-hold-released-scenarios.tsx +58 -0
- package/src/previews/escrow-hold-reminder-scenarios.tsx +52 -0
- package/src/previews/escrow-hold-started-scenarios.tsx +67 -0
- package/src/previews/escrow-pin-locked-scenarios.tsx +60 -0
- package/src/render.ts +111 -0
- package/src/templates/escrow-hold-cancelled.tsx +212 -0
- package/src/templates/escrow-hold-released.tsx +184 -0
- package/src/templates/escrow-hold-reminder.tsx +184 -0
- package/src/templates/escrow-hold-started.tsx +216 -0
- package/src/templates/escrow-pin-locked.tsx +233 -0
- package/src/templates/index.ts +15 -0
|
@@ -138,6 +138,34 @@ 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
|
+
},
|
|
141
169
|
};
|
|
142
170
|
|
|
143
171
|
const ALL_TEMPLATE_IDS = Object.keys(TEMPLATE_FIXTURES) as TemplateId[];
|
|
@@ -386,6 +414,169 @@ describe('renderEmail — credential-updated', () => {
|
|
|
386
414
|
});
|
|
387
415
|
});
|
|
388
416
|
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Escrow hold lifecycle (nitro-escrow-enclave, P5.1) — plain-language recovery
|
|
419
|
+
// notifications. No user-facing jargon: never render "wallet", "DID",
|
|
420
|
+
// "share", "enclave", "escrow", "SSS", or "key derivation".
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
describe('renderEmail — escrow hold lifecycle', () => {
|
|
424
|
+
const JARGON = ['wallet', 'DID', 'share', 'enclave', 'escrow', 'SSS', 'key derivation'];
|
|
425
|
+
|
|
426
|
+
const assertNoJargon = (text: string) => {
|
|
427
|
+
const lower = text.toLowerCase();
|
|
428
|
+
for (const word of JARGON) {
|
|
429
|
+
expect(lower).not.toContain(word.toLowerCase());
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
it('escrow-hold-started includes the cancel URL, both dates, and the device hint', async () => {
|
|
434
|
+
const data = TEMPLATE_FIXTURES['escrow-hold-started'];
|
|
435
|
+
const { html, text, subject } = await renderEmail(
|
|
436
|
+
'escrow-hold-started',
|
|
437
|
+
DEFAULT_BRANDING,
|
|
438
|
+
data
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
expect(html).toContain(data.cancelUrl);
|
|
442
|
+
expect(html).toContain(data.requestedAt);
|
|
443
|
+
expect(html).toContain(data.releaseAfter);
|
|
444
|
+
expect(html).toContain(data.deviceHint);
|
|
445
|
+
expect(subject).toBe('Account recovery started');
|
|
446
|
+
assertNoJargon(html);
|
|
447
|
+
assertNoJargon(text);
|
|
448
|
+
assertNoJargon(subject);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('escrow-hold-started renders without the optional device hint', async () => {
|
|
452
|
+
const { html } = await renderEmail('escrow-hold-started', DEFAULT_BRANDING, {
|
|
453
|
+
requestedAt: 'September 25, 2026, 3:00 PM',
|
|
454
|
+
releaseAfter: 'October 2, 2026, 3:00 PM',
|
|
455
|
+
cancelUrl: 'https://learncard.app/recovery/cancel?token=xyz',
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(html).toContain('https://learncard.app/recovery/cancel?token=xyz');
|
|
459
|
+
expect(html).not.toContain('undefined');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('escrow-hold-reminder includes the cancel URL and the release date', async () => {
|
|
463
|
+
const data = TEMPLATE_FIXTURES['escrow-hold-reminder'];
|
|
464
|
+
const { html, text, subject } = await renderEmail(
|
|
465
|
+
'escrow-hold-reminder',
|
|
466
|
+
DEFAULT_BRANDING,
|
|
467
|
+
data
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(html).toContain(data.cancelUrl);
|
|
471
|
+
expect(html).toContain(data.releaseAfter);
|
|
472
|
+
expect(subject).toBe('Recovery completes tomorrow');
|
|
473
|
+
assertNoJargon(html);
|
|
474
|
+
assertNoJargon(text);
|
|
475
|
+
assertNoJargon(subject);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('escrow-hold-released includes the completion date and the support link when provided', async () => {
|
|
479
|
+
const data = TEMPLATE_FIXTURES['escrow-hold-released'];
|
|
480
|
+
const { html, text, subject } = await renderEmail(
|
|
481
|
+
'escrow-hold-released',
|
|
482
|
+
DEFAULT_BRANDING,
|
|
483
|
+
data
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
expect(html).toContain(data.completedAt);
|
|
487
|
+
expect(html).toContain(data.supportUrl);
|
|
488
|
+
expect(subject).toBe('Your account was recovered');
|
|
489
|
+
assertNoJargon(html);
|
|
490
|
+
assertNoJargon(text);
|
|
491
|
+
assertNoJargon(subject);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('escrow-hold-released renders without the optional support link', async () => {
|
|
495
|
+
const { html } = await renderEmail('escrow-hold-released', DEFAULT_BRANDING, {
|
|
496
|
+
completedAt: 'October 2, 2026, 3:00 PM',
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
expect(html).toContain('October 2, 2026, 3:00 PM');
|
|
500
|
+
expect(html).not.toContain('undefined');
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it.each([
|
|
504
|
+
['user', 'cancelled this request'],
|
|
505
|
+
['superseded', 'newer recovery request'],
|
|
506
|
+
['pin-locked', 'incorrect PIN attempts'],
|
|
507
|
+
['release-failed', 'could not be completed'],
|
|
508
|
+
] as const)('escrow-hold-cancelled renders reason "%s"', async (reason, expectedSnippet) => {
|
|
509
|
+
const { html, subject } = await renderEmail('escrow-hold-cancelled', DEFAULT_BRANDING, {
|
|
510
|
+
cancelledAt: 'September 26, 2026, 9:15 AM',
|
|
511
|
+
reason,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
expect(html).toContain('September 26, 2026, 9:15 AM');
|
|
515
|
+
expect(html).toContain(expectedSnippet);
|
|
516
|
+
expect(subject).toBe('Recovery request cancelled');
|
|
517
|
+
assertNoJargon(html);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('escrow-hold-cancelled renders a generic message when reason is omitted', async () => {
|
|
521
|
+
const { html } = await renderEmail('escrow-hold-cancelled', DEFAULT_BRANDING, {
|
|
522
|
+
cancelledAt: 'September 26, 2026, 9:15 AM',
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(html).toContain('No further action is needed');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('escrow-pin-locked includes the locked date, release date, and cancel URL when a hold is pending', async () => {
|
|
529
|
+
const data = TEMPLATE_FIXTURES['escrow-pin-locked'];
|
|
530
|
+
const { html, text, subject } = await renderEmail(
|
|
531
|
+
'escrow-pin-locked',
|
|
532
|
+
DEFAULT_BRANDING,
|
|
533
|
+
data
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
expect(html).toContain(data.lockedAt);
|
|
537
|
+
expect(html).toContain(data.releaseAfter);
|
|
538
|
+
expect(html).toContain(data.cancelUrl);
|
|
539
|
+
expect(subject).toBe('Recovery PIN locked');
|
|
540
|
+
assertNoJargon(html);
|
|
541
|
+
assertNoJargon(text);
|
|
542
|
+
assertNoJargon(subject);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('escrow-pin-locked renders generic fallback copy with no button when no hold is pending', async () => {
|
|
546
|
+
const { html } = await renderEmail('escrow-pin-locked', DEFAULT_BRANDING, {
|
|
547
|
+
lockedAt: 'September 25, 2026, 3:00 PM',
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
expect(html).toContain('September 25, 2026, 3:00 PM');
|
|
551
|
+
expect(html).toContain('waiting-period recovery is still available');
|
|
552
|
+
expect(html).not.toContain('undefined');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('all escrow hold templates include the "This wasn\'t me" cancel CTA when a cancel URL is present', async () => {
|
|
556
|
+
const started = await renderEmail(
|
|
557
|
+
'escrow-hold-started',
|
|
558
|
+
DEFAULT_BRANDING,
|
|
559
|
+
TEMPLATE_FIXTURES['escrow-hold-started']
|
|
560
|
+
);
|
|
561
|
+
const reminder = await renderEmail(
|
|
562
|
+
'escrow-hold-reminder',
|
|
563
|
+
DEFAULT_BRANDING,
|
|
564
|
+
TEMPLATE_FIXTURES['escrow-hold-reminder']
|
|
565
|
+
);
|
|
566
|
+
const pinLocked = await renderEmail(
|
|
567
|
+
'escrow-pin-locked',
|
|
568
|
+
DEFAULT_BRANDING,
|
|
569
|
+
TEMPLATE_FIXTURES['escrow-pin-locked']
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
// Plain text output has no HTML entity escaping, so the apostrophe is literal.
|
|
573
|
+
for (const { text } of [started, reminder, pinLocked]) {
|
|
574
|
+
expect(text).toContain("This wasn't me");
|
|
575
|
+
expect(text).toContain('Cancel recovery');
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
389
580
|
// ---------------------------------------------------------------------------
|
|
390
581
|
// Branding tests — custom branding flows through to rendered output
|
|
391
582
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -39,7 +39,13 @@ export type {
|
|
|
39
39
|
GuardianRejectedCredentialData,
|
|
40
40
|
CredentialUpdatedData,
|
|
41
41
|
AccountSignInChangedData,
|
|
42
|
+
EscrowHoldStartedData,
|
|
43
|
+
EscrowHoldReminderData,
|
|
44
|
+
EscrowHoldReleasedData,
|
|
45
|
+
EscrowHoldCancelledData,
|
|
46
|
+
EscrowPinLockedData,
|
|
42
47
|
} from './render';
|
|
48
|
+
export type { EscrowHoldCancelledReason } from './templates/escrow-hold-cancelled';
|
|
43
49
|
|
|
44
50
|
// SMS rendering
|
|
45
51
|
export { renderSms } from './sms';
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escrow-pin-locked — all preview scenarios stacked.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
import { EscrowPinLocked } from '../templates/escrow-pin-locked';
|
|
8
|
+
import { LEARNCARD_BRANDING, VETPASS_BRANDING, ScenarioDivider } from './_fixtures';
|
|
9
|
+
|
|
10
|
+
export default function EscrowPinLockedScenarios() {
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<ScenarioDivider label="With pending waiting-period recovery (cancel link)" />
|
|
14
|
+
<EscrowPinLocked
|
|
15
|
+
branding={LEARNCARD_BRANDING}
|
|
16
|
+
lockedAt="September 25, 2026, 3:00 PM"
|
|
17
|
+
releaseAfter="October 2, 2026, 3:00 PM"
|
|
18
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<ScenarioDivider label="No pending recovery" />
|
|
22
|
+
<EscrowPinLocked branding={LEARNCARD_BRANDING} lockedAt="September 25, 2026, 3:00 PM" />
|
|
23
|
+
|
|
24
|
+
<ScenarioDivider label="Spanish" />
|
|
25
|
+
<EscrowPinLocked
|
|
26
|
+
branding={LEARNCARD_BRANDING}
|
|
27
|
+
lockedAt="25 de septiembre de 2026, 3:00 p.m."
|
|
28
|
+
releaseAfter="2 de octubre de 2026, 3:00 p.m."
|
|
29
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
30
|
+
locale="es"
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
<ScenarioDivider label="French" />
|
|
34
|
+
<EscrowPinLocked
|
|
35
|
+
branding={LEARNCARD_BRANDING}
|
|
36
|
+
lockedAt="25 septembre 2026, 15h00"
|
|
37
|
+
releaseAfter="2 octobre 2026, 15h00"
|
|
38
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
39
|
+
locale="fr"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<ScenarioDivider label="Arabic (RTL)" />
|
|
43
|
+
<EscrowPinLocked
|
|
44
|
+
branding={LEARNCARD_BRANDING}
|
|
45
|
+
lockedAt="25 سبتمبر 2026، 3:00 م"
|
|
46
|
+
releaseAfter="2 أكتوبر 2026، 3:00 م"
|
|
47
|
+
cancelUrl="https://learncard.app/recovery/cancel?token=abc123"
|
|
48
|
+
locale="ar"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<ScenarioDivider label="VetPass tenant branding" />
|
|
52
|
+
<EscrowPinLocked
|
|
53
|
+
branding={VETPASS_BRANDING}
|
|
54
|
+
lockedAt="September 25, 2026, 3:00 PM"
|
|
55
|
+
releaseAfter="October 2, 2026, 3:00 PM"
|
|
56
|
+
cancelUrl="https://vetpass.app/recovery/cancel?token=abc123"
|
|
57
|
+
/>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -43,6 +43,16 @@ import {
|
|
|
43
43
|
getCredentialUpdatedSubject,
|
|
44
44
|
AccountSignInChanged,
|
|
45
45
|
getAccountSignInChangedSubject,
|
|
46
|
+
EscrowHoldStarted,
|
|
47
|
+
getEscrowHoldStartedSubject,
|
|
48
|
+
EscrowHoldReminder,
|
|
49
|
+
getEscrowHoldReminderSubject,
|
|
50
|
+
EscrowHoldReleased,
|
|
51
|
+
getEscrowHoldReleasedSubject,
|
|
52
|
+
EscrowHoldCancelled,
|
|
53
|
+
getEscrowHoldCancelledSubject,
|
|
54
|
+
EscrowPinLocked,
|
|
55
|
+
getEscrowPinLockedSubject,
|
|
46
56
|
} from './templates';
|
|
47
57
|
|
|
48
58
|
import type {
|
|
@@ -59,6 +69,12 @@ import type {
|
|
|
59
69
|
EmailVerificationProps,
|
|
60
70
|
CredentialUpdatedProps,
|
|
61
71
|
AccountSignInChangedProps,
|
|
72
|
+
EscrowHoldStartedProps,
|
|
73
|
+
EscrowHoldReminderProps,
|
|
74
|
+
EscrowHoldReleasedProps,
|
|
75
|
+
EscrowHoldCancelledProps,
|
|
76
|
+
EscrowHoldCancelledReason,
|
|
77
|
+
EscrowPinLockedProps,
|
|
62
78
|
} from './templates';
|
|
63
79
|
|
|
64
80
|
// ---------------------------------------------------------------------------
|
|
@@ -157,6 +173,23 @@ export interface TemplateDataMap {
|
|
|
157
173
|
'credential-updated': CredentialUpdatedData;
|
|
158
174
|
/** lca-api: lost-login identity rebind security notification */
|
|
159
175
|
'account-sign-in-changed': AccountSignInChangedData;
|
|
176
|
+
|
|
177
|
+
// -- Recovery hold lifecycle (nitro-escrow-enclave) ----------------------
|
|
178
|
+
|
|
179
|
+
/** lca-api: recovery hold started — includes a cancel link */
|
|
180
|
+
'escrow-hold-started': EscrowHoldStartedData;
|
|
181
|
+
|
|
182
|
+
/** lca-api: recovery hold reminder (T-24h) — includes a cancel link */
|
|
183
|
+
'escrow-hold-reminder': EscrowHoldReminderData;
|
|
184
|
+
|
|
185
|
+
/** lca-api: recovery hold completed and access was restored */
|
|
186
|
+
'escrow-hold-released': EscrowHoldReleasedData;
|
|
187
|
+
|
|
188
|
+
/** lca-api: recovery hold cancelled (user, superseded, PIN lock, or failure) */
|
|
189
|
+
'escrow-hold-cancelled': EscrowHoldCancelledData;
|
|
190
|
+
|
|
191
|
+
/** lca-api: recovery PIN locked after too many failed attempts */
|
|
192
|
+
'escrow-pin-locked': EscrowPinLockedData;
|
|
160
193
|
}
|
|
161
194
|
|
|
162
195
|
export type TemplateId = keyof TemplateDataMap;
|
|
@@ -243,6 +276,34 @@ export interface CredentialUpdatedData {
|
|
|
243
276
|
|
|
244
277
|
export type AccountSignInChangedData = Record<string, never>;
|
|
245
278
|
|
|
279
|
+
export interface EscrowHoldStartedData {
|
|
280
|
+
requestedAt: string;
|
|
281
|
+
releaseAfter: string;
|
|
282
|
+
cancelUrl: string;
|
|
283
|
+
deviceHint?: string;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface EscrowHoldReminderData {
|
|
287
|
+
releaseAfter: string;
|
|
288
|
+
cancelUrl: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface EscrowHoldReleasedData {
|
|
292
|
+
completedAt: string;
|
|
293
|
+
supportUrl?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface EscrowHoldCancelledData {
|
|
297
|
+
cancelledAt: string;
|
|
298
|
+
reason?: EscrowHoldCancelledReason;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface EscrowPinLockedData {
|
|
302
|
+
lockedAt: string;
|
|
303
|
+
releaseAfter?: string;
|
|
304
|
+
cancelUrl?: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
246
307
|
// ---------------------------------------------------------------------------
|
|
247
308
|
// renderEmail()
|
|
248
309
|
// ---------------------------------------------------------------------------
|
|
@@ -459,6 +520,56 @@ function buildElement(
|
|
|
459
520
|
};
|
|
460
521
|
}
|
|
461
522
|
|
|
523
|
+
case 'escrow-hold-started': {
|
|
524
|
+
const d = data as EscrowHoldStartedData;
|
|
525
|
+
const props: EscrowHoldStartedProps = { branding, ...d, locale };
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
element: React.createElement(EscrowHoldStarted, props),
|
|
529
|
+
subject: getEscrowHoldStartedSubject(branding, locale),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
case 'escrow-hold-reminder': {
|
|
534
|
+
const d = data as EscrowHoldReminderData;
|
|
535
|
+
const props: EscrowHoldReminderProps = { branding, ...d, locale };
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
element: React.createElement(EscrowHoldReminder, props),
|
|
539
|
+
subject: getEscrowHoldReminderSubject(branding, locale),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
case 'escrow-hold-released': {
|
|
544
|
+
const d = data as EscrowHoldReleasedData;
|
|
545
|
+
const props: EscrowHoldReleasedProps = { branding, ...d, locale };
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
element: React.createElement(EscrowHoldReleased, props),
|
|
549
|
+
subject: getEscrowHoldReleasedSubject(branding, locale),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
case 'escrow-hold-cancelled': {
|
|
554
|
+
const d = data as EscrowHoldCancelledData;
|
|
555
|
+
const props: EscrowHoldCancelledProps = { branding, ...d, locale };
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
element: React.createElement(EscrowHoldCancelled, props),
|
|
559
|
+
subject: getEscrowHoldCancelledSubject(branding, locale),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
case 'escrow-pin-locked': {
|
|
564
|
+
const d = data as EscrowPinLockedData;
|
|
565
|
+
const props: EscrowPinLockedProps = { branding, ...d, locale };
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
element: React.createElement(EscrowPinLocked, props),
|
|
569
|
+
subject: getEscrowPinLockedSubject(branding, locale),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
462
573
|
default: {
|
|
463
574
|
const _exhaustive: never = templateId;
|
|
464
575
|
throw new Error(`Unknown template: ${_exhaustive}`);
|