mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-08-29 11:56:04 +08:00
Adds ability to inject a preheader into emails
This commit is contained in:
parent
c453448985
commit
d26a1e086d
8 changed files with 144 additions and 12 deletions
|
@ -16,11 +16,17 @@ export default class EmailChannel {
|
|||
async send(template: EmailTemplate, variables: Variables) {
|
||||
if (!variables.user.email) throw new Error('Unable to send a text message to a user with no email.')
|
||||
|
||||
// TODO: Explore caching the Handlebars template
|
||||
// before passing in variables for better performance
|
||||
const compiled = template.compile(variables)
|
||||
const email = {
|
||||
...compiled,
|
||||
to: variables.user.email,
|
||||
html: Wrap(compiled.html, variables), // Add link and open tracking
|
||||
html: Wrap({
|
||||
html: compiled.html,
|
||||
preheader: compiled.preheader,
|
||||
variables,
|
||||
}), // Add link and open tracking
|
||||
}
|
||||
await this.provider.send(email)
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ export const encodedLinkToParts = async (link: string | URL): Promise<TrackedLin
|
|||
return parts
|
||||
}
|
||||
|
||||
export const clickWrapHTML = (html: string, params: TrackedLinkParams) => {
|
||||
export const clickWrapHtml = (html: string, params: TrackedLinkParams) => {
|
||||
const regex = /href\s*=\s*(['"])(https?:\/\/.+?)\1/gi
|
||||
let link
|
||||
|
||||
|
@ -77,14 +77,31 @@ export const clickWrapHTML = (html: string, params: TrackedLinkParams) => {
|
|||
}
|
||||
|
||||
export const openWrapHtml = (html: string, params: TrackedLinkParams) => {
|
||||
const bodyTag = '</body>'
|
||||
const link = paramsToEncodedLink({ ...params, path: 'o' })
|
||||
const imageHtml = `<img border="0" width="1" height="1" alt="" src="${link}" />`
|
||||
const hasBody = html.includes(bodyTag)
|
||||
if (hasBody) {
|
||||
html = html.replace(bodyTag, (imageHtml + bodyTag))
|
||||
return injectInBody(html, imageHtml, 'end')
|
||||
}
|
||||
|
||||
export const preheaderWrapHtml = (html: string, preheader: string) => {
|
||||
const preheaderHtml = `<span style="color:transparent;visibility:hidden;display:none;opacity:0;height:0;width:0;font-size:0">${preheader}</span>`
|
||||
return injectInBody(html, preheaderHtml, 'start')
|
||||
}
|
||||
|
||||
export const injectInBody = (html: string, injection: string, placement: 'start' | 'end') => {
|
||||
if (placement === 'end') {
|
||||
const bodyTag = '</body'
|
||||
html = html.includes(bodyTag)
|
||||
? html = html.replace(bodyTag, (injection + bodyTag))
|
||||
: html += injection
|
||||
} else {
|
||||
html += imageHtml
|
||||
const regex = /<body(?:.|\n)*?>/
|
||||
const match = html.match(regex)
|
||||
if (match) {
|
||||
const offset = match.index! + match[0].length
|
||||
html = html.substring(0, offset) + injection + html.substring(offset)
|
||||
} else {
|
||||
html = injection + html
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export interface CompiledEmail {
|
|||
bcc?: string
|
||||
reply_to?: string
|
||||
subject: string
|
||||
preheader?: string
|
||||
text: string
|
||||
html: string
|
||||
}
|
||||
|
@ -61,6 +62,7 @@ export class EmailTemplate extends Template {
|
|||
bcc?: string
|
||||
reply_to?: string
|
||||
subject!: string
|
||||
preheader?: string
|
||||
text!: string
|
||||
html!: string
|
||||
|
||||
|
@ -72,6 +74,7 @@ export class EmailTemplate extends Template {
|
|||
this.bcc = json?.data.bcc
|
||||
this.reply_to = json?.data.reply_to
|
||||
this.subject = json?.data.subject ?? ''
|
||||
this.preheader = json?.data.preheader
|
||||
this.text = json?.data.text ?? ''
|
||||
this.html = json?.data.html ?? ''
|
||||
}
|
||||
|
@ -83,6 +86,7 @@ export class EmailTemplate extends Template {
|
|||
html: Render(this.html, variables),
|
||||
text: Render(this.text, variables),
|
||||
}
|
||||
if (this.preheader) email.preheader = Render(this.preheader, variables)
|
||||
if (this.reply_to) email.reply_to = Render(this.reply_to, variables)
|
||||
if (this.cc) email.cc = Render(this.cc, variables)
|
||||
if (this.bcc) email.bcc = Render(this.bcc, variables)
|
||||
|
|
76
apps/platform/src/render/__spec__/LinkService.spec.ts
Normal file
76
apps/platform/src/render/__spec__/LinkService.spec.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { Admin } from '../../auth/Admin'
|
||||
import { createProject } from '../../projects/ProjectService'
|
||||
import { createUser } from '../../users/UserRepository'
|
||||
import { uuid } from '../../utilities'
|
||||
import { clickWrapHtml, encodedLinkToParts, openWrapHtml, paramsToEncodedLink, preheaderWrapHtml } from '../LinkService'
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('LinkService', () => {
|
||||
describe('encodedLinkToParts', () => {
|
||||
test('a properly encoded link decodes to parts', async () => {
|
||||
const adminId = await Admin.insert({
|
||||
first_name: uuid(),
|
||||
last_name: uuid(),
|
||||
email: `${uuid()}@test.com`,
|
||||
})
|
||||
const project = await createProject(adminId, { name: uuid() })
|
||||
const user = await createUser(project.id, {
|
||||
anonymous_id: uuid(),
|
||||
external_id: uuid(),
|
||||
})
|
||||
|
||||
const redirect = 'http://test.com'
|
||||
const link = paramsToEncodedLink({
|
||||
userId: user.id,
|
||||
campaignId: 1,
|
||||
path: 'c',
|
||||
redirect,
|
||||
})
|
||||
|
||||
const parts = await encodedLinkToParts(link)
|
||||
expect(parts.user?.id).toEqual(user.id)
|
||||
expect(parts.redirect).toEqual(redirect)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clickWrapHtml', () => {
|
||||
test('links are wrapped', async () => {
|
||||
const html = 'This is some html <a href="https://test.com">Test Link</a>'
|
||||
const output = clickWrapHtml(html, { userId: 1, campaignId: 2 })
|
||||
expect(output).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('openWrapHtml', () => {
|
||||
test('open tracking image is added to end of body', async () => {
|
||||
const html = '<html><body>This is some html</body></html>'
|
||||
const output = openWrapHtml(html, { userId: 3, campaignId: 4 })
|
||||
expect(output).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('preheaderWrapHtml', () => {
|
||||
test('simple html injects preheader', async () => {
|
||||
const preheader = 'This is some preheader'
|
||||
const html = '<html><body>This is some html</body></html>'
|
||||
const output = preheaderWrapHtml(html, preheader)
|
||||
expect(output).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('complex html injects preheader', async () => {
|
||||
const preheader = 'This is some preheader'
|
||||
const html = `<html>
|
||||
<body
|
||||
style="color:#000"
|
||||
class="body-class">
|
||||
This is some html
|
||||
</body>
|
||||
</html>`
|
||||
const output = preheaderWrapHtml(html, preheader)
|
||||
expect(output).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LinkService clickWrapHtml links are wrapped 1`] = `"This is some html <a href=\\"https://twochris.com/c?u=M8LRMWZ645&c=1MLXN7R524&r=https%253A%252F%252Ftest.com\\">Test Link</a>"`;
|
||||
|
||||
exports[`LinkService openWrapHtml open tracking image is added to end of body 1`] = `"<html><body>This is some html<img border=\\"0\\" width=\\"1\\" height=\\"1\\" alt=\\"\\" src=\\"https://twochris.com/o?u=P9KR98X8L4&c=Y3QRV4XL74\\" /></body></html>"`;
|
||||
|
||||
exports[`LinkService preheaderWrapHtml complex html injects preheader 1`] = `
|
||||
"<html>
|
||||
<body
|
||||
style=\\"color:#000\\"
|
||||
class=\\"body-class\\"><span style=\\"color:transparent;visibility:hidden;display:none;opacity:0;height:0;width:0;font-size:0\\">This is some preheader</span>
|
||||
This is some html
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`LinkService preheaderWrapHtml simple html injects preheader 1`] = `"<html><body><span style=\\"color:transparent;visibility:hidden;display:none;opacity:0;height:0;width:0;font-size:0\\">This is some preheader</span>This is some html</body></html>"`;
|
|
@ -6,7 +6,7 @@ import * as UrlHelpers from './Helpers/Url'
|
|||
import * as ArrayHelpers from './Helpers/Array'
|
||||
import { User } from '../users/User'
|
||||
import { unsubscribeEmailLink } from '../subscriptions/SubscriptionService'
|
||||
import { clickWrapHTML, openWrapHtml } from './LinkService'
|
||||
import { clickWrapHtml, openWrapHtml, preheaderWrapHtml } from './LinkService'
|
||||
|
||||
export interface RenderContext {
|
||||
template_id: number
|
||||
|
@ -42,10 +42,16 @@ export const Compile = (template: string, context: Record<string, any> = {}) =>
|
|||
return Handlebars.compile(template)(context)
|
||||
}
|
||||
|
||||
export const Wrap = (html: string, { user, context }: Variables) => {
|
||||
interface WrapParams {
|
||||
html: string
|
||||
preheader?: string
|
||||
variables: Variables
|
||||
}
|
||||
export const Wrap = ({ html, preheader, variables: { user, context } }: WrapParams) => {
|
||||
const trackingParams = { userId: user.id, campaignId: context.campaign_id }
|
||||
html = clickWrapHTML(html, trackingParams)
|
||||
html = clickWrapHtml(html, trackingParams)
|
||||
html = openWrapHtml(html, trackingParams)
|
||||
if (preheader) html = preheaderWrapHtml(html, preheader)
|
||||
return html
|
||||
}
|
||||
|
||||
|
|
|
@ -283,6 +283,7 @@ export interface EmailTemplateData {
|
|||
bcc?: string
|
||||
reply_to?: string
|
||||
subject: string
|
||||
preheader?: string
|
||||
text: string
|
||||
html: string
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ const EmailTable = ({ data }: { data: EmailTemplateData }) => <InfoTable rows={{
|
|||
CC: data.cc,
|
||||
BCC: data.bcc,
|
||||
Subject: data.subject,
|
||||
Preheader: data.preheader,
|
||||
}} />
|
||||
|
||||
const EmailForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> }) => <>
|
||||
|
@ -27,10 +28,15 @@ const EmailForm = ({ form }: { form: UseFormReturn<TemplateUpdateParams, any> })
|
|||
label="Subject"
|
||||
textarea
|
||||
required />
|
||||
<TextField
|
||||
form={form}
|
||||
name="data.preheader"
|
||||
label="Preheader"
|
||||
textarea />
|
||||
<TextField form={form} name="data.from" label="From Email" required />
|
||||
<TextField form={form} name="data.reply_to" label="Reply To" />
|
||||
<TextField form={form} name="data.cc" label="CC" />
|
||||
<TextField form={form} name="data.cc" label="BCC" />
|
||||
<TextField form={form} name="data.bcc" label="BCC" />
|
||||
</>
|
||||
|
||||
const TextTable = ({ data: { text } }: { data: TextTemplateData }) => {
|
||||
|
@ -96,7 +102,6 @@ export default function TemplateDetail({ template }: TemplateDetailProps) {
|
|||
const newCampaign = { ...campaign }
|
||||
newCampaign.templates = campaign.templates.map(obj => obj.id === id ? value : obj)
|
||||
setCampaign(newCampaign)
|
||||
console.log('new campaign', newCampaign)
|
||||
setIsEditOpen(false)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue