From 5522b2f1f79a17fa4c8edd49dd9ff88a3df1297a Mon Sep 17 00:00:00 2001 From: "Aidar Shaikhutdin @makeweb.space" Date: Thu, 8 Jun 2023 19:06:11 +0300 Subject: [PATCH] feat: Chargebee card component --- src/components/Modal/index.tsx | 11 +- src/components/Modal/styles.css | 6 +- src/components/PaymentPage/index.tsx | 30 +++-- .../PaymentPage/methods/Card/Modal.tsx | 117 +++++++++++++----- .../PaymentPage/methods/Card/styles.ts | 18 +++ src/components/PaymentPage/styles.css | 25 ++++ src/locales/dev.ts | 1 + src/payment/types.ts | 19 +++ src/payment/usePayment.ts | 13 +- 9 files changed, 184 insertions(+), 56 deletions(-) create mode 100644 src/components/PaymentPage/methods/Card/styles.ts create mode 100644 src/payment/types.ts diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 3f772ae..742ab64 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -8,18 +8,19 @@ interface ModalProps { } function Modal({ open, children, onClose }: ModalProps): JSX.Element { - const handleClose = () => { + const handleClose = (event: React.MouseEvent) => { + if (event.target !== event.currentTarget) return onClose?.() } if (!open) return <> return ( -
+
{children}
diff --git a/src/components/Modal/styles.css b/src/components/Modal/styles.css index 496687c..683cae2 100644 --- a/src/components/Modal/styles.css +++ b/src/components/Modal/styles.css @@ -50,10 +50,10 @@ max-width: 100%; min-height: 48px; width: 100%; - color: #121620; } .modal .main-btn:disabled { + color: #121620; background: #c7c7c7; cursor: not-allowed; opacity: 100%; @@ -61,5 +61,9 @@ .modal .main-btn svg { margin-right: 12px; + fill: #fff; +} + +.modal .main-btn:disabled svg { fill: #121620; } diff --git a/src/components/PaymentPage/index.tsx b/src/components/PaymentPage/index.tsx index c6df6ea..2bc536f 100644 --- a/src/components/PaymentPage/index.tsx +++ b/src/components/PaymentPage/index.tsx @@ -15,11 +15,13 @@ import Title from '../Title' import secure from './secure.png' import './styles.css' import { useState } from 'react' +import Loader from '../Loader' function PaymentPage(): JSX.Element { const { t } = useTranslation() const { applePay } = usePayment() const [open, setOpen] = useState(false) + const isLoading = applePay === null const isApplePayAvailable = import.meta.env.PROD && applePay?.canMakePayments() const email = useSelector(selectors.selectEmail) @@ -27,18 +29,22 @@ function PaymentPage(): JSX.Element { <>
-
- { isApplePayAvailable ? : } - 100% Secure -
- {t('choose_payment')} - { isApplePayAvailable ? : } -
{t('or').toUpperCase()}
- setOpen(true)} /> -

- {t('will_be_charged', { strongText: {t('trial_price')} })} -

- setOpen(false)} /> + { isLoading ? : ( + <> +
+ { isApplePayAvailable ? : } + 100% Secure +
+ {t('choose_payment')} + { isApplePayAvailable ? : } +
{t('or').toUpperCase()}
+ setOpen(true)} /> +

+ {t('will_be_charged', { strongText: {t('trial_price')} })} +

+ setOpen(false)} /> + + )}
) diff --git a/src/components/PaymentPage/methods/Card/Modal.tsx b/src/components/PaymentPage/methods/Card/Modal.tsx index b9af53c..39e430c 100644 --- a/src/components/PaymentPage/methods/Card/Modal.tsx +++ b/src/components/PaymentPage/methods/Card/Modal.tsx @@ -1,58 +1,119 @@ -import { useRef } from 'react' import { useTranslation } from 'react-i18next' +import { useEffect, useRef, useState, ChangeEvent } from 'react' import { CardCVV, CardComponent, CardExpiry, CardNumber, Provider } from '@chargebee/chargebee-js-react-wrapper' +import ChargebeeComponents from '@chargebee/chargebee-js-react-wrapper/dist/components/ComponentGroup' import { usePayment } from '../../../../payment' import Modal from '../../../Modal' import Title from '../../../Title' import MainButton from '../../../MainButton' +import Loader from '../../../Loader' import visa from './visa.svg' import mastercard from './mastercard.svg' import amex from './amex.svg' import diners from './diners.svg' import discover from './discover.svg' +import { cardStyles } from './styles' interface CardModalProps { open: boolean onClose: () => void } +interface Field { + cardType: string | undefined + complete: boolean + empty: boolean + error: Error | undefined + field: 'number' | 'expiry' | 'cvv' + key: string | undefined + type: string +} + +type Status = 'idle' | 'loading' | 'filling' | 'tokenizing' | 'ready' | 'success' | 'error' + +const initCompletedFields = { + number: false, + expiry: false, + cvv: false, +} + +type CompletedFields = typeof initCompletedFields + +const isReady = (fields: CompletedFields) => Object.values(fields).every((complete: boolean) => complete) + export function CardModal({ open, onClose }: CardModalProps): JSX.Element { - const cardRef = useRef(null) + const cardRef = useRef(null) + const [status, setStatus] = useState('idle') + const [fields, setFields] = useState(initCompletedFields) const { t, i18n } = useTranslation() const locale = i18n.language + const isInit = status === 'idle' + const isLoading = status === 'loading' + const isTokenizing = status === 'tokenizing' + const isDisabled = status !== 'ready' const { chargebee } = usePayment() - const payWithCard = () => { - // cardRef.current?.tokenize() + const handleReady = () => setStatus('filling') + const handleClose = () => { + setStatus('loading') + onClose() } + const handleChange = ({ field, complete, error }: ChangeEvent & Field) => { + setFields((state) => ({ ...state, [field]: complete && !error })) + } + const payWithCard = () => { + setStatus('tokenizing') + cardRef.current?.tokenize({}) + .then(console.log) + .catch(console.error) + .finally(() => setStatus('success')) + } + + useEffect(() => { + setStatus(isReady(fields) ? 'ready' : 'filling') + }, [fields]) + + useEffect(() => { + if (isInit) setStatus('loading') + }, [isInit]) + return ( - -
- {t('card')} -
- Visa Card - Mastercard Card - Amex Card - Diners Card - Discover Card + + { isLoading ?
: null} +
+
+ {t('card')} +
+ Visa Card + Mastercard Card + Amex Card + Diners Card + Discover Card +
+ + +
+
+
+
+
+
+
+

{t('charged_only')}

+ + { isTokenizing ? : ( + <> + + + + + {t('start_trial')} + + ) } +
- - - - - - - -

{t('charged_only')}

- - - - - - Start 7-Day Trial -
) } diff --git a/src/components/PaymentPage/methods/Card/styles.ts b/src/components/PaymentPage/methods/Card/styles.ts new file mode 100644 index 0000000..6af1925 --- /dev/null +++ b/src/components/PaymentPage/methods/Card/styles.ts @@ -0,0 +1,18 @@ +export const cardStyles = { + base: { + color: '#121620', + lineHeight: '18px', + fontSize: '16px', + fontWeight: '400', + fontFamily: 'SF Pro Text, system-ui, sans-serif', + '::placeholder': { + color: '#8E8E93', + } + }, + invalid: { + color: '#FF5758', + }, + empty: { + fontWeight: '400', + }, +} diff --git a/src/components/PaymentPage/styles.css b/src/components/PaymentPage/styles.css index c5d5de6..33e7784 100644 --- a/src/components/PaymentPage/styles.css +++ b/src/components/PaymentPage/styles.css @@ -62,3 +62,28 @@ flex-direction: column; margin-bottom: 16px; } + +.payment-input { + padding-bottom: 4px; + border-radius: 10px; + border: 1px solid #c7c7c7 !important; + padding: 14px 12px; + width: 100%; + height: 48px; + text-rendering: optimizeLegibility; + margin-bottom: 12px; +} + +.payment-group { + display: flex; +} + +.payment-group > .payment-input:first-child { + margin-right: 12px; +} + +.payment-loader { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/locales/dev.ts b/src/locales/dev.ts index 7c2e62e..04144c1 100644 --- a/src/locales/dev.ts +++ b/src/locales/dev.ts @@ -42,5 +42,6 @@ export default { contact_us: "Contact us", will_be_charged: "You will be charged only . We'll email your a reminder before your trial period ends.", trial_price: "$1 for your 7-day trial", + start_trial: "Start 7-Day Trial", }, } diff --git a/src/payment/types.ts b/src/payment/types.ts new file mode 100644 index 0000000..69888ac --- /dev/null +++ b/src/payment/types.ts @@ -0,0 +1,19 @@ +import { PaymentIntent } from '@chargebee/chargebee-js-types' + +interface Handler { + mountPaymentButton: (id: string) => Promise + handlePayment: () => Promise +} + +interface ApplePay extends Handler { + canMakePayments: () => boolean +} + +interface GooglePay extends Handler { + getPaymentIntent: () => PaymentIntent + setPaymentIntent: (paymentIntent: PaymentIntent) => void + updatePaymentIntent: (paymentIntent: PaymentIntent) => void +} + +export type ApplePayHandler = ApplePay | null +export type GooglePayHandler = GooglePay | null diff --git a/src/payment/usePayment.ts b/src/payment/usePayment.ts index 87a83b2..c8af806 100644 --- a/src/payment/usePayment.ts +++ b/src/payment/usePayment.ts @@ -1,18 +1,11 @@ import { useContext, useEffect, useState } from 'react' -import { PaymentIntent } from '@chargebee/chargebee-js-types' import { PaymentContext } from './PaymentContext' - -interface ApplePayHandler { - canMakePayments: () => boolean - setPaymentIntent: (paymentIntent: PaymentIntent) => void - mountPaymentButton: (id: string) => Promise - handlePayment: () => Promise -} +import { ApplePayHandler, GooglePayHandler } from './types' export const usePayment = () => { const chargebee = useContext(PaymentContext) - const [googlePay, setGooglePay] = useState(null) - const [applePay, setApplePay] = useState(null) + const [googlePay, setGooglePay] = useState(null) + const [applePay, setApplePay] = useState(null) useEffect(() => { Promise.all([