diff --git a/ghost/members-auth-pages/components/EmailInput.js b/ghost/members-auth-pages/components/EmailInput.js
new file mode 100644
index 0000000000..54844adce6
--- /dev/null
+++ b/ghost/members-auth-pages/components/EmailInput.js
@@ -0,0 +1,18 @@
+import FormInput from './FormInput';
+import { IconEmail } from './icons';
+
+export default ({value, error, children, onInput, className}) => (
+
+ {children}
+
+);
diff --git a/ghost/members-auth-pages/components/Form.js b/ghost/members-auth-pages/components/Form.js
new file mode 100644
index 0000000000..eb11edbf78
--- /dev/null
+++ b/ghost/members-auth-pages/components/Form.js
@@ -0,0 +1,82 @@
+import { Component } from 'preact';
+
+const states = {};
+
+export default class Form extends Component {
+
+ constructor(props) {
+ super(props);
+ const includeData = props.includeData || {};
+
+ if (props.bindTo) {
+ if (states[props.bindTo]) {
+ this.state = states[props.bindTo]
+ this.state.data = { ...this.state.data, ...includeData };
+ } else {
+ states[props.bindTo] = this.state = {
+ submitted: false,
+ data: {...includeData}
+ }
+ }
+ } else {
+ this.state = {
+ submitted: false,
+ data: {...includeData}
+ }
+ }
+ }
+
+ wrapChildren(children, data, onInput = () => {}) {
+ return children.map(child => {
+ const { bindTo } = child.attributes;
+ if (bindTo) {
+ child.attributes.value = data[bindTo];
+ child.attributes.onInput = (e) => {
+ // This is a hack
+ // Preact keeps copy of the child attributes to know whether to rerender
+ // The state change below will for a check, and the old attributes will be reused
+ child.attributes.error = false;
+ this.setState({
+ submitted: false,
+ data: Object.assign({}, this.state.data, {
+ [bindTo]: e.target.value
+ })
+ });
+ }
+
+ if (this.state.submitted && !data[bindTo]) {
+ child.attributes.error = true;
+ }
+ }
+ return child;
+ })
+ }
+
+ wrapSubmit(onSubmit = () => {}, children, data) {
+ return (e) => {
+ e.preventDefault();
+
+ const requiredFields = children.map(c => c.attributes.bindTo).filter(x => !!x)
+ if (!requiredFields.some(x => !data[x])) {
+ onSubmit(this.state.data)
+ }
+ this.setState({
+ submitted: true
+ });
+ }
+ }
+
+ render({bindTo, children, onInput, onSubmit}, state) {
+ if (bindTo) {
+ states[bindTo] = state;
+ }
+ const data = state.data;
+ return (
+
+
+
+ );
+ }
+}
diff --git a/ghost/members-auth-pages/components/FormFooter.js b/ghost/members-auth-pages/components/FormFooter.js
new file mode 100644
index 0000000000..e802e08a28
--- /dev/null
+++ b/ghost/members-auth-pages/components/FormFooter.js
@@ -0,0 +1,10 @@
+export default ({title, hash, label}) => (
+
+);
diff --git a/ghost/members-auth-pages/components/FormHeader.js b/ghost/members-auth-pages/components/FormHeader.js
new file mode 100644
index 0000000000..551d82b059
--- /dev/null
+++ b/ghost/members-auth-pages/components/FormHeader.js
@@ -0,0 +1,16 @@
+import { IconError } from './icons';
+
+export default ({title, error, errorText, children}) => (
+
+
+
+
{ title }
+ { children }
+
+ {(error ?
+
{ IconError }
+ {errorText}
+
: "")
+ }
+
+);
diff --git a/ghost/members-auth-pages/components/FormHeaderCTA.js b/ghost/members-auth-pages/components/FormHeaderCTA.js
new file mode 100644
index 0000000000..b4ddb69ca1
--- /dev/null
+++ b/ghost/members-auth-pages/components/FormHeaderCTA.js
@@ -0,0 +1,8 @@
+export default ({title, label, hash}) => (
+
+);
diff --git a/ghost/members-auth-pages/components/FormInput.js b/ghost/members-auth-pages/components/FormInput.js
new file mode 100644
index 0000000000..9d4f4f155f
--- /dev/null
+++ b/ghost/members-auth-pages/components/FormInput.js
@@ -0,0 +1,23 @@
+export default ({type, name, placeholder, value = '', error, onInput, required, className, children, icon}) => (
+
+
+ onInput(e, name) }
+ required={ required }
+ className={[
+ (value ? "gm-input-filled" : ""),
+ (error ? "gm-error" : ""),
+ className
+ ].join(' ')}
+ />
+
+ { icon }
+ { children }
+
+
+);
diff --git a/ghost/members-auth-pages/components/FormSubmit.js b/ghost/members-auth-pages/components/FormSubmit.js
new file mode 100644
index 0000000000..8b200be734
--- /dev/null
+++ b/ghost/members-auth-pages/components/FormSubmit.js
@@ -0,0 +1,7 @@
+export default ({onClick, label}) => (
+
+
+
+);
diff --git a/ghost/members-auth-pages/components/MembersProvider.js b/ghost/members-auth-pages/components/MembersProvider.js
new file mode 100644
index 0000000000..0331d3e900
--- /dev/null
+++ b/ghost/members-auth-pages/components/MembersProvider.js
@@ -0,0 +1,54 @@
+import { Component } from 'preact';
+
+const layer0 = require('../layer0');
+
+export default class MembersProvider extends Component {
+ constructor() {
+ super();
+ this.setGatewayFrame = gatewayFrame => this.gatewayFrame = gatewayFrame;
+ this.gateway = null;
+ }
+
+ getChildContext() {
+ return {
+ members: {
+ createSubscription: this.createMethod('createSubscription'),
+ signin: this.createMethod('signin'),
+ signup: this.createMethod('signup'),
+ requestPasswordReset: this.createMethod('requestPasswordReset'),
+ resetPassword: this.createMethod('resetPassword')
+ }
+ };
+ }
+
+ render({apiUrl, children}) {
+ const src = `${apiUrl}/members/gateway`;
+ return (
+
+ { children }
+
+
+ );
+ }
+
+ componentDidMount() {
+ const gatewayFrame = this.gatewayFrame;
+ gatewayFrame.addEventListener('load', () => {
+ this.gateway = layer0(gatewayFrame)
+ });
+ }
+
+ createMethod(method) {
+ return (options) => {
+ return new Promise((resolve, reject) =>
+ this.gateway.call(method, options, (err, successful) => {
+ if (err || !successful) {
+ reject(err || !successful);
+ }
+ resolve(successful);
+ })
+ );
+ }
+ }
+
+}
diff --git a/ghost/members-auth-pages/components/Modal.js b/ghost/members-auth-pages/components/Modal.js
new file mode 100644
index 0000000000..312cb72091
--- /dev/null
+++ b/ghost/members-auth-pages/components/Modal.js
@@ -0,0 +1,68 @@
+import { Component } from 'preact';
+
+import Pages from './Pages';
+
+import SigninPage from '../pages/SigninPage';
+import SignupPage from '../pages/SignupPage';
+import RequestPasswordResetPage from '../pages/RequestPasswordResetPage';
+import PasswordResetSentPage from '../pages/PasswordResetSentPage';
+import ResetPasswordPage from '../pages/ResetPasswordPage';
+
+export default class Modal extends Component {
+ constructor(props, context) {
+ super();
+ this.state = {
+ error: null,
+ containerClass: 'gm-page-overlay'
+ }
+ }
+
+ handleAction(promise) {
+ promise.then((success) => {
+ this.close(success);
+ }, (error) => {
+ this.setState({error});
+ });
+ }
+
+ render(props, state) {
+ const { queryToken } = props;
+ const { containerClass, error } = state;
+ const { members } = this.context;
+
+ const closeModal = () => this.close();
+ const clearError = () => this.setState({error: null});
+
+ const signin = (data) => this.handleAction(members.signin(data));
+ const signup = (data) => this.handleAction(members.signup(data));
+ const requestReset = (data) => this.handleAction(members.requestPasswordReset(data));
+ const resetPassword = (data) => this.handleAction(members.resetPassword(data));
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ close(success) {
+ this.setState({
+ containerClass: 'gm-page-overlay close'
+ });
+
+ window.setTimeout(() => {
+ this.setState({
+ containerClass: 'gm-page-overlay'
+ });
+ window.parent.postMessage({
+ msg: 'pls-close-auth-popup',
+ success
+ }, '*');
+ }, 700);
+ }
+}
diff --git a/ghost/members-auth-pages/components/NameInput.js b/ghost/members-auth-pages/components/NameInput.js
new file mode 100644
index 0000000000..631ba40e88
--- /dev/null
+++ b/ghost/members-auth-pages/components/NameInput.js
@@ -0,0 +1,18 @@
+import FormInput from './FormInput';
+import { IconName } from './icons';
+
+export default ({value, error, children, onInput, className}) => (
+
+ {children}
+
+);
diff --git a/ghost/members-auth-pages/components/Pages.js b/ghost/members-auth-pages/components/Pages.js
new file mode 100644
index 0000000000..bfc9f2bf46
--- /dev/null
+++ b/ghost/members-auth-pages/components/Pages.js
@@ -0,0 +1,41 @@
+import { Component } from 'preact';
+
+export default class Pages extends Component {
+ constructor(props) {
+ super(props);
+ this.state = this.getStateFromBrowser();
+ window.addEventListener("hashchange", () => this.onHashChange(), false);
+ this.handleChange = props.onChange || (() => {});
+ }
+
+ getStateFromBrowser() {
+ const [fullMatch, hash, query] = window.location.hash.match(/^#([^?]+)\??(.*)$/) || ['#', '', ''];
+ return {
+ hash,
+ query,
+ fullMatch
+ };
+ }
+
+ onHashChange() {
+ this.setState(this.getStateFromBrowser());
+ this.handleChange();
+ }
+
+ filterChildren(children, state) {
+ return children.filter((child) => {
+ return child.attributes.hash === state.hash;
+ }).map((child) => {
+ child.attributes.frameLocation = { ...this.state };
+ return child;
+ });
+ }
+
+ render({children, className, onClick}, state) {
+ return (
+
+ { this.filterChildren(children, state) }
+
+ );
+ }
+}
diff --git a/ghost/members-auth-pages/components/PasswordInput.js b/ghost/members-auth-pages/components/PasswordInput.js
new file mode 100644
index 0000000000..eda66484b3
--- /dev/null
+++ b/ghost/members-auth-pages/components/PasswordInput.js
@@ -0,0 +1,18 @@
+import FormInput from './FormInput';
+import { IconLock } from './icons';
+
+export default ({value, error, children, onInput, className}) => (
+
+ { children }
+
+);
diff --git a/ghost/members-auth-pages/index.js b/ghost/members-auth-pages/index.js
index f7ed55380e..1991796347 100644
--- a/ghost/members-auth-pages/index.js
+++ b/ghost/members-auth-pages/index.js
@@ -1,449 +1,24 @@
import './styles/members.css';
-import {IconEmail, IconLock, IconName, IconClose, IconError} from './components/icons';
import { Component } from 'preact';
-const origin = new URL(window.location).origin;
-const membersApi = location.pathname.replace(/\/members\/auth\/?$/, '/ghost/api/v2/members');
-const storage = window.localStorage;
-var layer0 = require('./layer0');
-function getFreshState() {
- const [hash, formType, query] = window.location.hash.match(/^#([^?]+)\??(.*)$/) || ['#signin?', 'signin', ''];
- return {
- formData: {},
- query,
- formType,
- parentContainerClass: 'gm-page-overlay',
- showError: false,
- submitFail: false
- };
-}
+import MembersProvider from './components/MembersProvider';
+import Modal from './components/Modal';
export default class App extends Component {
constructor() {
super();
- this.state = getFreshState();
- this.gatewayFrame = '';
- window.addEventListener("hashchange", () => this.onHashChange(), false);
- }
+ const apiUrl = window.location.href.substring(0, window.location.href.indexOf('/members/auth'));
- loadGateway() {
- const blogUrl = window.location.href.substring(0, window.location.href.indexOf('/members/auth'));
- const frame = window.document.createElement('iframe');
- frame.id = 'member-gateway';
- frame.style.display = 'none';
- frame.src = `${blogUrl}/members/gateway`;
- frame.onload = () => {
- this.gatewayFrame = layer0(frame);
+ this.state = {
+ apiUrl
};
- document.body.appendChild(frame);
- }
-
- componentDidMount() {
- this.loadGateway();
- }
-
- onHashChange() {
- this.setState(getFreshState());
- }
-
- onInputChange(e, name) {
- let value = e.target.value;
- this.setState({
- formData: {
- ...this.state.formData,
- [name]: value
- }
- });
- }
-
- submitForm(e) {
- e.preventDefault();
- if (this.hasFrontendError(this.state.formType)) {
- return false;
- }
- switch (this.state.formType) {
- case 'signin':
- this.signin(this.state.formData);
- break;
- case 'signup':
- this.signup(this.state.formData);
- break;
- case 'request-password-reset':
- this.requestPasswordReset(this.state.formData);
- break;
- case 'password-reset-sent':
- this.resendPasswordResetEmail(this.state.formData)
- break;
- case 'reset-password':
- this.resetPassword(this.state.formData)
- break;
- }
- return false;
- }
-
- signin({ email, password }) {
- this.gatewayFrame.call('signin', {email, password}, (err, successful) => {
- if (err || !successful) {
- this.setState({
- submitFail: true
- });
- }
- });
- }
-
- signup({ name, email, password }) {
- this.gatewayFrame.call('signup', { name, email, password }, (err, successful) => {
- if (err || !successful) {
- this.setState({
- submitFail: true
- });
- }
- });
- }
-
- requestPasswordReset({ email }) {
- this.gatewayFrame.call('request-password-reset', {email}, (err, successful) => {
- if (err || !successful) {
- this.setState({
- submitFail: true
- });
- } else {
- window.location.hash = 'password-reset-sent';
- }
- });
- }
-
- resendPasswordResetEmail({ email }) {
- this.gatewayFrame.call('request-password-reset', {email}, (err, successful) => {
- if (err || !successful) {
- this.setState({
- submitFail: true
- });
- } else {
- window.location.hash = 'password-reset-sent';
- }
- });
- }
-
- resetPassword({ password }) {
- const queryParams = new URLSearchParams(this.state.query);
- const token = queryParams.get('token') || '';
- this.gatewayFrame.call('reset-password', {password, token}, (err, successful) => {
- if (err || !successful) {
- this.setState({
- submitFail: true
- });
- }
- });
- }
-
- hasFrontendError(formType = this.state.formType) {
- switch(formType) {
- case 'signin':
- return (
- this.hasError({errorType: 'no-input', data: 'email'}) ||
- this.hasError({errorType: 'no-input', data: 'password'})
- );
- case 'signup':
- return (
- this.hasError({errorType: 'no-input', data: 'email'}) ||
- this.hasError({errorType: 'no-input', data: 'password'}) ||
- this.hasError({errorType: 'no-input', data: 'name'})
- );
- case 'request-password-reset':
- return (
- this.hasError({errorType: 'no-input', data: 'email'})
- );
- case 'reset-password':
- return (
- this.hasError({errorType: 'no-input', data: 'password'})
- );
- break;
- }
- return false;
- }
-
- hasError({errorType, data}) {
- if (!this.state.showError) {
- return false;
- }
- let value = '';
- switch(errorType) {
- case 'no-input':
- value = this.state.formData[data];
- return (!value);
- case 'form-submit':
- return this.state.submitFail;
- }
- }
-
- renderError({error, formType}) {
- if (this.hasError(error)) {
- let errorLabel = '';
- switch(error.errorType) {
- case 'no-input':
- errorLabel = `Enter ${error.data}`;
- break;
- case 'form-submit':
- switch(formType) {
- case 'signin':
- errorLabel = "Wrong email or password";
- break;
- case 'signup':
- errorLabel = "Email already registered"
- break;
- case 'request-password-reset':
- errorLabel = "Unable to send email"
- break;
- case 'password-reset-sent':
- errorLabel = "Unable to send email"
- break;
- }
- }
- return (
- { errorLabel }
- )
- }
- return null;
- }
-
- renderFormHeaders(formType) {
- let mainTitle = '';
- let ctaTitle = '';
- let ctaLabel = '';
- let hash = '';
- switch (formType) {
- case 'signup':
- mainTitle = 'Sign up';
- ctaTitle = 'Already a member?';
- ctaLabel = 'Log in';
- hash = 'signin';
- break;
- case 'signin':
- mainTitle = 'Log in';
- ctaTitle = 'Not a member?';
- ctaLabel = 'Sign up';
- hash = 'signup';
- break;
- case 'request-password-reset':
- mainTitle = 'Reset password';
- break;
- case 'password-reset-sent':
- mainTitle = 'Reset password';
- break;
- case 'reset-password':
- mainTitle = 'Reset password';
- break;
- }
- let formError = this.renderError({ error: {errorType: "form-submit"}, formType });
- return (
-
-
-
-
{ mainTitle }
- {(ctaTitle ?
-
- : "")}
-
- {(formError ?
{ IconError } { formError }
: "")}
-
- )
- }
-
- renderFormFooters(formType) {
- let mainTitle = '';
- let ctaTitle = '';
- let ctaLabel = '';
- let hash = '';
- switch (formType) {
- case 'request-password-reset':
- ctaTitle = 'Back to';
- ctaLabel = 'log in';
- hash = 'signin';
- break;
- case 'password-reset-sent':
- ctaTitle = 'Back to';
- ctaLabel = 'log in';
- hash = 'signin';
- break;
- case 'reset-password':
- ctaTitle = 'Back to';
- ctaLabel = 'log in';
- hash = 'signin';
- break;
- }
- if (ctaTitle) {
- return (
-
- )
- }
- }
-
- renderFormInput({type, name, label, icon, placeholder, required, formType}) {
- let value = this.state.formData[name];
- let className = "";
- let forgot = (type === 'password' && formType === 'signin');
- let inputError = this.renderError({ error: {errorType: 'no-input', data: name}, formType });
- className += (value ? "gm-input-filled" : "") + (forgot ? " gm-forgot-input" : "") + (inputError ? " gm-error" : "");
-
- return (
-
-
- {/* { (inputError ?
{ inputError }
: "")} */}
-
- )
- }
-
- renderFormText({formType}) {
- return (
-
-
We’ve sent a recovery email to your inbox. Follow the link in the email to reset your password.
-
- )
- }
-
- onSubmitClick(e) {
- this.setState({
- showError: true,
- submitFail: false
- });
- }
-
- renderFormSubmit({buttonLabel, formType}) {
- return (
-
-
-
- )
- }
-
- renderFormSection(formType) {
- const emailInput = this.renderFormInput({
- type: 'email',
- name: 'email',
- label: 'Email',
- icon: IconEmail,
- placeholder: 'Email...',
- required: true,
- formType: formType
- });
- const passwordInput = this.renderFormInput({
- type: 'password',
- name: 'password',
- label: 'Password',
- icon: IconLock,
- placeholder: 'Password...',
- required: true,
- formType: formType
- });
- const nameInput = this.renderFormInput({
- type: 'text',
- name: 'name',
- label: 'Name',
- icon: IconName,
- placeholder: 'Name...',
- required: true,
- formType: formType
- });
- const formText = this.renderFormText({formType});
-
- let formElements = [];
- let buttonLabel = '';
- let formContainerClass = 'flex flex-column'
- switch (formType) {
- case 'signin':
- buttonLabel = 'Log in';
- formElements = [emailInput, passwordInput, this.renderFormSubmit({formType, buttonLabel})];
- formContainerClass += ' mt3'
- break;
- case 'signup':
- buttonLabel = 'Sign up';
- formElements = [nameInput, emailInput, passwordInput, this.renderFormSubmit({formType, buttonLabel})];
- formContainerClass += ' mt3'
- break;
- case 'request-password-reset':
- buttonLabel = 'Send reset password instructions';
- formElements = [emailInput, this.renderFormSubmit({formType, buttonLabel})];
- break;
- case 'password-reset-sent':
- buttonLabel = 'Resend instructions';
- formElements = [formText, this.renderFormSubmit({formType, buttonLabel})];
- break;
- case 'reset-password':
- buttonLabel = 'Set password';
- formElements = [passwordInput, this.renderFormSubmit({formType, buttonLabel})];
- break;
- }
- return (
-
-
-
- )
- }
-
- renderFormComponent(formType = this.state.formType) {
- return (
-
-
e.stopPropagation()}>
-
this.close(e)}>{ IconClose }
- {this.renderFormHeaders(formType)}
- {this.renderFormSection(formType)}
- {this.renderFormFooters(formType)}
-
-
- );
}
render() {
return (
- this.close(e)}>
- {this.renderFormComponent()}
-
+
+
+
);
}
-
- close(event) {
- this.setState({
- parentContainerClass: 'gm-page-overlay close'
- });
-
- window.setTimeout(() => {
- this.setState({
- parentContainerClass: 'gm-page-overlay'
- });
- window.parent.postMessage('pls-close-auth-popup', '*');
- }, 700);
- }
}
diff --git a/ghost/members-auth-pages/pages/PasswordResetSentPage.js b/ghost/members-auth-pages/pages/PasswordResetSentPage.js
new file mode 100644
index 0000000000..9e8a134e85
--- /dev/null
+++ b/ghost/members-auth-pages/pages/PasswordResetSentPage.js
@@ -0,0 +1,18 @@
+import FormHeader from '../components/FormHeader';
+import FormSubmit from '../components/FormSubmit';
+import { IconClose } from '../components/icons';
+
+export default ({error, handleClose, handleSubmit}) => (
+
+);
diff --git a/ghost/members-auth-pages/pages/RequestPasswordResetPage.js b/ghost/members-auth-pages/pages/RequestPasswordResetPage.js
new file mode 100644
index 0000000000..5256eadf56
--- /dev/null
+++ b/ghost/members-auth-pages/pages/RequestPasswordResetPage.js
@@ -0,0 +1,19 @@
+import FormHeader from '../components/FormHeader';
+import EmailInput from '../components/EmailInput';
+import FormSubmit from '../components/FormSubmit';
+import { IconClose } from '../components/icons';
+
+import Form from '../components/Form';
+
+export default ({error, handleClose, handleSubmit}) => (
+
+);
diff --git a/ghost/members-auth-pages/pages/ResetPasswordPage.js b/ghost/members-auth-pages/pages/ResetPasswordPage.js
new file mode 100644
index 0000000000..84a92227ef
--- /dev/null
+++ b/ghost/members-auth-pages/pages/ResetPasswordPage.js
@@ -0,0 +1,25 @@
+import FormHeader from '../components/FormHeader';
+import PasswordInput from '../components/PasswordInput';
+import FormSubmit from '../components/FormSubmit';
+import Form from '../components/Form';
+import { IconClose } from '../components/icons';
+
+const getTokenData = frameLocation => {
+ const params = new URLSearchParams(frameLocation.query);
+ const token = params.get('token') || '';
+ return { token };
+
+};
+
+export default ({error, frameLocation, handleClose, handleSubmit}) => (
+
+);
diff --git a/ghost/members-auth-pages/pages/SigninPage.js b/ghost/members-auth-pages/pages/SigninPage.js
new file mode 100644
index 0000000000..a6f1adec62
--- /dev/null
+++ b/ghost/members-auth-pages/pages/SigninPage.js
@@ -0,0 +1,29 @@
+import Form from '../components/Form';
+import FormHeader from '../components/FormHeader';
+import FormHeaderCTA from '../components/FormHeaderCTA';
+import FormSubmit from '../components/FormSubmit';
+
+import EmailInput from '../components/EmailInput';
+import PasswordInput from '../components/PasswordInput';
+
+import { IconClose } from '../components/icons';
+
+export default ({error, handleClose, handleSubmit}) => (
+
+);
diff --git a/ghost/members-auth-pages/pages/SignupPage.js b/ghost/members-auth-pages/pages/SignupPage.js
new file mode 100644
index 0000000000..3c23d7a6e2
--- /dev/null
+++ b/ghost/members-auth-pages/pages/SignupPage.js
@@ -0,0 +1,26 @@
+import Form from '../components/Form';
+import FormHeader from '../components/FormHeader';
+import FormHeaderCTA from '../components/FormHeaderCTA';
+import FormSubmit from '../components/FormSubmit';
+
+import NameInput from '../components/NameInput';
+import EmailInput from '../components/EmailInput';
+import PasswordInput from '../components/PasswordInput';
+import { IconClose } from '../components/icons';
+
+export default ({error, handleClose, handleSubmit}) => (
+
+);