Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[backend] add new email template #4511

Merged
merged 10 commits into from
Sep 4, 2023
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
7 changes: 4 additions & 3 deletions packages/server/.env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ APP_SECRET_KEY: "SECRET_1234"
APP_LOG_LEVEL: "verbose"

#
# Email (only one of the api provider should be defined)
# Email
#

# EMAIL_SENDER: "[email protected]" # This is only needed when SENDGRID_API_KEY is defined
# EMAIL_SENDER: "[email protected]"

# Only one of the api provider should be defined
# SENDGRID_API_KEY: "SG.1234567890qwerty"

# MAILGUN_CONFIG: '{username: "api", key: "qwerty12345", url: "https://api.eu.mailgun.net"}'
# MAILGUN_API_KEY: "qwerty12345"
# MAILGUN_DOMAIN: "example.com"

#
Expand Down
2 changes: 2 additions & 0 deletions packages/server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ dist

# Keep environment variables out of version control
.env

.react-email
2 changes: 2 additions & 0 deletions packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ To run the API to develop locally:
- `yarn workspace server test`: Run tests.
- `yarn workspace server dev:notify`: Run the notifier `nodemon` and `ts-node`.
- `yarn workspace server dev:db:reset`: Clear the database data and re-synchronize its schema.
- `yarn workspace server dev:emails`: Run live development preview for emails.
- `yarn workspace server dev:mockEmail email [notificationKind]`: Send mock email.
- `yarn workspace server codegen`: Run `graphql-codegen`.
- [`yarn workspace server prisma studio`][prisma studio]: Launch an administration GUI for the database.
- [`yarn workspace server prisma db push`][prisma db:push]: Synchronize `schema.prisma` with the database schema.
Expand Down
1 change: 1 addition & 0 deletions packages/server/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Config } from 'jest'

