This commit is contained in:
Sung Won Cho 2020-02-07 16:49:06 +11:00
commit 1d4cb2b7e6
12 changed files with 238 additions and 46 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,6 +18,7 @@
// declaration.d.ts
declare module '*.scss';
declare module '*.png';
// globals defined by webpack-define-plugin
declare const __STRIPE_PUBLIC_KEY__: string;

View file

@ -18,7 +18,9 @@
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 w = `${width}px`;

View file

@ -51,6 +51,11 @@
font-weight: 600;
}
.instruction {
margin-top: rem(8px);
font-weight: 600;
}
.content-wrapper {
@include breakpoint(lg) {
padding-right: rem(52px);
@ -70,3 +75,50 @@
.label {
@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 Sidebar from './Sidebar';
import Flash from '../../Common/Flash';
import CreditCardIcon from '../../Icons/CreditCard';
import { getCurrentUser } from '../../../store/auth';
import { useDispatch } from '../../../store';
import { setMessage } from '../../../store/ui';
@ -34,17 +35,21 @@ import NameOnCardInput from '../../Common/PaymentInput/NameOnCard';
import CardInput from '../../Common/PaymentInput/Card';
import Footer from '../Footer';
import CountryInput from '../../Common/PaymentInput/Country';
import { PaymentMethod } from './helpers';
import PaymentMethodComponent from './PaymentMethod';
import styles from './Form.scss';
interface Props extends RouteComponentProps {
stripe: any;
stripeLoadError: string;
paypalLoadError: string;
history: History;
}
const Form: React.FunctionComponent<Props> = ({
stripe,
stripeLoadError,
paypalLoadError,
history
}) => {
const [nameOnCard, setNameOnCard] = useState('');
@ -55,6 +60,7 @@ const Form: React.FunctionComponent<Props> = ({
const [errMessage, setErrMessage] = useState('');
const dispatch = useDispatch();
const [yearly, setYearly] = useState(false);
const [paymentMethod, setPaymentMethod] = useState(PaymentMethod.CreditCard);
async function handleSubmit(e) {
e.preventDefault();
@ -142,36 +148,77 @@ const Form: React.FunctionComponent<Props> = ({
kind="danger"
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>
<div className="row">
<div className="col-12 col-md-6 col-xl-6 offset-xl-1">
<div className={styles['content-wrapper']}>
<h1 className={styles.heading}>Welcome to Dnote Pro.</h1>
<p className={styles.instruction}>
Please complete payment details.
</p>
<div className={styles.content}>
<NameOnCardInput
value={nameOnCard}
onUpdate={setNameOnCard}
containerClassName={styles['input-row']}
labelClassName={styles.label}
/>
<div className={styles['method-wrapper']}>
<PaymentMethodComponent
method={PaymentMethod.CreditCard}
isActive={paymentMethod === PaymentMethod.CreditCard}
setMethod={setPaymentMethod}
>
<CreditCardIcon width={20} height={20} fill="gray" />
<span className={styles['method-text']}>Credit card</span>
</PaymentMethodComponent>
<CardInput
cardElementRef={cardElementRef}
setCardElementLoaded={setCardElementLoaded}
containerClassName={styles['input-row']}
labelClassName={styles.label}
/>
<CountryInput
value={billingCountry}
onUpdate={setBillingCountry}
containerClassName={styles['input-row']}
labelClassName={styles.label}
/>
<PaymentMethodComponent
method={PaymentMethod.PayPal}
isActive={paymentMethod === PaymentMethod.PayPal}
setMethod={setPaymentMethod}
>
<img
src="/static/paypal.png"
alt="PayPal"
className={styles['paypal-logo']}
/>
</PaymentMethodComponent>
</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>
@ -181,6 +228,7 @@ const Form: React.FunctionComponent<Props> = ({
transacting={transacting}
yearly={yearly}
setYearly={setYearly}
paymentMethod={paymentMethod}
/>
</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 {
margin-top: rem(20px);
}
.assurance {
@ -129,3 +128,7 @@
@include font-size('small');
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/>.
*/
import React from 'react';
import React, { useEffect } from 'react';
import classnames from 'classnames';
import Button from '../../Common/Button';
@ -24,6 +24,7 @@ import Toggle, { ToggleKind } from '../../Common/Toggle';
import PaymentSummary from './PaymentSummary';
import Price from './Price';
import ScheduleSummary from './ScheduleSummary';
import { PaymentMethod } from './helpers';
import styles from './Sidebar.scss';
interface Props {
@ -31,9 +32,31 @@ interface Props {
transacting: boolean;
yearly: boolean;
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 (
<div className={styles.wrapper}>
<div className={styles.header}>
@ -87,20 +110,27 @@ function Sidebar({ isReady, transacting, yearly, setYearly }) {
<PaymentSummary yearly={yearly} />
<Button
id="T-purchase-button"
type="submit"
kind="first"
size="normal"
className={classnames(
'button button-large button-third button-stretch',
styles['purchase-button']
)}
disabled={transacting}
isBusy={transacting || !isReady}
>
Purchase Dnote Pro
</Button>
<div
id={paypalButtonNodeID}
className={styles['paypal-button-container']}
/>
{paymentMethod === PaymentMethod.CreditCard && (
<Button
id="T-purchase-button"
type="submit"
kind="first"
size="normal"
className={classnames(
'button button-large button-third button-stretch',
styles['purchase-button']
)}
disabled={transacting}
isBusy={transacting || !isReady}
>
Purchase Dnote Pro
</Button>
)}
</div>
<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

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

View file

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