Handled updated tiers API data structure (#247)

refs https://github.com/TryGhost/Team/issues/1575

- updated to work with new tiers api data structure which removes stripe id references
This commit is contained in:
Rishabh Garg 2022-05-17 14:03:51 +05:30 committed by GitHub
parent 7699424cee
commit b306b0747d
7 changed files with 181 additions and 37 deletions

View File

@ -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}) { function switchPage({data, state}) {
return { return {
@ -99,7 +99,8 @@ async function signup({data, state, api}) {
if (plan.toLowerCase() === 'free') { if (plan.toLowerCase() === 'free') {
await api.member.sendMagicLink(data); await api.member.sendMagicLink(data);
} else { } 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 { return {
page: 'magiclink', page: 'magiclink',
@ -119,8 +120,11 @@ async function signup({data, state, api}) {
async function checkoutPlan({data, state, api}) { async function checkoutPlan({data, state, api}) {
try { try {
const {plan, offerId} = data; const {plan, offerId} = data;
const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: plan});
await api.member.checkoutPlan({ await api.member.checkoutPlan({
plan, plan,
tierId,
cadence,
offerId, offerId,
metadata: { metadata: {
checkoutType: 'upgrade' checkoutType: 'upgrade'
@ -140,8 +144,15 @@ async function checkoutPlan({data, state, api}) {
async function updateSubscription({data, state, api}) { async function updateSubscription({data, state, api}) {
try { try {
const {plan, planId, subscriptionId, cancelAtPeriodEnd} = data; const {plan, planId, subscriptionId, cancelAtPeriodEnd} = data;
const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: planId});
await api.member.updateSubscription({ 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 member = await api.member.sessionData();
const action = 'updateSubscription:success'; const action = 'updateSubscription:success';

View File

@ -1,7 +1,7 @@
import React, {useContext, useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {ReactComponent as LoaderIcon} from '../../images/icons/loader.svg'; import {ReactComponent as LoaderIcon} from '../../images/icons/loader.svg';
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.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 AppContext from '../../AppContext';
import calculateDiscount from '../../utils/discount'; import calculateDiscount from '../../utils/discount';
@ -882,7 +882,7 @@ function ProductDescription({product, selectedPrice, activePrice}) {
} }
function ChangeProductCard({product, onPlanSelect}) { function ChangeProductCard({product, onPlanSelect}) {
const {member} = useContext(AppContext); const {member, site} = useContext(AppContext);
const {selectedProduct, setSelectedProduct, selectedInterval} = useContext(ProductsContext); const {selectedProduct, setSelectedProduct, selectedInterval} = useContext(ProductsContext);
const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card'; const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card';
const monthlyPrice = product.monthlyPrice; const monthlyPrice = product.monthlyPrice;
@ -891,7 +891,7 @@ function ChangeProductCard({product, onPlanSelect}) {
const selectedPrice = selectedInterval === 'month' ? monthlyPrice : yearlyPrice; const selectedPrice = selectedInterval === 'month' ? monthlyPrice : yearlyPrice;
const currentPlan = (selectedPrice.id === memberActivePrice.id); const currentPlan = isMemberActivePrice({member, site, priceId: selectedPrice.id});
return ( return (
<div className={cardClass + (currentPlan ? ' disabled' : '')} key={product.id} onClick={(e) => { <div className={cardClass + (currentPlan ? ' disabled' : '')} key={product.id} onClick={(e) => {

View File

@ -327,7 +327,9 @@ describe('Signup', () => {
email: 'jamie@example.com', email: 'jamie@example.com',
name: 'Jamie Larsen', name: 'Jamie Larsen',
offerId: undefined, 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', email: 'jamie@example.com',
name: 'Jamie Larsen', name: 'Jamie Larsen',
offerId: undefined, 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); const magicLink = await within(popupIframeDocument).findByText(/now check your email/i);
expect(magicLink).toBeInTheDocument(); expect(magicLink).toBeInTheDocument();
@ -408,7 +412,9 @@ describe('Signup', () => {
email: 'jamie@example.com', email: 'jamie@example.com',
name: '', name: '',
offerId: undefined, 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', email: 'jamie@example.com',
name: 'Jamie Larsen', name: 'Jamie Larsen',
offerId: undefined, offerId: undefined,
plan: singleTierProduct.yearlyPrice.id plan: singleTierProduct.yearlyPrice.id,
tierId: singleTierProduct.id,
cadence: 'year'
}); });
}); });
@ -459,6 +467,7 @@ describe('Signup', () => {
offer: FixtureOffer offer: FixtureOffer
}); });
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; 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; let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument(); expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument(); expect(triggerButtonFrame).toBeInTheDocument();
@ -480,7 +489,9 @@ describe('Signup', () => {
email: 'jamie@example.com', email: 'jamie@example.com',
name: 'Jamie Larsen', name: 'Jamie Larsen',
offerId, offerId,
plan: planId plan: planId,
tierId: tier.id,
cadence: 'month'
}); });
window.location.hash = ''; window.location.hash = '';
@ -501,6 +512,7 @@ describe('Signup', () => {
offer: FixtureOffer offer: FixtureOffer
}); });
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; 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; let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument(); expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).not.toBeInTheDocument(); expect(triggerButtonFrame).not.toBeInTheDocument();
@ -516,7 +528,9 @@ describe('Signup', () => {
email: undefined, email: undefined,
name: undefined, name: undefined,
offerId: offerId, offerId: offerId,
plan: planId plan: planId,
tierId: tier.id,
cadence: 'month'
}); });
window.location.hash = ''; window.location.hash = '';
@ -679,6 +693,7 @@ describe('Signup', () => {
offer: FixtureOffer offer: FixtureOffer
}); });
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; 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; let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument(); expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument(); expect(triggerButtonFrame).toBeInTheDocument();
@ -700,7 +715,9 @@ describe('Signup', () => {
email: 'jamie@example.com', email: 'jamie@example.com',
name: 'Jamie Larsen', name: 'Jamie Larsen',
offerId, offerId,
plan: planId plan: planId,
tierId: tier.id,
cadence: 'month'
}); });
window.location.hash = ''; window.location.hash = '';
@ -720,6 +737,7 @@ describe('Signup', () => {
site, site,
offer: FixtureOffer 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 planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id;
let offerId = FixtureOffer.id; let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument(); expect(popupFrame).toBeInTheDocument();
@ -736,7 +754,9 @@ describe('Signup', () => {
email: undefined, email: undefined,
name: undefined, name: undefined,
offerId: offerId, offerId: offerId,
plan: planId plan: planId,
tierId: singleTier.id,
cadence: 'month'
}); });
window.location.hash = ''; window.location.hash = '';

View File

@ -215,7 +215,9 @@ describe('Logged-in free member', () => {
checkoutType: 'upgrade' checkoutType: 'upgrade'
}, },
offerId: undefined, 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' checkoutType: 'upgrade'
}, },
offerId: undefined, 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 offer: FixtureOffer
}); });
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; 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; let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument(); expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument(); expect(triggerButtonFrame).toBeInTheDocument();
@ -285,7 +290,9 @@ describe('Logged-in free member', () => {
email: 'jimmie@example.com', email: 'jimmie@example.com',
name: 'Jimmie Larson', name: 'Jimmie Larson',
offerId, offerId,
plan: planId plan: planId,
tierId: singleTierProduct.id,
cadence: 'month'
}); });
window.location.hash = ''; window.location.hash = '';
@ -308,6 +315,7 @@ describe('Logged-in free member', () => {
offer: FixtureOffer offer: FixtureOffer
}); });
let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; 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; let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument(); expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).not.toBeInTheDocument(); expect(triggerButtonFrame).not.toBeInTheDocument();
@ -324,7 +332,9 @@ describe('Logged-in free member', () => {
checkoutType: 'upgrade' checkoutType: 'upgrade'
}, },
offerId: offerId, offerId: offerId,
plan: planId plan: planId,
tierId: singleTierProduct.id,
cadence: 'month'
}); });
window.location.hash = ''; window.location.hash = '';
@ -364,7 +374,9 @@ describe('Logged-in free member', () => {
checkoutType: 'upgrade' checkoutType: 'upgrade'
}, },
offerId: undefined, 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 offer: FixtureOffer
}); });
let planId = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid').monthlyPrice.id; 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; let offerId = FixtureOffer.id;
expect(popupFrame).toBeInTheDocument(); expect(popupFrame).toBeInTheDocument();
expect(triggerButtonFrame).toBeInTheDocument(); expect(triggerButtonFrame).toBeInTheDocument();
@ -399,7 +412,9 @@ describe('Logged-in free member', () => {
email: 'jimmie@example.com', email: 'jimmie@example.com',
name: 'Jimmie Larson', name: 'Jimmie Larson',
offerId, offerId,
plan: planId plan: planId,
tierId: singleTierProduct.id,
cadence: 'month'
}); });
window.location.hash = ''; window.location.hash = '';

