mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Updated global notification handling flow
refs https://github.com/TryGhost/members.js/issues/92 - Adds new notification parser util - Handles slideout animation for notification - Clear search params on notification hide/close
This commit is contained in:
parent
81823f531b
commit
8af2340d42
4 changed files with 178 additions and 47 deletions
|
@ -12,7 +12,6 @@ const React = require('react');
|
|||
|
||||
const DEV_MODE_DATA = {
|
||||
showPopup: true,
|
||||
showNotification: true,
|
||||
site: Fixtures.site,
|
||||
member: Fixtures.member.paid,
|
||||
page: 'accountHome'
|
||||
|
@ -29,7 +28,6 @@ export default class App extends React.Component {
|
|||
member: null,
|
||||
page: 'loading',
|
||||
showPopup: false,
|
||||
showNotification: false,
|
||||
action: 'init:running',
|
||||
initStatus: 'running',
|
||||
lastPage: null
|
||||
|
@ -90,15 +88,12 @@ export default class App extends React.Component {
|
|||
async initSetup() {
|
||||
try {
|
||||
// Fetch data from API, links, preview, dev sources
|
||||
const {site, member, page, showPopup, showNotification, notificationType, notificationStatus} = await this.fetchData();
|
||||
const {site, member, page, showPopup} = await this.fetchData();
|
||||
this.setState({
|
||||
site,
|
||||
member,
|
||||
page,
|
||||
showPopup,
|
||||
showNotification,
|
||||
notificationType,
|
||||
notificationStatus,
|
||||
action: 'init:success',
|
||||
initStatus: 'success'
|
||||
});
|
||||
|
@ -125,7 +120,6 @@ export default class App extends React.Component {
|
|||
const {site: devSiteData, ...restDevData} = this.fetchDevData();
|
||||
const {site: linkSiteData, ...restLinkData} = this.fetchLinkData();
|
||||
const {site: previewSiteData, ...restPreviewData} = this.fetchPreviewData();
|
||||
const {site: notificationSiteData, ...restNotificationData} = this.fetchNotificationData();
|
||||
|
||||
const stripeParam = this.getStripeUrlParam();
|
||||
let page = '';
|
||||
|
@ -142,30 +136,14 @@ export default class App extends React.Component {
|
|||
...apiSiteData,
|
||||
...linkSiteData,
|
||||
...previewSiteData,
|
||||
...notificationSiteData,
|
||||
...devSiteData
|
||||
},
|
||||
...restNotificationData,
|
||||
...restDevData,
|
||||
...restLinkData,
|
||||
...restPreviewData
|
||||
};
|
||||
}
|
||||
|
||||
/** Fetch state for any notifications */
|
||||
fetchNotificationData() {
|
||||
const qs = window.location.search;
|
||||
const qsParams = new URLSearchParams(qs);
|
||||
const notificationType = qsParams.get('action');
|
||||
const notificationStatus = qsParams.get('success') ? JSON.parse(qsParams.get('success')) : null;
|
||||
const showNotification = !!notificationType;
|
||||
return {
|
||||
showNotification: showNotification,
|
||||
notificationType,
|
||||
notificationStatus
|
||||
};
|
||||
}
|
||||
|
||||
/** Fetch state for Dev mode */
|
||||
fetchDevData() {
|
||||
// Setup custom dev mode data from fixtures
|
||||
|
@ -350,7 +328,7 @@ export default class App extends React.Component {
|
|||
|
||||
/**Get final App level context from data/state*/
|
||||
getContextFromState() {
|
||||
const {site, member, action, page, lastPage, showPopup, showNotification, notificationType, notificationStatus} = this.state;
|
||||
const {site, member, action, page, lastPage, showPopup} = this.state;
|
||||
const contextPage = this.getContextPage({page, member});
|
||||
const contextMember = this.getContextMember({page: contextPage, member});
|
||||
return {
|
||||
|
@ -361,9 +339,6 @@ export default class App extends React.Component {
|
|||
member: contextMember,
|
||||
lastPage,
|
||||
showPopup,
|
||||
showNotification,
|
||||
notificationType,
|
||||
notificationStatus,
|
||||
onAction: (_action, data) => this.onAction(_action, data)
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,13 +2,11 @@ import Frame from './Frame';
|
|||
import AppContext from '../AppContext';
|
||||
import NotificationStyle from './Notification.styles';
|
||||
import {ReactComponent as CloseIcon} from '../images/icons/close.svg';
|
||||
import NotificationParser, {clearURLParams} from '../utils/notifications';
|
||||
|
||||
const React = require('react');
|
||||
|
||||
const Styles = ({brandColor, hasText}) => {
|
||||
const frame = {
|
||||
// ...(!hasText ? {width: '60px'} : {})
|
||||
};
|
||||
const Styles = () => {
|
||||
return {
|
||||
frame: {
|
||||
zIndex: '4000000',
|
||||
|
@ -20,25 +18,60 @@ const Styles = ({brandColor, hasText}) => {
|
|||
height: '80px',
|
||||
animation: '250ms ease 0s 1 normal none running animation-bhegco',
|
||||
transition: 'opacity 0.3s ease 0s',
|
||||
overflow: 'hidden',
|
||||
...frame
|
||||
overflow: 'hidden'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const NotificationText = ({type, status}) => {
|
||||
if (type === 'signin' && status === true) {
|
||||
if (type === 'signin' && status === 'success') {
|
||||
return (
|
||||
<p>
|
||||
Hey, you are successfully signed in!
|
||||
</p>
|
||||
);
|
||||
} else if (type === 'signin' && status === false) {
|
||||
} else if (type === 'signin' && status === 'error') {
|
||||
return (
|
||||
<p>
|
||||
Hey, looks like you used an invalid link to signin!
|
||||
</p>
|
||||
);
|
||||
} else if (type === 'signup' && status === 'success') {
|
||||
return (
|
||||
<p>
|
||||
Hey, you are successfully signed up!
|
||||
</p>
|
||||
);
|
||||
} else if (type === 'signup' && status === 'error') {
|
||||
return (
|
||||
<p>
|
||||
Hey, looks like you used an invalid link to signup!
|
||||
</p>
|
||||
);
|
||||
} else if (type === 'stripe:checkout' && status === 'success') {
|
||||
return (
|
||||
<p>
|
||||
You have successfully subscribed to the plan!
|
||||
</p>
|
||||
);
|
||||
} else if (type === 'stripe:checkout' && status === 'warning') {
|
||||
return (
|
||||
<p>
|
||||
Strpie checkout flow was cancelled.
|
||||
</p>
|
||||
);
|
||||
} else if (type === 'stripe:billing-update' && status === 'success') {
|
||||
return (
|
||||
<p>
|
||||
Successfully updated your billing details on Stripe
|
||||
</p>
|
||||
);
|
||||
} else if (type === 'stripe:billing-update' && status === 'warning') {
|
||||
return (
|
||||
<p>
|
||||
Cancelled your billing update flow on Stripe
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
|
@ -50,15 +83,42 @@ const NotificationText = ({type, status}) => {
|
|||
class NotificationContent extends React.Component {
|
||||
static contextType = AppContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
className: ''
|
||||
};
|
||||
}
|
||||
|
||||
onNotificationClose() {
|
||||
this.context.onAction('closeNotification');
|
||||
this.props.onHideNotification();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {autoHide, duration = 2000} = this.props;
|
||||
if (autoHide) {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
className: 'slideout'
|
||||
});
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
onAnimationEnd(e) {
|
||||
if (e.animationName === 'notification-slideout') {
|
||||
this.props.onHideNotification(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {notificationType: type, notificationStatus: status} = this.context;
|
||||
const {type, status} = this.props;
|
||||
const {className = ''} = this.state;
|
||||
const statusClass = status ? ` ${status}` : ' neutral';
|
||||
const slideClass = className ? ` ${className}` : '';
|
||||
return (
|
||||
<div className='gh-portal-notification-wrapper'>
|
||||
<div className={'gh-portal-notification' + (status ? ' success' : ' error')}>
|
||||
<div className={`gh-portal-notification${statusClass}${slideClass}`} onAnimationEnd={e => this.onAnimationEnd(e)}>
|
||||
<NotificationText type={type} status={status} />
|
||||
<CloseIcon className='gh-portal-notification-closeicon' alt='Close' onClick={e => this.onNotificationClose(e)} />
|
||||
</div>
|
||||
|
@ -70,6 +130,35 @@ class NotificationContent extends React.Component {
|
|||
export default class Notification extends React.Component {
|
||||
static contextType = AppContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const {type, status, autoHide, duration} = NotificationParser() || {};
|
||||
this.state = {
|
||||
active: true,
|
||||
type,
|
||||
status,
|
||||
autoHide,
|
||||
duration,
|
||||
className: ''
|
||||
};
|
||||
}
|
||||
|
||||
onHideNotification() {
|
||||
const qs = window.location.search || '';
|
||||
const qsParams = new URLSearchParams(qs);
|
||||
const type = this.state.type;
|
||||
const deleteParams = [];
|
||||
if (['signin', 'signout'].includes(type)) {
|
||||
deleteParams.push('action', 'success');
|
||||
} else if (['stripe:checkout', 'stripe:billing-update'].includes(type)) {
|
||||
deleteParams.push('stripe');
|
||||
}
|
||||
clearURLParams(qsParams, deleteParams);
|
||||
this.setState({
|
||||
active: false
|
||||
});
|
||||
}
|
||||
|
||||
renderFrameStyles() {
|
||||
const styles = `
|
||||
:root {
|
||||
|
@ -86,14 +175,17 @@ export default class Notification extends React.Component {
|
|||
const frameStyle = {
|
||||
...Style.frame
|
||||
};
|
||||
const {showNotification} = this.context;
|
||||
if (!showNotification) {
|
||||
if (!this.state.active) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Frame style={frameStyle} title="membersjs-notification" head={this.renderFrameStyles()}>
|
||||
<NotificationContent updateWidth={width => this.onWidthChange(width)} />
|
||||
</Frame>
|
||||
);
|
||||
const {type, status, autoHide, duration} = this.state;
|
||||
if (type && status) {
|
||||
return (
|
||||
<Frame style={frameStyle} title="membersjs-notification" head={this.renderFrameStyles()}>
|
||||
<NotificationContent {...{type, status, autoHide, duration}} onHideNotification={e => this.onHideNotification(e)} />
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ const NotificationStyles = `
|
|||
align-items: stretch;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
.gh-portal-notification {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
@ -26,6 +26,10 @@ const NotificationStyles = `
|
|||
animation: notification-slideout 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.gh-portal-notification.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gh-portal-notification p {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
|
@ -100,7 +104,7 @@ const NotificationStyles = `
|
|||
}
|
||||
`;
|
||||
|
||||
const NotificationStyle =
|
||||
const NotificationStyle =
|
||||
GlobalStyles +
|
||||
NotificationStyles;
|
||||
|
||||
|
|
60
ghost/portal/src/utils/notifications.js
Normal file
60
ghost/portal/src/utils/notifications.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
export const handleAuthActions = ({action, status}) => {
|
||||
if (status && ['true', 'false'].includes(status)) {
|
||||
const successStatus = JSON.parse(status);
|
||||
return {
|
||||
type: action,
|
||||
status: successStatus ? 'success' : 'error',
|
||||
duration: 2000,
|
||||
autoHide: successStatus ? true : false
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const handleStripeActions = ({status}) => {
|
||||
if (['cancel', 'success'].includes(status)) {
|
||||
const statusVal = status === 'success' ? 'success' : 'warning';
|
||||
return {
|
||||
type: 'stripe:checkout',
|
||||
status: statusVal,
|
||||
duration: 2000,
|
||||
autoHide: true
|
||||
};
|
||||
} else if (['billing-update-success', 'billing-update-cancel'].includes(status)) {
|
||||
const statusVal = status === 'billing-update-success' ? 'success' : 'warning';
|
||||
return {
|
||||
type: 'stripe:billing-update',
|
||||
status: statusVal,
|
||||
duration: 2000,
|
||||
autoHide: true
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const clearURLParams = (qsParams, paramsToClear = []) => {
|
||||
paramsToClear.forEach((param) => {
|
||||
qsParams.delete(param);
|
||||
});
|
||||
const newParams = qsParams.toString() ? `?${qsParams}` : '';
|
||||
window.history.replaceState({}, '', `${window.location.pathname}${newParams}`);
|
||||
};
|
||||
|
||||
/** Handle actions in the App, returns updated state */
|
||||
export default function NotificationParser() {
|
||||
const qs = window.location.search;
|
||||
if (!qs) {
|
||||
return null;
|
||||
}
|
||||
const qsParams = new URLSearchParams(qs);
|
||||
const action = qsParams.get('action');
|
||||
const successStatus = qsParams.get('success');
|
||||
const stripeStatus = qsParams.get('stripe');
|
||||
let notificationData = null;
|
||||
if (action && successStatus) {
|
||||
return handleAuthActions({action, status: successStatus});
|
||||
} else if (stripeStatus) {
|
||||
return handleStripeActions({status: stripeStatus});
|
||||
}
|
||||
|
||||
return notificationData;
|
||||
}
|
Loading…
Add table
Reference in a new issue