const config: Config = {
preset: 'ts-jest',
setupFiles: ['./test/setup.ts'],

moduleNameMapper: {
Expand Down
7 changes: 7 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"dev:db:build": "yarn dev:db && yarn dev:build",
"dev:api": "yarn nodemon ./src/common/scripts/startApi.ts",
"dev:notify": "yarn nodemon ./src/notifier/scripts/notify.ts",
"dev:mockEmail": "yarn ts-node ./src/notifier/scripts/mockEmail.ts",
"dev:authtoken": "yarn nodemon ./src/auth/scripts/generateToken.ts",
"dev:emails": "email dev --dir ./src/common/email-templates",
"lint": "yarn lint:prettier --check && yarn lint:eslint --max-warnings=0",
"lint:eslint": "eslint \"./{src,test}/**/*.ts\"",
"lint:fix": "yarn lint:eslint --fix && yarn lint:prettier --write --loglevel warn",
Expand All @@ -31,6 +33,8 @@
"@apollo/server-plugin-landing-page-graphql-playground": "^4.0.0",
"@polkadot/util-crypto": "^11.0.1",
"@prisma/client": "^4.10.1",
"@react-email/components": "^0.0.7",
"@react-email/render": "^0.0.7",
"@sendgrid/mail": "^7.7.0",
"apollo-server": "^3.11.1",
"graphql": "^16.6.0",
Expand All @@ -42,6 +46,8 @@
"nexus-prisma": "^1.0.4",
"node-cron": "^3.0.2",
"npmlog": "^7.0.1",
"react-email": "^1.9.4",
"ts-pattern": "^4.3.0",
"yup": "^0.32.9"
},
"devDependencies": {
Expand Down Expand Up @@ -70,6 +76,7 @@
"nodemon": "^2.0.21",
"prettier": "^2.4.1",
"prisma": "^4.10.1",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "^3.14.2",
"tscpaths": "^0.0.9",
Expand Down
17 changes: 14 additions & 3 deletions packages/server/src/auth/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { verifySignature } from '@/auth/model/signature'
import { createAuthToken, createEmailToken } from '@/auth/model/token'
import { Context } from '@/common/api'
import { PIONEER_URL } from '@/common/config'
import { configEmailProvider } from '@/common/utils/email'
import { renderPioneerEmail } from '@/common/email-templates/pioneer-email'
import { createEmailProvider } from '@/common/utils/email'

const emailProvider = createEmailProvider()

interface SignInArgs {
memberId: number
Expand Down Expand Up @@ -61,10 +64,18 @@ export const signup = mutationField('signup', {
const token = createEmailToken(pick(args as Required<SignUpArgs>, 'email', 'memberId'))
const verificationUrl = `${req?.headers.referer ?? PIONEER_URL}/#/?verify-email=${token}`

await configEmailProvider()({
await emailProvider.sendEmail({
to: args.email,
subject: 'Confirm your email for Pioneer',
text: `Token:${token}\nWith link to :${verificationUrl}`,
html: renderPioneerEmail({
memberHandle: args.name,
summary: 'Confirm your email for Pioneer',
text: 'Please use the link below to confirm your email address for Pioneer notifications',
button: {
label: 'Confirm email',
href: verificationUrl,
},
}),
})
}

Expand Down
9 changes: 5 additions & 4 deletions packages/server/src/common/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { config } from 'dotenv'
import { pick } from 'lodash'
import log from 'npmlog'

config()
Expand All @@ -12,16 +11,18 @@ const {
PIONEER_URL = 'https://pioneerapp.xyz',
STARTING_BLOCK: _STARTING_BLOCK,
EMAIL_SENDER,
SENDGRID_API_KEY,
MAILGUN_API_KEY,
MAILGUN_DOMAIN,
INITIAL_MEMBERSHIPS: _INITIAL_MEMBERSHIPS,
} = process.env as { [k: string]: string }

log.level = APP_LOG_LEVEL

const emailProvidersConfig = ['SENDGRID_API_KEY', 'MAILGUN_CONFIG'] as const
const emailProvider = pick(process.env, emailProvidersConfig)
const SENDGRID_CONFIG = SENDGRID_API_KEY ? { apiKey: SENDGRID_API_KEY } : null
const MAILGUN_CONFIG = MAILGUN_API_KEY && MAILGUN_DOMAIN ? { apiKey: MAILGUN_API_KEY, domain: MAILGUN_DOMAIN } : null

export { PORT, APP_SECRET_KEY, QUERY_NODE_ENDPOINT, PIONEER_URL, EMAIL_SENDER, emailProvider, MAILGUN_DOMAIN }
export { PORT, APP_SECRET_KEY, QUERY_NODE_ENDPOINT, PIONEER_URL, EMAIL_SENDER, SENDGRID_CONFIG, MAILGUN_CONFIG }

export const STARTING_BLOCK = Number(_STARTING_BLOCK ?? 0)

Expand Down
1 change: 1 addition & 0 deletions packages/server/src/common/email-templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
To see live previews of emails, use `yarn workspace server dev:emails`.
196 changes: 196 additions & 0 deletions packages/server/src/common/email-templates/pioneer-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { Body } from '@react-email/body'
import { Button } from '@react-email/button'
import { Container } from '@react-email/container'
import { Head } from '@react-email/head'
import { Heading } from '@react-email/heading'
import { Html } from '@react-email/html'
import { Img } from '@react-email/img'
import { Link } from '@react-email/link'
import { Preview } from '@react-email/preview'
import { render } from '@react-email/render'
import { Section } from '@react-email/section'
import { Text } from '@react-email/text'
import * as React from 'react'

import { PIONEER_URL } from '../config'

interface PioneerEmailTemplateProps {
memberHandle: string
text: string
summary: string
button: {
href: string
label: string
}
}

const PioneerEmailTemplate = ({
memberHandle = 'bob',
text = 'New council election has just started. Follow the link below to announce your candidacy.',
summary,
// button default value only for email development preview
button = {
href: `${PIONEER_URL}/#/election`,
label: 'See on Pioneer',
},
}: PioneerEmailTemplateProps) => (
kdembler marked this conversation as resolved.
Show resolved Hide resolved
<Html>
<Head />
<Preview>{summary}</Preview>
<Body style={bodyStyle}>
<Container style={containerStyle}>
<Section style={logoSectionStyle}>
<Img
src="https://github.com/Joystream/design/blob/master/logo/pioneer/horizontal/monochromatic/light/cropped/png/[email protected]?raw=true"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kdembler I'm not sure if it's ideal for emails but even with this bigger logo after optimizing the svg with Inkscape it weigh 4.2kb (url encoded). WDYT of embedding it in the email maybe even as a data-url for email client compatibility ?

Suggested change
src="https://github.com/Joystream/design/blob/master/logo/pioneer/horizontal/monochromatic/light/cropped/png/[email protected]?raw=true"
src="%3Csvg width='364' height='88' fill='white' version='1.1' viewBox='0 0 364 88' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m50 8c-8.18 0-14 5.82-14 13v67h-8v-67c0-11.6 9.4-21 21-21 21.5 0 39 17.5 39 39 0 21.2-16.9 38.5-38 39l-1.78-7.99h0.778c17.1 0 31-13.9 31-31s-13.9-31-31-31z'/%3E%3Cpath d='m46.9 64h2.11c13.8 0 25-11.2 25-25s-11.2-25-25-25c-3.87 0-7 3.13-7 7v1h7c9.39 0 17 7.61 17 17s-7.61 17-17 17h-3.89l1.78 8z'/%3E%3Cpath d='m43.8 50h5.22c6.08 0 11-4.92 11-11s-4.92-11-11-11h-7v8h7c1.66 0 3 1.34 3 3s-1.34 3-3 3h-7l1.78 8z'/%3E%3Cpath d='m22.1 18.4c-0.0811 0.852-0.123 1.72-0.123 2.59v67h-8v-53c0-6.75 3.18-12.7 8.12-16.6z'/%3E%3Cpath d='m0 49c0-6.75 3.18-12.7 8.12-16.6-0.0811 0.852-0.123 1.72-0.123 2.59v53h-8v-39z'/%3E%3Cpath d='m155 65.8v-40h8.01v40h-8.01z'/%3E%3Cpath d='m155 11.4v8.02h8.01v-8.02h-8.01z'/%3E%3Cpath d='m146 59.9c3.45-3.77 5.18-8.66 5.18-14.7 6e-3 -6.01-1.73-10.9-5.22-14.6-1.65-1.82-3.67-3.26-5.92-4.24s-4.69-1.44-7.15-1.39c-2.29-0.0284-4.55 0.462-6.63 1.43-1.61 0.679-3.08 1.64-4.35 2.83l-1.21 1.48h-0.387v-4.97h-8v52h8v-18.4h0.387c0.152 0.223 0.317 0.438 0.493 0.642 0.466 0.527 0.963 1.03 1.49 1.5 0.741 0.7 1.55 1.32 2.42 1.86 2.37 1.35 5.04 2.07 7.77 2.1 2.46 0.0583 4.9-0.408 7.16-1.37s4.29-2.39 5.95-4.2v-0.0088zm-5.37-24.1c2.18 2.35 3.27 5.5 3.27 9.47 0 3.97-1.09 7.12-3.27 9.48-2.27 2.25-5.34 3.51-8.53 3.51s-6.26-1.26-8.53-3.51c-2.19-2.35-3.29-5.51-3.28-9.48 6e-3 -3.97 1.1-7.13 3.28-9.47 2.27-2.25 5.34-3.52 8.53-3.52s6.26 1.27 8.53 3.52z'/%3E%3Cpath d='m169 53c0.996 2.48 2.49 4.74 4.39 6.62 2.86 2.8 6.48 4.69 10.4 5.44 3.93 0.754 7.99 0.334 11.7-1.21 3.69-1.54 6.84-4.13 9.07-7.46 2.22-3.32 3.42-7.23 3.44-11.2 0.056-2.69-0.425-5.36-1.41-7.86-0.99-2.5-2.47-4.78-4.35-6.7-1.88-1.92-4.12-3.45-6.6-4.49-2.48-1.04-5.14-1.58-7.83-1.58-2.69 0-5.35 0.536-7.82 1.58-2.48 1.04-4.72 2.57-6.6 4.49-1.88 1.92-3.36 4.2-4.35 6.7-0.989 2.5-1.47 5.17-1.41 7.86-0.07 2.67 0.407 5.34 1.4 7.82zm6.98-7.82c-6e-3 -3.97 1.09-7.12 3.28-9.47 2.27-2.25 5.33-3.52 8.53-3.52 3.2 0 6.26 1.27 8.53 3.52 2.18 2.34 3.27 5.5 3.27 9.47 0 3.97-1.09 7.13-3.27 9.48-2.27 2.25-5.34 3.51-8.53 3.51-3.2 0-6.26-1.26-8.53-3.51-2.18-2.35-3.28-5.51-3.28-9.48z'/%3E%3Cpath d='m258 59.7c-1.88-1.89-3.35-4.14-4.33-6.62-0.983-2.48-1.46-5.14-1.38-7.82-0.056-2.69 0.419-5.36 1.4-7.86s2.44-4.78 4.29-6.7 4.07-3.45 6.52-4.49c2.45-1.04 5.07-1.58 7.73-1.58s5.28 0.536 7.73 1.58c2.45 1.04 4.66 2.57 6.52 4.49s3.32 4.2 4.29 6.7 1.45 5.17 1.4 7.86c-5e-3 0.887-0.067 1.77-0.184 2.64h-31.2c0.389 2.75 1.41 5.03 3.06 6.84 2.24 2.25 5.27 3.51 8.42 3.51s6.18-1.26 8.42-3.51c0.561-0.61 1.05-1.28 1.46-2h8.6c-0.518 1.31-1.17 2.56-1.95 3.74-2.2 3.32-5.31 5.91-8.96 7.46s-7.66 1.96-11.5 1.21c-3.88-0.754-7.45-2.65-10.3-5.44zm5.8-23.9c-1.25 1.35-2.13 2.97-2.66 4.85h22.2c-0.526-1.89-1.41-3.5-2.65-4.85-2.24-2.25-5.27-3.52-8.42-3.52s-6.19 1.27-8.42 3.52z'/%3E%3Cpath d='m297 53c0.983 2.48 2.46 4.74 4.33 6.62 2.82 2.8 6.39 4.69 10.3 5.44 3.88 0.754 7.89 0.334 11.5-1.21s6.76-4.13 8.96-7.46c0.781-1.18 1.43-2.44 1.95-3.74h-8.6c-0.415 0.72-0.902 1.38-1.46 2-2.24 2.25-5.27 3.51-8.42 3.51s-6.18-1.26-8.42-3.51c-1.65-1.8-2.67-4.08-3.06-6.84h31.2c0.117-0.873 0.179-1.76 0.184-2.64 0.055-2.69-0.42-5.36-1.4-7.86-0.977-2.5-2.44-4.78-4.29-6.7s-4.07-3.45-6.52-4.49c-2.45-1.04-5.07-1.58-7.73-1.58s-5.28 0.536-7.73 1.58c-2.45 1.04-4.66 2.57-6.52 4.49s-3.32 4.2-4.29 6.7c-0.977 2.5-1.45 5.17-1.4 7.86-0.07 2.67 0.402 5.34 1.38 7.82zm7.47-12.4c0.527-1.88 1.41-3.5 2.66-4.85 2.24-2.25 5.27-3.52 8.42-3.52s6.19 1.27 8.42 3.52c1.24 1.35 2.12 2.96 2.65 4.85h-22.2z'/%3E%3Cpath d='m348 33-0.088 32.7h-8l0.079-34.2c3e-3 -1.54 0.615-3.01 1.7-4.09 1.09-1.09 2.56-1.7 4.1-1.7h17.8v7.26h-15.6z'/%3E%3Cpath d='m213 25.7v40h7.91v-22.5c-0.095-2.92 0.942-5.75 2.9-7.92 0.878-0.999 1.96-1.8 3.18-2.34 1.22-0.541 2.53-0.812 3.86-0.796 5.58 0 8.38 3.19 8.38 9.56v24h8v-24.4c-0.018-5.18-1.28-9.19-3.79-12v-2e-3c-0.595-0.624-1.24-1.2-1.91-1.74-1.1-0.823-2.34-1.45-3.66-1.84-1.64-0.524-3.35-0.782-5.07-0.766-1.49-0.0074-2.98 0.212-4.4 0.651-1.14 0.328-2.23 0.816-3.24 1.45-0.789 0.542-1.52 1.17-2.16 1.87-0.456 0.454-0.866 0.952-1.22 1.49-0.149 0.282-0.281 0.502-0.378 0.651h-0.387v-5.35h-8z'/%3E%3C/svg%3E%0A"

This way we don't have to rely on GH servers and also they're email clients which require users to allow the fetching of remote assets.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea but apparently the support for SVGs in emails is low, especially for embedded ones:
https://www.caniemail.com/features/image-svg/
https://www.caniemail.com/features/html-svg/
It seems it's the same case for base64 encoded PNGs:

Some bad news about base64 encoded images:

They are totally blocked by Outlook.
They are not displayed by most webmail services (especially if you use more than one inside the message).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for having a look. And I am bookmarking caniemail !

alt="Pioneer's Logo"
style={pioneerLogoStyle}
/>
</Section>
<Section style={mainSectionStyle}>
<Heading style={h1Style}>Hi {memberHandle},</Heading>
<Text style={textStyle}>{text}</Text>
{button && (
<Button href={button.href} style={ctaStyle} pX={16} pY={12}>
{button.label}
</Button>
)}
</Section>
<Section style={footerSectionStyle}>
<Img
src="https://github.com/Joystream/design/blob/master/logo/pioneer/icon/color/light/cropped/png/[email protected]?raw=true"
alt="Small Pioneer logo"
style={footerLogoStyle}
/>
<Text style={bigLinksStyle}>
<Link href={PIONEER_URL} style={bigLinkStyle}>
pioneerapp.xyz
</Link>
<span style={divisorStyle}>|</span>
<Link href="https://joystream.org" style={bigLinkStyle}>
joystream.org
</Link>
</Text>
<Text style={pioneerLinksStyle}>
<Link href={`${PIONEER_URL}/#/election`} style={pioneerLinkStyle}>
Council
</Link>
<Link href={`${PIONEER_URL}/#/proposals/current`} style={middlePioneerLinkStyle}>
Proposals
</Link>
<Link href={`${PIONEER_URL}/#/forum`} style={pioneerLinkStyle}>
Forum
</Link>
</Text>
// TODO: include link to notifications settings
</Section>
</Container>
</Body>
</Html>
)

export const renderPioneerEmail = (props: PioneerEmailTemplateProps) => render(PioneerEmailTemplate(props))

const bodyStyle = {
backgroundColor: '#ffffff',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
}

const containerStyle = {
margin: '0 auto',
maxWidth: '600px',
border: '1px solid #eee',
}

const logoSectionStyle: React.CSSProperties = {
backgroundColor: '#4038FF',
padding: '32px 0',
}

const pioneerLogoStyle = {
margin: '0 auto',
width: '194px',
}

const mainSectionStyle = {
padding: '40px 40px',
}

const h1Style = {
color: '#000',
fontSize: '24px',
lineHeight: '32px',
fontWeight: '700',
marginTop: '0px',
marginBottom: '8px',
}

const textStyle = {
color: '#000',
fontSize: '16px',
lineHeight: '24px',
fontWeight: '400',
margin: '0',
}

const ctaStyle = {
backgroundColor: '#4038FF',
color: '#fff',
fontSize: '14px',
lineHeight: '20px',
fontWeight: '500',
marginTop: '24px',
padding: '12px 16px',
borderRadius: '2px',
}

const footerSectionStyle = {
backgroundColor: '#000',
padding: '24px 16px 20px 16px',
}

const footerLogoStyle = {
margin: '0 auto',
width: '24px',
marginBottom: '10px',
}

const bigLinksStyle: React.CSSProperties = {
textAlign: 'center',
margin: '0',
marginBottom: '2px',
lineHeight: '20px',
}

const bigLinkStyle = {
fontSize: '14px',
lineHeight: '20px',
fontWeight: '700',
color: '#F9FAFC',
}

const divisorStyle = {
fontSize: '14px',
lineHeight: '20px',
fontWeight: '700',
color: '#5D6B80',
margin: '0 8px',
}

const pioneerLinksStyle: React.CSSProperties = {
textAlign: 'center',
margin: '0',
lineHeight: '14px',
}

const pioneerLinkStyle = {
fontSize: '10px',
lineHeight: '14px',
fontWeight: '400',
color: '#F9FAFC',
}

const middlePioneerLinkStyle = {
...pioneerLinkStyle,
margin: '0 16px',
}
Loading
Loading