mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-27 04:43:12 +03:00
Cleaned site and product helper usage
no refs - cleans up helpers for site and products to more consistent and predictable usage
This commit is contained in:
parent
c7a3fdc639
commit
21d1c1b9e8
@ -93,14 +93,13 @@ export default class App extends React.Component {
|
|||||||
const target = event.currentTarget;
|
const target = event.currentTarget;
|
||||||
const pagePath = (target && target.dataset.portal);
|
const pagePath = (target && target.dataset.portal);
|
||||||
const {page, pageQuery} = this.getPageFromLinkPath(pagePath) || {};
|
const {page, pageQuery} = this.getPageFromLinkPath(pagePath) || {};
|
||||||
|
|
||||||
if (this.state.initStatus === 'success') {
|
if (this.state.initStatus === 'success') {
|
||||||
|
if (pageQuery && pageQuery !== 'free') {
|
||||||
this.handleSignupQuery({site: this.state.site, pageQuery});
|
this.handleSignupQuery({site: this.state.site, pageQuery});
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (page) {
|
|
||||||
this.dispatchAction('openPopup', {page, pageQuery});
|
this.dispatchAction('openPopup', {page, pageQuery});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const customTriggerSelector = '[data-portal]';
|
const customTriggerSelector = '[data-portal]';
|
||||||
const popupCloseClass = 'gh-portal-close';
|
const popupCloseClass = 'gh-portal-close';
|
||||||
@ -485,11 +484,11 @@ export default class App extends React.Component {
|
|||||||
handleSignupQuery({site, pageQuery}) {
|
handleSignupQuery({site, pageQuery}) {
|
||||||
const queryPrice = getQueryPrice({site: site, priceId: pageQuery});
|
const queryPrice = getQueryPrice({site: site, priceId: pageQuery});
|
||||||
if (!this.state.member
|
if (!this.state.member
|
||||||
|
&& pageQuery
|
||||||
&& pageQuery !== 'free'
|
&& pageQuery !== 'free'
|
||||||
&& queryPrice
|
|
||||||
) {
|
) {
|
||||||
removePortalLinkFromUrl();
|
removePortalLinkFromUrl();
|
||||||
this.dispatchAction('signup', {plan: queryPrice.id});
|
this.dispatchAction('signup', {plan: queryPrice?.id || pageQuery});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, {useContext, useEffect, useState} from 'react';
|
import React, {useContext, useEffect, useState} from 'react';
|
||||||
import Switch from '../common/Switch';
|
import Switch from '../common/Switch';
|
||||||
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
|
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';
|
import AppContext from '../../AppContext';
|
||||||
|
|
||||||
export const ProductsSectionStyles = ({site}) => {
|
export const ProductsSectionStyles = ({site}) => {
|
||||||
const products = getAllProducts({site});
|
const products = getSiteProducts({site});
|
||||||
const noOfProducts = products.length;
|
const noOfProducts = products.length;
|
||||||
return `
|
return `
|
||||||
.gh-portal-products {
|
.gh-portal-products {
|
||||||
|
@ -3,9 +3,9 @@ import AppContext from '../../AppContext';
|
|||||||
import ActionButton from '../common/ActionButton';
|
import ActionButton from '../common/ActionButton';
|
||||||
import CloseButton from '../common/CloseButton';
|
import CloseButton from '../common/CloseButton';
|
||||||
import BackButton from '../common/BackButton';
|
import BackButton from '../common/BackButton';
|
||||||
import PlansSection from '../common/PlansSection';
|
import PlansSection, {MultipleProductsPlansSection} from '../common/PlansSection';
|
||||||
import {getDateString} from '../../utils/date-time';
|
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 = `
|
export const AccountPlanPageStyles = `
|
||||||
.gh-portal-accountplans-main {
|
.gh-portal-accountplans-main {
|
||||||
@ -188,7 +188,7 @@ const ChangePlanSection = ({plans, selectedPlan, onPlanSelect, onCancelSubscript
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className='gh-portal-section gh-portal-accountplans-main'>
|
<div className='gh-portal-section gh-portal-accountplans-main'>
|
||||||
<PlansSection
|
<PlansOrProductSection
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
plans={plans}
|
plans={plans}
|
||||||
selectedPlan={selectedPlan}
|
selectedPlan={selectedPlan}
|
||||||
@ -264,6 +264,7 @@ const PlansContainer = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class AccountPlanPage extends React.Component {
|
export default class AccountPlanPage extends React.Component {
|
||||||
static contextType = AppContext;
|
static contextType = AppContext;
|
||||||
|
|
||||||
@ -286,15 +287,19 @@ export default class AccountPlanPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
const {member, site, pageQuery} = this.context;
|
const {member, site} = this.context;
|
||||||
this.prices = getSitePrices({site, pageQuery, includeFree: false});
|
|
||||||
|
this.prices = getAvailablePrices({site});
|
||||||
let activePrice = getMemberActivePrice({member});
|
let activePrice = getMemberActivePrice({member});
|
||||||
|
|
||||||
|
if (activePrice) {
|
||||||
|
this.prices = getFilteredPrices({prices: this.prices, currency: activePrice.currency});
|
||||||
|
}
|
||||||
|
|
||||||
let selectedPrice = activePrice ? this.prices.find((d) => {
|
let selectedPrice = activePrice ? this.prices.find((d) => {
|
||||||
return (d.id === activePrice.id);
|
return (d.id === activePrice.id);
|
||||||
}) : null;
|
}) : null;
|
||||||
if (selectedPrice) {
|
|
||||||
this.prices = getFilteredPrices({prices: this.prices, currency: selectedPrice.currency});
|
|
||||||
}
|
|
||||||
// Select first plan as default for free member
|
// Select first plan as default for free member
|
||||||
if (!isPaidMember({member}) && this.prices.length > 0) {
|
if (!isPaidMember({member}) && this.prices.length > 0) {
|
||||||
selectedPrice = this.prices[0];
|
selectedPrice = this.prices[0];
|
||||||
|
@ -5,7 +5,7 @@ import PlansSection from '../common/PlansSection';
|
|||||||
import ProductsSection from '../common/ProductsSection';
|
import ProductsSection from '../common/ProductsSection';
|
||||||
import InputForm from '../common/InputForm';
|
import InputForm from '../common/InputForm';
|
||||||
import {ValidateInputForm} from '../../utils/form';
|
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';
|
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
@ -391,7 +391,7 @@ class SignupPage extends React.Component {
|
|||||||
|
|
||||||
renderProducts() {
|
renderProducts() {
|
||||||
const {site} = this.context;
|
const {site} = this.context;
|
||||||
const products = getAllProducts({site});
|
const products = getSiteProducts({site});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProductsSection
|
<ProductsSection
|
||||||
|
@ -8,12 +8,8 @@ export function removePortalLinkFromUrl() {
|
|||||||
|
|
||||||
export function getPortalLinkPath({page}) {
|
export function getPortalLinkPath({page}) {
|
||||||
const Links = {
|
const Links = {
|
||||||
default: '#/portal',
|
|
||||||
signin: '#/portal/signin',
|
signin: '#/portal/signin',
|
||||||
signup: '#/portal/signup',
|
signup: '#/portal/signup'
|
||||||
account: '#/portal/account',
|
|
||||||
'account-plans': '#/portal/account/plans',
|
|
||||||
'account-profile': '#/portal/account/profile'
|
|
||||||
};
|
};
|
||||||
if (Object.keys(Links).includes(page)) {
|
if (Object.keys(Links).includes(page)) {
|
||||||
return Links[page];
|
return Links[page];
|
||||||
@ -55,6 +51,34 @@ export function isPaidMember({member = {}}) {
|
|||||||
return (member && member.paid);
|
return (member && member.paid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUpgradePrices({site, member}) {
|
||||||
|
const activePrice = getMemberActivePrice({member});
|
||||||
|
|
||||||
|
if (activePrice) {
|
||||||
|
return getFilteredPrices({prices: this.prices, currency: activePrice.currency});
|
||||||
|
}
|
||||||
|
return getAvailablePrices({site});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProductCurrency({product}) {
|
||||||
|
if (!product?.monthlyPrice) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return product.monthlyPrice.currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpgradeProducts({site, member}) {
|
||||||
|
const activePrice = getMemberActivePrice({member});
|
||||||
|
const activePriceCurrency = activePrice?.currency;
|
||||||
|
const availableProducts = getAvailableProducts({site});
|
||||||
|
if (!activePrice) {
|
||||||
|
return availableProducts;
|
||||||
|
}
|
||||||
|
return availableProducts.filter((product) => {
|
||||||
|
return (getProductCurrency({product}) === activePriceCurrency);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getFilteredPrices({prices, currency}) {
|
export function getFilteredPrices({prices, currency}) {
|
||||||
return prices.filter((d) => {
|
return prices.filter((d) => {
|
||||||
return (d.currency || '').toLowerCase() === (currency || '').toLowerCase();
|
return (d.currency || '').toLowerCase() === (currency || '').toLowerCase();
|
||||||
@ -69,6 +93,7 @@ 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,
|
||||||
|
currency: subscription.price.currency.toLowerCase(),
|
||||||
currency_symbol: getCurrencySymbol(subscription.price.currency)
|
currency_symbol: getCurrencySymbol(subscription.price.currency)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -138,43 +163,44 @@ export function isInviteOnlySite({site = {}, pageQuery = ''}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasMultipleProducts({site = {}}) {
|
export function hasMultipleProducts({site = {}}) {
|
||||||
const {
|
const products = getAvailableProducts({site});
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (products?.length > 1) {
|
if (products?.length > 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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}) {
|
export function getAvailableProducts({site}) {
|
||||||
const {portal_products: portalProducts} = site;
|
const {portal_products: portalProducts, products = [], portal_plans: portalPlans = []} = site || {};
|
||||||
const products = getSiteProducts({site}).filter((product) => {
|
|
||||||
|
if (!portalPlans.includes('monthly') && !portalPlans.includes('yearly')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return products.filter(product => !!product).filter((product) => {
|
||||||
if (portalProducts) {
|
if (portalProducts) {
|
||||||
return portalProducts.includes(product.id);
|
return portalProducts.includes(product.id);
|
||||||
}
|
}
|
||||||
return true;
|
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});
|
const products = getAvailableProducts({site});
|
||||||
if (hasFreeProduct({site}) && products.length > 0) {
|
if (hasFreeProductPrice({site}) && products.length > 0) {
|
||||||
products.unshift({
|
products.unshift({
|
||||||
id: 'free'
|
id: 'free'
|
||||||
});
|
});
|
||||||
@ -182,7 +208,7 @@ export function getAllProducts({site}) {
|
|||||||
return products;
|
return products;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProductPrices({site}) {
|
export function getPricesFromProducts({site}) {
|
||||||
const products = getAvailableProducts({site}) || [];
|
const products = getAvailableProducts({site}) || [];
|
||||||
const prices = products.reduce((accumPrices, product) => {
|
const prices = products.reduce((accumPrices, product) => {
|
||||||
if (product.monthlyPrice && product.yearlyPrice) {
|
if (product.monthlyPrice && product.yearlyPrice) {
|
||||||
@ -194,63 +220,7 @@ export function getProductPrices({site}) {
|
|||||||
return prices;
|
return prices;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvailablePrices({site = {}, includeFree = true} = {}) {
|
export function hasFreeProductPrice({site}) {
|
||||||
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}) {
|
|
||||||
const {
|
const {
|
||||||
allow_self_signup: allowSelfSignup,
|
allow_self_signup: allowSelfSignup,
|
||||||
portal_plans: portalPlans
|
portal_plans: portalPlans
|
||||||
@ -258,22 +228,19 @@ export function hasFreeProduct({site}) {
|
|||||||
return allowSelfSignup && portalPlans.includes('free');
|
return allowSelfSignup && portalPlans.includes('free');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSitePrices({site = {}, includeFree = true, pageQuery = ''} = {}) {
|
export function getAvailablePrices({site}) {
|
||||||
const {
|
const {
|
||||||
prices = [],
|
portal_plans: portalPlans = [],
|
||||||
allow_self_signup: allowSelfSignup,
|
is_stripe_configured: isStripeConfigured
|
||||||
is_stripe_configured: isStripeConfigured,
|
|
||||||
portal_plans: portalPlans
|
|
||||||
} = site || {};
|
} = site || {};
|
||||||
|
|
||||||
if (!prices) {
|
if (!isStripeConfigured) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const availablePrices = getProductPrices({site});
|
|
||||||
|
|
||||||
const plansData = [];
|
const productPrices = getPricesFromProducts({site});
|
||||||
|
|
||||||
const stripePrices = availablePrices.filter((d) => {
|
return productPrices.filter((d) => {
|
||||||
return !!(d && d.id);
|
return !!(d && d.id);
|
||||||
}).map((d) => {
|
}).map((d) => {
|
||||||
return {
|
return {
|
||||||
@ -287,10 +254,10 @@ export function getSitePrices({site = {}, includeFree = true, pageQuery = ''} =
|
|||||||
return price.amount !== 0 && price.type === 'recurring';
|
return price.amount !== 0 && price.type === 'recurring';
|
||||||
}).filter((price) => {
|
}).filter((price) => {
|
||||||
if (price.interval === 'month') {
|
if (price.interval === 'month') {
|
||||||
return (portalPlans || []).includes('monthly');
|
return portalPlans.includes('monthly');
|
||||||
}
|
}
|
||||||
if (price.interval === 'year') {
|
if (price.interval === 'year') {
|
||||||
return (portalPlans || []).includes('yearly');
|
return portalPlans.includes('yearly');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
@ -300,19 +267,33 @@ export function getSitePrices({site = {}, includeFree = true, pageQuery = ''} =
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return a.currency.localeCompare(b.currency, undefined, {ignorePunctuation: true});
|
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 = {
|
let freePriceCurrencyDetail = {
|
||||||
currency: 'usd',
|
currency: 'usd',
|
||||||
currency_symbol: '$'
|
currency_symbol: '$'
|
||||||
};
|
};
|
||||||
if (stripePrices && stripePrices.length > 0) {
|
if (stripePrices?.length > 0) {
|
||||||
freePriceCurrencyDetail.currency = stripePrices[0].currency;
|
freePriceCurrencyDetail.currency = stripePrices[0].currency;
|
||||||
freePriceCurrencyDetail.currency_symbol = stripePrices[0].currency_symbol;
|
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({
|
plansData.push({
|
||||||
id: 'free',
|
id: 'free',
|
||||||
type: '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) => {
|
stripePrices.forEach((price) => {
|
||||||
plansData.push(price);
|
plansData.push(price);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user