mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -05:00
Refined multiple products change plan flow
refs https://github.com/TryGhost/Team/issues/824 - updates the change plan flow for multiple products flag - adds new plan components for change plan flow - updates helpers - updates fixtures
This commit is contained in:
parent
cdfbd18dbb
commit
d34d9c2489
7 changed files with 170 additions and 91 deletions
|
@ -18,7 +18,7 @@ const React = require('react');
|
|||
const DEV_MODE_DATA = {
|
||||
showPopup: true,
|
||||
site: Fixtures.site,
|
||||
member: Fixtures.member.free,
|
||||
member: Fixtures.member.paid,
|
||||
page: 'accountHome'
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import AppContext from '../../AppContext';
|
|||
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
|
||||
import calculateDiscount from '../../utils/discount';
|
||||
import {isCookiesDisabled, formatNumber, hasOnlyFreePlan, hasMultipleProductsFeature, getFreeBenefits, getProductBenefits} from '../../utils/helpers';
|
||||
import ProductsSection from './ProductsSection';
|
||||
import ProductsSection, {ChangeProductSection} from './ProductsSection';
|
||||
|
||||
export const PlanSectionStyles = `
|
||||
.gh-portal-plans-container {
|
||||
|
@ -28,6 +28,11 @@ export const PlanSectionStyles = `
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.gh-portal-change-plan-section {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.gh-portal-plans-container.disabled .gh-portal-plan-section {
|
||||
cursor: auto;
|
||||
}
|
||||
|
@ -64,6 +69,11 @@ export const PlanSectionStyles = `
|
|||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.gh-portal-plans-container.is-change-plan.has-multiple-products .gh-portal-plan-section::before {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.gh-portal-plans-container.disabled .gh-portal-plan-section.checked::before {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
@ -353,6 +363,11 @@ export const PlanSectionStyles = `
|
|||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.gh-portal-plans-container.has-multiple-products.is-change-plan {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.gh-portal-plan-product {
|
||||
border: 1px solid var(--grey11);
|
||||
border-radius: 5px;
|
||||
|
@ -572,6 +587,19 @@ export function MultipleProductsPlansSection({products, selectedPlan, onPlanSele
|
|||
onPlanSelect = () => {};
|
||||
}
|
||||
|
||||
if (changePlan) {
|
||||
return (
|
||||
<section className="gh-portal-plans">
|
||||
<div>
|
||||
<ChangeProductSection
|
||||
products={products}
|
||||
onPlanSelect={onPlanSelect}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="gh-portal-plans">
|
||||
<div>
|
||||
|
@ -585,7 +613,7 @@ export function MultipleProductsPlansSection({products, selectedPlan, onPlanSele
|
|||
);
|
||||
}
|
||||
|
||||
export function SingleProductPlansSection({product, plans, showLabel = true, selectedPlan, onPlanSelect, changePlan = false}) {
|
||||
export function SingleProductPlansSection({product, plans, selectedPlan, onPlanSelect, changePlan = false}) {
|
||||
const {site} = useContext(AppContext);
|
||||
if (!product || hasOnlyFreePlan({plans})) {
|
||||
return null;
|
||||
|
@ -609,6 +637,65 @@ export function SingleProductPlansSection({product, plans, showLabel = true, sel
|
|||
);
|
||||
}
|
||||
|
||||
function getChangePlanClassNames({cookiesDisabled, site}) {
|
||||
let className = 'gh-portal-plans-container is-change-plan hide-checkbox';
|
||||
if (cookiesDisabled) {
|
||||
className += ' disabled';
|
||||
}
|
||||
|
||||
if (hasMultipleProductsFeature({site})) {
|
||||
className += ' has-multiple-products';
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
function ChangePlanOptions({plans, selectedPlan, onPlanSelect, changePlan}) {
|
||||
addDiscountToPlans(plans);
|
||||
|
||||
return plans.map(({name, currency_symbol: currencySymbol, amount, description, interval, id}) => {
|
||||
const price = amount / 100;
|
||||
const isChecked = selectedPlan === id;
|
||||
let displayName = interval === 'month' ? 'Monthly' : 'Yearly';
|
||||
|
||||
let planClass = (isChecked ? 'gh-portal-plan-section checked' : 'gh-portal-plan-section');
|
||||
planClass += ' gh-portal-change-plan-section';
|
||||
const planNameClass = 'gh-portal-plan-name no-description';
|
||||
const featureClass = 'gh-portal-plan-featurewrapper';
|
||||
|
||||
return (
|
||||
<div className={planClass} key={id} onClick={e => onPlanSelect(e, id)}>
|
||||
<h4 className={planNameClass}>{displayName}</h4>
|
||||
<PriceLabel currencySymbol={currencySymbol} price={price} interval={interval} />
|
||||
<div className={featureClass} style={{border: 'none', paddingTop: '3px'}}>
|
||||
{(changePlan && selectedPlan === id ? <span className='gh-portal-plan-current'>Current plan</span> : '')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function ChangeProductPlansSection({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 = getChangePlanClassNames({cookiesDisabled, site});
|
||||
|
||||
return (
|
||||
<section className="gh-portal-plans">
|
||||
<div className={className}>
|
||||
<ChangePlanOptions plans={plans} onPlanSelect={onPlanSelect} selectedPlan={selectedPlan} changePlan={changePlan} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PlansSection({plans, showLabel = true, selectedPlan, onPlanSelect, changePlan = false}) {
|
||||
const {site} = useContext(AppContext);
|
||||
if (hasOnlyFreePlan({plans})) {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
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} from '../../utils/helpers';
|
||||
import {getSiteProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled, getPricesFromProducts, getMemberActivePrice} from '../../utils/helpers';
|
||||
import AppContext from '../../AppContext';
|
||||
import {ChangeProductPlansSection} from './PlansSection';
|
||||
|
||||
export const ProductsSectionStyles = ({site}) => {
|
||||
const products = getSiteProducts({site});
|
||||
|
@ -678,4 +679,54 @@ function ProductsSection({onPlanSelect, products, type = null}) {
|
|||
);
|
||||
}
|
||||
|
||||
function ChangeProductCard({product, onPlanSelect}) {
|
||||
const {member} = useContext(AppContext);
|
||||
const cardClass = 'gh-portal-product-card';
|
||||
const plans = getPricesFromProducts({products: [product]});
|
||||
const selectedPlan = getMemberActivePrice({member});
|
||||
return (
|
||||
<div>
|
||||
<div className={cardClass} key={product.id}>
|
||||
<div className="gh-portal-product-card-header" style={{
|
||||
gridTemplateColumns: 'auto auto'
|
||||
}}>
|
||||
<h4 className="gh-portal-product-name">{product.name}</h4>
|
||||
<div className="gh-portal-product-description" style={{
|
||||
gridColumn: '1/2'
|
||||
}}>{product.description}</div>
|
||||
<ProductBenefitsContainer product={product} />
|
||||
</div>
|
||||
{/* <ProductCardFooter product={product} /> */}
|
||||
<ProductBenefitsContainer product={product} showVertical={true} />
|
||||
</div>
|
||||
<ChangeProductPlansSection
|
||||
product={product}
|
||||
plans={plans}
|
||||
selectedPlan={selectedPlan?.id}
|
||||
changePlan={true}
|
||||
onPlanSelect={onPlanSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangeProductCards({products, onPlanSelect}) {
|
||||
return products.map((product) => {
|
||||
if (product.id === 'free') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ChangeProductCard product={product} key={product.id} onPlanSelect={onPlanSelect} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function ChangeProductSection({products, onPlanSelect}) {
|
||||
return (
|
||||
<div className="gh-portal-products-grid">
|
||||
<ChangeProductCards products={products} onPlanSelect={onPlanSelect} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProductsSection;
|
||||
|
|
|
@ -192,7 +192,7 @@ const ChangePlanSection = ({plans, selectedPlan, onPlanSelect, onCancelSubscript
|
|||
showLabel={false}
|
||||
plans={plans}
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanSelect={(e, priceId) => onPlanSelect(e, priceId)}
|
||||
onPlanSelect={onPlanSelect}
|
||||
changePlan={true}
|
||||
/>
|
||||
</div>
|
||||
|
@ -205,13 +205,22 @@ function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, ch
|
|||
const {site, member} = useContext(AppContext);
|
||||
const products = getUpgradeProducts({site, member});
|
||||
if (hasMultipleProductsFeature({site})) {
|
||||
if (hasMultipleProducts({site})) {
|
||||
if (changePlan === true) {
|
||||
return (
|
||||
<MultipleProductsPlansSection
|
||||
products={products}
|
||||
selectedPlan={selectedPlan}
|
||||
changePlan={true}
|
||||
onPlanSelect={onPlanSelect}
|
||||
/>
|
||||
);
|
||||
} else if (hasMultipleProducts({site})) {
|
||||
return (
|
||||
<MultipleProductsPlansSection
|
||||
products={products}
|
||||
selectedPlan={selectedPlan}
|
||||
changePlan={changePlan}
|
||||
onPlanSelect={(e, priceId) => onPlanSelect(e, priceId)}
|
||||
onPlanSelect={onPlanSelect}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -220,7 +229,7 @@ function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, ch
|
|||
product={products?.[0]}
|
||||
plans={plans}
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanSelect={(e, priceId) => onPlanSelect(e, priceId)}
|
||||
onPlanSelect={onPlanSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -231,7 +240,7 @@ function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, ch
|
|||
plans={plans}
|
||||
selectedPlan={selectedPlan}
|
||||
changePlan={changePlan}
|
||||
onPlanSelect={(e, priceId) => onPlanSelect(e, priceId)}
|
||||
onPlanSelect={onPlanSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -380,7 +389,7 @@ export default class AccountPlanPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onPlanSelect(e, priceId) {
|
||||
onPlanSelect = (e, priceId) => {
|
||||
e?.preventDefault();
|
||||
|
||||
const {member} = this.context;
|
||||
|
@ -466,7 +475,7 @@ export default class AccountPlanPage extends React.Component {
|
|||
{...{plans, selectedPlan, showConfirmation, confirmationPlan, confirmationType}}
|
||||
onConfirm={(...args) => this.onConfirm(...args)}
|
||||
onCancelSubscription = {data => this.onCancelSubscription(data)}
|
||||
onPlanSelect = {(e, name) => this.onPlanSelect(e, name)}
|
||||
onPlanSelect = {this.onPlanSelect}
|
||||
onPlanCheckout = {(e, name) => this.onPlanCheckout(e, name)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -183,12 +183,12 @@ export const SignupPageStyles = `
|
|||
.gh-portal-popup-wrapper.multiple-products footer.gh-portal-signin-footer {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
|
||||
.gh-portal-popup-wrapper.multiple-products.signin .gh-portal-powered {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.gh-portal-popup-wrapper.multiple-products footer.gh-portal-signup-footer,
|
||||
.gh-portal-popup-wrapper.multiple-products footer.gh-portal-signin-footer {
|
||||
|
|
|
@ -50,30 +50,6 @@ const products = [
|
|||
type: 'recurring',
|
||||
interval: 'year'
|
||||
},
|
||||
prices: [
|
||||
{
|
||||
id: '6086d2c776909b1a2382369a',
|
||||
stripe_price_id: '7d6c89c0289ca1731226e86b95b5a162085b8561ca0d10d3a4f03afd3e3e6ba6',
|
||||
stripe_product_id: '109c85c734fb9992e7bc30a26af66c22f5c94d8dc62e0a33cb797be902c06b2d',
|
||||
active: 1,
|
||||
nickname: 'Monthly',
|
||||
currency: 'usd',
|
||||
amount: 500,
|
||||
type: 'recurring',
|
||||
interval: 'month'
|
||||
},
|
||||
{
|
||||
id: '6086eff0823dd7240afc8083',
|
||||
stripe_price_id: 'price_1IkXgCFToJelIqAsTP3V1paQ',
|
||||
stripe_product_id: 'prod_JNGGBrrogUXcoM',
|
||||
active: 1,
|
||||
nickname: 'Yearly',
|
||||
currency: 'usd',
|
||||
amount: 5000,
|
||||
type: 'recurring',
|
||||
interval: 'year'
|
||||
}
|
||||
],
|
||||
benefits: [
|
||||
{
|
||||
id: 'a1',
|
||||
|
@ -123,30 +99,6 @@ const products = [
|
|||
type: 'recurring',
|
||||
interval: 'year'
|
||||
},
|
||||
prices: [
|
||||
{
|
||||
id: '6086d2c776909b1a2382369a',
|
||||
stripe_price_id: '7d6c89c0289ca1731226e86b95b5a162085b8561ca0d10d3a4f03afd3e3e6ba6',
|
||||
stripe_product_id: '109c85c734fb9992e7bc30a26af66c22f5c94d8dc62e0a33cb797be902c06b2d',
|
||||
active: 1,
|
||||
nickname: 'Monthly',
|
||||
currency: 'usd',
|
||||
amount: 1200,
|
||||
type: 'recurring',
|
||||
interval: 'month'
|
||||
},
|
||||
{
|
||||
id: '6086eff0823dd7240afc8083',
|
||||
stripe_price_id: 'price_1IkXgCFToJelIqAsTP3V1paQ',
|
||||
stripe_product_id: 'prod_JNGGBrrogUXcoM',
|
||||
active: 1,
|
||||
nickname: 'Yearly',
|
||||
currency: 'usd',
|
||||
amount: 12000,
|
||||
type: 'recurring',
|
||||
interval: 'year'
|
||||
}
|
||||
],
|
||||
benefits: [
|
||||
{
|
||||
id: 'b1',
|
||||
|
@ -192,30 +144,6 @@ const products = [
|
|||
type: 'recurring',
|
||||
interval: 'year'
|
||||
},
|
||||
prices: [
|
||||
{
|
||||
id: '6086d2c776909b1a2382369a',
|
||||
stripe_price_id: '7d6c89c0289ca1731226e86b95b5a162085b8561ca0d10d3a4f03afd3e3e6ba6',
|
||||
stripe_product_id: '109c85c734fb9992e7bc30a26af66c22f5c94d8dc62e0a33cb797be902c06b2d',
|
||||
active: 1,
|
||||
nickname: 'Monthly',
|
||||
currency: 'usd',
|
||||
amount: 1200,
|
||||
type: 'recurring',
|
||||
interval: 'month'
|
||||
},
|
||||
{
|
||||
id: '6086eff0823dd7240afc8083',
|
||||
stripe_price_id: 'price_1IkXgCFToJelIqAsTP3V1paQ',
|
||||
stripe_product_id: 'prod_JNGGBrrogUXcoM',
|
||||
active: 1,
|
||||
nickname: 'Yearly',
|
||||
currency: 'usd',
|
||||
amount: 12000,
|
||||
type: 'recurring',
|
||||
interval: 'year'
|
||||
}
|
||||
],
|
||||
benefits: [
|
||||
{
|
||||
id: 'c1',
|
||||
|
@ -292,17 +220,17 @@ export const member = {
|
|||
id: 'fd43b943666b97640188afb382cca39479de30f799985679dd7a71ad2925ac6c',
|
||||
nickname: 'Yearly',
|
||||
interval: 'year',
|
||||
amount: 1500,
|
||||
amount: 7000,
|
||||
currency: 'USD'
|
||||
},
|
||||
price: {
|
||||
id: 'price_1IkXLAFToJelIqAseQdK4WSU',
|
||||
price_id: '6086ead8070218227791fe4f',
|
||||
id: 'price_1IkXgCFToJelIqAsTP3V1paQ',
|
||||
price_id: '6086eff0823dd7240afc8012',
|
||||
nickname: 'Yearly',
|
||||
currency: 'usd',
|
||||
amount: 1500,
|
||||
amount: 7000,
|
||||
type: 'recurring',
|
||||
interval: 'month',
|
||||
interval: 'year',
|
||||
product: {
|
||||
id: 'prod_JNGGBrrogUXcoM',
|
||||
name: 'Main Product',
|
||||
|
|
|
@ -264,7 +264,11 @@ export function getProductBenefits({product}) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getPricesFromProducts({site, products = null}) {
|
||||
export function getPricesFromProducts({site = null, products = null}) {
|
||||
if (!site && !products) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const availableProducts = products || getAvailableProducts({site});
|
||||
const prices = availableProducts.reduce((accumPrices, product) => {
|
||||
if (product.monthlyPrice && product.yearlyPrice) {
|
||||
|
|
Loading…
Add table
Reference in a new issue