feat: Chargebee card component
This commit is contained in:
parent
70a00fd89a
commit
5522b2f1f7
@ -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 (
|
||||
<div className='modal'>
|
||||
<div className='modal' onClick={handleClose}>
|
||||
<div className='modal-content'>
|
||||
<button className='modal-close-btn' onClick={handleClose}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.7071 12.7071C12.3166 13.0976 11.6834 13.0976 11.2929 12.7071L6.29289 7.70711C5.90237 7.31658 5.90237 6.68342 6.29289 6.29289L11.2929 1.29289C11.6834 0.902369 12.3166 0.902369 12.7071 1.29289C13.0976 1.68342 13.0976 2.31658 12.7071 2.70711L8.41421 7L12.7071 11.2929C13.0976 11.6834 13.0976 12.3166 12.7071 12.7071Z" fill="#858DA5" stroke="#858DA5" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M1.29289 12.7071C1.68342 13.0976 2.31658 13.0976 2.70711 12.7071L7.70711 7.70711C8.09763 7.31658 8.09763 6.68342 7.70711 6.29289L2.70711 1.29289C2.31658 0.902369 1.68342 0.902369 1.29289 1.29289C0.902369 1.68342 0.902369 2.31658 1.29289 2.70711L5.58579 7L1.29289 11.2929C0.902369 11.6834 0.902369 12.3166 1.29289 12.7071Z" fill="#858DA5" stroke="#858DA5" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.7071 12.7071C12.3166 13.0976 11.6834 13.0976 11.2929 12.7071L6.29289 7.70711C5.90237 7.31658 5.90237 6.68342 6.29289 6.29289L11.2929 1.29289C11.6834 0.902369 12.3166 0.902369 12.7071 1.29289C13.0976 1.68342 13.0976 2.31658 12.7071 2.70711L8.41421 7L12.7071 11.2929C13.0976 11.6834 13.0976 12.3166 12.7071 12.7071Z" fill="#858DA5" stroke="#858DA5" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M1.29289 12.7071C1.68342 13.0976 2.31658 13.0976 2.70711 12.7071L7.70711 7.70711C8.09763 7.31658 8.09763 6.68342 7.70711 6.29289L2.70711 1.29289C2.31658 0.902369 1.68342 0.902369 1.29289 1.29289C0.902369 1.68342 0.902369 2.31658 1.29289 2.70711L5.58579 7L1.29289 11.2929C0.902369 11.6834 0.902369 12.3166 1.29289 12.7071Z" fill="#858DA5" stroke="#858DA5" strokeLinecap="round" strokeLinejoin="round"></path>
|
||||
</svg>
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
<>
|
||||
<UserHeader email={email} />
|
||||
<section className='page'>
|
||||
<div className='page-header'>
|
||||
{ isApplePayAvailable ? <ApplePayBanner /> : <GooglePayBanner /> }
|
||||
<img src={secure} alt='100% Secure' />
|
||||
</div>
|
||||
<Title variant='h1' className='mb-45'>{t('choose_payment')}</Title>
|
||||
{ isApplePayAvailable ? <ApplePayButton /> : <GooglePayButton /> }
|
||||
<div className='payment-divider'>{t('or').toUpperCase()}</div>
|
||||
<CardButton onClick={() => setOpen(true)} />
|
||||
<p className='payment-warining'>
|
||||
{t('will_be_charged', { strongText: <strong>{t('trial_price')}</strong> })}
|
||||
</p>
|
||||
<CardModal open={open} onClose={() => setOpen(false)} />
|
||||
{ isLoading ? <Loader /> : (
|
||||
<>
|
||||
<div className='page-header'>
|
||||
{ isApplePayAvailable ? <ApplePayBanner /> : <GooglePayBanner /> }
|
||||
<img src={secure} alt='100% Secure' />
|
||||
</div>
|
||||
<Title variant='h1' className='mb-45'>{t('choose_payment')}</Title>
|
||||
{ isApplePayAvailable ? <ApplePayButton /> : <GooglePayButton /> }
|
||||
<div className='payment-divider'>{t('or').toUpperCase()}</div>
|
||||
<CardButton onClick={() => setOpen(true)} />
|
||||
<p className='payment-warining'>
|
||||
{t('will_be_charged', { strongText: <strong>{t('trial_price')}</strong> })}
|
||||
</p>
|
||||
<CardModal open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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<ChargebeeComponents>(null)
|
||||
const [status, setStatus] = useState<Status>('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 (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<div className='payment-modal-header'>
|
||||
<Title variant='h3' className='mb-0'>{t('card')}</Title>
|
||||
<div className='payment-card-list'>
|
||||
<img src={visa} alt='Visa Card' />
|
||||
<img src={mastercard} alt='Mastercard Card' />
|
||||
<img src={amex} alt='Amex Card' />
|
||||
<img src={diners} alt='Diners Card' />
|
||||
<img src={discover} alt='Discover Card' />
|
||||
<Modal open={open} onClose={handleClose}>
|
||||
{ isLoading ? <div className='payment-loader'><Loader /></div> : null}
|
||||
<div style={{ display: isLoading ? 'none' : 'block' }}>
|
||||
<div className='payment-modal-header'>
|
||||
<Title variant='h3' className='mb-0'>{t('card')}</Title>
|
||||
<div className='payment-card-list'>
|
||||
<img src={visa} alt='Visa Card' />
|
||||
<img src={mastercard} alt='Mastercard Card' />
|
||||
<img src={amex} alt='Amex Card' />
|
||||
<img src={diners} alt='Diners Card' />
|
||||
<img src={discover} alt='Discover Card' />
|
||||
</div>
|
||||
</div>
|
||||
<Provider cbInstance={chargebee}>
|
||||
<CardComponent ref={cardRef} locale={locale} styles={cardStyles} onReady={handleReady}>
|
||||
<div className="payment-input"><CardNumber onChange={handleChange} /></div>
|
||||
<div className='payment-group'>
|
||||
<div className="payment-input"><CardExpiry onChange={handleChange} /></div>
|
||||
<div className="payment-input"><CardCVV onChange={handleChange} /></div>
|
||||
</div>
|
||||
</CardComponent>
|
||||
</Provider>
|
||||
<p className='payment-inforamtion'>{t('charged_only')}</p>
|
||||
<MainButton color='blue' onClick={payWithCard} disabled={isDisabled}>
|
||||
{ isTokenizing ? <Loader /> : (
|
||||
<>
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5556 6.24219H1.44444C0.6467 6.24219 0 6.97481 0 7.87855V13.6058C0 14.5096 0.6467 15.2422 1.44444 15.2422H11.5556C12.3533 15.2422 13 14.5096 13 13.6058V7.87855C13 6.97481 12.3533 6.24219 11.5556 6.24219Z"></path>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.5 0.242188C4.29086 0.242188 2.5 2.03305 2.5 4.24219V8.24219H10.5V4.24219C10.5 2.03305 8.70914 0.242188 6.5 0.242188ZM6.5 1.24219C4.84315 1.24219 3.5 2.58533 3.5 4.24219V7.24219H9.5V4.24219C9.5 2.58533 8.15685 1.24219 6.5 1.24219Z"></path>
|
||||
</svg>
|
||||
{t('start_trial')}
|
||||
</>
|
||||
) }
|
||||
</MainButton>
|
||||
</div>
|
||||
<Provider cbInstance={chargebee}>
|
||||
<CardComponent ref={cardRef} locale={locale} icon={false} className='payment-chargebee'>
|
||||
<CardNumber className="payment-input" />
|
||||
<CardExpiry className="payment-input"/>
|
||||
<CardCVV className="payment-input"/>
|
||||
</CardComponent>
|
||||
</Provider>
|
||||
<p className='payment-inforamtion'>{t('charged_only')}</p>
|
||||
<MainButton onClick={payWithCard} disabled>
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5556 6.24219H1.44444C0.6467 6.24219 0 6.97481 0 7.87855V13.6058C0 14.5096 0.6467 15.2422 1.44444 15.2422H11.5556C12.3533 15.2422 13 14.5096 13 13.6058V7.87855C13 6.97481 12.3533 6.24219 11.5556 6.24219Z"></path>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.5 0.242188C4.29086 0.242188 2.5 2.03305 2.5 4.24219V8.24219H10.5V4.24219C10.5 2.03305 8.70914 0.242188 6.5 0.242188ZM6.5 1.24219C4.84315 1.24219 3.5 2.58533 3.5 4.24219V7.24219H9.5V4.24219C9.5 2.58533 8.15685 1.24219 6.5 1.24219Z"></path>
|
||||
</svg>
|
||||
Start 7-Day Trial
|
||||
</MainButton>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
18
src/components/PaymentPage/methods/Card/styles.ts
Normal file
18
src/components/PaymentPage/methods/Card/styles.ts
Normal file
@ -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',
|
||||
},
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -42,5 +42,6 @@ export default {
|
||||
contact_us: "Contact us",
|
||||
will_be_charged: "You will be charged only <strongText>. 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",
|
||||
},
|
||||
}
|
||||
|
||||
19
src/payment/types.ts
Normal file
19
src/payment/types.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
||||
|
||||
interface Handler {
|
||||
mountPaymentButton: (id: string) => Promise<void>
|
||||
handlePayment: () => Promise<PaymentIntent>
|
||||
}
|
||||
|
||||
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
|
||||
@ -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<void>
|
||||
handlePayment: () => Promise<PaymentIntent>
|
||||
}
|
||||
import { ApplePayHandler, GooglePayHandler } from './types'
|
||||
|
||||
export const usePayment = () => {
|
||||
const chargebee = useContext(PaymentContext)
|
||||
const [googlePay, setGooglePay] = useState(null)
|
||||
const [applePay, setApplePay] = useState<ApplePayHandler | null>(null)
|
||||
const [googlePay, setGooglePay] = useState<GooglePayHandler>(null)
|
||||
const [applePay, setApplePay] = useState<ApplePayHandler>(null)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
|
||||
Loading…
Reference in New Issue
Block a user