@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
package/src/render.ts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render.ts — the main rendering entry point.
|
|
3
|
+
*
|
|
4
|
+
* Consumers call `renderEmail(templateId, branding, data)` and get back
|
|
5
|
+
* `{ html, text, subject }` — ready to hand to any email provider.
|
|
6
|
+
*
|
|
7
|
+
* Template IDs map to the IDs used by brain-service and lca-api today,
|
|
8
|
+
* so migration can happen one template at a time.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { render } from '@react-email/render';
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
|
|
14
|
+
import type { TenantBranding } from './branding';
|
|
15
|
+
import { resolveBranding } from './branding';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
VerificationCode,
|
|
19
|
+
getVerificationCodeSubject,
|
|
20
|
+
InboxClaim,
|
|
21
|
+
getInboxClaimSubject,
|
|
22
|
+
GuardianApproval,
|
|
23
|
+
getGuardianApprovalSubject,
|
|
24
|
+
AccountApproved,
|
|
25
|
+
getAccountApprovedSubject,
|
|
26
|
+
RecoveryKey,
|
|
27
|
+
getRecoveryKeySubject,
|
|
28
|
+
EndorsementRequest,
|
|
29
|
+
getEndorsementRequestSubject,
|
|
30
|
+
CredentialAwaitingGuardian,
|
|
31
|
+
getCredentialAwaitingGuardianSubject,
|
|
32
|
+
GuardianApprovedClaim,
|
|
33
|
+
getGuardianApprovedClaimSubject,
|
|
34
|
+
GuardianCredentialApproval,
|
|
35
|
+
getGuardianCredentialApprovalSubject,
|
|
36
|
+
GuardianEmailOtp,
|
|
37
|
+
getGuardianEmailOtpSubject,
|
|
38
|
+
GuardianRejectedCredential,
|
|
39
|
+
getGuardianRejectedCredentialSubject,
|
|
40
|
+
EmailVerification,
|
|
41
|
+
getEmailVerificationSubject,
|
|
42
|
+
} from './templates';
|
|
43
|
+
|
|
44
|
+
import type {
|
|
45
|
+
VerificationCodeVariant,
|
|
46
|
+
InboxClaimProps,
|
|
47
|
+
GuardianApprovalProps,
|
|
48
|
+
RecoveryKeyProps,
|
|
49
|
+
EndorsementRequestProps,
|
|
50
|
+
CredentialAwaitingGuardianProps,
|
|
51
|
+
GuardianApprovedClaimProps,
|
|
52
|
+
GuardianCredentialApprovalProps,
|
|
53
|
+
GuardianEmailOtpProps,
|
|
54
|
+
GuardianRejectedCredentialProps,
|
|
55
|
+
EmailVerificationProps,
|
|
56
|
+
} from './templates';
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Public types
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export interface RenderedEmail {
|
|
63
|
+
html: string;
|
|
64
|
+
text: string;
|
|
65
|
+
subject: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Template ID → data shape mapping.
|
|
70
|
+
*
|
|
71
|
+
* Keys are the template IDs / aliases used at call sites in brain-service and lca-api.
|
|
72
|
+
* This provides full type safety: `renderEmail('inbox-claim', branding, data)` will
|
|
73
|
+
* require `data` to match `InboxClaimData`.
|
|
74
|
+
*/
|
|
75
|
+
export interface TemplateDataMap {
|
|
76
|
+
// -- Verification code variants ------------------------------------------
|
|
77
|
+
|
|
78
|
+
/** brain-service: contact-methods.ts, embed flow */
|
|
79
|
+
'embed-email-verification': VerificationCodeData;
|
|
80
|
+
|
|
81
|
+
/** brain-service: contact-methods.ts — link-based email verification */
|
|
82
|
+
'contact-method-verification': EmailVerificationData;
|
|
83
|
+
|
|
84
|
+
/** lca-api: firebase.ts login flow */
|
|
85
|
+
'login-verification-code': VerificationCodeData;
|
|
86
|
+
|
|
87
|
+
/** Postmark alias for login-verification-code */
|
|
88
|
+
'contact-method-verification-1': VerificationCodeData;
|
|
89
|
+
|
|
90
|
+
/** lca-api: keys.ts recovery email verification */
|
|
91
|
+
'recovery-email-code': VerificationCodeData;
|
|
92
|
+
|
|
93
|
+
/** Postmark alias for recovery-email-code */
|
|
94
|
+
'recovery-email-verification': VerificationCodeData;
|
|
95
|
+
|
|
96
|
+
// -- Inbox / claim -------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** brain-service: inbox.helpers.ts */
|
|
99
|
+
'inbox-claim': InboxClaimData;
|
|
100
|
+
|
|
101
|
+
/** Postmark alias for inbox-claim */
|
|
102
|
+
'universal-inbox-claim': InboxClaimData;
|
|
103
|
+
|
|
104
|
+
// -- Guardian account approval -------------------------------------------
|
|
105
|
+
|
|
106
|
+
/** brain-service: inbox.ts */
|
|
107
|
+
'guardian-approval': GuardianApprovalData;
|
|
108
|
+
|
|
109
|
+
// -- Account approved ----------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/** brain-service: inbox.ts */
|
|
112
|
+
'account-approved': AccountApprovedData;
|
|
113
|
+
|
|
114
|
+
/** Postmark alias for account-approved */
|
|
115
|
+
'account-approved-email': AccountApprovedData;
|
|
116
|
+
|
|
117
|
+
// -- Recovery key --------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** lca-api: keys.ts */
|
|
120
|
+
'recovery-key': RecoveryKeyData;
|
|
121
|
+
|
|
122
|
+
/** Postmark alias for recovery-key */
|
|
123
|
+
'recovery-key-backup': RecoveryKeyData;
|
|
124
|
+
|
|
125
|
+
// -- Endorsement request -------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/** lca-api: credentials.ts */
|
|
128
|
+
'endorsement-request': EndorsementRequestData;
|
|
129
|
+
|
|
130
|
+
/** Postmark alias for endorsement-request */
|
|
131
|
+
'universal-inbox-claim-1': EndorsementRequestData;
|
|
132
|
+
|
|
133
|
+
// -- Guardian credential flow (NEW) --------------------------------------
|
|
134
|
+
|
|
135
|
+
/** Sent to student: credential pending guardian approval */
|
|
136
|
+
'credential-awaiting-guardian': CredentialAwaitingGuardianData;
|
|
137
|
+
|
|
138
|
+
/** Sent to student: guardian approved credential */
|
|
139
|
+
'guardian-approved-claim': GuardianApprovedClaimData;
|
|
140
|
+
|
|
141
|
+
/** Sent to guardian: approve/decline a specific credential */
|
|
142
|
+
'guardian-credential-approval': GuardianCredentialApprovalData;
|
|
143
|
+
|
|
144
|
+
/** Sent to guardian: OTP for identity verification */
|
|
145
|
+
'guardian-email-otp': GuardianEmailOtpData;
|
|
146
|
+
|
|
147
|
+
/** Sent to student: guardian rejected credential */
|
|
148
|
+
'guardian-rejected-credential': GuardianRejectedCredentialData;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type TemplateId = keyof TemplateDataMap;
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Per-template data shapes (without branding — that's injected by renderEmail)
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
export interface VerificationCodeData {
|
|
158
|
+
verificationCode: string;
|
|
159
|
+
verificationEmail?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface EmailVerificationData {
|
|
163
|
+
verificationToken: string;
|
|
164
|
+
recipient?: { name?: string };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface InboxClaimData {
|
|
168
|
+
claimUrl: string;
|
|
169
|
+
recipient?: { name?: string; email?: string; phone?: string };
|
|
170
|
+
issuer?: { name?: string; logoUrl?: string };
|
|
171
|
+
credential?: { name?: string; type?: string };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface GuardianApprovalData {
|
|
175
|
+
approvalUrl: string;
|
|
176
|
+
approvalToken: string;
|
|
177
|
+
requester?: { displayName?: string; profileId?: string };
|
|
178
|
+
guardian?: { email?: string };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface AccountApprovedData {
|
|
182
|
+
user?: { displayName?: string };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface RecoveryKeyData {
|
|
186
|
+
recoveryKey: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface EndorsementRequestData {
|
|
190
|
+
shareLink: string;
|
|
191
|
+
recipient?: { name?: string; email?: string };
|
|
192
|
+
issuer?: { name?: string; logoUrl?: string };
|
|
193
|
+
credential?: { name?: string; type?: string };
|
|
194
|
+
message?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface CredentialAwaitingGuardianData {
|
|
198
|
+
issuer?: { name?: string; logoUrl?: string };
|
|
199
|
+
credential?: { name?: string };
|
|
200
|
+
recipient?: { email?: string };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface GuardianApprovedClaimData {
|
|
204
|
+
issuer?: { name?: string; logoUrl?: string };
|
|
205
|
+
credential?: { name?: string };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface GuardianCredentialApprovalData {
|
|
209
|
+
approvalUrl: string;
|
|
210
|
+
approvalToken?: string;
|
|
211
|
+
issuer?: { name?: string; logoUrl?: string };
|
|
212
|
+
credential?: { name?: string };
|
|
213
|
+
recipient?: { email?: string };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface GuardianEmailOtpData {
|
|
217
|
+
verificationCode: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface GuardianRejectedCredentialData {
|
|
221
|
+
issuer?: { name?: string };
|
|
222
|
+
credential?: { name?: string };
|
|
223
|
+
recipient?: { email?: string };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// renderEmail()
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Render a fully branded email for the given template.
|
|
232
|
+
*
|
|
233
|
+
* @param templateId — one of the known template IDs
|
|
234
|
+
* @param branding — partial tenant branding (missing fields use LearnCard defaults)
|
|
235
|
+
* @param data — template-specific data
|
|
236
|
+
* @returns `{ html, text, subject }` ready for any email adapter
|
|
237
|
+
*/
|
|
238
|
+
export async function renderEmail<T extends TemplateId>(
|
|
239
|
+
templateId: T,
|
|
240
|
+
branding: Partial<TenantBranding> | TenantBranding,
|
|
241
|
+
data: TemplateDataMap[T],
|
|
242
|
+
): Promise<RenderedEmail> {
|
|
243
|
+
const resolved = resolveBranding(branding);
|
|
244
|
+
|
|
245
|
+
const { element, subject } = buildElement(templateId, resolved, data);
|
|
246
|
+
|
|
247
|
+
const html = await render(element);
|
|
248
|
+
const text = await render(element, { plainText: true });
|
|
249
|
+
|
|
250
|
+
return { html, text, subject };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Internal: build the React element + subject for a given template
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
function buildVerificationElement(
|
|
258
|
+
branding: TenantBranding,
|
|
259
|
+
data: VerificationCodeData,
|
|
260
|
+
variant: VerificationCodeVariant,
|
|
261
|
+
): { element: React.ReactElement; subject: string } {
|
|
262
|
+
return {
|
|
263
|
+
element: React.createElement(VerificationCode, {
|
|
264
|
+
branding,
|
|
265
|
+
verificationCode: data.verificationCode,
|
|
266
|
+
verificationEmail: data.verificationEmail,
|
|
267
|
+
variant,
|
|
268
|
+
}),
|
|
269
|
+
subject: getVerificationCodeSubject(branding, variant),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildElement(
|
|
274
|
+
templateId: TemplateId,
|
|
275
|
+
branding: TenantBranding,
|
|
276
|
+
data: TemplateDataMap[TemplateId],
|
|
277
|
+
): { element: React.ReactElement; subject: string } {
|
|
278
|
+
|
|
279
|
+
switch (templateId) {
|
|
280
|
+
case 'embed-email-verification':
|
|
281
|
+
return buildVerificationElement(branding, data as VerificationCodeData, 'embed-verification');
|
|
282
|
+
|
|
283
|
+
case 'contact-method-verification': {
|
|
284
|
+
const d = data as EmailVerificationData;
|
|
285
|
+
const props: EmailVerificationProps = { branding, ...d };
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
element: React.createElement(EmailVerification, props),
|
|
289
|
+
subject: getEmailVerificationSubject(branding),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case 'login-verification-code':
|
|
294
|
+
case 'contact-method-verification-1':
|
|
295
|
+
return buildVerificationElement(branding, data as VerificationCodeData, 'login');
|
|
296
|
+
|
|
297
|
+
case 'recovery-email-code':
|
|
298
|
+
case 'recovery-email-verification':
|
|
299
|
+
return buildVerificationElement(branding, data as VerificationCodeData, 'recovery-email');
|
|
300
|
+
|
|
301
|
+
case 'inbox-claim':
|
|
302
|
+
case 'universal-inbox-claim': {
|
|
303
|
+
const d = data as InboxClaimData;
|
|
304
|
+
const props: InboxClaimProps = { branding, ...d };
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
element: React.createElement(InboxClaim, props),
|
|
308
|
+
subject: getInboxClaimSubject(branding, props),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
case 'guardian-approval': {
|
|
313
|
+
const d = data as GuardianApprovalData;
|
|
314
|
+
const props: GuardianApprovalProps = { branding, ...d };
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
element: React.createElement(GuardianApproval, props),
|
|
318
|
+
subject: getGuardianApprovalSubject(branding, props),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case 'account-approved':
|
|
323
|
+
case 'account-approved-email': {
|
|
324
|
+
const d = data as AccountApprovedData;
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
element: React.createElement(AccountApproved, { branding, ...d }),
|
|
328
|
+
subject: getAccountApprovedSubject(branding),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case 'recovery-key':
|
|
333
|
+
case 'recovery-key-backup': {
|
|
334
|
+
const d = data as RecoveryKeyData;
|
|
335
|
+
const props: RecoveryKeyProps = { branding, ...d };
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
element: React.createElement(RecoveryKey, props),
|
|
339
|
+
subject: getRecoveryKeySubject(branding),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case 'endorsement-request':
|
|
344
|
+
case 'universal-inbox-claim-1': {
|
|
345
|
+
const d = data as EndorsementRequestData;
|
|
346
|
+
const props: EndorsementRequestProps = { branding, ...d };
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
element: React.createElement(EndorsementRequest, props),
|
|
350
|
+
subject: getEndorsementRequestSubject(branding, props),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case 'credential-awaiting-guardian': {
|
|
355
|
+
const d = data as CredentialAwaitingGuardianData;
|
|
356
|
+
const props: CredentialAwaitingGuardianProps = { branding, ...d };
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
element: React.createElement(CredentialAwaitingGuardian, props),
|
|
360
|
+
subject: getCredentialAwaitingGuardianSubject(branding),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case 'guardian-approved-claim': {
|
|
365
|
+
const d = data as GuardianApprovedClaimData;
|
|
366
|
+
const props: GuardianApprovedClaimProps = { branding, ...d };
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
element: React.createElement(GuardianApprovedClaim, props),
|
|
370
|
+
subject: getGuardianApprovedClaimSubject(branding),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case 'guardian-credential-approval': {
|
|
375
|
+
const d = data as GuardianCredentialApprovalData;
|
|
376
|
+
const props: GuardianCredentialApprovalProps = { branding, ...d };
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
element: React.createElement(GuardianCredentialApproval, props),
|
|
380
|
+
subject: getGuardianCredentialApprovalSubject(branding),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
case 'guardian-email-otp': {
|
|
385
|
+
const d = data as GuardianEmailOtpData;
|
|
386
|
+
const props: GuardianEmailOtpProps = { branding, ...d };
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
element: React.createElement(GuardianEmailOtp, props),
|
|
390
|
+
subject: getGuardianEmailOtpSubject(branding),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case 'guardian-rejected-credential': {
|
|
395
|
+
const d = data as GuardianRejectedCredentialData;
|
|
396
|
+
const props: GuardianRejectedCredentialProps = { branding, ...d };
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
element: React.createElement(GuardianRejectedCredential, props),
|
|
400
|
+
subject: getGuardianRejectedCredentialSubject(branding),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
default: {
|
|
405
|
+
const _exhaustive: never = templateId;
|
|
406
|
+
throw new Error(`Unknown template: ${_exhaustive}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
package/src/sms.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMS template rendering — tenant-aware replacement for sms.helpers.ts.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the hardcoded "LearnCard" in SMS text with the tenant's brand name.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TenantBranding } from './branding';
|
|
8
|
+
import { resolveBranding } from './branding';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// SMS template data shapes
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export interface SmsInboxClaimData {
|
|
15
|
+
claimUrl: string;
|
|
16
|
+
recipient?: { name?: string };
|
|
17
|
+
issuer?: { name?: string };
|
|
18
|
+
credential?: { name?: string; type?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SmsVerificationData {
|
|
22
|
+
verificationToken: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SmsTemplateId = 'inbox-claim' | 'contact-method-verification';
|
|
26
|
+
|
|
27
|
+
export interface SmsTemplateDataMap {
|
|
28
|
+
'inbox-claim': SmsInboxClaimData;
|
|
29
|
+
'contact-method-verification': SmsVerificationData;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// renderSms()
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render a branded SMS body for the given template.
|
|
38
|
+
*
|
|
39
|
+
* @returns The SMS body string, or `undefined` if the templateId is unknown.
|
|
40
|
+
*/
|
|
41
|
+
export function renderSms<T extends SmsTemplateId>(
|
|
42
|
+
templateId: T,
|
|
43
|
+
branding: Partial<TenantBranding> | TenantBranding,
|
|
44
|
+
data: SmsTemplateDataMap[T],
|
|
45
|
+
): string | undefined {
|
|
46
|
+
const resolved = resolveBranding(branding);
|
|
47
|
+
|
|
48
|
+
switch (templateId) {
|
|
49
|
+
case 'inbox-claim': {
|
|
50
|
+
const d = data as SmsInboxClaimData;
|
|
51
|
+
const greeting = d.recipient?.name ? `Hello, ${d.recipient.name}! ` : '';
|
|
52
|
+
const credType = d.credential?.type ?? 'credential';
|
|
53
|
+
const credName = d.credential?.name ?? 'Unnamed Credential';
|
|
54
|
+
const issuerPart = d.issuer?.name ? ` from ${d.issuer.name}.` : '.';
|
|
55
|
+
|
|
56
|
+
return `${greeting}You have a new ${credType}, "${credName}"${issuerPart} Claim it here: ${d.claimUrl}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'contact-method-verification': {
|
|
60
|
+
const d = data as SmsVerificationData;
|
|
61
|
+
|
|
62
|
+
return `Your ${resolved.brandName} verification code is: ${d.verificationToken}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
default:
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccountApproved — email sent when a user's account has been approved.
|
|
3
|
+
*
|
|
4
|
+
* Used by: brain-service inbox.ts (templateId: 'account-approved-email')
|
|
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
|
+
|
|
15
|
+
export interface AccountApprovedProps {
|
|
16
|
+
branding: TenantBranding;
|
|
17
|
+
user?: {
|
|
18
|
+
displayName?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AccountApproved: React.FC<AccountApprovedProps> = ({
|
|
23
|
+
branding,
|
|
24
|
+
user,
|
|
25
|
+
}) => {
|
|
26
|
+
const name = user?.displayName ?? 'there';
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Layout branding={branding} preview={`Your ${branding.brandName} account has been approved!`}>
|
|
30
|
+
<Text style={heading}>Your {branding.brandName} account has been approved!</Text>
|
|
31
|
+
|
|
32
|
+
<Text style={paragraph}>
|
|
33
|
+
Hello {name},
|
|
34
|
+
</Text>
|
|
35
|
+
|
|
36
|
+
<Text style={paragraph}>
|
|
37
|
+
Your {branding.brandName} account has been approved by your guardian/parent.
|
|
38
|
+
</Text>
|
|
39
|
+
|
|
40
|
+
<Text style={paragraph}>
|
|
41
|
+
{branding.brandName} is a private, digital passport for learning and work.
|
|
42
|
+
It lets users securely collect and share their verified skills and achievements online.
|
|
43
|
+
</Text>
|
|
44
|
+
|
|
45
|
+
<Text style={paragraph}>
|
|
46
|
+
Please click the button below to login to your account.
|
|
47
|
+
</Text>
|
|
48
|
+
|
|
49
|
+
<Section style={buttonWrapper}>
|
|
50
|
+
<EmailButton href={branding.appUrl} branding={branding}>
|
|
51
|
+
Login to {branding.brandName} →
|
|
52
|
+
</EmailButton>
|
|
53
|
+
</Section>
|
|
54
|
+
|
|
55
|
+
<Text style={signOff}>
|
|
56
|
+
Thank you,<br />The {branding.brandName} Team
|
|
57
|
+
</Text>
|
|
58
|
+
</Layout>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const getAccountApprovedSubject = (branding: TenantBranding): string =>
|
|
63
|
+
`Your ${branding.brandName} account has been 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 buttonWrapper: React.CSSProperties = {
|
|
84
|
+
margin: '0 0 24px',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const signOff: React.CSSProperties = {
|
|
88
|
+
fontSize: 14,
|
|
89
|
+
color: '#374151',
|
|
90
|
+
lineHeight: '20px',
|
|
91
|
+
margin: '24px 0 0',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Preview
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export default function Preview() {
|
|
99
|
+
return (
|
|
100
|
+
<AccountApproved
|
|
101
|
+
branding={DEFAULT_BRANDING}
|
|
102
|
+
user={{ displayName: 'Jane Doe' }}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|