diff --git a/ghost/portal/src/App.js b/ghost/portal/src/App.js index 6a3383443e..5afd27302b 100644 --- a/ghost/portal/src/App.js +++ b/ghost/portal/src/App.js @@ -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' }; diff --git a/ghost/portal/src/components/common/PlansSection.js b/ghost/portal/src/components/common/PlansSection.js index 05cfb03afa..dec5e5efb3 100644 --- a/ghost/portal/src/components/common/PlansSection.js +++ b/ghost/portal/src/components/common/PlansSection.js @@ -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 ( +
+
+ +
+
+ ); + } + return (
@@ -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 ( +
onPlanSelect(e, id)}> +

{displayName}

+ +
+ {(changePlan && selectedPlan === id ? Current plan : '')} +
+
+ ); + }); +} + +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 ( +
+
+ +
+
+ ); +} + function PlansSection({plans, showLabel = true, selectedPlan, onPlanSelect, changePlan = false}) { const {site} = useContext(AppContext); if (hasOnlyFreePlan({plans})) { diff --git a/ghost/portal/src/components/common/ProductsSection.js b/ghost/portal/src/components/common/ProductsSection.js index f65ded20b9..7ebd97ee55 100644 --- a/ghost/portal/src/components/common/ProductsSection.js +++ b/ghost/portal/src/components/common/ProductsSection.js @@ -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 ( +
+
+
+

{product.name}

+
{product.description}
+ +
+ {/* */} + +
+ +
+ ); +} + +function ChangeProductCards({products, onPlanSelect}) { + return products.map((product) => { + if (product.id === 'free') { + return null; + } + return ( + + ); + }); +} + +export function ChangeProductSection({products, onPlanSelect}) { + return ( +
+ +
+ ); +} + export default ProductsSection; diff --git a/ghost/portal/src/components/pages/AccountPlanPage.js b/ghost/portal/src/components/pages/AccountPlanPage.js index 3b2cdca613..7484e290ed 100644 --- a/ghost/portal/src/components/pages/AccountPlanPage.js +++ b/ghost/portal/src/components/pages/AccountPlanPage.js @@ -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} />
@@ -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 ( + + ); + } else if (hasMultipleProducts({site})) { return ( 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)} /> diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index 0043def626..7526dc2626 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -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 { diff --git a/ghost/portal/src/utils/fixtures.js b/ghost/portal/src/utils/fixtures.js index 770faa80de..9b4ddc7fea 100644 --- a/ghost/portal/src/utils/fixtures.js +++ b/ghost/portal/src/utils/fixtures.js @@ -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', diff --git a/ghost/portal/src/utils/helpers.js b/ghost/portal/src/utils/helpers.js index 354026a0e3..712145be51 100644 --- a/ghost/portal/src/utils/helpers.js +++ b/ghost/portal/src/utils/helpers.js @@ -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) {