Skip to content

Commit

Permalink
IMP-198: Add retry payment. Refactoring errors, result components (#299)
Browse files Browse the repository at this point in the history
  • Loading branch information
KeinAsylum authored Apr 18, 2024
1 parent dca5bd7 commit 8f2c9c6
Show file tree
Hide file tree
Showing 22 changed files with 215 additions and 219 deletions.
4 changes: 3 additions & 1 deletion src/common/paymentCondition/initPaymentCondition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,14 @@ const provideInvoiceUnpaid = async (model: PaymentModelInvoice): Promise<Payment
invoiceParams.invoiceID,
GET_INVOICE_EVENTS_LIMIT,
);
const conditions = invoiceEventsToConditions(events, skipUserInteraction);
const isInstantPayment = false;
const conditions = invoiceEventsToConditions(events, skipUserInteraction, isInstantPayment);
lastEventId = last(events).id;
const lastCondition = last(conditions);
if (!isNil(lastCondition)) {
switch (lastCondition.name) {
case 'paymentStarted':
case 'paymentStatusChanged':
case 'interactionRequested':
case 'interactionCompleted':
return conditions;
Expand Down
1 change: 1 addition & 0 deletions src/common/paymentCondition/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type PaymentStarted = {
eventId: number;
paymentId: string;
provider?: string;
isInstantPayment: boolean;
};

export type PaymentStatusChanged = {
Expand Down
12 changes: 9 additions & 3 deletions src/common/paymentCondition/utils/invoiceEventsToConditions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InvoiceEvent, PaymentStarted, UserInteraction } from '../../../common/backend/payments';
import { isNil } from '../../../common/utils';
import { InvoiceEvent, PaymentStarted, UserInteraction } from 'checkout/backend/payments';
import { isNil } from 'checkout/utils';

import { Interaction, PaymentCondition } from '../types';

const getProvider = (started: PaymentStarted): string | null => {
Expand Down Expand Up @@ -32,7 +33,11 @@ const toInteraction = (userInteraction: UserInteraction, skipUserInteraction: bo
}
};

export const invoiceEventsToConditions = (events: InvoiceEvent[], skipUserInteraction: boolean): PaymentCondition[] => {
export const invoiceEventsToConditions = (
events: InvoiceEvent[],
skipUserInteraction: boolean,
isInstantPayment: boolean,
): PaymentCondition[] => {
return events.reduce((result, { changes, id }) => {
const conditions = changes.reduce((acc, change) => {
switch (change.changeType) {
Expand All @@ -44,6 +49,7 @@ export const invoiceEventsToConditions = (events: InvoiceEvent[], skipUserIntera
eventId: id,
provider: getProvider(change),
paymentId: change.payment.id,
isInstantPayment,
},
];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { PollingResult } from 'checkout/paymentMgmt';

import { invoiceEventsToConditions } from './invoiceEventsToConditions';
import { PollingResult } from '../../paymentMgmt';
import { PaymentCondition } from '../types';

export const pollingResultToConditions = (
pollingResult: PollingResult,
skipUserInteraction: boolean = false,
isInstantPayment: boolean = false,
): PaymentCondition[] => {
switch (pollingResult.status) {
case 'TIMEOUT':
Expand All @@ -14,6 +16,6 @@ export const pollingResultToConditions = (
},
];
case 'POLLED':
return invoiceEventsToConditions(pollingResult.events, skipUserInteraction);
return invoiceEventsToConditions(pollingResult.events, skipUserInteraction, isInstantPayment);
}
};
12 changes: 7 additions & 5 deletions src/common/paymentCondition/utils/provideInstantPayment.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { StartPaymentPayload, createPayment, determineModel, pollInvoiceEvents } from 'checkout/paymentMgmt';
import { InvoiceContext, PaymentModel } from 'checkout/paymentModel';
import { extractError } from 'checkout/utils';
import { findMetadata } from 'checkout/utils/findMetadata';

import { pollingResultToConditions } from './pollingResultToConditions';
import { toDefaultFormValuesMetadata } from './toDefaultFormValuesMetadata';
import { StartPaymentPayload, createPayment, determineModel, pollInvoiceEvents } from '../../paymentMgmt';
import { InvoiceContext, PaymentModel } from '../../paymentModel';
import { extractError } from '../../utils';
import { findMetadata } from '../../utils/findMetadata';
import { InvoiceDetermined, PaymentCondition } from '../types';

const getInvoiceContext = async (model: PaymentModel): Promise<InvoiceContext> => {
Expand Down Expand Up @@ -61,7 +62,8 @@ export const provideInstantPayment = async (
name: 'invoiceDetermined',
invoiceContext,
};
const conditions = pollingResultToConditions(pollingResult, skipUserInteraction);
const isInstantPayment = true;
const conditions = pollingResultToConditions(pollingResult, skipUserInteraction, isInstantPayment);
return [invoiceDetermined, ...conditions];
} catch (exception) {
console.error(`provideInstantPayment error. ${extractError(exception)}`);
Expand Down
9 changes: 5 additions & 4 deletions src/components/AppLayout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { lazy, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ThemeProvider } from 'styled-components';

import { CustomizationContext } from 'checkout/contexts';
import { InitParams } from 'checkout/init';
import { getTheme } from 'checkout/theme';

import { ErrorBoundaryFallback } from './ErrorBoundaryFallback';
import { ModalError } from './ModalError';
import { useInitModels } from './useInitModels';
import { toCustomizationContext } from './utils';
import { CustomizationContext } from '../../common/contexts';
import { InitParams } from '../../common/init';
import { getTheme } from '../../common/theme';
import { ErrorBoundaryFallback } from '../legacy';

type AppLayoutProps = {
initParams: InitParams;
Expand Down
23 changes: 23 additions & 0 deletions src/components/AppLayout/ErrorBoundaryFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Alert, AlertDescription, AlertIcon, AlertTitle, Button } from '@chakra-ui/react';

export const ErrorBoundaryFallback = () => (
<Alert
alignItems="center"
flexDirection="column"
gap={3}
height="250px"
justifyContent="center"
status="error"
textAlign="center"
variant="subtle"
>
<AlertIcon boxSize="40px" mr={0} />
<AlertTitle fontSize="lg" mb={1} mt={4}>
Something went wrong.
</AlertTitle>
<AlertDescription maxWidth="sm">Try reloading.</AlertDescription>
<Button colorScheme="teal" onClick={() => location.reload()}>
Reload
</Button>
</Alert>
);
84 changes: 40 additions & 44 deletions src/components/ViewContainer/PaymentProcessFailedView.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,9 @@
import { useContext } from 'react';
import styled from 'styled-components';
import { WarningIcon } from '@chakra-ui/icons';
import { Flex, Spacer, VStack, Text, Button } from '@chakra-ui/react';
import { useContext, useMemo } from 'react';

import { LocaleContext, PaymentConditionsContext } from '../../common/contexts';
import { isNil, last } from '../../common/utils';
import { Button, ErrorIcon } from '../legacy';

const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0;
gap: 24px;
`;

const Label = styled.h2`
font-weight: 500;
font-size: 32px;
color: ${({ theme }) => theme.font.primaryColor};
line-height: 48px;
text-align: center;
margin: 0;
`;

const Description = styled.p`
font-weight: 500;
font-size: 16px;
color: ${({ theme }) => theme.font.primaryColor};
line-height: 24px;
text-align: center;
margin: 0;
`;
import { LocaleContext, PaymentConditionsContext } from 'checkout/contexts';
import { isNil, last } from 'checkout/utils';

const isResponseErrorWithMessage = (error: any): boolean => {
if (isNil(error)) return false;
Expand All @@ -50,20 +24,42 @@ export function PaymentProcessFailedView() {
const { l } = useContext(LocaleContext);
const { conditions } = useContext(PaymentConditionsContext);

const lastCondition = last(conditions);

if (lastCondition.name !== 'paymentProcessFailed') {
throw new Error(`Wrong payment condition type. Expected: paymentProcessFailed, actual: ${lastCondition.name}`);
}
const exception = useMemo(() => {
const lastCondition = last(conditions);
if (lastCondition.name === 'paymentProcessFailed') {
return lastCondition.exception;
}
return null;
}, [conditions]);

return (
<Wrapper>
<ErrorIcon />
<Label>{l['form.header.final.error.label']}</Label>
<Description>{getErrorDescription(lastCondition.exception)}</Description>
<Button color="primary" onClick={() => location.reload()}>
{l['form.button.reload']}
</Button>
</Wrapper>
<VStack align="stretch" minH="sm" spacing={6}>
<Spacer />
<Flex justifyContent="center">
<WarningIcon boxSize="28" color="red.500" />
</Flex>
<VStack align="stretch" spacing={3}>
<Text color="red.500" fontSize="4xl" fontWeight="medium" textAlign="center">
{l['form.header.final.error.label']}
</Text>
{!isNil(exception) && (
<Text fontSize="lg" textAlign="center">
{getErrorDescription(exception)}
</Text>
)}
</VStack>
<Spacer />
<VStack align="stretch" spacing={6}>
<Button
borderRadius="xl"
colorScheme="teal"
size="lg"
variant="solid"
onClick={() => location.reload()}
>
{l['form.button.reload']}
</Button>
</VStack>
</VStack>
);
}
100 changes: 44 additions & 56 deletions src/components/ViewContainer/PaymentResultView/PaymentResultView.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,17 @@
import { useContext, useEffect } from 'react';
import styled from 'styled-components';
import { Button, Flex, Spacer, VStack, Text } from '@chakra-ui/react';
import { useCallback, useContext, useEffect } from 'react';

import {
CompletePaymentContext,
LocaleContext,
PaymentConditionsContext,
PaymentModelContext,
ViewModelContext,
} from 'checkout/contexts';
import { isNil, last } from 'checkout/utils';

import { IconName } from './types';
import { getResultInfo } from './utils';
import { Button, ErrorIcon, Link, SuccessIcon, WarningIcon } from '../../../components/legacy';

const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0;
gap: 24px;
`;

const Label = styled.h2`
font-weight: 500;
font-size: 32px;
color: ${({ theme }) => theme.font.primaryColor};
line-height: 48px;
text-align: center;
margin: 0;
`;

const Description = styled.p`
font-weight: 500;
font-size: 16px;
color: ${({ theme }) => theme.font.primaryColor};
line-height: 24px;
text-align: center;
margin: 0;
`;

const OthersButton = styled(Link)`
padding-top: 12px;
`;

const ResultIcon = ({ iconName }: { iconName: IconName }) => (
<>
{iconName === 'SuccessIcon' && <SuccessIcon />}
{iconName === 'WarningIcon' && <WarningIcon />}
{iconName === 'ErrorIcon' && <ErrorIcon />}
</>
);
import { ResultIcon } from './ResultIcon';
import { getPaymentFormViewId, getResultInfo, isInstantPayment } from './utils';

export function PaymentResultView() {
const { l } = useContext(LocaleContext);
Expand All @@ -58,9 +20,18 @@ export function PaymentResultView() {
paymentModel: { initContext },
} = useContext(PaymentModelContext);
const { onComplete } = useContext(CompletePaymentContext);

const { viewModel, goTo } = useContext(ViewModelContext);
const lastCondition = last(conditions);
const { iconName, label, description, hasActions } = getResultInfo(lastCondition);
const { iconName, label, description, hasActions, color } = getResultInfo(lastCondition);

const retry = useCallback(() => {
if (isInstantPayment(conditions)) {
location.reload();
return;
}
const paymentFormViewId = getPaymentFormViewId(viewModel.views);
goTo(paymentFormViewId);
}, [conditions, viewModel.views]);

useEffect(() => {
switch (lastCondition.name) {
Expand All @@ -78,22 +49,39 @@ export function PaymentResultView() {
}, [onComplete, lastCondition]);

return (
<>
<Wrapper>
<ResultIcon iconName={iconName} />
<Label>{l[label]}</Label>
{!isNil(description) && <Description>{l[description]}</Description>}
<VStack align="stretch" minH="sm" spacing={6}>
<Spacer />
<Flex justifyContent="center">
<ResultIcon color={color} iconName={iconName} />
</Flex>
<VStack align="stretch" spacing={3}>
<Text color={color} fontSize="4xl" fontWeight="medium" textAlign="center">
{l[label]}
</Text>
{!isNil(description) && (
<Text fontSize="lg" textAlign="center">
{l[description]}
</Text>
)}
</VStack>
<Spacer />
<VStack align="stretch" spacing={6}>
{hasActions && (
<Button color="primary" onClick={() => location.reload()}>
{l['form.button.reload']}
<Button borderRadius="xl" colorScheme="teal" size="lg" variant="solid" onClick={retry}>
{l['form.button.pay.again.label']}
</Button>
)}
{initContext?.redirectUrl && (
<OthersButton onClick={() => window.open(initContext.redirectUrl, '_self')}>
<Button
colorScheme="teal"
size="lg"
variant="link"
onClick={() => window.open(initContext.redirectUrl, '_self')}
>
{l['form.button.back.to.website']}
</OthersButton>
</Button>
)}
</Wrapper>
</>
</VStack>
</VStack>
);
}
17 changes: 17 additions & 0 deletions src/components/ViewContainer/PaymentResultView/ResultIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CheckCircleIcon, WarningIcon, InfoIcon } from '@chakra-ui/icons';

const iconComponents = {
CheckIcon: CheckCircleIcon,
WarningIcon: WarningIcon,
InfoIcon: InfoIcon,
};

export type ResultIconProps = {
iconName: keyof typeof iconComponents;
color: string;
};

export function ResultIcon({ iconName, color }: ResultIconProps) {
const IconComponent = iconComponents[iconName];
return IconComponent ? <IconComponent boxSize="28" color={color} /> : null;
}
Loading

0 comments on commit 8f2c9c6

Please sign in to comment.