diff --git a/ghost/portal/src/App.js b/ghost/portal/src/App.js index 29e1f7ea07..5c290fbe32 100644 --- a/ghost/portal/src/App.js +++ b/ghost/portal/src/App.js @@ -93,13 +93,12 @@ export default class App extends React.Component { const target = event.currentTarget; const pagePath = (target && target.dataset.portal); const {page, pageQuery} = this.getPageFromLinkPath(pagePath) || {}; - if (this.state.initStatus === 'success') { - this.handleSignupQuery({site: this.state.site, pageQuery}); - } - - if (page) { - this.dispatchAction('openPopup', {page, pageQuery}); + if (pageQuery && pageQuery !== 'free') { + this.handleSignupQuery({site: this.state.site, pageQuery}); + } else { + this.dispatchAction('openPopup', {page, pageQuery}); + } } }; const customTriggerSelector = '[data-portal]'; @@ -485,11 +484,11 @@ export default class App extends React.Component { handleSignupQuery({site, pageQuery}) { const queryPrice = getQueryPrice({site: site, priceId: pageQuery}); if (!this.state.member + && pageQuery && pageQuery !== 'free' - && queryPrice ) { removePortalLinkFromUrl(); - this.dispatchAction('signup', {plan: queryPrice.id}); + this.dispatchAction('signup', {plan: queryPrice?.id || pageQuery}); } } diff --git a/ghost/portal/src/components/common/ProductsSection.js b/ghost/portal/src/components/common/ProductsSection.js index d6e1afe55d..05369d9eef 100644 --- a/ghost/portal/src/components/common/ProductsSection.js +++ b/ghost/portal/src/components/common/ProductsSection.js @@ -1,11 +1,11 @@ import React, {useContext, useEffect, useState} from 'react'; import Switch from '../common/Switch'; import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg'; -import {getAllProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled} from '../../utils/helpers'; +import {getSiteProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled} from '../../utils/helpers'; import AppContext from '../../AppContext'; export const ProductsSectionStyles = ({site}) => { - const products = getAllProducts({site}); + const products = getSiteProducts({site}); const noOfProducts = products.length; return ` .gh-portal-products { diff --git a/ghost/portal/src/components/pages/AccountPlanPage.js b/ghost/portal/src/components/pages/AccountPlanPage.js index d59f9f4530..81f104804b 100644 --- a/ghost/portal/src/components/pages/AccountPlanPage.js +++ b/ghost/portal/src/components/pages/AccountPlanPage.js @@ -3,9 +3,9 @@ import AppContext from '../../AppContext'; import ActionButton from '../common/ActionButton'; import CloseButton from '../common/CloseButton'; import BackButton from '../common/BackButton'; -import PlansSection from '../common/PlansSection'; +import PlansSection, {MultipleProductsPlansSection} from '../common/PlansSection'; import {getDateString} from '../../utils/date-time'; -import {formatNumber, getFilteredPrices, getMemberActivePrice, getMemberSubscription, getPriceFromSubscription, getSitePrices, getSubscriptionFromId, isPaidMember} from '../../utils/helpers'; +import {formatNumber, getAvailablePrices, getFilteredPrices, getMemberActivePrice, getMemberSubscription, getPriceFromSubscription, getSubscriptionFromId, getUpgradeProducts, hasMultipleProducts, isPaidMember} from '../../utils/helpers'; export const AccountPlanPageStyles = ` .gh-portal-accountplans-main { @@ -188,7 +188,7 @@ const ChangePlanSection = ({plans, selectedPlan, onPlanSelect, onCancelSubscript return (
- ); }; + export default class AccountPlanPage extends React.Component { static contextType = AppContext; @@ -286,15 +287,19 @@ export default class AccountPlanPage extends React.Component { } getInitialState() { - const {member, site, pageQuery} = this.context; - this.prices = getSitePrices({site, pageQuery, includeFree: false}); + const {member, site} = this.context; + + this.prices = getAvailablePrices({site}); let activePrice = getMemberActivePrice({member}); + + if (activePrice) { + this.prices = getFilteredPrices({prices: this.prices, currency: activePrice.currency}); + } + let selectedPrice = activePrice ? this.prices.find((d) => { return (d.id === activePrice.id); }) : null; - if (selectedPrice) { - this.prices = getFilteredPrices({prices: this.prices, currency: selectedPrice.currency}); - } + // Select first plan as default for free member if (!isPaidMember({member}) && this.prices.length > 0) { selectedPrice = this.prices[0]; diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index 8b072db320..dab266af32 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -5,7 +5,7 @@ import PlansSection from '../common/PlansSection'; import ProductsSection from '../common/ProductsSection'; import InputForm from '../common/InputForm'; import {ValidateInputForm} from '../../utils/form'; -import {getAllProducts, getSitePrices, hasMultipleProducts, hasOnlyFreePlan, isInviteOnlySite} from '../../utils/helpers'; +import {getSiteProducts, getSitePrices, hasMultipleProducts, hasOnlyFreePlan, isInviteOnlySite} from '../../utils/helpers'; import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg'; const React = require('react'); @@ -391,7 +391,7 @@ class SignupPage extends React.Component { renderProducts() { const {site} = this.context; - const products = getAllProducts({site}); + const products = getSiteProducts({site}); return ( <> { + return (getProductCurrency({product}) === activePriceCurrency); + }); +} + export function getFilteredPrices({prices, currency}) { return prices.filter((d) => { return (d.currency || '').toLowerCase() === (currency || '').toLowerCase(); @@ -69,6 +93,7 @@ export function getPriceFromSubscription({subscription}) { id: subscription.price.price_id, price: subscription.price.amount / 100, name: subscription.price.nickname, + currency: subscription.price.currency.toLowerCase(), currency_symbol: getCurrencySymbol(subscription.price.currency) }; } @@ -138,43 +163,44 @@ export function isInviteOnlySite({site = {}, pageQuery = ''}) { } export function hasMultipleProducts({site = {}}) { - const { - products = [] - } = site || {}; - if (site.portal_plans && !site.portal_plans.includes('monthly') && !site.portal_plans.includes('yearly')) { - return false; - } - if (site.portal_products && site.portal_products.length < 2) { - return false; - } + const products = getAvailableProducts({site}); + if (products?.length > 1) { return true; } return false; } -export function getSiteProducts({site = {}}) { - const products = site?.products || []; - return products.filter(product => !!product).sort((productA, productB) => { - return productA?.monthlyPrice?.amount - productB?.monthlyPrice.amount; - }); -} - export function getAvailableProducts({site}) { - const {portal_products: portalProducts} = site; - const products = getSiteProducts({site}).filter((product) => { + const {portal_products: portalProducts, products = [], portal_plans: portalPlans = []} = site || {}; + + if (!portalPlans.includes('monthly') && !portalPlans.includes('yearly')) { + return []; + } + + return products.filter(product => !!product).filter((product) => { if (portalProducts) { return portalProducts.includes(product.id); } return true; + }).sort((productA, productB) => { + return productA?.monthlyPrice?.amount - productB?.monthlyPrice.amount; + }).map((product) => { + product.monthlyPrice = { + ...product.monthlyPrice, + currency_symbol: getCurrencySymbol(product.monthlyPrice.currency) + }; + product.yearlyPrice = { + ...product.yearlyPrice, + currency_symbol: getCurrencySymbol(product.yearlyPrice.currency) + }; + return product; }); - - return products; } -export function getAllProducts({site}) { +export function getSiteProducts({site}) { const products = getAvailableProducts({site}); - if (hasFreeProduct({site}) && products.length > 0) { + if (hasFreeProductPrice({site}) && products.length > 0) { products.unshift({ id: 'free' }); @@ -182,7 +208,7 @@ export function getAllProducts({site}) { return products; } -export function getProductPrices({site}) { +export function getPricesFromProducts({site}) { const products = getAvailableProducts({site}) || []; const prices = products.reduce((accumPrices, product) => { if (product.monthlyPrice && product.yearlyPrice) { @@ -194,63 +220,7 @@ export function getProductPrices({site}) { return prices; } -export function getAvailablePrices({site = {}, includeFree = true} = {}) { - let { - prices, - products, - allow_self_signup: allowSelfSignup, - is_stripe_configured: isStripeConfigured - } = site || {}; - - if (!prices) { - prices = []; - } - - if (products) { - prices = []; - products.forEach((product) => { - if (product.prices) { - prices = prices.concat(product.prices); - } - }); - } - - const plansData = []; - - const stripePrices = prices.filter((d) => { - return !!(d && d.id); - }).map((d) => { - return { - ...d, - price_id: d.id, - price: d.amount / 100, - name: d.nickname, - currency_symbol: getCurrencySymbol(d.currency) - }; - }).filter((price) => { - return price.amount !== 0 && price.type === 'recurring'; - }); - - if (allowSelfSignup && includeFree) { - plansData.push({ - id: 'free', - type: 'free', - price: 0, - currency: 'usd', - currency_symbol: '$', - name: 'Free' - }); - } - - if (isStripeConfigured) { - stripePrices.forEach((price) => { - plansData.push(price); - }); - } - return plansData; -} - -export function hasFreeProduct({site}) { +export function hasFreeProductPrice({site}) { const { allow_self_signup: allowSelfSignup, portal_plans: portalPlans @@ -258,22 +228,19 @@ export function hasFreeProduct({site}) { return allowSelfSignup && portalPlans.includes('free'); } -export function getSitePrices({site = {}, includeFree = true, pageQuery = ''} = {}) { +export function getAvailablePrices({site}) { const { - prices = [], - allow_self_signup: allowSelfSignup, - is_stripe_configured: isStripeConfigured, - portal_plans: portalPlans + portal_plans: portalPlans = [], + is_stripe_configured: isStripeConfigured } = site || {}; - if (!prices) { + if (!isStripeConfigured) { return []; } - const availablePrices = getProductPrices({site}); - const plansData = []; + const productPrices = getPricesFromProducts({site}); - const stripePrices = availablePrices.filter((d) => { + return productPrices.filter((d) => { return !!(d && d.id); }).map((d) => { return { @@ -287,10 +254,10 @@ export function getSitePrices({site = {}, includeFree = true, pageQuery = ''} = return price.amount !== 0 && price.type === 'recurring'; }).filter((price) => { if (price.interval === 'month') { - return (portalPlans || []).includes('monthly'); + return portalPlans.includes('monthly'); } if (price.interval === 'year') { - return (portalPlans || []).includes('yearly'); + return portalPlans.includes('yearly'); } return false; }).sort((a, b) => { @@ -300,19 +267,33 @@ export function getSitePrices({site = {}, includeFree = true, pageQuery = ''} = return 0; } return a.currency.localeCompare(b.currency, undefined, {ignorePunctuation: true}); - }).sort((a, b) => { - return (a.active === b.active) ? 0 : (a.active ? -1 : 1); }); +} + +export function getFreePriceCurrency({site}) { + const stripePrices = getAvailablePrices({site}); + let freePriceCurrencyDetail = { currency: 'usd', currency_symbol: '$' }; - if (stripePrices && stripePrices.length > 0) { + if (stripePrices?.length > 0) { freePriceCurrencyDetail.currency = stripePrices[0].currency; freePriceCurrencyDetail.currency_symbol = stripePrices[0].currency_symbol; } + return freePriceCurrencyDetail; +} - if (allowSelfSignup && portalPlans.includes('free') && includeFree) { +export function getSitePrices({site = {}, pageQuery = ''} = {}) { + const { + allow_self_signup: allowSelfSignup, + portal_plans: portalPlans + } = site || {}; + + const plansData = []; + + if (allowSelfSignup && portalPlans.includes('free')) { + const freePriceCurrencyDetail = getFreePriceCurrency({site}); plansData.push({ id: 'free', type: 'free', @@ -323,9 +304,10 @@ export function getSitePrices({site = {}, includeFree = true, pageQuery = ''} = }); } - const showOnlyFree = pageQuery === 'free' && hasPrice({site, plan: 'free'}); + const showOnlyFree = pageQuery === 'free' && hasFreeProductPrice({site}); - if (isStripeConfigured && !showOnlyFree) { + if (!showOnlyFree) { + const stripePrices = getAvailablePrices({site}); stripePrices.forEach((price) => { plansData.push(price); });