diff --git a/ghost/portal/src/App.js b/ghost/portal/src/App.js index c07a02cc3e..df23bd7c1c 100644 --- a/ghost/portal/src/App.js +++ b/ghost/portal/src/App.js @@ -2,9 +2,11 @@ import TriggerButton from './components/TriggerButton'; import PopupModal from './components/PopupModal'; import setupGhostApi from './utils/api'; import AppContext from './AppContext'; +import {hasMode} from './utils/check-mode'; +import {getActivePage, isAccountPage} from './pages'; import * as Fixtures from './utils/fixtures'; +import ActionHandler from './actions'; import './App.css'; - const React = require('react'); export default class App extends React.Component { @@ -41,6 +43,27 @@ export default class App extends React.Component { }); } + /** Setup custom trigger buttons handling on page */ + setupCustomTriggerButton() { + // Handler for custom buttons + this.clickHandler = (event) => { + const target = event.currentTarget; + const pagePath = (target && target.dataset.portal); + const pageFromPath = this.getPageFromPath(pagePath); + + event.preventDefault(); + this.onAction('openPopup', {page: pageFromPath}); + }; + const customTriggerSelector = '[data-portal]'; + const popupCloseClass = 'gh-members-popup-close'; + this.customTriggerButtons = document.querySelectorAll(customTriggerSelector) || []; + this.customTriggerButtons.forEach((customTriggerButton) => { + customTriggerButton.classList.add(popupCloseClass); + customTriggerButton.addEventListener('click', this.clickHandler); + }); + } + + /** Handle portal class set on custom trigger buttons */ handleCustomTriggerClassUpdate() { const popupOpenClass = 'gh-members-popup-open'; const popupCloseClass = 'gh-members-popup-close'; @@ -52,103 +75,202 @@ export default class App extends React.Component { }); } - getStripeUrlParam() { - const url = new URL(window.location); - return url.searchParams.get('stripe'); + /** Initialize portal setup on load, fetch data and setup state*/ + async initSetup() { + try { + // Fetch data from API, links, preview, dev sources + const {site, member, page, showPopup} = await this.fetchData(); + this.setState({ + site, + member, + page, + showPopup, + action: 'init:success', + initStatus: 'success' + }); + + // Listen to preview mode changes + this.hashHandler = () => { + this.updateStateForPreview(); + }; + window.addEventListener('hashchange', this.hashHandler, false); + } catch (e) { + /* eslint-disable no-console */ + console.error(`[Portal] Failed to initialize:`, e); + /* eslint-enable no-console */ + this.setState({ + action: 'init:failed', + initStatus: 'failed' + }); + } } - getDefaultPage({member = this.state.member, stripeParam} = {}) { - // Loads default page and popup state for local UI testing - if (process.env.NODE_ENV === 'development') { - return { - page: 'accountHome', - showPopup: true - }; - } + /** Fetch data from all available sources */ + async fetchData() { + const {site: apiSiteData, member} = await this.fetchApiData(); + const {site: devSiteData, ...restDevData} = this.fetchDevData(); + const {site: linkSiteData, ...restLinkData} = this.fetchLinkData(); + const {site: previewSiteData, ...restPreviewData} = this.fetchPreviewData(); + + const stripeParam = this.getStripeUrlParam(); + let page = ''; + + /** Set page for magic link popup on stripe success*/ if (!member && stripeParam === 'success') { - return {page: 'magiclink', showPopup: true}; + page = 'magiclink'; } - if (member) { + + return { + member, + page, + site: { + ...apiSiteData, + ...linkSiteData, + ...previewSiteData, + ...devSiteData + }, + ...restDevData, + ...restLinkData, + ...restPreviewData + }; + } + + /** Fetch state for Dev mode */ + fetchDevData() { + // Setup custom dev mode data from fixtures + if (hasMode(['dev'])) { return { + showPopup: true, + site: Fixtures.site, + member: Fixtures.member.free, page: 'accountHome' }; } - return { - page: 'signup' - }; + return {}; } - updateStateForPreview() { - const {site: previewSite, ...restPreview} = this.getPreviewState(); - this.setState({ - site: { - ...this.state.site, - ...(previewSite || {}) - }, - member: this.getPreviewMember(this.state.member), - ...restPreview - }); - } - - getStateFromQueryString(qs = '') { - const previewState = { + /** Fetch state from Query String */ + fetchQueryStrData(qs = '') { + const qsParams = new URLSearchParams(qs); + const data = { site: {} }; const allowedPlans = []; - const qsParams = new URLSearchParams(qs); - // Handle the key/value pairs + + // Handle the query params key/value pairs for (let pair of qsParams.entries()) { const key = pair[0]; const value = decodeURIComponent(pair[1]); if (key === 'button') { - previewState.site.portal_button = JSON.parse(value); + data.site.portal_button = JSON.parse(value); } else if (key === 'name') { - previewState.site.portal_name = JSON.parse(value); + data.site.portal_name = JSON.parse(value); } else if (key === 'isFree' && JSON.parse(value)) { allowedPlans.push('free'); } else if (key === 'isMonthly' && JSON.parse(value)) { allowedPlans.push('monthly'); } else if (key === 'isYearly' && JSON.parse(value)) { allowedPlans.push('yearly'); - } else if (key === 'page') { - previewState.page = value; - } else if (key === 'accentColor') { - previewState.site.accent_color = value; - } else if (key === 'buttonIcon') { - previewState.site.portal_button_icon = value; + } else if (key === 'plans') { + data.site.portal_plans = value ? value.split(',') : []; + } else if (key === 'page' && value) { + data.page = value; + } else if (key === 'accentColor' && value) { + data.site.accent_color = value; + } else if (key === 'buttonIcon' && value) { + data.site.portal_button_icon = value; } else if (key === 'signupButtonText') { - previewState.site.portal_button_signup_text = value; - } else if (key === 'buttonStyle') { - previewState.site.portal_button_style = value; + data.site.portal_button_signup_text = value || ''; + } else if (key === 'buttonStyle' && value) { + data.site.portal_button_style = value; } } - previewState.site.portal_plans = allowedPlans; - previewState.showPopup = true; - return previewState; + data.site.portal_plans = allowedPlans; + return data; } - getPreviewState() { - const [path, qs] = window.location.hash.substr(1).split('?'); - const previewState = { - site: {} - }; - if (path.startsWith('/portal')) { - previewState.showPopup = true; - if (qs) { - return this.getStateFromQueryString(qs); - } - - if (path.startsWith('/portal/')) { - const pagePath = path.replace('/portal/', ''); - const pageFromPath = this.getPageFromPath(pagePath); - if (pageFromPath) { - previewState.page = pageFromPath; - } - } + /** Fetch state from Portal Links */ + fetchLinkData() { + const [path] = window.location.hash.substr(1).split('?'); + const linkRegex = /^\/portal(?:\/(\w+(?:\/\w+)?))?$/; + if (path && linkRegex.test(path)) { + const [,pagePath] = path.match(linkRegex); + const page = this.getPageFromPath(pagePath); + return { + showPopup: true, + ...(page ? {page} : {}) + }; } - return previewState; + return {}; } + /** Fetch state from Preview mode */ + fetchPreviewData() { + const [, qs] = window.location.hash.substr(1).split('?'); + if (hasMode(['preview'])) { + const data = this.fetchQueryStrData(qs); + data.showPopup = true; + return data; + } + return {}; + } + + /** Fetch site and member session data with Ghost Apis */ + async fetchApiData() { + try { + const {siteUrl} = this.props; + this.GhostApi = setupGhostApi({siteUrl}); + const {site, member} = await this.GhostApi.init(); + return {site, member}; + } catch (e) { + if (hasMode(['dev', 'test'])) { + return {}; + } + throw e; + } + } + + /** Handle actions from across App and update state */ + async onAction(action, data) { + this.setState({ + action: `${action}:running` + }); + try { + const updatedState = await ActionHandler({action, data, state: this.state, api: this.GhostApi}); + this.setState(updatedState); + + /** Reset action state after short timeout */ + setTimeout(() => { + this.setState({ + action: '' + }); + }, 5000); + } catch (e) { + this.setState({ + action: `${action}:failed` + }); + } + } + + /**Handle state update for preview url changes */ + updateStateForPreview() { + const {site: previewSite, ...restPreviewData} = this.fetchPreviewData(); + this.setState({ + site: { + ...this.state.site, + ...(previewSite || {}) + }, + ...restPreviewData + }); + } + + /**Fetch Stripe param from site url after redirect from Stripe page*/ + getStripeUrlParam() { + const url = new URL(window.location); + return url.searchParams.get('stripe'); + } + + /**Get Portal page from Link/Data-attribute path*/ getPageFromPath(path) { if (path === 'signup') { return 'signup'; @@ -163,236 +285,59 @@ export default class App extends React.Component { } } - getPreviewMember(member) { - const [path, qs] = window.location.hash.substr(1).split('?'); - - if (path === '/portal' && qs) { - const {site: previewSite, ...restPreview} = this.getPreviewState(); - if (restPreview.page.includes('account')) { - return member || Fixtures.member.free; - } - return null; - } else if (process.env.NODE_ENV === 'development') { - return member || Fixtures.member.paid; - } - return member; - } - - async initSetup() { - const {site, member} = await this.fetchData() || {}; - if (!site) { - this.setState({ - action: 'init:failed', - initStatus: 'failed' - }); - } else { - const stripeParam = this.getStripeUrlParam(); - const {page, showPopup = false} = this.getDefaultPage({member, stripeParam}); - const {site: previewSite, ...restPreview} = this.getPreviewState(); - const initState = { - site: { - ...site, - ...(previewSite || {}) - }, - member: this.getPreviewMember(member), - page, - showPopup, - action: 'init:success', - initStatus: 'success', - ...restPreview - }; - this.setState(initState); - this.hashHandler = () => { - this.updateStateForPreview(); - }; - window.addEventListener('hashchange', this.hashHandler, false); - } - } - - // Fetch site and member session data with Ghost Apis - async fetchData() { - const {siteUrl} = this.props; - try { - this.GhostApi = setupGhostApi({siteUrl}); - const {site, member} = await this.GhostApi.init(); - return {site, member}; - } catch (e) { - /* eslint-disable no-console */ - console.error(`[Members.js] Failed to initialize`); - /* eslint-enable no-console */ - return null; - } - } - - setupCustomTriggerButton() { - // Handler for custom buttons - this.clickHandler = (event) => { - const target = event.currentTarget; - const {page: defaultPage} = this.getDefaultPage(); - const pagePath = (target && target.dataset.portal); - const pageFromPath = this.getPageFromPath(pagePath) || defaultPage; - - event.preventDefault(); - this.onAction('openPopup', {page: pageFromPath}); - }; - const customTriggerSelector = '[data-portal]'; - const popupCloseClass = 'gh-members-popup-close'; - this.customTriggerButtons = document.querySelectorAll(customTriggerSelector) || []; - this.customTriggerButtons.forEach((customTriggerButton) => { - customTriggerButton.classList.add(popupCloseClass); - customTriggerButton.addEventListener('click', this.clickHandler); - }); - } - - getActionData(action) { - const [type, status, reason] = action.split(':'); - return {type, status, reason}; - } - + /**Get Accent color from site data, fallback to default*/ getAccentColor() { const {accent_color: accentColor = '#3db0ef'} = this.state.site || {}; return accentColor || '#3db0ef'; } - async onAction(action, data) { - this.setState({ - action: `${action}:running` - }); - try { - if (action === 'switchPage') { - this.setState({ - page: data.page, - lastPage: data.lastPage || null - }); - } else if (action === 'togglePopup') { - this.setState({ - showPopup: !this.state.showPopup - }); - } else if (action === 'openPopup') { - this.setState({ - showPopup: true, - page: data.page - }); - } else if (action === 'back') { - if (this.state.lastPage) { - this.setState({ - page: this.state.lastPage - }); - } - } else if (action === 'closePopup') { - const {page: defaultPage} = this.getDefaultPage(); - this.setState({ - showPopup: false, - page: this.state.page === 'magiclink' ? defaultPage : this.state.page - }); - } else if (action === 'signout') { - await this.GhostApi.member.signout(); - this.setState({ - action: 'signout:success' - }); - } else if (action === 'signin') { - await this.GhostApi.member.sendMagicLink(data); - this.setState({ - action: 'signin:success', - page: 'magiclink' - }); - } else if (action === 'signup') { - const {plan, email, name} = data; - if (plan.toLowerCase() === 'free') { - await this.GhostApi.member.sendMagicLink(data); - } else { - await this.GhostApi.member.checkoutPlan({plan, email, name}); - } - this.setState({ - action: 'signup:success', - page: 'magiclink' - }); - } else if (action === 'updateEmail') { - await this.GhostApi.member.sendMagicLink(data); - this.setState({ - action: 'updateEmail:success' - }); - } else if (action === 'checkoutPlan') { - const {plan} = data; - await this.GhostApi.member.checkoutPlan({ - plan - }); - } else if (action === 'updateSubscription') { - const {plan, subscriptionId, cancelAtPeriodEnd} = data; - await this.GhostApi.member.updateSubscription({ - planName: plan, subscriptionId, cancelAtPeriodEnd - }); - const member = await this.GhostApi.member.sessionData(); - this.setState({ - action: 'updateSubscription:success', - page: 'accountHome', - member: member - }); - } else if (action === 'editBilling') { - await this.GhostApi.member.editBilling(); - } else if (action === 'updateMember') { - const {name, subscribed} = data; - const member = await this.GhostApi.member.update({name, subscribed}); - if (!member) { - this.setState({ - action: 'updateMember:failed' - }); - } else { - this.setState({ - action: 'updateMember:success', - member: member - }); - } + /**Get final page set in App context from state data*/ + getContextPage({page, member}) { + /**Set default page based on logged-in status */ + if (!page) { + page = member ? 'accountHome' : 'signup'; + } + + return getActivePage({page}); + } + + /**Get final member set in App context from state data*/ + getContextMember({page, member}) { + if (hasMode(['dev', 'preview'])) { + /** Use dummy member(free or paid) for account pages in dev/preview mode*/ + if (isAccountPage({page})) { + return member || Fixtures.member.free; } - setTimeout(() => { - this.setState({ - action: '' - }); - }, 5000); - } catch (e) { - this.setState({ - action: `${action}:failed` - }); + + /** Ignore member for non-account pages in dev/preview mode*/ + return null; } + return member; } - renderPopup() { - if (this.state.showPopup) { - return ( - - ); - } - return null; - } - - renderTriggerButton() { - const {portal_button: portalButton} = this.state.site; - if (portalButton === undefined || portalButton) { - return ( - - ); - } - - return null; + /**Get final App level context from data/state*/ + getContextFromState() { + const {site, member, action, page, lastPage, showPopup} = this.state; + const contextPage = this.getContextPage({page, member}); + const contextMember = this.getContextMember({page: contextPage, member}); + return { + site, + action, + brandColor: this.getAccentColor(), + page: contextPage, + member: contextMember, + lastPage, + showPopup, + onAction: (_action, data) => this.onAction(_action, data) + }; } render() { if (this.state.initStatus === 'success') { - const {site, member, action, page, lastPage} = this.state; return ( - this.onAction(_action, data) - }}> - {this.renderPopup()} - {this.renderTriggerButton()} + + + ); } diff --git a/ghost/portal/src/actions.js b/ghost/portal/src/actions.js new file mode 100644 index 0000000000..d3e93bd3b5 --- /dev/null +++ b/ghost/portal/src/actions.js @@ -0,0 +1,133 @@ +function switchPage({data}) { + return { + page: data.page, + lastPage: data.lastPage || null + }; +} + +function togglePopup({state}) { + return { + showPopup: !state.showPopup + }; +} + +function openPopup({data}) { + return { + showPopup: true, + page: data.page + }; +} + +function back({state}) { + if (state.lastPage) { + return { + page: state.lastPage + }; + } +} + +function closePopup({state}) { + return { + showPopup: false, + page: state.page === 'magiclink' ? '' : state.page + }; +} + +async function signout({api}) { + await api.member.signout(); + return { + action: 'signout:success' + }; +} + +async function signin({data, api}) { + await api.member.sendMagicLink(data); + return { + action: 'signin:success', + page: 'magiclink' + }; +} + +async function signup({data, api}) { + const {plan, email, name} = data; + if (plan.toLowerCase() === 'free') { + await api.member.sendMagicLink(data); + } else { + await api.member.checkoutPlan({plan, email, name}); + } + return { + action: 'signup:success', + page: 'magiclink' + }; +} + +async function updateEmail({data, api}) { + await api.member.sendMagicLink(data); + return { + action: 'updateEmail:success' + }; +} + +async function checkoutPlan({data, api}) { + const {plan} = data; + await api.member.checkoutPlan({ + plan + }); +} + +async function updateSubscription({data, api}) { + const {plan, subscriptionId, cancelAtPeriodEnd} = data; + await api.member.updateSubscription({ + planName: plan, subscriptionId, cancelAtPeriodEnd + }); + const member = await api.member.sessionData(); + return { + action: 'updateSubscription:success', + page: 'accountHome', + member: member + }; +} + +async function editBilling({data, updateState, state, api}) { + await api.member.editBilling(); +} + +async function updateMember({data, updateState, state, api}) { + const {name, subscribed} = data; + const member = await api.member.update({name, subscribed}); + if (!member) { + return { + action: 'updateMember:failed' + }; + } else { + return { + action: 'updateMember:success', + member: member + }; + } +} + +const Actions = { + togglePopup, + openPopup, + closePopup, + switchPage, + back, + signout, + signin, + signup, + updateEmail, + updateSubscription, + updateMember, + editBilling, + checkoutPlan +}; + +/** Handle actions in the App, returns updated state */ +export default async function ActionHandler({action, data, updateState, state, api}) { + const handler = Actions[action]; + if (handler) { + return await handler({data, updateState, state, api}) || {}; + } + return {}; +} \ No newline at end of file diff --git a/ghost/portal/src/components/PopupModal.js b/ghost/portal/src/components/PopupModal.js index 6e419d4629..6fbb5455b4 100644 --- a/ghost/portal/src/components/PopupModal.js +++ b/ghost/portal/src/components/PopupModal.js @@ -1,15 +1,8 @@ import Frame from './Frame'; -import SigninPage from './pages/SigninPage'; -import SignupPage from './pages/SignupPage'; -import AccountHomePage from './pages/AccountHomePage'; -import MagicLinkPage from './pages/MagicLinkPage'; -import LoadingPage from './pages/LoadingPage'; import {ReactComponent as CloseIcon} from '../images/icons/close.svg'; import AppContext from '../AppContext'; import FrameStyle from './Frame.styles'; -import AccountPlanPage from './pages/AccountPlanPage'; -import AccountProfilePage from './pages/AccountProfilePage'; -import LinkPage from './pages/LinkPage'; +import Pages, {getActivePage} from '../pages'; const React = require('react'); @@ -89,17 +82,6 @@ const StylesWrapper = ({member}) => { }; }; -const Pages = { - signin: SigninPage, - signup: SignupPage, - accountHome: AccountHomePage, - accountPlan: AccountPlanPage, - accountProfile: AccountProfilePage, - magiclink: MagicLinkPage, - loading: LoadingPage, - links: LinkPage -}; - class PopupContent extends React.Component { static contextType = AppContext; @@ -127,16 +109,9 @@ class PopupContent extends React.Component { } } - getCurrentPage() { - const {page} = this.context; - if (Object.keys(Pages).includes(page)) { - return page; - } - return 'signup'; - } - - renderCurrentPage() { + renderActivePage() { const {page} = this.context; + getActivePage({page}); const PageComponent = Pages[page]; return ( @@ -153,11 +128,10 @@ class PopupContent extends React.Component { } render() { - const page = this.getCurrentPage(); return (
{this.renderPopupClose()} - {this.renderCurrentPage(page)} + {this.renderActivePage()}
); } @@ -193,16 +167,6 @@ export default class PopupModal extends React.Component { ); } - renderPopupContent() { - const page = this.getCurrentPage(); - return ( -
- {this.renderPopupClose()} - {this.renderCurrentPage(page)} -
- ); - } - handlePopupClose(e) { e.preventDefault(); if (e.target === e.currentTarget) { @@ -221,18 +185,10 @@ export default class PopupModal extends React.Component { ); } - getCurrentPage() { - const {page} = this.context; - if (Object.keys(Pages).includes(page)) { - return page; - } - return 'signup'; - } - renderFrameContainer() { const {member} = this.context; const Styles = StylesWrapper({member}); - const page = this.getCurrentPage(); + const page = getActivePage({page: this.context.page}); const frameStyle = { ...Styles.frame.common, ...Styles.frame[page] @@ -252,6 +208,10 @@ export default class PopupModal extends React.Component { } render() { - return this.renderFrameContainer(); + const {showPopup} = this.context; + if (showPopup) { + return this.renderFrameContainer(); + } + return null; } } diff --git a/ghost/portal/src/components/TriggerButton.js b/ghost/portal/src/components/TriggerButton.js index ba9ef046eb..35541057c1 100644 --- a/ghost/portal/src/components/TriggerButton.js +++ b/ghost/portal/src/components/TriggerButton.js @@ -210,6 +210,13 @@ export default class TriggerButton extends React.Component { } render() { + const {portal_button: portalButton} = this.context.site; + const {showPopup} = this.context; + + if (!portalButton) { + return null; + } + const hasText = this.hasText(); const Style = Styles({brandColor: this.context.brandColor, hasText}); @@ -223,7 +230,7 @@ export default class TriggerButton extends React.Component { return ( - this.onWidthChange(width)} /> + this.onWidthChange(width)} /> ); } diff --git a/ghost/portal/src/components/TriggerButton.test.js b/ghost/portal/src/components/TriggerButton.test.js index f5e4e45ae7..7709d80f93 100644 --- a/ghost/portal/src/components/TriggerButton.test.js +++ b/ghost/portal/src/components/TriggerButton.test.js @@ -1,13 +1,22 @@ import React from 'react'; -import {render} from '@testing-library/react'; +import {render} from 'test-utils'; import TriggerButton from './TriggerButton'; +const setup = (overrides) => { + const {mockOnActionFn, ...utils} = render( + + ); + + const triggerFrame = utils.getByTitle('membersjs-trigger'); + return { + triggerFrame, + ...utils + }; +}; + describe('Trigger Button', () => { test('renders', () => { - const {getByTitle} = render( - - ); - const triggerFrame = getByTitle('membersjs-trigger'); + const {triggerFrame} = setup(); expect(triggerFrame).toBeInTheDocument(); }); diff --git a/ghost/portal/src/components/pages/AccountHomePage.js b/ghost/portal/src/components/pages/AccountHomePage.js index 4612d3114a..0bca748b29 100644 --- a/ghost/portal/src/components/pages/AccountHomePage.js +++ b/ghost/portal/src/components/pages/AccountHomePage.js @@ -2,7 +2,6 @@ import AppContext from '../../AppContext'; import MemberAvatar from '../common/MemberGravatar'; import ActionButton from '../common/ActionButton'; import Switch from '../common/Switch'; -import isPreviewMode from '../../utils/is-preview-mode'; const React = require('react'); @@ -288,7 +287,7 @@ class PaidAccountHomePage extends React.Component { - +

Billing Info

@@ -298,7 +297,7 @@ class PaidAccountHomePage extends React.Component {
- +

Newsletter

@@ -335,7 +334,7 @@ export default class AccountHomePage extends React.Component { componentDidMount() { const {member} = this.context; - if (!member && !isPreviewMode()) { + if (!member) { this.context.onAction('switchPage', { page: 'signup' }); diff --git a/ghost/portal/src/components/pages/AccountPlanPage.js b/ghost/portal/src/components/pages/AccountPlanPage.js index be08f8664e..6b0f1c52be 100644 --- a/ghost/portal/src/components/pages/AccountPlanPage.js +++ b/ghost/portal/src/components/pages/AccountPlanPage.js @@ -1,7 +1,6 @@ import AppContext from '../../AppContext'; import ActionButton from '../common/ActionButton'; import PlansSection from '../common/PlansSection'; -import isPreviewMode from '../../utils/is-preview-mode'; import {ReactComponent as ArrowLeftIcon} from '../../images/icons/arrow-left.svg'; const React = require('react'); @@ -20,7 +19,7 @@ export default class AccountPlanPage extends React.Component { componentDidMount() { const {member} = this.context; - if (!member && !isPreviewMode()) { + if (!member) { this.context.onAction('switchPage', { page: 'signup' }); diff --git a/ghost/portal/src/components/pages/AccountProfilePage.js b/ghost/portal/src/components/pages/AccountProfilePage.js index d36f97019d..0a39782fd9 100644 --- a/ghost/portal/src/components/pages/AccountProfilePage.js +++ b/ghost/portal/src/components/pages/AccountProfilePage.js @@ -3,7 +3,6 @@ import MemberAvatar from '../common/MemberGravatar'; import ActionButton from '../common/ActionButton'; import InputField from '../common/InputField'; import Switch from '../common/Switch'; -import isPreviewMode from '../../utils/is-preview-mode'; import {ReactComponent as ArrowLeftIcon} from '../../images/icons/arrow-left.svg'; const React = require('react'); @@ -22,7 +21,7 @@ export default class AccountProfilePage extends React.Component { componentDidMount() { const {member} = this.context; - if (!member && !isPreviewMode()) { + if (!member) { this.context.onAction('switchPage', { page: 'signup' }); diff --git a/ghost/portal/src/components/pages/SigninPage.js b/ghost/portal/src/components/pages/SigninPage.js index 6ccc0ed5ef..9ef31f0f94 100644 --- a/ghost/portal/src/components/pages/SigninPage.js +++ b/ghost/portal/src/components/pages/SigninPage.js @@ -1,7 +1,6 @@ import ActionButton from '../common/ActionButton'; import InputField from '../common/InputField'; import AppContext from '../../AppContext'; -import isPreviewMode from '../../utils/is-preview-mode'; const React = require('react'); @@ -17,7 +16,7 @@ export default class SigninPage extends React.Component { componentDidMount() { const {member} = this.context; - if (member && !isPreviewMode()) { + if (member) { this.context.onAction('switchPage', { page: 'accountHome' }); diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index 7e7e29da38..d25b158641 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -2,7 +2,6 @@ import ActionButton from '../common/ActionButton'; import InputField from '../common/InputField'; import AppContext from '../../AppContext'; import PlansSection from '../common/PlansSection'; -import isPreviewMode from '../../utils/is-preview-mode'; const React = require('react'); @@ -20,7 +19,7 @@ class SignupPage extends React.Component { componentDidMount() { const {member} = this.context; - if (member && !isPreviewMode()) { + if (member) { this.context.onAction('switchPage', { page: 'accountHome' }); diff --git a/ghost/portal/src/pages.js b/ghost/portal/src/pages.js new file mode 100644 index 0000000000..57e9f90958 --- /dev/null +++ b/ghost/portal/src/pages.js @@ -0,0 +1,36 @@ +import SigninPage from './components/pages/SigninPage'; +import SignupPage from './components/pages/SignupPage'; +import AccountHomePage from './components/pages/AccountHomePage'; +import MagicLinkPage from './components/pages/MagicLinkPage'; +import LoadingPage from './components/pages/LoadingPage'; +import AccountPlanPage from './components/pages/AccountPlanPage'; +import AccountProfilePage from './components/pages/AccountProfilePage'; +import LinkPage from './components/pages/LinkPage'; + +/** List of all available pages in Portal, mapped to their UI component + * Any new page added to portal needs to be mapped here +*/ +const Pages = { + signin: SigninPage, + signup: SignupPage, + accountHome: AccountHomePage, + accountPlan: AccountPlanPage, + accountProfile: AccountProfilePage, + magiclink: MagicLinkPage, + loading: LoadingPage, + links: LinkPage +}; + +/** Return page if valid, fallback to signup */ +export const getActivePage = function ({page}) { + if (Object.keys(Pages).includes(page)) { + return page; + } + return 'signup'; +}; + +export const isAccountPage = function ({page}) { + return page.includes('account'); +}; + +export default Pages; \ No newline at end of file diff --git a/ghost/portal/src/utils/api.js b/ghost/portal/src/utils/api.js index de01eba5a2..65d2c008f6 100644 --- a/ghost/portal/src/utils/api.js +++ b/ghost/portal/src/utils/api.js @@ -1,4 +1,5 @@ import * as Fixtures from './fixtures'; +import {hasMode} from './check-mode'; function setupGhostApi({siteUrl = window.location.origin}) { const apiPath = 'members/api'; @@ -215,11 +216,10 @@ function setupGhostApi({siteUrl = window.location.origin}) { }; api.init = async () => { - // Load member from fixtures for local development - if (process.env.NODE_ENV === 'development') { - return {site: Fixtures.site, member: null}; - } - + // // Load site data from fixtures for dev/test modes + // if (hasMode(['dev', 'test'])) { + // return {site: Fixtures.site, member: null}; + // } const {site} = await api.site.read(); const member = await api.member.sessionData(); return {site, member}; diff --git a/ghost/portal/src/utils/check-mode.js b/ghost/portal/src/utils/check-mode.js new file mode 100644 index 0000000000..5a350adeb0 --- /dev/null +++ b/ghost/portal/src/utils/check-mode.js @@ -0,0 +1,25 @@ +export const isPreviewMode = function () { + const [path, qs] = window.location.hash.substr(1).split('?'); + return (path === '/portal/preview') || (path === '/portal' && qs); +}; + +export const isDevMode = function () { + return (process.env.NODE_ENV === 'development'); +}; + +export const isTestMode = function () { + return (process.env.NODE_ENV === 'test'); +}; + +const modeFns = { + preview: isPreviewMode, + dev: isDevMode, + test: isTestMode +}; + +export const hasMode = (modes = []) => { + return modes.some((mode) => { + const modeFn = modeFns[mode]; + return !!(modeFn && modeFn()); + }); +}; \ No newline at end of file diff --git a/ghost/portal/src/utils/is-preview-mode.js b/ghost/portal/src/utils/is-preview-mode.js deleted file mode 100644 index 74e1cf5d96..0000000000 --- a/ghost/portal/src/utils/is-preview-mode.js +++ /dev/null @@ -1,6 +0,0 @@ -function isPreviewMode() { - const [path, qs] = window.location.hash.substr(1).split('?'); - return ((process.env.NODE_ENV === 'development') || (path === '/portal' && qs)); -} - -export default isPreviewMode; \ No newline at end of file