mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Added subscription status text in newsletters (#16442)
fixes https://github.com/TryGhost/Team/issues/2736 Shows the actual subscription status (expires on DD MMM YYYY) in every email when show subscription details is enabled.
This commit is contained in:
parent
c28b4c61d2
commit
bc0126c54e
10 changed files with 1121 additions and 29 deletions
|
@ -6003,6 +6003,597 @@ member-uuid",
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Batch sending tests Newsletter settings Shows subscription details box 1 1`] = `
|
||||
Object {
|
||||
"html": "<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width\\">
|
||||
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=UTF-8\\">
|
||||
<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch><o:AllowPNG/></o:OfficeDocumentSettings></xml><![endif]-->
|
||||
<title>This is a test post title</title>
|
||||
<style>
|
||||
.post-title-link {
|
||||
color: #15212A;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.post-title-link-left {
|
||||
text-align: left;
|
||||
}
|
||||
.comment-link {
|
||||
color: #738a94;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
.comment-link img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: 1px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.view-online-link {
|
||||
word-wrap: none;
|
||||
white-space: nowrap;
|
||||
color: #738a94;
|
||||
}
|
||||
.kg-nft-link {
|
||||
display: block;
|
||||
text-decoration: none !important;
|
||||
color: #15212A !important;
|
||||
font-family: inherit !important;
|
||||
font-size: 14px;
|
||||
line-height: 1.3em;
|
||||
padding-top: 4px;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.kg-twitter-link {
|
||||
display: block;
|
||||
text-decoration: none !important;
|
||||
color: #15212A !important;
|
||||
font-family: inherit !important;
|
||||
font-size: 15px;
|
||||
padding: 8px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
@media only screen and (max-width: 620px) {
|
||||
table.body {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
table.body p,
|
||||
table.body ul,
|
||||
table.body ol,
|
||||
table.body td,
|
||||
table.body span {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table.body pre {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
table.body .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table.body .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.body .main {
|
||||
border-spacing: 10px 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table.body .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.body .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.body .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
table.body .site-icon img {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
table.body .site-url a {
|
||||
font-size: 14px !important;
|
||||
padding-bottom: 15px !important;
|
||||
}
|
||||
|
||||
table.body .post-meta,
|
||||
table.body .post-meta-date {
|
||||
white-space: normal !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
table.body .post-meta,
|
||||
table.body .view-online {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
table.body .post-meta.author-date {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.body .view-online {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
table.body .view-online-link,
|
||||
table.body .comment-link,
|
||||
table.body .footer,
|
||||
table.body .footer a {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
table.body .post-title a {
|
||||
font-size: 32px !important;
|
||||
line-height: 1.15em !important;
|
||||
}
|
||||
|
||||
table.feedback-buttons {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
table.feedback-buttons-mobile {
|
||||
display: table !important;
|
||||
width: 100% !important;
|
||||
max-width: 390px;
|
||||
}
|
||||
|
||||
table.body .feedback-button-mobile-text {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
table.body .subscription-box {
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
table.body .subscription-box h3 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
table.body .subscription-box p,
|
||||
table.body .subscription-box p span {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
table.body .subscription-details,
|
||||
table.body .manage-subscription {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: left !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
table.body .subscription-details {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
table.body .kg-bookmark-card {
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
table.body .kg-bookmark-thumbnail {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
table.body .kg-bookmark-metadata span {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
table.body .kg-embed-card {
|
||||
max-width: 90vw !important;
|
||||
}
|
||||
|
||||
table.body h1 {
|
||||
font-size: 32px !important;
|
||||
line-height: 1.3em !important;
|
||||
}
|
||||
|
||||
table.body h2 {
|
||||
font-size: 26px !important;
|
||||
line-height: 1.22em !important;
|
||||
}
|
||||
|
||||
table.body h3 {
|
||||
font-size: 21px !important;
|
||||
line-height: 1.25em !important;
|
||||
}
|
||||
|
||||
table.body h4 {
|
||||
font-size: 19px !important;
|
||||
line-height: 1.3em !important;
|
||||
}
|
||||
|
||||
table.body h5 {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4em !important;
|
||||
}
|
||||
|
||||
table.body h6 {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4em !important;
|
||||
}
|
||||
|
||||
table.body blockquote {
|
||||
font-size: 17px;
|
||||
line-height: 1.6em;
|
||||
margin-bottom: 0;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
table.body blockquote.kg-blockquote-alt {
|
||||
border-left: 0 none !important;
|
||||
margin: 0 0 2.5em 0 !important;
|
||||
padding: 0 50px 0 50px !important;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
table.body blockquote + * {
|
||||
margin-top: 1.5em !important;
|
||||
}
|
||||
|
||||
table.body hr {
|
||||
margin: 2em 0 !important;
|
||||
}
|
||||
|
||||
table.body figcaption,
|
||||
table.body figcaption a {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style=\\"background-color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 18px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; color: #15212A;\\">
|
||||
<span class=\\"preheader\\" style=\\"color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;\\">Hello world</span>
|
||||
<table role=\\"presentation\\" border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" class=\\"body\\" width=\\"100%\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #fff; width: 100%;\\" bgcolor=\\"#fff\\">
|
||||
<!-- Outlook doesn't respect max-width so we need an extra centered table -->
|
||||
<!--[if mso]>
|
||||
<tr>
|
||||
<td>
|
||||
<center>
|
||||
<table border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" width=\\"600\\">
|
||||
<![endif]-->
|
||||
<tr>
|
||||
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A;\\" valign=\\"top\\"> </td>
|
||||
<td class=\\"container\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A; display: block; max-width: 600px; margin: 0 auto;\\" valign=\\"top\\">
|
||||
<div class=\\"content\\" style=\\"box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px;\\">
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role=\\"presentation\\" border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" class=\\"main\\" width=\\"100%\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; border-spacing: 20px 0; width: 100%;\\">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class=\\"wrapper\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A; box-sizing: border-box;\\" valign=\\"top\\">
|
||||
<table role=\\"presentation\\" border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" width=\\"100%\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\\">
|
||||
<tr class=\\"header-image-row\\">
|
||||
<td class=\\"header-image\\" width=\\"100%\\" align=\\"center\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A; padding-top: 24px;\\" valign=\\"top\\">
|
||||
<img src=\\"http://127.0.0.1:2369/content/images/2022/05/test.jpg\\" style=\\"border: none; -ms-interpolation-mode: bicubic; max-width: 100%;\\">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=\\"site-info-bordered\\" width=\\"100%\\" align=\\"center\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A; padding-top: 50px; border-bottom: 1px solid #e5eff5;\\" valign=\\"top\\">
|
||||
<table role=\\"presentation\\" border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\\" width=\\"100%\\">
|
||||
<tr>
|
||||
<td class=\\"site-url \\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; color: #15212A; font-size: 16px; letter-spacing: -0.1px; font-weight: 700; text-transform: uppercase; text-align: center;\\" valign=\\"top\\" align=\\"center\\"><div style=\\"width: 100% !important;\\"><a href=\\"http://127.0.0.1:2369/\\" class=\\"site-title\\" style=\\"text-decoration: none; color: #15212A; overflow-wrap: anywhere;\\" target=\\"_blank\\">Ghost</a></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class=\\"site-url site-url-bottom-padding\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; color: #15212A; font-size: 16px; letter-spacing: -0.1px; font-weight: 700; text-transform: uppercase; text-align: center; padding-bottom: 50px;\\" valign=\\"top\\" align=\\"center\\"><div style=\\"width: 100% !important;\\"><a href=\\"http://127.0.0.1:2369/\\" class=\\"site-subtitle\\" style=\\"text-decoration: none; color: #8695a4; font-size: 14px; font-weight: 400; text-transform: none; overflow-wrap: anywhere;\\" target=\\"_blank\\">Daily newsletter</a></div></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=\\"post-title post-title-serif\\" style=\\"vertical-align: top; color: #15212A; padding-bottom: 16px; font-size: 42px; line-height: 1.1em; font-weight: 700; text-align: center; font-family: Georgia, serif; letter-spacing: -0.01em;\\" valign=\\"top\\" align=\\"center\\">
|
||||
<a href=\\"http://127.0.0.1:2369/this-is-a-test-post-title-8/\\" class=\\"post-title-link\\" style=\\"text-decoration: none; color: #15212A; display: block; text-align: center; margin-top: 50px; overflow-wrap: anywhere;\\" target=\\"_blank\\">This is a test post title</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A;\\" valign=\\"top\\">
|
||||
<table role=\\"presentation\\" border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" width=\\"100%\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; padding-bottom: 48px;\\">
|
||||
<tr>
|
||||
<td class=\\"post-meta post-meta-center author-date\\" align=\\"center\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; color: #738a94; font-size: 13px; letter-spacing: 0.1px; display: inline-block; width: 100%; text-align: center;\\" width=\\"100%\\" valign=\\"top\\">
|
||||
By Joe Bloggs • <span class=\\"post-meta-date\\" style=\\"white-space: nowrap;\\">1 Jan 2023 </span>
|
||||
</td>
|
||||
<td class=\\"post-meta post-meta-center view-online\\" align=\\"center\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; color: #738a94; font-size: 13px; letter-spacing: 0.1px; display: inline-block; text-decoration: underline; width: 100%; text-align: center;\\" width=\\"100%\\" valign=\\"top\\">
|
||||
<a href=\\"http://127.0.0.1:2369/this-is-a-test-post-title-8/\\" class=\\"view-online-link\\" style=\\"text-decoration: none; word-wrap: none; white-space: nowrap; color: #738a94; overflow-wrap: anywhere;\\" target=\\"_blank\\">View in browser</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class=\\"post-content-row\\">
|
||||
<td class=\\"post-content no-border\\" style=\\"vertical-align: top; font-family: Georgia, serif; font-size: 18px; line-height: 1.5em; color: #15212A; padding-bottom: 20px; border-bottom: 0; max-width: 600px;\\" valign=\\"top\\">
|
||||
<!-- POST CONTENT START -->
|
||||
<p style=\\"margin: 0 0 1.5em 0; line-height: 1.6em;\\">Hello world</p>
|
||||
<!-- POST CONTENT END -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td class=\\"subscription-box\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; padding: 24px; background: #F4F5F6; color: #15212A; border-radius: 3px;\\" valign=\\"top\\">
|
||||
<h3 style=\\"margin-top: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 1.11em; text-rendering: optimizeLegibility; font-size: 14px; font-weight: 700; margin: 0 0 16px;\\">Subscription details</h3>
|
||||
<p style=\\"margin: 0 0 1.5em 0; font-size: 14px; font-weight: 400; line-height: 1.45em; text-decoration: none; margin-bottom: 16px; color: #15212A;\\">
|
||||
<span>You are receiving this because you are a <strong style=\\"font-weight: 700;\\">free subscriber</strong> to Ghost.</span>
|
||||
|
||||
</p>
|
||||
<table role=\\"presentation\\" border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" width=\\"100%\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;\\">
|
||||
<tr>
|
||||
<td class=\\"subscription-details\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A;\\" valign=\\"top\\">
|
||||
<p style=\\"margin: 0 0 1.5em 0; margin-bottom: 0; font-size: 14px; font-weight: 400; line-height: 1.45em; text-decoration: none; color: #15212A;\\">Name: Simon Tester</p>
|
||||
<p style=\\"margin: 0 0 1.5em 0; margin-bottom: 0; font-size: 14px; font-weight: 400; line-height: 1.45em; text-decoration: none; color: #15212A;\\">Email: <a href=\\"mailto:replacements-test-2@example.com\\" style=\\"overflow-wrap: anywhere; text-decoration: none; color: #15212A;\\" target=\\"_blank\\">replacements-test-2@example.com</a></p>
|
||||
<p style=\\"margin: 0 0 1.5em 0; margin-bottom: 0; font-size: 14px; font-weight: 400; line-height: 1.45em; text-decoration: none; color: #15212A;\\">Member since: 22 March 2023</p>
|
||||
</td>
|
||||
<td align=\\"right\\" valign=\\"bottom\\" class=\\"manage-subscription\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; white-space: nowrap; font-size: 13.5px; font-weight: 500; text-align: right; line-height: 1.45em; vertical-align: bottom; color: #FF1A75;\\">
|
||||
<a href=\\"http://127.0.0.1:2369/#/portal/account\\" style=\\"color: #FF1A75; text-decoration: none; overflow-wrap: anywhere;\\" target=\\"_blank\\"> Manage subscription →</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=\\"wrapper\\" align=\\"center\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A; box-sizing: border-box;\\" valign=\\"top\\">
|
||||
<table role=\\"presentation\\" border=\\"0\\" cellpadding=\\"0\\" cellspacing=\\"0\\" width=\\"100%\\" style=\\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; padding-top: 40px; padding-bottom: 30px;\\">
|
||||
<tr>
|
||||
<td class=\\"footer\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; color: #738a94; margin-top: 20px; text-align: center; font-size: 13px; padding-bottom: 10px; padding-top: 10px; padding-left: 30px; padding-right: 30px; line-height: 1.5em;\\" valign=\\"top\\" align=\\"center\\">Ghost © 2023 – <a href=\\"unsubscribe_url\\" style=\\"overflow-wrap: anywhere; color: #738a94; text-decoration: underline;\\" target=\\"_blank\\">Unsubscribe</a></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class=\\"footer-powered\\" style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A; text-align: center; padding-top: 70px; padding-bottom: 40px;\\" valign=\\"top\\" align=\\"center\\"><a href=\\"https://ghost.org/\\" style=\\"color: #FF1A75; text-decoration: none; overflow-wrap: anywhere;\\" target=\\"_blank\\"><img src=\\"https://static.ghost.org/v4.0.0/images/powered.png\\" border=\\"0\\" width=\\"142\\" height=\\"30\\" class=\\"gh-powered\\" alt=\\"Powered by Ghost\\" style=\\"border: none; -ms-interpolation-mode: bicubic; max-width: 100%; width: 142px; height: 30px;\\"></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td style=\\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; vertical-align: top; color: #15212A;\\" valign=\\"top\\"> </td>
|
||||
</tr>
|
||||
|
||||
<!--[if mso]>
|
||||
</table>
|
||||
</center>
|
||||
</td>
|
||||
</tr>
|
||||
<![endif]-->
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Batch sending tests Newsletter settings Shows subscription details box 2 1`] = `
|
||||
Object {
|
||||
"html": "
|
||||
Hello world
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Ghost [http://127.0.0.1:2369/]
|
||||
|
||||
|
||||
Daily newsletter [http://127.0.0.1:2369/]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
This is a test post title [http://127.0.0.1:2369/this-is-a-test-post-title-8/]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
By Joe Bloggs • 1 Jan 2023
|
||||
|
||||
|
||||
View in browser [http://127.0.0.1:2369/this-is-a-test-post-title-8/]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Hello world
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Subscription details
|
||||
|
||||
|
||||
|
||||
You are receiving this because you are a free subscriber to Ghost.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Name: Simon Tester
|
||||
|
||||
|
||||
Email: replacements-test-2@example.com
|
||||
|
||||
|
||||
Member since: 22 March 2023
|
||||
|
||||
|
||||
|
||||
|
||||
Manage subscription → [http://127.0.0.1:2369/#/portal/account]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Ghost © 2023 – Unsubscribe [unsubscribe_url]
|
||||
|
||||
|
||||
|
||||
https://ghost.org/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Batch sending tests Replacements Does replace with and without fallback in both plaintext and html for member with name 1 1`] = `
|
||||
Object {
|
||||
"html": "<!doctype html>
|
||||
|
|
|
@ -1034,5 +1034,24 @@ describe('Batch sending tests', function () {
|
|||
// undo
|
||||
await models.Newsletter.edit({show_comment_cta: true}, {id: defaultNewsletter.id});
|
||||
});
|
||||
|
||||
it('Shows subscription details box', async function () {
|
||||
mockSetting('email_track_clicks', false); // Disable link replacement for this test
|
||||
|
||||
const defaultNewsletter = await getDefaultNewsletter();
|
||||
await models.Newsletter.edit({show_subscription_details: true}, {id: defaultNewsletter.id});
|
||||
|
||||
const {html} = await sendEmail({
|
||||
title: 'This is a test post title',
|
||||
mobiledoc: mobileDocExample
|
||||
});
|
||||
|
||||
// Currently the link is not present in plaintext version (because no text)
|
||||
assert.equal(html.match(/#\/portal\/account/g).length, 1, 'Subscription details box should contain a link to the account page');
|
||||
await lastEmailMatchSnapshot();
|
||||
|
||||
// undo
|
||||
await models.Newsletter.edit({show_subscription_details: false}, {id: defaultNewsletter.id});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -497,14 +497,21 @@ class BatchSendingService {
|
|||
* @returns {Promise<MemberLike[]>}
|
||||
*/
|
||||
async getBatchMembers(batchId) {
|
||||
const models = await this.#models.EmailRecipient.findAll({filter: `batch_id:${batchId}`, withRelated: ['member']});
|
||||
const models = await this.#models.EmailRecipient.findAll({filter: `batch_id:${batchId}`, withRelated: ['member', 'member.stripeSubscriptions', 'member.products']});
|
||||
return models.map((model) => {
|
||||
// Map subscriptions
|
||||
const subscriptions = model.related('member')?.related('stripeSubscriptions')?.toJSON() ?? [];
|
||||
const tiers = model.related('member')?.related('products')?.toJSON() ?? [];
|
||||
|
||||
return {
|
||||
id: model.get('member_id'),
|
||||
uuid: model.get('member_uuid'),
|
||||
email: model.get('member_email'),
|
||||
name: model.get('member_name'),
|
||||
createdAt: model.related('member')?.get('created_at') ?? null
|
||||
createdAt: model.related('member')?.get('created_at') ?? null,
|
||||
status: model.related('member')?.get('status') ?? 'free',
|
||||
subscriptions,
|
||||
tiers
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,6 +7,31 @@ const {isUnsplashImage} = require('@tryghost/kg-default-cards/lib/utils');
|
|||
const {textColorForBackgroundColor, darkenToContrastThreshold} = require('@tryghost/color-utils');
|
||||
const {DateTime} = require('luxon');
|
||||
const htmlToPlaintext = require('@tryghost/html-to-plaintext');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const messages = {
|
||||
subscriptionStatus: {
|
||||
free: 'You are currently subscribed to the free plan.',
|
||||
expired: 'Your subscription has expired.',
|
||||
canceled: 'Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.',
|
||||
active: 'Your subscription will renew on {date}.',
|
||||
trial: 'Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.',
|
||||
complimentaryExpires: 'Your subscription will expire on {date}.',
|
||||
complimentaryInfinite: ''
|
||||
}
|
||||
};
|
||||
|
||||
function formatDateLong(date, timezone) {
|
||||
return DateTime.fromJSDate(date).setZone(timezone).setLocale('en-gb').toLocaleString({
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {string|null} Segment
|
||||
|
@ -20,7 +45,20 @@ const htmlToPlaintext = require('@tryghost/html-to-plaintext');
|
|||
* @prop {string} uuid
|
||||
* @prop {string} email
|
||||
* @prop {string} name
|
||||
* @prop {'free'|'paid'|'comped'} status
|
||||
* @prop {Date|null} createdAt This can be null if the member has been deleted for older email recipient rows
|
||||
* @prop {MemberLikeSubscription[]} subscriptions Required to get trial end / next renewal date / expire at date for paid member
|
||||
* @prop {MemberLikeTier[]} tiers Required to get the expiry date in case of a comped member
|
||||
*
|
||||
* @typedef {object} MemberLikeSubscription
|
||||
* @prop {string} status
|
||||
* @prop {boolean} cancel_at_period_end
|
||||
* @prop {Date|null} trial_end_at
|
||||
* @prop {Date} current_period_end
|
||||
*
|
||||
* @typedef {object} MemberLikeTier
|
||||
* @prop {string} product_id
|
||||
* @prop {Date|null} expiry_at
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -330,7 +368,7 @@ class EmailRenderer {
|
|||
* Takes a member and newsletter uuid. Returns the url that should be used to unsubscribe
|
||||
* In case of no member uuid, generates the preview unsubscribe url - `?preview=1`
|
||||
*
|
||||
* @param {string} [uuid] post uuid
|
||||
* @param {string} [uuid] member uuid
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.newsletterUuid] newsletter uuid
|
||||
* @param {boolean} [options.comments] Unsubscribe from comment emails
|
||||
|
@ -354,6 +392,81 @@ class EmailRenderer {
|
|||
return unsubscribeUrl.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* createManageAccountUrl
|
||||
*
|
||||
* @param {string} [uuid] member uuid
|
||||
*/
|
||||
createManageAccountUrl(uuid) {
|
||||
const siteUrl = this.#urlUtils.urlFor('home', true);
|
||||
const url = new URL(siteUrl);
|
||||
url.hash = '#/portal/account';
|
||||
|
||||
return url.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MemberLike} member
|
||||
* @returns {string}
|
||||
*/
|
||||
getMemberStatusText(member) {
|
||||
if (member.status === 'free') {
|
||||
// Not really used, but as a backup
|
||||
return tpl(messages.subscriptionStatus.free);
|
||||
}
|
||||
|
||||
// Do we have an active subscription?
|
||||
if (member.status === 'paid') {
|
||||
let activeSubscription = member.subscriptions.find((subscription) => {
|
||||
return subscription.status === 'active';
|
||||
}) ?? member.subscriptions.find((subscription) => {
|
||||
return ['active', 'trialing', 'unpaid', 'past_due'].includes(subscription.status);
|
||||
});
|
||||
|
||||
if (!activeSubscription && !member.tiers.length) {
|
||||
// No subscription?
|
||||
return tpl(messages.subscriptionStatus.expired);
|
||||
}
|
||||
|
||||
if (!activeSubscription) {
|
||||
if (!member.tiers[0]?.expiry_at) {
|
||||
return tpl(messages.subscriptionStatus.complimentaryInfinite);
|
||||
}
|
||||
// Create one manually that is expiring
|
||||
activeSubscription = {
|
||||
cancel_at_period_end: true,
|
||||
current_period_end: member.tiers[0].expiry_at,
|
||||
status: 'active',
|
||||
trial_end_at: null
|
||||
};
|
||||
}
|
||||
const timezone = this.#settingsCache.get('timezone');
|
||||
|
||||
// Translate to a human readable string
|
||||
if (activeSubscription.trial_end_at && activeSubscription.trial_end_at > new Date() && activeSubscription.status === 'trialing') {
|
||||
const date = formatDateLong(activeSubscription.trial_end_at, timezone);
|
||||
return tpl(messages.subscriptionStatus.trial, {date});
|
||||
}
|
||||
|
||||
const date = formatDateLong(activeSubscription.current_period_end, timezone);
|
||||
if (activeSubscription.cancel_at_period_end) {
|
||||
return tpl(messages.subscriptionStatus.canceled, {date});
|
||||
}
|
||||
|
||||
return tpl(messages.subscriptionStatus.active, {date});
|
||||
}
|
||||
|
||||
const expires = member.tiers[0]?.expiry_at ?? null;
|
||||
|
||||
if (expires) {
|
||||
const timezone = this.#settingsCache.get('timezone');
|
||||
const date = formatDateLong(expires, timezone);
|
||||
return tpl(messages.subscriptionStatus.complimentaryExpires, {date});
|
||||
}
|
||||
|
||||
return tpl(messages.subscriptionStatus.complimentaryInfinite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that we only look in HTML because plaintext and HTML are essentially the same content
|
||||
* @returns {ReplacementDefinition[]}
|
||||
|
@ -366,6 +479,12 @@ class EmailRenderer {
|
|||
return this.createUnsubscribeUrl(member.uuid, {newsletterUuid});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'manage_account_url',
|
||||
getValue: (member) => {
|
||||
return this.createManageAccountUrl(member.uuid);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'uuid',
|
||||
getValue: (member) => {
|
||||
|
@ -394,11 +513,22 @@ class EmailRenderer {
|
|||
id: 'created_at',
|
||||
getValue: (member) => {
|
||||
const timezone = this.#settingsCache.get('timezone');
|
||||
return member.createdAt ? DateTime.fromJSDate(member.createdAt).setZone(timezone).setLocale('en-gb').toLocaleString({
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}) : '';
|
||||
return member.createdAt ? formatDateLong(member.createdAt, timezone) : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
getValue: (member) => {
|
||||
if (member.status === 'comped') {
|
||||
return 'complimentary';
|
||||
}
|
||||
return member.status;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'status_text',
|
||||
getValue: (member) => {
|
||||
return this.getMemberStatusText(member);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -407,10 +537,6 @@ class EmailRenderer {
|
|||
const EMAIL_REPLACEMENT_REGEX = /%%\{(.*?)\}%%/g;
|
||||
const REPLACEMENT_STRING_REGEX = /^(?<recipientProperty>\w+?)(?:,? *(?:"|")(?<fallback>.*?)(?:"|"))?$/;
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Stores the definitions that we are actually going to use
|
||||
const replacements = [];
|
||||
|
||||
|
|
|
@ -178,9 +178,10 @@ class EmailService {
|
|||
}
|
||||
|
||||
/**
|
||||
* @params {string} [segment]
|
||||
* @return {import('./email-renderer').MemberLike}
|
||||
*/
|
||||
getDefaultExampleMember() {
|
||||
getDefaultExampleMember(segment) {
|
||||
/**
|
||||
* @type {import('./email-renderer').MemberLike}
|
||||
*/
|
||||
|
@ -189,20 +190,31 @@ class EmailService {
|
|||
uuid: 'example-uuid',
|
||||
email: 'jamie@example.com',
|
||||
name: 'Jamie Larson',
|
||||
createdAt: new Date()
|
||||
createdAt: new Date(),
|
||||
status: segment === 'status:free' ? 'free' : 'paid',
|
||||
subscriptions: segment === 'status:free' ? [] : [
|
||||
{
|
||||
cancel_at_period_end: false,
|
||||
trial_end_at: null,
|
||||
current_period_end: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
status: 'active'
|
||||
}
|
||||
],
|
||||
tiers: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} [email] (optional) Search for a member with this email address and use it as the example. If not found, defaults to the default but still uses the provided email address.
|
||||
* @param {string} [segment] (optional) The segment to use for the example member
|
||||
* @return {Promise<import('./email-renderer').MemberLike>}
|
||||
*/
|
||||
async getExampleMember(email) {
|
||||
async getExampleMember(email, segment) {
|
||||
/**
|
||||
* @type {import('./email-renderer').MemberLike}
|
||||
*/
|
||||
const exampleMember = this.getDefaultExampleMember();
|
||||
const exampleMember = this.getDefaultExampleMember(segment);
|
||||
|
||||
// fetch any matching members so that replacements use expected values
|
||||
if (email) {
|
||||
|
@ -213,6 +225,16 @@ class EmailService {
|
|||
exampleMember.email = member.get('email');
|
||||
exampleMember.name = member.get('name');
|
||||
exampleMember.createdAt = member.get('created_at');
|
||||
|
||||
if (segment === 'status:-free' && member.get('status') !== 'free') {
|
||||
// Make sure the example member matches the chosen segment (otherwise we'll send an email to free segment, but include a paid member details, which looks like a bug)
|
||||
exampleMember.status = member.get('status');
|
||||
const subscriptions = (await member.getLazyRelation('stripeSubscriptions')).toJSON();
|
||||
exampleMember.subscriptions = subscriptions;
|
||||
|
||||
const tiers = (await member.getLazyRelation('products')).toJSON();
|
||||
exampleMember.tiers = tiers;
|
||||
}
|
||||
} else {
|
||||
exampleMember.name = ''; // Force empty name to simulate name fallbacks
|
||||
exampleMember.email = email;
|
||||
|
@ -246,7 +268,7 @@ class EmailService {
|
|||
* @returns {Promise<{subject: string, html: string, plaintext: string}>} Email preview
|
||||
*/
|
||||
async previewEmail(post, newsletter, segment) {
|
||||
const exampleMember = await this.getExampleMember();
|
||||
const exampleMember = await this.getExampleMember(null, segment);
|
||||
|
||||
const subject = this.#emailRenderer.getSubject(post);
|
||||
let {html, plaintext, replacements} = await this.#emailRenderer.renderBody(post, newsletter, segment, {clickTrackingEnabled: false});
|
||||
|
@ -268,7 +290,7 @@ class EmailService {
|
|||
async sendTestEmail(post, newsletter, segment, emails) {
|
||||
const members = [];
|
||||
for (const email of emails) {
|
||||
members.push(await this.getExampleMember(email));
|
||||
members.push(await this.getExampleMember(email, segment));
|
||||
}
|
||||
|
||||
await this.#sendingService.send({
|
||||
|
|
|
@ -179,18 +179,18 @@
|
|||
<td class="subscription-box">
|
||||
<h3>Subscription details</h3>
|
||||
<p style="margin-bottom: 16px;">
|
||||
<span>You are receiving this because you are a <strong><span data-gh-segment="status:-free">paid</span><span data-gh-segment="status:free">free</span> subscriber</strong> to {{site.title}}.</span>
|
||||
<span data-gh-segment="status:-free">Your subscription will renew on 20 March 2024.</span>
|
||||
<span>You are receiving this because you are a <strong>%%{status}%% subscriber</strong> to {{site.title}}.</span>
|
||||
<span data-gh-segment="status:-free">%%{status_text}%%</span>
|
||||
</p>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td class="subscription-details">
|
||||
<p>Name: %%{name}%%</p>
|
||||
<p>Email: <a href="#">%%{email}%%</a></p>
|
||||
<p>Email: <a href="mailto:%%{email}%%">%%{email}%%</a></p>
|
||||
<p>Member since: %%{created_at}%%</p>
|
||||
</td>
|
||||
<td align="right" valign="bottom" class="manage-subscription">
|
||||
<a href="#"> Manage subscription →</a>
|
||||
<a href="%%{manage_account_url}%%"> Manage subscription →</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -591,7 +591,11 @@ describe('Batch Sending Service', function () {
|
|||
member_name: 'Test User',
|
||||
loaded: ['member'],
|
||||
member: createModel({
|
||||
created_at: new Date()
|
||||
created_at: new Date(),
|
||||
loaded: ['stripeSubscriptions', 'products'],
|
||||
status: 'free',
|
||||
stripeSubscriptions: [],
|
||||
products: []
|
||||
})
|
||||
},
|
||||
{
|
||||
|
@ -601,7 +605,11 @@ describe('Batch Sending Service', function () {
|
|||
member_name: 'Test User 2',
|
||||
loaded: ['member'],
|
||||
member: createModel({
|
||||
created_at: new Date()
|
||||
created_at: new Date(),
|
||||
status: 'free',
|
||||
loaded: ['stripeSubscriptions', 'products'],
|
||||
stripeSubscriptions: [],
|
||||
products: []
|
||||
})
|
||||
}
|
||||
]
|
||||
|
|
|
@ -80,7 +80,7 @@ describe('Email renderer', function () {
|
|||
beforeEach(function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com/subdirectory'
|
||||
urlFor: () => 'http://example.com/subdirectory/'
|
||||
},
|
||||
labs: {
|
||||
isSet: () => true
|
||||
|
@ -101,7 +101,8 @@ describe('Email renderer', function () {
|
|||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0)
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'free'
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -165,6 +166,43 @@ describe('Email renderer', function () {
|
|||
assert.equal(replacements[0].getValue(member), 'test@example.com');
|
||||
});
|
||||
|
||||
it('returns correct status', function () {
|
||||
const html = 'Hello %%{status}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 1);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'status');
|
||||
assert.equal(replacements[0].getValue(member), 'free');
|
||||
});
|
||||
|
||||
it('returns mapped complimentary status', function () {
|
||||
member.status = 'comped';
|
||||
const html = 'Hello %%{status}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 1);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{status\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'status');
|
||||
assert.equal(replacements[0].getValue(member), 'complimentary');
|
||||
});
|
||||
|
||||
it('returns manage_account_url', function () {
|
||||
const html = 'Hello %%{manage_account_url}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 1);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{manage_account_url\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'manage_account_url');
|
||||
assert.equal(replacements[0].getValue(member), 'http://example.com/subdirectory/#/portal/account');
|
||||
});
|
||||
|
||||
it('returns status_text', function () {
|
||||
const html = 'Hello %%{status_text}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 1);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{status_text\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'status_text');
|
||||
assert.equal(replacements[0].getValue(member), 'You are currently subscribed to the free plan.');
|
||||
});
|
||||
|
||||
it('returns correct createdAt', function () {
|
||||
const html = 'Hello %%{created_at}%%,';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
|
@ -223,6 +261,213 @@ describe('Email renderer', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getMemberStatusText', function () {
|
||||
let emailRenderer;
|
||||
|
||||
beforeEach(function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com/subdirectory/'
|
||||
},
|
||||
labs: {
|
||||
isSet: () => true
|
||||
},
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'timezone') {
|
||||
return 'UTC';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns for free member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'free'
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'You are currently subscribed to the free plan.');
|
||||
});
|
||||
|
||||
it('Returns for active paid member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [
|
||||
{
|
||||
status: 'active',
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'Your subscription will renew on 13 March 2023.');
|
||||
});
|
||||
|
||||
it('Returns for canceled paid member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [
|
||||
{
|
||||
status: 'active',
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'Your subscription has been canceled and will expire on 13 March 2023. You can resume your subscription via your account settings.');
|
||||
});
|
||||
|
||||
it('Returns for expired paid member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [
|
||||
{
|
||||
status: 'canceled',
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: true
|
||||
}
|
||||
],
|
||||
tiers: []
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'Your subscription has expired.');
|
||||
});
|
||||
|
||||
it('Returns for trialing paid member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [
|
||||
{
|
||||
status: 'trialing',
|
||||
trial_end_at: new Date(2050, 2, 13, 12, 0),
|
||||
current_period_end: new Date(2023, 2, 13, 12, 0),
|
||||
cancel_at_period_end: false
|
||||
}
|
||||
],
|
||||
tiers: []
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'Your free trial ends on 13 March 2050, at which time you will be charged the regular price. You can always cancel before then.');
|
||||
});
|
||||
|
||||
it('Returns for infinite complimentary member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'comped',
|
||||
subscriptions: [],
|
||||
tiers: [
|
||||
{
|
||||
name: 'Silver',
|
||||
expiry_at: null
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, '');
|
||||
});
|
||||
|
||||
it('Returns for expiring complimentary member', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'comped',
|
||||
subscriptions: [],
|
||||
tiers: [
|
||||
{
|
||||
name: 'Silver',
|
||||
expiry_at: new Date(2050, 2, 13, 12, 0)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'Your subscription will expire on 13 March 2050.');
|
||||
});
|
||||
|
||||
it('Returns for a paid member without subscriptions', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [],
|
||||
tiers: [
|
||||
{
|
||||
name: 'Silver',
|
||||
expiry_at: new Date(2050, 2, 13, 12, 0)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, 'Your subscription has been canceled and will expire on 13 March 2050. You can resume your subscription via your account settings.');
|
||||
});
|
||||
|
||||
it('Returns for an infinte paid member without subscriptions', function () {
|
||||
const member = {
|
||||
id: '456',
|
||||
uuid: 'myuuid',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'paid',
|
||||
subscriptions: [],
|
||||
tiers: [
|
||||
{
|
||||
name: 'Silver',
|
||||
expiry_at: null
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = emailRenderer.getMemberStatusText(member);
|
||||
assert.equal(result, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubject', function () {
|
||||
const emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
|
|
|
@ -293,14 +293,78 @@ describe('Email Service', function () {
|
|||
const member = createModel({
|
||||
uuid: '123',
|
||||
name: 'Example member',
|
||||
email: 'example@example.com'
|
||||
email: 'example@example.com',
|
||||
status: 'free'
|
||||
});
|
||||
membersRepository.get.resolves(member);
|
||||
const exampleMember = await service.getExampleMember('example@example.com');
|
||||
const exampleMember = await service.getExampleMember('example@example.com', 'status:free');
|
||||
assert.strictEqual(exampleMember.id, member.id);
|
||||
assert.strictEqual(exampleMember.name, member.get('name'));
|
||||
assert.strictEqual(exampleMember.email, member.get('email'));
|
||||
assert.strictEqual(exampleMember.uuid, member.get('uuid'));
|
||||
assert.strictEqual(exampleMember.status, 'free');
|
||||
assert.deepEqual(exampleMember.subscriptions, []);
|
||||
assert.deepEqual(exampleMember.tiers, []);
|
||||
});
|
||||
|
||||
it('Returns a paid member', async function () {
|
||||
const member = createModel({
|
||||
uuid: '123',
|
||||
name: 'Example member',
|
||||
email: 'example@example.com',
|
||||
status: 'paid',
|
||||
stripeSubscriptions: [
|
||||
createModel({
|
||||
status: 'active',
|
||||
current_period_end: new Date(2050, 0, 1),
|
||||
cancel_at_period_end: false
|
||||
})
|
||||
],
|
||||
products: [createModel({
|
||||
name: 'Silver',
|
||||
expiry_at: null
|
||||
})]
|
||||
});
|
||||
membersRepository.get.resolves(member);
|
||||
const exampleMember = await service.getExampleMember('example@example.com', 'status:-free');
|
||||
assert.strictEqual(exampleMember.id, member.id);
|
||||
assert.strictEqual(exampleMember.name, member.get('name'));
|
||||
assert.strictEqual(exampleMember.email, member.get('email'));
|
||||
assert.strictEqual(exampleMember.uuid, member.get('uuid'));
|
||||
assert.strictEqual(exampleMember.status, 'paid');
|
||||
assert.deepEqual(exampleMember.subscriptions, [
|
||||
{
|
||||
status: 'active',
|
||||
current_period_end: new Date(2050, 0, 1),
|
||||
cancel_at_period_end: false,
|
||||
id: member.related('stripeSubscriptions')[0].id
|
||||
}
|
||||
]);
|
||||
assert.deepEqual(exampleMember.tiers, [
|
||||
{
|
||||
name: 'Silver',
|
||||
expiry_at: null,
|
||||
id: member.related('products')[0].id
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('Returns a forced free member', async function () {
|
||||
const member = createModel({
|
||||
uuid: '123',
|
||||
name: 'Example member',
|
||||
email: 'example@example.com',
|
||||
status: 'paid'
|
||||
});
|
||||
membersRepository.get.resolves(member);
|
||||
const exampleMember = await service.getExampleMember('example@example.com', 'status:free');
|
||||
assert.strictEqual(exampleMember.id, member.id);
|
||||
assert.strictEqual(exampleMember.name, member.get('name'));
|
||||
assert.strictEqual(exampleMember.email, member.get('email'));
|
||||
assert.strictEqual(exampleMember.uuid, member.get('uuid'));
|
||||
assert.strictEqual(exampleMember.status, 'free');
|
||||
assert.deepEqual(exampleMember.subscriptions, []);
|
||||
assert.deepEqual(exampleMember.tiers, []);
|
||||
});
|
||||
|
||||
it('Returns a member without name if member does not exist', async function () {
|
||||
|
|
|
@ -12,7 +12,10 @@ const createModel = (propertiesAndRelations) => {
|
|||
}
|
||||
if (Array.isArray(propertiesAndRelations[relation])) {
|
||||
return Promise.resolve({
|
||||
models: propertiesAndRelations[relation]
|
||||
models: propertiesAndRelations[relation],
|
||||
toJSON: () => {
|
||||
return propertiesAndRelations[relation].map(m => m.toJSON());
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(propertiesAndRelations[relation]);
|
||||
|
@ -24,6 +27,13 @@ const createModel = (propertiesAndRelations) => {
|
|||
if (!propertiesAndRelations.loaded.includes(relation)) {
|
||||
throw new Error(`Model.related('${relation}') was used on a test model that didn't explicitly loaded that relation.`);
|
||||
}
|
||||
if (Array.isArray(propertiesAndRelations[relation])) {
|
||||
const arr = [...propertiesAndRelations[relation]];
|
||||
arr.toJSON = () => {
|
||||
return arr.map(m => m.toJSON());
|
||||
};
|
||||
return arr;
|
||||
}
|
||||
return propertiesAndRelations[relation];
|
||||
},
|
||||
get: (property) => {
|
||||
|
|
Loading…
Add table
Reference in a new issue