Payment method selector refactoring (#320)

This commit is contained in:
Ildar Galeev 2024-07-19 16:57:56 +07:00 committed by GitHub
parent 0ef7ba8985
commit 20fc041110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 165 additions and 159 deletions

View File

@ -1,21 +0,0 @@
import { Center, Image, Square, useColorModeValue } from '@chakra-ui/react';
import { HiOutlineCash } from 'react-icons/hi';
import { ServiceProviderIconMetadata } from 'checkout/backend/payments/serviceProviderMetadata';
import { isNil } from 'checkout/utils';
export type MetadataLogoBoxProps = {
logo?: ServiceProviderIconMetadata;
height: number;
};
export function MetadataLogoBox({ logo, height }: MetadataLogoBoxProps) {
const bgColor = useColorModeValue('white', 'gray.100');
return (
<Center bgColor={bgColor} borderRadius="md" height={height} p={4} userSelect="none">
{!isNil(logo) && <Image height={logo.height} src={logo.src} width={logo.width} />}
{isNil(logo) && <Square as={HiOutlineCash} color="gray.600" size={12} />}
</Center>
);
}

View File

@ -0,0 +1,24 @@
import { StackProps, useColorModeValue, VStack } from '@chakra-ui/react';
export type PaneProps = StackProps;
export function Pane(props: PaneProps) {
const { children, ...rest } = props;
const hoverBorderColor = useColorModeValue('gray.300', 'whiteAlpha.400');
return (
<VStack
_hover={{ borderColor: hoverBorderColor }}
align="stretch"
border="1px"
borderColor="chakra-border-color"
borderRadius="lg"
cursor="pointer"
p={2}
spacing={2}
{...rest}
>
{children}
</VStack>
);
}

View File

@ -0,0 +1,5 @@
import { Square, SquareProps } from '@chakra-ui/react';
export function PaneLogo(props: SquareProps) {
return <Square color="gray.600" size={12} {...props} />;
}

View File

@ -0,0 +1,12 @@
import { Center, CenterProps, useColorModeValue } from '@chakra-ui/react';
export function PaneLogoBox(props: CenterProps) {
const { children, ...rest } = props;
const bgColor = useColorModeValue('white', 'gray.100');
return (
<Center bgColor={bgColor} borderRadius="md" height={16} p={4} userSelect="none" {...rest}>
{children}
</Center>
);
}

View File

@ -0,0 +1,10 @@
import { ImageProps, Image } from '@chakra-ui/react';
import { ServiceProviderIconMetadata } from 'checkout/backend/payments/serviceProviderMetadata';
export type PaneMetadataLogoProps = { logo: ServiceProviderIconMetadata } & ImageProps;
export function PaneMetadataLogo(props: PaneMetadataLogoProps) {
const { logo, ...rest } = props;
return <Image height={logo.height} src={logo.src} width={logo.width} {...rest} />;
}

View File

@ -0,0 +1,18 @@
import { Text, TextProps } from '@chakra-ui/react';
export function PaneText(props: TextProps) {
const { children, ...rest } = props;
return (
<Text
fontWeight="medium"
overflow="hidden"
textAlign="center"
textOverflow="ellipsis"
userSelect="none"
whiteSpace="nowrap"
{...rest}
>
{children}
</Text>
);
}

View File

@ -0,0 +1,7 @@
export { Pane } from './Pane';
export { PaneText } from './PaneText';
export { PaneLogoBox } from './PaneLogoBox';
export { PaneLogo } from './PaneLogo';
export { PaneMetadataLogo } from './PaneMetadataLogo';
export type { PaneMetadataLogoProps } from './PaneMetadataLogo';

View File

@ -1,4 +1,6 @@
export { ErrorAlert } from './ErrorAlert';
export { BackwardBox } from './BackwardBox';
export { GlobalSpinner } from './GlobalSpinner';
export { MetadataLogoBox } from './MetadataLogoBox';
export { Pane, PaneLogo, PaneLogoBox, PaneText, PaneMetadataLogo } from './Pane';
export type { PaneMetadataLogoProps } from './Pane';

View File

@ -52,7 +52,7 @@ export function CardForm() {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<VStack align="stretch" spacing={5}>
<Flex alignItems="center" direction="row">
<Flex alignItems="center">
{hasBackward && <BackwardBox onClick={backward} />}
<Text fontWeight="medium" textAlign="center" width="full">
{l['form.header.pay.card.label']}

View File

@ -1,8 +1,8 @@
import { Button, Center, LightMode, Spacer, VStack } from '@chakra-ui/react';
import { Button, LightMode, Spacer, VStack } from '@chakra-ui/react';
import { useContext } from 'react';
import { FieldError, SubmitHandler, useForm } from 'react-hook-form';
import { BackwardBox, MetadataLogoBox } from 'checkout/components';
import { BackwardBox, PaneLogoBox, PaneMetadataLogo } from 'checkout/components';
import { LocaleContext, PaymentContext, PaymentModelContext, ViewModelContext } from 'checkout/contexts';
import { toDefaultFormValuesMetadata } from 'checkout/paymentCondition';
import { TerminalValues } from 'checkout/paymentMgmt';
@ -61,9 +61,9 @@ export function MetadataForm({ provider }: MetadataFormProps) {
<VStack align="stretch" spacing={5}>
{hasBackward ? <BackwardBox onClick={backward} /> : <Spacer />}
{!isNil(logo) && (
<Center>
<MetadataLogoBox height={16} logo={logo} />
</Center>
<PaneLogoBox alignSelf="center" width="50%">
<PaneMetadataLogo logo={logo} />
</PaneLogoBox>
)}
{!isNil(form) &&
form

View File

@ -1,29 +1,22 @@
import { useContext } from 'react';
import { HiCreditCard } from 'react-icons/hi';
import { LocaleContext, ViewModelContext } from '../../../common/contexts';
import { Method, PaymentMethodIcon, PaymentMethodTitle } from '../../legacy';
import { Pane, PaneLogo, PaneLogoBox, PaneText } from 'checkout/components';
import { LocaleContext } from 'checkout/contexts';
export type BankCardPaneProps = {
destinationViewId: string;
onClick: () => void;
};
export function BankCardPane({ destinationViewId }: BankCardPaneProps) {
export function BankCardPane({ onClick }: BankCardPaneProps) {
const { l } = useContext(LocaleContext);
const {
viewModel: { views },
forward,
} = useContext(ViewModelContext);
const destination = views.get(destinationViewId);
if (destination.name !== 'PaymentFormView') {
throw new Error(`Wrong View. Expected: PaymentFormView, actual: ${destination.name}`);
}
return (
<Method onClick={() => forward(destinationViewId)}>
<PaymentMethodIcon name="bank-card" />
<PaymentMethodTitle>{l['form.payment.method.name.card.label']}</PaymentMethodTitle>
</Method>
<Pane onClick={onClick}>
<PaneLogoBox>
<PaneLogo as={HiCreditCard} />
</PaneLogoBox>
<PaneText>{l['form.payment.method.name.card.label']}</PaneText>
</Pane>
);
}

View File

@ -1,21 +1,17 @@
import { VStack, Text, SimpleGrid } from '@chakra-ui/react';
import { useContext } from 'react';
import styled from 'styled-components';
import { LocaleContext, ViewModelContext } from 'checkout/contexts';
import { BankCardPane } from './BankCardPane';
import { PaymentTerminalPane } from './PaymentTerminalPane';
import { TerminalSelectorPane } from './TerminalSelectorPane';
import { LocaleContext, ViewModelContext } from '../../../common/contexts';
import { HeaderWrapper, Title } from '../../../components/legacy';
const PanesWrapper = styled.div`
display: flex;
flex-direction: column;
`;
export function PaymentMethodSelectorView() {
const { l } = useContext(LocaleContext);
const {
viewModel: { views, activeViewId },
forward,
} = useContext(ViewModelContext);
const view = views.get(activeViewId);
@ -24,22 +20,26 @@ export function PaymentMethodSelectorView() {
}
return (
<>
<HeaderWrapper>
<Title>{l['form.header.payment.methods.label']}</Title>
</HeaderWrapper>
<PanesWrapper>
{view.items.map(({ name, viewId }, key) => {
<VStack align="stretch" minH="sm" spacing={5}>
<Text fontWeight="medium" textAlign="center">
{l['form.header.payment.methods.label']}
</Text>
<SimpleGrid columns={[1, 2, 2]} spacing={5}>
{view.items.map(({ name, viewId, provider, category }, key) => {
switch (name) {
case 'BankCard':
return <BankCardPane key={key} destinationViewId={viewId} />;
return <BankCardPane key={key} onClick={() => forward(viewId)} />;
case 'PaymentTerminal':
return <PaymentTerminalPane key={key} destinationViewId={viewId} />;
return (
<PaymentTerminalPane key={key} provider={provider} onClick={() => forward(viewId)} />
);
case 'TerminalSelector':
return <TerminalSelectorPane key={key} destinationViewId={viewId} />;
return (
<TerminalSelectorPane key={key} category={category} onClick={() => forward(viewId)} />
);
}
})}
</PanesWrapper>
</>
</SimpleGrid>
</VStack>
);
}

View File

@ -1,38 +1,29 @@
import { Text } from '@chakra-ui/react';
import { useContext } from 'react';
import { HiCash } from 'react-icons/hi';
import { PaymentModelContext, ViewModelContext } from '../../../common/contexts';
import { isNil } from '../../../common/utils';
import { findMetadata } from '../../../common/utils/findMetadata';
import { MetadataLogo, PaymentMethodItemContainer } from '../../legacy';
import { Pane, PaneLogoBox, PaneLogo, PaneText, PaneMetadataLogo } from 'checkout/components';
import { PaymentModelContext } from 'checkout/contexts';
import { isNil } from 'checkout/utils';
import { findMetadata } from 'checkout/utils/findMetadata';
export type PaymentTerminalPaneProps = {
destinationViewId: string;
provider: string;
onClick: () => void;
};
export function PaymentTerminalPane({ destinationViewId }: PaymentTerminalPaneProps) {
export function PaymentTerminalPane({ onClick, provider }: PaymentTerminalPaneProps) {
const {
paymentModel: { serviceProviders },
} = useContext(PaymentModelContext);
const {
forward,
viewModel: { views },
} = useContext(ViewModelContext);
const destination = views.get(destinationViewId);
if (destination.name !== 'PaymentFormView') {
throw new Error(`Wrong View. Expected: PaymentFormView, actual: ${destination.name}`);
}
const { logo } = findMetadata(serviceProviders, destination.provider);
const { logo } = findMetadata(serviceProviders, provider);
const serviceProvider = serviceProviders.find(({ id }) => id === provider);
return (
<PaymentMethodItemContainer
id={`${Math.floor(Math.random() * 100)}-payment-method-item`}
onClick={() => forward(destinationViewId)}
>
{!isNil(logo) && <MetadataLogo metadata={logo} />}
{isNil(logo) && <Text>{destination.provider}</Text>}
</PaymentMethodItemContainer>
<Pane onClick={onClick}>
<PaneLogoBox>
{isNil(logo) && <PaneLogo as={HiCash} />} {!isNil(logo) && <PaneMetadataLogo logo={logo} />}
</PaneLogoBox>
<PaneText>{serviceProvider?.brandName}</PaneText>
</Pane>
);
}

View File

@ -1,28 +1,19 @@
import { useContext } from 'react';
import { HiCash } from 'react-icons/hi';
import { ViewModelContext } from '../../../common/contexts';
import { Method, PaymentMethodIcon, PaymentMethodTitle } from '../../legacy';
import { Pane, PaneLogoBox, PaneLogo, PaneText } from 'checkout/components';
export type TerminalSelectorPaneProps = {
destinationViewId: string;
category: string;
onClick: () => void;
};
export function TerminalSelectorPane({ destinationViewId }: TerminalSelectorPaneProps) {
const {
forward,
viewModel: { views },
} = useContext(ViewModelContext);
const destination = views.get(destinationViewId);
if (destination.name !== 'TerminalSelectorView') {
throw new Error(`Wrong View. Expected: TerminalSelectorView, actual: ${destination.name}`);
}
export function TerminalSelectorPane({ onClick, category }: TerminalSelectorPaneProps) {
return (
<Method onClick={() => forward(destinationViewId)}>
<PaymentMethodIcon name="terminals" />
<PaymentMethodTitle>{destination.category}</PaymentMethodTitle>
</Method>
<Pane onClick={onClick}>
<PaneLogoBox>
<PaneLogo as={HiCash} />
</PaneLogoBox>
<PaneText>{category}</PaneText>
</Pane>
);
}

View File

@ -1,40 +0,0 @@
import { VStack, Text, useColorModeValue } from '@chakra-ui/react';
import { ServiceProviderIconMetadata } from 'checkout/backend/payments/serviceProviderMetadata';
import { MetadataLogoBox } from 'checkout/components';
type ServiceProviderPaneProps = {
logo?: ServiceProviderIconMetadata;
text: string;
onClick: () => void;
};
export function ServiceProviderPane({ text, logo, onClick }: ServiceProviderPaneProps) {
const hoverBorderColor = useColorModeValue('gray.300', 'whiteAlpha.400');
return (
<VStack
_hover={{ borderColor: hoverBorderColor }}
align="stretch"
border="1px"
borderColor="chakra-border-color"
borderRadius="lg"
cursor="pointer"
p={2}
spacing={2}
onClick={onClick}
>
<MetadataLogoBox height={16} logo={logo} />
<Text
fontWeight="medium"
overflow="hidden"
textAlign="center"
textOverflow="ellipsis"
userSelect="none"
whiteSpace="nowrap"
>
{text}
</Text>
</VStack>
);
}

View File

@ -1,25 +1,26 @@
import { VStack, SimpleGrid, Input } from '@chakra-ui/react';
import { VStack, SimpleGrid, Input, Flex, Text } from '@chakra-ui/react';
import { useContext, useMemo } from 'react';
import { HiCash } from 'react-icons/hi';
import { BackwardBox } from 'checkout/components';
import { BackwardBox, Pane, PaneLogo, PaneLogoBox, PaneMetadataLogo, PaneText } from 'checkout/components';
import { LocaleContext, PaymentModelContext, ViewModelContext } from 'checkout/contexts';
import { isNil } from 'checkout/utils';
import { PageNavigation } from './PageNavigation';
import { ServiceProviderPane } from './ServiceProviderPane';
import { useGridPages } from './useGrigPages';
const ITEMS_ON_PAGE = 6;
export function TerminalSelectorView() {
const { l } = useContext(LocaleContext);
const {
paymentModel: { serviceProviders },
} = useContext(PaymentModelContext);
const {
viewModel: { views, activeViewId, hasBackward },
backward,
forward,
} = useContext(ViewModelContext);
const {
paymentModel: { serviceProviders },
} = useContext(PaymentModelContext);
const view = views.get(activeViewId);
if (view.name !== 'TerminalSelectorView') {
@ -39,12 +40,22 @@ export function TerminalSelectorView() {
};
return (
<VStack align="stretch" spacing={5}>
{hasBackward && <BackwardBox onClick={backward} />}
<VStack align="stretch" minH="sm" spacing={5}>
<Flex alignItems="center">
{hasBackward && <BackwardBox onClick={backward} />}
<Text fontWeight="medium" textAlign="center" width="full">
{l['form.header.payment.methods.label']}
</Text>
</Flex>
{isSearchAvailable && <Input placeholder={l['form.serviceProvidersGrid.search']} onChange={onChange} />}
<SimpleGrid columns={[1, 2, 2]} spacing={5}>
{pageItems.map(({ logo, brandName, viewId }, i) => (
<ServiceProviderPane key={i} logo={logo} text={brandName} onClick={() => forward(viewId)} />
<Pane key={i} onClick={() => forward(viewId)}>
<PaneLogoBox>
{isNil(logo) && <PaneLogo as={HiCash} />} {!isNil(logo) && <PaneMetadataLogo logo={logo} />}
</PaneLogoBox>
<PaneText>{brandName}</PaneText>
</Pane>
))}
</SimpleGrid>
{totalPages > 1 && (

View File

@ -31,6 +31,7 @@ export type TerminalSelectorView = {
export type PaymentMethodSelectorItem = {
name: PaymentMethodName | 'TerminalSelector';
viewId: string;
category?: string;
provider?: string;
};

View File

@ -126,6 +126,7 @@ const toViews = (paymentMethods: PaymentMethod[]): [Map<string, View>, string] =
const singleProviderSelectorItem: PaymentMethodSelectorItem = {
name: methodName,
viewId: singleProviderTerminalId,
category,
provider: providers[0],
};
return [
@ -150,6 +151,7 @@ const toViews = (paymentMethods: PaymentMethod[]): [Map<string, View>, string] =
};
const terminalSelectorItem: PaymentMethodSelectorItem = {
name: 'TerminalSelector',
category,
viewId: terminalSelectorId,
};
return [