mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
🐛 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:
parent
a17b2f024e
commit
96b678a20d
7 changed files with 42 additions and 10 deletions
|
@ -109,6 +109,12 @@ export const GlobalStyles = `
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--brandcolor);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, {useContext, useEffect, useState} from 'react';
|
import React, {useContext, useEffect, useState} from 'react';
|
||||||
import {ReactComponent as LoaderIcon} from '../../images/icons/loader.svg';
|
import {ReactComponent as LoaderIcon} from '../../images/icons/loader.svg';
|
||||||
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.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 AppContext from '../../AppContext';
|
||||||
import calculateDiscount from '../../utils/discount';
|
import calculateDiscount from '../../utils/discount';
|
||||||
import Interpolate from '@doist/react-interpolate';
|
import Interpolate from '@doist/react-interpolate';
|
||||||
|
@ -913,7 +913,7 @@ function getActiveInterval({portalPlans, selectedInterval = 'year'}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProductsSection({onPlanSelect, products, type = null, handleChooseSignup, errors}) {
|
function ProductsSection({onPlanSelect, products, type = null, handleChooseSignup, errors}) {
|
||||||
const {site} = useContext(AppContext);
|
const {site, member, t} = useContext(AppContext);
|
||||||
const {portal_plans: portalPlans} = site;
|
const {portal_plans: portalPlans} = site;
|
||||||
const defaultInterval = getActiveInterval({portalPlans});
|
const defaultInterval = getActiveInterval({portalPlans});
|
||||||
|
|
||||||
|
@ -924,6 +924,8 @@ function ProductsSection({onPlanSelect, products, type = null, handleChooseSignu
|
||||||
const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct});
|
const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct});
|
||||||
const activeInterval = getActiveInterval({portalPlans, selectedInterval});
|
const activeInterval = getActiveInterval({portalPlans, selectedInterval});
|
||||||
|
|
||||||
|
const isComplimentary = isComplimentaryMember({member});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedProduct(defaultProductId);
|
setSelectedProduct(defaultProductId);
|
||||||
}, [defaultProductId]);
|
}, [defaultProductId]);
|
||||||
|
@ -937,7 +939,16 @@ function ProductsSection({onPlanSelect, products, type = null, handleChooseSignu
|
||||||
}
|
}
|
||||||
|
|
||||||
if (products.length === 0) {
|
if (products.length === 0) {
|
||||||
return null;
|
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';
|
let className = 'gh-portal-products';
|
||||||
|
@ -1091,7 +1102,7 @@ function ChangeProductCard({product, onPlanSelect}) {
|
||||||
|
|
||||||
function ChangeProductCards({products, onPlanSelect}) {
|
function ChangeProductCards({products, onPlanSelect}) {
|
||||||
return products.map((product) => {
|
return products.map((product) => {
|
||||||
if (product.id === 'free') {
|
if (!product || product.id === 'free') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import AppContext from '../../../../AppContext';
|
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 {getDateString} from '../../../../utils/date-time';
|
||||||
import {ReactComponent as LoaderIcon} from '../../../../images/icons/loader.svg';
|
import {ReactComponent as LoaderIcon} from '../../../../images/icons/loader.svg';
|
||||||
import {ReactComponent as OfferTagIcon} from '../../../../images/icons/offer-tag.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;
|
const hideUpgrade = allowCompMemberUpgrade({member}) ? false : isComplimentary;
|
||||||
if (hideUpgrade || hasOnlyFreePlan({site})) {
|
if (hideUpgrade || (hasOnlyFreePlan({site}) && !isPaid)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -138,6 +138,8 @@ const PaidAccountActions = () => {
|
||||||
|
|
||||||
const subscription = getMemberSubscription({member});
|
const subscription = getMemberSubscription({member});
|
||||||
const isComplimentary = isComplimentaryMember({member});
|
const isComplimentary = isComplimentaryMember({member});
|
||||||
|
const isPaid = isPaidMember({member});
|
||||||
|
const isCancelled = subscription?.cancel_at_period_end;
|
||||||
if (subscription || isComplimentary) {
|
if (subscription || isComplimentary) {
|
||||||
const {
|
const {
|
||||||
price,
|
price,
|
||||||
|
@ -160,7 +162,7 @@ const PaidAccountActions = () => {
|
||||||
<h3>{planLabel}</h3>
|
<h3>{planLabel}</h3>
|
||||||
<PlanLabel price={price} isComplimentary={isComplimentary} subscription={subscription} />
|
<PlanLabel price={price} isComplimentary={isComplimentary} subscription={subscription} />
|
||||||
</div>
|
</div>
|
||||||
<PlanUpdateButton isComplimentary={isComplimentary} />
|
<PlanUpdateButton isComplimentary={isComplimentary} isPaid={isPaid} isCancelled={isCancelled} />
|
||||||
</section>
|
</section>
|
||||||
<BillingSection isComplimentary={isComplimentary} defaultCardLast4={defaultCardLast4} />
|
<BillingSection isComplimentary={isComplimentary} defaultCardLast4={defaultCardLast4} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import CloseButton from '../common/CloseButton';
|
||||||
import BackButton from '../common/BackButton';
|
import BackButton from '../common/BackButton';
|
||||||
import {MultipleProductsPlansSection} from '../common/PlansSection';
|
import {MultipleProductsPlansSection} from '../common/PlansSection';
|
||||||
import {getDateString} from '../../utils/date-time';
|
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 Interpolate from '@doist/react-interpolate';
|
||||||
import {SYNTAX_I18NEXT} 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}) {
|
function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, onPlanCheckout, changePlan = false}) {
|
||||||
const {site, member} = useContext(AppContext);
|
const {site, member} = useContext(AppContext);
|
||||||
const products = getUpgradeProducts({site, member});
|
const products = getUpgradeProducts({site, member});
|
||||||
|
const isComplimentary = isComplimentaryMember({member});
|
||||||
|
const activeProduct = getMemberActiveProduct({member, site});
|
||||||
return (
|
return (
|
||||||
<MultipleProductsPlansSection
|
<MultipleProductsPlansSection
|
||||||
products={products}
|
products={products.length > 0 || isComplimentary ? products : [activeProduct]}
|
||||||
selectedPlan={selectedPlan}
|
selectedPlan={selectedPlan}
|
||||||
changePlan={changePlan}
|
changePlan={changePlan}
|
||||||
onPlanSelect={onPlanSelect}
|
onPlanSelect={onPlanSelect}
|
||||||
|
|
|
@ -137,6 +137,15 @@ export function getMemberActivePrice({member}) {
|
||||||
return getPriceFromSubscription({subscription});
|
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}) {
|
export function isMemberActivePrice({priceId, site, member}) {
|
||||||
const activePrice = getMemberActivePrice({member});
|
const activePrice = getMemberActivePrice({member});
|
||||||
const {tierId, cadence} = getProductCadenceFromPrice({site, priceId});
|
const {tierId, cadence} = getProductCadenceFromPrice({site, priceId});
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
"Plan checkout was cancelled.": "Notification for when a plan checkout was cancelled",
|
"Plan checkout was cancelled.": "Notification for when a plan checkout was cancelled",
|
||||||
"Plan upgrade was cancelled.": "Notification for when a plan upgrade 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 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 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",
|
"Please fill in required fields": "Error message when a required field is missing",
|
||||||
"Price": "A label to indicate price of a tier",
|
"Price": "A label to indicate price of a tier",
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
"Plan checkout was cancelled.": "",
|
"Plan checkout was cancelled.": "",
|
||||||
"Plan upgrade was cancelled.": "",
|
"Plan upgrade was cancelled.": "",
|
||||||
"Please fill in required fields": "",
|
"Please fill in required fields": "",
|
||||||
|
"Please contact {{supportAddress}} to adjust your complimentary subscription.": "",
|
||||||
"Price": "",
|
"Price": "",
|
||||||
"Re-enable emails": "",
|
"Re-enable emails": "",
|
||||||
"Renews at {{price}}.": "",
|
"Renews at {{price}}.": "",
|
||||||
|
|
Loading…
Add table
Reference in a new issue