🐛 Fixed members unable to unsubscribe from plan if hidden in Portal (#17251)

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 <sodbileg.gansukh@gmail.com>
This commit is contained in:
Chris Raible 2023-07-19 18:14:20 -07:00 committed by GitHub
parent a17b2f024e
commit 96b678a20d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 42 additions and 10 deletions

View File

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

View File

@ -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,8 +939,17 @@ function ProductsSection({onPlanSelect, products, type = null, handleChooseSignu
}
if (products.length === 0) {
if (isComplimentary) {
const supportAddress = getSupportAddress({site});
return (
<p style={{textAlign: 'center'}}>
{t('Please contact {{supportAddress}} to adjust your complimentary subscription.', {supportAddress})}
</p>
);
} else {
return null;
}
}
let className = 'gh-portal-products';
if (type === 'upgrade') {
@ -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 (

View File

@ -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 = () => {
<h3>{planLabel}</h3>
<PlanLabel price={price} isComplimentary={isComplimentary} subscription={subscription} />
</div>
<PlanUpdateButton isComplimentary={isComplimentary} />
<PlanUpdateButton isComplimentary={isComplimentary} isPaid={isPaid} isCancelled={isCancelled} />
</section>
<BillingSection isComplimentary={isComplimentary} defaultCardLast4={defaultCardLast4} />
</>

View File

@ -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 (
<MultipleProductsPlansSection
products={products}
products={products.length > 0 || isComplimentary ? products : [activeProduct]}
selectedPlan={selectedPlan}
changePlan={changePlan}
onPlanSelect={onPlanSelect}

View File

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

View File

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

View File

@ -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}}.": "",