diff --git a/core/server/api/v2/utils/serializers/output/utils/members.js b/core/server/api/v2/utils/serializers/output/utils/members.js index 8a4a5b25b6..edcf2b682f 100644 --- a/core/server/api/v2/utils/serializers/output/utils/members.js +++ b/core/server/api/v2/utils/serializers/output/utils/members.js @@ -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) { diff --git a/core/server/data/schema/default-settings.json b/core/server/data/schema/default-settings.json index 87bd6449ac..bae986a012 100644 --- a/core/server/data/schema/default-settings.json +++ b/core/server/data/schema/default-settings.json @@ -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\":\"\"}]}}]}" } } } diff --git a/core/server/lib/members/index.js b/core/server/lib/members/index.js index c9e2a7abca..65184b70da 100644 --- a/core/server/lib/members/index.js +++ b/core/server/lib/members/index.js @@ -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; }; diff --git a/core/server/lib/members/static/auth/components/CheckoutForm.js b/core/server/lib/members/static/auth/components/CheckoutForm.js index a4c857c7ad..ba88e88499 100644 --- a/core/server/lib/members/static/auth/components/CheckoutForm.js +++ b/core/server/lib/members/static/auth/components/CheckoutForm.js @@ -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 ( -
- -
- ); - } + render() { + let style = { + base: { + '::placeholder': { + color: '#8795A1', + fontSize: '15px' + } + }, + invalid: { + '::placeholder': { + color: 'rgba(240, 82, 48, 0.75)' + } + } + }; + return ( +
+ +
+ ); + } } export default CheckoutForm; diff --git a/core/server/lib/members/static/auth/components/Form.js b/core/server/lib/members/static/auth/components/Form.js index c02661c15e..496ac6f4f6 100644 --- a/core/server/lib/members/static/auth/components/Form.js +++ b/core/server/lib/members/static/auth/components/Form.js @@ -72,7 +72,7 @@ export default class Form extends Component { } const data = state.data; return ( -
+
{ this.wrapChildren(children, data, onInput) }
diff --git a/core/server/lib/members/static/auth/components/FormHeader.js b/core/server/lib/members/static/auth/components/FormHeader.js index b438fc632a..a13836f5ea 100644 --- a/core/server/lib/members/static/auth/components/FormHeader.js +++ b/core/server/lib/members/static/auth/components/FormHeader.js @@ -3,7 +3,7 @@ import { IconError } from './icons'; export default ({title, error, errorText, children}) => (
-

{ title }

+ { title ? (

{title}

) : "" } { children }
{(error ? diff --git a/core/server/lib/members/static/auth/components/FormHeaderCTA.js b/core/server/lib/members/static/auth/components/FormHeaderCTA.js index 170649291f..3f3888e7b1 100644 --- a/core/server/lib/members/static/auth/components/FormHeaderCTA.js +++ b/core/server/lib/members/static/auth/components/FormHeaderCTA.js @@ -1,11 +1,9 @@ -import { IconRightArrow } from './icons'; - -export default ({title, label, hash}) => ( -
-

{ title }

+export default ({title, label, icon, hash}) => ( +
+ {title ? (

{ title }

) : ""} { label } - { IconRightArrow } + { icon }
); diff --git a/core/server/lib/members/static/auth/components/FormInput.js b/core/server/lib/members/static/auth/components/FormInput.js index 8eb1e12272..a60c82f733 100644 --- a/core/server/lib/members/static/auth/components/FormInput.js +++ b/core/server/lib/members/static/auth/components/FormInput.js @@ -1,6 +1,9 @@ export default ({type, name, placeholder, value = '', error, onInput, required, className, children, icon}) => (
-
+
{ icon } diff --git a/core/server/lib/members/static/auth/components/FormSubmit.js b/core/server/lib/members/static/auth/components/FormSubmit.js index 8b200be734..3a6ce75328 100644 --- a/core/server/lib/members/static/auth/components/FormSubmit.js +++ b/core/server/lib/members/static/auth/components/FormSubmit.js @@ -1,5 +1,5 @@ export default ({onClick, label}) => ( -
+
diff --git a/core/server/lib/members/static/auth/components/Modal.js b/core/server/lib/members/static/auth/components/Modal.js index dc12c4e9d9..32887f2ae6 100644 --- a/core/server/lib/members/static/auth/components/Modal.js +++ b/core/server/lib/members/static/auth/components/Modal.js @@ -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 + 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 } return ( @@ -67,12 +71,12 @@ export default class Modal extends Component { members.createSubscription(data) ); const stripeConfig = paymentConfig && paymentConfig.find(({adapter}) => adapter === 'stripe'); - return + return } 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 ( - + - {this.renderSignupPage({error, stripeConfig, members, signup, closeModal})} + {this.renderSignupPage({error, stripeConfig, members, signup, closeModal, siteConfig})} {this.renderUpgradePage(props, state)} - + ); diff --git a/core/server/lib/members/static/auth/components/Pages.js b/core/server/lib/members/static/auth/components/Pages.js index c847d7916b..437b01fe0c 100644 --- a/core/server/lib/members/static/auth/components/Pages.js +++ b/core/server/lib/members/static/auth/components/Pages.js @@ -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 ( -
+
+
+
+

{title}

+
+
{IconClose}
e.stopPropagation()}> {this.filterChildren(children, state)}
+
); } diff --git a/core/server/lib/members/static/auth/components/icons.js b/core/server/lib/members/static/auth/components/icons.js index 4093a92867..d767ab78d1 100644 --- a/core/server/lib/members/static/auth/components/icons.js +++ b/core/server/lib/members/static/auth/components/icons.js @@ -21,4 +21,8 @@ export const IconError = ( export const IconRightArrow = ( +); + +export const GhostLogo = ( + ghost-logo ); \ No newline at end of file diff --git a/core/server/lib/members/static/auth/pages/PasswordResetSentPage.js b/core/server/lib/members/static/auth/pages/PasswordResetSentPage.js index f4aa6e3cc5..e5eff70df6 100644 --- a/core/server/lib/members/static/auth/pages/PasswordResetSentPage.js +++ b/core/server/lib/members/static/auth/pages/PasswordResetSentPage.js @@ -1,14 +1,16 @@ import FormHeader from '../components/FormHeader'; import FormSubmit from '../components/FormSubmit'; +import Form from '../components/Form'; + export default ({ error, handleSubmit }) => (
- +

We’ve sent a recovery email to your inbox. Follow the link in the email to reset your password.

- +
); diff --git a/core/server/lib/members/static/auth/pages/RequestPasswordResetPage.js b/core/server/lib/members/static/auth/pages/RequestPasswordResetPage.js index 55204e99c8..78c9b1632e 100644 --- a/core/server/lib/members/static/auth/pages/RequestPasswordResetPage.js +++ b/core/server/lib/members/static/auth/pages/RequestPasswordResetPage.js @@ -5,11 +5,14 @@ import FormSubmit from '../components/FormSubmit'; import Form from '../components/Form'; export default ({ error, handleClose, handleSubmit }) => ( -
+
- + +
+ Cancel +
); diff --git a/core/server/lib/members/static/auth/pages/ResetPasswordPage.js b/core/server/lib/members/static/auth/pages/ResetPasswordPage.js index e75fa196cb..640cb55582 100644 --- a/core/server/lib/members/static/auth/pages/ResetPasswordPage.js +++ b/core/server/lib/members/static/auth/pages/ResetPasswordPage.js @@ -14,7 +14,7 @@ export default ({ error, frameLocation, handleSubmit }) => (
- +
diff --git a/core/server/lib/members/static/auth/pages/SigninPage.js b/core/server/lib/members/static/auth/pages/SigninPage.js index 0c7dc680af..e76dec9329 100644 --- a/core/server/lib/members/static/auth/pages/SigninPage.js +++ b/core/server/lib/members/static/auth/pages/SigninPage.js @@ -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 }) => ( -
- - - -
- - - - Forgot + ); diff --git a/core/server/lib/members/static/auth/pages/SignupPage.js b/core/server/lib/members/static/auth/pages/SignupPage.js index 3fcb8eaff8..078e636555 100644 --- a/core/server/lib/members/static/auth/pages/SignupPage.js +++ b/core/server/lib/members/static/auth/pages/SignupPage.js @@ -9,14 +9,17 @@ import PasswordInput from '../components/PasswordInput'; import { IconClose } from '../components/icons'; export default ({ error, handleClose, handleSubmit }) => ( -
- - +
+ +
+

Sign up

+ +
- + - +
diff --git a/core/server/lib/members/static/auth/pages/StripeSubscribePage.js b/core/server/lib/members/static/auth/pages/StripeSubscribePage.js index cd0521b502..29ee1efe35 100644 --- a/core/server/lib/members/static/auth/pages/StripeSubscribePage.js +++ b/core/server/lib/members/static/auth/pages/StripeSubscribePage.js @@ -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 ( -
this.handleSubmit(data)}> - + onClick = () => { + this.props.stripe.createToken({ name: name }); + } - + render() { + return ( + this.handleSubmit(data)}> + + + + + this.onClick()}/> ); } @@ -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 ( -
- - -
+ ) } @@ -72,9 +79,16 @@ export default class StripeSubscriptionPage extends Component { }) } - renderPlans(plans) { + renderPlans(plans, title, iconStyle) { return ( -
this.changePlan(e)}> +
this.changePlan(e)}> +
+
+
+

{title}

+ Subscription +
+
{ plans.map((plan) => this.renderPlan(plan)) } @@ -82,36 +96,39 @@ export default class StripeSubscriptionPage extends Component { ); } - renderPlansSection() { + renderPlansSection(title, iconStyle) { return (
-
-
-
-

Expensive Publication

- Subscription -
-
- {this.renderPlans(this.plans)} + {this.renderPlans(this.plans, title, iconStyle)}
) } - 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 ( -
-
- - - - - - - - + ) } diff --git a/core/server/lib/members/static/auth/pages/StripePaymentPage.js b/core/server/lib/members/static/auth/pages/StripeUpgradePage.js similarity index 60% rename from core/server/lib/members/static/auth/pages/StripePaymentPage.js rename to core/server/lib/members/static/auth/pages/StripeUpgradePage.js index c98aa56ae1..90fdc8c550 100644 --- a/core/server/lib/members/static/auth/pages/StripePaymentPage.js +++ b/core/server/lib/members/static/auth/pages/StripeUpgradePage.js @@ -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 ( -
this.handleSubmit(data)}> - - - + this.handleSubmit(data)}> - + + ); } @@ -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 ( -
- - -
+ ) } @@ -76,7 +75,7 @@ export default class StripePaymentPage extends Component { renderPlans(plans) { return ( -
this.changePlan(e)}> +
this.changePlan(e)}> { plans.map((plan) => this.renderPlan(plan)) } @@ -87,13 +86,7 @@ export default class StripePaymentPage extends Component { renderPlansSection() { return (
-
-
-
-

Expensive Publication

- Subscription -
-
+

Billing period

{this.renderPlans(this.plans)}
) @@ -102,18 +95,21 @@ export default class StripePaymentPage extends Component { render({ error, handleSubmit, stripeConfig }) { const publicKey = stripeConfig.config.publicKey || ''; return ( -
+
- - - - - - - - + +
+ { this.renderPlansSection() } +
+

Card details

+
+ + + + + +
- {this.renderPlansSection()}
) } diff --git a/core/server/lib/members/static/auth/styles/components.css b/core/server/lib/members/static/auth/styles/components.css index 84b08d8007..6775ae3ba6 100644 --- a/core/server/lib/members/static/auth/styles/components.css +++ b/core/server/lib/members/static/auth/styles/components.css @@ -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; } \ No newline at end of file diff --git a/core/server/lib/members/static/auth/styles/screen.css b/core/server/lib/members/static/auth/styles/screen.css index d8499b7d6e..93f5443c95 100644 --- a/core/server/lib/members/static/auth/styles/screen.css +++ b/core/server/lib/members/static/auth/styles/screen.css @@ -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; } \ No newline at end of file diff --git a/core/server/services/labs.js b/core/server/services/labs.js index ed905997fa..fb88d4b941 100644 --- a/core/server/services/labs.js +++ b/core/server/services/labs.js @@ -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; }; diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index 7e149e41be..3a3eea793f 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -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; +}; \ No newline at end of file diff --git a/core/server/services/members/index.js b/core/server/services/members/index.js index 7a291e280c..44c7e4aa0c 100644 --- a/core/server/services/members/index.js +++ b/core/server/services/members/index.js @@ -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'); } diff --git a/core/server/web/api/index.js b/core/server/web/api/index.js index 4adf90cff9..2d2d433e5b 100644 --- a/core/server/web/api/index.js +++ b/core/server/web/api/index.js @@ -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); diff --git a/core/server/web/parent-app.js b/core/server/web/parent-app.js index b9b28bef5a..1673792d18 100644 --- a/core/server/web/parent-app.js +++ b/core/server/web/parent-app.js @@ -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));