apps/portal/src/components/common/ProductsSection.js
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, 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';
import {SYNTAX_I18NEXT} from '@doist/react-interpolate';
export const ProductsSectionStyles = () => {
// const products = getSiteProducts({site});
// const noOfProducts = products.length;
return `
.gh-portal-products {
display: flex;
flex-direction: column;
align-items: center;
}
.gh-portal-products-pricetoggle {
position: relative;
display: flex;
background: #F3F3F3;
width: 100%;
border-radius: 999px;
padding: 4px;
height: 44px;
margin: 0 0 40px;
}
.gh-portal-products-pricetoggle:before {
position: absolute;
content: "";
display: block;
width: 50%;
top: 4px;
bottom: 4px;
right: 4px;
background: var(--white);
box-shadow: 0px 1px 3px rgba(var(--blackrgb), 0.08);
border-radius: 999px;
transition: all 0.15s ease-in-out;
}
.gh-portal-products-pricetoggle.left:before {
transform: translateX(calc(-100% + 8px));
}
.gh-portal-products-pricetoggle .gh-portal-btn {
border: 0;
height: 100% !important;
width: 50%;
border-radius: 999px;
background: transparent;
font-size: 1.5rem;
}
.gh-portal-products-pricetoggle .gh-portal-btn.active {
border: 0;
height: 100%;
width: 50%;
color: var(--grey0);
}
.gh-portal-priceoption-label {
font-size: 1.4rem;
font-weight: 400;
letter-spacing: 0.3px;
margin: 0 6px;
min-width: 180px;
}
.gh-portal-priceoption-label.monthly {
text-align: right;
}
.gh-portal-priceoption-label.inactive {
color: var(--grey8);
}
.gh-portal-maximum-discount {
font-weight: 400;
margin-left: 4px;
opacity: 0.5;
}
.gh-portal-products-grid {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
gap: 40px;
margin: 0 auto;
padding: 0;
width: 100%;
}
.gh-portal-product-card {
flex: 1;
max-width: 420px;
min-width: 320px;
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: stretch;
background: var(--white);
padding: 32px;
border-radius: 7px;
border: 1px solid var(--grey11);
min-height: 200px;
transition: border-color 0.25s ease-in-out;
}
.gh-portal-product-card.top {
border-bottom: none;
border-radius: 7px 7px 0 0;
padding-bottom: 0;
}
.gh-portal-product-card.bottom {
border-top: none;
border-radius: 0 0 7px 7px;
padding-top: 0;
}
.gh-portal-product-card:not(.disabled):hover {
border-color: var(--grey9);
}
.gh-portal-product-card.checked::before {
position: absolute;
display: block;
top: -2px;
right: -2px;
bottom: -2px;
left: -2px;
content: "";
z-index: 999;
border: 0px solid var(--brandcolor);
pointer-events: none;
border-radius: 7px;
}
.gh-portal-product-card-header {
width: 100%;
min-height: 56px;
}
.gh-portal-product-card-name-trial {
display: flex;
align-items: center;
}
.gh-portal-product-card-name-trial .gh-portal-discount-label {
margin-top: -4px;
}
.gh-portal-product-card-details {
flex: 1;
display: flex;
flex-direction: column;
width: 100%;
}
.gh-portal-product-name {
font-size: 1.8rem;
font-weight: 600;
line-height: 1.3em;
letter-spacing: 0px;
margin-top: -4px;
word-break: break-word;
width: 100%;
color: var(--brandcolor);
}
.gh-portal-discount-label-trial {
color: var(--brandcolor);
font-weight: 600;
font-size: 1.3rem;
line-height: 1;
margin-top: 4px;
}
.gh-portal-discount-label {
position: relative;
font-size: 1.25rem;
line-height: 1em;
font-weight: 600;
letter-spacing: 0.3px;
color: var(--grey0);
padding: 6px 9px;
text-align: center;
white-space: nowrap;
border-radius: 999px;
margin-right: -4px;
max-height: 24.5px;
}
.gh-portal-discount-label:before {
position: absolute;
content: "";
display: block;
background: var(--brandcolor);
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 999px;
opacity: 0.2;
}
.gh-portal-product-card-price-trial {
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: space-between;
flex-wrap: wrap;
row-gap: 10px;
column-gap: 4px;
width: 100%;
}
.gh-portal-product-card-pricecontainer {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
margin-top: 16px;
}
.gh-portal-product-price {
display: flex;
justify-content: center;
color: var(--grey0);
}
.gh-portal-product-price .currency-sign {
align-self: flex-start;
font-size: 2.7rem;
font-weight: 700;
line-height: 1.135em;
}
.gh-portal-product-price .currency-sign.long {
margin-right: 5px;
}
.gh-portal-product-price .amount {
font-size: 3.5rem;
font-weight: 700;
line-height: 1em;
letter-spacing: -1.3px;
color: var(--grey0);
}
.gh-portal-product-price .amount.trial-duration {
letter-spacing: -0.022em;
}
.gh-portal-product-price .billing-period {
align-self: flex-end;
font-size: 1.5rem;
line-height: 1.6em;
color: var(--grey5);
letter-spacing: 0.3px;
margin-left: 5px;
}
.gh-portal-product-alternative-price {
font-size: 1.3rem;
line-height: 1.6em;
color: var(--grey8);
letter-spacing: 0.3px;
display: none;
}
.after-trial-amount {
display: block;
font-size: 1.5rem;
color: var(--grey5);
margin-top: 6px;
margin-bottom: 6px;
line-height: 1;
}
.gh-portal-product-card-detaildata {
flex: 1;
}
.gh-portal-product-description {
font-size: 1.55rem;
font-weight: 600;
line-height: 1.4em;
width: 100%;
margin-top: 16px;
}
.gh-portal-product-benefits {
font-size: 1.5rem;
line-height: 1.4em;
width: 100%;
margin-top: 16px;
}
.gh-portal-product-benefit {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
}
.gh-portal-benefit-checkmark {
width: 14px;
height: 14px;
min-width: 14px;
margin: 3px 10px 0 0;
overflow: visible;
}
.gh-portal-benefit-checkmark polyline,
.gh-portal-benefit-checkmark g {
stroke-width: 3px;
}
.gh-portal-products-grid.change-plan {
padding: 0;
}
.gh-portal-btn-product {
position: sticky;
bottom: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
justify-self: flex-end;
padding: 40px 0 32px;
margin-bottom: -32px;
/*background: rgb(255,255,255);
background: linear-gradient(0deg, rgba(255,255,255,1) 75%, rgba(255,255,255,0) 100%);*/
background: transparent;
}
.gh-portal-btn-product::before {
position: absolute;
content: "";
display: block;
top: -16px;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(0deg, rgba(var(--whitergb),1) 60%, rgba(var(--whitergb),0) 100%);
z-index: 800;
}
.gh-portal-btn-product .gh-portal-btn {
background: var(--brandcolor);
color: var(--white);
border: none;
width: 100%;
z-index: 900;
}
.gh-portal-btn-product .gh-portal-btn:hover {
opacity: 0.9;
}
.gh-portal-btn-product .gh-portal-btn {
background: var(--brandcolor);
color: var(--white);
border: none;
width: 100%;
z-index: 900;
}
.gh-portal-btn-product .gh-portal-error-message {
z-index: 900;
color: var(--red);
font-size: 1.4rem;
min-height: 40px;
padding-bottom: 13px;
margin-bottom: -40px;
}
.gh-portal-current-plan {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
white-space: nowrap;
width: 100%;
height: 44px;
border-radius: 5px;
color: var(--grey5);
font-size: 1.4rem;
font-weight: 500;
line-height: 1em;
letter-spacing: 0.2px;
font-weight: 500;
background: var(--grey14);
z-index: 900;
}
.gh-portal-product-card.only-free {
margin: 0 0 16px;
min-height: unset;
}
.gh-portal-product-card.only-free .gh-portal-product-card-header {
min-height: unset;
}
@media (max-width: 670px) {
.gh-portal-products-grid {
grid-template-columns: unset;
grid-gap: 20px;
width: 100%;
max-width: 440px;
}
.gh-portal-priceoption-label {
font-size: 1.25rem;
}
.gh-portal-products-priceswitch .gh-portal-discount-label {
display: none;
}
.gh-portal-products-priceswitch {
padding-top: 18px;
}
.gh-portal-product-card {
min-height: unset;
}
.gh-portal-singleproduct-benefits .gh-portal-product-description {
text-align: center;
}
.gh-portal-product-benefit:last-of-type {
margin-bottom: 0;
}
}
@media (max-width: 480px) {
.gh-portal-product-price .amount {
font-size: 3.4rem;
}
.gh-portal-product-card {
min-width: unset;
}
.gh-portal-btn-product {
position: static;
}
.gh-portal-btn-product::before {
display: none;
}
}
@media (max-width: 370px) {
.gh-portal-product-price .currency-sign {
font-size: 1.8rem;
}
.gh-portal-product-price .amount {
font-size: 2.8rem;
}
}
/* Upgrade and change plan*/
.gh-portal-upgrade-product {
margin-top: -70px;
padding-top: 60px;
}
.gh-portal-upgrade-product .gh-portal-products-grid {
grid-template-columns: unset;
grid-gap: 20px;
width: 100%;
}
.gh-portal-upgrade-product .gh-portal-product-card .gh-portal-plan-current {
display: inline-block;
position: relative;
padding: 2px 8px;
font-size: 1.2rem;
letter-spacing: 0.3px;
text-transform: uppercase;
margin-bottom: 4px;
}
.gh-portal-upgrade-product .gh-portal-product-card .gh-portal-plan-current::before {
position: absolute;
content: "";
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 999px;
background: var(--brandcolor);
opacity: 0.15;
}
@media (max-width: 880px) {
.gh-portal-products-grid {
flex-direction: column;
margin: 0 auto;
max-width: 420px;
}
.gh-portal-product-card-header {
min-height: unset;
}
}
`;
};
const ProductsContext = React.createContext({
selectedInterval: 'month',
selectedProduct: 'free',
selectedPlan: null,
setSelectedProduct: null
});
function ProductBenefits({product}) {
if (!product.benefits || !product.benefits.length) {
return null;
}
return product.benefits.map((benefit, idx) => {
const key = benefit?.id || `benefit-${idx}`;
return (
<div className="gh-portal-product-benefit" key={key}>
<CheckmarkIcon className='gh-portal-benefit-checkmark' alt=''/>
<div className="gh-portal-benefit-title">{benefit.name}</div>
</div>
);
});
}
function ProductBenefitsContainer({product, hide = false}) {
if (!product.benefits || !product.benefits.length || hide) {
return null;
}
let className = 'gh-portal-product-benefits';
return (
<div className={className}>
<ProductBenefits product={product} />
</div>
);
}
function ProductCardAlternatePrice({price}) {
const {site} = useContext(AppContext);
const {portal_plans: portalPlans} = site;
if (!portalPlans.includes('monthly') || !portalPlans.includes('yearly')) {
return (
<div className="gh-portal-product-alternative-price"></div>
);
}
return (
<div className="gh-portal-product-alternative-price">{getPriceString(price)}</div>
);
}
function ProductCardTrialDays({trialDays, discount, selectedInterval}) {
const {site, t} = useContext(AppContext);
if (hasFreeTrialTier({site})) {
if (trialDays) {
return (
<span className="gh-portal-discount-label">{t('{{trialDays}} days free', {trialDays})}</span>
);
} else {
return null;
}
}
if (selectedInterval === 'year') {
return (
<span className="gh-portal-discount-label">{t('{{discount}}% discount', {discount})}</span>
);
}
return null;
}
function ProductCardPrice({product}) {
const {selectedInterval} = useContext(ProductsContext);
const {site} = useContext(AppContext);
const monthlyPrice = product.monthlyPrice;
const yearlyPrice = product.yearlyPrice;
const trialDays = product.trial_days;
const activePrice = selectedInterval === 'month' ? monthlyPrice : yearlyPrice;
const alternatePrice = selectedInterval === 'month' ? yearlyPrice : monthlyPrice;
if (!monthlyPrice || !yearlyPrice) {
return null;
}
const yearlyDiscount = calculateDiscount(product.monthlyPrice.amount, product.yearlyPrice.amount);
const currencySymbol = getCurrencySymbol(activePrice.currency);
if (hasFreeTrialTier({site})) {
return (
<>
<div className="gh-portal-product-card-pricecontainer">
<div className="gh-portal-product-card-price-trial">
<div className="gh-portal-product-price">
<span className={'currency-sign' + (currencySymbol.length > 1 ? ' long' : '')}>{currencySymbol}</span>
<span className="amount" data-testid="product-amount">{formatNumber(getStripeAmount(activePrice.amount))}</span>
<span className="billing-period">/{activePrice.interval}</span>
</div>
<ProductCardTrialDays trialDays={trialDays} discount={yearlyDiscount} selectedInterval={selectedInterval} />
</div>
{(selectedInterval === 'year' ? <YearlyDiscount discount={yearlyDiscount} trialDays={trialDays} /> : '')}
<ProductCardAlternatePrice price={alternatePrice} />
</div>
{/* <span className="after-trial-amount">Then {currencySymbol}{formatNumber(getStripeAmount(activePrice.amount))}/{activePrice.interval}</span> */}
</>
);
}
return (
<div className="gh-portal-product-card-pricecontainer">
<div className="gh-portal-product-card-price-trial">
<div className="gh-portal-product-price">
<span className={'currency-sign' + (currencySymbol.length > 1 ? ' long' : '')}>{currencySymbol}</span>
<span className="amount" data-testid="product-amount">{formatNumber(getStripeAmount(activePrice.amount))}</span>
<span className="billing-period">/{activePrice.interval}</span>
</div>
{(selectedInterval === 'year' ? <YearlyDiscount discount={yearlyDiscount} /> : '')}
</div>
<ProductCardAlternatePrice price={alternatePrice} />
</div>
);
}
function FreeProductCard({products, handleChooseSignup, error}) {
const {site, action, t} = useContext(AppContext);
const {selectedProduct, setSelectedProduct} = useContext(ProductsContext);
let cardClass = selectedProduct === 'free' ? 'gh-portal-product-card free checked' : 'gh-portal-product-card free';
const product = getFreeProduct({site});
let freeProductDescription = getFreeTierDescription({site});
let disabled = (action === 'signup:running') ? true : false;
if (isCookiesDisabled()) {
disabled = true;
}
// @TODO: doublecheck this!
let currencySymbol = '$';
if (products && products[1]) {
currencySymbol = getCurrencySymbol(products[1].monthlyPrice.currency);
} else {
currencySymbol = '$';
}
const hasOnlyFree = hasOnlyFreeProduct({site});
const freeBenefits = getFreeProductBenefits({site});
if (hasOnlyFree) {
if (!freeProductDescription && !freeBenefits.length) {
return null;
}
cardClass += ' only-free';
}
if (!freeProductDescription && !freeBenefits.length) {
freeProductDescription = 'Free preview';
}
return (
<>
<div className={cardClass} onClick={(e) => {
e.stopPropagation();
setSelectedProduct('free');
}} data-test-tier="free">
<div className='gh-portal-product-card-header'>
<h4 className="gh-portal-product-name">{getFreeTierTitle({site})}</h4>
{(!hasOnlyFree ?
<div className="gh-portal-product-card-pricecontainer free-trial-disabled">
<div className="gh-portal-product-price">
<span className={'currency-sign' + (currencySymbol.length > 1 ? ' long' : '')}>{currencySymbol}</span>
<span className="amount" data-testid="product-amount">0</span>
</div>
{/* <div className="gh-portal-product-alternative-price"></div> */}
</div>
: '')}
</div>
<div className='gh-portal-product-card-details'>
<div className='gh-portal-product-card-detaildata'>
{freeProductDescription
? <div className="gh-portal-product-description" data-testid="product-description">{freeProductDescription}</div>
: ''
}
<ProductBenefitsContainer product={product} />
</div>
{(!hasOnlyFree ?
<div className='gh-portal-btn-product'>
{}
<button
data-test-button='select-tier'
className='gh-portal-btn'
disabled={disabled}
onClick={(e) => {
handleChooseSignup(e, 'free');
}}>
{((selectedProduct === 'free' && disabled) ? <LoaderIcon className='gh-portal-loadingicon' /> : t('Choose'))}
</button>
{error && <div className="gh-portal-error-message">{error}</div>}
</div>
: '')}
</div>
</div>
</>
);
}
function ProductCardButton({selectedProduct, product, disabled, noOfProducts, trialDays}) {
const {t} = useContext(AppContext);
if (selectedProduct === product.id && disabled) {
return (
<LoaderIcon className='gh-portal-loadingicon' />
);
}
if (trialDays > 0) {
return (
<Interpolate
syntax={SYNTAX_I18NEXT}
string={t('Start {{amount}}-day free trial')}
mapping={{
amount: trialDays
}}
/>
);
}
return (noOfProducts > 1 ? t('Choose') : t('Continue'));
}
function ProductCard({product, products, selectedInterval, handleChooseSignup, error}) {
const {selectedProduct, setSelectedProduct} = useContext(ProductsContext);
const {action} = useContext(AppContext);
const trialDays = product.trial_days;
const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card';
const noOfProducts = products?.filter((d) => {
return d.type === 'paid';
})?.length;
let disabled = (['signup:running', 'checkoutPlan:running'].includes(action)) ? true : false;
if (isCookiesDisabled()) {
disabled = true;
}
let productDescription = product.description;
if ((!product.benefits || !product.benefits.length) && !productDescription) {
productDescription = 'Full access';
}
return (
<>
<div className={cardClass} key={product.id} onClick={(e) => {
e.stopPropagation();
setSelectedProduct(product.id);
}} data-test-tier="paid">
<div className='gh-portal-product-card-header'>
<h4 className="gh-portal-product-name">{product.name}</h4>
<ProductCardPrice product={product} />
</div>
<div className='gh-portal-product-card-details'>
<div className='gh-portal-product-card-detaildata'>
<div className="gh-portal-product-description" data-testid="product-description">
{productDescription}
</div>
<ProductBenefitsContainer product={product} />
</div>
<div className='gh-portal-btn-product'>
<button
data-test-button='select-tier'
disabled={disabled}
className='gh-portal-btn'
onClick={(e) => {
const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct: product.id});
handleChooseSignup(e, selectedPrice.id);
}}>
<ProductCardButton
{...{selectedProduct, product, disabled, noOfProducts, trialDays}}
/>
</button>
{error && <div className="gh-portal-error-message">{error}</div>}
</div>
</div>
</div>
</>
);
}
function getProductErrorMessage({product, products, selectedInterval, errors}) {
const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct: product.id});
if (selectedPrice && selectedPrice.id && errors && errors[selectedPrice.id]) {
return errors[selectedPrice.id];
}
return null;
}
function ProductCards({products, selectedInterval, handleChooseSignup, errors}) {
return products.map((product) => {
const error = getProductErrorMessage({product, products, selectedInterval, errors});
if (product.id === 'free') {
return (
<FreeProductCard products={products} key={product.id} handleChooseSignup={handleChooseSignup} error={error} />
);
}
return (
<ProductCard products={products} product={product} selectedInterval={selectedInterval} key={product.id} handleChooseSignup={handleChooseSignup} error={error}/>
);
});
}
function YearlyDiscount({discount}) {
const {site, t} = useContext(AppContext);
const {portal_plans: portalPlans} = site;
if (discount === 0 || !portalPlans.includes('monthly')) {
return null;
}
if (hasFreeTrialTier({site})) {
return (
<>
<span className="gh-portal-discount-label-trial">{t('{{discount}}% discount', {discount})}</span>
</>
);
} else {
return (
<>
<span className="gh-portal-discount-label">{t('{{discount}}% discount', {discount})}</span>
</>
);
}
}
function ProductPriceSwitch({selectedInterval, setSelectedInterval, products}) {
const {site, t} = useContext(AppContext);
const {portal_plans: portalPlans} = site;
const paidProducts = products.filter(product => product.type !== 'free');
// Extract discounts from products
const prices = paidProducts.map(product => calculateDiscount(product.monthlyPrice?.amount, product.yearlyPrice?.amount));
// Find the highest price using Math.max
const highestYearlyDiscount = Math.max(...prices);
if (!portalPlans.includes('monthly') || !portalPlans.includes('yearly')) {
return null;
}
return (
<div className='gh-portal-logged-out-form-container'>
<div className={'gh-portal-products-pricetoggle' + (selectedInterval === 'month' ? ' left' : '')}>
<button
data-test-button='switch-monthly'
data-testid="monthly-switch"
className={'gh-portal-btn' + (selectedInterval === 'month' ? ' active' : '')}
onClick={() => {
setSelectedInterval('month');
}}
>
{t('Monthly')}
</button>
<button
data-test-button='switch-yearly'
data-testid="yearly-switch"
className={'gh-portal-btn' + (selectedInterval === 'year' ? ' active' : '')}
onClick={() => {
setSelectedInterval('year');
}}
>
{t('Yearly')}
{(highestYearlyDiscount > 0) && <span className='gh-portal-maximum-discount'>{t('(save {{highestYearlyDiscount}}%)', {highestYearlyDiscount})}</span>}
</button>
</div>
</div>
);
}
function getSelectedPrice({products, selectedProduct, selectedInterval}) {
let selectedPrice = null;
if (selectedProduct === 'free') {
selectedPrice = {id: 'free'};
} else {
let product = products.find(prod => prod.id === selectedProduct);
if (!product) {
product = products.find(p => p.type === 'paid');
}
selectedPrice = selectedInterval === 'month' ? product?.monthlyPrice : product?.yearlyPrice;
}
return selectedPrice;
}
function getActiveInterval({portalPlans, portalDefaultPlan, selectedInterval}) {
if (selectedInterval === 'month' && portalPlans.includes('monthly')) {
return 'month';
}
if (selectedInterval === 'year' && portalPlans.includes('yearly')) {
return 'year';
}
if (portalDefaultPlan) {
if (portalDefaultPlan === 'monthly' && portalPlans.includes('monthly')) {
return 'month';
}
}
if (portalPlans.includes('yearly')) {
return 'year';
}
if (portalPlans.includes('monthly')) {
return 'month';
}
}
function ProductsSection({onPlanSelect, products, type = null, handleChooseSignup, errors}) {
const {site, member, t} = useContext(AppContext);
const {portal_plans: portalPlans, portal_default_plan: portalDefaultPlan} = site;
const defaultProductId = products.length > 0 ? products[0].id : 'free';
// Note: by default we set it to null, so that it changes reactively in the preview version of Portal
const [selectedInterval, setSelectedInterval] = useState(null);
const [selectedProduct, setSelectedProduct] = useState(defaultProductId);
const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct});
const activeInterval = getActiveInterval({portalPlans, portalDefaultPlan, selectedInterval});
const isComplimentary = isComplimentaryMember({member});
const hasOnlyFree = hasOnlyFreeProduct({site});
useEffect(() => {
setSelectedProduct(defaultProductId);
}, [defaultProductId]);
useEffect(() => {
onPlanSelect(null, selectedPrice.id);
}, [selectedPrice.id, onPlanSelect]);
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') {
className += ' gh-portal-upgrade-product';
}
let finalProduct = products.find(p => p.id === selectedProduct)?.id || products.find(p => p.type === 'paid')?.id;
return (
<ProductsContext.Provider value={{
selectedInterval: activeInterval,
selectedProduct: finalProduct,
setSelectedProduct
}}>
<section className={className}>
{(!(hasOnlyFree) ?
<ProductPriceSwitch
products={products}
selectedInterval={activeInterval}
setSelectedInterval={setSelectedInterval}
/>
: '')}
<div className="gh-portal-products-grid">
<ProductCards products={products} selectedInterval={activeInterval} handleChooseSignup={handleChooseSignup} errors={errors}/>
</div>
</section>
</ProductsContext.Provider>
);
}
export function ChangeProductSection({onPlanSelect, selectedPlan, products, type = null}) {
const {site, member} = useContext(AppContext);
const {portal_plans: portalPlans} = site;
const activePrice = getMemberActivePrice({member});
const activeMemberProduct = getProductFromPrice({site, priceId: activePrice.id});
const defaultInterval = getActiveInterval({portalPlans, selectedInterval: activePrice.interval});
const defaultProductId = activeMemberProduct?.id || products?.[0]?.id;
const [selectedInterval, setSelectedInterval] = useState(defaultInterval);
const [selectedProduct, setSelectedProduct] = useState(defaultProductId);
// const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct});
const activeInterval = getActiveInterval({portalPlans, selectedInterval});
useEffect(() => {
setSelectedProduct(defaultProductId);
}, [defaultProductId]);
if (!portalPlans.includes('monthly') && !portalPlans.includes('yearly')) {
return null;
}
if (products.length === 0) {
return null;
}
let className = 'gh-portal-products';
if (type === 'upgrade') {
className += ' gh-portal-upgrade-product';
}
if (type === 'changePlan') {
className += ' gh-portal-upgrade-product gh-portal-change-plan';
}
return (
<ProductsContext.Provider value={{
selectedInterval: activeInterval,
selectedProduct,
selectedPlan,
setSelectedProduct
}}>
<section className={className}>
<ProductPriceSwitch
selectedInterval={activeInterval}
setSelectedInterval={setSelectedInterval}
products={products}
/>
<div className="gh-portal-products-grid">
<ChangeProductCards products={products} onPlanSelect={onPlanSelect} />
</div>
{/* <ActionButton
onClick={e => onPlanSelect(null, selectedPrice?.id)}
isRunning={false}
disabled={!selectedPrice?.id || (activePrice.id === selectedPrice?.id)}
isPrimary={true}
brandColor={brandColor}
label={'Continue'}
style={{height: '40px', width: '100%', marginTop: '24px'}}
/> */}
</section>
</ProductsContext.Provider>
);
}
function ProductDescription({product}) {
if (product?.description) {
return (
<div className="gh-portal-product-description" data-testid="product-description">
{product.description}
</div>
);
}
return null;
}
function ChangeProductCard({product, onPlanSelect}) {
const {member, site, t} = useContext(AppContext);
const {selectedProduct, setSelectedProduct, selectedInterval} = useContext(ProductsContext);
const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card';
const monthlyPrice = product.monthlyPrice;
const yearlyPrice = product.yearlyPrice;
const memberActivePrice = getMemberActivePrice({member});
const selectedPrice = selectedInterval === 'month' ? monthlyPrice : yearlyPrice;
const currentPlan = isMemberActivePrice({member, site, priceId: selectedPrice.id});
return (
<div className={cardClass + (currentPlan ? ' disabled' : '')} key={product.id} onClick={(e) => {
e.stopPropagation();
setSelectedProduct(product.id);
}} data-test-tier="paid">
<div className='gh-portal-product-card-header'>
<h4 className="gh-portal-product-name">{product.name}</h4>
<ProductCardPrice product={product} />
</div>
<div className='gh-portal-product-card-details'>
<div className='gh-portal-product-card-detaildata'>
{product.description ? <ProductDescription product={product} selectedPrice={selectedPrice} activePrice={memberActivePrice} /> : ''}
<ProductBenefitsContainer product={product} />
</div>
{(currentPlan ?
<div className='gh-portal-btn-product'>
<span className='gh-portal-current-plan'><span>{t('Current plan')}</span></span>
</div>
:
<div className='gh-portal-btn-product'>
<button
data-test-button='select-tier'
className='gh-portal-btn'
onClick={() => {
onPlanSelect(null, selectedPrice?.id);
}}
>{t('Choose')}</button>
</div>)}
</div>
</div>
);
}
function ChangeProductCards({products, onPlanSelect}) {
return products.map((product) => {
if (!product || product.id === 'free') {
return null;
}
return (
<ChangeProductCard product={product} key={product.id} onPlanSelect={onPlanSelect} />
);
});
}
export default ProductsSection;