From 96b678a20d589e22471f0f99495f586ec898f155 Mon Sep 17 00:00:00 2001 From: Chris Raible Date: Wed, 19 Jul 2023 18:14:20 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20members=20unable=20to=20?= =?UTF-8?q?unsubscribe=20from=20plan=20if=20hidden=20in=20Portal=20(#17251?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs TryGhost/Product#3563 - For a member on a paid plan, which had subsequently been hidden from portal, the member was unable to unsubscribe/change plan because the 'Change' button was hidden - This change restores the 'Change' button for members on a paid plan, even if the plan is hidden from portal - This change also makes some modifications to the 'Change Plan' page, like showing the current active plan even if it is hidden, and displays a message to comped members to contact support if they want to change their plan --------- Co-authored-by: Sodbileg Gansukh --- apps/portal/src/components/Global.styles.js | 6 ++++++ .../src/components/common/ProductsSection.js | 19 +++++++++++++++---- .../components/PaidAccountActions.js | 10 ++++++---- .../src/components/pages/AccountPlanPage.js | 6 ++++-- apps/portal/src/utils/helpers.js | 9 +++++++++ ghost/i18n/locales/context.json | 1 + ghost/i18n/locales/en/portal.json | 1 + 7 files changed, 42 insertions(+), 10 deletions(-) diff --git a/apps/portal/src/components/Global.styles.js b/apps/portal/src/components/Global.styles.js index ffa0009663..b9fddfc6af 100644 --- a/apps/portal/src/components/Global.styles.js +++ b/apps/portal/src/components/Global.styles.js @@ -109,6 +109,12 @@ export const GlobalStyles = ` cursor: pointer; } + p a { + font-weight: 500; + color: var(--brandcolor); + text-decoration: none; + } + svg { box-sizing: content-box; } diff --git a/apps/portal/src/components/common/ProductsSection.js b/apps/portal/src/components/common/ProductsSection.js index f0eee36a2f..930a2a1a17 100644 --- a/apps/portal/src/components/common/ProductsSection.js +++ b/apps/portal/src/components/common/ProductsSection.js @@ -1,7 +1,7 @@ import React, {useContext, useEffect, useState} from 'react'; import {ReactComponent as LoaderIcon} from '../../images/icons/loader.svg'; import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg'; -import {getCurrencySymbol, getPriceString, getStripeAmount, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct, getFreeProductBenefits, formatNumber, isCookiesDisabled, hasOnlyFreeProduct, isMemberActivePrice, hasFreeTrialTier} from '../../utils/helpers'; +import {getCurrencySymbol, getPriceString, getStripeAmount, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct, getFreeProductBenefits, getSupportAddress, formatNumber, isCookiesDisabled, hasOnlyFreeProduct, isMemberActivePrice, hasFreeTrialTier, isComplimentaryMember} from '../../utils/helpers'; import AppContext from '../../AppContext'; import calculateDiscount from '../../utils/discount'; import Interpolate from '@doist/react-interpolate'; @@ -913,7 +913,7 @@ function getActiveInterval({portalPlans, selectedInterval = 'year'}) { } function ProductsSection({onPlanSelect, products, type = null, handleChooseSignup, errors}) { - const {site} = useContext(AppContext); + const {site, member, t} = useContext(AppContext); const {portal_plans: portalPlans} = site; const defaultInterval = getActiveInterval({portalPlans}); @@ -924,6 +924,8 @@ function ProductsSection({onPlanSelect, products, type = null, handleChooseSignu const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct}); const activeInterval = getActiveInterval({portalPlans, selectedInterval}); + const isComplimentary = isComplimentaryMember({member}); + useEffect(() => { setSelectedProduct(defaultProductId); }, [defaultProductId]); @@ -937,7 +939,16 @@ function ProductsSection({onPlanSelect, products, type = null, handleChooseSignu } if (products.length === 0) { - return null; + if (isComplimentary) { + const supportAddress = getSupportAddress({site}); + return ( +

+ {t('Please contact {{supportAddress}} to adjust your complimentary subscription.', {supportAddress})} +

+ ); + } else { + return null; + } } let className = 'gh-portal-products'; @@ -1091,7 +1102,7 @@ function ChangeProductCard({product, onPlanSelect}) { function ChangeProductCards({products, onPlanSelect}) { return products.map((product) => { - if (product.id === 'free') { + if (!product || product.id === 'free') { return null; } return ( diff --git a/apps/portal/src/components/pages/AccountHomePage/components/PaidAccountActions.js b/apps/portal/src/components/pages/AccountHomePage/components/PaidAccountActions.js index 9a62234e10..7adb91ef0a 100644 --- a/apps/portal/src/components/pages/AccountHomePage/components/PaidAccountActions.js +++ b/apps/portal/src/components/pages/AccountHomePage/components/PaidAccountActions.js @@ -1,5 +1,5 @@ import AppContext from '../../../../AppContext'; -import {allowCompMemberUpgrade, getCompExpiry, getMemberSubscription, getMemberTierName, getUpdatedOfferPrice, hasMultipleProductsFeature, hasOnlyFreePlan, isComplimentaryMember, isInThePast, subscriptionHasFreeTrial} from '../../../../utils/helpers'; +import {allowCompMemberUpgrade, getCompExpiry, getMemberSubscription, getMemberTierName, getUpdatedOfferPrice, hasMultipleProductsFeature, hasOnlyFreePlan, isComplimentaryMember, isPaidMember, isInThePast, subscriptionHasFreeTrial} from '../../../../utils/helpers'; import {getDateString} from '../../../../utils/date-time'; import {ReactComponent as LoaderIcon} from '../../../../images/icons/loader.svg'; import {ReactComponent as OfferTagIcon} from '../../../../images/icons/offer-tag.svg'; @@ -83,9 +83,9 @@ const PaidAccountActions = () => { ); }; - const PlanUpdateButton = ({isComplimentary}) => { + const PlanUpdateButton = ({isComplimentary, isPaid}) => { const hideUpgrade = allowCompMemberUpgrade({member}) ? false : isComplimentary; - if (hideUpgrade || hasOnlyFreePlan({site})) { + if (hideUpgrade || (hasOnlyFreePlan({site}) && !isPaid)) { return null; } return ( @@ -138,6 +138,8 @@ const PaidAccountActions = () => { const subscription = getMemberSubscription({member}); const isComplimentary = isComplimentaryMember({member}); + const isPaid = isPaidMember({member}); + const isCancelled = subscription?.cancel_at_period_end; if (subscription || isComplimentary) { const { price, @@ -160,7 +162,7 @@ const PaidAccountActions = () => {

{planLabel}

- + diff --git a/apps/portal/src/components/pages/AccountPlanPage.js b/apps/portal/src/components/pages/AccountPlanPage.js index 2794f4d62c..1cdd4273d5 100644 --- a/apps/portal/src/components/pages/AccountPlanPage.js +++ b/apps/portal/src/components/pages/AccountPlanPage.js @@ -5,7 +5,7 @@ import CloseButton from '../common/CloseButton'; import BackButton from '../common/BackButton'; import {MultipleProductsPlansSection} from '../common/PlansSection'; import {getDateString} from '../../utils/date-time'; -import {allowCompMemberUpgrade, formatNumber, getAvailablePrices, getFilteredPrices, getMemberActivePrice, getMemberSubscription, getPriceFromSubscription, getProductFromPrice, getSubscriptionFromId, getUpgradeProducts, hasMultipleProductsFeature, isComplimentaryMember, isPaidMember} from '../../utils/helpers'; +import {allowCompMemberUpgrade, formatNumber, getAvailablePrices, getFilteredPrices, getMemberActivePrice, getMemberActiveProduct, getMemberSubscription, getPriceFromSubscription, getProductFromPrice, getSubscriptionFromId, getUpgradeProducts, hasMultipleProductsFeature, isComplimentaryMember, isPaidMember} from '../../utils/helpers'; import Interpolate from '@doist/react-interpolate'; import {SYNTAX_I18NEXT} from '@doist/react-interpolate'; @@ -225,9 +225,11 @@ const ChangePlanSection = ({plans, selectedPlan, onPlanSelect, onCancelSubscript function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, onPlanCheckout, changePlan = false}) { const {site, member} = useContext(AppContext); const products = getUpgradeProducts({site, member}); + const isComplimentary = isComplimentaryMember({member}); + const activeProduct = getMemberActiveProduct({member, site}); return ( 0 || isComplimentary ? products : [activeProduct]} selectedPlan={selectedPlan} changePlan={changePlan} onPlanSelect={onPlanSelect} diff --git a/apps/portal/src/utils/helpers.js b/apps/portal/src/utils/helpers.js index 99e9ae6ca2..009242f2e2 100644 --- a/apps/portal/src/utils/helpers.js +++ b/apps/portal/src/utils/helpers.js @@ -137,6 +137,15 @@ export function getMemberActivePrice({member}) { return getPriceFromSubscription({subscription}); } +export function getMemberActiveProduct({member, site}) { + const subscription = getMemberSubscription({member}); + const price = getPriceFromSubscription({subscription}); + const allProducts = getAllProductsForSite({site}); + return allProducts.find((product) => { + return product.id === price?.product.product_id; + }); +} + export function isMemberActivePrice({priceId, site, member}) { const activePrice = getMemberActivePrice({member}); const {tierId, cadence} = getProductCadenceFromPrice({site, priceId}); diff --git a/ghost/i18n/locales/context.json b/ghost/i18n/locales/context.json index cc587b8de6..8752018c47 100644 --- a/ghost/i18n/locales/context.json +++ b/ghost/i18n/locales/context.json @@ -91,6 +91,7 @@ "Plan checkout was cancelled.": "Notification for when a plan checkout was cancelled", "Plan upgrade was cancelled.": "Notification for when a plan upgrade was cancelled", "Please confirm your email address with this link:": "Descriptive text in signup emails, right before the button members click to confirm their address", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "A message to comped members when trying to change their subscription, but no other paid plans are available.", "Please enter a valid email address": "Err message when an email address is invalid", "Please fill in required fields": "Error message when a required field is missing", "Price": "A label to indicate price of a tier", diff --git a/ghost/i18n/locales/en/portal.json b/ghost/i18n/locales/en/portal.json index 25fd4673e6..e3a4683d51 100644 --- a/ghost/i18n/locales/en/portal.json +++ b/ghost/i18n/locales/en/portal.json @@ -87,6 +87,7 @@ "Plan checkout was cancelled.": "", "Plan upgrade was cancelled.": "", "Please fill in required fields": "", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "", "Price": "", "Re-enable emails": "", "Renews at {{price}}.": "",