diff --git a/ghost/portal/src/AppContext.js b/ghost/portal/src/AppContext.js index 90eb346460..f402b7d5d7 100644 --- a/ghost/portal/src/AppContext.js +++ b/ghost/portal/src/AppContext.js @@ -7,6 +7,7 @@ const AppContext = React.createContext({ action: '', lastPage: '', brandColor: '', + pageData: {}, onAction: (action, data) => { return {action, data}; } diff --git a/ghost/portal/src/actions.js b/ghost/portal/src/actions.js index 3e5d07590c..880718d49c 100644 --- a/ghost/portal/src/actions.js +++ b/ghost/portal/src/actions.js @@ -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', diff --git a/ghost/portal/src/components/common/ActionButton.js b/ghost/portal/src/components/common/ActionButton.js index f7585ba07c..745c7d7591 100644 --- a/ghost/portal/src/components/common/ActionButton.js +++ b/ghost/portal/src/components/common/ActionButton.js @@ -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'; diff --git a/ghost/portal/src/components/pages/AccountEmailPage.js b/ghost/portal/src/components/pages/AccountEmailPage.js index b48fecc6fb..651ae424a8 100644 --- a/ghost/portal/src/components/pages/AccountEmailPage.js +++ b/ghost/portal/src/components/pages/AccountEmailPage.js @@ -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' diff --git a/ghost/portal/src/components/pages/AccountHomePage.js b/ghost/portal/src/components/pages/AccountHomePage.js index 56ff97dbae..1ae595afcb 100644 --- a/ghost/portal/src/components/pages/AccountHomePage.js +++ b/ghost/portal/src/components/pages/AccountHomePage.js @@ -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})) { diff --git a/ghost/portal/src/components/pages/NewsletterSelectionPage.js b/ghost/portal/src/components/pages/NewsletterSelectionPage.js new file mode 100644 index 0000000000..260469ba66 --- /dev/null +++ b/ghost/portal/src/components/pages/NewsletterSelectionPage.js @@ -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> + ); +} diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index 70bf90937a..0d3c282bf9 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -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'> diff --git a/ghost/portal/src/pages.js b/ghost/portal/src/pages.js index 7b21fae585..0595ed6d8a 100644 --- a/ghost/portal/src/pages.js +++ b/ghost/portal/src/pages.js @@ -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 diff --git a/ghost/portal/src/utils/api.js b/ghost/portal/src/utils/api.js index 5b3d5d99b6..aeb147e963 100644 --- a/ghost/portal/src/utils/api.js +++ b/ghost/portal/src/utils/api.js @@ -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 diff --git a/ghost/portal/src/utils/fixtures.js b/ghost/portal/src/utils/fixtures.js index 18390bf564..e99e3052a0 100644 --- a/ghost/portal/src/utils/fixtures.js +++ b/ghost/portal/src/utils/fixtures.js @@ -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 } ] });