feat: add error handling during the card payment process
This commit is contained in:
parent
8229ab3292
commit
8a37644d1c
@ -14,36 +14,58 @@ export interface BaseDomainError {
|
||||
base: ShortDomainError[]
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
export interface ErrorListResponse {
|
||||
errors: BaseDomainError | FullDomainError[] | ShortDomainError[]
|
||||
}
|
||||
|
||||
export interface ErrorPayload extends ErrorResponse {
|
||||
export interface SingleErrorResponse {
|
||||
error: ShortDomainError
|
||||
}
|
||||
|
||||
export type ErrorResponse = ErrorListResponse | SingleErrorResponse
|
||||
|
||||
export type ErrorPayload = {
|
||||
body: ErrorResponse
|
||||
statusCode: number
|
||||
}
|
||||
|
||||
export type MaybeError = ShortDomainError | FullDomainError | undefined
|
||||
|
||||
export const buildUnknownError = (statusCode: number) => `Unknown Error occurred from Server with status code ${statusCode}`
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly body: ErrorResponse['errors']
|
||||
readonly body: ErrorResponse
|
||||
readonly statusCode: number
|
||||
constructor(payload: ErrorPayload) {
|
||||
super('Caught an error while fetching the API Server endpoint...')
|
||||
this.name = 'ApiError'
|
||||
this.body = payload.errors
|
||||
this.body = payload.body
|
||||
this.statusCode = payload.statusCode
|
||||
}
|
||||
}
|
||||
|
||||
export function isErrorResponse<R>(data: R | ErrorResponse): data is ErrorResponse {
|
||||
return isSingleErrorResponse<R>(data) || isErrorListResponse<R>(data)
|
||||
}
|
||||
|
||||
export function isSingleErrorResponse<R>(data: R | ErrorResponse): data is SingleErrorResponse {
|
||||
return typeof data === 'object' && data !== null && 'error' in data
|
||||
}
|
||||
|
||||
export function isErrorListResponse<R>(data: R | ErrorResponse): data is ErrorListResponse {
|
||||
return typeof data === 'object' && data !== null && 'errors' in data
|
||||
}
|
||||
|
||||
export function extractErrorMessage(error: ApiError): string {
|
||||
const errors = Array.isArray(error.body) ? error.body : error.body.base
|
||||
export function isShortDomainError(error: MaybeError): error is ShortDomainError {
|
||||
return typeof error === 'string'
|
||||
}
|
||||
|
||||
export function extractErrorMessage(apiError: ApiError): string {
|
||||
const body = isSingleErrorResponse(apiError.body) ? [apiError.body.error] : apiError.body.errors
|
||||
const errors = Array.isArray(body) ? body : body.base
|
||||
const firstError = errors.at(0)
|
||||
if (firstError === undefined) {
|
||||
return `Unknown Error occurred from Server with status code ${error.statusCode}`
|
||||
return buildUnknownError(apiError.statusCode)
|
||||
}
|
||||
if (isShortDomainError(firstError)) {
|
||||
return firstError
|
||||
@ -53,7 +75,3 @@ export function extractErrorMessage(error: ApiError): string {
|
||||
}
|
||||
return firstError.title
|
||||
}
|
||||
|
||||
export function isShortDomainError(error: MaybeError): error is ShortDomainError {
|
||||
return typeof error === 'string'
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { AuthToken } from './types'
|
||||
import { ErrorResponse, isErrorResponse, ApiError } from './errors'
|
||||
import { ErrorResponse, isErrorResponse, ApiError, buildUnknownError } from './errors'
|
||||
|
||||
export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
||||
return async (payload: P): Promise<R> => {
|
||||
@ -8,8 +8,13 @@ export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
||||
const data: R | ErrorResponse = await response.json()
|
||||
const statusCode = response.status
|
||||
|
||||
if (!response.ok) {
|
||||
const body = isErrorResponse<R>(data) ? data : { error: buildUnknownError(statusCode) }
|
||||
throw new ApiError({ body, statusCode })
|
||||
}
|
||||
|
||||
if (isErrorResponse<R>(data)) {
|
||||
throw new ApiError({ ...data, statusCode })
|
||||
throw new ApiError({ body: data, statusCode })
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
42
src/components/PaymentPage/ErrorModal.tsx
Normal file
42
src/components/PaymentPage/ErrorModal.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MainButton from '../MainButton'
|
||||
import Modal from '../Modal'
|
||||
import Title from '../Title'
|
||||
import Policy from '../Policy'
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function ErrorModal({ open, onClose }: ModalProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const supportLink = <a href='https://aura.wit.life/' target='_blank' rel='noopener noreferrer'>{t('our_support')}</a>
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<div className='modal-body error-modal'>
|
||||
<div className='ta-c mb-24'>
|
||||
<svg fill="#000000" height="60px" width="60px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 27.963 27.963" xmlSpace="preserve">
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g>
|
||||
<g id="c129_exclamation">
|
||||
<path d="M13.983,0C6.261,0,0.001,6.259,0.001,13.979c0,7.724,6.26,13.984,13.982,13.984s13.98-6.261,13.98-13.984 C27.963,6.259,21.705,0,13.983,0z M13.983,26.531c-6.933,0-12.55-5.62-12.55-12.553c0-6.93,5.617-12.548,12.55-12.548 c6.931,0,12.549,5.618,12.549,12.548C26.531,20.911,20.913,26.531,13.983,26.531z"></path>
|
||||
<polygon points="15.579,17.158 16.191,4.579 11.804,4.579 12.414,17.158 "></polygon>
|
||||
<path d="M13.998,18.546c-1.471,0-2.5,1.029-2.5,2.526c0,1.443,0.999,2.528,2.444,2.528h0.056c1.499,0,2.469-1.085,2.469-2.528 C16.441,19.575,15.468,18.546,13.998,18.546z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<Title variant='h3'>{t('error_processing')}</Title>
|
||||
<blockquote className='blockquote'>{t('please_try_again')}</blockquote>
|
||||
<Policy className='mb-24 ta-l' sizing='medium'>{t('any_dificulties', { supportLink })}</Policy>
|
||||
<MainButton color='blue' onClick={onClose}>{t('try_again')}</MainButton>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorModal
|
||||
@ -13,6 +13,7 @@ import {
|
||||
CardButton,
|
||||
CardModal
|
||||
} from './methods'
|
||||
import ErrorModal from './ErrorModal'
|
||||
import UserHeader from '../UserHeader'
|
||||
import Title from '../Title'
|
||||
import Loader from '../Loader'
|
||||
@ -23,7 +24,8 @@ import './styles.css'
|
||||
function PaymentPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const { applePay } = usePayment()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [openCardModal, setOpenCardModal] = useState(false)
|
||||
const [openErrorModal, setOpenErrorModal] = useState(false)
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const isLoading = applePay === null
|
||||
@ -33,7 +35,10 @@ function PaymentPage(): JSX.Element {
|
||||
dispatch(actions.status.update('subscribed'))
|
||||
navigate(routes.client.wallpaper())
|
||||
}, [dispatch, navigate])
|
||||
const onError = useCallback((error: Error) => console.error(error), [])
|
||||
const onError = useCallback((error: Error) => {
|
||||
console.error(error)
|
||||
setOpenErrorModal(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -52,11 +57,17 @@ function PaymentPage(): JSX.Element {
|
||||
:
|
||||
<GooglePayButton onSuccess={onSuccess} onError={onError} /> }
|
||||
<div className='payment-divider'>{t('or').toUpperCase()}</div>
|
||||
<CardButton onClick={() => setOpen(true)} />
|
||||
<CardButton onClick={() => setOpenCardModal(true)} />
|
||||
<p className='payment-warining'>
|
||||
{t('will_be_charged', { strongText: <strong>{t('trial_price')}</strong> })}
|
||||
</p>
|
||||
<CardModal open={open} onClose={() => setOpen(false)} onSuccess={onSuccess} onError={onError} />
|
||||
<CardModal
|
||||
open={openCardModal}
|
||||
onClose={() => setOpenCardModal(false)}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/>
|
||||
<ErrorModal open={openErrorModal} onClose={() => setOpenErrorModal(false)} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@ -90,7 +90,7 @@ export function CardModal({ open, onClose, onSuccess, onError }: CardModalProps)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'filling' && status !== 'ready') return
|
||||
if (status !== 'filling' && status !== 'ready' && status !== 'error') return
|
||||
setStatus(isReady(fields) ? 'ready' : 'filling')
|
||||
}, [fields, status])
|
||||
|
||||
|
||||
@ -87,3 +87,15 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-modal .title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.blockquote {
|
||||
margin: 12px 0;
|
||||
border-left: 3px solid #000;
|
||||
padding-left: 12px;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
@ -3,6 +3,7 @@ import './styles.css'
|
||||
interface PolicyProps {
|
||||
children: string
|
||||
sizing?: 'small' | 'medium' | 'large'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
@ -11,9 +12,9 @@ const sizes = {
|
||||
large: 'policy--large',
|
||||
}
|
||||
|
||||
function Policy({ children, sizing = 'small' }: PolicyProps): JSX.Element {
|
||||
function Policy({ children, sizing = 'small', className = '' }: PolicyProps): JSX.Element {
|
||||
return (
|
||||
<div className={`policy ${sizes[sizing]}`}>
|
||||
<div className={`policy ${sizes[sizing]} ${className}`}>
|
||||
<p>{children}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -10,6 +10,8 @@ import UserHeader from '../UserHeader'
|
||||
import CallToAction from '../CallToAction'
|
||||
import routes from '../../routes'
|
||||
|
||||
const currency = Currency.USD
|
||||
const locale = Locale.EN
|
||||
const itemPriceId = 'aura-membership-2-week-USD'
|
||||
const paymentItems = [
|
||||
{
|
||||
@ -24,9 +26,8 @@ function SubscriptionPage(): JSX.Element {
|
||||
const navigate = useNavigate()
|
||||
const email = useSelector(selectors.selectEmail)
|
||||
const itemPrice = useSelector(selectors.selectPlanById(itemPriceId))
|
||||
const currency = Currency.USD
|
||||
const locale = Locale.EN
|
||||
const handleClick = () => navigate(routes.client.paymentMethod())
|
||||
const policyLink = <a href='https://aura.wit.life/' target='_blank' rel='noopener noreferrer'>{t('subscription_policy')}</a>
|
||||
console.log({ itemPrice })
|
||||
return (
|
||||
<>
|
||||
@ -36,11 +37,7 @@ function SubscriptionPage(): JSX.Element {
|
||||
<Countdown start={10}/>
|
||||
<PaymentTable items={paymentItems} currency={currency} locale={locale}/>
|
||||
<MainButton onClick={handleClick}>{t('get_access')}</MainButton>
|
||||
<Policy>
|
||||
{t('subscription_text', {
|
||||
policyLink: <a href='https://aura.wit.life/' target='_blank' rel='noopener noreferrer'>{t('subscription_policy')}</a>,
|
||||
})}
|
||||
</Policy>
|
||||
<Policy>{t('subscription_text', { policyLink })}</Policy>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -110,3 +110,11 @@ a,button,div,input,select,textarea {
|
||||
.pa {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ta-c {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ta-l {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@ -44,5 +44,10 @@ export default {
|
||||
trial_price: "$1 for your 7-day trial",
|
||||
start_trial: "Start 7-Day Trial",
|
||||
analysis_background: "Analysis of personal background",
|
||||
error_processing: "An error processing your order.",
|
||||
please_try_again: "Please try again.",
|
||||
any_dificulties: "If you have any difficulties with solving this problem, do not hesitate to contact <supportLink>",
|
||||
our_support: "our support",
|
||||
try_again: "Try again",
|
||||
},
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user