mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-26 04:08:01 +03:00
Added page attribution to member email alerts (#16381)
refs https://github.com/TryGhost/Team/issues/2489 - adds attribution title and url for new free members and paid subscriptions to email alert --------- Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
parent
89493893d1
commit
c71582877c
@ -12,6 +12,7 @@ class StaffServiceWrapper {
|
||||
|
||||
const logging = require('@tryghost/logging');
|
||||
const models = require('../../models');
|
||||
const memberAttribution = require('../member-attribution');
|
||||
const {GhostMailer} = require('../mail');
|
||||
const mailer = new GhostMailer();
|
||||
const settingsCache = require('../../../shared/settings-cache');
|
||||
@ -26,6 +27,7 @@ class StaffServiceWrapper {
|
||||
settingsCache,
|
||||
urlUtils,
|
||||
DomainEvents,
|
||||
memberAttributionService: memberAttribution.service,
|
||||
labs
|
||||
});
|
||||
|
||||
|
@ -37,21 +37,18 @@
|
||||
<td align="left" style="padding: 16px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding-right: 14px; background-color: #F9F9FA; text-align: left; vertical-align: top;" valign="top">
|
||||
<td style="padding-right: 14px; background-color: #F9F9FA; text-align: left; vertical-align: middle;" valign="middle">
|
||||
<div style="width: 48px; height: 48px; background-color: #15171A; border-radius: 999px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 19px; color: #FFFFFF; text-align: center; vertical-align: center; font-weight: 500; line-height: 47px;">
|
||||
{{memberData.initials}}
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding-right: 8px; background-color: #F9F9FA; text-align: left; vertical-align: top;" valign="top">
|
||||
<td style="padding-right: 8px; background-color: #F9F9FA; text-align: left; vertical-align: middle;" valign="middle">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{memberData.name}}</p>
|
||||
{{#if memberData.showEmail}}
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #394047; font-weight: 400;">{{memberData.email}}</p>
|
||||
{{/if}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Created on {{memberData.createdAt}}{{#if memberData.location}} • {{memberData.location}} {{/if}}
|
||||
</p>
|
||||
{{#if referrerSource}}
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Source: {{referrerSource}}</p>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -60,6 +57,25 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||
<tr>
|
||||
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;">
|
||||
{{#if referrerSource}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Signup info</p>
|
||||
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 4px;">Source
|
||||
<span style="font-weight: normal; color:#3A464C;"> - {{referrerSource}}</span>
|
||||
</p>
|
||||
{{#if attributionTitle}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">Page
|
||||
<span style="font-weight: normal; color:#3A464C;"> - <a href="{{attributionUrl}}" style="font-weight: normal; color:#3A464C;text-decoration:none">{{attributionTitle}}</a></span>
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -37,45 +37,58 @@
|
||||
<td align="left" style="padding: 16px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding-right: 14px; vertical-align: top;" valign="top">
|
||||
<td style="padding-right: 14px; vertical-align: middle;" valign="middle">
|
||||
<div style="width: 48px; height: 48px; background-color: #15171A; border-radius: 999px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 19px; color: #FFFFFF; text-align: center; vertical-align: center; font-weight: 500; line-height: 47px;">
|
||||
{{memberData.initials}}
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding-right: 8px; vertical-align: top;" valign="top">
|
||||
<td style="padding-right: 8px; vertical-align: middle;" valign="middle">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{memberData.name}}</p>
|
||||
{{#if memberData.showEmail}}
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #394047; font-weight: 400;">{{memberData.email}}</p>
|
||||
{{/if}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Subscription started on {{subscriptionData.startedOn}} </p>
|
||||
{{#if referrerSource}}
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Source: {{referrerSource}}</p>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding-top: 0; padding-right: 16px; padding-bottom: 16px; padding-left: 16px;">
|
||||
<hr style="border-bottom: 2px solid #F4F4F5; margin-top: 0; margin-bottom: 16px;" />
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Tier</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{tierData.name}}
|
||||
{{#if tierData.details}} <span style="font-weight: normal; color:#3A464C;"> - {{tierData.details}}</span>{{/if}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{#if offerData}}
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding-top: 0; padding-right: 16px; padding-bottom: 16px; padding-left: 16px;">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Offer</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{offerData.name}} <span style="font-weight: normal; color:#3A464C;"> - {{offerData.details}}</span></p>
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||
<tr>
|
||||
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;">
|
||||
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Tier</p>
|
||||
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 32px;">{{tierData.name}}
|
||||
{{#if tierData.details}} <span style="font-weight: normal; color:#3A464C;"> - {{tierData.details}}</span>{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if offerData}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Offer</p>
|
||||
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 32px;">{{offerData.name}} <span style="font-weight: normal; color:#3A464C;"> - {{offerData.details}}</span></p>
|
||||
{{/if}}
|
||||
|
||||
{{#if referrerSource}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Signup info</p>
|
||||
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 4px;">Source
|
||||
<span style="font-weight: normal; color:#3A464C;"> - {{referrerSource}}</span>
|
||||
</p>
|
||||
{{#if attributionTitle}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">Page
|
||||
<span style="font-weight: normal; color:#3A464C;"> - <a href="{{attributionUrl}}" style="font-weight: normal; color:#3A464C;text-decoration:none">{{attributionTitle}}</a></span>
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -27,8 +27,16 @@ class StaffServiceEmails {
|
||||
|
||||
const subject = `🥳 Free member signup: ${memberData.name}`;
|
||||
|
||||
let attributionTitle = attribution?.title || '';
|
||||
// In case of a homepage attribution, we want to show the title as "Homepage" on email
|
||||
if (attributionTitle === 'homepage') {
|
||||
attributionTitle = 'Homepage';
|
||||
}
|
||||
|
||||
const templateData = {
|
||||
memberData,
|
||||
attributionTitle,
|
||||
attributionUrl: attribution?.url || '',
|
||||
referrerSource: attribution?.referrerSource,
|
||||
siteTitle: this.settingsCache.get('title'),
|
||||
siteUrl: this.urlUtils.getSiteUrl(),
|
||||
@ -73,8 +81,16 @@ class StaffServiceEmails {
|
||||
|
||||
let offerData = this.getOfferData(offer);
|
||||
|
||||
let attributionTitle = attribution?.title || '';
|
||||
// In case of a homepage attribution, we want to show the title as "Homepage" on email
|
||||
if (attributionTitle === 'homepage') {
|
||||
attributionTitle = 'Homepage';
|
||||
}
|
||||
|
||||
const templateData = {
|
||||
memberData,
|
||||
attributionTitle,
|
||||
attributionUrl: attribution?.url || '',
|
||||
referrerSource: attribution?.referrerSource,
|
||||
tierData,
|
||||
offerData,
|
||||
|
@ -5,13 +5,14 @@ const {MilestoneCreatedEvent} = require('@tryghost/milestones');
|
||||
// @NOTE: 'StaffService' is a vague name that does not describe what it's actually doing.
|
||||
// Possibly, "StaffNotificationService" or "StaffEventNotificationService" would be a more accurate name
|
||||
class StaffService {
|
||||
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, DomainEvents, labs}) {
|
||||
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, DomainEvents, labs, memberAttributionService}) {
|
||||
this.logging = logging;
|
||||
this.labs = labs;
|
||||
/** @private */
|
||||
this.settingsCache = settingsCache;
|
||||
this.models = models;
|
||||
this.DomainEvents = DomainEvents;
|
||||
this.memberAttributionService = memberAttributionService;
|
||||
|
||||
const Emails = require('./emails');
|
||||
|
||||
@ -98,17 +99,29 @@ class StaffService {
|
||||
});
|
||||
|
||||
if (type === MemberCreatedEvent && member.status === 'free') {
|
||||
let attribution;
|
||||
try {
|
||||
attribution = await this.memberAttributionService.getMemberCreatedAttribution(event.data.memberId);
|
||||
} catch (e) {
|
||||
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
|
||||
}
|
||||
await this.emails.notifyFreeMemberSignup({
|
||||
member,
|
||||
attribution: event?.data?.attribution
|
||||
attribution
|
||||
});
|
||||
} else if (type === SubscriptionActivatedEvent) {
|
||||
let attribution;
|
||||
try {
|
||||
attribution = await this.memberAttributionService.getSubscriptionCreatedAttribution(event.data.subscriptionId);
|
||||
} catch (e) {
|
||||
this.logging.warn(`Failed to get attribution for member - ${event?.data?.memberId}`);
|
||||
}
|
||||
await this.emails.notifyPaidSubscriptionStarted({
|
||||
member,
|
||||
offer,
|
||||
tier,
|
||||
subscription,
|
||||
attribution: event?.data?.attribution
|
||||
attribution
|
||||
});
|
||||
} else if (type === SubscriptionCancelledEvent) {
|
||||
subscription.canceledAt = event.timestamp;
|
||||
|
@ -429,7 +429,9 @@ describe('StaffService', function () {
|
||||
};
|
||||
|
||||
const attribution = {
|
||||
referrerSource: 'Twitter'
|
||||
referrerSource: 'Twitter',
|
||||
title: 'Welcome Post',
|
||||
url: 'https://example.com/welcome'
|
||||
};
|
||||
|
||||
await service.emails.notifyFreeMemberSignup({member, attribution}, options);
|
||||
@ -447,8 +449,23 @@ describe('StaffService', function () {
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 • France'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Source: Twitter'))
|
||||
sinon.match.has('html', sinon.match('Source'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Twitter'))
|
||||
).should.be.true();
|
||||
|
||||
// check attribution page
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Welcome Post'))
|
||||
).should.be.true();
|
||||
|
||||
// check attribution url
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('https://example.com/welcome'))
|
||||
).should.be.true();
|
||||
});
|
||||
});
|
||||
@ -486,7 +503,9 @@ describe('StaffService', function () {
|
||||
|
||||
it('sends paid subscription start alert with attribution', async function () {
|
||||
const attribution = {
|
||||
referrerSource: 'Twitter'
|
||||
referrerSource: 'Twitter',
|
||||
title: 'Welcome Post',
|
||||
url: 'https://example.com/welcome'
|
||||
};
|
||||
await service.emails.notifyPaidSubscriptionStarted({member, offer: null, tier, subscription, attribution}, options);
|
||||
|
||||
@ -495,7 +514,22 @@ describe('StaffService', function () {
|
||||
|
||||
// check attribution text
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Source: Twitter'))
|
||||
sinon.match.has('html', sinon.match('Twitter'))
|
||||
).should.be.true();
|
||||
|
||||
// check attribution text
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Source'))
|
||||
).should.be.true();
|
||||
|
||||
// check attribution page
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Welcome Post'))
|
||||
).should.be.true();
|
||||
|
||||
// check attribution url
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('https://example.com/welcome'))
|
||||
).should.be.true();
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user