View File

@ -1,4 +1,4 @@
import {transformApiSiteData} from './helpers'; import {transformApiSiteData, transformApiTiersData} from './helpers';
function getAnalyticsMetadata() { function getAnalyticsMetadata() {
const analyticsTag = document.querySelector('meta[name=ghost-analytics-id]'); 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 siteUrlObj = new URL(siteUrl);
const identity = await api.member.identity(); const identity = await api.member.identity();
const url = endpointFor({type: 'members', resource: 'create-stripe-checkout-session'}); 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, fp_tid: (window.FPROM || window.$FPROM)?.data?.tid,
...metadata ...metadata
}; };
const analyticsData = getAnalyticsMetadata();
if (analyticsData) { const body = {
metadataObj.ghost_analytics_entry_id = analyticsData.entry_id; priceId: offerId ? null : plan,
metadataObj.ghost_analytics_source_url = analyticsData.source_url; 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({ return makeRequest({
url, url,
@ -339,15 +349,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify(body)
priceId: offerId ? null : plan,
offerId,
identity: identity,
metadata: metadataObj,
successUrl,
cancelUrl,
customerEmail: customerEmail
})
}).then(function (res) { }).then(function (res) {
if (!res.ok) { if (!res.ok) {
throw new Error('Could not create stripe checkout session'); 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 identity = await api.member.identity();
const url = endpointFor({type: 'members', resource: 'subscriptions'}) + subscriptionId + '/'; const url = endpointFor({type: 'members', resource: 'subscriptions'}) + subscriptionId + '/';
const body = { const body = {
@ -427,6 +429,13 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
if (body) { if (body) {
body.metadata = analyticsData; body.metadata = analyticsData;
} }
if (tierId && cadence) {
delete body.priceId;
body.tierId = tierId;
body.cadence = cadence;
}
return makeRequest({ return makeRequest({
url, url,
method: 'PUT', method: 'PUT',
@ -456,7 +465,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) {
site = { site = {
...settings, ...settings,
newsletters, newsletters,
tiers tiers: transformApiTiersData({tiers})
}; };
} catch (e) { } catch (e) {
// Ignore // Ignore

View File

@ -110,6 +110,8 @@ export function getPriceFromSubscription({subscription}) {
id: subscription.price.price_id, id: subscription.price.price_id,
price: subscription.price.amount / 100, price: subscription.price.amount / 100,
name: subscription.price.nickname, name: subscription.price.nickname,
tierId: subscription.tier?.id,
cadence: subscription.price?.interval === 'month' ? 'month' : 'year',
currency: subscription.price.currency.toLowerCase(), currency: subscription.price.currency.toLowerCase(),
currency_symbol: getCurrencySymbol(subscription.price.currency) currency_symbol: getCurrencySymbol(subscription.price.currency)
}; };
@ -122,6 +124,15 @@ export function getMemberActivePrice({member}) {
return getPriceFromSubscription({subscription}); 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}) { export function getSubscriptionFromId({member, subscriptionId}) {
if (isPaidMember({member})) { if (isPaidMember({member})) {
const subscriptions = member.subscriptions || []; 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}) { export function getAvailablePrices({site, products = null}) {
const { const {
portal_plans: portalPlans = [], portal_plans: portalPlans = [],
@ -659,3 +688,61 @@ export const getUpdatedOfferPrice = ({offer, price, useFormatted = false}) => {
export const isActiveOffer = ({offer}) => { export const isActiveOffer = ({offer}) => {
return offer?.status === 'active'; 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
};
});
};

View File

@ -169,6 +169,8 @@ describe('Helpers - ', () => {
const value = getPriceFromSubscription({subscription}); const value = getPriceFromSubscription({subscription});
expect(value).toStrictEqual({ expect(value).toStrictEqual({
...subscription.price, ...subscription.price,
tierId: undefined,
cadence: 'year',
stripe_price_id: subscription.price.id, stripe_price_id: subscription.price.id,
id: subscription.price.price_id, id: subscription.price.price_id,
price: subscription.price.amount / 100, price: subscription.price.amount / 100,