mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
Split AccountHomePage file on separate files
closes TryGhost/Team#2256 - Moved components to separate file for better navigation.
This commit is contained in:
parent
991e628022
commit
69c2af1ffe
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import {render, fireEvent} from '../../utils/test-utils';
|
||||
import {render, fireEvent} from 'utils/test-utils';
|
||||
import AccountHomePage from './AccountHomePage';
|
||||
|
||||
const setup = (overrides) => {
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -1,6 +1,6 @@
|
||||
import SigninPage from './components/pages/SigninPage';
|
||||
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 LoadingPage from './components/pages/LoadingPage';
|
||||
import AccountPlanPage from './components/pages/AccountPlanPage';
|
||||
|
Loading…
Reference in New Issue
Block a user