Split AccountHomePage file on separate files

closes TryGhost/Team#2256
- Moved components to separate file for better navigation.
This commit is contained in:
e.baidakova 2022-11-22 11:52:10 +04:00 committed by Elena Baidakova
parent 991e628022
commit 69c2af1ffe
14 changed files with 547 additions and 617 deletions

View File

@ -1,615 +0,0 @@
import AppContext from '../../AppContext';
import MemberAvatar from '../common/MemberGravatar';
import ActionButton from '../common/ActionButton';
import CloseButton from '../common/CloseButton';
import Switch from '../common/Switch';
import {allowCompMemberUpgrade, getCompExpiry, getMemberSubscription, getMemberTierName, getSiteNewsletters, getSupportAddress, getUpdatedOfferPrice, hasCommentsEnabled, hasMultipleNewsletters, hasMultipleProductsFeature, hasOnlyFreePlan, isComplimentaryMember, 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';
import {useContext} from 'react';
const React = require('react');
export const AccountHomePageStyles = `
.gh-portal-account-header {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 0 32px;
}
.gh-portal-account-header .gh-portal-avatar {
margin: 6px 0 8px !important;
}
.gh-portal-account-data {
margin-bottom: 40px;
}
footer.gh-portal-account-footer {
display: flex;
}
.gh-portal-account-footer.paid {
margin-top: 12px;
}
.gh-portal-account-footermenu {
display: flex;
align-items: center;
list-style: none;
padding: 0;
margin: 0;
}
.gh-portal-account-footerright {
display: flex;
flex-grow: 1;
align-items: center;
justify-content: flex-end;
}
.gh-portal-account-footermenu li {
margin-right: 16px;
}
.gh-portal-account-footermenu li:last-of-type {
margin-right: 0;
}
.gh-portal-freeaccount-newsletter {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24px;
}
.gh-portal-freeaccount-newsletter .label {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.gh-portal-free-ctatext {
margin-top: -12px;
}
.gh-portal-cancelcontinue-container {
margin: 24px 0 32px;
}
.gh-portal-billing-button-loader {
width: 32px;
height: 32px;
margin-right: -3px;
opacity: 0.6;
}
.gh-portal-product-icon {
width: 52px;
margin-right: 12px;
border-radius: 2px;
}
.gh-portal-account-discountcontainer {
position: relative;
display: flex;
align-items: center;
}
.gh-portal-account-old-price {
text-decoration: line-through;
color: var(--grey9) !important;
}
.gh-portal-account-tagicon {
width: 16px;
height: 16px;
color: var(--brandcolor);
margin-right: 5px;
z-index: 999;
}
@media (max-width: 390px) {
.gh-portal-account-footer {
padding: 0 !important;
}
}
@media (max-width: 340px) {
.gh-portal-account-footer {
padding: 0 !important;
flex-wrap: wrap;
gap: 12px;
}
.gh-portal-account-footer .gh-portal-account-footerright {
justify-content: flex-start;
}
}
`;
const UserAvatar = ({avatar, brandColor}) => {
return (
<>
<MemberAvatar gravatar={avatar} style={{userIcon: {color: brandColor, width: '56px', height: '56px', padding: '2px'}}} />
</>
);
};
const AccountFooter = ({onClose, handleSignout, supportAddress = ''}) => {
const supportAddressMail = `mailto:${supportAddress}`;
return (
<footer className='gh-portal-account-footer'>
<ul className='gh-portal-account-footermenu'>
<li><button className='gh-portal-btn' name='logout' aria-label='logout' onClick={e => handleSignout(e)}>Sign out</button></li>
</ul>
<div className='gh-portal-account-footerright'>
<ul className='gh-portal-account-footermenu'>
<li><a className='gh-portal-btn gh-portal-btn-branded' href={supportAddressMail} onClick={() => {
supportAddressMail && window.open(supportAddressMail);
}}>Contact support</a></li>
</ul>
</div>
</footer>
);
};
const UserHeader = () => {
const {member, brandColor} = useContext(AppContext);
const avatar = member.avatar_image;
return (
<header className='gh-portal-account-header'>
<UserAvatar avatar={avatar} brandColor={brandColor} />
<h2 className="gh-portal-main-title">Your account</h2>
</header>
);
};
function getOfferLabel({offer, price, subscriptionStartDate}) {
let offerLabel = '';
if (offer) {
const discountDuration = offer.duration;
let durationLabel = '';
if (discountDuration === 'forever') {
durationLabel = `Forever`;
} else if (discountDuration === 'repeating') {
const durationInMonths = offer.duration_in_months || 0;
let offerStartDate = new Date(subscriptionStartDate);
let offerEndDate = new Date(offerStartDate.setMonth(offerStartDate.getMonth() + durationInMonths));
durationLabel = `Ends ${getDateString(offerEndDate)}`;
}
offerLabel = `${getUpdatedOfferPrice({offer, price, useFormatted: true})}/${price.interval}${durationLabel ? `${durationLabel}` : ``}`;
}
return offerLabel;
}
function FreeTrialLabel({subscription, priceLabel}) {
if (subscriptionHasFreeTrial({sub: subscription})) {
const trialEnd = getDateString(subscription.trial_end_at);
return (
<p className="gh-portal-account-discountcontainer">
<div>
<span>Free Trial Ends {trialEnd}</span>
{/* <span>{getSubFreeTrialDaysLeft({sub: subscription})} days left</span> */}
</div>
</p>
);
}
return null;
}
const PaidAccountActions = () => {
const {member, site, onAction} = useContext(AppContext);
const onEditBilling = () => {
const subscription = getMemberSubscription({member});
onAction('editBilling', {subscriptionId: subscription.id});
};
const openUpdatePlan = () => {
const {is_stripe_configured: isStripeConfigured} = site;
if (isStripeConfigured) {
onAction('switchPage', {
page: 'accountPlan',
lastPage: 'accountHome'
});
}
};
const PlanLabel = ({price, isComplimentary, subscription}) => {
const {
offer,
start_date: startDate
} = subscription || {};
let label = '';
if (price) {
const {amount = 0, currency, interval} = price;
label = `${Intl.NumberFormat('en', {currency, style: 'currency'}).format(amount / 100)}/${interval}`;
}
let offerLabelStr = getOfferLabel({price, offer, subscriptionStartDate: startDate});
const compExpiry = getCompExpiry({member});
if (isComplimentary) {
if (compExpiry) {
label = `Complimentary - Expires ${compExpiry}`;
} else {
label = label ? `Complimentary (${label})` : `Complimentary`;
}
}
let oldPriceClassName = '';
if (offerLabelStr) {
oldPriceClassName = 'gh-portal-account-old-price';
}
const OfferLabel = () => {
if (offerLabelStr) {
return (
<p className="gh-portal-account-discountcontainer">
<OfferTagIcon className="gh-portal-account-tagicon" />
<span>{offerLabelStr}</span>
</p>
);
}
return null;
};
const hasFreeTrial = subscriptionHasFreeTrial({sub: subscription});
if (hasFreeTrial) {
oldPriceClassName = 'gh-portal-account-old-price';
}
if (hasFreeTrial) {
return (
<>
<p className={oldPriceClassName}>
{label}
</p>
<FreeTrialLabel subscription={subscription} />
</>
);
}
return (
<>
<p className={oldPriceClassName}>
{label}
</p>
<OfferLabel />
</>
);
};
const PlanUpdateButton = ({isComplimentary}) => {
const hideUpgrade = allowCompMemberUpgrade({member}) ? false : isComplimentary;
if (hideUpgrade || hasOnlyFreePlan({site})) {
return null;
}
return (
<button className='gh-portal-btn gh-portal-btn-list' onClick={e => openUpdatePlan(e)}>Change</button>
);
};
const CardLabel = ({defaultCardLast4}) => {
if (defaultCardLast4) {
const label = `**** **** **** ${defaultCardLast4}`;
return (
<p>
{label}
</p>
);
}
return null;
};
const BillingSection = ({defaultCardLast4, isComplimentary}) => {
const {action} = useContext(AppContext);
const label = action === 'editBilling:running' ? (
<LoaderIcon className='gh-portal-billing-button-loader' />
) : 'Update';
if (isComplimentary) {
return null;
}
return (
<section>
<div className='gh-portal-list-detail'>
<h3>Billing info</h3>
<CardLabel defaultCardLast4={defaultCardLast4} />
</div>
<button className='gh-portal-btn gh-portal-btn-list' onClick={e => onEditBilling(e)}>{label}</button>
</section>
);
};
const subscription = getMemberSubscription({member});
const isComplimentary = isComplimentaryMember({member});
if (subscription || isComplimentary) {
const {
price,
default_payment_card_last4: defaultCardLast4
} = subscription || {};
let planLabel = 'Plan';
// Show name of tiers if there are multiple tiers
if (hasMultipleProductsFeature({site}) && getMemberTierName({member})) {
planLabel = getMemberTierName({member});
}
// const hasFreeTrial = subscriptionHasFreeTrial({sub: subscription});
// if (hasFreeTrial) {
// planLabel += ' (Free Trial)';
// }
return (
<>
<section>
<div className='gh-portal-list-detail'>
<h3>{planLabel}</h3>
<PlanLabel price={price} isComplimentary={isComplimentary} subscription={subscription} />
</div>
<PlanUpdateButton isComplimentary={isComplimentary} />
</section>
<BillingSection isComplimentary={isComplimentary} defaultCardLast4={defaultCardLast4} />
</>
);
}
return null;
};
const AccountActions = () => {
const {member, onAction} = useContext(AppContext);
const {name, email} = member;
const openEditProfile = () => {
onAction('switchPage', {
page: 'accountProfile',
lastPage: 'accountHome'
});
};
return (
<div>
<div className='gh-portal-list'>
<section>
<div className='gh-portal-list-detail'>
<h3>{(name ? name : 'Account')}</h3>
<p>{email}</p>
</div>
<button className='gh-portal-btn gh-portal-btn-list' onClick={e => openEditProfile(e)}>Edit</button>
</section>
<PaidAccountActions />
<EmailPreferencesAction />
<EmailNewsletterAction />
</div>
{/* <ProductList openUpdatePlan={openUpdatePlan}></ProductList> */}
</div>
);
};
function EmailNewsletterAction() {
const {member, site, onAction} = useContext(AppContext);
let {newsletters} = member;
if (hasMultipleNewsletters({site}) || hasCommentsEnabled({site})) {
return null;
}
const subscribed = !!newsletters?.length;
let label = subscribed ? 'Subscribed' : 'Unsubscribed';
const onToggleSubscription = (e, sub) => {
e.preventDefault();
const siteNewsletters = getSiteNewsletters({site});
const subscribedNewsletters = !member?.newsletters?.length ? siteNewsletters : [];
onAction('updateNewsletterPreference', {newsletters: subscribedNewsletters});
};
return (
<section>
<div className='gh-portal-list-detail'>
<h3>Email newsletter</h3>
<p>{label}</p>
</div>
<div>
<Switch
id="default-newsletter-toggle"
onToggle={(e) => {
onToggleSubscription(e, subscribed);
}} checked={subscribed}
/>
</div>
</section>
);
}
function EmailPreferencesAction() {
const {site, onAction} = useContext(AppContext);
if (!hasMultipleNewsletters({site}) && !hasCommentsEnabled({site})) {
return null;
}
return (
<section>
<div className='gh-portal-list-detail'>
<h3>Emails</h3>
<p>Update your preferences</p>
</div>
<button className='gh-portal-btn gh-portal-btn-list' onClick={(e) => {
onAction('switchPage', {
page: 'accountEmail',
lastPage: 'accountHome'
});
}}>Manage</button>
</section>
);
}
const SubscribeButton = () => {
const {site, action, brandColor, onAction} = useContext(AppContext);
const {is_stripe_configured: isStripeConfigured} = site;
if (!isStripeConfigured || hasOnlyFreePlan({site})) {
return null;
}
const isRunning = ['checkoutPlan:running'].includes(action);
const openPlanPage = () => {
onAction('switchPage', {
page: 'accountPlan',
lastPage: 'accountHome'
});
};
return (
<ActionButton
isRunning={isRunning}
label="View plans"
onClick={() => openPlanPage()}
brandColor={brandColor}
style={{width: '100%'}}
/>
);
};
const AccountWelcome = () => {
const {member, site} = useContext(AppContext);
const {is_stripe_configured: isStripeConfigured} = site;
if (!isStripeConfigured || hasOnlyFreePlan({site})) {
return null;
}
const subscription = getMemberSubscription({member});
const isComplimentary = isComplimentaryMember({member});
if (isComplimentary && !subscription) {
return null;
}
if (subscription) {
const currentPeriodEnd = subscription?.current_period_end;
if (isComplimentary && getCompExpiry({member})) {
const expiryDate = getCompExpiry({member});
const expiryAt = getDateString(expiryDate);
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will expire on {expiryAt}</p>
</div>
);
}
if (subscription?.cancel_at_period_end) {
return null;
}
if (subscriptionHasFreeTrial({sub: subscription})) {
const trialEnd = getDateString(subscription.trial_end_at);
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will start on {trialEnd}</p>
</div>
);
}
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will renew on {getDateString(currentPeriodEnd)}</p>
</div>
);
}
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>You currently have a free membership, upgrade to a paid subscription for full access.</p>
<SubscribeButton />
</div>
);
};
const ContinueSubscriptionButton = () => {
const {member, onAction, action, brandColor} = useContext(AppContext);
const subscription = getMemberSubscription({member});
if (!subscription) {
return null;
}
// To show only continue button and not 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;
const isPrimary = !!subscription.cancel_at_period_end;
const CancelNotice = () => {
if (!subscription.cancel_at_period_end) {
return null;
}
const currentPeriodEnd = subscription.current_period_end;
return (
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will expire on {getDateString(currentPeriodEnd)}</p>
);
};
return (
<div className='gh-portal-cancelcontinue-container'>
<CancelNotice />
<ActionButton
onClick={(e) => {
onAction('continueSubscription', {
subscriptionId: subscription.id
});
}}
isRunning={isRunning}
disabled={disabled}
isPrimary={isPrimary}
brandColor={brandColor}
label={label}
style={{
width: '100%'
}}
/>
</div>
);
};
const AccountMain = () => {
return (
<div className='gh-portal-content gh-portal-account-main'>
<CloseButton />
<UserHeader />
<section className='gh-portal-account-data'>
<AccountWelcome />
<ContinueSubscriptionButton />
<AccountActions />
</section>
</div>
);
};
export default class AccountHomePage extends React.Component {
static contextType = AppContext;
componentDidMount() {
const {member} = this.context;
if (!member) {
this.context.onAction('switchPage', {
page: 'signin'
});
}
}
handleSignout(e) {
e.preventDefault();
this.context.onAction('signout');
}
render() {
const {member, site} = this.context;
const supportAddress = getSupportAddress({site});
if (!member) {
return null;
}
return (
<div className='gh-portal-account-wrapper'>
<AccountMain />
<AccountFooter
onClose={() => this.context.onAction('closePopup')}
handleSignout={e => this.handleSignout(e)}
supportAddress={supportAddress}
/>
</div>
);
}
}

View File

@ -0,0 +1,42 @@
import React from 'react';
import AppContext from 'AppContext';
import {getSupportAddress} from 'utils/helpers';
import AccountFooter from './components/AccountFooter';
import AccountMain from './components/AccountMain';
export default class AccountHomePage extends React.Component {
static contextType = AppContext;
componentDidMount() {
const {member} = this.context;
if (!member) {
this.context.onAction('switchPage', {
page: 'signin'
});
}
}
handleSignout(e) {
e.preventDefault();
this.context.onAction('signout');
}
render() {
const {member, site} = this.context;
const supportAddress = getSupportAddress({site});
if (!member) {
return null;
}
return (
<div className='gh-portal-account-wrapper'>
<AccountMain />
<AccountFooter
onClose={() => this.context.onAction('closePopup')}
handleSignout={e => this.handleSignout(e)}
supportAddress={supportAddress}
/>
</div>
);
}
}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import {render, fireEvent} from '../../utils/test-utils'; import {render, fireEvent} from 'utils/test-utils';
import AccountHomePage from './AccountHomePage'; import AccountHomePage from './AccountHomePage';
const setup = (overrides) => { const setup = (overrides) => {

View File

@ -0,0 +1,39 @@
import AppContext from 'AppContext';
import {useContext} from 'react';
import PaidAccountActions from './PaidAccountActions';
import EmailNewsletterAction from './EmailNewsletterAction';
import EmailPreferencesAction from './EmailPreferencesAction';
const AccountActions = () => {
const {member, onAction} = useContext(AppContext);
const {name, email} = member;
const openEditProfile = () => {
onAction('switchPage', {
page: 'accountProfile',
lastPage: 'accountHome'
});
};
return (
<div>
<div className='gh-portal-list'>
<section>
<div className='gh-portal-list-detail'>
<h3>{(name ? name : 'Account')}</h3>
<p>{email}</p>
</div>
<button className='gh-portal-btn gh-portal-btn-list' onClick={e => openEditProfile(e)}>Edit</button>
</section>
<PaidAccountActions />
<EmailPreferencesAction />
<EmailNewsletterAction />
</div>
{/* <ProductList openUpdatePlan={openUpdatePlan}></ProductList> */}
</div>
);
};
export default AccountActions;

View File

@ -0,0 +1,19 @@
const AccountFooter = ({onClose, handleSignout, supportAddress = ''}) => {
const supportAddressMail = `mailto:${supportAddress}`;
return (
<footer className='gh-portal-account-footer'>
<ul className='gh-portal-account-footermenu'>
<li><button className='gh-portal-btn' name='logout' aria-label='logout' onClick={e => handleSignout(e)}>Sign out</button></li>
</ul>
<div className='gh-portal-account-footerright'>
<ul className='gh-portal-account-footermenu'>
<li><a className='gh-portal-btn gh-portal-btn-branded' href={supportAddressMail} onClick={() => {
supportAddressMail && window.open(supportAddressMail);
}}>Contact support</a></li>
</ul>
</div>
</footer>
);
};
export default AccountFooter;

View File

@ -0,0 +1,22 @@
import CloseButton from 'components/common/CloseButton';
import UserHeader from './UserHeader';
import AccountWelcome from './AccountWelcome';
import ContinueSubscriptionButton from './ContinueSubscriptionButton';
import AccountActions from './AccountActions';
const AccountMain = () => {
return (
<div className='gh-portal-content gh-portal-account-main'>
<CloseButton />
<UserHeader />
<section className='gh-portal-account-data'>
<AccountWelcome />
<ContinueSubscriptionButton />
<AccountActions />
</section>
</div>
);
};
export default AccountMain;

View File

@ -0,0 +1,58 @@
import AppContext from 'AppContext';
import {getCompExpiry, getMemberSubscription, hasOnlyFreePlan, isComplimentaryMember, subscriptionHasFreeTrial} from 'utils/helpers';
import {getDateString} from 'utils/date-time';
import {useContext} from 'react';
import SubscribeButton from './SubscribeButton';
const AccountWelcome = () => {
const {member, site} = useContext(AppContext);
const {is_stripe_configured: isStripeConfigured} = site;
if (!isStripeConfigured || hasOnlyFreePlan({site})) {
return null;
}
const subscription = getMemberSubscription({member});
const isComplimentary = isComplimentaryMember({member});
if (isComplimentary && !subscription) {
return null;
}
if (subscription) {
const currentPeriodEnd = subscription?.current_period_end;
if (isComplimentary && getCompExpiry({member})) {
const expiryDate = getCompExpiry({member});
const expiryAt = getDateString(expiryDate);
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will expire on {expiryAt}</p>
</div>
);
}
if (subscription?.cancel_at_period_end) {
return null;
}
if (subscriptionHasFreeTrial({sub: subscription})) {
const trialEnd = getDateString(subscription.trial_end_at);
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will start on {trialEnd}</p>
</div>
);
}
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will renew on {getDateString(currentPeriodEnd)}</p>
</div>
);
}
return (
<div className='gh-portal-section'>
<p className='gh-portal-text-center gh-portal-free-ctatext'>You currently have a free membership, upgrade to a paid subscription for full access.</p>
<SubscribeButton />
</div>
);
};
export default AccountWelcome;

