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:
Rish 2019-02-26 10:09:16 +07:00 committed by Rishabh Garg
parent 90aef4f6c9
commit 20a898a986
26 changed files with 634 additions and 322 deletions

View File

@ -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) {

View File

@ -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\":\"\"}]}}]}"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,3 +22,7 @@ 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>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
@ -397,3 +365,7 @@ select:-webkit-autofill:active {
.gm-form-errortext i {
margin: 1px 8px 0 0;
}
.gm-form-errortext i svg path {
stroke: #FF7254;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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