Updated signup page for members (#10493)

no issue

* Added new subscribe page with stripe integration
This commit is contained in:
Rishabh Garg 2019-02-14 22:29:41 +05:30 committed by Fabien O'Carroll
parent 464caaf5df
commit beeedf7005
11 changed files with 339 additions and 20 deletions

View File

@ -0,0 +1,18 @@
import React, {Component} from 'react';
import {CardElement} from 'react-stripe-elements';
class CheckoutForm extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="gm-form-element">
<CardElement />
</div>
);
}
}
export default CheckoutForm;

View File

@ -28,7 +28,7 @@ export default class Form extends Component {
wrapChildren(children, data, onInput = () => {}) { wrapChildren(children, data, onInput = () => {}) {
return children.map(child => { return children.map(child => {
const { bindTo } = child.attributes; const { bindTo } = child.attributes || {};
if (bindTo) { if (bindTo) {
child.attributes.value = data[bindTo]; child.attributes.value = data[bindTo];
child.attributes.onInput = (e) => { child.attributes.onInput = (e) => {

View File

@ -6,6 +6,9 @@ export default class MembersProvider extends Component {
constructor() { constructor() {
super(); super();
this.setGatewayFrame = gatewayFrame => this.gatewayFrame = gatewayFrame; this.setGatewayFrame = gatewayFrame => this.gatewayFrame = gatewayFrame;
this.ready = new Promise((resolve) => {
this.setReady = resolve;
})
this.gateway = null; this.gateway = null;
} }
@ -16,7 +19,8 @@ export default class MembersProvider extends Component {
signin: this.createMethod('signin'), signin: this.createMethod('signin'),
signup: this.createMethod('signup'), signup: this.createMethod('signup'),
requestPasswordReset: this.createMethod('requestPasswordReset'), requestPasswordReset: this.createMethod('requestPasswordReset'),
resetPassword: this.createMethod('resetPassword') resetPassword: this.createMethod('resetPassword'),
getConfig: this.createMethod('getConfig'),
} }
}; };
} }
@ -35,19 +39,23 @@ export default class MembersProvider extends Component {
const gatewayFrame = this.gatewayFrame; const gatewayFrame = this.gatewayFrame;
gatewayFrame.addEventListener('load', () => { gatewayFrame.addEventListener('load', () => {
this.gateway = layer0(gatewayFrame) this.gateway = layer0(gatewayFrame)
this.setReady();
}); });
} }
createMethod(method) { createMethod(method) {
return (options) => { return (options) => {
return new Promise((resolve, reject) => return this.ready.then(() => {
return new Promise((resolve, reject) => {
this.gateway.call(method, options, (err, successful) => { this.gateway.call(method, options, (err, successful) => {
console.log({method, options, err, successful});
if (err || !successful) { if (err || !successful) {
reject(err || !successful); reject(err || !successful);
} }
resolve(successful); resolve(successful);
}) })
); });
});
} }
} }

View File

@ -7,6 +7,7 @@ import SignupPage from '../pages/SignupPage';
import RequestPasswordResetPage from '../pages/RequestPasswordResetPage'; import RequestPasswordResetPage from '../pages/RequestPasswordResetPage';
import PasswordResetSentPage from '../pages/PasswordResetSentPage'; import PasswordResetSentPage from '../pages/PasswordResetSentPage';
import ResetPasswordPage from '../pages/ResetPasswordPage'; import ResetPasswordPage from '../pages/ResetPasswordPage';
import StripePaymentPage from '../pages/StripePaymentPage';
export default class Modal extends Component { export default class Modal extends Component {
constructor(props, context) { constructor(props, context) {
@ -17,6 +18,21 @@ export default class Modal extends Component {
} }
} }
loadConfig() {
if (this.state.loadingConfig) {
return;
}
this.context.members.getConfig().then(paymentConfig => {
this.setState({paymentConfig, loadingConfig: false});
}).catch((error) => {
this.setState({error, loadingConfig: false});
});
}
componentWillMount() {
this.loadConfig();
}
handleAction(promise) { handleAction(promise) {
promise.then((success) => { promise.then((success) => {
this.close(success); this.close(success);
@ -25,9 +41,29 @@ export default class Modal extends Component {
}); });
} }
renderSignupPage(state) {
const { error, paymentConfig } = state;
const { members } = this.context;
const signup = (data) => this.handleAction(members.signup(data));
const closeModal = () => this.close();
const createAccountWithSubscription = (data) => this.handleAction(
members.signup(data).then(() => {
members.createSubscription(data);
})
);
const stripeConfig = paymentConfig && paymentConfig.find(({adapter}) => adapter === 'stripe');
if (stripeConfig) {
return <StripePaymentPage stripeConfig={stripeConfig} error={error} hash="signup" handleSubmit={createAccountWithSubscription} handleClose={closeModal}/>
}
return (
<SignupPage error={error} hash="signup" handleSubmit={signup} handleClose={closeModal}/>
)
}
render(props, state) { render(props, state) {
const { queryToken } = props; const { queryToken } = props;
const { containerClass, error } = state; const { containerClass, error, loadingConfig, paymentConfig } = state;
const { members } = this.context; const { members } = this.context;
const closeModal = () => this.close(); const closeModal = () => this.close();
@ -38,11 +74,18 @@ export default class Modal extends Component {
const requestReset = (data) => this.handleAction(members.requestPasswordReset(data)); const requestReset = (data) => this.handleAction(members.requestPasswordReset(data));
const resetPassword = (data) => this.handleAction(members.resetPassword(data)); const resetPassword = (data) => this.handleAction(members.resetPassword(data));
if (loadingConfig) {
return (
<Pages className={containerClass} onChange={clearError} onClick={closeModal}>
Loading...
</Pages>
);
}
return ( return (
<Pages className={containerClass} onChange={clearError} onClick={closeModal}> <Pages className={containerClass} onChange={clearError} onClick={closeModal}>
<SigninPage error={error} hash="" handleSubmit={signup} handleClose={closeModal}/> <SigninPage error={error} hash="" handleSubmit={signup} handleClose={closeModal}/>
<SigninPage error={error} hash="signin" handleSubmit={signin} handleClose={closeModal}/> <SigninPage error={error} hash="signin" handleSubmit={signin} handleClose={closeModal}/>
<SignupPage error={error} hash="signup" handleSubmit={signup} handleClose={closeModal}/> {this.renderSignupPage(state)}
<RequestPasswordResetPage error={error} hash="request-password-reset" handleSubmit={requestReset} handleClose={closeModal}/> <RequestPasswordResetPage error={error} hash="request-password-reset" handleSubmit={requestReset} handleClose={closeModal}/>
<PasswordResetSentPage error={error} hash="password-reset-sent" handleSubmit={requestReset} handleClose={closeModal}/> <PasswordResetSentPage error={error} hash="password-reset-sent" handleSubmit={requestReset} handleClose={closeModal}/>
<ResetPasswordPage error={error} hash="reset-password" handleSubmit={resetPassword} handleClose={closeModal}/> <ResetPasswordPage error={error} hash="reset-password" handleSubmit={resetPassword} handleClose={closeModal}/>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="manifest" href="<%= htmlWebpackPlugin.files.publicPath %>manifest.json">
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
<meta name="theme-color" content="<%= htmlWebpackPlugin.options.manifest.theme_color %>">
<% } %>
<% for (var chunk of webpack.chunks) { %>
<% if (chunk.names.length === 1 && chunk.names[0] === 'polyfills') continue; %>
<% for (var file of chunk.files) { %>
<% if (htmlWebpackPlugin.options.preload && file.match(/\.(js|css)$/)) { %>
<link rel="preload" href="<%= htmlWebpackPlugin.files.publicPath + file %>" as="<%= file.match(/\.css$/)?'style':'script' %>">
<% } else if (file.match(/manifest\.json$/)) { %>
<link rel="manifest" href="<%= htmlWebpackPlugin.files.publicPath + file %>">
<% } %>
<% } %>
<% } %>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<%= htmlWebpackPlugin.options.ssr({
url: '/'
}) %>
<script defer src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"></script>
<script>window.fetch||document.write('<script src="<%= htmlWebpackPlugin.files.chunks["polyfills"].entry %>"><\/script>')</script>
</body>
</html>

View File

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "preact build --src=index.js --dest=dist --service-worker=false --no-prerender", "build": "preact build --template=index.html --src=index.js --dest=dist --service-worker=false --no-prerender",
"dev": "yarn build --no-production && preact watch --port=8080", "dev": "yarn build --no-production && preact watch --port=8080",
"lint": "eslint src" "lint": "eslint src"
}, },
@ -23,6 +23,7 @@
}, },
"dependencies": { "dependencies": {
"preact": "^8.2.1", "preact": "^8.2.1",
"preact-compat": "^3.17.0" "preact-compat": "^3.17.0",
"react-stripe-elements": "^2.0.3"
} }
} }

View File

@ -0,0 +1,146 @@
import { Elements, StripeProvider, injectStripe } from 'react-stripe-elements';
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) {
super(props);
}
handleSubmit = ({ name, email, password, plan }) => {
// Within the context of `Elements`, this call to createToken knows which Element to
// tokenize, since there's only one in this group.
plan = this.props.selectedPlan ? this.props.selectedPlan.name : "";
this.props.stripe.createToken({ name: name }).then(({ token }) => {
this.props.handleSubmit({
adapter: 'stripe',
plan: plan,
stripeToken: token.id,
name, email, password
});
});
};
render() {
return (
<Form bindTo="request-password-reset" onSubmit={(data) => this.handleSubmit(data)}>
<NameInput bindTo="name" />
<EmailInput bindTo="email" />
<PasswordInput bindTo="password" />
<CheckoutForm />
<FormSubmit label="Confirm Payment" />
</Form>
);
}
}
const PaymentFormWrapped = injectStripe(PaymentForm);
export default class StripePaymentPage extends Component {
constructor(props) {
super(props);
this.plans = props.stripeConfig.config.plans || [];
this.state = {
selectedPlan: this.plans[0] ? this.plans[0] : ""
}
}
renderPlan({ currency, amount, id, interval, name }) {
let planStyle = {
padding: "12px",
border: "1px solid #e2e8ed",
borderRadius: "6px",
marginBottom: "12px",
marginTop: "12px",
display: 'flex',
alignItems: 'center',
width: "200px"
};
const selectedPlanId = this.state.selectedPlan ? this.state.selectedPlan.id : "";
return (
<div style={planStyle}>
<input type="radio" id={id} name="radio-group" value={id} defaultChecked={id === selectedPlanId} />
<label for={id}>
<span style={{fontSize: "24px", marginLeft: "9px"}}> {`$${amount}`}</span>
<span style={{padding: "0px 1px", color: "#77919c"}}> / </span>
<span style={{color: "#77919c"}}> {`${interval}`}</span>
</label>
</div>
)
}
changePlan(e) {
const plan = this.plans.find(plan => plan.id === e.target.value);
this.setState({
selectedPlan: plan
})
}
renderPlans(plans) {
return (
<div onChange={(e) => this.changePlan(e)}>
{
plans.map((plan) => this.renderPlan(plan))
}
</div>
);
}
renderPlansSection() {
const separatorStyle = {
height: "1px",
borderTop: "2px solid #e7f0f6",
width: "180px",
margin: "12px 0"
}
return (
<div style={{ padding: "20px", width: "295px", display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column", background: "#fcfdfd" }}>
<div style={{display: "flex", alignItems: "center"}}>
<div className="gm-logo"></div>
<div style={{display: "flex", flexDirection: "column", paddingLeft: "12px"}}>
<span style={{fontSize: "16px", fontWeight: "bold"}}> The Blueprint</span>
<span style={{fontSize: "14px", color: "#9cb2bc", marginTop: "3px"}}> Subscription</span>
</div>
</div>
<div className="separator" style={separatorStyle}> </div>
{this.renderPlans(this.plans)}
</div>
)
}
render({ error, handleClose, handleSubmit, stripeConfig }) {
const publicKey = stripeConfig.config.publicKey || '';
return (
<div className="gm-modal-container">
<div className="gm-modal gm-auth-modal gm-subscribe-modal" onClick={(e) => e.stopPropagation()}>
<a className="gm-modal-close" onClick={handleClose}>{IconClose}</a>
<div style={{ display: "flex" }}>
<div style={{ width: "300px", padding: "20px" }}>
<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>
<div style={{ border: "1px solid black" }}></div>
{this.renderPlansSection()}
</div>
</div>
</div>
)
}
};

View File

@ -41,6 +41,10 @@
animation: openModal 0.6s ease 1 forwards; animation: openModal 0.6s ease 1 forwards;
} }
.gm-subscribe-modal {
width: 600px;
}
.gm-modal-close { .gm-modal-close {
position: absolute; position: absolute;
top: 8px; top: 8px;
@ -393,3 +397,43 @@ select:-webkit-autofill:active {
.gm-form-errortext i { .gm-form-errortext i {
margin: 1px 8px 0 0; margin: 1px 8px 0 0;
} }
/**
* 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);
height: 38px;
-webkit-appearance: none;
box-sizing: border-box;
background: var(--white);
width: 100%;
outline: none;
transition: border var(--animation-speed-f1) ease-in-out;
letter-spacing: 0.2px;
line-height: 14px;
padding: 10px 12px;
}
.StripeElement:hover {
border: 1px solid var(--grey);
}
.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;
}
.StripeElement--invalid {
border: 1px solid color-mod(var(--red) a(0.8));
background: color-mod(var(--red) a(0.02))
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}

View File

@ -6823,7 +6823,7 @@ promise-polyfill@^6.0.2:
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc= integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=
prop-types@^15.6.2: prop-types@^15.5.10, prop-types@^15.6.2:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==
@ -6980,6 +6980,13 @@ rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-stripe-elements@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/react-stripe-elements/-/react-stripe-elements-2.0.3.tgz#cfd0f68d00ce52a07aab1cb2b59b29dc12309486"
integrity sha512-aKLiWyfP0n3Gq42BKykULgoruNVRXEaeYh8NSokdgH3ubGU3nsHFZJg3LgbT/XOquttDGHE7kLhleaX+UnN81A==
dependencies:
prop-types "^15.5.10"
read-all-stream@^3.0.0: read-all-stream@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa"

View File

@ -60,6 +60,26 @@
addMethod('getToken', getToken); addMethod('getToken', getToken);
addMethod('createSubscription', function createSubscription({adapter, plan, stripeToken}) {
return fetch(`${membersApi}/subscription`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
origin,
adapter,
plan,
stripeToken
})
}).then((res) => {
if (res.ok) {
storage.setItem('signedin', true);
}
return res.ok;
});
});
addMethod('signin', function signin({email, password}) { addMethod('signin', function signin({email, password}) {
return fetch(`${membersApi}/signin`, { return fetch(`${membersApi}/signin`, {
method: 'POST', method: 'POST',

View File

@ -45,7 +45,7 @@ module.exports = class StripePaymentProcessor {
return { return {
adapter: 'stripe', adapter: 'stripe',
config: { config: {
public_token: this._public_token, publicKey: this._public_token,
plans: this._plans.map(({id, currency, amount, interval, nickname}) => ({ plans: this._plans.map(({id, currency, amount, interval, nickname}) => ({
id, currency, amount, interval, id, currency, amount, interval,
name: nickname name: nickname