Skip to content

Commit 746b413

Browse files
[PM-21741] MJML welcome emails (#6549)
feat: Implement welcome email using MJML templating - Implement MJML templates for welcome emails (individual, family, org) - Create reusable MJML components (mj-bw-icon-row, mj-bw-learn-more-footer) - Update documentation for MJML development process
1 parent b2543b5 commit 746b413

File tree

14 files changed

+580
-262
lines changed

14 files changed

+580
-262
lines changed

src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs

Lines changed: 195 additions & 188 deletions
Large diffs are not rendered by default.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"packages": [
3-
"components/mj-bw-hero"
3+
"components/mj-bw-hero",
4+
"components/mj-bw-icon-row",
5+
"components/mj-bw-learn-more-footer"
46
]
57
}

src/Core/MailTemplates/Mjml/README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ When using MJML templating you can use the above [commands](#building-mjml-files
4545

4646
Not all MJML tags have the same attributes, it is highly recommended to review the documentation on the official MJML website to understand the usages of each of the tags.
4747

48-
### Recommended development
48+
### Recommended development - IMailService
4949

5050
#### Mjml email template development
5151

@@ -58,11 +58,17 @@ Not all MJML tags have the same attributes, it is highly recommended to review t
5858

5959
After the email is developed from the [initial step](#mjml-email-template-development) make sure the email `{{variables}}` are populated properly by running it through an `IMailService` implementation.
6060

61-
1. run `npm run build:minify`
61+
1. run `npm run build:hbs`
6262
2. copy built `*.html.hbs` files from the build directory to a location the mail service can consume them
63+
1. all files in the `Core/MailTemplates/Mjml/out` directory can be copied to the `src/Core/MailTemplates/Handlebars/MJML` directory. If a shared component is modified it is important to copy and overwrite all files in that directory to capture
64+
changes in the `*.html.hbs`.
6365
3. run code that will send the email
6466

65-
The minified `html.hbs` artifacts are deliverables and must be placed into the correct `src/Core/MailTemplates/Handlebars/` directories in order to be used by `IMailService` implementations.
67+
The minified `html.hbs` artifacts are deliverables and must be placed into the correct `src/Core/MailTemplates/Handlebars/` directories in order to be used by `IMailService` implementations, see 2.1 above.
68+
69+
### Recommended development - IMailer
70+
71+
TBD - PM-26475
6672

6773
### Custom tags
6874

@@ -110,3 +116,8 @@ You are also able to reference other more static MJML templates in your MJML fil
110116
<mj-include path="../../components/learn-more-footer.mjml" />
111117
</mj-wrapper>
112118
```
119+
120+
#### `head.mjml`
121+
Currently we include the `head.mjml` file in all MJML templates as it contains shared styling and formatting that ensures consistency across all email implementations.
122+
123+
In the future we may deviate from this practice to support different layouts. At that time we will modify the docs with direction.

src/Core/MailTemplates/Mjml/components/head.mjml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,3 @@
2222
border-radius: 3px;
2323
}
2424
</mj-style>
25-
26-
<!-- Responsive icon visibility -->
27-
<mj-style>
28-
@media only screen and
29-
(max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } }
30-
</mj-style>

src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/Core/MailTemplates/Mjml/components/mj-bw-hero.js

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,19 @@ class MjBwHero extends BodyComponent {
1818

1919
static defaultAttributes = {};
2020

21+
componentHeadStyle = breakpoint => {
22+
return `
23+
@media only screen and (max-width:${breakpoint}) {
24+
.mj-bw-hero-responsive-img {
25+
display: none !important;
26+
}
27+
}
28+
`
29+
}
30+
2131
render() {
22-
if (this.getAttribute("button-text") && this.getAttribute("button-url")) {
23-
return this.renderMJML(`
24-
<mj-section
25-
full-width="full-width"
26-
background-color="#175ddc"
27-
border-radius="4px 4px 0px 0px"
28-
>
29-
<mj-column width="70%">
30-
<mj-image
31-
align="left"
32-
src="https://bitwarden.com/images/logo-horizontal-white.png"
33-
width="150px"
34-
height="30px"
35-
></mj-image>
36-
<mj-text color="#fff" padding-top="0" padding-bottom="0">
37-
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
38-
${this.getAttribute("title")}
39-
</h1>
40-
</mj-text>
41-
<mj-button
32+
const buttonElement = this.getAttribute("button-text") && this.getAttribute("button-url") ?
33+
`<mj-button
4234
href="${this.getAttribute("button-url")}"
4335
background-color="#fff"
4436
color="#1A41AC"
@@ -47,22 +39,16 @@ class MjBwHero extends BodyComponent {
4739
>
4840
${this.getAttribute("button-text")}
4941
</mj-button
50-
>
51-
</mj-column>
52-
<mj-column width="30%" vertical-align="bottom">
53-
<mj-image
54-
src="${this.getAttribute("img-src")}"
55-
alt=""
56-
width="140px"
57-
height="140px"
58-
padding="0px"
59-
css-class="hide-small-img"
60-
/>
61-
</mj-column>
62-
</mj-section>
63-
`);
64-
} else {
65-
return this.renderMJML(`
42+
>` : "";
43+
const subTitleElement = this.getAttribute("sub-title") ?
44+
`<mj-text color="#fff" padding-top="0" padding-bottom="0">
45+
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
46+
${this.getAttribute("sub-title")}
47+
</h2>
48+
</mj-text>` : "";
49+
50+
return this.renderMJML(
51+
`
6652
<mj-section
6753
full-width="full-width"
6854
background-color="#175ddc"
@@ -79,21 +65,25 @@ class MjBwHero extends BodyComponent {
7965
<h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
8066
${this.getAttribute("title")}
8167
</h1>
82-
</mj-text>
68+
` +
69+
subTitleElement +
70+
`
71+
</mj-text>` +
72+
buttonElement +
73+
`
8374
</mj-column>
8475
<mj-column width="30%" vertical-align="bottom">
8576
<mj-image
8677
src="${this.getAttribute("img-src")}"
8778
alt=""
88-
width="140px"
89-
height="140px"
79+
width="155px"
9080
padding="0px"
91-
css-class="hide-small-img"
92-
/>
81+
css-class="mj-bw-hero-responsive-img"
82+
/>
9383
</mj-column>
9484
</mj-section>
95-
`);
96-
}
85+
`,
86+
);
9787
}
9888
}
9989

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const { BodyComponent } = require("mjml-core");
2+
class MjBwIconRow extends BodyComponent {
3+
static dependencies = {
4+
"mj-column": ["mj-bw-icon-row"],
5+
"mj-wrapper": ["mj-bw-icon-row"],
6+
"mj-bw-icon-row": [],
7+
};
8+
9+
static allowedAttributes = {
10+
"icon-src": "string",
11+
"icon-alt": "string",
12+
"head-url-text": "string",
13+
"head-url": "string",
14+
text: "string",
15+
"foot-url-text": "string",
16+
"foot-url": "string",
17+
};
18+
19+
static defaultAttributes = {};
20+
21+
componentHeadStyle = (breakpoint) => {
22+
return `
23+
@media only screen and (max-width:${breakpoint}): {
24+
".mj-bw-icon-row-text": {
25+
padding-left: "5px !important",
26+
line-height: "20px",
27+
},
28+
".mj-bw-icon-row": {
29+
padding: "10px 15px",
30+
width: "fit-content !important",
31+
}
32+
}
33+
`;
34+
};
35+
36+
render() {
37+
const headAnchorElement =
38+
this.getAttribute("head-url-text") && this.getAttribute("head-url")
39+
? `<a href="${this.getAttribute("head-url")}" class="link">
40+
${this.getAttribute("head-url-text")}
41+
<span style="text-decoration: none">
42+
<img src="https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png"
43+
alt="External Link Icon"
44+
width="16px"
45+
style="vertical-align: middle;"
46+
/>
47+
</span>
48+
</a>`
49+
: "";
50+
51+
const footAnchorElement =
52+
this.getAttribute("foot-url-text") && this.getAttribute("foot-url")
53+
? `<a href="${this.getAttribute("foot-url")}" class="link">
54+
${this.getAttribute("foot-url-text")}
55+
<span style="text-decoration: none">
56+
<img src="https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png"
57+
alt="External Link Icon"
58+
width="16px"
59+
style="vertical-align: middle;"
60+
/>
61+
</span>
62+
</a>`
63+
: "";
64+
65+
return this.renderMJML(
66+
`
67+
<mj-section background-color="#fff" padding="10px 10px 10px 10px">
68+
<mj-group css-class="mj-bw-icon-row">
69+
<mj-column width="15%" vertical-align="top">
70+
<mj-image
71+
src="${this.getAttribute("icon-src")}"
72+
alt="${this.getAttribute("icon-alt")}"
73+
width="48px"
74+
padding="0px"
75+
border-radius="8px"
76+
/>
77+
</mj-column>
78+
<mj-column width="85%" vertical-align="top">
79+
<mj-text css-class="mj-bw-icon-row-text" padding="5px 10px 0px 10px">
80+
` +
81+
headAnchorElement +
82+
`
83+
</mj-text>
84+
<mj-text css-class="mj-bw-icon-row-text" padding="5px 10px 0px 10px">
85+
${this.getAttribute("text")}
86+
</mj-text>
87+
<mj-text css-class="mj-bw-icon-row-text" padding="5px 10px 0px 10px">
88+
` +
89+
footAnchorElement +
90+
`
91+
</mj-text>
92+
</mj-column>
93+
</mj-group>
94+
</mj-section>
95+
`,
96+
);
97+
}
98+
}
99+
100+
module.exports = MjBwIconRow;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const { BodyComponent } = require("mjml-core");
2+
class MjBwLearnMoreFooter extends BodyComponent {
3+
static dependencies = {
4+
// Tell the validator which tags are allowed as our component's parent
5+
"mj-column": ["mj-bw-learn-more-footer"],
6+
"mj-wrapper": ["mj-bw-learn-more-footer"],
7+
// Tell the validator which tags are allowed as our component's children
8+
"mj-bw-learn-more-footer": [],
9+
};
10+
11+
static allowedAttributes = {};
12+
13+
static defaultAttributes = {};
14+
15+
componentHeadStyle = (breakpoint) => {
16+
return `
17+
@media only screen and (max-width:${breakpoint}) {
18+
.mj-bw-learn-more-footer-responsive-img {
19+
display: none !important;
20+
}
21+
}
22+
`;
23+
};
24+
25+
render() {
26+
return this.renderMJML(
27+
`
28+
<mj-section border-radius="0px 0px 4px 4px" background-color="#f6f6f6" padding="5px 10px 10px 10px">
29+
<mj-column width="70%">
30+
<mj-text line-height="24px">
31+
<p style="font-size: 18px; line-height: 28px; font-weight: bold;">
32+
Learn more about Bitwarden
33+
</p>
34+
Find user guides, product documentation, and videos on the
35+
<a href="https://bitwarden.com/help/" class="link"> Bitwarden Help Center</a>.
36+
</mj-text>
37+
</mj-column>
38+
<mj-column width="30%">
39+
<mj-image
40+
src="https://assets.bitwarden.com/email/v1/spot-community.png"
41+
css-class="mj-bw-learn-more-footer-responsive-img"
42+
width="94px"
43+
/>
44+
</mj-column>
45+
</mj-section>
46+
`,
47+
);
48+
}
49+
}
50+
51+
module.exports = MjBwLearnMoreFooter;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<mjml>
2+
<mj-head>
3+
<mj-include path="../../../components/head.mjml" />
4+
</mj-head>
5+
6+
<mj-body css-class="border-fix">
7+
<!-- Blue Header Section -->
8+
<mj-wrapper css-class="border-fix" padding="20px 20px 10px 20px">
9+
<mj-bw-hero
10+
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
11+
title="Welcome to Bitwarden!"
12+
sub-title="Let's get set up to autofill."
13+
/>
14+
</mj-wrapper>
15+
16+
<!-- Main Content -->
17+
<mj-wrapper padding="5px 20px 10px 20px">
18+
<mj-section background-color="#fff" padding="15px 10px 10px 10px">
19+
<mj-column>
20+
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
21+
An administrator from <b>{{OrganizationName}}</b> will approve you
22+
before you can share passwords. While you wait for approval, get
23+
started with Bitwarden Password Manager:
24+
</mj-text>
25+
</mj-column>
26+
</mj-section>
27+
<mj-bw-icon-row
28+
icon-src="https://assets.bitwarden.com/email/v1/icon-browser-extension.png"
29+
icon-alt="Browser Extension Icon"
30+
head-url-text="Get the browser extension"
31+
head-url="https://bitwarden.com/download/"
32+
text="With the Bitwarden extension, you can fill passwords with one click."
33+
/>
34+
<mj-bw-icon-row
35+
icon-src="https://assets.bitwarden.com/email/v1/icon-install.png"
36+
icon-alt="Install Icon"
37+
head-url-text="Add passwords to your vault"
38+
head-url="https://bitwarden.com/help/import-data/"
39+
text="Quickly transfer existing passwords to Bitwarden using the importer."
40+
/>
41+
<mj-bw-icon-row
42+
icon-src="https://assets.bitwarden.com/email/v1/icon-devices.png"
43+
icon-alt="Devices Icon"
44+
head-url-text="Download Bitwarden on all devices"
45+
head-url="https://bitwarden.com/download/"
46+
text="Take your passwords with you anywhere."
47+
/>
48+
<mj-section background-color="#fff" padding="0 20px 20px 20px">
49+
</mj-section>
50+
</mj-wrapper>
51+
52+
<!-- Learn More Section -->
53+
<mj-wrapper padding="5px 20px 10px 20px">
54+
<mj-bw-learn-more-footer />
55+
</mj-wrapper>
56+
57+
<!-- Footer -->
58+
<mj-include path="../../../components/footer.mjml" />
59+
</mj-body>
60+
</mjml>

0 commit comments

Comments
 (0)