refact: error handling
This commit is contained in:
parent
a98faabafb
commit
2bc71feb3e
@ -1,37 +1,4 @@
|
||||
import { createContext } from 'react'
|
||||
import { createMethod } from './utils'
|
||||
import {
|
||||
User,
|
||||
Auras,
|
||||
Element,
|
||||
Elements,
|
||||
AuthTokens,
|
||||
Assets,
|
||||
AssetCategories,
|
||||
DailyForecasts,
|
||||
SubscriptionItems,
|
||||
SubscriptionCheckout,
|
||||
SubscriptionStatus,
|
||||
SubscriptionReceipts,
|
||||
PaymentIntents,
|
||||
} from './resources'
|
||||
|
||||
export interface ApiContextValue {
|
||||
auth: ReturnType<typeof createMethod<AuthTokens.Payload, AuthTokens.Response>>
|
||||
getElement: ReturnType<typeof createMethod<Element.Payload, Element.Response>>
|
||||
getElements: ReturnType<typeof createMethod<Elements.Payload, Elements.Response>>
|
||||
getUser: ReturnType<typeof createMethod<User.GetPayload, User.Response>>
|
||||
updateUser: ReturnType<typeof createMethod<User.PatchPayload, User.Response>>
|
||||
getAssets: ReturnType<typeof createMethod<Assets.Payload, Assets.Response>>
|
||||
getAssetCategories: ReturnType<typeof createMethod<AssetCategories.Payload, AssetCategories.Response>>
|
||||
getDailyForecasts: ReturnType<typeof createMethod<DailyForecasts.Payload, DailyForecasts.Response>>
|
||||
getAuras: ReturnType<typeof createMethod<Auras.Payload, Auras.Response>>
|
||||
getSubscriptionItems: ReturnType<typeof createMethod<SubscriptionItems.Payload, SubscriptionItems.Response>>
|
||||
getSubscriptionCheckout: ReturnType<typeof createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>>
|
||||
getSubscriptionStatus: ReturnType<typeof createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>>,
|
||||
getSubscriptionReceipt: ReturnType<typeof createMethod<SubscriptionReceipts.GetPayload, SubscriptionReceipts.Response>>,
|
||||
createSubscriptionReceipt: ReturnType<typeof createMethod<SubscriptionReceipts.Payload, SubscriptionReceipts.Response>>,
|
||||
createPaymentIntent: ReturnType<typeof createMethod<PaymentIntents.Payload, PaymentIntents.Response>>
|
||||
}
|
||||
import type { ApiContextValue } from './api'
|
||||
|
||||
export const ApiContext = createContext<ApiContextValue>({} as ApiContextValue)
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { ApiContextValue } from './ApiContext'
|
||||
import { createMethod } from './utils'
|
||||
import {
|
||||
User,
|
||||
@ -34,6 +33,8 @@ const api = {
|
||||
createPaymentIntent: createMethod<PaymentIntents.Payload, PaymentIntents.Response>(PaymentIntents.createRequest)
|
||||
}
|
||||
|
||||
export type ApiContextValue = typeof api
|
||||
|
||||
export function createApi(): ApiContextValue {
|
||||
return api
|
||||
}
|
||||
|
||||
59
src/api/errors.ts
Normal file
59
src/api/errors.ts
Normal file
@ -0,0 +1,59 @@
|
||||
export interface FullDomainError {
|
||||
title: string
|
||||
message?: string
|
||||
detail?: string
|
||||
source?: {
|
||||
pointer: string
|
||||
parameter: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ShortDomainError = FullDomainError['title']
|
||||
|
||||
export interface BaseDomainError {
|
||||
base: ShortDomainError[]
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
errors: BaseDomainError | FullDomainError[] | ShortDomainError[]
|
||||
}
|
||||
|
||||
export interface ErrorPayload extends ErrorResponse {
|
||||
statusCode: number
|
||||
}
|
||||
|
||||
export type MaybeError = ShortDomainError | FullDomainError | undefined
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly body: ErrorResponse['errors']
|
||||
readonly statusCode: number
|
||||
constructor(payload: ErrorPayload) {
|
||||
super('Caught an error while fetching the API Server endpoint...')
|
||||
this.name = 'ApiError'
|
||||
this.body = payload.errors
|
||||
this.statusCode = payload.statusCode
|
||||
}
|
||||
}
|
||||
|
||||
export function isErrorResponse<R>(data: R | ErrorResponse): data is ErrorResponse {
|
||||
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
|
||||
const firstError = errors.at(0)
|
||||
if (firstError === undefined) {
|
||||
return `Unknown Error occurred from Server with status code ${error.statusCode}`
|
||||
}
|
||||
if (isShortDomainError(firstError)) {
|
||||
return firstError
|
||||
}
|
||||
if ('message' in firstError && firstError.message) {
|
||||
return firstError.message
|
||||
}
|
||||
return firstError.title
|
||||
}
|
||||
|
||||
export function isShortDomainError(error: MaybeError): error is ShortDomainError {
|
||||
return typeof error === 'string'
|
||||
}
|
||||
@ -3,4 +3,5 @@ export * from './useApiCall'
|
||||
export * from './ApiContext'
|
||||
export * from './api'
|
||||
export * from './types'
|
||||
export * from './errors'
|
||||
export * from './resources'
|
||||
|
||||
@ -1,20 +1,3 @@
|
||||
export interface AuthError {
|
||||
title: string
|
||||
detail: string
|
||||
source: {
|
||||
pointer: string
|
||||
parameter: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
base: string[]
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
errors: AuthError[] | ApiError | string[]
|
||||
}
|
||||
|
||||
export type AuthToken = string
|
||||
|
||||
export interface AuthPayload {
|
||||
|
||||
@ -1,24 +1,34 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ApiError } from './errors'
|
||||
|
||||
interface HookResult<T> {
|
||||
isPending: boolean
|
||||
error: Error | null
|
||||
data: T | null
|
||||
error: ApiError | null
|
||||
isPending: boolean
|
||||
state: ApiCallState
|
||||
}
|
||||
|
||||
type ApiMethod<T> = () => Promise<T>
|
||||
type ApiCallState = 'idle' | 'pending' | 'success' | 'error'
|
||||
|
||||
export function useApiCall<T>(apiMethod: ApiMethod<T>): HookResult<T> {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [isPending, setIsPending] = useState<boolean>(true)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
const [state, setState] = useState<ApiCallState>('idle')
|
||||
const isPending = state === 'pending'
|
||||
|
||||
useEffect(() => {
|
||||
setState('pending')
|
||||
apiMethod()
|
||||
.then((data: T) => setData(data))
|
||||
.catch((error: Error) => setError(error))
|
||||
.finally(() => setIsPending(false))
|
||||
.then((data: T) => {
|
||||
setData(data)
|
||||
setState('success')
|
||||
})
|
||||
.catch((error: ApiError) => {
|
||||
setError(error)
|
||||
setState('error')
|
||||
})
|
||||
}, [apiMethod])
|
||||
|
||||
return { isPending, error, data }
|
||||
return { isPending, error, data, state }
|
||||
}
|
||||
|
||||
@ -1,25 +1,18 @@
|
||||
import { AuthToken } from "./types"
|
||||
import { ErrorResponse } from "./types"
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
}
|
||||
import { AuthToken } from './types'
|
||||
import { ErrorResponse, isErrorResponse, ApiError } from './errors'
|
||||
|
||||
export function createMethod<P, R>(createRequest: (payload: P) => Request) {
|
||||
return async (payload: P): Promise<R> => {
|
||||
const request = createRequest(payload)
|
||||
const response = await fetch(request)
|
||||
const data: R & ErrorResponse = await response.json()
|
||||
const data: R | ErrorResponse = await response.json()
|
||||
const statusCode = response.status
|
||||
|
||||
if (response.ok) return data
|
||||
if (isErrorResponse<R>(data)) {
|
||||
throw new ApiError({ ...data, statusCode })
|
||||
}
|
||||
|
||||
const error = Array.isArray(data.errors) ? data.errors[0] : data.errors.base[0]
|
||||
const errorMessage = typeof error === 'object' ? error.title : error
|
||||
|
||||
throw new ApiError(errorMessage)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'
|
||||
import { actions, selectors } from '../../store'
|
||||
import { getClientTimezone } from '../../locales'
|
||||
import { useAuth } from '../../auth'
|
||||
import { useApi } from '../../api'
|
||||
import { useApi, ApiError, extractErrorMessage } from '../../api'
|
||||
import Title from '../Title'
|
||||
import Policy from '../Policy'
|
||||
import EmailInput from './EmailInput'
|
||||
@ -24,7 +24,7 @@ function EmailEnterPage(): JSX.Element {
|
||||
const birthday = useSelector(selectors.selectBirthday)
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
const timezone = getClientTimezone()
|
||||
const locale = i18n.language
|
||||
const handleValidEmail = (email: string) => {
|
||||
@ -52,7 +52,7 @@ function EmailEnterPage(): JSX.Element {
|
||||
dispatch(actions.subscriptionPlan.setAll(item_prices))
|
||||
})
|
||||
.then(() => navigate(routes.client.subscription()))
|
||||
.catch((error: Error) => setError(error))
|
||||
.catch((error: ApiError) => setError(error))
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ function EmailEnterPage(): JSX.Element {
|
||||
<MainButton onClick={handleClick} disabled={isDisabled}>
|
||||
{isLoading ? <Loader color={LoaderColor.White} /> : t('continue')}
|
||||
</MainButton>
|
||||
<ErrorText size='medium' isShown={Boolean(error)} message={error?.message} />
|
||||
<ErrorText size='medium' isShown={Boolean(error)} message={error ? extractErrorMessage(error) : null} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import './styles.css'
|
||||
|
||||
type ErrorTextProps = {
|
||||
isShown: boolean
|
||||
message: string | null | undefined
|
||||
isShown?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useId } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
||||
import { useApi, useApiCall } from '../../../../api'
|
||||
import { useApi, useApiCall, extractErrorMessage } from '../../../../api'
|
||||
import { usePayment, ApplePayButtonOptions } from '../../../../payment'
|
||||
import { useAuth } from '../../../../auth'
|
||||
import Loader from '../../../Loader'
|
||||
@ -49,7 +49,7 @@ export function ApplePayButton(): JSX.Element {
|
||||
|
||||
return isPending ? <Loader /> : (
|
||||
<div id={buttonId} style={{ height: 60 }}>{
|
||||
error ? <ErrorText message={error.message} isShown={true} size='large'/> : null
|
||||
error ? <ErrorText message={extractErrorMessage(error)} isShown={true} size='large'/> : null
|
||||
}</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ interface ChargebeeTokenizeResult {
|
||||
vaultToken: string
|
||||
}
|
||||
|
||||
type Status = 'idle' | 'loading' | 'filling' | 'tokenizing' | 'ready' | 'success' | 'error'
|
||||
type Status = 'idle' | 'loading' | 'filling' | 'subscribing' | 'ready' | 'success' | 'error'
|
||||
|
||||
const initCompletedFields = {
|
||||
number: false,
|
||||
@ -65,8 +65,6 @@ export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
||||
const locale = i18n.language
|
||||
const isInit = status === 'idle'
|
||||
const isLoading = status === 'loading'
|
||||
const isTokenizing = status === 'tokenizing'
|
||||
const isDisabled = status !== 'ready'
|
||||
const { chargebee } = usePayment()
|
||||
const handleReady = () => setStatus('filling')
|
||||
const handleClose = () => {
|
||||
@ -77,20 +75,23 @@ export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
||||
setFields((state) => ({ ...state, [field]: complete && !error }))
|
||||
}
|
||||
const payWithCard = () => {
|
||||
setStatus('tokenizing')
|
||||
setStatus('subscribing')
|
||||
cardRef.current?.tokenize({})
|
||||
.then((result: ChargebeeTokenizeResult) => {
|
||||
return api.createSubscriptionReceipt({ token, itemPriceId, gwToken: result.token })
|
||||
})
|
||||
.then(() => console.log('Success payment by Card'))
|
||||
.then(() => navigate(routes.client.wallpaper()))
|
||||
.then(() => {
|
||||
setStatus('success')
|
||||
navigate(routes.client.wallpaper())
|
||||
console.log('Success payment by Card')
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setStatus('success'))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== 'filling' && status !== 'ready') return
|
||||
setStatus(isReady(fields) ? 'ready' : 'filling')
|
||||
}, [fields])
|
||||
}, [fields, status])
|
||||
|
||||
useEffect(() => {
|
||||
if (isInit) setStatus('loading')
|
||||
@ -120,8 +121,8 @@ export function CardModal({ open, onClose }: CardModalProps): JSX.Element {
|
||||
</CardComponent>
|
||||
</Provider>
|
||||
<p className='payment-inforamtion'>{t('charged_only')}</p>
|
||||
<MainButton color='blue' onClick={payWithCard} disabled={isDisabled}>
|
||||
{ isTokenizing ? <Loader /> : (
|
||||
<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>
|
||||
|
||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useId } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { PaymentIntent } from '@chargebee/chargebee-js-types'
|
||||
import { useApi, useApiCall } from '../../../../api'
|
||||
import { extractErrorMessage, useApi, useApiCall } from '../../../../api'
|
||||
import { usePayment, GooglePayButtonOptions } from '../../../../payment'
|
||||
import { useAuth } from '../../../../auth'
|
||||
import Loader from '../../../Loader'
|
||||
@ -48,7 +48,7 @@ export function GooglePayButton(): JSX.Element {
|
||||
|
||||
return isPending ? <Loader /> : (
|
||||
<div id={buttonId} style={{ height: 60 }}>{
|
||||
error ? <ErrorText message={error.message} isShown={true} size='large'/> : null
|
||||
error ? <ErrorText message={extractErrorMessage(error)} isShown={true} size='large'/> : null
|
||||
}</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user