mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 11:55:03 +03:00
Refactored members auth flow with dynamic settings
no issue - Updated members auth flow UI - Updated members settings and routing to be dynamic
This commit is contained in:
parent
90aef4f6c9
commit
20a898a986
@ -22,9 +22,8 @@ function hideMembersOnlyContent(attrs, frame) {
|
||||
return BLOCK_CONTENT;
|
||||
}
|
||||
|
||||
const planRequired = membersService.api.paymentConfigured;
|
||||
const memberHasPlan = !!(frame.original.context.member.plans || []).length;
|
||||
if (!planRequired) {
|
||||
if (!membersService.api.isPaymentConfigured()) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
if (memberHasPlan) {
|
||||
|
@ -128,6 +128,9 @@
|
||||
},
|
||||
"members_session_secret": {
|
||||
"defaultValue": null
|
||||
},
|
||||
"members_subscription_settings": {
|
||||
"defaultValue": "{\"isPaid\":false,\"paymentProcessors\":[{\"adapter\":\"stripe\",\"config\":{\"secret_token\":\"\",\"public_token\":\"\",\"product\":{\"name\":\"Ghost Subscription\"},\"plans\":[{\"name\":\"Monthly\",\"currency\":\"usd\",\"interval\":\"month\",\"amount\":\"\"},{\"name\":\"Yearly\",\"currency\":\"usd\",\"interval\":\"year\",\"amount\":\"\"}]}}]}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,13 +23,14 @@ module.exports = function MembersApi({
|
||||
updateMember,
|
||||
getMember,
|
||||
listMembers,
|
||||
sendEmail
|
||||
sendEmail,
|
||||
siteConfig
|
||||
}) {
|
||||
const {encodeToken, decodeToken, getPublicKeys} = Tokens({privateKey, publicKey, issuer});
|
||||
|
||||
const subscriptions = new Subscriptions(paymentConfig);
|
||||
let subscriptions = new Subscriptions(paymentConfig);
|
||||
|
||||
const users = Users({
|
||||
let users = Users({
|
||||
subscriptions,
|
||||
createMember,
|
||||
updateMember,
|
||||
@ -86,7 +87,10 @@ module.exports = function MembersApi({
|
||||
return subscriptions.getPublicConfig(adapter);
|
||||
}));
|
||||
})
|
||||
.then(data => res.json(data))
|
||||
.then(data => res.json({
|
||||
paymentConfig: data,
|
||||
siteConfig: siteConfig
|
||||
}))
|
||||
.catch(handleError(500, res));
|
||||
});
|
||||
|
||||
@ -211,6 +215,21 @@ module.exports = function MembersApi({
|
||||
httpHandler.staticRouter = staticRouter;
|
||||
httpHandler.apiRouter = apiRouter;
|
||||
httpHandler.memberUserObject = users;
|
||||
httpHandler.reconfigureSettings = function (data) {
|
||||
subscriptions = new Subscriptions(data.paymentConfig);
|
||||
users = Users({
|
||||
subscriptions,
|
||||
createMember,
|
||||
updateMember,
|
||||
getMember,
|
||||
validateMember,
|
||||
sendEmail,
|
||||
encodeToken,
|
||||
listMembers,
|
||||
decodeToken
|
||||
});
|
||||
siteConfig = data.siteConfig;
|
||||
};
|
||||
|
||||
return httpHandler;
|
||||
};
|
||||
|
@ -1,18 +1,31 @@
|
||||
import React, {Component} from 'react';
|
||||
import {CardElement} from 'react-stripe-elements';
|
||||
import React, { Component } from 'react';
|
||||
import { CardElement } from 'react-stripe-elements';
|
||||
|
||||
class CheckoutForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gm-form-element">
|
||||
<CardElement />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let style = {
|
||||
base: {
|
||||
'::placeholder': {
|
||||
color: '#8795A1',
|
||||
fontSize: '15px'
|
||||
}
|
||||
},
|
||||
invalid: {
|
||||
'::placeholder': {
|
||||
color: 'rgba(240, 82, 48, 0.75)'
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="gm-form-element">
|
||||
<CardElement style={ style } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckoutForm;
|
||||
|
@ -72,7 +72,7 @@ export default class Form extends Component {
|
||||
}
|
||||
const data = state.data;
|
||||
return (
|
||||
<div className="flex flex-column mt3">
|
||||
<div className="flex flex-column mt7">
|
||||
<form className="gm-signup-form" onSubmit={this.wrapSubmit(onSubmit, children, data)} noValidate>
|
||||
{ this.wrapChildren(children, data, onInput) }
|
||||
</form>
|
||||
|
@ -3,7 +3,7 @@ import { IconError } from './icons';
|
||||
export default ({title, error, errorText, children}) => (
|
||||
<div>
|
||||
<div className="gm-auth-header">
|
||||
<h1>{ title }</h1>
|
||||
{ title ? (<h1>{title}</h1>) : "" }
|
||||
{ children }
|
||||
</div>
|
||||
{(error ?
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { IconRightArrow } from './icons';
|
||||
|
||||
export default ({title, label, hash}) => (
|
||||
<div className="flex items-baseline mt2">
|
||||
<h4>{ title }</h4>
|
||||
export default ({title, label, icon, hash}) => (
|
||||
<div className="gm-auth-cta">
|
||||
{title ? (<h4>{ title }</h4>) : ""}
|
||||
<a href={hash}>
|
||||
{ label }
|
||||
{ IconRightArrow }
|
||||
{ icon }
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,9 @@
|
||||
export default ({type, name, placeholder, value = '', error, onInput, required, className, children, icon}) => (
|
||||
<div className="gm-form-element">
|
||||
<div className="gm-input">
|
||||
<div className={[
|
||||
(className ? className : ""),
|
||||
"gm-input"
|
||||
].join(' ')}>
|
||||
<input
|
||||
type={ type }
|
||||
name={ name }
|
||||
@ -11,8 +14,7 @@ export default ({type, name, placeholder, value = '', error, onInput, required,
|
||||
required={ required }
|
||||
className={[
|
||||
(value ? "gm-input-filled" : ""),
|
||||
(error ? "gm-error" : ""),
|
||||
className
|
||||
(error ? "gm-error" : "")
|
||||
].join(' ')}
|
||||
/>
|
||||
<i>{ icon }</i>
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default ({onClick, label}) => (
|
||||
<div className="mt6">
|
||||
<div className="mt7">
|
||||
<button type="submit" className="gm-btn-blue" onClick={onClick}>
|
||||
{ label }
|
||||
</button>
|
||||
|
@ -7,9 +7,9 @@ import SignupPage from '../pages/SignupPage';
|
||||
import RequestPasswordResetPage from '../pages/RequestPasswordResetPage';
|
||||
import PasswordResetSentPage from '../pages/PasswordResetSentPage';
|
||||
import ResetPasswordPage from '../pages/ResetPasswordPage';
|
||||
import StripePaymentPage from '../pages/StripePaymentPage';
|
||||
import { IconClose } from '../components/icons';
|
||||
import StripeSubscribePage from '../pages/StripeSubscribePage';
|
||||
import { IconClose } from '../components/icons';
|
||||
import StripeUpgradePage from '../pages/StripeUpgradePage';
|
||||
|
||||
export default class Modal extends Component {
|
||||
constructor(props, context) {
|
||||
@ -24,8 +24,8 @@ export default class Modal extends Component {
|
||||
if (this.state.loadingConfig) {
|
||||
return;
|
||||
}
|
||||
this.context.members.getConfig().then(paymentConfig => {
|
||||
this.setState({ paymentConfig, loadingConfig: false });
|
||||
this.context.members.getConfig().then(({paymentConfig, siteConfig}) => {
|
||||
this.setState({ paymentConfig, siteConfig, loadingConfig: false });
|
||||
}).catch((error) => {
|
||||
this.setState({ error, loadingConfig: false });
|
||||
});
|
||||
@ -43,15 +43,19 @@ export default class Modal extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
renderSignupPage({error, stripeConfig, members, signup, closeModal}) {
|
||||
renderSignupPage({error, stripeConfig, members, signup, closeModal, siteConfig}) {
|
||||
|
||||
if (stripeConfig) {
|
||||
const createAccountWithSubscription = (data) => this.handleAction(
|
||||
members.signup(data).then(() => {
|
||||
members.createSubscription(data);
|
||||
})
|
||||
);
|
||||
return <StripePaymentPage stripeConfig={stripeConfig} error={error} hash="signup" handleSubmit={createAccountWithSubscription} handleClose={closeModal} />
|
||||
const createAccountWithSubscription = (data) => members.signup(data).then((success) => {
|
||||
members.createSubscription(data).then((success) => {
|
||||
this.close();
|
||||
}, (error) => {
|
||||
this.setState({ error: "Unable to confirm payment" });
|
||||
});
|
||||
}, (error) => {
|
||||
this.setState({ error: "Unable to signup" });
|
||||
});
|
||||
return <StripeSubscribePage stripeConfig={stripeConfig} error={error} hash="signup" handleSubmit={createAccountWithSubscription} handleClose={closeModal} siteConfig={siteConfig} />
|
||||
|
||||
}
|
||||
return (
|
||||
@ -67,12 +71,12 @@ export default class Modal extends Component {
|
||||
members.createSubscription(data)
|
||||
);
|
||||
const stripeConfig = paymentConfig && paymentConfig.find(({adapter}) => adapter === 'stripe');
|
||||
return <StripeSubscribePage frameLocation={props.frameLocation} stripeConfig={stripeConfig} error={error} hash="upgrade" handleSubmit={createSubscription} handleClose={closeModal}/>
|
||||
return <StripeUpgradePage frameLocation={props.frameLocation} stripeConfig={stripeConfig} error={error} hash="upgrade" handleSubmit={createSubscription} handleClose={closeModal}/>
|
||||
|
||||
}
|
||||
|
||||
render(props, state) {
|
||||
const { containerClass, error, loadingConfig, paymentConfig } = state;
|
||||
const { containerClass, error, loadingConfig, paymentConfig, siteConfig } = state;
|
||||
const { members } = this.context;
|
||||
|
||||
const closeModal = () => this.close();
|
||||
@ -80,7 +84,11 @@ export default class Modal extends Component {
|
||||
|
||||
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 requestReset = (data) => members.requestPasswordReset(data).then((success) => {
|
||||
window.location.hash = 'password-reset-sent';
|
||||
}, (error) => {
|
||||
this.setState({ error });
|
||||
});
|
||||
const resetPassword = (data) => this.handleAction(members.resetPassword(data));
|
||||
const stripeConfig = paymentConfig && paymentConfig.find(({ adapter }) => adapter === 'stripe');
|
||||
|
||||
@ -92,13 +100,13 @@ export default class Modal extends Component {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Pages className={containerClass} onChange={clearError} onClick={closeModal} stripeConfig={stripeConfig}>
|
||||
<Pages className={containerClass} onChange={clearError} onClick={closeModal} stripeConfig={stripeConfig} siteConfig={siteConfig}>
|
||||
<SigninPage error={error} hash="" handleSubmit={signup} />
|
||||
<SigninPage error={error} hash="signin" handleSubmit={signin} />
|
||||
{this.renderSignupPage({error, stripeConfig, members, signup, closeModal})}
|
||||
{this.renderSignupPage({error, stripeConfig, members, signup, closeModal, siteConfig})}
|
||||
{this.renderUpgradePage(props, state)}
|
||||
<RequestPasswordResetPage error={error} hash="request-password-reset" handleSubmit={requestReset} />
|
||||
<PasswordResetSentPage error={error} hash="password-reset-sent" handleSubmit={requestReset} />
|
||||
<PasswordResetSentPage error={ error } hash="password-reset-sent" handleSubmit={closeModal} />
|
||||
<ResetPasswordPage error={error} hash="reset-password" handleSubmit={resetPassword} />
|
||||
</Pages>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component } from 'preact';
|
||||
import { IconClose, GhostLogo } from './icons';
|
||||
|
||||
export default class Pages extends Component {
|
||||
constructor(props) {
|
||||
@ -31,18 +32,32 @@ export default class Pages extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
render({ children, className, onClick, stripeConfig }, state) {
|
||||
render({ children, className, onClick, stripeConfig, siteConfig }, state) {
|
||||
let modalClassName = "gm-modal gm-auth-modal";
|
||||
if (state.hash === 'signup' && stripeConfig) {
|
||||
modalClassName += " gm-subscribe-modal"
|
||||
}
|
||||
let iconUrl = siteConfig && siteConfig.icon;
|
||||
let title = (siteConfig && siteConfig.title) || "Ghost Publication";
|
||||
let iconStyle = iconUrl ? {
|
||||
backgroundImage: `url(${iconUrl})`,
|
||||
backgroundSize: `38px`
|
||||
} : {};
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<div className={className}>
|
||||
<div className="gm-modal-header">
|
||||
<div className="gm-logo" style={iconStyle}></div>
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
<div className="gm-modal-close" onClick={ onClick }>{IconClose}</div>
|
||||
<div className="gm-modal-container">
|
||||
<div className={modalClassName} onClick={(e) => e.stopPropagation()}>
|
||||
{this.filterChildren(children, state)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="gm-powered-by">
|
||||
<a href="https://ghost.org" target="_blank"><span>Powered by</span> {GhostLogo}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,4 +21,8 @@ export const IconError = (
|
||||
|
||||
export const IconRightArrow = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12.75 23.25c-.2 0-.389-.078-.53-.22-.292-.292-.292-.768 0-1.061l9.22-9.22H.75C.336 12.75 0 12.414 0 12s.336-.75.75-.75h20.689l-9.22-9.22C12.078 1.889 12 1.7 12 1.5s.078-.389.22-.53c.141-.142.33-.22.53-.22s.389.078.53.22l10.5 10.5c.07.07.125.152.163.245l.01.026c.031.081.047.17.047.259 0 .087-.016.174-.047.258l-.006.016c-.042.104-.098.187-.168.257L13.28 23.03c-.141.142-.33.22-.53.22z" /></svg>
|
||||
);
|
||||
|
||||
export const GhostLogo = (
|
||||
<svg width="493" height="161" viewBox="0 0 493 161" xmlns="http://www.w3.org/2000/svg"><title>ghost-logo</title><g fill="#FFF" fill-rule="nonzero"><path d="M328.52 37.36c-27.017 0-40.97 19.323-40.97 43.16 0 23.837 13.61 43.162 40.97 43.162s40.968-19.325 40.968-43.163c0-23.84-13.954-43.16-40.97-43.16l.002.001zm20.438 43.237c-.02 15.328-5.126 27.743-20.44 27.743-15.312 0-20.42-12.414-20.435-27.743v-.078c.016-15.33 5.124-27.74 20.437-27.74 15.312 0 20.42 12.41 20.438 27.74V80.597zM207.553 5.19c0-1.103.885-2.124 1.984-2.282 0 0 13.577-1.95 14.784-2.115 1.37-.187 3.19.798 3.19 2.744v44.236c3.23-3.105 6.79-5.608 10.66-7.515 3.88-1.906 8.43-2.86 13.66-2.86 4.53 0 8.53.776 12.03 2.33 3.5 1.55 6.43 3.73 8.77 6.533 2.34 2.81 4.12 6.16 5.33 10.05 1.21 3.9 1.82 8.19 1.82 12.87v51.35c0 1.1-.89 2-2 2h-15.95c-1.1 0-2-.9-2-1.99V69.18c0-5.118-1.17-9.08-3.51-11.888-2.35-2.804-5.86-4.207-10.544-4.207-3.45 0-6.677.79-9.69 2.37-3.02 1.58-5.87 3.73-8.564 6.46v58.617c0 1.102-.894 2-2.002 2h-15.94c-1.11 0-2.005-.895-2.005-2V5.188l-.023.002zm244.007 95.327v-43.68h-13.482c-1.1 0-1.742-.87-1.443-1.916l3-10.49c.262-.9.942-1.87 2.308-2.07l9.597-1.35 3.508-23.49c.163-1.09 1.18-2.1 2.274-2.26 0 0 9.192-1.31 10.963-1.58 1.673-.25 3.19.97 3.19 2.81v24.52h17.565c1.106 0 2.002.9 2.002 2.01v11.82c0 1.11-.89 2.01-2.002 2.01h-17.566v43.08c0 6.02 3.623 8.32 7.095 8.32 2.12 0 5.02-1.14 7.19-2.16 1.34-.62 3.41-.16 3.95 1.73l2.45 8.65c.3 1.07-.25 2.37-1.23 2.86 0 0-7.29 4.37-17.06 4.37-13.73 0-22.33-8.08-22.33-23.16l.021-.024zm-44.584-47.74c-7.084 0-12.657 2.476-12.657 8.433 0 7.44 12.01 9.606 20.23 12.64 5.49 2.027 20.24 5.98 20.24 22.016 0 19.48-16 27.807-33.06 27.807-17.06 0-25.4-5.465-25.4-5.465-.96-.527-1.5-1.822-1.2-2.89 0 0 2.1-7.52 2.64-9.386.48-1.68 2.41-2.27 3.64-1.792 4.39 1.712 12.32 4.092 21.28 4.092 9.07 0 13.46-2.803 13.46-8.777 0-7.95-12.26-10.38-20.36-12.967-5.59-1.78-20.36-5.93-20.36-23.566 0-17.373 15.08-25.524 31.2-25.524 13.64 0 23.5 4.69 23.5 4.69 1.01.427 1.58 1.635 1.28 2.698l-2.658 9.357c-.488 1.74-1.898 2.537-3.666 1.957-3.89-1.277-11.2-3.322-18.15-3.322l.041-.001zm-210.313-15.28c-6.695.775-11.472 3.962-14.562 6.93-6.06-4.81-14.49-7.106-23.94-7.106-18.95 0-33.76 9.26-33.76 29.43 0 11.58 4.88 19.56 12.62 24.26-5.75 2.75-9.57 8.59-9.57 14.34 0 9.61 7.5 12.61 7.5 12.61s-13.11 6.44-13.11 19.32c0 16.49 15.01 23.16 33.34 23.16 26.43 0 44.61-11.04 44.61-31.31 0-12.47-9.44-19.36-30.01-20.18-12.2-.48-20.11-.93-22.07-1.58-2.59-.87-3.86-2.96-3.86-5.28 0-2.55 2.08-4.98 5.35-6.65 2.86.516 5.87.768 8.99.768 18.97 0 33.76-9.223 33.76-29.425 0-4.897-.87-9.15-2.46-12.78 2.79-1.506 8.34-2.25 8.34-2.25 1.09-.17 1.975-1.21 1.974-2.31V40.3c0-1.88-1.59-2.955-3.1-2.78l-.042-.023zm-49.13 85.132s9.954.38 19.9.84c11.172.52 14.654 2.96 14.654 8.81 0 7.15-9.71 14.1-23.28 14.1-12.88 0-19.314-4.54-19.314-12.08 0-4.33 2.26-9.18 8.04-11.69v.02zm10.66-40.54c-8.978 0-15.983-4.83-15.983-15.35 0-10.53 7.01-15.35 15.983-15.35 8.974 0 15.984 4.81 15.984 15.34 0 10.53-7.002 15.34-15.984 15.34v.02z" /><g opacity=".6" transform="translate(0 36)"><rect x=".209" y="69.017" width="33.643" height="17.014" rx="4" /><rect x="50.672" y="69.017" width="33.622" height="17.014" rx="4" /><rect x=".184" y="34.99" width="84.121" height="17.014" rx="4" /><rect x=".209" y=".964" width="50.469" height="17.013" rx="4" /><rect x="67.494" y=".964" width="16.821" height="17.013" rx="4" /></g></g></svg>
|
||||
);
|
@ -1,14 +1,16 @@
|
||||
import FormHeader from '../components/FormHeader';
|
||||
import FormSubmit from '../components/FormSubmit';
|
||||
|
||||
import Form from '../components/Form';
|
||||
|
||||
export default ({ error, handleSubmit }) => (
|
||||
<div className="gm-modal-form">
|
||||
<FormHeader title="Sign up" error={error} errorText="Unable to send email" />
|
||||
<FormHeader title="Reset password" error={error} errorText="Unable to send email" />
|
||||
<Form bindTo="request-password-reset" onSubmit={handleSubmit}>
|
||||
<div className="gm-reset-sent">
|
||||
<p>We’ve sent a recovery email to your inbox. Follow the link in the email to reset your password.</p>
|
||||
</div>
|
||||
<FormSubmit label="Resend instructions" />
|
||||
<FormSubmit label="Close" />
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -5,11 +5,14 @@ import FormSubmit from '../components/FormSubmit';
|
||||
import Form from '../components/Form';
|
||||
|
||||
export default ({ error, handleClose, handleSubmit }) => (
|
||||
<div className="gm-modal-form">
|
||||
<div className="gm-modal-form gm-reset-pwd-page">
|
||||
<FormHeader title="Reset password" error={error} errorText="Unable to send email" />
|
||||
<Form bindTo="request-password-reset" onSubmit={handleSubmit}>
|
||||
<EmailInput bindTo="email" />
|
||||
<EmailInput bindTo="email" className="single" />
|
||||
<FormSubmit label="Send reset password instructions" />
|
||||
<div class="flex justify-center mt5">
|
||||
<a href="#signin">Cancel</a>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -14,7 +14,7 @@ export default ({ error, frameLocation, handleSubmit }) => (
|
||||
<div className="gm-modal-form">
|
||||
<FormHeader title="Reset password" error={error} errorText="Unable to reset password" />
|
||||
<Form includeData={getTokenData(frameLocation)} onSubmit={handleSubmit}>
|
||||
<PasswordInput bindTo="password" />
|
||||
<PasswordInput bindTo="password" className="single" />
|
||||
<FormSubmit label="Set password" />
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -2,23 +2,29 @@ import Form from '../components/Form';
|
||||
import FormHeader from '../components/FormHeader';
|
||||
import FormHeaderCTA from '../components/FormHeaderCTA';
|
||||
import FormSubmit from '../components/FormSubmit';
|
||||
import { IconRightArrow } from '../components/icons';
|
||||
|
||||
import EmailInput from '../components/EmailInput';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
|
||||
export default ({ error, handleSubmit }) => (
|
||||
<div className="gm-modal-form">
|
||||
<FormHeader title="Log in" error={error} errorText="Wrong email or password">
|
||||
<FormHeaderCTA title="Not a member?" label="Sign up" hash="#signup" />
|
||||
</FormHeader>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<EmailInput bindTo="email" />
|
||||
<PasswordInput bindTo="password" className="gm-forgot-input">
|
||||
<a href="#request-password-reset" className="gm-forgot-link">
|
||||
Forgot
|
||||
<div className="flex flex-column items-center">
|
||||
<div className="gm-modal-form gm-signin-page">
|
||||
<FormHeader title="" error={error} errorText="Wrong email or password">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<h1>Log in</h1>
|
||||
<FormHeaderCTA title="Not a member?" label="Sign up" icon={IconRightArrow} hash="#signup" />
|
||||
</div>
|
||||
</FormHeader>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<EmailInput bindTo="email" className="first" />
|
||||
<PasswordInput bindTo="password" className="gm-forgot-input last">
|
||||
<a href="#request-password-reset" className="gm-forgot-link">
|
||||
Forgot
|
||||
</a>
|
||||
</PasswordInput>
|
||||
<FormSubmit label="Log in" />
|
||||
</Form>
|
||||
</PasswordInput>
|
||||
<FormSubmit label="Log in" />
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,14 +9,17 @@ import PasswordInput from '../components/PasswordInput';
|
||||
import { IconClose } from '../components/icons';
|
||||
|
||||
export default ({ error, handleClose, handleSubmit }) => (
|
||||
<div className="gm-modal-form">
|
||||
<FormHeader title="Sign up" error={error} errorText="Email already registered">
|
||||
<FormHeaderCTA title="Already a member?" label="Log in" hash="#signin" />
|
||||
<div className="gm-modal-form gm-signup-page">
|
||||
<FormHeader title="" error={error} errorText="Email already registered">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<h1>Sign up</h1>
|
||||
<FormHeaderCTA title="Already a member?" label="Log in" hash="#signin" />
|
||||
</div>
|
||||
</FormHeader>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<NameInput bindTo="name" />
|
||||
<NameInput bindTo="name" className="first" />
|
||||
<EmailInput bindTo="email" />
|
||||
<PasswordInput bindTo="password" />
|
||||
<PasswordInput bindTo="password" className="last" />
|
||||
<FormSubmit label="Sign up" />
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -3,13 +3,13 @@ import { Component } from 'react';
|
||||
import FormHeader from '../components/FormHeader';
|
||||
import FormSubmit from '../components/FormSubmit';
|
||||
import FormHeaderCTA from '../components/FormHeaderCTA';
|
||||
import { IconClose } from '../components/icons';
|
||||
import NameInput from '../components/NameInput';
|
||||
import EmailInput from '../components/EmailInput';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
import CheckoutForm from '../components/CheckoutForm';
|
||||
import Form from '../components/Form';
|
||||
|
||||
|
||||
class PaymentForm extends Component {
|
||||
|
||||
constructor(props) {
|
||||
@ -30,12 +30,18 @@ class PaymentForm extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
render({frameLocation}) {
|
||||
return (
|
||||
<Form onSubmit={(data) => this.handleSubmit(data)}>
|
||||
<CheckoutForm />
|
||||
onClick = () => {
|
||||
this.props.stripe.createToken({ name: name });
|
||||
}
|
||||
|
||||
<FormSubmit label="Confirm Payment" />
|
||||
render() {
|
||||
return (
|
||||
<Form bindTo="request-password-reset" onSubmit={(data) => this.handleSubmit(data)}>
|
||||
<NameInput bindTo="name" className="first" />
|
||||
<EmailInput bindTo="email" />
|
||||
<PasswordInput bindTo="password" />
|
||||
<CheckoutForm />
|
||||
<FormSubmit label="Confirm payment" onClick={() => this.onClick()}/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -43,7 +49,7 @@ class PaymentForm extends Component {
|
||||
|
||||
const PaymentFormWrapped = injectStripe(PaymentForm);
|
||||
|
||||
export default class StripeSubscriptionPage extends Component {
|
||||
export default class StripePaymentPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.plans = props.stripeConfig.config.plans || [];
|
||||
@ -54,14 +60,15 @@ export default class StripeSubscriptionPage extends Component {
|
||||
|
||||
renderPlan({ currency, amount, id, interval, name }) {
|
||||
const selectedPlanId = this.state.selectedPlan ? this.state.selectedPlan.id : "";
|
||||
const dollarAmount = (amount / 100);
|
||||
return (
|
||||
<div class="gm-plan">
|
||||
<input type="radio" id={id} name="radio-group" value={id} defaultChecked={id === selectedPlanId} />
|
||||
<label for={id}>
|
||||
<span class="gm-amount">{`$${amount}`}</span>
|
||||
<span class="gm-interval">{`${interval}`}</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for={ id }>
|
||||
<div className={ (selectedPlanId === id ? "gm-plan selected" : "gm-plan") }>
|
||||
<input type="radio" id={id} name="radio-group" value={id} defaultChecked={id === selectedPlanId} />
|
||||
<span className="gm-amount">{`$${dollarAmount}`}</span>
|
||||
<span className="gm-interval"><span className="gm-currency">{ `${currency}` }</span> {`${interval}`}</span>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
@ -72,9 +79,16 @@ export default class StripeSubscriptionPage extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
renderPlans(plans) {
|
||||
renderPlans(plans, title, iconStyle) {
|
||||
return (
|
||||
<div onChange={(e) => this.changePlan(e)}>
|
||||
<div className="gm-plans" onChange={(e) => this.changePlan(e)}>
|
||||
<div className="gm-publication-info">
|
||||
<div className="gm-logo" style={iconStyle}></div>
|
||||
<div className="gm-publication-name">
|
||||
<h2>{title}</h2>
|
||||
<span>Subscription</span>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
plans.map((plan) => this.renderPlan(plan))
|
||||
}
|
||||
@ -82,36 +96,39 @@ export default class StripeSubscriptionPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPlansSection() {
|
||||
renderPlansSection(title, iconStyle) {
|
||||
return (
|
||||
<div className="gm-plans-container">
|
||||
<div className="gm-publication-info">
|
||||
<div className="gm-logo"></div>
|
||||
<div className="gm-publication-name">
|
||||
<h2>Expensive Publication</h2>
|
||||
<span>Subscription</span>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderPlans(this.plans)}
|
||||
{this.renderPlans(this.plans, title, iconStyle)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render({ error, handleSubmit, stripeConfig }) {
|
||||
render({ error, handleSubmit, stripeConfig, siteConfig }) {
|
||||
const publicKey = stripeConfig.config.publicKey || '';
|
||||
let iconUrl = siteConfig && siteConfig.icon;
|
||||
let title = (siteConfig && siteConfig.title) || "Ghost Publication";
|
||||
let iconStyle = iconUrl ? {
|
||||
backgroundImage: `url(${iconUrl})`,
|
||||
backgroundSize: `44px`
|
||||
} : {};
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="gm-modal-form gm-subscribe-form">
|
||||
<FormHeader title="Subscribe" error={error} errorText="Unable to confirm payment">
|
||||
<FormHeaderCTA title="Already a member?" label="Log in" hash="#signin" />
|
||||
</FormHeader>
|
||||
<StripeProvider apiKey={publicKey}>
|
||||
<Elements>
|
||||
<PaymentFormWrapped handleSubmit={handleSubmit} publicKey={publicKey} selectedPlan={this.state.selectedPlan} />
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
<div class="gm-subscribe-page">
|
||||
<div className="gm-subscribe-form-wrapper">
|
||||
<div className="gm-modal-form gm-subscribe-form">
|
||||
<FormHeader title="Subscribe" error={ error } errorText={ error } />
|
||||
<StripeProvider apiKey={publicKey}>
|
||||
<Elements>
|
||||
<PaymentFormWrapped handleSubmit={handleSubmit} publicKey={publicKey} selectedPlan={this.state.selectedPlan} />
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
<div className="flex justify-center mt4">
|
||||
<FormHeaderCTA title="Already a member?" label="Log in" hash="#signin" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="gm-plans-divider"></div>
|
||||
{this.renderPlansSection(title, iconStyle)}
|
||||
</div>
|
||||
{this.renderPlansSection()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ import { Component } from 'react';
|
||||
import FormHeader from '../components/FormHeader';
|
||||
import FormSubmit from '../components/FormSubmit';
|
||||
import FormHeaderCTA from '../components/FormHeaderCTA';
|
||||
import { IconClose } from '../components/icons';
|
||||
import NameInput from '../components/NameInput';
|
||||
import EmailInput from '../components/EmailInput';
|
||||
import PasswordInput from '../components/PasswordInput';
|
||||
import CheckoutForm from '../components/CheckoutForm';
|
||||
import Form from '../components/Form';
|
||||
|
||||
|
||||
class PaymentForm extends Component {
|
||||
|
||||
constructor(props) {
|
||||
@ -30,14 +30,12 @@ class PaymentForm extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
render({frameLocation}) {
|
||||
return (
|
||||
<Form bindTo="request-password-reset" onSubmit={(data) => this.handleSubmit(data)}>
|
||||
<NameInput bindTo="name" />
|
||||
<EmailInput bindTo="email" />
|
||||
<PasswordInput bindTo="password" />
|
||||
<Form onSubmit={(data) => this.handleSubmit(data)}>
|
||||
<CheckoutForm />
|
||||
<FormSubmit label="Confirm payment" />
|
||||
|
||||
<FormSubmit label="Confirm Payment" />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -45,7 +43,7 @@ class PaymentForm extends Component {
|
||||
|
||||
const PaymentFormWrapped = injectStripe(PaymentForm);
|
||||
|
||||
export default class StripePaymentPage extends Component {
|
||||
export default class StripeSubscriptionPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.plans = props.stripeConfig.config.plans || [];
|
||||
@ -56,14 +54,15 @@ export default class StripePaymentPage extends Component {
|
||||
|
||||
renderPlan({ currency, amount, id, interval, name }) {
|
||||
const selectedPlanId = this.state.selectedPlan ? this.state.selectedPlan.id : "";
|
||||
const dollarAmount = (amount / 100);
|
||||
return (
|
||||
<div class="gm-plan">
|
||||
<input type="radio" id={id} name="radio-group" value={id} defaultChecked={id === selectedPlanId} />
|
||||
<label for={id}>
|
||||
<span class="gm-amount">{`$${amount}`}</span>
|
||||
<span class="gm-interval">{`${interval}`}</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for={ id }>
|
||||
<div className={ (selectedPlanId === id ? "gm-plan selected" : "gm-plan") }>
|
||||
<input type="radio" id={ id } name="radio-group" value={ id } defaultChecked={ id === selectedPlanId } />
|
||||
<span className="gm-amount">{ `$${dollarAmount}` }</span>
|
||||
<span className="gm-interval"><span className="gm-currency">{ `${currency}` }</span> { `${interval}` }</span>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
@ -76,7 +75,7 @@ export default class StripePaymentPage extends Component {
|
||||
|
||||
renderPlans(plans) {
|
||||
return (
|
||||
<div className="gm-plans" onChange={(e) => this.changePlan(e)}>
|
||||
<div className="mt3" onChange={(e) => this.changePlan(e)}>
|
||||
{
|
||||
plans.map((plan) => this.renderPlan(plan))
|
||||
}
|
||||
@ -87,13 +86,7 @@ export default class StripePaymentPage extends Component {
|
||||
renderPlansSection() {
|
||||
return (
|
||||
<div className="gm-plans-container">
|
||||
<div className="gm-publication-info">
|
||||
<div className="gm-logo"></div>
|
||||
<div className="gm-publication-name">
|
||||
<h2>Expensive Publication</h2>
|
||||
<span>Subscription</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="gm-form-section">Billing period</h2>
|
||||
{this.renderPlans(this.plans)}
|
||||
</div>
|
||||
)
|
||||
@ -102,18 +95,21 @@ export default class StripePaymentPage extends Component {
|
||||
render({ error, handleSubmit, stripeConfig }) {
|
||||
const publicKey = stripeConfig.config.publicKey || '';
|
||||
return (
|
||||
<div className="flex">
|
||||
<div class="gm-upgrade-page">
|
||||
<div className="gm-modal-form gm-subscribe-form">
|
||||
<FormHeader title="Subscribe" error={error} errorText="Unable to confirm payment">
|
||||
<FormHeaderCTA title="Already a member?" label="Log in" hash="#signin" />
|
||||
</FormHeader>
|
||||
<StripeProvider apiKey={publicKey}>
|
||||
<Elements>
|
||||
<PaymentFormWrapped handleSubmit={handleSubmit} publicKey={publicKey} selectedPlan={this.state.selectedPlan} />
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
<FormHeader title="Upgrade" error={error} errorText="Unable to confirm payment" />
|
||||
<div className="flex flex-column justfiy-stretch mt7">
|
||||
{ this.renderPlansSection() }
|
||||
<div className="mt4 nb3">
|
||||
<h2 className="gm-form-section">Card details</h2>
|
||||
</div>
|
||||
<StripeProvider apiKey={publicKey}>
|
||||
<Elements>
|
||||
<PaymentFormWrapped handleSubmit={handleSubmit} publicKey={publicKey} selectedPlan={this.state.selectedPlan} />
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderPlansSection()}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
overflow-y: scroll;
|
||||
background: rgba(10, 17, 23, 0.9);
|
||||
background: linear-gradient(45deg, rgba(13,25,40,0.96) 0%,rgba(0,0,0,0.96) 100%);
|
||||
animation: fadeInOverlay 0.2s ease 1 forwards;
|
||||
}
|
||||
|
||||
@ -21,36 +21,35 @@
|
||||
}
|
||||
|
||||
.gm-page-overlay.close .gm-modal {
|
||||
animation: closeModal 0.5s ease-in 1 forwards;
|
||||
animation: closeModal 0.4s ease-in-out 1 forwards;
|
||||
}
|
||||
|
||||
.gm-modal-container {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
top: 25vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gm-modal {
|
||||
position: relative;
|
||||
background: white;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--box-shadow-base);
|
||||
animation: openModal 0.6s ease 1 forwards;
|
||||
}
|
||||
|
||||
.gm-modal-form {
|
||||
width: 300px;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.gm-modal-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gm-modal-close svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.gm-modal-close svg path {
|
||||
@ -72,35 +71,25 @@
|
||||
to {opacity: 0;}
|
||||
}
|
||||
|
||||
@keyframes openModal { /* Safari and Chrome */
|
||||
@keyframes openModal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(25px) scale(0.85);
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 1.0;
|
||||
transform: translateY(-8px) scale(1.04);
|
||||
/* opacity: 0; */
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0) scale(1.0);
|
||||
/* opacity: 1.0; */
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes closeModal { /* Safari and Chrome */
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
@keyframes closeModal {
|
||||
90% {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 1.0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(85px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +99,7 @@
|
||||
padding: 0;
|
||||
top: 0;
|
||||
transform: none;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.gm-modal {
|
||||
@ -121,7 +110,7 @@
|
||||
}
|
||||
|
||||
.gm-page-overlay {
|
||||
background: rgba(10, 17, 23, 0.0);
|
||||
background: linear-gradient(45deg, rgba(13,25,40,0.96) 0%,rgba(0,0,0,0.96) 100%);
|
||||
}
|
||||
|
||||
@keyframes openModal {
|
||||
@ -163,8 +152,8 @@ button {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
padding: 13px 16px 14px;
|
||||
border-radius: 4px;
|
||||
padding: 15px 16px 16px;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
transition: all var(--animation-speed-f1) ease-in-out;
|
||||
position: relative;
|
||||
@ -231,107 +220,87 @@ select:-webkit-autofill:active {
|
||||
::-moz-placeholder,
|
||||
:-ms-input-placeholder,
|
||||
:-moz-placeholder {
|
||||
color: var(--grey);
|
||||
color: var(--grey-d1);
|
||||
}
|
||||
|
||||
.gm-form-element {
|
||||
margin: 24px 0 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gm-input {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--grey-l1);
|
||||
}
|
||||
|
||||
.gm-input.last,
|
||||
.gm-input.single {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.gm-input.first input {
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.gm-input.last input {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
|
||||
.gm-input.single input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.gm-input input {
|
||||
font-size: var(--text-s);
|
||||
font-size: var(--text-base);
|
||||
color: var(--grey-d3);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--grey-l1);
|
||||
-webkit-appearance: none;
|
||||
box-sizing: border-box;
|
||||
background: var(--white);
|
||||
width: 100%;
|
||||
outline: none;
|
||||
transition: border var(--animation-speed-f1) ease-in-out;
|
||||
padding: 12px 14px 12px 38px; /* 1px bottom padding fixes jump that's caused by the border change */
|
||||
padding: 15px 14px 15px 38px;
|
||||
letter-spacing: 0.2px;
|
||||
vertical-align: middle;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.gm-input input:hover {
|
||||
border: 1px solid var(--grey);
|
||||
}
|
||||
|
||||
.gm-input input.gm-error {
|
||||
border: 1px solid color-mod(var(--red) a(0.8));
|
||||
background: color-mod(var(--red) a(0.02))
|
||||
}
|
||||
|
||||
.gm-input input:focus {
|
||||
border: 1px solid color-mod(var(--blue) a(0.8));
|
||||
box-shadow: 0 0 6px rgba(62, 176, 239, 0.3), 0 0 0px 40px #FFF inset;
|
||||
|
||||
}
|
||||
|
||||
.gm-input label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
font-size: var(--text-s);
|
||||
padding: 0 0 2px 0;
|
||||
width: 100%;
|
||||
top: 14px;
|
||||
left: 38px;
|
||||
color: var(--grey);
|
||||
transition: all var(--animation-speed-base) ease-in-out;
|
||||
pointer-events: none;
|
||||
letter-spacing: 0.4px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.gm-input input:hover + label {
|
||||
color: var(--grey-d1);
|
||||
}
|
||||
|
||||
.gm-input input.gm-input-filled + label,
|
||||
.gm-input input:focus + label {
|
||||
opacity: 0;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.gm-input input.gm-error + label {
|
||||
color: color-mod(var(--red) a(0.5));
|
||||
}
|
||||
|
||||
.gm-input label i svg {
|
||||
.gm-input i svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.gm-input label i svg path,
|
||||
.gm-input i svg path {
|
||||
stroke: var(--grey);
|
||||
stroke: var(--grey-d1);
|
||||
transition: stroke var(--animation-speed-base) ease-in-out;
|
||||
}
|
||||
|
||||
.gm-input input:hover + label + i svg path {
|
||||
stroke: var(--grey-d1);
|
||||
.gm-input input.gm-error {
|
||||
background: #FEEBE6;
|
||||
box-shadow: 0 0 1px rgba(255, 0, 0, 1);
|
||||
}
|
||||
|
||||
.gm-input input.gm-error + label + i svg path {
|
||||
.gm-input input.gm-error + i svg path {
|
||||
stroke: color-mod(var(--red) a(0.5));
|
||||
}
|
||||
|
||||
.gm-input input.gm-input-filled + label + i svg path,
|
||||
.gm-input input:focus + label + i svg path {
|
||||
.gm-input input.gm-error::-webkit-input-placeholder,
|
||||
.gm-input input.gm-error::-moz-placeholder,
|
||||
.gm-input input.gm-error:-ms-input-placeholder,
|
||||
.gm-input input.gm-error:-moz-placeholder {
|
||||
color: color-mod(var(--red) a(0.75));
|
||||
}
|
||||
|
||||
.gm-input input.gm-input-filled + i svg path {
|
||||
stroke: var(--grey-d2);
|
||||
}
|
||||
|
||||
.gm-input i {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
top: 16px;
|
||||
left: 14px;
|
||||
opacity: 1.0;
|
||||
transition: all var(--animation-speed-base) ease-in-out;
|
||||
@ -377,13 +346,12 @@ select:-webkit-autofill:active {
|
||||
}
|
||||
|
||||
.gm-form-errortext {
|
||||
color: var(--red);
|
||||
font-size: var(--text-s);
|
||||
color: #FF7254;
|
||||
letter-spacing: 0.4px;
|
||||
margin: 40px -2px -12px;
|
||||
background: color-mod(var(--red) a(0.08));
|
||||
padding: 11px 14px;
|
||||
font-weight: 500;
|
||||
margin: 24px -2px 0px;
|
||||
border: 1px solid color-mod(var(--red) a(0.4));
|
||||
padding: 13px 14px;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
@ -396,4 +364,8 @@ select:-webkit-autofill:active {
|
||||
|
||||
.gm-form-errortext i {
|
||||
margin: 1px 8px 0 0;
|
||||
}
|
||||
|
||||
.gm-form-errortext i svg path {
|
||||
stroke: #FF7254;
|
||||
}
|
@ -6,7 +6,7 @@ html {
|
||||
|
||||
html, body {
|
||||
font-family: var(--default-font);
|
||||
color: var(--black);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
body {
|
||||
@ -22,15 +22,13 @@ p {
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--grey-d3);
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--grey-d3);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
@ -57,10 +55,28 @@ a:hover {
|
||||
.gm-logo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
margin: 0 auto;
|
||||
background: #343F44 url('../assets/images/ghost-logo.svg') center center no-repeat;
|
||||
}
|
||||
|
||||
.gm-modal-header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gm-modal-header .gm-logo {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
margin: 0 12px 0 0;
|
||||
}
|
||||
|
||||
.gm-modal-header h2 {
|
||||
font-size: var(--text-l);
|
||||
color: var(--grey-l1);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.gm-auth-header,
|
||||
@ -72,21 +88,33 @@ a:hover {
|
||||
}
|
||||
|
||||
.gm-auth-header h1 {
|
||||
font-size: var(--text-2xl);
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 500;
|
||||
color: var(--white);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gm-auth-header h4,
|
||||
.gm-auth-footer h4 {
|
||||
.gm-auth-footer {
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
|
||||
.gm-auth-cta {
|
||||
margin: 5px 0 0;
|
||||
font-size: var(--text-s);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gm-auth-cta h4 {
|
||||
font-weight: normal;
|
||||
font-size: var(--text-s);
|
||||
letter-spacing: 0.4px;
|
||||
color: var(--grey-d1);
|
||||
}
|
||||
|
||||
.gm-auth-header a,
|
||||
.gm-auth-footer a {
|
||||
.gm-auth-cta a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-s);
|
||||
letter-spacing: 0.4px;
|
||||
padding: 8px;
|
||||
margin: -8px -8px -8px -2px;
|
||||
@ -95,116 +123,203 @@ a:hover {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gm-auth-header a:hover,
|
||||
.gm-auth-footer a:hover {
|
||||
.gm-auth-cta a:hover {
|
||||
color: var(--blue-d3);
|
||||
}
|
||||
|
||||
.gm-auth-header a svg {
|
||||
.gm-auth-cta a svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.gm-auth-header a svg g,
|
||||
.gm-auth-header a svg path {
|
||||
.gm-auth-cta a svg g,
|
||||
.gm-auth-cta a svg path {
|
||||
fill: var(--blue);
|
||||
transition: fill var(--animation-speed-f1) ease-in-out;
|
||||
}
|
||||
|
||||
.gm-auth-header a:hover svg g,
|
||||
.gm-auth-header a:hover svg path {
|
||||
.gm-auth-cta a:hover svg g,
|
||||
.gm-auth-cta a:hover svg path {
|
||||
fill: var(--blue-d3);
|
||||
}
|
||||
|
||||
.gm-auth-footer {
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
|
||||
.gm-forgot-link {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
z-index: 9999;
|
||||
font-size: var(--text-s);
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.gm-reset-sent {
|
||||
margin: 24px 0 0;
|
||||
background: color-mod(var(--green) a(0.2));
|
||||
border-radius: 4px;
|
||||
color: color-mod(var(--green) l(-30%) s(+8%));
|
||||
padding: 12px 14px 14px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
color: var(--grey-l1);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
/* Custom forms */
|
||||
.gm-form-section {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: var(--text-l);
|
||||
font-weight: 400;
|
||||
color: var(--grey-d1);
|
||||
}
|
||||
|
||||
.gm-floating-input .gm-forgot-input {
|
||||
padding-right: 60px;
|
||||
}
|
||||
|
||||
.gm-modal-form {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
.gm-modal-form {
|
||||
width: 100%;
|
||||
/* margin-top: 60px; */
|
||||
}
|
||||
|
||||
h4 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pages */
|
||||
@keyframes fadeInPage {
|
||||
from {opacity: 0;}
|
||||
to {opacity: 1;}
|
||||
}
|
||||
|
||||
@keyframes fadeOutPage {
|
||||
from {opacity: 1;}
|
||||
to {opacity: 0;}
|
||||
}
|
||||
|
||||
@keyframes showPlans {
|
||||
from {left: -280px;}
|
||||
to {left: 40px;}
|
||||
}
|
||||
|
||||
@keyframes showPlansContainer {
|
||||
from {width: 0px;}
|
||||
to {width: 380px;}
|
||||
}
|
||||
|
||||
@keyframes showPlansDivider {
|
||||
from {margin-left: -1px; opacity: 0;}
|
||||
to {margin-left: 40px; opacity: 0.3;}
|
||||
}
|
||||
|
||||
|
||||
/* Subscribe page */
|
||||
.gm-signin-page,
|
||||
.gm-signup-page,
|
||||
.gm-subscribe-page,
|
||||
.gm-reset-pwd-page {
|
||||
animation: fadeInPage 0.6s ease 1;
|
||||
}
|
||||
|
||||
.gm-subscribe-header {
|
||||
display: flex;
|
||||
width: 340px;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.gm-subscribe-form-wrapper {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.gm-subscribe-form {
|
||||
width: 340px;
|
||||
}
|
||||
|
||||
.gm-plans-container {
|
||||
background: var(--grey-l3);
|
||||
border-left: 1px solid var(--grey-l2);
|
||||
padding: 48px;
|
||||
min-width: 240px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
padding: 0;
|
||||
width: 0px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-items: start;
|
||||
animation: showPlansContainer 0.6s ease 0.1s forwards;
|
||||
}
|
||||
|
||||
.gm-upgrade-page .gm-plans-container {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
flex-direction: column;
|
||||
animation: none;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.gm-plans-divider {
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 20%,rgba(255,255,255,1) 80%,rgba(255,255,255,0) 100%);
|
||||
width: 1px;
|
||||
margin-left: -1px;
|
||||
opacity: 0;
|
||||
animation: showPlansDivider 0.6s ease 0.1s forwards;
|
||||
}
|
||||
|
||||
.gm-signin-page .gm-publication-name h2 {
|
||||
margin: 0;
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gm-plans-container .gm-publication-name {
|
||||
margin: 0 0 0 12px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gm-plans-container .gm-publication-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin: 92px 0 0;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.gm-publication-name {
|
||||
margin: 0 0 0 12px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gm-publication-name h2 {
|
||||
.gm-plans-container .gm-publication-name h2 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: var(--text-l);
|
||||
color: var(--grey-d3);
|
||||
color: var(--white);
|
||||
word-wrap: none;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.gm-publication-name span {
|
||||
.gm-plans-container .gm-publication-name span {
|
||||
color: var(--grey-d1);
|
||||
font-size: var(--text-s);
|
||||
}
|
||||
|
||||
.gm-plans {
|
||||
border: 1px solid var(--grey-l1);
|
||||
border-radius: 8px;
|
||||
margin: 26px 0 0;
|
||||
width: 270px;
|
||||
margin: 70px 0 0;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -270px;
|
||||
animation: showPlans 0.75s ease 0.25s forwards;
|
||||
}
|
||||
|
||||
.gm-plan {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--grey-l1);
|
||||
margin: 0;
|
||||
padding: 13px 18px;
|
||||
border: 1px solid var(--grey-d3);
|
||||
margin: 0 0 20px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.gm-plan:last-child {
|
||||
border-bottom: none;
|
||||
.gm-plan.selected {
|
||||
background: color-mod(var(--blue) alpha(15%));
|
||||
border: 1px solid color-mod(var(--blue) alpha(80%));
|
||||
}
|
||||
|
||||
.gm-plan input[type="radio"] {
|
||||
@ -222,7 +337,7 @@ a:hover {
|
||||
font-size: var(--text-s);
|
||||
color: var(--grey-d1);
|
||||
padding: 0 0 0 9px;
|
||||
margin: 0 0 0 6px;
|
||||
margin: 8px 0 0 6px;
|
||||
}
|
||||
|
||||
.gm-plan .gm-interval::before {
|
||||
@ -237,16 +352,80 @@ a:hover {
|
||||
transform: rotate(25deg);
|
||||
}
|
||||
|
||||
.gm-plan .gm-currency {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
.gm-subscribe-form-wrapper {
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.gm-subscribe-form {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gm-plans-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gm-plans-container {
|
||||
order: 1;
|
||||
width: 100%;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.gm-plans {
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
animation: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gm-plans .gm-publication-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gm-auth-header {
|
||||
margin-top: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.gm-powered-by {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 32px;
|
||||
}
|
||||
|
||||
.gm-powered-by a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gm-powered-by svg {
|
||||
height: 18px;
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
.gm-powered-by span {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 1px;
|
||||
color: var(--grey-d2);
|
||||
}
|
||||
|
||||
/**
|
||||
* The CSS shown here will not be introduced in the Quickstart guide, but shows
|
||||
* how you can use CSS to style your Element's container.
|
||||
*/
|
||||
.StripeElement {
|
||||
font-size: var(--text-s);
|
||||
color: var(--grey-d3);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--grey-l1);
|
||||
border-radius: 0 0 6px 6px;
|
||||
-webkit-appearance: none;
|
||||
box-sizing: border-box;
|
||||
background: var(--white);
|
||||
@ -255,23 +434,21 @@ a:hover {
|
||||
transition: border var(--animation-speed-f1) ease-in-out;
|
||||
letter-spacing: 0.2px;
|
||||
line-height: 18px;
|
||||
padding: 14px 12px 14px 10px;
|
||||
}
|
||||
|
||||
.StripeElement:hover {
|
||||
border: 1px solid var(--grey);
|
||||
padding: 16px 12px 16px 10px;
|
||||
}
|
||||
|
||||
.StripeElement--focus {
|
||||
border: 1px solid color-mod(var(--blue) a(0.8));
|
||||
box-shadow: 0 0 6px rgba(62, 176, 239, 0.3), 0 0 0px 40px #FFF inset;
|
||||
/* box-shadow: 0 0 6px rgba(62, 176, 239, 0.3), 0 0 0px 40px #FFF inset; */
|
||||
}
|
||||
|
||||
.StripeElement--invalid {
|
||||
border: 1px solid color-mod(var(--red) a(0.8));
|
||||
background: color-mod(var(--red) a(0.02))
|
||||
background: #FEEBE6;
|
||||
}
|
||||
|
||||
.StripeElement--webkit-autofill {
|
||||
background-color: #fefde5 !important;
|
||||
}
|
||||
|
||||
.gm-upgrade-page .StripeElement {
|
||||
border-radius: 6px;
|
||||
}
|
@ -7,14 +7,13 @@ const config = require('../config');
|
||||
let labs = module.exports = {};
|
||||
|
||||
labs.isSet = function isSet(flag) {
|
||||
var labsConfig = settingsCache.get('labs');
|
||||
/**
|
||||
* TODO: Uses hard-check for members prototype, removed here when added to settings
|
||||
*/
|
||||
if (flag === 'members' && config.get('enableDeveloperExperiments')) {
|
||||
return true;
|
||||
if (flag === 'members') {
|
||||
return config.get('enableDeveloperExperiments') && labsConfig && labsConfig[flag] && labsConfig[flag] === true;
|
||||
}
|
||||
|
||||
var labsConfig = settingsCache.get('labs');
|
||||
return labsConfig && labsConfig[flag] && labsConfig[flag] === true;
|
||||
};
|
||||
|
||||
|
@ -2,8 +2,10 @@ const url = require('url');
|
||||
const settingsCache = require('../settings/cache');
|
||||
const config = require('../../config');
|
||||
const MembersApi = require('../../lib/members');
|
||||
const common = require('../../lib/common');
|
||||
const models = require('../../models');
|
||||
const mail = require('../mail');
|
||||
const blogIcon = require('../../lib/image/blog-icon');
|
||||
|
||||
function createMember({name, email, password}) {
|
||||
return models.Member.add({
|
||||
@ -59,6 +61,43 @@ function validateMember({email, password}) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseMembersSettings() {
|
||||
let membersSettings = settingsCache.get('members_subscription_settings');
|
||||
if (!membersSettings) {
|
||||
membersSettings = {
|
||||
isPaid: false,
|
||||
paymentProcessors: [{
|
||||
adapter: 'stripe',
|
||||
config: {
|
||||
secret_token: '',
|
||||
public_token: '',
|
||||
product: {
|
||||
name: 'Ghost Subscription'
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
name: 'Monthly',
|
||||
currency: 'usd',
|
||||
interval: 'month',
|
||||
amount: ''
|
||||
},
|
||||
{
|
||||
name: 'Yearly',
|
||||
currency: 'usd',
|
||||
interval: 'year',
|
||||
amount: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
if (!membersSettings.isPaid) {
|
||||
membersSettings.paymentProcessors = [];
|
||||
}
|
||||
return membersSettings;
|
||||
}
|
||||
|
||||
const publicKey = settingsCache.get('members_public_key');
|
||||
const privateKey = settingsCache.get('members_private_key');
|
||||
const sessionSecret = settingsCache.get('members_session_secret');
|
||||
@ -70,6 +109,7 @@ const ssoOrigin = siteOrigin;
|
||||
let mailer;
|
||||
|
||||
const membersConfig = config.get('members');
|
||||
const membersSettings = parseMembersSettings();
|
||||
|
||||
function validateAudience({audience, origin}) {
|
||||
if (audience === origin) {
|
||||
@ -110,6 +150,8 @@ function sendEmail(member, {token}) {
|
||||
});
|
||||
}
|
||||
|
||||
const defaultBlogTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : 'Publication';
|
||||
const blogIconUrl = blogIcon.getIconUrl();
|
||||
const api = MembersApi({
|
||||
authConfig: {
|
||||
issuer,
|
||||
@ -119,7 +161,11 @@ const api = MembersApi({
|
||||
ssoOrigin
|
||||
},
|
||||
paymentConfig: {
|
||||
processors: membersConfig.paymentProcessors
|
||||
processors: membersSettings.paymentProcessors
|
||||
},
|
||||
siteConfig: {
|
||||
title: defaultBlogTitle,
|
||||
icon: blogIconUrl
|
||||
},
|
||||
validateAudience,
|
||||
createMember,
|
||||
@ -130,6 +176,31 @@ const api = MembersApi({
|
||||
sendEmail
|
||||
});
|
||||
|
||||
const updateSettingFromModel = function updateSettingFromModel(settingModel) {
|
||||
if (settingModel.get('key') === 'members_subscription_settings'
|
||||
|| settingModel.get('key') === 'title'
|
||||
|| settingModel.get('key') === 'icon') {
|
||||
let membersSettings = parseMembersSettings();
|
||||
const defaultBlogTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : 'Publication';
|
||||
const blogIconUrl = blogIcon.getIconUrl();
|
||||
api.reconfigureSettings({
|
||||
paymentConfig: {
|
||||
processors: membersSettings.paymentProcessors
|
||||
},
|
||||
siteConfig: {
|
||||
title: defaultBlogTitle,
|
||||
icon: blogIconUrl
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Bind to events to automatically keep subscription info up-to-date from settings
|
||||
common.events.on('settings.edited', updateSettingFromModel);
|
||||
|
||||
module.exports = api;
|
||||
module.exports.publicKey = publicKey;
|
||||
module.exports.paymentConfigured = !!membersConfig.paymentProcessors.length;
|
||||
module.exports.isPaymentConfigured = function () {
|
||||
let membersSettings = parseMembersSettings();
|
||||
return !!membersSettings.paymentProcessors.length;
|
||||
};
|
@ -1,8 +1,17 @@
|
||||
const labs = require('../labs');
|
||||
const config = require('../../config/index.js');
|
||||
const common = require('../../lib/common');
|
||||
|
||||
module.exports = {
|
||||
get api() {
|
||||
if (!labs.isSet('members')) {
|
||||
return {};
|
||||
if (!config.get('enableDeveloperExperiments')) {
|
||||
return {
|
||||
apiRouter: function (req, res, next) {
|
||||
return next(new common.errors.NotFoundError());
|
||||
},
|
||||
staticRouter: function (req, res, next) {
|
||||
return next(new common.errors.NotFoundError());
|
||||
}
|
||||
};
|
||||
}
|
||||
return require('./api');
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ const urlUtils = require('../../services/url/utils');
|
||||
const errorHandler = require('../shared/middlewares/error-handler');
|
||||
const membersService = require('../../services/members');
|
||||
|
||||
const labs = require('../../services/labs');
|
||||
const labs = require('../shared/middlewares/labs');
|
||||
|
||||
module.exports = function setupApiApp() {
|
||||
debug('Parent API setup start');
|
||||
@ -14,9 +14,7 @@ module.exports = function setupApiApp() {
|
||||
apiApp.use(urlUtils.getVersionPath({version: 'v0.1'}), require('./v0.1/app')());
|
||||
apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'content'}), require('./v2/content/app')());
|
||||
apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'admin'}), require('./v2/admin/app')());
|
||||
if (labs.isSet('members')) {
|
||||
apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'members'}), membersService.api.apiRouter);
|
||||
}
|
||||
apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'members'}), labs.members, membersService.api.apiRouter);
|
||||
|
||||
// Error handling for requests to non-existent API versions
|
||||
apiApp.use(errorHandler.resourceNotFound);
|
||||
|
@ -4,7 +4,7 @@ const config = require('../config');
|
||||
const compress = require('compression');
|
||||
const netjet = require('netjet');
|
||||
const shared = require('./shared');
|
||||
const labs = require('../services/labs');
|
||||
const labs = require('./shared/middlewares/labs');
|
||||
const membersService = require('../services/members');
|
||||
|
||||
module.exports = function setupParentApp(options = {}) {
|
||||
@ -49,9 +49,7 @@ module.exports = function setupParentApp(options = {}) {
|
||||
parentApp.use('/ghost', require('./admin')());
|
||||
|
||||
// MEMBERS
|
||||
if (labs.isSet('members')) {
|
||||
parentApp.use('/members', membersService.api.staticRouter);
|
||||
}
|
||||
parentApp.use('/members', labs.members, membersService.api.staticRouter);
|
||||
|
||||
// BLOG
|
||||
parentApp.use(require('./site')(options));
|
||||
|
Loading…
Reference in New Issue
Block a user