Compare commits

...

2 commits

Author SHA1 Message Date
Sung Won Cho
1d4cb2b7e6 wip 2020-02-07 16:49:06 +11:00
Sung Won Cho
37e1cc5ba3 wip 2020-02-07 13:46:59 +11:00
13 changed files with 250 additions and 45 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -18,9 +18,11 @@
// declaration.d.ts // declaration.d.ts
declare module '*.scss'; declare module '*.scss';
declare module '*.png';
// globals defined by webpack-define-plugin // globals defined by webpack-define-plugin
declare const __STRIPE_PUBLIC_KEY__: string; declare const __STRIPE_PUBLIC_KEY__: string;
declare const __PAYPAL_CLIENT_ID__: string;
declare const __ROOT_URL__: string; declare const __ROOT_URL__: string;
declare const __CDN_URL__: string; declare const __CDN_URL__: string;
declare const __VERSION__: string; declare const __VERSION__: string;

View file

@ -18,7 +18,9 @@
import React from 'react'; import React from 'react';
const Icon = ({ fill, width, height, className }) => { import { IconProps } from './types';
const Icon = ({ fill, width, height, className }: IconProps) => {
const h = `${height}px`; const h = `${height}px`;
const w = `${width}px`; const w = `${width}px`;

View file

@ -51,6 +51,11 @@
font-weight: 600; font-weight: 600;
} }
.instruction {
margin-top: rem(8px);
font-weight: 600;
}
.content-wrapper { .content-wrapper {
@include breakpoint(lg) { @include breakpoint(lg) {
padding-right: rem(52px); padding-right: rem(52px);
@ -70,3 +75,50 @@
.label { .label {
@include font-size('medium'); @include font-size('medium');
} }
.method-wrapper {
margin-top: rem(20px);
display: flex;
@include breakpoint(lg) {
display: block;
}
.method {
@include font-size('regular');
width: 152px;
padding-left: 0;
padding-right: 0;
flex: 1;
font-weight: 600;
color: $gray;
display: inline-flex;
justify-content: center;
align-items: center;
@include breakpoint(lg) {
width: 200px;
flex: auto;
}
input {
display: none;
}
}
label.method-active {
border-color: $first;
}
.method-text {
margin-left: rem(8px);
}
}
.stripe-logo {
margin-top: rem(12px);
}
.paypal-logo {
width: 80px;
}

View file

@ -27,6 +27,7 @@ import services from 'web/libs/services';
import { getHomePath } from 'web/libs/paths'; import { getHomePath } from 'web/libs/paths';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import Flash from '../../Common/Flash'; import Flash from '../../Common/Flash';
import CreditCardIcon from '../../Icons/CreditCard';
import { getCurrentUser } from '../../../store/auth'; import { getCurrentUser } from '../../../store/auth';
import { useDispatch } from '../../../store'; import { useDispatch } from '../../../store';
import { setMessage } from '../../../store/ui'; import { setMessage } from '../../../store/ui';
@ -34,17 +35,21 @@ import NameOnCardInput from '../../Common/PaymentInput/NameOnCard';
import CardInput from '../../Common/PaymentInput/Card'; import CardInput from '../../Common/PaymentInput/Card';
import Footer from '../Footer'; import Footer from '../Footer';
import CountryInput from '../../Common/PaymentInput/Country'; import CountryInput from '../../Common/PaymentInput/Country';
import { PaymentMethod } from './helpers';
import PaymentMethodComponent from './PaymentMethod';
import styles from './Form.scss'; import styles from './Form.scss';
interface Props extends RouteComponentProps { interface Props extends RouteComponentProps {
stripe: any; stripe: any;
stripeLoadError: string; stripeLoadError: string;
paypalLoadError: string;
history: History; history: History;
} }
const Form: React.FunctionComponent<Props> = ({ const Form: React.FunctionComponent<Props> = ({
stripe, stripe,
stripeLoadError, stripeLoadError,
paypalLoadError,
history history
}) => { }) => {
const [nameOnCard, setNameOnCard] = useState(''); const [nameOnCard, setNameOnCard] = useState('');
@ -55,6 +60,7 @@ const Form: React.FunctionComponent<Props> = ({
const [errMessage, setErrMessage] = useState(''); const [errMessage, setErrMessage] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const [yearly, setYearly] = useState(false); const [yearly, setYearly] = useState(false);
const [paymentMethod, setPaymentMethod] = useState(PaymentMethod.CreditCard);
async function handleSubmit(e) { async function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
@ -142,36 +148,77 @@ const Form: React.FunctionComponent<Props> = ({
kind="danger" kind="danger"
wrapperClassName={styles.flash} wrapperClassName={styles.flash}
> >
Failed to load stripe. {stripeLoadError} Failed to load Stripe. {stripeLoadError}
</Flash>
<Flash
when={paypalLoadError !== ''}
kind="danger"
wrapperClassName={styles.flash}
>
Failed to load PayPal. {paypalLoadError}
</Flash> </Flash>
<div className="row"> <div className="row">
<div className="col-12 col-md-6 col-xl-6 offset-xl-1"> <div className="col-12 col-md-6 col-xl-6 offset-xl-1">
<div className={styles['content-wrapper']}> <div className={styles['content-wrapper']}>
<h1 className={styles.heading}>Welcome to Dnote Pro.</h1> <h1 className={styles.heading}>Welcome to Dnote Pro.</h1>
<p className={styles.instruction}>
Please complete payment details.
</p>
<div className={styles.content}> <div className={styles['method-wrapper']}>
<NameOnCardInput <PaymentMethodComponent
value={nameOnCard} method={PaymentMethod.CreditCard}
onUpdate={setNameOnCard} isActive={paymentMethod === PaymentMethod.CreditCard}
containerClassName={styles['input-row']} setMethod={setPaymentMethod}
labelClassName={styles.label} >
/> <CreditCardIcon width={20} height={20} fill="gray" />
<span className={styles['method-text']}>Credit card</span>
</PaymentMethodComponent>
<CardInput <PaymentMethodComponent
cardElementRef={cardElementRef} method={PaymentMethod.PayPal}
setCardElementLoaded={setCardElementLoaded} isActive={paymentMethod === PaymentMethod.PayPal}
containerClassName={styles['input-row']} setMethod={setPaymentMethod}
labelClassName={styles.label} >
/> <img
src="/static/paypal.png"
<CountryInput alt="PayPal"
value={billingCountry} className={styles['paypal-logo']}
onUpdate={setBillingCountry} />
containerClassName={styles['input-row']} </PaymentMethodComponent>
labelClassName={styles.label}
/>
</div> </div>
{paymentMethod === PaymentMethod.CreditCard && (
<div className={styles.content}>
<NameOnCardInput
value={nameOnCard}
onUpdate={setNameOnCard}
containerClassName={styles['input-row']}
labelClassName={styles.label}
/>
<CardInput
cardElementRef={cardElementRef}
setCardElementLoaded={setCardElementLoaded}
containerClassName={styles['input-row']}
labelClassName={styles.label}
/>
<CountryInput
value={billingCountry}
onUpdate={setBillingCountry}
containerClassName={styles['input-row']}
labelClassName={styles.label}
/>
<img
src="/static/stripe.png"
alt="Stripe"
className={styles['stripe-logo']}
/>
</div>
)}
</div> </div>
</div> </div>
@ -181,6 +228,7 @@ const Form: React.FunctionComponent<Props> = ({
transacting={transacting} transacting={transacting}
yearly={yearly} yearly={yearly}
setYearly={setYearly} setYearly={setYearly}
paymentMethod={paymentMethod}
/> />
</div> </div>
</div> </div>

View file

@ -0,0 +1,43 @@
import React from 'react';
import classnames from 'classnames';
import { PaymentMethod } from './helpers';
import styles from './Form.scss';
interface Props {
method: PaymentMethod;
isActive: boolean;
setMethod: (PaymentMethod) => void;
}
const Component: React.SFC<Props> = ({
children,
method,
isActive,
setMethod
}) => {
return (
<label
className={classnames(
'button button-large button-second-outline',
styles.method,
{
[styles['method-active']]: isActive
}
)}
>
<input
type="radio"
name="payment_method"
onChange={() => {
setMethod(method);
}}
value={method}
checked={isActive}
/>
{children}
</label>
);
};
export default Component;

View file

@ -72,7 +72,6 @@
} }
.purchase-button { .purchase-button {
margin-top: rem(20px);
} }
.assurance { .assurance {
@ -129,3 +128,7 @@
@include font-size('small'); @include font-size('small');
color: $gray; color: $gray;
} }
.paypal-button-container {
margin-top: rem(12px);
}

View file

@ -16,7 +16,7 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>. * along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/ */
import React from 'react'; import React, { useEffect } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Button from '../../Common/Button'; import Button from '../../Common/Button';
@ -24,6 +24,7 @@ import Toggle, { ToggleKind } from '../../Common/Toggle';
import PaymentSummary from './PaymentSummary'; import PaymentSummary from './PaymentSummary';
import Price from './Price'; import Price from './Price';
import ScheduleSummary from './ScheduleSummary'; import ScheduleSummary from './ScheduleSummary';
import { PaymentMethod } from './helpers';
import styles from './Sidebar.scss'; import styles from './Sidebar.scss';
interface Props { interface Props {
@ -31,9 +32,31 @@ interface Props {
transacting: boolean; transacting: boolean;
yearly: boolean; yearly: boolean;
setYearly: (boolean) => null; setYearly: (boolean) => null;
paymentMethod: PaymentMethod;
} }
function Sidebar({ isReady, transacting, yearly, setYearly }) { const paypalButtonNodeID = 'paypal-button-container';
function Sidebar({ isReady, transacting, yearly, setYearly, paymentMethod }) {
useEffect(() => {
if (paymentMethod === PaymentMethod.PayPal) {
(window as any).paypal
.Buttons({
style: {
layout: 'vertical',
color: 'blue',
shape: 'rect',
label: 'paypal'
}
})
.render(`#${paypalButtonNodeID}`);
} else {
const node = document.getElementById(paypalButtonNodeID);
node.innerHTML = '';
}
}, [paymentMethod]);
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.header}> <div className={styles.header}>
@ -87,20 +110,27 @@ function Sidebar({ isReady, transacting, yearly, setYearly }) {
<PaymentSummary yearly={yearly} /> <PaymentSummary yearly={yearly} />
<Button <div
id="T-purchase-button" id={paypalButtonNodeID}
type="submit" className={styles['paypal-button-container']}
kind="first" />
size="normal"
className={classnames( {paymentMethod === PaymentMethod.CreditCard && (
'button button-large button-third button-stretch', <Button
styles['purchase-button'] id="T-purchase-button"
)} type="submit"
disabled={transacting} kind="first"
isBusy={transacting || !isReady} size="normal"
> className={classnames(
Purchase Dnote Pro 'button button-large button-third button-stretch',
</Button> styles['purchase-button']
)}
disabled={transacting}
isBusy={transacting || !isReady}
>
Purchase Dnote Pro
</Button>
)}
</div> </div>
<p className={styles.assurance}>You can cancel auto-renewal any time.</p> <p className={styles.assurance}>You can cancel auto-renewal any time.</p>

View file

@ -0,0 +1,5 @@
// PaymentMethod represents the possible payment methods
export enum PaymentMethod {
CreditCard = 'creditCard',
PayPal = 'paypal'
}

View file

@ -27,6 +27,11 @@ import CheckoutForm from './Form';
const Checkout: React.FunctionComponent = () => { const Checkout: React.FunctionComponent = () => {
const [stripeLoaded, stripeLoadError] = useScript('https://js.stripe.com/v3'); const [stripeLoaded, stripeLoadError] = useScript('https://js.stripe.com/v3');
const [paypalLoaded, paypalLoadError] = useScript(
`https://www.paypal.com/sdk/js?client-id=${__PAYPAL_CLIENT_ID__}&vault=true`
);
console.log(paypalLoaded);
const key = `${__STRIPE_PUBLIC_KEY__}`; const key = `${__STRIPE_PUBLIC_KEY__}`;
@ -49,7 +54,10 @@ const Checkout: React.FunctionComponent = () => {
return ( return (
<StripeProvider stripe={stripe}> <StripeProvider stripe={stripe}>
<Elements> <Elements>
<CheckoutForm stripeLoadError={stripeLoadError} /> <CheckoutForm
stripeLoadError={stripeLoadError}
paypalLoadError={paypalLoadError}
/>
</Elements> </Elements>
</StripeProvider> </StripeProvider>
); );

View file

@ -30,12 +30,21 @@ module.exports = ({ production = false, test = false } = {}) => {
stripePublicKey = 'pk_live_xvouPZFPDDBSIyMUSLZwkXfR'; stripePublicKey = 'pk_live_xvouPZFPDDBSIyMUSLZwkXfR';
} }
let paypalClientID;
if (test) {
paypalClientID =
'AR3QRJuMdHc3korILtegw4QMbCGoRhIR3E-zygzAvuvGS0QdRi0_M1mofZi6QTC1Y5NvWEBhQ3SN6T78';
} else {
paypalClientID = 'unknown';
}
const cdnUrl = 'https://cdn.getdnote.com'; const cdnUrl = 'https://cdn.getdnote.com';
const version = process.env.VERSION; const version = process.env.VERSION;
const compileTimeConstantForMinification = { const compileTimeConstantForMinification = {
__ROOT_URL__: JSON.stringify(rootUrl), __ROOT_URL__: JSON.stringify(rootUrl),
__STRIPE_PUBLIC_KEY__: JSON.stringify(stripePublicKey), __STRIPE_PUBLIC_KEY__: JSON.stringify(stripePublicKey),
__PAYPAL_CLIENT_ID__: JSON.stringify(paypalClientID),
__CDN_URL__: JSON.stringify(cdnUrl), __CDN_URL__: JSON.stringify(cdnUrl),
__VERSION__: JSON.stringify(version) __VERSION__: JSON.stringify(version)
}; };

View file

@ -18,12 +18,15 @@
const PATHS = require('../paths'); const PATHS = require('../paths');
module.exports = ({ limit = 10000 } = {}) => ({ module.exports = () => ({
test: /\.(bmp|gif|jpg|jpeg|png|svg)$/, test: /\.(bmp|gif|jpg|jpeg|png|svg)$/,
use: [ use: [
{ {
loader: 'url-loader', loader: 'file-loader',
options: { name: '[hash].[ext]', limit } options: {
publicPath: PATHS.public,
outputPath: PATHS.public
}
} }
] ]
}); });