feat: Chargebee card component
This commit is contained in:
parent
70a00fd89a
commit
5522b2f1f7
@ -8,12 +8,13 @@ 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">
|
||||
|
||||
@ -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,6 +29,8 @@ function PaymentPage(): JSX.Element {
|
||||
<>
|
||||
<UserHeader email={email} />
|
||||
<section className='page'>
|
||||
{ isLoading ? <Loader /> : (
|
||||
<>
|
||||
<div className='page-header'>
|
||||
{ isApplePayAvailable ? <ApplePayBanner /> : <GooglePayBanner /> }
|
||||
<img src={secure} alt='100% Secure' />
|
||||
@ -39,6 +43,8 @@ function PaymentPage(): JSX.Element {
|
||||
{t('will_be_charged', { strongText: <strong>{t('trial_price')}</strong> })}
|
||||
</p>
|
||||
<CardModal open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,33 +1,87 @@
|
||||
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}>
|
||||
<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'>
|
||||
@ -39,20 +93,27 @@ export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
||||
</div>
|
||||
</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 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 onClick={payWithCard} disabled>
|
||||
<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>
|
||||
Start 7-Day Trial
|
||||
{t('start_trial')}
|
||||
</>
|
||||
) }
|
||||
</MainButton>
|
||||
</div>
|
||||
</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