Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ contradicts this file is a bug.

## Canonical assets

The optional welcome email uses the subject "Your skilld account is ready".
Its body is "Like Skills to find them again in your account." and "Watch the Repositories you use and see what changed."
Its final line is "Your weekly and monthly email choices stay as you set them."
The optional welcome email uses the subject "Welcome to skilld".
Its greeting is "Hi! Iโ€™m Harlan."
Its story is "I built skilld to discover new Skills without spending all day on social media, read their source, and keep up with updates."
Its account line is "Your account lets you save liked Skills, watch Repositories, and choose which update emails you receive."
After its main action, it says "Let me know if youโ€™d like to see any changes :)" and signs off "Harlan".
Replies go to `harlan@harlanzw.com`.
Its one main action is "Open your account", linking to `/me`.
Its footer is "Stop welcome emails", linking to a signed welcome unsubscribe URL.
Onboarding offers an unchecked choice: "Send me one welcome email with account tips."
Expand Down
2 changes: 2 additions & 0 deletions layers/identity/server/utils/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
export interface SendEmailInput {
to: string
from?: EmailAddress
replyTo?: string
subject: string
html: string
text?: string
Expand Down Expand Up @@ -46,6 +47,7 @@ export async function sendEmailWithEnv(
const outcome = await binding.send({
to: input.to,
from,
replyTo: input.replyTo,
subject: input.subject,
html: input.html,
text: input.text,
Expand Down
12 changes: 8 additions & 4 deletions layers/identity/server/utils/welcome-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ export function renderWelcomeEmail(unsubscribeUrl: string): Pick<SendEmailInput,
<table role="presentation" style="width:100%;max-width:560px;margin:auto;border-collapse:collapse"><tr><td style="padding:28px;background:#fff;border:1px solid #d6d3d1;border-radius:8px">
<p style="margin:0 0 28px;font-family:monospace;font-size:20px;font-weight:bold">&gt; skilld<span style="color:#be123c">.</span></p>
<h1 style="font-size:26px;line-height:1.25;margin:0 0 24px">${copy.subject}</h1>
<p style="font-size:16px;line-height:1.6">${copy.skills}</p><p style="font-size:16px;line-height:1.6">${copy.watch}</p>
<p style="font-size:14px;line-height:1.6;color:#57504b">${copy.choices}</p>
<p style="margin:28px 0 0"><a href="${link}" style="display:inline-block;background:#be123c;color:#fff;text-decoration:none;font-weight:bold;padding:14px 20px;border-radius:8px">${copy.action}</a></p>
<p style="font-size:16px;line-height:1.6">${copy.greeting}</p>
<p style="font-size:16px;line-height:1.6">${copy.story}</p>
<p style="font-size:16px;line-height:1.6">${copy.account}</p>
<p style="margin:28px 0"><a href="${link}" style="display:inline-block;background:#be123c;color:#fff;text-decoration:none;font-weight:bold;padding:14px 20px;border-radius:8px">${copy.action}</a></p>
<p style="font-size:16px;line-height:1.6">${copy.feedback}</p>
<p style="font-size:16px;line-height:1.6;margin-bottom:0">${copy.signature}</p>
</td></tr><tr><td style="padding:16px 0;font-size:12px"><a href="${safeUnsubscribeUrl}" style="color:#57504b">${copy.unsubscribe}</a></td></tr></table>
</td></tr></table></body></html>`,
text: `${copy.skills}\n\n${copy.watch}\n\n${copy.choices}\n\n${copy.action}: ${link}\n\n${copy.unsubscribe}: ${unsubscribeUrl}`,
text: `${copy.greeting}\n\n${copy.story}\n\n${copy.account}\n\n${copy.action}: ${link}\n\n${copy.feedback}\n\n${copy.signature}\n\n${copy.unsubscribe}: ${unsubscribeUrl}`,
}
}

Expand Down Expand Up @@ -60,6 +63,7 @@ export async function sendWelcomeEmail(input: WelcomeEmailInput): Promise<'skipp
return 'skipped'
const outcome = await input.send({
to: input.to.trim(),
replyTo: 'harlan@harlanzw.com',
...renderWelcomeEmail(unsubscribeUrl),
headers: digestEmailHeaders(unsubscribeUrl, `skilld-welcome:${userId}`),
}).catch((error): SendEmailResult => ({
Expand Down
10 changes: 6 additions & 4 deletions shared/welcome-email-copy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export const WELCOME_EMAIL_COPY = {
subject: 'Your skilld account is ready',
skills: 'Like Skills to find them again in your account.',
watch: 'Watch the Repositories you use and see what changed.',
choices: 'Your weekly and monthly email choices stay as you set them.',
subject: 'Welcome to skilld',
greeting: 'Hi! Iโ€™m Harlan.',
story: 'I built skilld to discover new Skills without spending all day on social media, read their source, and keep up with updates.',
account: 'Your account lets you save liked Skills, watch Repositories, and choose which update emails you receive.',
feedback: 'Let me know if youโ€™d like to see any changes :)',
signature: 'Harlan',
action: 'Open your account',
unsubscribe: 'Stop welcome emails',
consent: 'Send me one welcome email with account tips.',
Expand Down
17 changes: 15 additions & 2 deletions test/unit/welcome-email.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SqliteD1 } from './helpers/d1-sqlite'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { planAccountSettings, updateAccountSettings } from '../../layers/identity/server/utils/account-settings'
import { signUnsubToken, verifyUnsubToken } from '../../layers/identity/server/utils/email'
import { sendEmailWithEnv, signUnsubToken, verifyUnsubToken } from '../../layers/identity/server/utils/email'
import { applyUnsubscribe, renderUnsubscribePage } from '../../layers/identity/server/utils/unsubscribe'
import { sendWelcomeEmail } from '../../layers/identity/server/utils/welcome-email'
import { identityEmailPatchBodySchema } from '../../layers/identity/shared/contracts/account'
Expand Down Expand Up @@ -29,6 +29,19 @@ describe('one optional welcome email', () => {
}
}

it('routes welcome replies to Harlan through the email binding', async () => {
const send = vi.fn().mockResolvedValue({ messageId: 'receipt' })
const deps = input()
deps.send.mockImplementation(message => sendEmailWithEnv(
{ EMAIL: { send } } as unknown as Pick<Cloudflare.Env, 'EMAIL'>,
{ ...message, from: { email: 'noreply@mail.skilld.dev', name: 'skilld' } },
))
expect(await sendWelcomeEmail(deps)).toBe('accepted')
expect(send).toHaveBeenCalledWith(expect.objectContaining({
replyTo: 'harlan@harlanzw.com',
}))
})

it('requires an address when only the welcome email is selected', () => {
expect(identityEmailPatchBodySchema.safeParse({ welcome_email: true }).success).toBe(false)
})
Expand Down Expand Up @@ -79,7 +92,7 @@ describe('one optional welcome email', () => {
expect(deps.send).toHaveBeenCalledTimes(1)
expect(deps.send).toHaveBeenCalledWith(expect.objectContaining({
to: 'account@example.com',
subject: 'Your skilld account is ready',
subject: 'Welcome to skilld',
text: expect.stringContaining('https://skilld.dev/me'),
headers: expect.objectContaining({ 'List-Unsubscribe': '<https://skilld.dev/api/unsubscribe?t=signed-token&list=welcome>' }),
}))
Expand Down
Loading