diff --git a/ghost/portal/src/actions.js b/ghost/portal/src/actions.js
index 10b8807d7b..ad2bb36c84 100644
--- a/ghost/portal/src/actions.js
+++ b/ghost/portal/src/actions.js
@@ -1,4 +1,4 @@
-import {createPopupNotification, getMemberEmail, getMemberName, removePortalLinkFromUrl} from './utils/helpers';
+import {createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl} from './utils/helpers';
function switchPage({data, state}) {
return {
@@ -99,7 +99,8 @@ async function signup({data, state, api}) {
if (plan.toLowerCase() === 'free') {
await api.member.sendMagicLink(data);
} else {
- await api.member.checkoutPlan({plan, email, name, newsletters, offerId});
+ const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: plan});
+ await api.member.checkoutPlan({plan, tierId, cadence, email, name, newsletters, offerId});
}
return {
page: 'magiclink',
@@ -119,8 +120,11 @@ async function signup({data, state, api}) {
async function checkoutPlan({data, state, api}) {
try {
const {plan, offerId} = data;
+ const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: plan});
await api.member.checkoutPlan({
plan,
+ tierId,
+ cadence,
offerId,
metadata: {
checkoutType: 'upgrade'
@@ -140,8 +144,15 @@ async function checkoutPlan({data, state, api}) {
async function updateSubscription({data, state, api}) {
try {
const {plan, planId, subscriptionId, cancelAtPeriodEnd} = data;
+ const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: planId});
+
await api.member.updateSubscription({
- planName: plan, subscriptionId, cancelAtPeriodEnd, planId: planId
+ planName: plan,
+ tierId,
+ cadence,
+ subscriptionId,
+ cancelAtPeriodEnd,
+ planId: planId
});
const member = await api.member.sessionData();
const action = 'updateSubscription:success';
diff --git a/ghost/portal/src/components/common/ProductsSection.js b/ghost/portal/src/components/common/ProductsSection.js
index 7bff1e6599..75f57d7120 100644
--- a/ghost/portal/src/components/common/ProductsSection.js
+++ b/ghost/portal/src/components/common/ProductsSection.js
@@ -1,7 +1,7 @@
import React, {useContext, useEffect, useState} from 'react';
import {ReactComponent as LoaderIcon} from '../../images/icons/loader.svg';
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
-import {getCurrencySymbol, getPriceString, getStripeAmount, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct, getFreeProductBenefits, formatNumber, isCookiesDisabled, hasOnlyFreeProduct} from '../../utils/helpers';
+import {getCurrencySymbol, getPriceString, getStripeAmount, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct, getFreeProductBenefits, formatNumber, isCookiesDisabled, hasOnlyFreeProduct, isMemberActivePrice} from '../../utils/helpers';
import AppContext from '../../AppContext';
import calculateDiscount from '../../utils/discount';
@@ -882,7 +882,7 @@ function ProductDescription({product, selectedPrice, activePrice}) {
}
function ChangeProductCard({product, onPlanSelect}) {
- const {member} = useContext(AppContext);
+ const {member, site} = useContext(AppContext);
const {selectedProduct, setSelectedProduct, selectedInterval} = useContext(ProductsContext);
const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card';
const monthlyPrice = product.monthlyPrice;
@@ -891,7 +891,7 @@ function ChangeProductCard({product, onPlanSelect}) {
const selectedPrice = selectedInterval === 'month' ? monthlyPrice : yearlyPrice;
- const currentPlan = (selectedPrice.id === memberActivePrice.id);
+ const currentPlan = isMemberActivePrice({member, site, priceId: selectedPrice.id});
return (
{
diff --git a/ghost/portal/src/tests/SignupFlow.test.js b/ghost/portal/src/tests/SignupFlow.test.js
index 8317b2378c..3a3a81f83a 100644
--- a/ghost/portal/src/tests/SignupFlow.test.js
+++ b/ghost/portal/src/tests/SignupFlow.test.js
@@ -327,7 +327,9 @@ describe('Signup', () => {
email: 'jamie@example.com',
name: 'Jamie Larsen',
offerId: undefined,
- plan: singleTierProduct.yearlyPrice.id
+ plan: singleTierProduct.yearlyPrice.id,
+ tierId: singleTierProduct.id,
+ cadence: 'year'
});
});
@@ -367,7 +369,9 @@ describe('Signup', () => {
email: 'jamie@example.com',
name: 'Jamie Larsen',
offerId: undefined,
- plan: singleTierProduct.yearlyPrice.id
+ plan: singleTierProduct.yearlyPrice.id,
+ tierId: singleTierProduct.id,
+ cadence: 'year'
});
const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
expect(magicLink).toBeInTheDocument();
@@ -408,7 +412,9 @@ describe('Signup', () => {
email: 'jamie@example.com',
name: '',
offerId: undefined,
- plan: singleTierProduct.monthlyPrice.id
+ plan: singleTierProduct.monthlyPrice.id,
+ tierId: singleTierProduct.id,
+ cadence: 'month'
});
});
@@ -444,7 +450,9 @@ describe('Signup', () => {
email: 'jamie@example.com',
name: 'Jamie Larsen',
offerId: undefined,
- plan: singleTierProduct.yearlyPrice.id
+ plan: singleTierProduct.yearlyPrice.id,
+ tierId: singleTierProduct.id,
+ cadence: 'year'
});
});
@@ -459,6 +467,7 @@ describe('Signup', () => {
offer: FixtureOffer
});
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
+ let tier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid');
let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument();
@@ -480,7 +489,9 @@ describe('Signup', () => {
email: 'jamie@example.com',
name: 'Jamie Larsen',
offerId,
- plan: planId
+ plan: planId,
+ tierId: tier.id,
+ cadence: 'month'
});
window.location.hash = '';
@@ -501,6 +512,7 @@ describe('Signup', () => {
offer: FixtureOffer
});
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
+ let tier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid');
let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).not.toBeInTheDocument();
@@ -516,7 +528,9 @@ describe('Signup', () => {
email: undefined,
name: undefined,
offerId: offerId,
- plan: planId
+ plan: planId,
+ tierId: tier.id,
+ cadence: 'month'
});
window.location.hash = '';
@@ -679,6 +693,7 @@ describe('Signup', () => {
offer: FixtureOffer
});
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
+ let tier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid');
let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument();
@@ -700,7 +715,9 @@ describe('Signup', () => {
email: 'jamie@example.com',
name: 'Jamie Larsen',
offerId,
- plan: planId
+ plan: planId,
+ tierId: tier.id,
+ cadence: 'month'
});
window.location.hash = '';
@@ -720,6 +737,7 @@ describe('Signup', () => {
site,
offer: FixtureOffer
});
+ const singleTier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid');
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument();
@@ -736,7 +754,9 @@ describe('Signup', () => {
email: undefined,
name: undefined,
offerId: offerId,
- plan: planId
+ plan: planId,
+ tierId: singleTier.id,
+ cadence: 'month'
});
window.location.hash = '';
diff --git a/ghost/portal/src/tests/UpgradeFlow.test.js b/ghost/portal/src/tests/UpgradeFlow.test.js
index a8b10f2f33..c9842cbb6b 100644
--- a/ghost/portal/src/tests/UpgradeFlow.test.js
+++ b/ghost/portal/src/tests/UpgradeFlow.test.js
@@ -215,7 +215,9 @@ describe('Logged-in free member', () => {
checkoutType: 'upgrade'
},
offerId: undefined,
- plan: singleTierProduct.monthlyPrice.id
+ plan: singleTierProduct.monthlyPrice.id,
+ tierId: singleTierProduct.id,
+ cadence: 'month'
});
});
@@ -250,7 +252,9 @@ describe('Logged-in free member', () => {
checkoutType: 'upgrade'
},
offerId: undefined,
- plan: singleTierProduct.yearlyPrice.id
+ plan: singleTierProduct.yearlyPrice.id,
+ tierId: singleTierProduct.id,
+ cadence: 'year'
});
});
@@ -266,6 +270,7 @@ describe('Logged-in free member', () => {
offer: FixtureOffer
});
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
+ let singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid');
let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument();
@@ -285,7 +290,9 @@ describe('Logged-in free member', () => {
email: 'jimmie@example.com',
name: 'Jimmie Larson',
offerId,
- plan: planId
+ plan: planId,
+ tierId: singleTierProduct.id,
+ cadence: 'month'
});
window.location.hash = '';
@@ -308,6 +315,7 @@ describe('Logged-in free member', () => {
offer: FixtureOffer
});
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
+ let singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid');
let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).not.toBeInTheDocument();
@@ -324,7 +332,9 @@ describe('Logged-in free member', () => {
checkoutType: 'upgrade'
},
offerId: offerId,
- plan: planId
+ plan: planId,
+ tierId: singleTierProduct.id,
+ cadence: 'month'
});
window.location.hash = '';
@@ -364,7 +374,9 @@ describe('Logged-in free member', () => {
checkoutType: 'upgrade'
},
offerId: undefined,
- plan: singleTierProduct.yearlyPrice.id
+ plan: singleTierProduct.yearlyPrice.id,
+ tierId: singleTierProduct.id,
+ cadence: 'year'
});
});
@@ -380,6 +392,7 @@ describe('Logged-in free member', () => {
offer: FixtureOffer
});
let planId = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
+ let singleTierProduct = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid');
let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument();
@@ -399,7 +412,9 @@ describe('Logged-in free member', () => {
email: 'jimmie@example.com',
name: 'Jimmie Larson',
offerId,
- plan: planId
+ plan: planId,
+ tierId: singleTierProduct.id,
+ cadence: 'month'
});
window.location.hash = '';
diff --git a/ghost/portal/src/utils/api.js b/ghost/portal/src/utils/api.js
index 14c11afed7..006905717f 100644
--- a/ghost/portal/src/utils/api.js
+++ b/ghost/portal/src/utils/api.js
@@ -1,4 +1,4 @@
-import {transformApiSiteData} from './helpers';
+import {transformApiSiteData, transformApiTiersData} from './helpers';
function getAnalyticsMetadata() {
const analyticsTag = document.querySelector('meta[name=ghost-analytics-id]');
@@ -311,7 +311,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
});
},
- async checkoutPlan({plan, cancelUrl, successUrl, email: customerEmail, name, offerId, newsletters, metadata = {}} = {}) {
+ async checkoutPlan({plan, tierId, cadence, cancelUrl, successUrl, email: customerEmail, name, offerId, newsletters, metadata = {}} = {}) {
const siteUrlObj = new URL(siteUrl);
const identity = await api.member.identity();
const url = endpointFor({type: 'members', resource: 'create-stripe-checkout-session'});
@@ -328,10 +328,20 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
fp_tid: (window.FPROM || window.$FPROM)?.data?.tid,
...metadata
};
- const analyticsData = getAnalyticsMetadata();
- if (analyticsData) {
- metadataObj.ghost_analytics_entry_id = analyticsData.entry_id;
- metadataObj.ghost_analytics_source_url = analyticsData.source_url;
+
+ const body = {
+ priceId: offerId ? null : plan,
+ offerId,
+ identity: identity,
+ metadata: metadataObj,
+ successUrl,
+ cancelUrl,
+ customerEmail: customerEmail
+ };
+ if (tierId && cadence) {
+ delete body.priceId;
+ body.tierId = offerId ? null : tierId;
+ body.cadence = offerId ? null : cadence;
}
return makeRequest({
url,
@@ -339,15 +349,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
headers: {
'Content-Type': 'application/json'
},
- body: JSON.stringify({
- priceId: offerId ? null : plan,
- offerId,
- identity: identity,
- metadata: metadataObj,
- successUrl,
- cancelUrl,
- customerEmail: customerEmail
- })
+ body: JSON.stringify(body)
}).then(function (res) {
if (!res.ok) {
throw new Error('Could not create stripe checkout session');
@@ -413,7 +415,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
});
},
- async updateSubscription({subscriptionId, planName, planId, smartCancel, cancelAtPeriodEnd, cancellationReason}) {
+ async updateSubscription({subscriptionId, tierId, cadence, planId, smartCancel, cancelAtPeriodEnd, cancellationReason}) {
const identity = await api.member.identity();
const url = endpointFor({type: 'members', resource: 'subscriptions'}) + subscriptionId + '/';
const body = {
@@ -427,6 +429,13 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
if (body) {
body.metadata = analyticsData;
}
+
+ if (tierId && cadence) {
+ delete body.priceId;
+ body.tierId = tierId;
+ body.cadence = cadence;
+ }
+
return makeRequest({
url,
method: 'PUT',
@@ -456,7 +465,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
site = {
...settings,
newsletters,
- tiers
+ tiers: transformApiTiersData({tiers})
};
} catch (e) {
// Ignore
diff --git a/ghost/portal/src/utils/helpers.js b/ghost/portal/src/utils/helpers.js
index f70268ce50..c4ee664f44 100644
--- a/ghost/portal/src/utils/helpers.js
+++ b/ghost/portal/src/utils/helpers.js
@@ -110,6 +110,8 @@ export function getPriceFromSubscription({subscription}) {
id: subscription.price.price_id,
price: subscription.price.amount / 100,
name: subscription.price.nickname,
+ tierId: subscription.tier?.id,
+ cadence: subscription.price?.interval === 'month' ? 'month' : 'year',
currency: subscription.price.currency.toLowerCase(),
currency_symbol: getCurrencySymbol(subscription.price.currency)
};
@@ -122,6 +124,15 @@ export function getMemberActivePrice({member}) {
return getPriceFromSubscription({subscription});
}
+export function isMemberActivePrice({priceId, site, member}) {
+ const activePrice = getMemberActivePrice({member});
+ const {tierId, cadence} = getProductCadenceFromPrice({site, priceId});
+ if (activePrice?.tierId === tierId && activePrice?.cadence === cadence) {
+ return true;
+ }
+ return false;
+}
+
export function getSubscriptionFromId({member, subscriptionId}) {
if (isPaidMember({member})) {
const subscriptions = member.subscriptions || [];
@@ -452,6 +463,24 @@ export function getProductFromPrice({site, priceId}) {
});
}
+export function getProductCadenceFromPrice({site, priceId}) {
+ if (priceId === 'free') {
+ return getFreeProduct({site});
+ }
+ const products = getAllProductsForSite({site});
+ const tier = products.find((product) => {
+ return (product?.monthlyPrice?.id === priceId) || (product?.yearlyPrice?.id === priceId);
+ });
+ let cadence = 'month';
+ if (tier?.yearlyPrice?.id === priceId) {
+ cadence = 'year';
+ }
+ return {
+ tierId: tier?.id,
+ cadence
+ };
+}
+
export function getAvailablePrices({site, products = null}) {
const {
portal_plans: portalPlans = [],
@@ -659,3 +688,61 @@ export const getUpdatedOfferPrice = ({offer, price, useFormatted = false}) => {
export const isActiveOffer = ({offer}) => {
return offer?.status === 'active';
};
+
+function createMonthlyPrice({tier, priceId}) {
+ if (tier?.monthly_price) {
+ return {
+ id: `price-${priceId}`,
+ active: true,
+ type: 'recurring',
+ nickname: 'Monthly',
+ currency: tier.currency,
+ amount: tier.monthly_price,
+ interval: 'month'
+ };
+ }
+ return null;
+}
+
+function createYearlyPrice({tier, priceId}) {
+ if (tier?.yearly_price) {
+ return {
+ id: `price-${priceId}`,
+ active: true,
+ type: 'recurring',
+ nickname: 'Yearly',
+ currency: tier.currency,
+ amount: tier.yearly_price,
+ interval: 'year'
+ };
+ }
+ return null;
+}
+
+function createBenefits({tier}) {
+ tier?.benefits?.map((benefit) => {
+ return {
+ name: benefit
+ };
+ });
+}
+
+export const transformApiTiersData = ({tiers}) => {
+ let priceId = 0;
+
+ return tiers.map((tier) => {
+ let monthlyPrice = createMonthlyPrice({tier, priceId});
+ priceId += 1;
+
+ let yearlyPrice = createYearlyPrice({tier, priceId});
+ priceId += 1;
+
+ let benefits = createBenefits({tier});
+ return {
+ ...tier,
+ benefits: benefits,
+ monthly_price: monthlyPrice,
+ yearly_price: yearlyPrice
+ };
+ });
+};
diff --git a/ghost/portal/src/utils/helpers.test.js b/ghost/portal/src/utils/helpers.test.js
index c57840d549..40356a5406 100644
--- a/ghost/portal/src/utils/helpers.test.js
+++ b/ghost/portal/src/utils/helpers.test.js
@@ -169,6 +169,8 @@ describe('Helpers - ', () => {
const value = getPriceFromSubscription({subscription});
expect(value).toStrictEqual({
...subscription.price,
+ tierId: undefined,
+ cadence: 'year',
stripe_price_id: subscription.price.id,
id: subscription.price.price_id,
price: subscription.price.amount / 100,