View File

@ -0,0 +1,55 @@
import AppContext from 'AppContext';
import ActionButton from 'components/common/ActionButton';
import {getMemberSubscription} from 'utils/helpers';
import {getDateString} from 'utils/date-time';
import {useContext} from 'react';
const ContinueSubscriptionButton = () => {
const {member, onAction, action, brandColor} = useContext(AppContext);
const subscription = getMemberSubscription({member});
if (!subscription) {
return null;
}
// To show only continue button and not 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;
const isPrimary = !!subscription.cancel_at_period_end;
const CancelNotice = () => {
if (!subscription.cancel_at_period_end) {
return null;
}
const currentPeriodEnd = subscription.current_period_end;
return (
<p className='gh-portal-text-center gh-portal-free-ctatext'>Your subscription will expire on {getDateString(currentPeriodEnd)}</p>
);
};
return (
<div className='gh-portal-cancelcontinue-container'>
<CancelNotice />
<ActionButton
onClick={(e) => {
onAction('continueSubscription', {
subscriptionId: subscription.id
});
}}
isRunning={isRunning}
disabled={disabled}
isPrimary={isPrimary}
brandColor={brandColor}
label={label}
style={{
width: '100%'
}}
/>
</div>
);
};
export default ContinueSubscriptionButton;

