From a32683fbb06700296a5d8238412b09fae8ccb3b0 Mon Sep 17 00:00:00 2001 From: Rish Date: Wed, 16 Sep 2020 13:05:15 +0530 Subject: [PATCH] Updated change Plan UX no issue - Updates various flows to update/change plan for a member - Adds a confirmation step for different change plan actions - Adds new helpers for plans and members - Updates Account plan page to use more streamlined components - Fixed lint --- .../src/components/common/BackButton.js | 4 +- .../src/components/common/PlansSection.js | 8 +- .../src/components/pages/AccountHomePage.js | 38 ++-- .../src/components/pages/AccountPlanPage.js | 171 +++++------------- .../portal/src/components/pages/SignupPage.js | 11 +- ghost/portal/src/utils/helpers.js | 47 ++++- 6 files changed, 128 insertions(+), 151 deletions(-) diff --git a/ghost/portal/src/components/common/BackButton.js b/ghost/portal/src/components/common/BackButton.js index 6bd33639d9..e3fa4c79ab 100644 --- a/ghost/portal/src/components/common/BackButton.js +++ b/ghost/portal/src/components/common/BackButton.js @@ -37,9 +37,7 @@ function ActionButton({label = 'Back', brandColor = '#3eb0ef', hidden = false, o if (hidden) { return; } - let style = { - color: brandColor - }; + return ( - +
@@ -224,19 +224,25 @@ const AccountActions = ({member, action, openEditProfile, openUpdatePlan, onEdit ); }; -const SubscribeButton = ({site, openSubscribe, brandColor}) => { +const SubscribeButton = ({site, action, openSubscribe, brandColor}) => { const {is_stripe_configured: isStripeConfigured} = site; - if (!isStripeConfigured) { + if (!isStripeConfigured || hasOnlyFreePlan({site})) { return null; } - + const isRunning = ['checkoutPlan:running'].includes(action); return ( - openSubscribe()} brandColor={brandColor} style={{width: '100%'}} /> + openSubscribe()} + brandColor={brandColor} + style={{width: '100%'}} + /> ); }; -const AccountWelcome = ({member, site, openSubscribe, brandColor}) => { +const AccountWelcome = ({member, action, site, openSubscribe, brandColor}) => { const {name, firstname, email} = member; const {title: siteTitle} = site; const {is_stripe_configured: isStripeConfigured} = site; @@ -255,7 +261,7 @@ const AccountWelcome = ({member, site, openSubscribe, brandColor}) => { Hey {firstname || name || email}! You are subscribed to free updates from {siteTitle}, but you don't have a paid subscription to unlock full access

- +
); }; @@ -318,7 +324,7 @@ const AccountMain = ({member, site, onAction, action, openSubscribe, brandColor,
- openSubscribe(e)} brandColor={brandColor} /> + openSubscribe(e)} brandColor={brandColor} /> openEditProfile(e)} onToggleSubscription={(e, subscribed) => onToggleSubscription(e, subscribed)} openUpdatePlan={(e, subscribed) => openUpdatePlan(e, subscribed)} @@ -364,6 +371,11 @@ export default class AccountHomePage extends React.Component { }); } + checkoutPlan(plan) { + const {onAction} = this.context; + onAction('checkoutPlan', {plan: plan.name}); + } + openUpdatePlan() { const {is_stripe_configured: isStripeConfigured} = this.context.site; if (isStripeConfigured) { diff --git a/ghost/portal/src/components/pages/AccountPlanPage.js b/ghost/portal/src/components/pages/AccountPlanPage.js index e8d08d91e4..ad839479fc 100644 --- a/ghost/portal/src/components/pages/AccountPlanPage.js +++ b/ghost/portal/src/components/pages/AccountPlanPage.js @@ -3,7 +3,7 @@ import ActionButton from '../common/ActionButton'; import BackButton from '../common/BackButton'; import PlansSection from '../common/PlansSection'; import {getDateString} from '../../utils/date-time'; -import {getMemberSubscription, getPlanFromSubscription, getSitePlans, getSubscriptionFromId} from '../../utils/helpers'; +import {getMemberActivePlan, getMemberSubscription, getPlanFromSubscription, getSitePlans, getSubscriptionFromId, isPaidMember} from '../../utils/helpers'; export const AccountPlanPageStyles = ` .gh-portal-accountplans-main { @@ -41,7 +41,7 @@ const GlobalError = ({message, style}) => { ); }; -export const CancelContinueSubscription = ({member, onCancelContinueSubscription, onAction, action, brandColor, showOnlyContinue = false}) => { +const CancelContinueSubscription = ({member, onCancelContinueSubscription, onAction, action, brandColor, showOnlyContinue = false}) => { if (!member.paid) { return null; } @@ -54,6 +54,11 @@ export const CancelContinueSubscription = ({member, onCancelContinueSubscription if (showOnlyContinue && !subscription.cancel_at_period_end) { return null; } + + // Hide the button if subscription is due cancellation + if (subscription.cancel_at_period_end) { + return null; + } const label = subscription.cancel_at_period_end ? 'Continue subscription' : 'Cancel subscription'; const isRunning = ['cancelSubscription:running'].includes(action); const disabled = (isRunning) ? true : false; @@ -77,10 +82,6 @@ export const CancelContinueSubscription = ({member, onCancelContinueSubscription { - // onAction('cancelSubscription', { - // subscriptionId: subscription.id, - // cancelAtPeriodEnd: !subscription.cancel_at_period_end - // }); onCancelContinueSubscription({ subscriptionId: subscription.id, cancelAtPeriodEnd: !subscription.cancel_at_period_end @@ -100,54 +101,20 @@ export const CancelContinueSubscription = ({member, onCancelContinueSubscription ); }; -const Header = ({member, brandColor, onBack, showConfirmation, confirmationType}) => { +const Header = ({member, lastPage, brandColor, onBack, showConfirmation, confirmationType}) => { let title = member.paid ? 'Choose plan' : 'Choose your plan'; if (showConfirmation) { title = getConfirmationPageTitle({confirmationType}); } return (
- onBack(e)} /> + {lastPage ? onBack(e)} /> : null}

{title}

); }; -const Footer = ({member, action, plan, brandColor, onPlanCheckout, onBack}) => { - return ( -
- - -
- ); -}; - -const SubmitButton = ({member, action, plan, brandColor, onPlanCheckout}) => { - const isRunning = ['updateSubscription:running', 'checkoutPlan:running'].includes(action); - const label = member.paid ? 'Change Plan' : 'Continue'; - const disabled = (isRunning || !plan) ? true : false; - const subscription = getMemberSubscription({member}); - const isPrimary = (!subscription || !subscription.cancel_at_period_end); - return ( - onPlanCheckout(e)} - disabled={disabled} - isRunning={isRunning} - isPrimary={isPrimary} - brandColor={brandColor} - label={label} - style={{height: '40px'}} - /> - ); -}; - -const PlanConfirmation = ({plan, type, brandColor, onConfirm, member}) => { +const PlanConfirmation = ({action, member, plan, type, brandColor, onConfirm}) => { let actionDescription = ''; let confirmMessage = 'Are you sure ?'; if (type === 'changePlan') { @@ -158,6 +125,7 @@ const PlanConfirmation = ({plan, type, brandColor, onConfirm, member}) => { const subscription = getMemberSubscription({member}); actionDescription = `If you confirm and end your subscription now, you can still access it until ${getDateString(subscription.current_period_end)}`; } + const isRunning = ['updateSubscription:running', 'checkoutPlan:running', 'cancelSubscription:running'].includes(action); const label = 'Confirm'; return (
@@ -165,7 +133,7 @@ const PlanConfirmation = ({plan, type, brandColor, onConfirm, member}) => {
{confirmMessage}
onConfirm(e, plan)} - isRunning={false} + isRunning={isRunning} isPrimary={true} brandColor={brandColor} label={label} @@ -204,12 +172,13 @@ const PlanMain = ({ if (!showConfirmation) { return ( ); } return ( - + ); }; export default class AccountPlanPage extends React.Component { @@ -217,19 +186,29 @@ export default class AccountPlanPage extends React.Component { constructor(props, context) { super(props, context); - const {member} = this.context; - const {site} = this.context; - this.plans = getSitePlans({site}); - let activePlan = this.getActivePlan({member}); + this.state = this.getInitialState(); + } + + getInitialState() { + const {member, site} = this.context; + this.plans = getSitePlans({site, includeFree: false}); + let activePlan = getMemberActivePlan({member}); const selectedPlan = activePlan ? this.plans.find((d) => { return (d.name === activePlan.name && d.price === activePlan.price && d.currency === activePlan.currency); }) : null; - // const activePlanExists = this.plans.some(d => d.name === activePlan); - // if (!activePlanExists) { - // activePlan = this.plans[0].name; - // } - this.state = { - plan: selectedPlan ? selectedPlan.name : null + const isFreeMember = !isPaidMember({member}); + const selectedPlanName = selectedPlan ? selectedPlan.name : null; + if (isFreeMember && this.plans.length === 1) { + return { + selectedPlan: selectedPlanName, + showConfirmation: true, + isDirectConfirmation: true, + confirmationPlan: this.plans[0], + confirmationType: 'subscribe' + }; + } + return { + selectedPlan: selectedPlanName }; } @@ -239,7 +218,7 @@ export default class AccountPlanPage extends React.Component { } onBack(e) { - if (this.state.showConfirmation) { + if (this.state.showConfirmation && !this.state.isDirectConfirmation) { this.cancelConfirmPage(); } else { this.context.onAction('back'); @@ -254,29 +233,6 @@ export default class AccountPlanPage extends React.Component { }); } - onPlanCheckoutOld(e) { - e.preventDefault(); - this.setState((state) => { - const errors = this.validateForm({state}); - return { - errors - }; - }, () => { - const {onAction, member} = this.context; - const {plan, errors} = this.state; - const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0); - if (!hasFormErrors) { - if (member.paid) { - const {subscriptions} = member; - const subscriptionId = subscriptions[0].id; - onAction('updateSubscription', {plan, subscriptionId}); - } else { - onAction('checkoutPlan', {plan}); - } - } - }); - } - onPlanCheckout(e) { const {onAction, member} = this.context; const {confirmationPlan: plan, errors} = this.state; @@ -285,7 +241,7 @@ export default class AccountPlanPage extends React.Component { if (member.paid) { const {subscriptions} = member; const subscriptionId = subscriptions[0].id; - onAction('updateSubscription', {plan: plan.name, subscriptionId}); + onAction('updateSubscription', {plan: plan.name, subscriptionId, cancelAtPeriodEnd: false}); } else { onAction('checkoutPlan', {plan: plan.name}); } @@ -298,21 +254,13 @@ export default class AccountPlanPage extends React.Component { const confirmationPlan = this.plans.find(d => d.name === name); const activePlan = this.getActivePlanName({member}); const confirmationType = activePlan ? 'changePlan' : 'subscribe'; - if (name !== this.state.plan) { + if (name !== this.state.selectedPlan) { this.setState({ confirmationPlan, confirmationType, showConfirmation: true }); } - // // Hack: React checkbox gets out of sync with dom state with instant update - // setTimeout(() => { - // this.setState((state) => { - // return { - // plan: name - // }; - // }); - // }, 5); } onCancelContinueSubscription({subscriptionId, cancelAtPeriodEnd}) { @@ -345,19 +293,6 @@ export default class AccountPlanPage extends React.Component { }); } - getActivePlan({member}) { - if (member && member.paid && member.subscriptions[0]) { - const {plan} = member.subscriptions[0]; - return { - type: plan.interval, - price: plan.amount / 100, - currency: plan.currency_symbol, - name: plan.nickname - }; - } - return null; - } - getActivePlanName({member}) { if (member && member.paid && member.subscriptions[0]) { const {plan} = member.subscriptions[0]; @@ -366,40 +301,32 @@ export default class AccountPlanPage extends React.Component { return null; } - validateForm({state}) { - const {member} = this.context; - const activePlan = this.getActivePlanName({member}); - if (activePlan === state.plan) { - return { - global: 'Please select a different plan' - }; + onConfirm() { + const {confirmationType} = this.state; + if (confirmationType === 'cancel') { + return this.onCancelSubscriptionConfirmation(); + } else if (['changePlan', 'subscribe'].includes(confirmationType)) { + return this.onPlanCheckout(); } - return {}; } render() { - const {member, brandColor} = this.context; + const {member, brandColor, lastPage} = this.context; const plans = this.plans; - const selectedPlan = this.state.plan; - const errors = this.state.errors || {}; - const {showConfirmation, confirmationPlan, confirmationType} = this.state; - let onConfirm = () => {}; - if (confirmationType === 'cancel') { - onConfirm = () => this.onCancelSubscriptionConfirmation(); - } else if (['changePlan', 'subscribe'].includes(confirmationType)) { - onConfirm = e => this.onPlanCheckout(e); - } + const {selectedPlan, errors = {}, showConfirmation, confirmationPlan, confirmationType} = this.state; return (
this.onBack(e)} confirmationType = {confirmationType} showConfirmation = {showConfirmation} /> this.onConfirm()} onCancelSubscriptionConfirmation = {() => this.onCancelSubscriptionConfirmation()} onCancelContinueSubscription = {data => this.onCancelContinueSubscription(data)} onPlanSelect = {(e, name) => this.onPlanSelect(e, name)} diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index 5893930a7e..ad3adb4979 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -4,6 +4,7 @@ import PlansSection from '../common/PlansSection'; import InputForm from '../common/InputForm'; import {ValidateInputForm} from '../../utils/form'; import CalculateDiscount from '../../utils/discount'; +import {getSitePlans, hasOnlyFreePlan} from '../../utils/helpers'; const React = require('react'); @@ -259,11 +260,10 @@ class SignupPage extends React.Component { } renderSubmitButton() { - const {action, brandColor} = this.context; - const plans = this.getPlans(); + const {action, site, brandColor} = this.context; let label = 'Continue'; - if (!plans || plans.length === 0 || (plans.length === 1 && plans[0].type === 'free')) { + if (hasOnlyFreePlan({site})) { label = 'Sign up'; } @@ -293,7 +293,8 @@ class SignupPage extends React.Component { } renderPlans() { - const plansData = this.getPlans(); + const {site} = this.context; + const plansData = getSitePlans({site}); return (
@@ -326,7 +327,7 @@ class SignupPage extends React.Component { const plansData = this.getPlans(); const fields = this.getInputFields({state: this.state}); let sectionClass = ''; - + if (plansData.length <= 1) { if ((plansData.length === 1 && plansData[0].type === 'free') || plansData.length === 0) { sectionClass = 'noplan'; diff --git a/ghost/portal/src/utils/helpers.js b/ghost/portal/src/utils/helpers.js index e6d2898b1f..8635e43b8e 100644 --- a/ghost/portal/src/utils/helpers.js +++ b/ghost/portal/src/utils/helpers.js @@ -8,7 +8,7 @@ export function getMemberSubscription({member = {}}) { return null; } -export function isMemberComplimentary({member = {}}) { +export function isComplimentaryMember({member = {}}) { const subscription = getMemberSubscription({member}); if (subscription) { const {plan} = subscription; @@ -33,6 +33,11 @@ export function getPlanFromSubscription({subscription}) { return null; } +export function getMemberActivePlan({member}) { + const subscription = getMemberSubscription({member}); + return getPlanFromSubscription({subscription}); +} + export function getSubscriptionFromId({member, subscriptionId}) { if (member.paid) { const subscriptions = member.subscriptions || []; @@ -41,10 +46,26 @@ export function getSubscriptionFromId({member, subscriptionId}) { return null; } -export function getSitePlans({site = {}}) { - const {plans} = site; +export function hasOnlyFreePlan({site = {}}) { + const plans = getSitePlans({site}); + return !plans || plans.length === 0 || (plans.length === 1 && plans[0].type === 'free'); +} + +export function getSitePlans({site = {}, includeFree = true}) { + const { + plans, + allow_self_signup: allowSelfSignup, + is_stripe_configured: isStripeConfigured, + portal_plans: portalPlans + } = site || {}; + + if (!plans) { + return []; + } + + const plansData = []; const discount = CalculateDiscount(plans.monthly, plans.yearly); - return [ + const stripePlans = [ { type: 'month', price: plans.monthly, @@ -59,4 +80,22 @@ export function getSitePlans({site = {}}) { discount } ]; + + if (allowSelfSignup && portalPlans.includes('free') && includeFree) { + plansData.push({ + type: 'free', + price: 0, + currency: plans.currency_symbol, + name: 'Free' + }); + } + + if (isStripeConfigured) { + stripePlans.forEach((plan) => { + if (portalPlans.includes(plan.name.toLowerCase())) { + plansData.push(plan); + } + }); + } + return plansData; }