0
Fork 0
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:
Rishabh 2021-06-29 17:12:18 +05:30
parent cdfbd18dbb
commit d34d9c2489
7 changed files with 170 additions and 91 deletions

View file

@ -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'
};

View file

@ -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})) {

View file

@ -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;

View file

@ -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>

View file

@ -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 {

View file

@ -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',

View file

@ -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) {