mirror of
https://github.com/dnote/dnote
synced 2026-03-16 07:25:49 +01:00
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4cb2b7e6 | ||
|
|
37e1cc5ba3 |
13 changed files with 250 additions and 45 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 |
2
web/declrations.d.ts
vendored
2
web/declrations.d.ts
vendored
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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`;
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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 {
|
.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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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'
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue