0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-25 02:31:59 -05:00

Added description and benefits for free tier (#210)

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

Free tier is now setup the same way as other tiers, to allow custom description/benefits. This change - 

- adds custom description and benefits UI for free tier when tiers beta is enabled
- updates fixtures structure
- fixes react overlay error for fast refresh

Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
Rishabh Garg 2022-01-17 23:24:34 +05:30 committed by GitHub
parent 542e585f89
commit 7cf843d3c7
8 changed files with 607 additions and 374 deletions

View file

@ -85,5 +85,9 @@
"serve-handler": "6.1.3",
"source-map-explorer": "2.5.2",
"webpack-cli": "3.3.12"
},
"resolutions": {
"//": "See https://github.com/facebook/create-react-app/issues/11773",
"react-error-overlay": "6.0.9"
}
}

View file

@ -2,7 +2,7 @@ import React, {useContext} from 'react';
import AppContext from '../../AppContext';
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
import calculateDiscount from '../../utils/discount';
import {isCookiesDisabled, formatNumber, hasOnlyFreePlan, hasMultipleProductsFeature, getProductBenefits} from '../../utils/helpers';
import {isCookiesDisabled, formatNumber, hasOnlyFreePlan, hasMultipleProductsFeature, getProductBenefits, getFreeTierDescription, getFreeTierTitle, getFreeProductBenefits, getProductFromPrice} from '../../utils/helpers';
import ProductsSection, {ChangeProductSection} from './ProductsSection';
export const PlanSectionStyles = `
@ -68,7 +68,7 @@ export const PlanSectionStyles = `
margin-bottom: 2px;
}
.gh-portal-plans-container.has-multiple-products .gh-portal-plan-section::before {
.gh-portal-plans-container.has-multiple-products:not(.empty-selected-benefits) .gh-portal-plan-section::before {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
@ -371,7 +371,7 @@ export const PlanSectionStyles = `
border: none;
}
.gh-portal-plans-container.has-multiple-products {
.gh-portal-plans-container.has-multiple-products:not(.empty-selected-benefits) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
@ -422,12 +422,22 @@ export const PlanSectionStyles = `
border-radius: 0 0 5px 5px;
}
.gh-portal-singleproduct-benefits.onlyfree {
border-top: 1px solid var(--grey11) !important;
border-radius: 5px;
margin-top: 30px !important;
}
.gh-portal-singleproduct-benefits .gh-portal-product-benefit {
padding: 0 8px;
}
.gh-portal-singleproduct-benefits .gh-portal-product-benefit:last-of-type {
margin-bottom: 12px;
margin-bottom: 16px;
}
.gh-portal-singleproduct-benefits.onlyfree .gh-portal-product-benefit:last-of-type {
margin-bottom: 4px;
}
.gh-portal-singleproduct-benefits:not(.no-benefits) .gh-portal-product-description {
@ -489,21 +499,28 @@ function PlanOptions({plans, selectedPlan, onPlanSelect, changePlan}) {
const {site} = useContext(AppContext);
addDiscountToPlans(plans);
return plans.map(({name, currency_symbol: currencySymbol, amount, description, interval, id}) => {
return plans.map(({name, type, currency_symbol: currencySymbol, amount, description, interval, id}) => {
const price = amount / 100;
const isChecked = selectedPlan === id;
const planDetails = {};
let displayName = name;
switch (name) {
case 'Free':
displayName = name;
if (type === 'free') {
displayName = getFreeTierTitle({site});
planDetails.feature = 'Free preview';
break;
default:
} else {
displayName = interval === 'month' ? 'Monthly' : 'Yearly';
planDetails.feature = description || 'Full access';
break;
}
// switch (name) {
// case 'Free':
// displayName = getFreeTierTitle({site});
// planDetails.feature = 'Free preview';
// break;
// default:
// displayName = interval === 'month' ? 'Monthly' : 'Yearly';
// planDetails.feature = description || 'Full access';
// break;
// }
let planClass = isChecked ? 'gh-portal-plan-section checked' : 'gh-portal-plan-section';
const planNameClass = planDetails.feature ? 'gh-portal-plan-name' : 'gh-portal-plan-name no-description';
@ -560,13 +577,10 @@ function PlanBenefits({product, plans, selectedPlan}) {
return _plan.id === selectedPlan;
});
let planBenefits = [];
let planDescription = product.description;
if (!product.description) {
planDescription = `Full access to ` + site.title;
}
let planDescription = product?.description || '';
if (selectedPlan === 'free') {
planBenefits = [];
planDescription = `Free preview of ` + site.title;
planBenefits = getFreeProductBenefits({site});
planDescription = getFreeTierDescription({site});
} else if (plan?.interval === 'month') {
planBenefits = productBenefits.monthly;
} else if (plan?.interval === 'year') {
@ -579,12 +593,18 @@ function PlanBenefits({product, plans, selectedPlan}) {
);
});
let benefitsClass = (selectedPlan === 'free') ? `no-benefits` : ``;
benefitsClass = benefits.length === 0 ? `no-benefits` : ``;
if (!planDescription && benefits.length === 0) {
return '';
}
let benefitsClass = benefits.length === 0 ? `no-benefits` : '';
if (!product || hasOnlyFreePlan({plans})) {
benefitsClass += ' onlyfree';
}
return (
<div className={'gh-portal-singleproduct-benefits gh-portal-product-benefits ' + benefitsClass}>
<div className='gh-portal-product-description'> {planDescription} </div>
{planDescription ? <div className='gh-portal-product-description'> {planDescription} </div> : ''}
{benefits}
</div>
);
@ -599,7 +619,14 @@ function PlanLabel({showLabel}) {
);
}
function getPlanClassNames({changePlan, cookiesDisabled, plans = [], showVertical = false, site}) {
function productHasDescriptionOrBenefits({product}) {
if (product?.description || product?.benefits?.length) {
return true;
}
return false;
}
function getPlanClassNames({changePlan, cookiesDisabled, plans = [], selectedPlan, showVertical = false, site}) {
let className = 'gh-portal-plans-container';
if (changePlan) {
className += ' hide-checkbox';
@ -612,6 +639,11 @@ function getPlanClassNames({changePlan, cookiesDisabled, plans = [], showVertica
}
if (hasMultipleProductsFeature({site})) {
className += ' has-multiple-products';
const selectedProduct = getProductFromPrice({site, priceId: selectedPlan});
if (!productHasDescriptionOrBenefits({product: selectedProduct})) {
className += ' empty-selected-benefits';
}
const filteredPlans = plans.filter(d => d.id !== 'free');
const monthlyPlan = plans.find((d) => {
@ -668,19 +700,23 @@ export function MultipleProductsPlansSection({products, selectedPlan, onPlanSele
export function SingleProductPlansSection({product, plans, selectedPlan, onPlanSelect, changePlan = false}) {
const {site} = useContext(AppContext);
if (!product || hasOnlyFreePlan({plans})) {
return null;
}
const cookiesDisabled = isCookiesDisabled();
/**Don't allow plans selection if cookies are disabled */
if (cookiesDisabled) {
onPlanSelect = () => {};
}
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, site});
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, selectedPlan, site});
if (!product || hasOnlyFreePlan({plans})) {
return (
<section>
<PlanBenefits product={product} plans={plans} selectedPlan={selectedPlan} />
</section>
);
}
return (
<section className="gh-portal-plans mt8">
<section>
<div className={className}>
<PlanOptions plans={plans} onPlanSelect={onPlanSelect} selectedPlan={selectedPlan} changePlan={changePlan} />
</div>
@ -758,7 +794,7 @@ function PlansSection({plans, showLabel = true, selectedPlan, onPlanSelect, chan
if (cookiesDisabled) {
onPlanSelect = () => {};
}
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, site});
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, selectedPlan, site});
return (
<section className="gh-portal-plans">
<PlanLabel showLabel={showLabel} />

View file

@ -1,7 +1,7 @@
import React, {useContext, useEffect, useState} from 'react';
import Switch from '../common/Switch';
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
import {getSiteProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled, getMemberActivePrice, getProductFromPrice} from '../../utils/helpers';
import {getSiteProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct} from '../../utils/helpers';
import AppContext from '../../AppContext';
import ActionButton from './ActionButton';
import calculateDiscount from '../../utils/discount';
@ -204,7 +204,7 @@ export const ProductsSectionStyles = ({site}) => {
width: 100%;
}
.gh-portal-product-card:not(.free) .gh-portal-product-description {
.gh-portal-product-card .gh-portal-product-description {
padding-bottom: 20px;
margin-bottom: 16px;
}
@ -658,7 +658,8 @@ function FreeProductCard() {
const {selectedProduct, selectedInterval, setSelectedProduct} = useContext(ProductsContext);
const cardClass = selectedProduct === 'free' ? 'gh-portal-product-card free checked' : 'gh-portal-product-card free';
const product = getFreeProduct({site});
const freeProductDescription = getFreeTierDescription({site});
// Product cards are duplicated because their design is too different for mobile devices to handle it purely in CSS
return (
<>
@ -671,8 +672,9 @@ function FreeProductCard() {
<Checkbox name='x' id='x' isChecked={selectedProduct === 'free'} onProductSelect={() => {
setSelectedProduct('free');
}} />
<h4 className="gh-portal-product-name">Free</h4>
<div className="gh-portal-product-description">Free preview of {(site.title)}</div>
<h4 className="gh-portal-product-name">{getFreeTierTitle({site})}</h4>
{freeProductDescription ? <div className="gh-portal-product-description">{freeProductDescription}</div> : ''}
<ProductBenefitsContainer product={product} />
</div>
<div className="gh-portal-product-card-pricecontainer">
<div className="gh-portal-product-price">
@ -691,13 +693,14 @@ function FreeProductCard() {
<Checkbox name='x' id='x' isChecked={selectedProduct === 'free'} onProductSelect={() => {
setSelectedProduct('free');
}} />
<h4 className="gh-portal-product-name">Free</h4>
<h4 className="gh-portal-product-name">{getFreeTierTitle({site})}</h4>
<div className="gh-portal-product-price">
<span className="currency-sign">$</span>
<span className="amount">0</span>
<span className="billing-period">/{selectedInterval}</span>
</div>
<div className="gh-portal-product-description">Free preview of {(site.title)}</div>
{freeProductDescription ? <div className="gh-portal-product-description">{freeProductDescription}</div> : ''}
<ProductBenefitsContainer product={product} />
</div>
</>
);

View file

@ -5,7 +5,7 @@ import PlansSection, {SingleProductPlansSection} from '../common/PlansSection';
import ProductsSection from '../common/ProductsSection';
import InputForm from '../common/InputForm';
import {ValidateInputForm} from '../../utils/form';
import {getSiteProducts, getSitePrices, hasMultipleProducts, hasOnlyFreePlan, isInviteOnlySite, getAvailableProducts, hasMultipleProductsFeature} from '../../utils/helpers';
import {getSiteProducts, getSitePrices, hasMultipleProducts, hasOnlyFreePlan, isInviteOnlySite, getAvailableProducts, hasMultipleProductsFeature, freeHasBenefitsOrDescription} from '../../utils/helpers';
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
const React = require('react');
@ -87,18 +87,6 @@ export const SignupPageStyles = `
height: unset;
}
/* Needed to cover small horizontal line glitch by the scroll shadow */
footer.gh-portal-signup-footer::before {
position: absolute;
content: "";
top: -2px;
left: 0;
right: 0;
height: 3px;
background: #fff;
z-index: 9999;
}
.gh-portal-content.signup,
.gh-portal-content.signin {
max-height: unset !important;
@ -231,7 +219,7 @@ export const SignupPageStyles = `
@media (min-width: 480px) and (max-width: 820px) {
.gh-portal-powered.outside {
left: 50%;
left: 50%;
transform: translateX(-50%);
}
@ -563,7 +551,7 @@ class SignupPage extends React.Component {
if (plansData.length <= 1 || isInviteOnlySite({site})) {
if ((plansData.length === 1 && plansData[0].type === 'free') || isInviteOnlySite({site, pageQuery})) {
sectionClass = 'noplan';
sectionClass = freeHasBenefitsOrDescription({site}) ? 'singleplan' : 'noplan';
if (fields.length === 1) {
sectionClass = 'single-field';
}

View file

@ -0,0 +1,384 @@
export const sites = {
singleProduct: getSiteData({
products: getProductsData({numOfProducts: 1})
})
};
export function objectId() {
const timestamp = (new Date().getTime() / 1000 | 0).toString(16);
return timestamp + 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function () {
return (Math.random() * 16 | 0).toString(16);
}).toLowerCase();
}
export function getSiteData({
title = 'The Blueprint',
description = 'Thoughts, stories and ideas.',
logo = 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
icon = 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
url = 'https://portal.localhost',
plans = {
monthly: 5000,
yearly: 150000,
currency: 'USD'
},
products = getProductsData({numOfProducts: 1}),
portalProducts = products.map(p => p.id),
accentColor: accent_color = '#45C32E',
portalPlans: portal_plans = ['free', 'monthly', 'yearly'],
allowSelfSignup: allow_self_signup = true,
membersSignupAccess: members_signup_access = 'all',
freePriceName: free_price_name = 'Free',
freePriceDescription: free_price_description = 'Free preview',
isStripeConfigured: is_stripe_configured = true,
portalButton: portal_button = true,
portalName: portal_name = true,
portalButtonIcon: portal_button_icon = 'icon-1',
portalButtonSignupText: portal_button_signup_text = 'Subscribe now',
portalButtonStyle: portal_button_style = 'icon-and-text',
membersSupportAddress: members_support_address = 'support@example.com'
} = {}) {
return {
title,
description,
logo,
icon,
accent_color,
url,
plans,
products,
portal_products: portalProducts,
allow_self_signup,
members_signup_access,
free_price_name,
free_price_description,
is_stripe_configured,
portal_button,
portal_name,
portal_plans,
portal_button_icon,
portal_button_signup_text,
portal_button_style,
members_support_address
};
}
export function getOfferData({
name = 'Black Friday',
code = 'black-friday',
displayTitle = 'Black Friday',
displayDescription = 'Special deal',
type = 'percent',
cadence = 'month',
amount = 50,
duration = 'repeating',
durationInMonths = null,
currencyRestriction = false,
currency = null,
status = 'active',
tierId = '',
tierName = 'Basic'
} = {}) {
return {
id: `offer_${objectId()}`,
name,
code,
display_title: displayTitle,
display_description: displayDescription,
type,
cadence,
amount,
duration,
duration_in_months: durationInMonths,
currency_restriction: currencyRestriction,
currency,
status,
tier: {
id: `${tierId}`,
name: tierName
}
};
}
export function getMemberData({
name = 'Jamie Larson',
email = 'jamie@example.com',
firstname = 'Jamie',
subscriptions = [],
paid = false,
avatarImage: avatar_image = '',
subscribed = true
} = {}) {
return {
uuid: `member_${objectId()}`,
email,
name,
firstname,
paid,
subscribed,
avatar_image,
subscriptions
};
}
export function getProductsData({numOfProducts = 3} = {}) {
const products = [
getProductData({
name: 'Bronze',
description: 'Access to all members articles',
monthlyPrice: getPriceData({
interval: 'month',
amount: 700
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 7000
}),
numOfBenefits: 2
}),
getProductData({
name: 'Silver',
description: 'Access to all members articles and weekly podcast',
monthlyPrice: getPriceData({
interval: 'month',
amount: 1200
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 12000
}),
numOfBenefits: 3
}),
getProductData({
name: 'Friends of the Blueprint',
description: 'Get access to everything and lock in early adopter pricing for life + listen to my podcast',
monthlyPrice: getPriceData({
interval: 'month',
amount: 18000
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 17000
}),
numOfBenefits: 4
})
];
const paidProducts = products.slice(0, numOfProducts);
const freeProduct = getFreeProduct({});
return [
...paidProducts,
freeProduct
];
}
export function getProductData({
type = 'paid',
name = 'Basic',
description = '',
id = `product_${objectId()}`,
monthlyPrice = getPriceData(),
yearlyPrice = getPriceData({interval: 'year'}),
numOfBenefits = 2
}) {
return {
id: id,
name: name,
description,
monthlyPrice: type === 'free' ? null : monthlyPrice,
yearlyPrice: type === 'free' ? null : yearlyPrice,
type: type,
benefits: getBenefits({numOfBenefits})
};
}
export function getFreeProduct({
name = 'Free tier',
description = 'Free tier description',
id = `product_${objectId()}`,
numOfBenefits = 2
}) {
return {
id,
name: name,
type: 'free',
description,
benefits: getBenefits({numOfBenefits})
};
}
export function getBenefits({numOfBenefits}) {
const beenfits = [
getBenefitData({name: 'Limited early adopter pricing'}),
getBenefitData({name: 'Latest gear reviews'}),
getBenefitData({name: 'Weekly email newsletter'}),
getBenefitData({name: 'Listen to my podcast'})
];
return beenfits.slice(0, numOfBenefits);
}
export function getBenefitData({
id = `benefit_${objectId()}`,
name = 'Benefit'
}) {
return {
id,
name
};
}
export function getPriceData({
interval = 'month',
amount = (interval === 'month' ? 500 : 5000),
nickname = interval === 'month' ? 'Monthly' : 'Yearly',
description = null,
currency = 'usd',
active = true,
id = `price_${objectId()}`
} = {}) {
return {
id: id,
active,
nickname,
currency,
amount,
interval,
description,
stripe_price_id: `price_${objectId()}`,
stripe_product_id: `prod_${objectId()}`,
type: 'recurring'
};
}
export function getSubscriptionData({
id = `sub_${objectId()}`,
status = 'active',
currency = 'USD',
interval = 'year',
amount = (interval === 'month' ? 500 : 5000),
nickname = (interval === 'month' ? 'Monthly' : 'Yearly'),
cardLast4 = '4242',
priceId: price_id = `price_${objectId()}`,
startDate: start_date = '2021-10-05T03:18:30.000Z',
currentPeriodEnd: current_period_end = '2022-10-05T03:18:30.000Z',
cancelAtPeriodEnd: cancel_at_period_end = false
} = {}) {
return {
id,
customer: {
id: `cus_${objectId()}`,
name: 'Jamie',
email: 'jamie@example.com'
},
plan: {
id: `price_${objectId()}`,
nickname,
amount,
interval,
currency
},
offer,
status,
start_date,
default_payment_card_last4: cardLast4,
cancel_at_period_end,
cancellation_reason: null,
current_period_end,
price: {
id: `stripe_price_${objectId()}`,
price_id,
nickname,
amount,
interval,
type: 'recurring',
currency,
product: {
id: `stripe_prod_${objectId()}`,
product_id: `prod_${objectId()}`
}
}
};
}
export function getTestSite() {
const products = getProductsData({numOfProducts: 1});
const portalProducts = products.map(p => p.id);
const portalPlans = ['free', 'monthly', 'yearly'];
return getSiteData({
products,
portalPlans,
portalProducts
});
}
export const testSite = getTestSite();
export const site = getSiteData({
products: [getProductData({numOfBenefits: 2, type: 'free'})]
});
export const offer = getOfferData({
tierId: site.products[0]?.id
});
export const member = {
free: getMemberData(),
paid: getMemberData({
paid: true,
subscriptions: [
getSubscriptionData()
]
}),
complimentary: getMemberData({
paid: true,
subscriptions: []
}),
complimentaryWithSubscription: getMemberData({
paid: true,
subscriptions: [
getSubscriptionData({
amount: 0
})
]
}),
preview: getMemberData({
paid: true,
subscriptions: [
getSubscriptionData({
amount: 1500,
startDate: '2019-05-01T11:42:40.000Z',
currentPeriodEnd: '2021-06-05T11:42:40.000Z'
})
]
})
};
export function generateAccountPlanFixture() {
const products = getProductsData({numOfProducts: 3});
return {
site: getSiteData({
portalProducts: [products[1]]
}),
member: member.paid
};
}
export function basic() {
const products = getProductsData();
const siteData = getSiteData({
products
});
const defaultMemberPrice = products?.[0].monthlyPrice;
const memberData = getMemberData({
paid: true,
subscriptions: [
getSubscriptionData({
priceId: defaultMemberPrice.id,
amount: defaultMemberPrice.amount,
currency: defaultMemberPrice.currency
})
]
});
return {
site: siteData,
member: memberData
};
}

View file

@ -1,290 +1,123 @@
export const sites = {
singleProduct: getSiteData({
products: getProductsData({numOfProducts: 1})
})
};
function objectId() {
const timestamp = (new Date().getTime() / 1000 | 0).toString(16);
return timestamp + 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function () {
return (Math.random() * 16 | 0).toString(16);
}).toLowerCase();
}
export function getSiteData({
products = getProductsData({numOfProducts: 1}),
portalProducts = products.map(p => p.id),
portalPlans: portal_plans = ['free', 'monthly', 'yearly']
} = {}) {
return {
title: 'The Blueprint',
description: 'Thoughts, stories and ideas.',
logo: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
icon: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
accent_color: '#45C32E',
url: 'https://portal.localhost',
plans: {
monthly: 5000,
yearly: 150000,
currency: 'USD'
},
products,
portal_products: portalProducts,
allow_self_signup: true,
members_signup_access: 'all',
free_price_name: 'Free',
free_price_description: 'Free preview',
is_stripe_configured: true,
portal_button: true,
portal_name: true,
portal_plans,
portal_button_icon: 'icon-1',
portal_button_signup_text: 'Subscribe now',
portal_button_style: 'icon-and-text',
members_support_address: 'support@example.com'
};
}
function getOfferData({
name = 'Black Friday',
code = 'black-friday',
displayTitle = 'Black Friday',
displayDescription = 'Special deal',
type = 'percent',
cadence = 'month',
amount = 50,
duration = 'repeating',
durationInMonths = null,
currencyRestriction = false,
currency = null,
status = 'active',
tierId = ''
} = {}) {
return {
id: `offer_${objectId()}`,
name,
code,
display_title: displayTitle,
display_description: displayDescription,
type,
cadence,
amount,
duration,
duration_in_months: durationInMonths,
currency_restriction: currencyRestriction,
currency,
status,
tier: {
id: `${tierId}`,
name: 'Basic'
}
};
}
function getMemberData({
name = 'Jamie Larson',
email = 'jamie@example.com',
firstname = 'Jamie',
subscriptions = [],
paid = false,
avatarImage: avatar_image = '',
subscribed = true
} = {}) {
return {
uuid: `member_${objectId()}`,
email,
name,
firstname,
paid,
subscribed,
avatar_image,
subscriptions
};
}
export function getProductsData({numOfProducts = 3} = {}) {
const products = [
getProductData({
name: 'Bronze',
description: 'Access to all members articles',
monthlyPrice: getPriceData({
interval: 'month',
amount: 700
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 7000
}),
numOfBenefits: 2
}),
getProductData({
name: 'Silver',
description: 'Access to all members articles and weekly podcast',
monthlyPrice: getPriceData({
interval: 'month',
amount: 1200
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 12000
}),
numOfBenefits: 3
}),
getProductData({
name: 'Friends of the Blueprint',
description: 'Get access to everything and lock in early adopter pricing for life + listen to my podcast',
monthlyPrice: getPriceData({
interval: 'month',
amount: 18000
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 17000
}),
numOfBenefits: 4
})
];
return products.slice(0, numOfProducts);
}
function getProductData({
name = 'Basic',
description = '',
id = `product_${objectId()}`,
monthlyPrice = getPriceData(),
yearlyPrice = getPriceData({interval: 'year'}),
numOfBenefits = 2
}) {
return {
id: id,
name: name,
description,
monthlyPrice,
yearlyPrice,
benefits: getBenefits({numOfBenefits})
};
}
function getBenefits({numOfBenefits}) {
const beenfits = [
getBenefitData({name: 'Limited early adopter pricing'}),
getBenefitData({name: 'Latest gear reviews'}),
getBenefitData({name: 'Weekly email newsletter'}),
getBenefitData({name: 'Listen to my podcast'})
];
return beenfits.slice(0, numOfBenefits);
}
function getBenefitData({
id = `benefit_${objectId()}`,
name = 'Benefit'
}) {
return {
id,
name
};
}
function getPriceData({
interval = 'month',
amount = (interval === 'month' ? 500 : 5000),
nickname = interval === 'month' ? 'Monthly' : 'Yearly',
description = null,
currency = 'usd',
active = true,
id = `price_${objectId()}`
}) {
return {
id: id,
active,
nickname,
currency,
amount,
interval,
description,
stripe_price_id: `price_${objectId()}`,
stripe_product_id: `prod_${objectId()}`,
type: 'recurring'
};
}
function getSubscriptionData({
id = `sub_${objectId()}`,
status = 'active',
currency = 'USD',
interval = 'year',
amount = (interval === 'month' ? 500 : 5000),
nickname = (interval === 'month' ? 'Monthly' : 'Yearly'),
cardLast4 = '4242',
priceId: price_id = `price_${objectId()}`,
startDate: start_date = '2021-10-05T03:18:30.000Z',
currentPeriodEnd: current_period_end = '2022-10-05T03:18:30.000Z',
cancelAtPeriodEnd: cancel_at_period_end = false
} = {}) {
return {
id,
customer: {
id: `cus_${objectId()}`,
name: 'Jamie',
email: 'jamie@example.com'
},
plan: {
id: `price_${objectId()}`,
nickname,
amount,
interval,
currency
},
offer,
status,
start_date,
default_payment_card_last4: cardLast4,
cancel_at_period_end,
cancellation_reason: null,
current_period_end,
price: {
id: `stripe_price_${objectId()}`,
price_id,
nickname,
amount,
interval,
type: 'recurring',
currency,
product: {
id: `stripe_prod_${objectId()}`,
product_id: `prod_${objectId()}`
}
}
};
}
function getTestSite() {
const products = getProductsData({numOfProducts: 1});
const portalProducts = products.map(p => p.id);
const portalPlans = ['free', 'monthly', 'yearly'];
return getSiteData({
products,
portalPlans,
portalProducts
});
}
/* eslint-disable no-unused-vars*/
import {getFreeProduct, getMemberData, getOfferData, getPriceData, getProductData, getSiteData, getSubscriptionData, getTestSite} from './fixtures-generator';
export const testSite = getTestSite();
const products = [
getFreeProduct({
name: 'Free',
// description: 'Free tier description which is actually a pretty long description',
description: '',
numOfBenefits: 0
})
,
getProductData({
name: 'Bronze',
// description: 'Access to all members articles',
description: '',
monthlyPrice: getPriceData({
interval: 'month',
amount: 700
}),
yearlyPrice: getPriceData({
interval: 'year',
amount: 7000
}),
numOfBenefits: 2
})
// ,
// getProductData({
// name: 'Silver',
// description: 'Access to all members articles and weekly podcast',
// monthlyPrice: getPriceData({
// interval: 'month',
// amount: 1200
// }),
// yearlyPrice: getPriceData({
// interval: 'year',
// amount: 12000
// }),
// numOfBenefits: 3
// })
// ,
// getProductData({
// name: 'Friends of the Blueprint',
// description: 'Get access to everything and lock in early adopter pricing for life + listen to my podcast',
// monthlyPrice: getPriceData({
// interval: 'month',
// amount: 18000
// }),
// yearlyPrice: getPriceData({
// interval: 'year',
// amount: 17000
// }),
// numOfBenefits: 4
// })
];
export const site = getSiteData({
products: getProductsData({numOfProducts: 1})
title: 'The Blueprint',
description: 'Thoughts, stories and ideas.',
logo: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
icon: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
accentColor: '#45C32E',
url: 'https://portal.localhost',
plans: {
monthly: 5000,
yearly: 150000,
currency: 'USD'
},
// Simulate pre-multiple-tiers state:
// products: [products.find(d => d.type === 'paid')],
// portalProducts: null,
// Simulate multiple-tiers state:
products,
portalProducts: products.map(p => p.id),
//
allowSelfSignup: true,
membersSignupAccess: 'all',
freePriceName: 'Free',
freePriceDescription: 'Free preview',
isStripeConfigured: true,
portalButton: true,
portalName: true,
portalPlans: ['free', 'monthly', 'yearly'],
portalButtonIcon: 'icon-1',
portalButtonSignupText: 'Subscribe now',
portalButtonStyle: 'icon-and-text',
membersSupportAddress: 'support@example.com'
});
export const offer = getOfferData({
tierId: site.products[0].id
tierId: site.products[0]?.id
});
export const member = {
free: getMemberData(),
free: getMemberData({
name: 'Jamie Larson',
email: 'jamie@example.com',
firstname: 'Jamie',
subscriptions: [],
paid: false,
avatarImage: '',
subscribed: true
}),
paid: getMemberData({
paid: true,
subscriptions: [
getSubscriptionData()
getSubscriptionData({
status: 'active',
currency: 'USD',
interval: 'year',
amount: 5000,
cardLast4: '4242',
startDate: '2021-10-05T03:18:30.000Z',
currentPeriodEnd: '2022-10-05T03:18:30.000Z',
cancelAtPeriodEnd: false
})
]
}),
complimentary: getMemberData({
@ -310,34 +143,4 @@ export const member = {
]
})
};
export function generateAccountPlanFixture() {
const products = getProductsData({numOfProducts: 3});
return {
site: getSiteData({
portalProducts: [products[1]]
}),
member: member.paid
};
}
export function basic() {
const products = getProductsData();
const siteData = getSiteData({
products
});
const defaultMemberPrice = products?.[0].monthlyPrice;
const memberData = getMemberData({
paid: true,
subscriptions: [
getSubscriptionData({
priceId: defaultMemberPrice.id,
amount: defaultMemberPrice.amount,
currency: defaultMemberPrice.currency
})
]
});
return {
site: siteData,
member: memberData
};
}
/* eslint-enable no-unused-vars*/

View file

@ -1,5 +1,3 @@
// import calculateDiscount from './discount';
export function removePortalLinkFromUrl() {
const [path] = window.location.hash.substr(1).split('?');
const linkRegex = /^\/portal\/?(?:\/(\w+(?:\/\w+)*))?\/?$/;
@ -215,6 +213,11 @@ export function getAvailableProducts({site}) {
});
}
export function getFreeProduct({site}) {
const {products = []} = site || {};
return products.find(product => product.type === 'free');
}
export function getAllProductsForSite({site}) {
const {products = [], portal_plans: portalPlans = []} = site || {};
@ -263,10 +266,27 @@ export function getSiteProducts({site}) {
return products;
}
export function getFreeBenefits() {
return [{
name: 'Access to free articles'
}];
export function getFreeProductBenefits({site}) {
const freeProduct = getFreeProduct({site});
return freeProduct?.benefits || [];
}
export function getFreeTierTitle({site}) {
return 'Free';
}
export function getFreeTierDescription({site}) {
const freeProduct = getFreeProduct({site});
return freeProduct?.description;
}
export function freeHasBenefitsOrDescription({site}) {
const freeProduct = getFreeProduct({site});
if (freeProduct?.description || freeProduct?.benefits?.length) {
return true;
}
return false;
}
export function getProductBenefits({product, site = null}) {
@ -274,14 +294,6 @@ export function getProductBenefits({product, site = null}) {
const productBenefits = product?.benefits || [];
const monthlyBenefits = productBenefits;
const yearlyBenefits = productBenefits;
// const availablePrices = getAvailablePrices({site, products: [product]});
// const yearlyDiscount = calculateDiscount(product.monthlyPrice.amount, product.yearlyPrice.amount);
// if (yearlyDiscount > 0 && availablePrices.length > 1) {
// yearlyBenefits.push({
// name: `${yearlyDiscount}% annual discount`,
// className: `gh-portal-strong`
// });
// }
return {
monthly: monthlyBenefits,
yearly: yearlyBenefits
@ -319,6 +331,9 @@ export function hasFreeProductPrice({site}) {
}
export function getProductFromPrice({site, priceId}) {
if (priceId === 'free') {
return getFreeProduct({site});
}
const products = getAllProductsForSite({site});
return products.find((product) => {
return (product?.monthlyPrice?.id === priceId) || (product?.yearlyPrice?.id === priceId);
@ -396,7 +411,7 @@ export function getSitePrices({site = {}, pageQuery = ''} = {}) {
type: 'free',
price: 0,
amount: 0,
name: 'Free',
name: getFreeTierTitle({site}),
...freePriceCurrencyDetail
});

View file

@ -9463,10 +9463,10 @@ react-dom@17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
react-error-overlay@^6.0.9:
version "6.0.10"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
react-error-overlay@6.0.9, react-error-overlay@^6.0.9:
version "6.0.9"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"