Skip to content

Commit 093eb89

Browse files
Merge pull request #20284 from mozilla/PAY-3549
feat(payments-next): Add FreeTrialEnding email
2 parents 775169c + 9bbd45b commit 093eb89

28 files changed

Lines changed: 1480 additions & 51 deletions

File tree

libs/accounts/email-renderer/gruntfile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
'src/templates/postVerify/en.ftl',
9999
'src/templates/postVerifySecondary/en.ftl',
100100
'src/templates/recovery/en.ftl',
101+
// 'src/templates/freeTrialEndingReminder/en.ftl',
101102
// 'src/templates/subscriptionAccountDeletion/en.ftl',
102103
// 'src/templates/subscriptionAccountReminderFirst/en.ftl',
103104
// 'src/templates/subscriptionAccountReminderSecond/en.ftl',

libs/accounts/email-renderer/src/renderer/subplat-email-renderer.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as SubscriptionAccountReminderFirst from '../templates/subscriptionAcco
77
import * as SubscriptionAccountReminderSecond from '../templates/subscriptionAccountReminderSecond';
88
import * as SubscriptionCancellation from '../templates/subscriptionCancellation';
99
import * as SubscriptionDowngrade from '../templates/subscriptionDowngrade';
10+
import * as FreeTrialEndingReminder from '../templates/freeTrialEndingReminder';
1011
import * as SubscriptionEndingReminder from '../templates/subscriptionEndingReminder';
1112
import * as SubscriptionFailedPaymentsCancellation from '../templates/subscriptionFailedPaymentsCancellation';
1213
import * as SubscriptionFirstInvoice from '../templates/subscriptionFirstInvoice';
@@ -130,6 +131,20 @@ export class SubplatEmailRender extends EmailRenderer {
130131
});
131132
}
132133

