mirror of
https://github.com/dnote/dnote
synced 2026-03-15 15:05:51 +01:00
wip
This commit is contained in:
parent
37e1cc5ba3
commit
1d4cb2b7e6
12 changed files with 238 additions and 46 deletions
BIN
web/assets/static/paypal.png
Normal file
BIN
web/assets/static/paypal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
web/assets/static/stripe.png
Normal file
BIN
web/assets/static/stripe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
1
web/declrations.d.ts
vendored
1
web/declrations.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
43
web/src/components/Subscription/Checkout/PaymentMethod.tsx
Normal file
43
web/src/components/Subscription/Checkout/PaymentMethod.tsx
Normal 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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
5
web/src/components/Subscription/Checkout/helpers.ts
Normal file
5
web/src/components/Subscription/Checkout/helpers.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// PaymentMethod represents the possible payment methods
|
||||
export enum PaymentMethod {
|
||||
CreditCard = 'creditCard',
|
||||
PayPal = 'paypal'
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue