0
Fork 0
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:
Rishabh 2022-04-12 19:54:25 +05:30
parent ffafed05d0
commit 031aa5f2bc
10 changed files with 261 additions and 59 deletions

View file

@ -7,6 +7,7 @@ const AppContext = React.createContext({
action: '',
lastPage: '',
brandColor: '',
pageData: {},
onAction: (action, data) => {
return {action, data};
}

View file

@ -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',

View file

@ -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';

View file

@ -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'

View file

@ -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})) {

View 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>
);
}

View file

@ -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'>

View file

@ -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

View file

@ -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

View file

@ -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
}
]
});