134+
async renderFreeTrialEndingReminder(
135+
templateValues: FreeTrialEndingReminder.TemplateData,
136+
layoutTemplateValues: SubscriptionLayouts.TemplateData
137+
) {
138+
return this.renderEmail({
139+
template: FreeTrialEndingReminder.template,
140+
version: FreeTrialEndingReminder.version,
141+
layout: FreeTrialEndingReminder.layout,
142+
includes: FreeTrialEndingReminder.includes,
143+
...templateValues,
144+
...layoutTemplateValues,
145+
});
146+
}
147+
133148
async renderSubscriptionEndingReminder(
134149
templateValues: SubscriptionEndingReminder.TemplateData,
135150
layoutTemplateValues: SubscriptionLayouts.TemplateData
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Variables
2+
# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN
3+
freeTrialEndingReminder-subject = Your { $productName } free trial ends soon
4+
5+
# Variables:
6+
# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN
7+
freeTrialEndingReminder-content-greeting = Dear { $productName } customer,
8+
9+
# Variables:
10+
# $serviceLastActiveDateOnly (String) - The date the free trial ends, e.g. January 20, 2016
11+
freeTrialEndingReminder-content-trial-ending = Your free trial is scheduled to end on <strong>{ $serviceLastActiveDateOnly }</strong>.
12+
freeTrialEndingReminder-content-trial-ending-plaintext = Your free trial is scheduled to end on { $serviceLastActiveDateOnly }.
13+
14+
# Variables:
15+
# $invoiceTotal (String) - The total amount that will be charged, e.g. $9.99
16+
# $serviceLastActiveDateOnly (String) - The date the charge will occur, e.g. January 20, 2016
17+
freeTrialEndingReminder-content-auto-charge = Unless you cancel before then, your subscription will automatically begin and we'll charge <strong>{ $invoiceTotal }</strong> to the payment method on your account on <strong>{ $serviceLastActiveDateOnly }</strong>.
18+
freeTrialEndingReminder-content-auto-charge-plaintext = Unless you cancel before then, your subscription will automatically begin and we'll charge { $invoiceTotal } to the payment method on your account on { $serviceLastActiveDateOnly }.
19+
20+
freeTrialEndingReminder-content-charge-heading = Charge details
21+
22+
# Variables:
23+
# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN
24+
# $invoiceSubtotal (String) - The subtotal amount of the subscription, e.g. $12.99
25+
freeTrialEndingReminder-content-charge-subscription = { $productName } subscription: { $invoiceSubtotal }
26+
27+
# Variables:
28+
# $invoiceDiscountAmount (String) - The discount amount, as a negative number, e.g. -$3.00
29+
freeTrialEndingReminder-content-charge-discount = Discount: { $invoiceDiscountAmount }
30+
31+
# Variables:
32+
# $invoiceTaxAmount (String) - The tax amount, e.g. $1.20
33+
freeTrialEndingReminder-content-charge-tax = Tax: { $invoiceTaxAmount }
34+
35+
# Variables:
36+
# $serviceLastActiveDateOnly (String) - The date the charge will occur, e.g. January 20, 2016
37+
# $invoiceTotal (String) - The total amount due, e.g. $9.99
38+
freeTrialEndingReminder-content-charge-total = Total due on { $serviceLastActiveDateOnly }: { $invoiceTotal }
39+
40+
freeTrialEndingReminder-content-account-link = You can review or update your payment method and account information <a data-l10n-name="freeTrialEndingReminder-update-billing">here</a>.
41+
freeTrialEndingReminder-content-account-link-plaintext = You can review or update your payment method and account information here:
42+
43+
# Variables:
44+
# $serviceLastActiveDateOnly (String) - The date the trial ends, e.g. January 20, 2016
45+
freeTrialEndingReminder-content-cancel-link = To avoid being charged, cancel before <strong>{ $serviceLastActiveDateOnly }</strong>: <a data-l10n-name="freeTrialEndingReminder-cancel-subscription">Cancel subscription</a>
46+
freeTrialEndingReminder-content-cancel-link-plaintext = To avoid being charged, cancel before { $serviceLastActiveDateOnly }:
47+
48+
# Variables:
49+
# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN
50+
freeTrialEndingReminder-content-thanks = Thank you for trying { $productName }. If you have any questions about your trial or subscription, please <a data-l10n-name="freeTrialEndingReminder-contact-support">contact us</a>.
51+
freeTrialEndingReminder-content-thanks-plaintext = Thank you for trying { $productName }. If you have any questions about your trial or subscription, please contact us.
52+
53+
freeTrialEndingReminder-content-closing = Sincerely,
54+
55+
# Variables:
56+
# $productName (String) - The name of the subscribed product, e.g. Mozilla VPN
57+
freeTrialEndingReminder-content-signature = The { $productName } team
58+
59+
# Variables:
60+
# $subscriptionSupportUrlWithUtm (String) - URL to the subscription products support page
61+
freeTrialEndingReminder-content-support-plaintext = Contact us: { $subscriptionSupportUrlWithUtm }
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<%# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/. %>
4+
5+
<%- include ('/partials/icon/index.mjml') %>
6+
7+
<mj-section>
8+
<mj-column>
9+
<mj-text css-class="text-header">
10+
<span data-l10n-id="freeTrialEndingReminder-subject" data-l10n-args="<%= JSON.stringify({ productName }) %>">Your <%- productName %> free trial ends soon</span>
11+
</mj-text>
12+
13+
<mj-text css-class="text-body">
14+
<span data-l10n-id="freeTrialEndingReminder-content-greeting" data-l10n-args="<%= JSON.stringify({ productName }) %>">
15+
Dear <%- productName %> customer,
16+
</span>
17+
</mj-text>
18+
19+
<mj-text css-class="text-body">
20+
<span data-l10n-id="freeTrialEndingReminder-content-trial-ending" data-l10n-args="<%= JSON.stringify({ serviceLastActiveDateOnly }) %>">
21+
Your free trial is scheduled to end on <strong><%- serviceLastActiveDateOnly %></strong>.
22+
</span>
23+
</mj-text>
24+
25+
<mj-text css-class="text-body">
26+
<span data-l10n-id="freeTrialEndingReminder-content-auto-charge" data-l10n-args="<%= JSON.stringify({ invoiceTotal, serviceLastActiveDateOnly }) %>">
27+
Unless you cancel before then, your subscription will automatically begin and we'll charge <strong><%- invoiceTotal %></strong> to the payment method on your account on <strong><%- serviceLastActiveDateOnly %></strong>.
28+
</span>
29+
</mj-text>
30+
31+
<mj-text css-class="text-title-table">
32+
<span data-l10n-id="freeTrialEndingReminder-content-charge-heading">Charge details</span>
33+
</mj-text>
34+
35+
<mj-text css-class="text-body-top-margin">
36+
<table width="100%">
37+
<tr style="line-height: 24px;">
38+
<td style="text-align: start;">
39+
<span data-l10n-id="freeTrialEndingReminder-content-charge-subscription" data-l10n-args="<%= JSON.stringify({ productName, invoiceSubtotal }) %>">
40+
<%- productName %> subscription
41+
</span>
42+
</td>
43+
<td style="text-align: end;">
44+
<%- invoiceSubtotal %>
45+
</td>
46+
</tr>
47+
</table>
48+
</mj-text>
49+
50+
<% if (locals.showDiscount && locals.invoiceDiscountAmount) { %>
51+
<mj-text css-class="text-body-table-no-bottom-margin">
52+
<table width="100%">
53+
<tr style="line-height: 24px; border-top: 1px solid #E0E0E6;">
54+
<td style="text-align: start;">
55+
<span data-l10n-id="freeTrialEndingReminder-content-charge-discount" data-l10n-args="<%= JSON.stringify({ invoiceDiscountAmount }) %>">
56+
Discount
57+
</span>
58+
</td>
59+
<td style="text-align: end;">
60+
<%- invoiceDiscountAmount %>
61+
</td>
62+
</tr>
63+
</table>
64+
</mj-text>
65+
<% } %>
66+
67+
<% if (locals.showTaxAmount && locals.invoiceTaxAmount) { %>
68+
<mj-text css-class="text-body-table-no-bottom-margin">
69+
<table width="100%">
70+
<tr style="line-height: 24px; border-top: 1px solid #E0E0E6;">
71+
<td style="text-align: start;">
72+
<span data-l10n-id="freeTrialEndingReminder-content-charge-tax" data-l10n-args="<%= JSON.stringify({ invoiceTaxAmount }) %>">
73+
Tax
74+
</span>
75+
</td>
76+
<td style="text-align: end;">
77+
<%- invoiceTaxAmount %>
78+
</td>
79+
</tr>
80+
</table>
81+
</mj-text>
82+
<% } %>
83+
84+
<mj-text css-class="text-body">
85+
<table width="100%">
86+
<tr style="line-height: 24px; border-top: 1px solid grey;">
87+
<td style="text-align: start; padding: 4px 0;">
88+
<span data-l10n-id="freeTrialEndingReminder-content-charge-total" data-l10n-args="<%= JSON.stringify({ serviceLastActiveDateOnly, invoiceTotal }) %>">
89+
<b>Total due on <%- serviceLastActiveDateOnly %></b>
90+
</span>
91+
</td>
92+
<td style="text-align: end; padding: 4px 0;">
93+
<b><%- invoiceTotal %></b>
94+
</td>
95+
</tr>
96+
</table>
97+
</mj-text>
98+
99+
<mj-text css-class="text-body">
100+
<span data-l10n-id="freeTrialEndingReminder-content-account-link" data-l10n-args="<%= JSON.stringify({ updateBillingUrl }) %>">
101+
You can review or update your payment method and account information <a href="<%- updateBillingUrl %>" class="link-blue" data-l10n-name="freeTrialEndingReminder-update-billing">here</a>.
102+
</span>
103+
</mj-text>
104+
105+
<mj-text css-class="text-body">
106+
<span data-l10n-id="freeTrialEndingReminder-content-cancel-link" data-l10n-args="<%= JSON.stringify({ serviceLastActiveDateOnly, cancelSubscriptionUrl }) %>">
107+
To avoid being charged, cancel before <strong><%- serviceLastActiveDateOnly %></strong>: <a href="<%- cancelSubscriptionUrl %>" class="link-blue" data-l10n-name="freeTrialEndingReminder-cancel-subscription">Cancel subscription</a>
108+
</span>
109+
</mj-text>
110+
111+
<mj-text css-class="text-body">
112+
<span data-l10n-id="freeTrialEndingReminder-content-thanks" data-l10n-args="<%= JSON.stringify({ productName, subscriptionSupportUrlWithUtm }) %>">
113+
Thank you for trying <%- productName %>. If you have any questions about your trial or subscription, please <a href="<%- subscriptionSupportUrlWithUtm %>" class="link-blue" data-l10n-name="freeTrialEndingReminder-contact-support">contact us</a>.
114+
</span>
115+
</mj-text>
116+
117+
<mj-text css-class="text-body-no-bottom-margin">
118+
<span data-l10n-id="freeTrialEndingReminder-content-closing">Sincerely,</span>
119+
</mj-text>
120+
121+
<mj-text css-class="text-body">
122+
<span data-l10n-id="freeTrialEndingReminder-content-signature" data-l10n-args="<%= JSON.stringify({ productName }) %>">The <%- productName %> team</span>
123+
</mj-text>
124+
</mj-column>
125+
</mj-section>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { Meta } from '@storybook/html';
6+
import { subplatStoryWithProps } from '../../storybook-email';
7+
import { includes, TemplateData } from './index';
8+
9+
export default {
10+
title: 'SubPlat Emails/Templates/freeTrialEndingReminder',
11+
} as Meta;
12+
13+
const data = {
14+
productName: '123Done Pro',
15+
serviceLastActiveDateOnly: 'July 15, 2025',
16+
invoiceTotal: '$9.99',
17+
invoiceSubtotal: '$12.99',
18+
invoiceDiscountAmount: '$3.00',
19+
invoiceTaxAmount: '$1.20',
20+
showTaxAmount: true,
21+
showDiscount: true,
22+
updateBillingUrl: 'http://localhost:3030/subscriptions',
23+
cancelSubscriptionUrl: 'http://localhost:3030/subscriptions',
24+
subscriptionSupportUrlWithUtm: 'http://localhost:3030/support',
25+
productIconURLNew:
26+
'https://cdn.accounts.firefox.com/product-icons/mozilla-vpn-email.png',
27+
};
28+
29+
const createStory = subplatStoryWithProps<TemplateData>(
30+
'freeTrialEndingReminder',
31+
'Sent to remind a user their free trial is ending and will convert to a paid subscription.',
32+
data,
33+
includes
34+
);
35+
36+
export const FreeTrialEndingReminderFull = createStory(
37+
{},
38+
'With Tax and Discount'
39+
);
40+
41+
export const FreeTrialEndingReminderNoTaxNoDiscount = createStory(
42+
{
43+
showTaxAmount: false,
44+
showDiscount: false,
45+
invoiceTotal: '$12.99',
46+
},
47+
'Without Tax or Discount'
48+
);
49+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
export type TemplateData = {
6+
productName: string;
7+
serviceLastActiveDateOnly: string;
8+
invoiceTotal: string;
9+
invoiceSubtotal: string;
10+
invoiceDiscountAmount?: string;
11+
invoiceTaxAmount?: string;
12+
showTaxAmount: boolean;
13+
showDiscount: boolean;
14+
updateBillingUrl: string;
15+
cancelSubscriptionUrl: string;
16+
subscriptionSupportUrlWithUtm: string;
17+
productIconURLNew: string;
18+
};
19+
20+
export const template = 'freeTrialEndingReminder';
21+
export const version = 1;
22+
export const layout = 'subscription';
23+
export const includes = {
24+
subject: {
25+
id: 'freeTrialEndingReminder-subject',
26+
message: 'Your <%- productName %> free trial ends soon',
27+
},
28+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
freeTrialEndingReminder-subject = "Your <%- productName %> free trial ends soon"
2+
3+
freeTrialEndingReminder-content-greeting = "Dear <%- productName %> customer,"
4+
5+
freeTrialEndingReminder-content-trial-ending-plaintext = "Your free trial is scheduled to end on <%- serviceLastActiveDateOnly %>."
6+
7+
freeTrialEndingReminder-content-auto-charge-plaintext = "Unless you cancel before then, your subscription will automatically begin and we'll charge <%- invoiceTotal %> to the payment method on your account on <%- serviceLastActiveDateOnly %>."
8+
9+
freeTrialEndingReminder-content-charge-heading = "Charge details"
10+
11+
freeTrialEndingReminder-content-charge-subscription = "<%- productName %> subscription: <%- invoiceSubtotal %>"
12+
13+
<% if (locals.showDiscount && locals.invoiceDiscountAmount) { %>
14+
freeTrialEndingReminder-content-charge-discount = "Discount: <%- invoiceDiscountAmount %>"
15+
<% } %>
16+
17+
<% if (locals.showTaxAmount && locals.invoiceTaxAmount) { %>
18+
freeTrialEndingReminder-content-charge-tax = "Tax: <%- invoiceTaxAmount %>"
19+
<% } %>
20+
21+
freeTrialEndingReminder-content-charge-total = "Total due on <%- serviceLastActiveDateOnly %>: <%- invoiceTotal %>"
22+
23+
freeTrialEndingReminder-content-account-link-plaintext = "You can review or update your payment method and account information here:"
24+
<%- updateBillingUrl %>
25+
26+
freeTrialEndingReminder-content-cancel-link-plaintext = "To avoid being charged, cancel before <%- serviceLastActiveDateOnly %>:"
27+
<%- cancelSubscriptionUrl %>
28+
29+
freeTrialEndingReminder-content-thanks-plaintext = "Thank you for trying <%- productName %>. If you have any questions about your trial or subscription, please contact us."
30+
31+
freeTrialEndingReminder-content-closing = "Sincerely,"
32+
33+
freeTrialEndingReminder-content-signature = "The <%- productName %> team"
34+
35+
freeTrialEndingReminder-content-support-plaintext = "Contact us: <%- subscriptionSupportUrlWithUtm %>"

libs/accounts/email-renderer/src/templates/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export * as verificationReminderFinal from './verificationReminderFinal';
4646
export * as cadReminderFirst from './cadReminderFirst';
4747
export * as cadReminderSecond from './cadReminderSecond';
4848
export * as downloadSubscription from './downloadSubscription';
49+
export * as freeTrialEndingReminder from './freeTrialEndingReminder';
4950
export * as fraudulentAccountDeletion from './fraudulentAccountDeletion';
5051
export * as inactiveAccountFirstWarning from './inactiveAccountFirstWarning';
5152
export * as inactiveAccountSecondWarning from './inactiveAccountSecondWarning';

libs/payments/customer/src/lib/subscription.manager.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,47 @@ describe('SubscriptionManager', () => {
139139
});
140140
});
141141

142+
describe('listTrialingGenerator', () => {
143+
const mockCurrentPeriodEnd = StripeRangeQueryParamFactory();
144+
it('returns generator that yields trialing subscriptions', async () => {
145+
const mockSubscription = StripeSubscriptionFactory({
146+
status: 'trialing',
147+
});
148+
const mockGenerator = StripeSubscriptionAsyncGeneratorFactory([
149+
mockSubscription,
150+
]);
151+
const expected = mockSubscription;
152+
153+
jest
154+
.spyOn(stripeClient, 'subscriptionsListGenerator')
155+
.mockReturnValue(mockGenerator);
156+
157+
const generator =
158+
subscriptionManager.listTrialingGenerator(mockCurrentPeriodEnd);
159+
const result = (await generator.next()).value;
160+
161+
expect(result).toEqual(expected);
162+
expect(stripeClient.subscriptionsListGenerator).toHaveBeenCalledWith({
163+
status: 'trialing',
164+
current_period_end: mockCurrentPeriodEnd,
165+
});
166+
});
167+
168+
it('returns generator that yields no subscriptions when none are trialing', async () => {
169+
const mockGenerator = StripeSubscriptionAsyncGeneratorFactory([]);
170+
171+
jest
172+
.spyOn(stripeClient, 'subscriptionsListGenerator')
173+
.mockReturnValue(mockGenerator);
174+
175+
const generator =
176+
subscriptionManager.listTrialingGenerator(mockCurrentPeriodEnd);
177+
const result = (await generator.next()).value;
178+
179+
expect(result).toEqual(undefined);
180+
});
181+
});
182+
142183
describe('cancel', () => {
143184
it('calls stripeclient', async () => {
144185
const mockSubscription = StripeSubscriptionFactory();

0 commit comments

Comments
 (0)