View File

@ -0,0 +1,40 @@
import AppContext from 'AppContext';
import Switch from 'components/common/Switch';
import {getSiteNewsletters, hasCommentsEnabled, hasMultipleNewsletters} from 'utils/helpers';
import {useContext} from 'react';
function EmailNewsletterAction() {
const {member, site, onAction} = useContext(AppContext);
let {newsletters} = member;
if (hasMultipleNewsletters({site}) || hasCommentsEnabled({site})) {
return null;
}
const subscribed = !!newsletters?.length;
let label = subscribed ? 'Subscribed' : 'Unsubscribed';
const onToggleSubscription = (e, sub) => {
e.preventDefault();
const siteNewsletters = getSiteNewsletters({site});
const subscribedNewsletters = !member?.newsletters?.length ? siteNewsletters : [];
onAction('updateNewsletterPreference', {newsletters: subscribedNewsletters});
};
return (
<section>
<div className='gh-portal-list-detail'>
<h3>Email newsletter</h3>
<p>{label}</p>
</div>
<div>
<Switch
id="default-newsletter-toggle"
onToggle={(e) => {
onToggleSubscription(e, subscribed);
}} checked={subscribed}
/>
</div>
</section>
);
}
export default EmailNewsletterAction;

View File

@ -0,0 +1,26 @@
import AppContext from 'AppContext';
import {hasCommentsEnabled, hasMultipleNewsletters} from 'utils/helpers';
import {useContext} from 'react';
function EmailPreferencesAction() {
const {site, onAction} = useContext(AppContext);
if (!hasMultipleNewsletters({site}) && !hasCommentsEnabled({site})) {
return null;
}
return (
<section>
<div className='gh-portal-list-detail'>
<h3>Emails</h3>
<p>Update your preferences</p>
</div>
<button className='gh-portal-btn gh-portal-btn-list' onClick={(e) => {
onAction('switchPage', {
page: 'accountEmail',
lastPage: 'accountHome'
});
}}>Manage</button>
</section>
);
}
export default EmailPreferencesAction;

