mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Added newsletter selection for signup flows
refs https://github.com/TryGhost/Team/issues/1490 Adds newsletter subscription preference page as an intermediary step before signing up as paid or free member.
This commit is contained in:
parent
ffafed05d0
commit
031aa5f2bc
10 changed files with 261 additions and 59 deletions
ghost/portal/src
|
@ -7,6 +7,7 @@ const AppContext = React.createContext({
|
|||
action: '',
|
||||
lastPage: '',
|
||||
brandColor: '',
|
||||
pageData: {},
|
||||
onAction: (action, data) => {
|
||||
return {action, data};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {createPopupNotification, getMemberEmail, getMemberName, removePortalLinkFromUrl} from './utils/helpers';
|
||||
|
||||
function switchPage({data}) {
|
||||
function switchPage({data, state}) {
|
||||
return {
|
||||
page: data.page,
|
||||
popupNotification: null,
|
||||
lastPage: data.lastPage || null
|
||||
lastPage: data.lastPage || null,
|
||||
pageData: data.pageData || state.pageData
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -94,11 +95,13 @@ async function signin({data, api, state}) {
|
|||
|
||||
async function signup({data, state, api}) {
|
||||
try {
|
||||
const {plan, email, name, offerId} = data;
|
||||
const {plan, email, name, newsletters, offerId} = data;
|
||||
if (plan.toLowerCase() === 'free') {
|
||||
/*eslint-disable no-console */
|
||||
console.log('Sending data', data);
|
||||
await api.member.sendMagicLink(data);
|
||||
} else {
|
||||
await api.member.checkoutPlan({plan, email, name, offerId});
|
||||
await api.member.checkoutPlan({plan, email, name, newsletters, offerId});
|
||||
}
|
||||
return {
|
||||
page: 'magiclink',
|
||||
|
|
|
@ -78,7 +78,7 @@ const Styles = ({brandColor, retry, disabled, style = {}, isPrimary}) => {
|
|||
};
|
||||
};
|
||||
|
||||
function ActionButton({label, type = undefined, onClick, disabled = false, retry = false, brandColor, isRunning, isPrimary = true, isDestructive = false, classes, style = {}, tabindex = undefined}) {
|
||||
function ActionButton({label, type = undefined, onClick, disabled = false, retry = false, brandColor, isRunning, isPrimary = true, isDestructive = false, classes = '', style = {}, tabindex = undefined}) {
|
||||
let Style = Styles({disabled, retry, brandColor, style, isPrimary});
|
||||
|
||||
let className = 'gh-portal-btn';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import AppContext from '../../AppContext';
|
||||
import CloseButton from '../common/CloseButton';
|
||||
import BackButton from '../common/BackButton';
|
||||
import {useContext} from 'react';
|
||||
import {useContext, useState} from 'react';
|
||||
import Switch from '../common/Switch';
|
||||
import {getSiteNewsletters} from '../../utils/helpers';
|
||||
import ActionButton from '../common/ActionButton';
|
||||
|
@ -20,10 +20,8 @@ function AccountHeader() {
|
|||
);
|
||||
}
|
||||
|
||||
function NewsletterPrefSection({newsletter}) {
|
||||
const {onAction, member} = useContext(AppContext);
|
||||
let newsletters = [...(member.newsletters || [])];
|
||||
const isChecked = newsletters.some((d) => {
|
||||
function NewsletterPrefSection({newsletter, subscribedNewsletters, setSubscribedNewsletters}) {
|
||||
const isChecked = subscribedNewsletters.some((d) => {
|
||||
return d.id === newsletter?.id;
|
||||
});
|
||||
return (
|
||||
|
@ -34,45 +32,53 @@ function NewsletterPrefSection({newsletter}) {
|
|||
</div>
|
||||
<div>
|
||||
<Switch id={newsletter.id} onToggle={(e, checked) => {
|
||||
onAction('showPopupNotification', {
|
||||
action: 'updated:success',
|
||||
message: `${newsletter.name} newsletter preference updated.`
|
||||
});
|
||||
let updatedNewsletters = [];
|
||||
if (!checked) {
|
||||
newsletters = newsletters.filter((d) => {
|
||||
updatedNewsletters = subscribedNewsletters.filter((d) => {
|
||||
return d.id !== newsletter.id;
|
||||
});
|
||||
} else {
|
||||
newsletters = newsletters.filter((d) => {
|
||||
updatedNewsletters = subscribedNewsletters.filter((d) => {
|
||||
return d.id !== newsletter.id;
|
||||
}).concat(newsletter);
|
||||
}
|
||||
onAction('updateNewsletterPreference', {newsletters});
|
||||
setSubscribedNewsletters(updatedNewsletters);
|
||||
}} checked={isChecked} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function NewsletterPrefs() {
|
||||
function NewsletterPrefs({subscribedNewsletters, setSubscribedNewsletters}) {
|
||||
const {site} = useContext(AppContext);
|
||||
const newsletters = getSiteNewsletters({site});
|
||||
return newsletters.map((newsletter) => {
|
||||
return (
|
||||
<NewsletterPrefSection key={newsletter?.id} newsletter={newsletter} />
|
||||
<NewsletterPrefSection
|
||||
key={newsletter?.id}
|
||||
newsletter={newsletter}
|
||||
subscribedNewsletters={subscribedNewsletters}
|
||||
setSubscribedNewsletters={setSubscribedNewsletters}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default function AccountEmailPage() {
|
||||
const {brandColor} = useContext(AppContext);
|
||||
const {brandColor, member, onAction} = useContext(AppContext);
|
||||
const defaultSubscribedNewsletters = [...(member.newsletters || [])];
|
||||
const [subscribedNewsletters, setSubscribedNewsletters] = useState(defaultSubscribedNewsletters);
|
||||
|
||||
return (
|
||||
<div className='gh-portal-content with-footer'>
|
||||
<CloseButton />
|
||||
<AccountHeader />
|
||||
<div className='gh-portal-section'>
|
||||
<div className='gh-portal-list'>
|
||||
<NewsletterPrefs />
|
||||
<NewsletterPrefs
|
||||
subscribedNewsletters={subscribedNewsletters}
|
||||
setSubscribedNewsletters={setSubscribedNewsletters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<footer className='gh-portal-action-footer'>
|
||||
|
@ -80,7 +86,18 @@ export default function AccountEmailPage() {
|
|||
<div style={{marginBottom: '12px'}}>
|
||||
<ActionButton
|
||||
isRunning={false}
|
||||
onClick={(e) => {}}
|
||||
onClick={(e) => {
|
||||
let newsletters = subscribedNewsletters.map((d) => {
|
||||
return {
|
||||
id: d.id
|
||||
};
|
||||
});
|
||||
onAction('showPopupNotification', {
|
||||
action: 'updated:success',
|
||||
message: `Newsletter preference updated.`
|
||||
});
|
||||
onAction('updateNewsletterPreference', {newsletters});
|
||||
}}
|
||||
disabled={false}
|
||||
brandColor={brandColor}
|
||||
label='Update'
|
||||
|
|
|
@ -111,7 +111,7 @@ export const AccountHomePageStyles = `
|
|||
z-index: 999;
|
||||
}
|
||||
|
||||
@media (max-width: 390px) {
|
||||
@media (max-width: 390px) {
|
||||
.gh-portal-account-footer {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ const PaidAccountActions = () => {
|
|||
|
||||
const AccountActions = () => {
|
||||
const {member, onAction} = useContext(AppContext);
|
||||
const {name, email, subscribed} = member;
|
||||
const {name, email} = member;
|
||||
|
||||
const openEditProfile = () => {
|
||||
onAction('switchPage', {
|
||||
|
@ -324,12 +324,6 @@ const AccountActions = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const onToggleSubscription = (e, sub) => {
|
||||
e.preventDefault();
|
||||
onAction('updateNewsletter', {subscribed: !sub});
|
||||
};
|
||||
|
||||
let label = subscribed ? 'Subscribed' : 'Unsubscribed';
|
||||
return (
|
||||
<div>
|
||||
<div className='gh-portal-list'>
|
||||
|
@ -343,23 +337,45 @@ const AccountActions = () => {
|
|||
|
||||
<PaidAccountActions />
|
||||
<EmailPreferencesAction />
|
||||
<section>
|
||||
<div className='gh-portal-list-detail'>
|
||||
<h3>Email newsletter</h3>
|
||||
<p>{label}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch onToggle={(e) => {
|
||||
onToggleSubscription(e, subscribed);
|
||||
}} checked={subscribed} />
|
||||
</div>
|
||||
</section>
|
||||
<EmailNewsletterAction />
|
||||
</div>
|
||||
{/* <ProductList openUpdatePlan={openUpdatePlan}></ProductList> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function EmailNewsletterAction() {
|
||||
const {member, site, onAction} = useContext(AppContext);
|
||||
const {subscribed} = member;
|
||||
|
||||
if (hasMultipleNewsletters({site})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let label = subscribed ? 'Subscribed' : 'Unsubscribed';
|
||||
const onToggleSubscription = (e, sub) => {
|
||||
e.preventDefault();
|
||||
onAction('updateNewsletter', {subscribed: !sub});
|
||||
};
|
||||
|
||||
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})) {
|
||||
|
|
129
ghost/portal/src/components/pages/NewsletterSelectionPage.js
Normal file
129
ghost/portal/src/components/pages/NewsletterSelectionPage.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
import AppContext from '../../AppContext';
|
||||
import CloseButton from '../common/CloseButton';
|
||||
import BackButton from '../common/BackButton';
|
||||
import {useContext, useState} from 'react';
|
||||
import Switch from '../common/Switch';
|
||||
import {getSiteNewsletters} from '../../utils/helpers';
|
||||
import ActionButton from '../common/ActionButton';
|
||||
|
||||
const React = require('react');
|
||||
|
||||
function AccountHeader() {
|
||||
const {brandColor, lastPage, onAction} = useContext(AppContext);
|
||||
return (
|
||||
<header className='gh-portal-detail-header'>
|
||||
<BackButton brandColor={brandColor} hidden={!lastPage} onClick={(e) => {
|
||||
onAction('back');
|
||||
}} />
|
||||
<h3 className='gh-portal-main-title'>Email preferences</h3>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function NewsletterPrefSection({newsletter, subscribedNewsletters, setSubscribedNewsletters}) {
|
||||
const isChecked = subscribedNewsletters.some((d) => {
|
||||
return d.id === newsletter?.id;
|
||||
});
|
||||
return (
|
||||
<section>
|
||||
<div className='gh-portal-list-detail'>
|
||||
<h3>{newsletter.name}</h3>
|
||||
<p>{newsletter.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch id={newsletter.id} onToggle={(e, checked) => {
|
||||
let updatedNewsletters = [];
|
||||
if (!checked) {
|
||||
updatedNewsletters = subscribedNewsletters.filter((d) => {
|
||||
return d.id !== newsletter.id;
|
||||
});
|
||||
} else {
|
||||
updatedNewsletters = subscribedNewsletters.filter((d) => {
|
||||
return d.id !== newsletter.id;
|
||||
}).concat(newsletter);
|
||||
}
|
||||
setSubscribedNewsletters(updatedNewsletters);
|
||||
}} checked={isChecked} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function NewsletterPrefs({subscribedNewsletters, setSubscribedNewsletters}) {
|
||||
const {site} = useContext(AppContext);
|
||||
const newsletters = getSiteNewsletters({site});
|
||||
return newsletters.map((newsletter) => {
|
||||
return (
|
||||
<NewsletterPrefSection
|
||||
key={newsletter?.id}
|
||||
newsletter={newsletter}
|
||||
subscribedNewsletters={subscribedNewsletters}
|
||||
setSubscribedNewsletters={setSubscribedNewsletters}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default function NewsletterSelectionPage() {
|
||||
const {brandColor, site, onAction, pageData, action} = useContext(AppContext);
|
||||
const siteNewsletters = getSiteNewsletters({site});
|
||||
const defaultNewsletters = siteNewsletters.filter((d) => {
|
||||
return d.subscribe_on_signup;
|
||||
});
|
||||
|
||||
let isRunning = false;
|
||||
if (action === 'signup:running') {
|
||||
isRunning = true;
|
||||
}
|
||||
let label = 'Continue';
|
||||
let retry = false;
|
||||
if (action === 'signup:failed') {
|
||||
label = 'Retry';
|
||||
retry = true;
|
||||
}
|
||||
|
||||
const disabled = (action === 'signup:running') ? true : false;
|
||||
|
||||
const [subscribedNewsletters, setSubscribedNewsletters] = useState(defaultNewsletters);
|
||||
return (
|
||||
<div className='gh-portal-content with-footer'>
|
||||
<CloseButton />
|
||||
<AccountHeader />
|
||||
<div className='gh-portal-section'>
|
||||
<div className='gh-portal-list'>
|
||||
<NewsletterPrefs
|
||||
subscribedNewsletters={subscribedNewsletters}
|
||||
setSubscribedNewsletters={setSubscribedNewsletters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<footer className='gh-portal-action-footer'>
|
||||
<div style={{width: '100%'}}>
|
||||
<div style={{marginBottom: '12px'}}>
|
||||
<ActionButton
|
||||
isRunning={isRunning}
|
||||
retry={retry}
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
/* eslint-disable no-console */
|
||||
console.log(pageData);
|
||||
console.log(subscribedNewsletters);
|
||||
/* eslint-enable no-console */
|
||||
let newsletters = subscribedNewsletters.map((d) => {
|
||||
return {
|
||||
id: d.id
|
||||
};
|
||||
});
|
||||
const {name, email, plan} = pageData;
|
||||
onAction('signup', {name, email, plan, newsletters});
|
||||
}}
|
||||
brandColor={brandColor}
|
||||
label={label}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,7 @@ import SiteTitleBackButton from '../common/SiteTitleBackButton';
|
|||
import ProductsSection from '../common/ProductsSection';
|
||||
import InputForm from '../common/InputForm';
|
||||
import {ValidateInputForm} from '../../utils/form';
|
||||
import {getSiteProducts, getSitePrices, hasOnlyFreePlan, isInviteOnlySite, freeHasBenefitsOrDescription, hasOnlyFreeProduct, getFreeProductBenefits, getFreeTierDescription, hasFreeProductPrice} from '../../utils/helpers';
|
||||
import {getSiteProducts, getSitePrices, hasOnlyFreePlan, isInviteOnlySite, freeHasBenefitsOrDescription, hasOnlyFreeProduct, getFreeProductBenefits, getFreeTierDescription, hasFreeProductPrice, hasMultipleNewsletters} from '../../utils/helpers';
|
||||
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
||||
|
||||
const React = require('react');
|
||||
|
@ -260,20 +260,32 @@ class SignupPage extends React.Component {
|
|||
}
|
||||
|
||||
handleSignup(e) {
|
||||
const {site, onAction} = this.context;
|
||||
e.preventDefault();
|
||||
this.setState((state) => {
|
||||
return {
|
||||
errors: ValidateInputForm({fields: this.getInputFields({state})})
|
||||
};
|
||||
}, () => {
|
||||
const {onAction} = this.context;
|
||||
const {name, email, plan, errors} = this.state;
|
||||
const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0);
|
||||
if (!hasFormErrors) {
|
||||
onAction('signup', {name, email, plan});
|
||||
this.setState({
|
||||
errors: {}
|
||||
});
|
||||
if (hasMultipleNewsletters({site})) {
|
||||
onAction('switchPage', {
|
||||
page: 'signupNewsletter',
|
||||
lastPage: 'signup',
|
||||
pageData: {name, email, plan}
|
||||
});
|
||||
this.setState({
|
||||
errors: {},
|
||||
showNewsletterSelection: true
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errors: {}
|
||||
});
|
||||
onAction('signup', {name, email, plan});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -285,14 +297,27 @@ class SignupPage extends React.Component {
|
|||
errors: ValidateInputForm({fields: this.getInputFields({state})})
|
||||
};
|
||||
}, () => {
|
||||
const {onAction} = this.context;
|
||||
const {onAction, site} = this.context;
|
||||
const {name, email, errors} = this.state;
|
||||
const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0);
|
||||
if (!hasFormErrors) {
|
||||
onAction('signup', {name, email, plan});
|
||||
this.setState({
|
||||
errors: {}
|
||||
});
|
||||
if (hasMultipleNewsletters({site})) {
|
||||
onAction('switchPage', {
|
||||
page: 'signupNewsletter',
|
||||
lastPage: 'signup',
|
||||
pageData: {name, email, plan}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
errors: {},
|
||||
showNewsletterSelection: true
|
||||
});
|
||||
} else {
|
||||
onAction('signup', {name, email, plan});
|
||||
this.setState({
|
||||
errors: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -484,14 +509,14 @@ class SignupPage extends React.Component {
|
|||
<div>
|
||||
{this.renderProducts()}
|
||||
|
||||
{(hasOnlyFree ?
|
||||
{(hasOnlyFree ?
|
||||
<div className={'gh-portal-btn-container' + (sticky ? ' sticky m24' : '')}>
|
||||
<div className='gh-portal-logged-out-form-container'>
|
||||
{this.renderSubmitButton()}
|
||||
{this.renderLoginMessage()}
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
:
|
||||
this.renderLoginMessage())}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -555,9 +580,14 @@ class SignupPage extends React.Component {
|
|||
return {sectionClass, footerClass};
|
||||
}
|
||||
|
||||
onNewsletterSelectionBack() {
|
||||
this.setState({
|
||||
showNewsletterSelection: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let {sectionClass} = this.getClassNames();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='gh-portal-back-sitetitle'>
|
||||
|
|
|
@ -7,6 +7,7 @@ import AccountPlanPage from './components/pages/AccountPlanPage';
|
|||
import AccountProfilePage from './components/pages/AccountProfilePage';
|
||||
import AccountEmailPage from './components/pages/AccountEmailPage';
|
||||
import OfferPage from './components/pages/OfferPage';
|
||||
import NewsletterSelectionPage from './components/pages/NewsletterSelectionPage';
|
||||
|
||||
/** List of all available pages in Portal, mapped to their UI component
|
||||
* Any new page added to portal needs to be mapped here
|
||||
|
@ -18,6 +19,7 @@ const Pages = {
|
|||
accountPlan: AccountPlanPage,
|
||||
accountProfile: AccountProfilePage,
|
||||
accountEmail: AccountEmailPage,
|
||||
signupNewsletter: NewsletterSelectionPage,
|
||||
magiclink: MagicLinkPage,
|
||||
loading: LoadingPage,
|
||||
offer: OfferPage
|
||||
|
|
|
@ -146,11 +146,12 @@ function setupGhostApi({siteUrl = window.location.origin}) {
|
|||
});
|
||||
},
|
||||
|
||||
sendMagicLink({email, emailType, labels, name, oldEmail}) {
|
||||
sendMagicLink({email, emailType, labels, name, oldEmail, newsletters}) {
|
||||
const url = endpointFor({type: 'members', resource: 'send-magic-link'});
|
||||
const body = {
|
||||
name,
|
||||
email,
|
||||
newsletters,
|
||||
oldEmail,
|
||||
emailType,
|
||||
labels,
|
||||
|
@ -215,7 +216,7 @@ function setupGhostApi({siteUrl = window.location.origin}) {
|
|||
});
|
||||
},
|
||||
|
||||
async checkoutPlan({plan, cancelUrl, successUrl, email: customerEmail, name, offerId, metadata = {}} = {}) {
|
||||
async checkoutPlan({plan, cancelUrl, successUrl, email: customerEmail, name, offerId, newsletters, metadata = {}} = {}) {
|
||||
const siteUrlObj = new URL(siteUrl);
|
||||
const identity = await api.member.identity();
|
||||
const url = endpointFor({type: 'members', resource: 'create-stripe-checkout-session'});
|
||||
|
@ -227,6 +228,7 @@ function setupGhostApi({siteUrl = window.location.origin}) {
|
|||
}
|
||||
const metadataObj = {
|
||||
name,
|
||||
newsletters: JSON.stringify(newsletters),
|
||||
requestSrc: 'portal',
|
||||
fp_tid: (window.FPROM || window.$FPROM)?.data?.tid,
|
||||
...metadata
|
||||
|
|
|
@ -109,12 +109,14 @@ export const site = getSiteData({
|
|||
{
|
||||
id: 'weekly',
|
||||
name: 'Weekly Rundown',
|
||||
description: 'Best links from previous week every Monday'
|
||||
description: 'Best of last week',
|
||||
subscribe_on_signup: true
|
||||
},
|
||||
{
|
||||
id: 'daily',
|
||||
name: 'Daily Brief',
|
||||
description: 'One email every day'
|
||||
description: 'One email every day',
|
||||
subscribe_on_signup: false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue