@learncard/email-templates 1.2.1 → 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.
- package/dist/i18n.d.ts +15 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +31 -0
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/previews/account-secured-scenarios.d.ts +6 -0
- package/dist/previews/account-secured-scenarios.d.ts.map +1 -0
- package/dist/previews/account-secured-scenarios.js +7 -0
- package/dist/previews/account-secured-scenarios.js.map +1 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.d.ts +6 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.d.ts.map +1 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.js +7 -0
- package/dist/previews/escrow-pin-sign-in-scenarios.js.map +1 -0
- package/dist/render.d.ts +11 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +27 -1
- package/dist/render.js.map +1 -1
- package/dist/templates/account-secured.d.ts +27 -0
- package/dist/templates/account-secured.d.ts.map +1 -0
- package/dist/templates/account-secured.js +118 -0
- package/dist/templates/account-secured.js.map +1 -0
- package/dist/templates/escrow-pin-sign-in.d.ts +24 -0
- package/dist/templates/escrow-pin-sign-in.d.ts.map +1 -0
- package/dist/templates/escrow-pin-sign-in.js +96 -0
- package/dist/templates/escrow-pin-sign-in.js.map +1 -0
- package/dist/templates/index.d.ts +21 -38
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +20 -14
- package/dist/templates/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/i18n.test.ts +32 -1
- package/src/__tests__/render.test.ts +115 -0
- package/src/i18n.ts +33 -0
- package/src/index.ts +2 -0
- package/src/previews/account-secured-scenarios.tsx +41 -0
- package/src/previews/escrow-pin-sign-in-scenarios.tsx +52 -0
- package/src/render.ts +51 -0
- package/src/templates/account-secured.tsx +236 -0
- package/src/templates/escrow-pin-sign-in.tsx +197 -0
- package/src/templates/index.ts +95 -35
|
@@ -1,39 +1,22 @@
|
|
|
1
|
-
export { VerificationCode, getVerificationCodeSubject } from './verification-code';
|
|
2
|
-
export
|
|
3
|
-
export {
|
|
4
|
-
export type
|
|
5
|
-
export {
|
|
6
|
-
export type
|
|
7
|
-
export {
|
|
8
|
-
export type
|
|
9
|
-
export {
|
|
10
|
-
export type
|
|
11
|
-
export {
|
|
12
|
-
export type
|
|
13
|
-
export {
|
|
14
|
-
export type
|
|
15
|
-
export {
|
|
16
|
-
export type
|
|
17
|
-
export {
|
|
18
|
-
export type
|
|
19
|
-
export {
|
|
20
|
-
export type
|
|
21
|
-
export {
|
|
22
|
-
export type { GuardianRejectedCredentialProps } from './guardian-rejected-credential';
|
|
23
|
-
export { EmailVerification, getEmailVerificationSubject } from './email-verification';
|
|
24
|
-
export type { EmailVerificationProps } from './email-verification';
|
|
25
|
-
export { CredentialUpdated, getCredentialUpdatedSubject } from './credential-updated';
|
|
26
|
-
export type { CredentialUpdatedProps } from './credential-updated';
|
|
27
|
-
export { AccountSignInChanged, getAccountSignInChangedSubject } from './account-sign-in-changed';
|
|
28
|
-
export type { AccountSignInChangedProps } from './account-sign-in-changed';
|
|
29
|
-
export { EscrowHoldStarted, getEscrowHoldStartedSubject } from './escrow-hold-started';
|
|
30
|
-
export type { EscrowHoldStartedProps } from './escrow-hold-started';
|
|
31
|
-
export { EscrowHoldReminder, getEscrowHoldReminderSubject } from './escrow-hold-reminder';
|
|
32
|
-
export type { EscrowHoldReminderProps } from './escrow-hold-reminder';
|
|
33
|
-
export { EscrowHoldReleased, getEscrowHoldReleasedSubject } from './escrow-hold-released';
|
|
34
|
-
export type { EscrowHoldReleasedProps } from './escrow-hold-released';
|
|
35
|
-
export { EscrowHoldCancelled, getEscrowHoldCancelledSubject } from './escrow-hold-cancelled';
|
|
36
|
-
export type { EscrowHoldCancelledProps, EscrowHoldCancelledReason } from './escrow-hold-cancelled';
|
|
37
|
-
export { EscrowPinLocked, getEscrowPinLockedSubject } from './escrow-pin-locked';
|
|
38
|
-
export type { EscrowPinLockedProps } from './escrow-pin-locked';
|
|
1
|
+
export { VerificationCode, getVerificationCodeSubject, type VerificationCodeProps, type VerificationCodeVariant, } from './verification-code';
|
|
2
|
+
export { InboxClaim, getInboxClaimSubject, type InboxClaimProps } from './inbox-claim';
|
|
3
|
+
export { GuardianApproval, getGuardianApprovalSubject, type GuardianApprovalProps, } from './guardian-approval';
|
|
4
|
+
export { AccountApproved, getAccountApprovedSubject, type AccountApprovedProps, } from './account-approved';
|
|
5
|
+
export { RecoveryKey, getRecoveryKeySubject, type RecoveryKeyProps } from './recovery-key';
|
|
6
|
+
export { EndorsementRequest, getEndorsementRequestSubject, type EndorsementRequestProps, } from './endorsement-request';
|
|
7
|
+
export { CredentialAwaitingGuardian, getCredentialAwaitingGuardianSubject, type CredentialAwaitingGuardianProps, } from './credential-awaiting-guardian';
|
|
8
|
+
export { GuardianApprovedClaim, getGuardianApprovedClaimSubject, type GuardianApprovedClaimProps, } from './guardian-approved-claim';
|
|
9
|
+
export { GuardianCredentialApproval, getGuardianCredentialApprovalSubject, type GuardianCredentialApprovalProps, } from './guardian-credential-approval';
|
|
10
|
+
export { GuardianEmailOtp, getGuardianEmailOtpSubject, type GuardianEmailOtpProps, } from './guardian-email-otp';
|
|
11
|
+
export { GuardianRejectedCredential, getGuardianRejectedCredentialSubject, type GuardianRejectedCredentialProps, } from './guardian-rejected-credential';
|
|
12
|
+
export { EmailVerification, getEmailVerificationSubject, type EmailVerificationProps, } from './email-verification';
|
|
13
|
+
export { CredentialUpdated, getCredentialUpdatedSubject, type CredentialUpdatedProps, } from './credential-updated';
|
|
14
|
+
export { AccountSignInChanged, getAccountSignInChangedSubject, type AccountSignInChangedProps, } from './account-sign-in-changed';
|
|
15
|
+
export { EscrowHoldStarted, getEscrowHoldStartedSubject, type EscrowHoldStartedProps, } from './escrow-hold-started';
|
|
16
|
+
export { EscrowHoldReminder, getEscrowHoldReminderSubject, type EscrowHoldReminderProps, } from './escrow-hold-reminder';
|
|
17
|
+
export { EscrowHoldReleased, getEscrowHoldReleasedSubject, type EscrowHoldReleasedProps, } from './escrow-hold-released';
|
|
18
|
+
export { EscrowHoldCancelled, getEscrowHoldCancelledSubject, type EscrowHoldCancelledProps, type EscrowHoldCancelledReason, } from './escrow-hold-cancelled';
|
|
19
|
+
export { EscrowPinLocked, getEscrowPinLockedSubject, type EscrowPinLockedProps, } from './escrow-pin-locked';
|
|
20
|
+
export { EscrowPinSignIn, getEscrowPinSignInSubject, type EscrowPinSignInProps, } from './escrow-pin-sign-in';
|
|
21
|
+
export { AccountSecured, getAccountSecuredSubject, type AccountSecuredProps, } from './account-secured';
|
|
39
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACH,gBAAgB,EAChB,0BAA0B,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,GAC/B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEvF,OAAO,EACH,gBAAgB,EAChB,0BAA0B,EAC1B,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACH,eAAe,EACf,yBAAyB,EACzB,KAAK,oBAAoB,GAC5B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAE3F,OAAO,EACH,kBAAkB,EAClB,4BAA4B,EAC5B,KAAK,uBAAuB,GAC/B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACH,0BAA0B,EAC1B,oCAAoC,EACpC,KAAK,+BAA+B,GACvC,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACH,qBAAqB,EACrB,+BAA+B,EAC/B,KAAK,0BAA0B,GAClC,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACH,0BAA0B,EAC1B,oCAAoC,EACpC,KAAK,+BAA+B,GACvC,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACH,gBAAgB,EAChB,0BAA0B,EAC1B,KAAK,qBAAqB,GAC7B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,0BAA0B,EAC1B,oCAAoC,EACpC,KAAK,+BAA+B,GACvC,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACH,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,oBAAoB,EACpB,8BAA8B,EAC9B,KAAK,yBAAyB,GACjC,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACH,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACH,kBAAkB,EAClB,4BAA4B,EAC5B,KAAK,uBAAuB,GAC/B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACH,kBAAkB,EAClB,4BAA4B,EAC5B,KAAK,uBAAuB,GAC/B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACH,mBAAmB,EACnB,6BAA6B,EAC7B,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GACjC,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACH,eAAe,EACf,yBAAyB,EACzB,KAAK,oBAAoB,GAC5B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACH,eAAe,EACf,yBAAyB,EACzB,KAAK,oBAAoB,GAC5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,cAAc,EACd,wBAAwB,EACxB,KAAK,mBAAmB,GAC3B,MAAM,mBAAmB,CAAC"}
|
package/dist/templates/index.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
export
|
|
1
|
+
// One export declaration per template module. Projects that resolve this package
|
|
2
|
+
// through its `development` condition (source) typecheck without `jsx`, and TypeScript
|
|
3
|
+
// reports TS6142 once per declaration that imports a .tsx module from this .ts file;
|
|
4
|
+
// a separate `export type` line for the same module would add a second report.
|
|
5
|
+
export { VerificationCode, getVerificationCodeSubject, } from './verification-code';
|
|
2
6
|
export { InboxClaim, getInboxClaimSubject } from './inbox-claim';
|
|
3
|
-
export { GuardianApproval, getGuardianApprovalSubject } from './guardian-approval';
|
|
4
|
-
export { AccountApproved, getAccountApprovedSubject } from './account-approved';
|
|
7
|
+
export { GuardianApproval, getGuardianApprovalSubject, } from './guardian-approval';
|
|
8
|
+
export { AccountApproved, getAccountApprovedSubject, } from './account-approved';
|
|
5
9
|
export { RecoveryKey, getRecoveryKeySubject } from './recovery-key';
|
|
6
|
-
export { EndorsementRequest, getEndorsementRequestSubject } from './endorsement-request';
|
|
10
|
+
export { EndorsementRequest, getEndorsementRequestSubject, } from './endorsement-request';
|
|
7
11
|
export { CredentialAwaitingGuardian, getCredentialAwaitingGuardianSubject, } from './credential-awaiting-guardian';
|
|
8
|
-
export { GuardianApprovedClaim, getGuardianApprovedClaimSubject } from './guardian-approved-claim';
|
|
12
|
+
export { GuardianApprovedClaim, getGuardianApprovedClaimSubject, } from './guardian-approved-claim';
|
|
9
13
|
export { GuardianCredentialApproval, getGuardianCredentialApprovalSubject, } from './guardian-credential-approval';
|
|
10
|
-
export { GuardianEmailOtp, getGuardianEmailOtpSubject } from './guardian-email-otp';
|
|
14
|
+
export { GuardianEmailOtp, getGuardianEmailOtpSubject, } from './guardian-email-otp';
|
|
11
15
|
export { GuardianRejectedCredential, getGuardianRejectedCredentialSubject, } from './guardian-rejected-credential';
|
|
12
|
-
export { EmailVerification, getEmailVerificationSubject } from './email-verification';
|
|
13
|
-
export { CredentialUpdated, getCredentialUpdatedSubject } from './credential-updated';
|
|
14
|
-
export { AccountSignInChanged, getAccountSignInChangedSubject } from './account-sign-in-changed';
|
|
15
|
-
export { EscrowHoldStarted, getEscrowHoldStartedSubject } from './escrow-hold-started';
|
|
16
|
-
export { EscrowHoldReminder, getEscrowHoldReminderSubject } from './escrow-hold-reminder';
|
|
17
|
-
export { EscrowHoldReleased, getEscrowHoldReleasedSubject } from './escrow-hold-released';
|
|
18
|
-
export { EscrowHoldCancelled, getEscrowHoldCancelledSubject } from './escrow-hold-cancelled';
|
|
19
|
-
export { EscrowPinLocked, getEscrowPinLockedSubject } from './escrow-pin-locked';
|
|
16
|
+
export { EmailVerification, getEmailVerificationSubject, } from './email-verification';
|
|
17
|
+
export { CredentialUpdated, getCredentialUpdatedSubject, } from './credential-updated';
|
|
18
|
+
export { AccountSignInChanged, getAccountSignInChangedSubject, } from './account-sign-in-changed';
|
|
19
|
+
export { EscrowHoldStarted, getEscrowHoldStartedSubject, } from './escrow-hold-started';
|
|
20
|
+
export { EscrowHoldReminder, getEscrowHoldReminderSubject, } from './escrow-hold-reminder';
|
|
21
|
+
export { EscrowHoldReleased, getEscrowHoldReleasedSubject, } from './escrow-hold-released';
|
|
22
|
+
export { EscrowHoldCancelled, getEscrowHoldCancelledSubject, } from './escrow-hold-cancelled';
|
|
23
|
+
export { EscrowPinLocked, getEscrowPinLockedSubject, } from './escrow-pin-locked';
|
|
24
|
+
export { EscrowPinSignIn, getEscrowPinSignInSubject, } from './escrow-pin-sign-in';
|
|
25
|
+
export { AccountSecured, getAccountSecuredSubject, } from './account-secured';
|
|
20
26
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,uFAAuF;AACvF,qFAAqF;AACrF,+EAA+E;AAE/E,OAAO,EACH,gBAAgB,EAChB,0BAA0B,GAG7B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAwB,MAAM,eAAe,CAAC;AAEvF,OAAO,EACH,gBAAgB,EAChB,0BAA0B,GAE7B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACH,eAAe,EACf,yBAAyB,GAE5B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAyB,MAAM,gBAAgB,CAAC;AAE3F,OAAO,EACH,kBAAkB,EAClB,4BAA4B,GAE/B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACH,0BAA0B,EAC1B,oCAAoC,GAEvC,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACH,qBAAqB,EACrB,+BAA+B,GAElC,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACH,0BAA0B,EAC1B,oCAAoC,GAEvC,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACH,gBAAgB,EAChB,0BAA0B,GAE7B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,0BAA0B,EAC1B,oCAAoC,GAEvC,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACH,iBAAiB,EACjB,2BAA2B,GAE9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,iBAAiB,EACjB,2BAA2B,GAE9B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,oBAAoB,EACpB,8BAA8B,GAEjC,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACH,iBAAiB,EACjB,2BAA2B,GAE9B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACH,kBAAkB,EAClB,4BAA4B,GAE/B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACH,kBAAkB,EAClB,4BAA4B,GAE/B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACH,mBAAmB,EACnB,6BAA6B,GAGhC,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACH,eAAe,EACf,yBAAyB,GAE5B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACH,eAAe,EACf,yBAAyB,GAE5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,cAAc,EACd,wBAAwB,GAE3B,MAAM,mBAAmB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/email-templates",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@react-email/components": "^0.0.36",
|
|
22
|
-
"@react-email/render": "^
|
|
22
|
+
"@react-email/render": "^2.2.0",
|
|
23
23
|
"react": "^18.3.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
@@ -13,7 +13,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
13
13
|
import { renderEmail } from '../render';
|
|
14
14
|
import { renderSms } from '../sms';
|
|
15
15
|
import { DEFAULT_BRANDING } from '../branding';
|
|
16
|
-
import { resolveCatalogLocale, htmlDir } from '../i18n';
|
|
16
|
+
import { resolveCatalogLocale, htmlDir, localeProp } from '../i18n';
|
|
17
17
|
import type { NotificationLocale } from '../i18n';
|
|
18
18
|
import type { TemplateId, TemplateDataMap } from '../render';
|
|
19
19
|
|
|
@@ -120,6 +120,11 @@ const FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
|
|
|
120
120
|
releaseAfter: 'October 2, 2026, 3:00 PM',
|
|
121
121
|
cancelUrl: 'https://learncard.app/recovery/cancel?token=abc123',
|
|
122
122
|
},
|
|
123
|
+
'escrow-pin-sign-in': {
|
|
124
|
+
signedInAt: 'October 2, 2026, 3:00 PM',
|
|
125
|
+
secureUrl: 'https://learncard.app/account/secure',
|
|
126
|
+
},
|
|
127
|
+
'account-secured': { securedAt: 'October 2, 2026, 3:00 PM' },
|
|
123
128
|
};
|
|
124
129
|
|
|
125
130
|
const ALL_TEMPLATE_IDS = Object.keys(FIXTURES) as TemplateId[];
|
|
@@ -162,6 +167,7 @@ describe('i18n — non-EN locales localize content', () => {
|
|
|
162
167
|
'credential-awaiting-guardian',
|
|
163
168
|
'escrow-hold-started',
|
|
164
169
|
'escrow-hold-cancelled',
|
|
170
|
+
'account-secured',
|
|
165
171
|
];
|
|
166
172
|
|
|
167
173
|
for (const templateId of SAMPLES) {
|
|
@@ -363,4 +369,29 @@ describe('i18n — renderSms localizes', () => {
|
|
|
363
369
|
expect(es).toContain('Tu código de verificación de LearnCard es:');
|
|
364
370
|
expect(en).toContain('999222');
|
|
365
371
|
});
|
|
372
|
+
|
|
373
|
+
it('localeProp omits an absent locale instead of passing undefined', () => {
|
|
374
|
+
expect(localeProp(undefined)).toEqual({});
|
|
375
|
+
expect(localeProp(null)).toEqual({});
|
|
376
|
+
expect('locale' in localeProp(undefined)).toBe(false);
|
|
377
|
+
expect(localeProp('fr')).toEqual({ locale: 'fr' });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('PIN sign-in and account-secured notices are unchanged for EN vs undefined', async () => {
|
|
381
|
+
for (const [id, data] of [
|
|
382
|
+
[
|
|
383
|
+
'escrow-pin-sign-in',
|
|
384
|
+
{
|
|
385
|
+
signedInAt: 'October 2, 2026',
|
|
386
|
+
secureUrl: 'https://learncard.app/account/secure',
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
['account-secured', { securedAt: 'October 2, 2026' }],
|
|
390
|
+
] as const) {
|
|
391
|
+
const en = await renderEmail(id, DEFAULT_BRANDING, data, 'en');
|
|
392
|
+
const none = await renderEmail(id, DEFAULT_BRANDING, data);
|
|
393
|
+
expect(none.html).toBe(en.html);
|
|
394
|
+
expect(none.subject).toBe(en.subject);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
366
397
|
});
|
|
@@ -166,6 +166,13 @@ const TEMPLATE_FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
|
|
|
166
166
|
releaseAfter: 'October 2, 2026, 3:00 PM',
|
|
167
167
|
cancelUrl: 'https://learncard.app/recovery/cancel?token=abc123',
|
|
168
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
|
+
},
|
|
169
176
|
};
|
|
170
177
|
|
|
171
178
|
const ALL_TEMPLATE_IDS = Object.keys(TEMPLATE_FIXTURES) as TemplateId[];
|
|
@@ -552,6 +559,114 @@ describe('renderEmail — escrow hold lifecycle', () => {
|
|
|
552
559
|
expect(html).not.toContain('undefined');
|
|
553
560
|
});
|
|
554
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
|
+
|
|
555
670
|
it('all escrow hold templates include the "This wasn\'t me" cancel CTA when a cancel URL is present', async () => {
|
|
556
671
|
const started = await renderEmail(
|
|
557
672
|
'escrow-hold-started',
|
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
|
@@ -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
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
getEscrowHoldCancelledSubject,
|
|
54
54
|
EscrowPinLocked,
|
|
55
55
|
getEscrowPinLockedSubject,
|
|
56
|
+
EscrowPinSignIn,
|
|
57
|
+
getEscrowPinSignInSubject,
|
|
58
|
+
AccountSecured,
|
|
59
|
+
getAccountSecuredSubject,
|
|
56
60
|
} from './templates';
|
|
57
61
|
|
|
58
62
|
import type {
|
|
@@ -75,7 +79,10 @@ import type {
|
|
|
75
79
|
EscrowHoldCancelledProps,
|
|
76
80
|
EscrowHoldCancelledReason,
|
|
77
81
|
EscrowPinLockedProps,
|
|
82
|
+
EscrowPinSignInProps,
|
|
83
|
+
AccountSecuredProps,
|
|
78
84
|
} from './templates';
|
|
85
|
+
import { localeProp } from './i18n';
|
|
79
86
|
|
|
80
87
|
// ---------------------------------------------------------------------------
|
|
81
88
|
// Public types
|
|
@@ -190,6 +197,12 @@ export interface TemplateDataMap {
|
|
|
190
197
|
|
|
191
198
|
/** lca-api: recovery PIN locked after too many failed attempts */
|
|
192
199
|
'escrow-pin-locked': EscrowPinLockedData;
|
|
200
|
+
|
|
201
|
+
/** lca-api: a new device signed in with the recovery PIN — links to "Secure my account" */
|
|
202
|
+
'escrow-pin-sign-in': EscrowPinSignInData;
|
|
203
|
+
|
|
204
|
+
/** lca-api: "Secure my account" signed out every other device and login */
|
|
205
|
+
'account-secured': AccountSecuredData;
|
|
193
206
|
}
|
|
194
207
|
|
|
195
208
|
export type TemplateId = keyof TemplateDataMap;
|
|
@@ -304,6 +317,15 @@ export interface EscrowPinLockedData {
|
|
|
304
317
|
cancelUrl?: string;
|
|
305
318
|
}
|
|
306
319
|
|
|
320
|
+
export interface EscrowPinSignInData {
|
|
321
|
+
signedInAt: string;
|
|
322
|
+
secureUrl: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface AccountSecuredData {
|
|
326
|
+
securedAt: string;
|
|
327
|
+
}
|
|
328
|
+
|
|
307
329
|
// ---------------------------------------------------------------------------
|
|
308
330
|
// renderEmail()
|
|
309
331
|
// ---------------------------------------------------------------------------
|
|
@@ -570,6 +592,35 @@ function buildElement(
|
|
|
570
592
|
};
|
|
571
593
|
}
|
|
572
594
|
|
|
595
|
+
case 'escrow-pin-sign-in': {
|
|
596
|
+
const d = data as EscrowPinSignInData;
|
|
597
|
+
const props: EscrowPinSignInProps = {
|
|
598
|
+
branding,
|
|
599
|
+
signedInAt: d.signedInAt,
|
|
600
|
+
secureUrl: d.secureUrl,
|
|
601
|
+
...localeProp(locale),
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
element: React.createElement(EscrowPinSignIn, props),
|
|
606
|
+
subject: getEscrowPinSignInSubject(branding, locale),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
case 'account-secured': {
|
|
611
|
+
const d = data as AccountSecuredData;
|
|
612
|
+
const props: AccountSecuredProps = {
|
|
613
|
+
branding,
|
|
614
|
+
securedAt: d.securedAt,
|
|
615
|
+
...localeProp(locale),
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
element: React.createElement(AccountSecured, props),
|
|
620
|
+
subject: getAccountSecuredSubject(branding, locale),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
573
624
|
default: {
|
|
574
625
|
const _exhaustive: never = templateId;
|
|
575
626
|
throw new Error(`Unknown template: ${_exhaustive}`);
|