fix: switch to 3D Serure method for card payment

This commit is contained in:
Aidar Shaikhutdin @makeweb.space 2023-06-20 16:26:11 +03:00
parent 75358b9362
commit 94271bacd0
4 changed files with 41 additions and 34 deletions

View File

@ -9,6 +9,7 @@ import ErrorText from '../../../ErrorText'
const currencyCode = 'USD'
const paymentMethod = 'apple_pay'
const buttonId = 'apple-pay-btn'
interface ApplePayButtonProps {
onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void
@ -17,7 +18,6 @@ interface ApplePayButtonProps {
export function ApplePayButton({ onSuccess, onError }: ApplePayButtonProps): JSX.Element {
const api = useApi()
const buttonId = 'apple-pay-btn'
const { i18n } = useTranslation()
const { token } = useAuth()
const { applePay } = usePayment()
@ -48,14 +48,12 @@ export function ApplePayButton({ onSuccess, onError }: ApplePayButtonProps): JSX
})
.then(({ subscription_receipt }: SubscriptionReceipts.Response) => onSuccess(subscription_receipt))
.catch((error: Error) => onError(error))
}, [data, applePay, buttonId, i18n.language, api, token, onSuccess, onError])
}, [data, applePay, i18n.language, api, token, onSuccess, onError])
return (
<>
<div id={buttonId} className='pay-btn'>
{isPending || isMounting ? <Loader /> : null}
<div id={buttonId} className='pay-btn'>
{error ? <ErrorText message={extractErrorMessage(error)} isShown={true} size='large'/> : null}
</div>
</>
{error ? <ErrorText message={extractErrorMessage(error)} isShown={true} size='large'/> : null}
</div>
)
}

View File

@ -1,11 +1,12 @@
import { useEffect, useRef, useState, ChangeEvent } from 'react'
import { useCallback, useEffect, useRef, useState, ChangeEvent } from 'react'
import { useTranslation } from 'react-i18next'
import {
CardCVV, CardComponent, CardExpiry, CardNumber, Provider
} from '@chargebee/chargebee-js-react-wrapper'
import ChargebeeComponents from '@chargebee/chargebee-js-react-wrapper/dist/components/ComponentGroup'
import { PaymentIntent } from '@chargebee/chargebee-js-types'
import { usePayment } from '../../../../payment'
import { useApi, SubscriptionReceipts } from '../../../../api'
import { useApi, SubscriptionReceipts, useApiCall } from '../../../../api'
import { useAuth } from '../../../../auth'
import Modal from '../../../Modal'
import Title from '../../../Title'
@ -35,11 +36,6 @@ interface Field {
type: string
}
interface ChargebeeTokenizeResult {
token: string
vaultToken: string
}
type Status = 'idle' | 'loading' | 'filling' | 'subscribing' | 'ready' | 'success' | 'error'
const initCompletedFields = {
@ -52,6 +48,8 @@ type CompletedFields = typeof initCompletedFields
const isReady = (fields: CompletedFields) => Object.values(fields).every((complete: boolean) => complete)
const currencyCode = 'USD'
const paymentMethod = 'card'
const itemPriceId = 'aura-membership-2-week-USD'
export function CardModal({ open, onClose, onSuccess, onError }: CardModalProps): JSX.Element {
@ -59,12 +57,17 @@ export function CardModal({ open, onClose, onSuccess, onError }: CardModalProps)
const cardRef = useRef<ChargebeeComponents>(null)
const [status, setStatus] = useState<Status>('idle')
const [fields, setFields] = useState(initCompletedFields)
const { token } = useAuth()
const { token, user } = useAuth()
const { t, i18n } = useTranslation()
const locale = i18n.language
const isInit = status === 'idle'
const isLoading = status === 'loading'
const { chargebee } = usePayment()
const loadData = useCallback(() => {
return api.createPaymentIntent({ token, paymentMethod, currencyCode })
.then(({ payment_intent }) => payment_intent)
}, [api, token])
const { data, error } = useApiCall<PaymentIntent>(loadData)
const handleReady = () => setStatus('filling')
const handleClose = () => {
setStatus('loading')
@ -74,10 +77,11 @@ export function CardModal({ open, onClose, onSuccess, onError }: CardModalProps)
setFields((state) => ({ ...state, [field]: complete && !error }))
}
const payWithCard = () => {
if (data === null) return
setStatus('subscribing')
cardRef.current?.tokenize({})
.then((result: ChargebeeTokenizeResult) => {
return api.createSubscriptionReceipt({ token, itemPriceId, gwToken: result.vaultToken })
cardRef.current?.authorizeWith3ds(data, { email: user?.email }, {})
.then((paymentIntent: PaymentIntent) => {
return api.createSubscriptionReceipt({ token, itemPriceId, gwToken: paymentIntent.id })
})
.then(({ subscription_receipt }: SubscriptionReceipts.Response) => {
setStatus('success')
@ -89,6 +93,8 @@ export function CardModal({ open, onClose, onSuccess, onError }: CardModalProps)
})
}
if (error) console.error(error)
useEffect(() => {
if (status !== 'filling' && status !== 'ready' && status !== 'error') return
setStatus(isReady(fields) ? 'ready' : 'filling')
@ -123,17 +129,18 @@ export function CardModal({ open, onClose, onSuccess, onError }: CardModalProps)
</Provider>
<p className='payment-inforamtion'>{t('charged_only')}</p>
<MainButton color='blue' onClick={payWithCard} disabled={status !== 'ready'}>
{ status === 'subscribing' ? <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')}
</>
) }
{ status === 'subscribing' ? <Loader /> : <><LockIcon />{t('start_trial')}</> }
</MainButton>
</div>
</Modal>
)
}
function LockIcon(): JSX.Element {
return (
<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>
)
}

View File

@ -9,6 +9,7 @@ import ErrorText from '../../../ErrorText'
const currencyCode = 'USD'
const paymentMethod = 'google_pay'
const buttonId = 'google-pay-btn'
interface GooglePayButtonProps {
onSuccess: (receipt: SubscriptionReceipts.SubscriptionReceipt) => void
@ -17,7 +18,6 @@ interface GooglePayButtonProps {
export function GooglePayButton({ onSuccess, onError }: GooglePayButtonProps): JSX.Element {
const api = useApi()
const buttonId = 'google-pay-btn'
const { i18n } = useTranslation()
const { token } = useAuth()
const { googlePay } = usePayment()
@ -48,14 +48,12 @@ export function GooglePayButton({ onSuccess, onError }: GooglePayButtonProps): J
})
.then(() => onSuccess({} as SubscriptionReceipts.SubscriptionReceipt))
.catch((error: Error) => onError(error))
}, [data, googlePay, buttonId, i18n.language, onSuccess, onError])
}, [data, googlePay, i18n.language, onSuccess, onError])
return (
<>
<div id={buttonId} className='pay-btn'>
{isPending || isMounting ? <Loader /> : null}
<div id={buttonId} className='pay-btn'>
{error ? <ErrorText message={extractErrorMessage(error)} isShown={true} size='large'/> : null}
</div>
</>
{error ? <ErrorText message={extractErrorMessage(error)} isShown={true} size='large'/> : null}
</div>
)
}

View File

@ -101,6 +101,10 @@
}
.pay-btn {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 60px;
max-width: 400px;
min-width: 250px;