mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-01 23:37:43 +03:00
Refactored app setup to more modular structure
no issue - Adds new mode check util for dev, preview and test, cleaned up manual checks - Extracts Pages setup to its own file allowing common use - Extracts action handling to its own file for neater structure - Breaks up main App.js into proper methods for data fetching across usecases - Adds better handling for App context through state - Fixed trigger button test
This commit is contained in:
parent
47e7b86637
commit
0e30b63f62
@ -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 (
|
||||
<PopupModal />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderTriggerButton() {
|
||||
const {portal_button: portalButton} = this.state.site;
|
||||
if (portalButton === undefined || portalButton) {
|
||||
return (
|
||||
<TriggerButton
|
||||
isPopupOpen={this.state.showPopup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<AppContext.Provider value={{
|
||||
site,
|
||||
member,
|
||||
action,
|
||||
brandColor: this.getAccentColor(),
|
||||
page,
|
||||
lastPage,
|
||||
onAction: (_action, data) => this.onAction(_action, data)
|
||||
}}>
|
||||
{this.renderPopup()}
|
||||
{this.renderTriggerButton()}
|
||||
<AppContext.Provider value={this.getContextFromState()}>
|
||||
<PopupModal />
|
||||
<TriggerButton />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
133
ghost/portal/src/actions.js
Normal file
133
ghost/portal/src/actions.js
Normal file
@ -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 {};
|
||||
}
|
@ -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 (
|
||||
<div className='gh-portal-popup-container' ref={this.container}>
|
||||
{this.renderPopupClose()}
|
||||
{this.renderCurrentPage(page)}
|
||||
{this.renderActivePage()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -193,16 +167,6 @@ export default class PopupModal extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPopupContent() {
|
||||
const page = this.getCurrentPage();
|
||||
return (
|
||||
<div className='gh-portal-popup-container'>
|
||||
{this.renderPopupClose()}
|
||||
{this.renderCurrentPage(page)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<Frame style={frameStyle} title="membersjs-trigger">
|
||||
<TriggerButtonContent isPopupOpen={this.props.isPopupOpen} updateWidth={width => this.onWidthChange(width)} />
|
||||
<TriggerButtonContent isPopupOpen={showPopup} updateWidth={width => this.onWidthChange(width)} />
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
<TriggerButton />
|
||||
);
|
||||
|
||||
const triggerFrame = utils.getByTitle('membersjs-trigger');
|
||||
return {
|
||||
triggerFrame,
|
||||
...utils
|
||||
};
|
||||
};
|
||||
|
||||
describe('Trigger Button', () => {
|
||||
test('renders', () => {
|
||||
const {getByTitle} = render(
|
||||
<TriggerButton />
|
||||
);
|
||||
const triggerFrame = getByTitle('membersjs-trigger');
|
||||
const {triggerFrame} = setup();
|
||||
|
||||
expect(triggerFrame).toBeInTheDocument();
|
||||
});
|
||||
|
@ -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 {
|
||||
</div>
|
||||
<button className='gh-portal-btn gh-portal-btn-accountdetail' onClick={e => this.openUpdatePlan(e)}>Change</button>
|
||||
</section>
|
||||
|
||||
|
||||
<section className='gh-portal-accountdetail-section'>
|
||||
<div className='flex flex-column flex-grow-1'>
|
||||
<h3 className='gh-portal-setting-heading paid-home'>Billing Info</h3>
|
||||
@ -298,7 +297,7 @@ class PaidAccountHomePage extends React.Component {
|
||||
</div>
|
||||
<button className='gh-portal-btn gh-portal-btn-accountdetail' onClick={e => this.onEditBilling(e)}>Update</button>
|
||||
</section>
|
||||
|
||||
|
||||
<section className='gh-portal-accountdetail-section'>
|
||||
<div className='flex flex-column flex-grow-1'>
|
||||
<h3 className='gh-portal-setting-heading paid-home'>Newsletter</h3>
|
||||
@ -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'
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
|
36
ghost/portal/src/pages.js
Normal file
36
ghost/portal/src/pages.js
Normal file
@ -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;
|
@ -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};
|
||||
|
25
ghost/portal/src/utils/check-mode.js
Normal file
25
ghost/portal/src/utils/check-mode.js
Normal file
@ -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());
|
||||
});
|
||||
};
|
@ -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;
|
Loading…
Reference in New Issue
Block a user