@learncard/email-templates 1.0.1 → 1.0.3
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/README.md +43 -43
- package/dist/previews/account-approved-scenarios.d.ts +2 -1
- package/dist/previews/account-approved-scenarios.d.ts.map +1 -1
- package/dist/previews/endorsement-request-scenarios.d.ts +2 -1
- package/dist/previews/endorsement-request-scenarios.d.ts.map +1 -1
- package/dist/previews/guardian-approval-scenarios.d.ts +2 -1
- package/dist/previews/guardian-approval-scenarios.d.ts.map +1 -1
- package/dist/previews/guardian-credential-scenarios.d.ts +2 -1
- package/dist/previews/guardian-credential-scenarios.d.ts.map +1 -1
- package/dist/previews/inbox-claim-scenarios.d.ts +2 -1
- package/dist/previews/inbox-claim-scenarios.d.ts.map +1 -1
- package/dist/previews/recovery-key-scenarios.d.ts +2 -1
- package/dist/previews/recovery-key-scenarios.d.ts.map +1 -1
- package/dist/previews/verification-code-scenarios.d.ts +2 -1
- package/dist/previews/verification-code-scenarios.d.ts.map +1 -1
- package/dist/templates/account-approved.d.ts +1 -1
- package/dist/templates/account-approved.d.ts.map +1 -1
- package/dist/templates/credential-awaiting-guardian.d.ts +1 -1
- package/dist/templates/credential-awaiting-guardian.d.ts.map +1 -1
- package/dist/templates/email-verification.d.ts +1 -1
- package/dist/templates/email-verification.d.ts.map +1 -1
- package/dist/templates/endorsement-request.d.ts +1 -1
- package/dist/templates/endorsement-request.d.ts.map +1 -1
- package/dist/templates/guardian-approval.d.ts +1 -1
- package/dist/templates/guardian-approval.d.ts.map +1 -1
- package/dist/templates/guardian-approved-claim.d.ts +1 -1
- package/dist/templates/guardian-approved-claim.d.ts.map +1 -1
- package/dist/templates/guardian-credential-approval.d.ts +1 -1
- package/dist/templates/guardian-credential-approval.d.ts.map +1 -1
- package/dist/templates/guardian-email-otp.d.ts +1 -1
- package/dist/templates/guardian-email-otp.d.ts.map +1 -1
- package/dist/templates/guardian-rejected-credential.d.ts +1 -1
- package/dist/templates/guardian-rejected-credential.d.ts.map +1 -1
- package/dist/templates/inbox-claim.d.ts +1 -1
- package/dist/templates/inbox-claim.d.ts.map +1 -1
- package/dist/templates/recovery-key.d.ts +1 -1
- package/dist/templates/recovery-key.d.ts.map +1 -1
- package/dist/templates/verification-code.d.ts +1 -1
- package/dist/templates/verification-code.d.ts.map +1 -1
- package/dist/templates/verification-code.js +9 -9
- package/dist/templates/verification-code.js.map +1 -1
- package/package.json +49 -39
- package/src/__tests__/render.test.ts +432 -0
- package/src/__tests__/sms.test.ts +95 -0
- package/src/__tests__/tenant-registry.test.ts +257 -0
- package/src/branding.ts +61 -0
- package/src/components/CodeBlock.tsx +61 -0
- package/src/components/EmailButton.tsx +36 -0
- package/src/components/IssuerLogo.tsx +41 -0
- package/src/components/Layout.tsx +192 -0
- package/src/components/LinkFallback.tsx +50 -0
- package/src/index.ts +50 -0
- package/src/previews/_fixtures.tsx +51 -0
- package/src/previews/account-approved-scenarios.tsx +31 -0
- package/src/previews/endorsement-request-scenarios.tsx +50 -0
- package/src/previews/guardian-approval-scenarios.tsx +39 -0
- package/src/previews/guardian-credential-scenarios.tsx +88 -0
- package/src/previews/inbox-claim-scenarios.tsx +66 -0
- package/src/previews/recovery-key-scenarios.tsx +32 -0
- package/src/previews/verification-code-scenarios.tsx +60 -0
- package/src/render.ts +409 -0
- package/src/sms.ts +68 -0
- package/src/templates/account-approved.tsx +105 -0
- package/src/templates/credential-awaiting-guardian.tsx +137 -0
- package/src/templates/email-verification.tsx +110 -0
- package/src/templates/endorsement-request.tsx +157 -0
- package/src/templates/guardian-approval.tsx +122 -0
- package/src/templates/guardian-approved-claim.tsx +114 -0
- package/src/templates/guardian-credential-approval.tsx +127 -0
- package/src/templates/guardian-email-otp.tsx +101 -0
- package/src/templates/guardian-rejected-credential.tsx +103 -0
- package/src/templates/inbox-claim.tsx +141 -0
- package/src/templates/index.ts +35 -0
- package/src/templates/recovery-key.tsx +102 -0
- package/src/templates/verification-code.tsx +164 -0
- package/src/tenant-registry.ts +204 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardianCredentialApproval — email sent to a guardian asking them to
|
|
3
|
+
* approve or decline a specific credential issued to their ward.
|
|
4
|
+
*
|
|
5
|
+
* Used by: brain-service (templateAlias: 'guardian-credential-approval')
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Text, Section } from '@react-email/components';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
|
|
11
|
+
import type { TenantBranding } from '../branding';
|
|
12
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
13
|
+
import { Layout } from '../components/Layout';
|
|
14
|
+
import { EmailButton } from '../components/EmailButton';
|
|
15
|
+
import { IssuerLogo } from '../components/IssuerLogo';
|
|
16
|
+
import { LinkFallback } from '../components/LinkFallback';
|
|
17
|
+
|
|
18
|
+
export interface GuardianCredentialApprovalProps {
|
|
19
|
+
branding: TenantBranding;
|
|
20
|
+
approvalUrl: string;
|
|
21
|
+
approvalToken?: string;
|
|
22
|
+
issuer?: {
|
|
23
|
+
name?: string;
|
|
24
|
+
logoUrl?: string;
|
|
25
|
+
};
|
|
26
|
+
credential?: {
|
|
27
|
+
name?: string;
|
|
28
|
+
};
|
|
29
|
+
recipient?: {
|
|
30
|
+
email?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const GuardianCredentialApproval: React.FC<GuardianCredentialApprovalProps> = ({
|
|
35
|
+
branding,
|
|
36
|
+
approvalUrl,
|
|
37
|
+
issuer,
|
|
38
|
+
credential,
|
|
39
|
+
recipient,
|
|
40
|
+
}) => {
|
|
41
|
+
const issuerName = issuer?.name ?? 'An issuer';
|
|
42
|
+
const credentialName = credential?.name ?? 'a credential';
|
|
43
|
+
const recipientEmail = recipient?.email ?? 'a student';
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Layout branding={branding} preview={`A credential needs your approval for ${recipientEmail}`} showHeaderLogo={false}>
|
|
47
|
+
<IssuerLogo logoUrl={issuer?.logoUrl} alt={issuerName ? `${issuerName} logo` : undefined} />
|
|
48
|
+
|
|
49
|
+
<Text style={heading}>A credential needs your approval</Text>
|
|
50
|
+
|
|
51
|
+
<Text style={paragraph}>Hello,</Text>
|
|
52
|
+
|
|
53
|
+
<Text style={paragraph}>
|
|
54
|
+
<strong>{issuerName}</strong> has issued the credential{' '}
|
|
55
|
+
<strong>“{credentialName}”</strong> to{' '}
|
|
56
|
+
<strong>{recipientEmail}</strong>.
|
|
57
|
+
</Text>
|
|
58
|
+
|
|
59
|
+
<Text style={paragraph}>
|
|
60
|
+
As their guardian, your approval is required before they can claim it.
|
|
61
|
+
Please review the credential and approve or decline below.
|
|
62
|
+
</Text>
|
|
63
|
+
|
|
64
|
+
<Section style={buttonWrapper}>
|
|
65
|
+
<EmailButton href={approvalUrl} branding={branding}>
|
|
66
|
+
Review & Approve →
|
|
67
|
+
</EmailButton>
|
|
68
|
+
</Section>
|
|
69
|
+
|
|
70
|
+
<Text style={signOff}>
|
|
71
|
+
Thank you,<br />The {branding.brandName} Team
|
|
72
|
+
</Text>
|
|
73
|
+
|
|
74
|
+
<LinkFallback href={approvalUrl} />
|
|
75
|
+
</Layout>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const getGuardianCredentialApprovalSubject = (
|
|
80
|
+
_branding: TenantBranding,
|
|
81
|
+
): string => 'A credential needs your approval';
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Styles
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
const heading: React.CSSProperties = {
|
|
88
|
+
fontSize: 24,
|
|
89
|
+
fontWeight: 600,
|
|
90
|
+
color: '#111827',
|
|
91
|
+
margin: '0 0 24px',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const paragraph: React.CSSProperties = {
|
|
95
|
+
fontSize: 16,
|
|
96
|
+
color: '#374151',
|
|
97
|
+
lineHeight: '24px',
|
|
98
|
+
margin: '0 0 24px',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const buttonWrapper: React.CSSProperties = {
|
|
102
|
+
margin: '0 0 24px',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const signOff: React.CSSProperties = {
|
|
106
|
+
fontSize: 14,
|
|
107
|
+
color: '#374151',
|
|
108
|
+
lineHeight: '20px',
|
|
109
|
+
margin: '24px 0 0',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Preview
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export default function Preview() {
|
|
117
|
+
return (
|
|
118
|
+
<GuardianCredentialApproval
|
|
119
|
+
branding={DEFAULT_BRANDING}
|
|
120
|
+
approvalUrl="https://learncard.app/guardian/approve?token=abc123"
|
|
121
|
+
approvalToken="abc123"
|
|
122
|
+
issuer={{ name: 'Springfield School District' }}
|
|
123
|
+
credential={{ name: 'Perfect Attendance Award' }}
|
|
124
|
+
recipient={{ email: 'student@example.com' }}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardianEmailOtp — verification code email sent to a guardian when they
|
|
3
|
+
* open an approval link and need to confirm their identity via OTP.
|
|
4
|
+
*
|
|
5
|
+
* Used by: brain-service (templateAlias: 'guardian-email-otp')
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Text } from '@react-email/components';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
|
|
11
|
+
import type { TenantBranding } from '../branding';
|
|
12
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
13
|
+
import { Layout } from '../components/Layout';
|
|
14
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
15
|
+
|
|
16
|
+
export interface GuardianEmailOtpProps {
|
|
17
|
+
branding: TenantBranding;
|
|
18
|
+
verificationCode: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const GuardianEmailOtp: React.FC<GuardianEmailOtpProps> = ({
|
|
22
|
+
branding,
|
|
23
|
+
verificationCode,
|
|
24
|
+
}) => {
|
|
25
|
+
return (
|
|
26
|
+
<Layout branding={branding} preview={`Your verification code: ${verificationCode}`}>
|
|
27
|
+
<Text style={heading}>Your verification code</Text>
|
|
28
|
+
|
|
29
|
+
<Text style={paragraph}>Hi there,</Text>
|
|
30
|
+
|
|
31
|
+
<Text style={paragraph}>
|
|
32
|
+
Here is your verification code to confirm your identity as guardian and
|
|
33
|
+
complete your review:
|
|
34
|
+
</Text>
|
|
35
|
+
|
|
36
|
+
<CodeBlock code={verificationCode} label="Verification code" />
|
|
37
|
+
|
|
38
|
+
<Text style={muted}>
|
|
39
|
+
This code expires in <strong style={{ color: '#374151' }}>1 hour</strong>.
|
|
40
|
+
Do not share it with anyone.
|
|
41
|
+
</Text>
|
|
42
|
+
|
|
43
|
+
<Text style={muted}>
|
|
44
|
+
If you did not request this code, you can safely ignore this email.
|
|
45
|
+
</Text>
|
|
46
|
+
|
|
47
|
+
<Text style={signOff}>
|
|
48
|
+
Sincerely,<br />The {branding.brandName} Team
|
|
49
|
+
</Text>
|
|
50
|
+
</Layout>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getGuardianEmailOtpSubject = (
|
|
55
|
+
_branding: TenantBranding,
|
|
56
|
+
): string => 'Your Verification Code';
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Styles
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const heading: React.CSSProperties = {
|
|
63
|
+
fontSize: 24,
|
|
64
|
+
fontWeight: 600,
|
|
65
|
+
color: '#111827',
|
|
66
|
+
margin: '0 0 24px',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const paragraph: React.CSSProperties = {
|
|
70
|
+
fontSize: 16,
|
|
71
|
+
color: '#374151',
|
|
72
|
+
lineHeight: '24px',
|
|
73
|
+
margin: '0 0 24px',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const muted: React.CSSProperties = {
|
|
77
|
+
fontSize: 14,
|
|
78
|
+
color: '#6b7280',
|
|
79
|
+
lineHeight: '20px',
|
|
80
|
+
margin: '0 0 24px',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const signOff: React.CSSProperties = {
|
|
84
|
+
fontSize: 14,
|
|
85
|
+
color: '#374151',
|
|
86
|
+
lineHeight: '20px',
|
|
87
|
+
margin: '24px 0 0',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Preview
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export default function Preview() {
|
|
95
|
+
return (
|
|
96
|
+
<GuardianEmailOtp
|
|
97
|
+
branding={DEFAULT_BRANDING}
|
|
98
|
+
verificationCode="847293"
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardianRejectedCredential — email sent to a student when their guardian
|
|
3
|
+
* has declined to approve a credential.
|
|
4
|
+
*
|
|
5
|
+
* Used by: brain-service (templateAlias: 'guardian-rejected-credential')
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Text } from '@react-email/components';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
|
|
11
|
+
import type { TenantBranding } from '../branding';
|
|
12
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
13
|
+
import { Layout } from '../components/Layout';
|
|
14
|
+
|
|
15
|
+
export interface GuardianRejectedCredentialProps {
|
|
16
|
+
branding: TenantBranding;
|
|
17
|
+
issuer?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
};
|
|
20
|
+
credential?: {
|
|
21
|
+
name?: string;
|
|
22
|
+
};
|
|
23
|
+
recipient?: {
|
|
24
|
+
email?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const GuardianRejectedCredential: React.FC<GuardianRejectedCredentialProps> = ({
|
|
29
|
+
branding,
|
|
30
|
+
issuer,
|
|
31
|
+
credential,
|
|
32
|
+
}) => {
|
|
33
|
+
const issuerName = issuer?.name ?? 'An issuer';
|
|
34
|
+
const credentialName = credential?.name;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Layout branding={branding} preview="Your guardian has declined a credential">
|
|
38
|
+
<Text style={heading}>Credential not approved</Text>
|
|
39
|
+
|
|
40
|
+
<Text style={paragraph}>Hi there,</Text>
|
|
41
|
+
|
|
42
|
+
<Text style={paragraph}>
|
|
43
|
+
Your guardian has reviewed and <strong>declined</strong> the credential{' '}
|
|
44
|
+
{credentialName ? <><strong>“{credentialName}”</strong> </> : ''}
|
|
45
|
+
issued by <strong>{issuerName}</strong>.
|
|
46
|
+
</Text>
|
|
47
|
+
|
|
48
|
+
<Text style={paragraph}>
|
|
49
|
+
The credential will not be added to your account. If you think this was
|
|
50
|
+
a mistake, please reach out to your guardian or contact{' '}
|
|
51
|
+
<strong>{issuerName}</strong> directly.
|
|
52
|
+
</Text>
|
|
53
|
+
|
|
54
|
+
<Text style={signOff}>
|
|
55
|
+
Sincerely,<br />The {branding.brandName} Team
|
|
56
|
+
</Text>
|
|
57
|
+
</Layout>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getGuardianRejectedCredentialSubject = (
|
|
62
|
+
_branding: TenantBranding,
|
|
63
|
+
): string => 'Credential Not Approved';
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Styles
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const heading: React.CSSProperties = {
|
|
70
|
+
fontSize: 24,
|
|
71
|
+
fontWeight: 600,
|
|
72
|
+
color: '#111827',
|
|
73
|
+
margin: '0 0 24px',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const paragraph: React.CSSProperties = {
|
|
77
|
+
fontSize: 16,
|
|
78
|
+
color: '#374151',
|
|
79
|
+
lineHeight: '24px',
|
|
80
|
+
margin: '0 0 24px',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const signOff: React.CSSProperties = {
|
|
84
|
+
fontSize: 14,
|
|
85
|
+
color: '#374151',
|
|
86
|
+
lineHeight: '20px',
|
|
87
|
+
margin: '24px 0 0',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Preview
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export default function Preview() {
|
|
95
|
+
return (
|
|
96
|
+
<GuardianRejectedCredential
|
|
97
|
+
branding={DEFAULT_BRANDING}
|
|
98
|
+
issuer={{ name: 'Springfield School District' }}
|
|
99
|
+
credential={{ name: 'Perfect Attendance Award' }}
|
|
100
|
+
recipient={{ email: 'student@example.com' }}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboxClaim — email notifying a recipient they have a credential to claim.
|
|
3
|
+
*
|
|
4
|
+
* Used by: brain-service inbox.helpers.ts (templateId: 'universal-inbox-claim')
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Text, Section } from '@react-email/components';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
|
|
10
|
+
import type { TenantBranding } from '../branding';
|
|
11
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
12
|
+
import { Layout } from '../components/Layout';
|
|
13
|
+
import { EmailButton } from '../components/EmailButton';
|
|
14
|
+
import { IssuerLogo } from '../components/IssuerLogo';
|
|
15
|
+
import { LinkFallback } from '../components/LinkFallback';
|
|
16
|
+
|
|
17
|
+
export interface InboxClaimProps {
|
|
18
|
+
branding: TenantBranding;
|
|
19
|
+
claimUrl: string;
|
|
20
|
+
recipient?: {
|
|
21
|
+
name?: string;
|
|
22
|
+
email?: string;
|
|
23
|
+
phone?: string;
|
|
24
|
+
};
|
|
25
|
+
issuer?: {
|
|
26
|
+
name?: string;
|
|
27
|
+
logoUrl?: string;
|
|
28
|
+
};
|
|
29
|
+
credential?: {
|
|
30
|
+
name?: string;
|
|
31
|
+
type?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const InboxClaim: React.FC<InboxClaimProps> = ({
|
|
36
|
+
branding,
|
|
37
|
+
claimUrl,
|
|
38
|
+
recipient,
|
|
39
|
+
issuer,
|
|
40
|
+
credential,
|
|
41
|
+
}) => {
|
|
42
|
+
const credentialName = credential?.name;
|
|
43
|
+
const credentialType = credential?.type ?? 'record';
|
|
44
|
+
const greeting = recipient?.name ? `Hello ${recipient.name},` : 'Hello,';
|
|
45
|
+
const issuerName = issuer?.name;
|
|
46
|
+
const issuerLogo = issuer?.logoUrl;
|
|
47
|
+
const showHeaderLogo = !issuerLogo;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Layout branding={branding} preview={`Your digital ${credentialType}${issuerName ? ` from ${issuerName}` : ''} is ready`} showHeaderLogo={showHeaderLogo}>
|
|
51
|
+
<IssuerLogo logoUrl={issuerLogo} alt={issuerName ? `${issuerName} logo` : undefined} />
|
|
52
|
+
|
|
53
|
+
<Text style={heading}>Your digital {credentialType} is ready</Text>
|
|
54
|
+
|
|
55
|
+
<Text style={paragraph}>
|
|
56
|
+
{greeting}
|
|
57
|
+
</Text>
|
|
58
|
+
|
|
59
|
+
<Text style={paragraph}>
|
|
60
|
+
{issuerName
|
|
61
|
+
? <><strong>{issuerName}</strong> has sent you</>
|
|
62
|
+
: <>You've received</>}{' '}
|
|
63
|
+
a secure, digital version of your{' '}
|
|
64
|
+
<strong>{credentialName ? `\u201C${credentialName}\u201D` : 'achievement'}</strong>.
|
|
65
|
+
</Text>
|
|
66
|
+
|
|
67
|
+
<Text style={paragraph}>
|
|
68
|
+
{branding.brandName} is your private, digital passport for learning and work.
|
|
69
|
+
It lets you securely collect and share your verified skills and achievements
|
|
70
|
+
online. By claiming this item, you are saving a verifiable, official {credentialType} to
|
|
71
|
+
your personal passport—one that only you control.
|
|
72
|
+
</Text>
|
|
73
|
+
|
|
74
|
+
<Section style={buttonWrapper}>
|
|
75
|
+
<EmailButton href={claimUrl} branding={branding}>
|
|
76
|
+
Claim Your Record →
|
|
77
|
+
</EmailButton>
|
|
78
|
+
</Section>
|
|
79
|
+
|
|
80
|
+
<Text style={signOff}>
|
|
81
|
+
Sincerely,<br />The {branding.brandName} Team
|
|
82
|
+
</Text>
|
|
83
|
+
|
|
84
|
+
<LinkFallback href={claimUrl} />
|
|
85
|
+
</Layout>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const getInboxClaimSubject = (
|
|
90
|
+
_branding: TenantBranding,
|
|
91
|
+
props: InboxClaimProps,
|
|
92
|
+
): string => {
|
|
93
|
+
const issuerPart = props.issuer?.name ? `from ${props.issuer.name} ` : '';
|
|
94
|
+
|
|
95
|
+
return `Your digital record ${issuerPart}is ready`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Styles
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
const heading: React.CSSProperties = {
|
|
103
|
+
fontSize: 24,
|
|
104
|
+
fontWeight: 600,
|
|
105
|
+
color: '#111827',
|
|
106
|
+
margin: '0 0 24px',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const paragraph: React.CSSProperties = {
|
|
110
|
+
fontSize: 16,
|
|
111
|
+
color: '#374151',
|
|
112
|
+
lineHeight: '24px',
|
|
113
|
+
margin: '0 0 24px',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const buttonWrapper: React.CSSProperties = {
|
|
117
|
+
margin: '0 0 24px',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const signOff: React.CSSProperties = {
|
|
121
|
+
fontSize: 14,
|
|
122
|
+
color: '#374151',
|
|
123
|
+
lineHeight: '20px',
|
|
124
|
+
margin: '24px 0 0',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Preview
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
export default function Preview() {
|
|
132
|
+
return (
|
|
133
|
+
<InboxClaim
|
|
134
|
+
branding={DEFAULT_BRANDING}
|
|
135
|
+
claimUrl="https://learncard.app/claim/abc123"
|
|
136
|
+
recipient={{ name: 'Jane Doe' }}
|
|
137
|
+
issuer={{ name: 'Acme University', logoUrl: 'https://cdn.filestackcontent.com/J6suaVcQ467W9o1k48Kj' }}
|
|
138
|
+
credential={{ name: 'Bachelor of Science', type: 'Achievement' }}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { VerificationCode, getVerificationCodeSubject } from './verification-code';
|
|
2
|
+
export type { VerificationCodeProps, VerificationCodeVariant } from './verification-code';
|
|
3
|
+
|
|
4
|
+
export { InboxClaim, getInboxClaimSubject } from './inbox-claim';
|
|
5
|
+
export type { InboxClaimProps } from './inbox-claim';
|
|
6
|
+
|
|
7
|
+
export { GuardianApproval, getGuardianApprovalSubject } from './guardian-approval';
|
|
8
|
+
export type { GuardianApprovalProps } from './guardian-approval';
|
|
9
|
+
|
|
10
|
+
export { AccountApproved, getAccountApprovedSubject } from './account-approved';
|
|
11
|
+
export type { AccountApprovedProps } from './account-approved';
|
|
12
|
+
|
|
13
|
+
export { RecoveryKey, getRecoveryKeySubject } from './recovery-key';
|
|
14
|
+
export type { RecoveryKeyProps } from './recovery-key';
|
|
15
|
+
|
|
16
|
+
export { EndorsementRequest, getEndorsementRequestSubject } from './endorsement-request';
|
|
17
|
+
export type { EndorsementRequestProps } from './endorsement-request';
|
|
18
|
+
|
|
19
|
+
export { CredentialAwaitingGuardian, getCredentialAwaitingGuardianSubject } from './credential-awaiting-guardian';
|
|
20
|
+
export type { CredentialAwaitingGuardianProps } from './credential-awaiting-guardian';
|
|
21
|
+
|
|
22
|
+
export { GuardianApprovedClaim, getGuardianApprovedClaimSubject } from './guardian-approved-claim';
|
|
23
|
+
export type { GuardianApprovedClaimProps } from './guardian-approved-claim';
|
|
24
|
+
|
|
25
|
+
export { GuardianCredentialApproval, getGuardianCredentialApprovalSubject } from './guardian-credential-approval';
|
|
26
|
+
export type { GuardianCredentialApprovalProps } from './guardian-credential-approval';
|
|
27
|
+
|
|
28
|
+
export { GuardianEmailOtp, getGuardianEmailOtpSubject } from './guardian-email-otp';
|
|
29
|
+
export type { GuardianEmailOtpProps } from './guardian-email-otp';
|
|
30
|
+
|
|
31
|
+
export { GuardianRejectedCredential, getGuardianRejectedCredentialSubject } from './guardian-rejected-credential';
|
|
32
|
+
export type { GuardianRejectedCredentialProps } from './guardian-rejected-credential';
|
|
33
|
+
|
|
34
|
+
export { EmailVerification, getEmailVerificationSubject } from './email-verification';
|
|
35
|
+
export type { EmailVerificationProps } from './email-verification';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecoveryKey — email containing a user's account recovery key.
|
|
3
|
+
*
|
|
4
|
+
* Used by: lca-api keys.ts (templateAlias: RECOVERY_KEY_TEMPLATE_ALIAS)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Text } from '@react-email/components';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
|
|
10
|
+
import type { TenantBranding } from '../branding';
|
|
11
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
12
|
+
import { Layout } from '../components/Layout';
|
|
13
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
14
|
+
|
|
15
|
+
export interface RecoveryKeyProps {
|
|
16
|
+
branding: TenantBranding;
|
|
17
|
+
recoveryKey: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const RecoveryKey: React.FC<RecoveryKeyProps> = ({
|
|
21
|
+
branding,
|
|
22
|
+
recoveryKey,
|
|
23
|
+
}) => (
|
|
24
|
+
<Layout branding={branding} preview={`Your ${branding.brandName} recovery key — keep this safe`}>
|
|
25
|
+
<Text style={heading}>Your {branding.brandName} Recovery Key</Text>
|
|
26
|
+
|
|
27
|
+
<Text style={paragraph}>Hello,</Text>
|
|
28
|
+
|
|
29
|
+
<Text style={paragraph}>
|
|
30
|
+
Keep this email safe. You can use the recovery key below to regain access
|
|
31
|
+
to your {branding.brandName} account if you lose your device.
|
|
32
|
+
</Text>
|
|
33
|
+
|
|
34
|
+
<CodeBlock code={recoveryKey} label="Recovery Key — do NOT share this with anyone" variant="key" />
|
|
35
|
+
|
|
36
|
+
<Text style={paragraph}>
|
|
37
|
+
To recover your account, choose <strong>“Recover via Email”</strong> in
|
|
38
|
+
the app and paste the recovery key above when prompted.
|
|
39
|
+
</Text>
|
|
40
|
+
|
|
41
|
+
<Text style={muted}>
|
|
42
|
+
If you did not request this, you can safely ignore this email.
|
|
43
|
+
</Text>
|
|
44
|
+
|
|
45
|
+
<Text style={paragraph}>
|
|
46
|
+
{branding.brandName} is your private, digital passport for learning and work.
|
|
47
|
+
It lets you securely collect and share your verified skills and achievements online.
|
|
48
|
+
</Text>
|
|
49
|
+
|
|
50
|
+
<Text style={signOff}>
|
|
51
|
+
Sincerely,<br />The {branding.brandName} Team
|
|
52
|
+
</Text>
|
|
53
|
+
</Layout>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export const getRecoveryKeySubject = (branding: TenantBranding): string =>
|
|
57
|
+
`Your ${branding.brandName} recovery key`;
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Styles
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const heading: React.CSSProperties = {
|
|
64
|
+
fontSize: 24,
|
|
65
|
+
fontWeight: 600,
|
|
66
|
+
color: '#111827',
|
|
67
|
+
margin: '0 0 24px',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const paragraph: React.CSSProperties = {
|
|
71
|
+
fontSize: 16,
|
|
72
|
+
color: '#374151',
|
|
73
|
+
lineHeight: '24px',
|
|
74
|
+
margin: '0 0 24px',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const muted: React.CSSProperties = {
|
|
78
|
+
fontSize: 14,
|
|
79
|
+
color: '#6b7280',
|
|
80
|
+
lineHeight: '20px',
|
|
81
|
+
margin: '0 0 24px',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const signOff: React.CSSProperties = {
|
|
85
|
+
fontSize: 14,
|
|
86
|
+
color: '#374151',
|
|
87
|
+
lineHeight: '20px',
|
|
88
|
+
margin: '24px 0 0',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Preview
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export default function Preview() {
|
|
96
|
+
return (
|
|
97
|
+
<RecoveryKey
|
|
98
|
+
branding={DEFAULT_BRANDING}
|
|
99
|
+
recoveryKey="mango-delta-fox-echo-bravo-seven-lima-niner"
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|