View File

@ -0,0 +1,196 @@
import AppContext from 'AppContext';
import {allowCompMemberUpgrade, getCompExpiry, getMemberSubscription, getMemberTierName, getUpdatedOfferPrice, hasMultipleProductsFeature, hasOnlyFreePlan, isComplimentaryMember, 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';
import {useContext} from 'react';
const PaidAccountActions = () => {
const {member, site, onAction} = useContext(AppContext);
const onEditBilling = () => {
const subscription = getMemberSubscription({member});
onAction('editBilling', {subscriptionId: subscription.id});
};
const openUpdatePlan = () => {
const {is_stripe_configured: isStripeConfigured} = site;
if (isStripeConfigured) {
onAction('switchPage', {
page: 'accountPlan',
lastPage: 'accountHome'
});
}
};
const PlanLabel = ({price, isComplimentary, subscription}) => {
const {
offer,
start_date: startDate
} = subscription || {};
let label = '';
if (price) {
const {amount = 0, currency, interval} = price;
label = `${Intl.NumberFormat('en', {currency, style: 'currency'}).format(amount / 100)}/${interval}`;
}
let offerLabelStr = getOfferLabel({price, offer, subscriptionStartDate: startDate});
const compExpiry = getCompExpiry({member});
if (isComplimentary) {
if (compExpiry) {
label = `Complimentary - Expires ${compExpiry}`;
} else {
label = label ? `Complimentary (${label})` : `Complimentary`;
}
}
let oldPriceClassName = '';
if (offerLabelStr) {
oldPriceClassName = 'gh-portal-account-old-price';
}
const OfferLabel = () => {
if (offerLabelStr) {
return (
<p className="gh-portal-account-discountcontainer">
<OfferTagIcon className="gh-portal-account-tagicon" />
<span>{offerLabelStr}</span>
</p>
);
}
return null;
};
const hasFreeTrial = subscriptionHasFreeTrial({sub: subscription});
if (hasFreeTrial) {
oldPriceClassName = 'gh-portal-account-old-price';
}
if (hasFreeTrial) {
return (
<>
<p className={oldPriceClassName}>
{label}
</p>
<FreeTrialLabel subscription={subscription} />
</>
);
}
return (
<>
<p className={oldPriceClassName}>
{label}
</p>
<OfferLabel />
</>
);
};
const PlanUpdateButton = ({isComplimentary}) => {
const hideUpgrade = allowCompMemberUpgrade({member}) ? false : isComplimentary;
if (hideUpgrade || hasOnlyFreePlan({site})) {
return null;
}
return (
<button className='gh-portal-btn gh-portal-btn-list' onClick={e => openUpdatePlan(e)}>Change</button>
);
};
const CardLabel = ({defaultCardLast4}) => {
if (defaultCardLast4) {
const label = `**** **** **** ${defaultCardLast4}`;
return (
<p>
{label}
</p>
);
}
return null;
};
const BillingSection = ({defaultCardLast4, isComplimentary}) => {
const {action} = useContext(AppContext);
const label = action === 'editBilling:running' ? (
<LoaderIcon className='gh-portal-billing-button-loader' />
) : 'Update';
if (isComplimentary) {
return null;
}
return (
<section>
<div className='gh-portal-list-detail'>
<h3>Billing info</h3>
<CardLabel defaultCardLast4={defaultCardLast4} />
</div>
<button className='gh-portal-btn gh-portal-btn-list' onClick={e => onEditBilling(e)}>{label}</button>
</section>
);
};
const subscription = getMemberSubscription({member});
const isComplimentary = isComplimentaryMember({member});
if (subscription || isComplimentary) {
const {
price,
default_payment_card_last4: defaultCardLast4
} = subscription || {};
let planLabel = 'Plan';
// Show name of tiers if there are multiple tiers
if (hasMultipleProductsFeature({site}) && getMemberTierName({member})) {
planLabel = getMemberTierName({member});
}
// const hasFreeTrial = subscriptionHasFreeTrial({sub: subscription});
// if (hasFreeTrial) {
// planLabel += ' (Free Trial)';
// }
return (
<>
<section>
<div className='gh-portal-list-detail'>
<h3>{planLabel}</h3>
<PlanLabel price={price} isComplimentary={isComplimentary} subscription={subscription} />
</div>
<PlanUpdateButton isComplimentary={isComplimentary} />
</section>
<BillingSection isComplimentary={isComplimentary} defaultCardLast4={defaultCardLast4} />
</>
);
}
return null;
};
function FreeTrialLabel({subscription, priceLabel}) {
if (subscriptionHasFreeTrial({sub: subscription})) {
const trialEnd = getDateString(subscription.trial_end_at);
return (
<p className="gh-portal-account-discountcontainer">
<div>
<span>Free Trial Ends {trialEnd}</span>
{/* <span>{getSubFreeTrialDaysLeft({sub: subscription})} days left</span> */}
</div>
</p>
);
}
return null;
}
function getOfferLabel({offer, price, subscriptionStartDate}) {
let offerLabel = '';
if (offer) {
const discountDuration = offer.duration;
let durationLabel = '';
if (discountDuration === 'forever') {
durationLabel = `Forever`;
} else if (discountDuration === 'repeating') {
const durationInMonths = offer.duration_in_months || 0;
let offerStartDate = new Date(subscriptionStartDate);
let offerEndDate = new Date(offerStartDate.setMonth(offerStartDate.getMonth() + durationInMonths));
durationLabel = `Ends ${getDateString(offerEndDate)}`;
}
offerLabel = `${getUpdatedOfferPrice({offer, price, useFormatted: true})}/${price.interval}${durationLabel ? `${durationLabel}` : ``}`;
}
return offerLabel;
}
export default PaidAccountActions;

View File

@ -0,0 +1,32 @@
import AppContext from 'AppContext';
import ActionButton from 'components/common/ActionButton';
import {hasOnlyFreePlan} from 'utils/helpers';
import {useContext} from 'react';
const SubscribeButton = () => {
const {site, action, brandColor, onAction} = useContext(AppContext);
const {is_stripe_configured: isStripeConfigured} = site;
if (!isStripeConfigured || hasOnlyFreePlan({site})) {
return null;
}
const isRunning = ['checkoutPlan:running'].includes(action);
const openPlanPage = () => {
onAction('switchPage', {
page: 'accountPlan',
lastPage: 'accountHome'
});
};
return (
<ActionButton
isRunning={isRunning}
label="View plans"
onClick={() => openPlanPage()}
brandColor={brandColor}
style={{width: '100%'}}
/>
);
};
export default SubscribeButton;

View File

@ -0,0 +1,16 @@
import AppContext from 'AppContext';
import MemberAvatar from 'components/common/MemberGravatar';
import React, {useContext} from 'react';
const UserHeader = () => {
const {member, brandColor} = useContext(AppContext);
const avatar = member.avatar_image;
return (
<header className='gh-portal-account-header'>
<MemberAvatar gravatar={avatar} style={{userIcon: {color: brandColor, width: '56px', height: '56px', padding: '2px'}}} />
<h2 className="gh-portal-main-title">Your account</h2>
</header>
);
};
export default UserHeader;

View File

@ -1,6 +1,6 @@
import SigninPage from './components/pages/SigninPage'; import SigninPage from './components/pages/SigninPage';
import SignupPage from './components/pages/SignupPage'; import SignupPage from './components/pages/SignupPage';
import AccountHomePage from './components/pages/AccountHomePage'; import AccountHomePage from './components/pages/AccountHomePage/AccountHomePage';
import MagicLinkPage from './components/pages/MagicLinkPage'; import MagicLinkPage from './components/pages/MagicLinkPage';
import LoadingPage from './components/pages/LoadingPage'; import LoadingPage from './components/pages/LoadingPage';
import AccountPlanPage from './components/pages/AccountPlanPage'; import AccountPlanPage from './components/pages/AccountPlanPage';