From 8a37644d1c85f8991b811531360f42039a8b3fd5 Mon Sep 17 00:00:00 2001 From: "Aidar Shaikhutdin @makeweb.space" Date: Mon, 19 Jun 2023 19:17:43 +0300 Subject: [PATCH] feat: add error handling during the card payment process --- src/api/errors.ts | 40 +++++++++++++----- src/api/utils.ts | 9 +++- src/components/PaymentPage/ErrorModal.tsx | 42 +++++++++++++++++++ src/components/PaymentPage/index.tsx | 19 +++++++-- .../PaymentPage/methods/Card/Modal.tsx | 2 +- src/components/PaymentPage/styles.css | 12 ++++++ src/components/Policy/index.tsx | 5 ++- src/components/SubscriptionPage/index.tsx | 11 ++--- src/index.css | 8 ++++ src/locales/dev.ts | 5 +++ 10 files changed, 126 insertions(+), 27 deletions(-) create mode 100644 src/components/PaymentPage/ErrorModal.tsx diff --git a/src/api/errors.ts b/src/api/errors.ts index 598cfa1..d5a9427 100644 --- a/src/api/errors.ts +++ b/src/api/errors.ts @@ -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(data: R | ErrorResponse): data is ErrorResponse { + return isSingleErrorResponse(data) || isErrorListResponse(data) +} + +export function isSingleErrorResponse(data: R | ErrorResponse): data is SingleErrorResponse { + return typeof data === 'object' && data !== null && 'error' in data +} + +export function isErrorListResponse(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' -} diff --git a/src/api/utils.ts b/src/api/utils.ts index 8e1447d..6464a96 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -1,5 +1,5 @@ import { AuthToken } from './types' -import { ErrorResponse, isErrorResponse, ApiError } from './errors' +import { ErrorResponse, isErrorResponse, ApiError, buildUnknownError } from './errors' export function createMethod(createRequest: (payload: P) => Request) { return async (payload: P): Promise => { @@ -8,8 +8,13 @@ export function createMethod(createRequest: (payload: P) => Request) { const data: R | ErrorResponse = await response.json() const statusCode = response.status + if (!response.ok) { + const body = isErrorResponse(data) ? data : { error: buildUnknownError(statusCode) } + throw new ApiError({ body, statusCode }) + } + if (isErrorResponse(data)) { - throw new ApiError({ ...data, statusCode }) + throw new ApiError({ body: data, statusCode }) } return data diff --git a/src/components/PaymentPage/ErrorModal.tsx b/src/components/PaymentPage/ErrorModal.tsx new file mode 100644 index 0000000..c737cde --- /dev/null +++ b/src/components/PaymentPage/ErrorModal.tsx @@ -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 = {t('our_support')} + return ( + +
+
+ + + + + + + + + + + + + +
+ {t('error_processing')} +
{t('please_try_again')}
+ {t('any_dificulties', { supportLink })} + {t('try_again')} +
+
+ ) +} + +export default ErrorModal \ No newline at end of file diff --git a/src/components/PaymentPage/index.tsx b/src/components/PaymentPage/index.tsx index a8c3fa8..ebdb2d0 100644 --- a/src/components/PaymentPage/index.tsx +++ b/src/components/PaymentPage/index.tsx @@ -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 { : }
{t('or').toUpperCase()}
- setOpen(true)} /> + setOpenCardModal(true)} />

{t('will_be_charged', { strongText: {t('trial_price')} })}

- setOpen(false)} onSuccess={onSuccess} onError={onError} /> + setOpenCardModal(false)} + onSuccess={onSuccess} + onError={onError} + /> + setOpenErrorModal(false)} /> )} diff --git a/src/components/PaymentPage/methods/Card/Modal.tsx b/src/components/PaymentPage/methods/Card/Modal.tsx index c570787..bed6297 100644 --- a/src/components/PaymentPage/methods/Card/Modal.tsx +++ b/src/components/PaymentPage/methods/Card/Modal.tsx @@ -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]) diff --git a/src/components/PaymentPage/styles.css b/src/components/PaymentPage/styles.css index 33e7784..c43074b 100644 --- a/src/components/PaymentPage/styles.css +++ b/src/components/PaymentPage/styles.css @@ -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; +} \ No newline at end of file diff --git a/src/components/Policy/index.tsx b/src/components/Policy/index.tsx index 06a2930..b62527d 100644 --- a/src/components/Policy/index.tsx +++ b/src/components/Policy/index.tsx @@ -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 ( -
+

{children}

) diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx index 465d6f7..e28d884 100644 --- a/src/components/SubscriptionPage/index.tsx +++ b/src/components/SubscriptionPage/index.tsx @@ -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 = {t('subscription_policy')} console.log({ itemPrice }) return ( <> @@ -36,11 +37,7 @@ function SubscriptionPage(): JSX.Element { {t('get_access')} - - {t('subscription_text', { - policyLink: {t('subscription_policy')}, - })} - + {t('subscription_text', { policyLink })} ) diff --git a/src/index.css b/src/index.css index cc3a117..d107c1d 100644 --- a/src/index.css +++ b/src/index.css @@ -110,3 +110,11 @@ a,button,div,input,select,textarea { .pa { position: absolute; } + +.ta-c { + text-align: center; +} + +.ta-l { + text-align: left; +} diff --git a/src/locales/dev.ts b/src/locales/dev.ts index ac5a192..422bc95 100644 --- a/src/locales/dev.ts +++ b/src/locales/dev.ts @@ -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 ", + our_support: "our support", + try_again: "Try again", }, }