0
Fork 0
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:
Rish 2020-09-23 12:50:01 +05:30
parent 81823f531b
commit 8af2340d42
4 changed files with 178 additions and 47 deletions

View file

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

View file

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

View file

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

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