mirror of
https://github.com/valitydev/checkout.git
synced 2024-11-06 02:25:18 +00:00
Custom theme config (#315)
This commit is contained in:
parent
8a8923d417
commit
daa3fcdf73
@ -15,7 +15,7 @@
|
||||
/>
|
||||
<title>Checkout</title>
|
||||
</head>
|
||||
<body style="background: #163735">
|
||||
<body>
|
||||
<div id="global-spinner" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%)">
|
||||
<div style="display: flex; height: 128px">
|
||||
<svg
|
||||
@ -26,8 +26,8 @@
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="loaderGradient" x1="100%" x2="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#F6E05E" />
|
||||
<stop offset="100%" stop-color="#ED8936" />
|
||||
<stop offset="0%" stop-color="#000000" />
|
||||
<stop offset="100%" stop-color="#EEEEEE" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd" stroke="none" stroke-width="1">
|
||||
|
114
src/App.tsx
114
src/App.tsx
@ -1,5 +1,5 @@
|
||||
import { ChakraBaseProvider, extendBaseTheme, theme as chakraTheme } from '@chakra-ui/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ErrorAlert } from 'checkout/components';
|
||||
import { CompletePaymentContext } from 'checkout/contexts';
|
||||
@ -11,12 +11,7 @@ import { useInitialize } from './useInitialize';
|
||||
|
||||
const { Button, Spinner, Divider, Heading, Alert, Menu, Drawer } = chakraTheme.components;
|
||||
|
||||
const theme = extendBaseTheme({
|
||||
fonts: {
|
||||
body: 'Roboto, sans-serif',
|
||||
heading: 'Roboto, sans-serif',
|
||||
mono: 'monospace',
|
||||
},
|
||||
const common = {
|
||||
components: {
|
||||
Button,
|
||||
Spinner,
|
||||
@ -26,12 +21,62 @@ const theme = extendBaseTheme({
|
||||
Menu,
|
||||
Drawer,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const defaultTheme = {
|
||||
fonts: {
|
||||
body: 'Roboto, sans-serif',
|
||||
heading: 'Roboto, sans-serif',
|
||||
mono: 'monospace',
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#E6FFFA',
|
||||
100: '#B2F5EA',
|
||||
200: '#81E6D9',
|
||||
300: '#4FD1C5',
|
||||
400: '#38B2AC',
|
||||
500: '#319795',
|
||||
600: '#2C7A7B',
|
||||
700: '#285E61',
|
||||
800: '#234E52',
|
||||
900: '#1D4044',
|
||||
},
|
||||
},
|
||||
semanticTokens: {
|
||||
colors: {
|
||||
mainContainerBg: {
|
||||
default: 'gray.100',
|
||||
},
|
||||
viewContainerBg: {
|
||||
default: 'white',
|
||||
},
|
||||
viewContainerLoaderBg: {
|
||||
default: 'whiteAlpha.800',
|
||||
},
|
||||
bodyText: {
|
||||
default: 'gray.800',
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
global: {
|
||||
body: {
|
||||
bg: '#163735',
|
||||
},
|
||||
},
|
||||
},
|
||||
qrCode: {
|
||||
back: '#FFFFFF',
|
||||
fill: '#1A202C',
|
||||
},
|
||||
};
|
||||
|
||||
const ON_COMPLETE_TIMEOUT_MS = 1000 * 5;
|
||||
|
||||
export function App() {
|
||||
const { state, init } = useInitialize();
|
||||
const [theme, setTheme] = useState(extendBaseTheme({ ...common, ...defaultTheme }));
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
@ -44,25 +89,42 @@ export function App() {
|
||||
}
|
||||
}, [state.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'SUCCESS') {
|
||||
const themes = state.data[1].appConfig?.themes;
|
||||
if (isNil(themes) || themes.length === 0) return;
|
||||
|
||||
const initConfigThemeName = state.data[1].initConfig?.theme;
|
||||
if (isNil(initConfigThemeName)) return;
|
||||
|
||||
const customTheme = themes.find((t) => t.themeName === initConfigThemeName);
|
||||
if (isNil(customTheme)) return;
|
||||
|
||||
setTheme(extendBaseTheme({ ...common, ...customTheme }));
|
||||
}
|
||||
}, [state.status]);
|
||||
|
||||
return (
|
||||
<ChakraBaseProvider theme={theme}>
|
||||
<>
|
||||
{state.status === 'SUCCESS' && (
|
||||
<CompletePaymentContext.Provider
|
||||
value={{
|
||||
onComplete: () =>
|
||||
setTimeout(() => {
|
||||
const [transport, params] = state.data;
|
||||
transport.emit(CommunicatorEvents.finished);
|
||||
transport.destroy();
|
||||
const redirectUrl = params.initConfig?.redirectUrl;
|
||||
if (!isNil(redirectUrl)) {
|
||||
window.open(redirectUrl, '_self');
|
||||
}
|
||||
}, ON_COMPLETE_TIMEOUT_MS),
|
||||
}}
|
||||
>
|
||||
<AppLayout initParams={state.data[1]} />
|
||||
</CompletePaymentContext.Provider>
|
||||
<ChakraBaseProvider theme={theme}>
|
||||
<CompletePaymentContext.Provider
|
||||
value={{
|
||||
onComplete: () =>
|
||||
setTimeout(() => {
|
||||
const [transport, params] = state.data;
|
||||
transport.emit(CommunicatorEvents.finished);
|
||||
transport.destroy();
|
||||
const redirectUrl = params.initConfig?.redirectUrl;
|
||||
if (!isNil(redirectUrl)) {
|
||||
window.open(redirectUrl, '_self');
|
||||
}
|
||||
}, ON_COMPLETE_TIMEOUT_MS),
|
||||
}}
|
||||
>
|
||||
<AppLayout initParams={state.data[1]} styledComponentsTheme={theme?.__styledComponents} />
|
||||
</CompletePaymentContext.Provider>
|
||||
</ChakraBaseProvider>
|
||||
)}
|
||||
{state.status === 'FAILURE' && (
|
||||
<ErrorAlert
|
||||
@ -71,6 +133,6 @@ export function App() {
|
||||
title="Initialization failure"
|
||||
/>
|
||||
)}
|
||||
</ChakraBaseProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export function ErrorAlert({ title, description, isReloading }: ErrorAlertProps)
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="lg">{description}</AlertDescription>
|
||||
{isReloading && (
|
||||
<Button colorScheme="teal" onClick={() => location.reload()}>
|
||||
<Button colorScheme="gray" onClick={() => location.reload()}>
|
||||
Reload
|
||||
</Button>
|
||||
)}
|
||||
|
@ -15,8 +15,8 @@ export function GlobalSpinner({ l }: GlobalSpinnerProps) {
|
||||
<VStack align="center" minHeight={32} spacing={4}>
|
||||
<Spinner
|
||||
alignItems="center"
|
||||
color="yellow.300"
|
||||
emptyColor="orange.400"
|
||||
color="brand.500"
|
||||
emptyColor="brand.200"
|
||||
size="xl"
|
||||
speed="0.65s"
|
||||
thickness="4px"
|
||||
|
@ -29,6 +29,7 @@ it('should return resolved init config', () => {
|
||||
phoneNumber: null,
|
||||
redirectUrl: null,
|
||||
skipUserInteraction: false,
|
||||
theme: null,
|
||||
};
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
@ -30,6 +30,7 @@ export const resolveInitConfig = (userConfig: Partial<InitConfig>): InitConfig =
|
||||
terminalFormValues,
|
||||
skipUserInteraction,
|
||||
isExternalIDIncluded,
|
||||
theme,
|
||||
} = userConfig;
|
||||
return {
|
||||
...resolvedIntegrationType,
|
||||
@ -46,5 +47,6 @@ export const resolveInitConfig = (userConfig: Partial<InitConfig>): InitConfig =
|
||||
terminalFormValues: setDefault(resolveObject(terminalFormValues), undefined),
|
||||
skipUserInteraction: setDefault(resolveBoolean(skipUserInteraction), false),
|
||||
isExternalIDIncluded: setDefault(resolveBoolean(isExternalIDIncluded), true),
|
||||
theme: resolveString(theme),
|
||||
};
|
||||
};
|
||||
|
@ -20,6 +20,8 @@ export type InitConfig = {
|
||||
isExternalIDIncluded?: boolean;
|
||||
};
|
||||
|
||||
export type ThemeConfig = Record<string, any>;
|
||||
|
||||
export type AppConfig = {
|
||||
capiEndpoint?: string;
|
||||
wrapperEndpoint?: string;
|
||||
@ -28,6 +30,7 @@ export type AppConfig = {
|
||||
brandName?: string;
|
||||
urlShortenerEndpoint?: string;
|
||||
sentryDsn?: string;
|
||||
themes: ThemeConfig[];
|
||||
};
|
||||
|
||||
export type InitParams = {
|
||||
|
@ -40,7 +40,9 @@ const theme: Theme = {
|
||||
border: palette.Loblolly,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'none',
|
||||
border: palette.Loblolly,
|
||||
color: '#000000',
|
||||
placeholder: palette.RegentGray,
|
||||
error: palette.Cinnabar,
|
||||
focus: palette.Zeus,
|
||||
|
@ -40,7 +40,9 @@ const theme: Theme = {
|
||||
border: palette.SilverChalice,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'none',
|
||||
border: palette.SilverChalice,
|
||||
color: '#000000',
|
||||
placeholder: palette.RegentGray,
|
||||
error: palette.Cinnabar,
|
||||
focus: palette.CodGray,
|
||||
|
@ -20,7 +20,9 @@ export type Theme = {
|
||||
border: string;
|
||||
};
|
||||
input: {
|
||||
backgroundColor: string;
|
||||
border: string;
|
||||
color: string;
|
||||
placeholder: string;
|
||||
error: string;
|
||||
focus: string;
|
||||
|
@ -15,6 +15,7 @@ import { toCustomizationContext } from './utils';
|
||||
|
||||
type AppLayoutProps = {
|
||||
initParams: InitParams;
|
||||
styledComponentsTheme?: Record<string, any>;
|
||||
};
|
||||
|
||||
const GlobalContainer = lazy(() => import('../GlobalContainer/GlobalContainer'));
|
||||
@ -35,8 +36,8 @@ const ModalContainer = ({ children }: { children: React.ReactNode }) => (
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export function AppLayout({ initParams }: AppLayoutProps) {
|
||||
const theme = getTheme(initParams.appConfig.fixedTheme);
|
||||
export function AppLayout({ initParams, styledComponentsTheme }: AppLayoutProps) {
|
||||
const theme = styledComponentsTheme || getTheme(initParams.appConfig.fixedTheme);
|
||||
const { modelsState, init } = useInitModels();
|
||||
const customizationContextValue = toCustomizationContext(initParams.initConfig);
|
||||
const initLocaleCode = customizationContextValue.initLocaleCode;
|
||||
|
@ -38,7 +38,9 @@ export function DestinationInfo({ destination }: DestinationInfoProps) {
|
||||
{isAmountRandomized && (
|
||||
<Alert borderRadius="xl" p={3} status="warning">
|
||||
<AlertIcon />
|
||||
<Text fontSize="sm">{l['form.p2p.destination.randomizeAmountDescription']}</Text>
|
||||
<Text color="bodyText" fontSize="sm">
|
||||
{l['form.p2p.destination.randomizeAmountDescription']}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<InfoItem isDivider={false} label={l['form.p2p.destination.amount']} value={viewAmount} />
|
||||
|
@ -35,26 +35,28 @@ export function Destinations({ destinations }: DestinationsProps) {
|
||||
|
||||
return (
|
||||
<VStack align="stretch" minH="md" spacing={5}>
|
||||
<Heading as="h5" size="sm" textAlign="center">
|
||||
<Heading as="h5" color="bodyText" size="sm" textAlign="center">
|
||||
{l['form.p2p.destinations.heading']}
|
||||
</Heading>
|
||||
<Divider />
|
||||
<P2PAlert />
|
||||
<VStack align="stretch" spacing={3}>
|
||||
<Text fontWeight="medium">{l['form.p2p.destination.info']}</Text>
|
||||
<Text color="bodyText" fontWeight="medium">
|
||||
{l['form.p2p.destination.info']}
|
||||
</Text>
|
||||
{destinations.map((destination, index) => (
|
||||
<DestinationInfo key={index} destination={destination} />
|
||||
))}
|
||||
</VStack>
|
||||
<Spacer />
|
||||
<VStack align="stretch" spacing={3}>
|
||||
<Text fontSize="sm" textAlign="center">
|
||||
<Text color="bodyText" fontSize="sm" textAlign="center">
|
||||
{l['form.p2p.complete.info']}
|
||||
</Text>
|
||||
<VStack align="stretch" spacing={5}>
|
||||
<Button
|
||||
borderRadius="lg"
|
||||
colorScheme="teal"
|
||||
colorScheme="brand"
|
||||
isLoading={status === 'LOADING' || status === 'SUCCESS'}
|
||||
loadingText={l['form.p2p.complete.loading']}
|
||||
size="lg"
|
||||
@ -64,7 +66,7 @@ export function Destinations({ destinations }: DestinationsProps) {
|
||||
</Button>
|
||||
{status === 'SUCCESS' && initContext?.redirectUrl && (
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
colorScheme="brand"
|
||||
size="lg"
|
||||
variant="link"
|
||||
onClick={() => window.open(initContext.redirectUrl, '_self')}
|
||||
|
@ -62,11 +62,11 @@ export function InfoItem({ label, value, isCopyable, formatter, icon, isDivider
|
||||
return (
|
||||
<VStack align="stretch">
|
||||
<Flex>
|
||||
<Text>{label}</Text>
|
||||
<Text color="bodyText">{label}</Text>
|
||||
<Spacer />
|
||||
<Flex alignItems="center" gap={2}>
|
||||
{icon && <IconContainer />}
|
||||
<Text fontWeight="medium" textAlign="end">
|
||||
<Text color="bodyText" fontWeight="medium" textAlign="end">
|
||||
{displayValue}
|
||||
</Text>
|
||||
{isCopyable && <IconButton aria-label="Copy" icon={<CopyIcon />} size="xs" onClick={onCopy} />}
|
||||
|
@ -18,7 +18,7 @@ export function FetchRequisitesError() {
|
||||
<Spacer />
|
||||
{initContext?.redirectUrl && (
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
colorScheme="brand"
|
||||
size="lg"
|
||||
variant="link"
|
||||
onClick={() => window.open(initContext.redirectUrl, '_self')}
|
||||
|
@ -26,7 +26,7 @@ export function GatewaySelector({ gateways, onSelect }: GatewaySelectorProps) {
|
||||
<Spacer />
|
||||
<Button
|
||||
borderRadius="lg"
|
||||
colorScheme="teal"
|
||||
colorScheme="brand"
|
||||
isDisabled={isNil(gateway)}
|
||||
size="lg"
|
||||
onClick={() => onSelect(gateway.id)}
|
||||
|
@ -29,7 +29,7 @@ export function IconPane({ label, icon, isActive, onClick }: IconPaneProps) {
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconContainer />
|
||||
<Text mb={4} ml={2} mr={2} userSelect="none">
|
||||
<Text color="bodyText" mb={4} ml={2} mr={2} userSelect="none">
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
@ -11,9 +11,9 @@ export function RequisitesLoader() {
|
||||
return (
|
||||
<VStack alignItems="center" justifyContent="center" minH="md">
|
||||
<VStack align="center" minHeight={32} spacing={4}>
|
||||
<Spinner color="teal.500" emptyColor="gray.200" size="xl" speed="0.65s" thickness="4px" />
|
||||
<Spinner color="brand.500" emptyColor="brand.200" size="xl" speed="0.65s" thickness="4px" />
|
||||
{message !== '' && (
|
||||
<Text fontSize="md" fontWeight="medium" textAlign="center">
|
||||
<Text color="bodyText" fontSize="md" fontWeight="medium" textAlign="center">
|
||||
{message}
|
||||
</Text>
|
||||
)}
|
||||
|
@ -23,15 +23,19 @@ export function DetailsDrawer({ isOpen, onClose, name, description }: DetailsDra
|
||||
return (
|
||||
<Drawer isOpen={isOpen} placement="top" size="sm" onClose={onClose}>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerContent background="viewContainerBg">
|
||||
<DrawerBody>
|
||||
<VStack align="stretch">
|
||||
{name && (
|
||||
<Text fontSize="xl" fontWeight="medium">
|
||||
<Text color="bodyText" fontSize="xl" fontWeight="medium">
|
||||
{truncate(name, 100)}
|
||||
</Text>
|
||||
)}
|
||||
{description && <Text fontSize="lg">{truncate(description, 140)}</Text>}
|
||||
{description && (
|
||||
<Text color="bodyText" fontSize="lg">
|
||||
{truncate(description, 140)}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</DrawerBody>
|
||||
<DrawerFooter>
|
||||
|
@ -34,22 +34,26 @@ export function InfoContainer({ viewAmount }: InfoProps) {
|
||||
)}
|
||||
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Text fontSize="3xl" fontWeight="medium">
|
||||
<Text color="bodyText" fontSize="3xl" fontWeight="medium">
|
||||
{viewAmount}
|
||||
</Text>
|
||||
{!isLargerThan768 && (name || description) && (
|
||||
<Button colorScheme="gray" rightIcon={<ChevronDownIcon />} variant="ghost" onClick={onOpen}>
|
||||
<Button colorScheme="gray" rightIcon={<ChevronDownIcon />} onClick={onOpen}>
|
||||
{l['info.details']}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isLargerThan768 && name && (
|
||||
<Text fontSize="xl" fontWeight="medium">
|
||||
<Text color="bodyText" fontSize="xl" fontWeight="medium">
|
||||
{truncate(name, 80)}
|
||||
</Text>
|
||||
)}
|
||||
{isLargerThan768 && description && <Text fontSize="lg">{truncate(description, 120)}</Text>}
|
||||
{isLargerThan768 && description && (
|
||||
<Text color="bodyText" fontSize="lg">
|
||||
{truncate(description, 120)}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
<DetailsDrawer description={description} isOpen={isOpen} name={name} onClose={onClose} />
|
||||
</>
|
||||
|
@ -11,7 +11,7 @@ export const Loader = () => (
|
||||
<motion.div animate="show" exit="exit" initial="hidden" variants={fadeIn}>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
background="whiteAlpha.800"
|
||||
background="viewContainerLoaderBg"
|
||||
borderRadius="xl"
|
||||
height="100%"
|
||||
justifyContent="center"
|
||||
@ -20,7 +20,7 @@ export const Loader = () => (
|
||||
top={0}
|
||||
width="100%"
|
||||
>
|
||||
<Spinner color="teal.500" emptyColor="gray.200" size="xl" speed="0.65s" thickness="4px" />
|
||||
<Spinner color="brand.500" emptyColor="brand.200" size="xl" speed="0.65s" thickness="4px" />
|
||||
</Flex>
|
||||
</motion.div>
|
||||
);
|
||||
|
@ -76,10 +76,11 @@ export function LocaleSelector({ initLocaleCode, onLocaleChange }: LocaleSelecto
|
||||
<ChevronDownIcon />
|
||||
</Flex>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuList backgroundColor="viewContainerBg">
|
||||
{Object.entries(localeInfo).map(([code, { flag, long }]) => (
|
||||
<MenuItem
|
||||
key={code}
|
||||
backgroundColor="viewContainerBg"
|
||||
onClick={() => {
|
||||
setActiveLocaleCode(code);
|
||||
onLocaleChange(code);
|
||||
@ -91,7 +92,9 @@ export function LocaleSelector({ initLocaleCode, onLocaleChange }: LocaleSelecto
|
||||
{flag}
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="md">{long}</Text>
|
||||
<Text color="bodyText" fontSize="md">
|
||||
{long}
|
||||
</Text>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
@ -20,7 +20,9 @@ export function NoAvailablePaymentMethodsView() {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Text centered={true}>{l['info.modal.no.available.payment.method']}</Text>
|
||||
<Text centered={true} color="bodyText">
|
||||
{l['info.modal.no.available.payment.method']}
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { Spacer, VStack, Text, Flex, HStack, Button } from '@chakra-ui/react';
|
||||
import { useContext } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
||||
import { BackwardBox } from 'checkout/components';
|
||||
import {
|
||||
CustomizationContext,
|
||||
LocaleContext,
|
||||
PaymentContext,
|
||||
PaymentModelContext,
|
||||
ViewModelContext,
|
||||
} from 'checkout/contexts';
|
||||
|
||||
import { CardHolder } from './CardHolder';
|
||||
import { CardNumber } from './CardNumber';
|
||||
import { ExpireDate } from './ExpireDate';
|
||||
import { SecureCode } from './SecureCode';
|
||||
import { CardFormInputs } from './types';
|
||||
import { isSecureCodeAvailable } from './utils';
|
||||
import {
|
||||
CustomizationContext,
|
||||
LocaleContext,
|
||||
PaymentContext,
|
||||
PaymentModelContext,
|
||||
ViewModelContext,
|
||||
} from '../../../../common/contexts';
|
||||
import { ChevronButton, FormGroup, HeaderWrapper, PayButton, Title } from '../../../../components/legacy';
|
||||
|
||||
export function CardForm() {
|
||||
const { l } = useContext(LocaleContext);
|
||||
@ -48,22 +50,22 @@ export function CardForm() {
|
||||
const isSecureCode = isSecureCodeAvailable(watch('cardNumber'));
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
{hasBackward && <ChevronButton type="left" onClick={backward} />}
|
||||
<Title>{l['form.header.pay.card.label']}</Title>
|
||||
</HeaderWrapper>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormGroup>
|
||||
<CardNumber
|
||||
fieldError={errors.cardNumber}
|
||||
isDirty={dirtyFields.cardNumber}
|
||||
locale={l}
|
||||
register={register}
|
||||
watch={watch}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup $gap={10}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<VStack align="stretch" spacing={5}>
|
||||
<Flex alignItems="center" direction="row">
|
||||
{hasBackward && <BackwardBox onClick={backward} />}
|
||||
<Text color="bodyText" fontWeight="medium" textAlign="center" width="full">
|
||||
{l['form.header.pay.card.label']}
|
||||
</Text>
|
||||
</Flex>
|
||||
<CardNumber
|
||||
fieldError={errors.cardNumber}
|
||||
isDirty={dirtyFields.cardNumber}
|
||||
locale={l}
|
||||
register={register}
|
||||
watch={watch}
|
||||
/>
|
||||
<HStack align="stretch" spacing={5}>
|
||||
<ExpireDate
|
||||
fieldError={errors.expireDate}
|
||||
isDirty={dirtyFields.expireDate}
|
||||
@ -80,19 +82,20 @@ export function CardForm() {
|
||||
register={register}
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
</HStack>
|
||||
{requireCardHolder && (
|
||||
<FormGroup>
|
||||
<CardHolder
|
||||
fieldError={errors.cardHolder}
|
||||
isDirty={dirtyFields.cardHolder}
|
||||
locale={l}
|
||||
register={register}
|
||||
/>
|
||||
</FormGroup>
|
||||
<CardHolder
|
||||
fieldError={errors.cardHolder}
|
||||
isDirty={dirtyFields.cardHolder}
|
||||
locale={l}
|
||||
register={register}
|
||||
/>
|
||||
)}
|
||||
<PayButton l={l} viewAmount={viewAmount} />
|
||||
</form>
|
||||
</>
|
||||
<Spacer />
|
||||
<Button borderRadius="lg" colorScheme="brand" size="lg" type="submit" variant="solid">
|
||||
{l['form.button.pay.label']} {viewAmount}
|
||||
</Button>
|
||||
</VStack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import { validateCardHolder } from './validateCardHolder';
|
||||
import { Locale } from '../../../../../common/contexts';
|
||||
import { isNil } from '../../../../../common/utils';
|
||||
import { Input } from '../../../../legacy';
|
||||
import { ReactComponent as UserIcon } from '../../../../legacy/icon/user.svg';
|
||||
import { CardFormInputs } from '../types';
|
||||
|
||||
export type CardHolderProps = {
|
||||
@ -24,7 +23,7 @@ export const CardHolder = ({ register, locale, fieldError, isDirty }: CardHolder
|
||||
autoComplete="cc-name"
|
||||
dirty={isDirty}
|
||||
error={!isNil(fieldError)}
|
||||
icon={<UserIcon />}
|
||||
// icon={<UserIcon />}
|
||||
id="card-holder-input"
|
||||
mark={true}
|
||||
placeholder={locale['form.input.cardholder.placeholder']}
|
||||
|
@ -5,8 +5,7 @@ import { formatCardNumber } from './formatCardNumber';
|
||||
import { validateCardNumber } from './validateCardNumber';
|
||||
import { Locale } from '../../../../../common/contexts';
|
||||
import { isNil } from '../../../../../common/utils';
|
||||
import { Input, CardTypeIcon } from '../../../../legacy';
|
||||
import { ReactComponent as CardIcon } from '../../../../legacy/icon/card.svg';
|
||||
import { Input } from '../../../../legacy';
|
||||
import { CardFormInputs } from '../types';
|
||||
|
||||
const InputContainer = styled.div`
|
||||
@ -28,7 +27,7 @@ export type CardNumberProps = {
|
||||
isDirty: boolean;
|
||||
};
|
||||
|
||||
export const CardNumber = ({ register, locale, fieldError, isDirty, watch }: CardNumberProps) => (
|
||||
export const CardNumber = ({ register, locale, fieldError, isDirty }: CardNumberProps) => (
|
||||
<InputContainer>
|
||||
<CardNumberInput
|
||||
{...register('cardNumber', {
|
||||
@ -38,13 +37,13 @@ export const CardNumber = ({ register, locale, fieldError, isDirty, watch }: Car
|
||||
autoComplete="cc-number"
|
||||
dirty={isDirty}
|
||||
error={!isNil(fieldError)}
|
||||
icon={<CardIcon />}
|
||||
// icon={<CardIcon />}
|
||||
id="card-number-input"
|
||||
mark={true}
|
||||
placeholder={locale['form.input.card.placeholder']}
|
||||
type="tel"
|
||||
onInput={formatCardNumber}
|
||||
/>
|
||||
<CardTypeIcon cardNumber={watch('cardNumber')} />
|
||||
{/* <CardTypeIcon cardNumber={watch('cardNumber')} /> */}
|
||||
</InputContainer>
|
||||
);
|
||||
|
@ -5,7 +5,6 @@ import { validateExpireDate } from './validateExpireDate';
|
||||
import { Locale } from '../../../../../common/contexts';
|
||||
import { isNil } from '../../../../../common/utils';
|
||||
import { Input } from '../../../../legacy';
|
||||
import { ReactComponent as CalendarIcon } from '../../../../legacy/icon/calendar.svg';
|
||||
import { CardFormInputs } from '../types';
|
||||
|
||||
export type ExpireDateProps = {
|
||||
@ -24,7 +23,7 @@ export const ExpireDate = ({ register, locale, fieldError, isDirty }: ExpireDate
|
||||
autoComplete="cc-exp"
|
||||
dirty={isDirty}
|
||||
error={!isNil(fieldError)}
|
||||
icon={<CalendarIcon />}
|
||||
// icon={<CalendarIcon />}
|
||||
id="expire-date-input"
|
||||
mark={true}
|
||||
placeholder={locale['form.input.expiry.placeholder']}
|
||||
|
@ -6,7 +6,6 @@ import { validateSecureCode } from './validateSecureCode';
|
||||
import { Locale } from '../../../../../common/contexts';
|
||||
import { isNil, safeVal } from '../../../../../common/utils';
|
||||
import { Input } from '../../../../legacy';
|
||||
import { ReactComponent as LockIcon } from '../../../../legacy/icon/lock.svg';
|
||||
import { CardFormInputs } from '../types';
|
||||
|
||||
export interface SecureCodeProps {
|
||||
@ -32,7 +31,7 @@ export const SecureCode = ({ cardNumber, locale, obscureCardCvv, register, field
|
||||
autoComplete="cc-csc"
|
||||
dirty={isDirty}
|
||||
error={!isNil(fieldError)}
|
||||
icon={<LockIcon />}
|
||||
// icon={<LockIcon />}
|
||||
id="secure-code-input"
|
||||
mark={true}
|
||||
placeholder={getPlaceholder(cardNumber, locale)}
|
||||
|
@ -17,7 +17,7 @@ export function PinikleAddon({ localePath, redirectLink }: PinikleAddonProps) {
|
||||
<Text fontSize="md" fontWeight="medium">
|
||||
{locale['label']}
|
||||
</Text>
|
||||
<Button colorScheme="teal" variant="link" onClick={() => window.open(redirectLink, '_blank')}>
|
||||
<Button colorScheme="brand" variant="link" onClick={() => window.open(redirectLink, '_blank')}>
|
||||
{locale['link']}
|
||||
</Button>
|
||||
</VStack>
|
||||
|
@ -96,7 +96,7 @@ export function MetadataForm({ provider }: MetadataFormProps) {
|
||||
)}
|
||||
{!isNil(addon) && <Addon addon={addon} />}
|
||||
<Spacer />
|
||||
<Button borderRadius="lg" colorScheme="teal" size="lg" type="submit" variant="solid">
|
||||
<Button borderRadius="lg" colorScheme="brand" size="lg" type="submit" variant="solid">
|
||||
{l['form.button.pay.label']} {viewAmount}
|
||||
</Button>
|
||||
</VStack>
|
||||
|
@ -43,7 +43,7 @@ export function PaymentProcessFailedView() {
|
||||
{l['form.header.final.error.label']}
|
||||
</Text>
|
||||
{!isNil(exception) && (
|
||||
<Text fontSize="lg" textAlign="center">
|
||||
<Text color="bodyText" fontSize="lg" textAlign="center">
|
||||
{getErrorDescription(exception)}
|
||||
</Text>
|
||||
)}
|
||||
@ -52,7 +52,7 @@ export function PaymentProcessFailedView() {
|
||||
<VStack align="stretch" spacing={6}>
|
||||
<Button
|
||||
borderRadius="lg"
|
||||
colorScheme="teal"
|
||||
colorScheme="brand"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
onClick={() => location.reload()}
|
||||
|
@ -59,7 +59,7 @@ export function PaymentResultView() {
|
||||
{l[label]}
|
||||
</Text>
|
||||
{!isNil(description) && (
|
||||
<Text fontSize="lg" textAlign="center">
|
||||
<Text color="bodyText" fontSize="lg" textAlign="center">
|
||||
{l[description]}
|
||||
</Text>
|
||||
)}
|
||||
@ -67,13 +67,13 @@ export function PaymentResultView() {
|
||||
<Spacer />
|
||||
<VStack align="stretch" spacing={6}>
|
||||
{hasActions && isExternalIdEmpty(conditions) && (
|
||||
<Button borderRadius="lg" colorScheme="teal" size="lg" variant="solid" onClick={retry}>
|
||||
<Button borderRadius="lg" colorScheme="brand" size="lg" variant="solid" onClick={retry}>
|
||||
{l['form.button.pay.again.label']}
|
||||
</Button>
|
||||
)}
|
||||
{initContext?.redirectUrl && (
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
colorScheme="brand"
|
||||
size="lg"
|
||||
variant="link"
|
||||
onClick={() => window.open(initContext.redirectUrl, '_self')}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useTheme } from '@chakra-ui/react';
|
||||
import kjua from 'kjua';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -11,12 +12,16 @@ export type QRCodeProps = {
|
||||
};
|
||||
|
||||
export function QRCode({ text }: QRCodeProps) {
|
||||
const {
|
||||
qrCode: { back, fill },
|
||||
} = useTheme();
|
||||
return (
|
||||
<Wrapper
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kjua({
|
||||
size: 224,
|
||||
fill: '#2596A1',
|
||||
back,
|
||||
fill,
|
||||
rounded: 100,
|
||||
crisp: true,
|
||||
ecLevel: 'H',
|
||||
|
@ -1,28 +1,15 @@
|
||||
import { Divider, useClipboard, useToast, VStack, Text, Button } from '@chakra-ui/react';
|
||||
import isMobile from 'ismobilejs';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { LocaleContext, PaymentConditionsContext, PaymentContext, PaymentModelContext } from 'checkout/contexts';
|
||||
import { PaymentInteractionRequested, PaymentStarted } from 'checkout/paymentCondition';
|
||||
import { isNil } from 'checkout/utils';
|
||||
import { findMetadata } from 'checkout/utils/findMetadata';
|
||||
|
||||
import { QRCode } from './QrCode';
|
||||
import { QrCodeFormMetadata } from '../../../common/backend/payments';
|
||||
import { LocaleContext, PaymentConditionsContext, PaymentContext, PaymentModelContext } from '../../../common/contexts';
|
||||
import { PaymentInteractionRequested, PaymentStarted } from '../../../common/paymentCondition';
|
||||
import { isNil } from '../../../common/utils';
|
||||
import { findMetadata } from '../../../common/utils/findMetadata';
|
||||
import { Button, CopyToClipboardButton, Hr, Input } from '../../../components/legacy';
|
||||
|
||||
const Instruction = styled.p`
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
import { Input } from '../../../components/legacy';
|
||||
|
||||
const isQrCodeRedirect = (formMetadata: QrCodeFormMetadata) =>
|
||||
!isNil(formMetadata) &&
|
||||
@ -48,13 +35,21 @@ export function QrCodeView() {
|
||||
startWaitingPaymentResult();
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
qrCodeInputRef.current.select();
|
||||
document.execCommand('copy');
|
||||
};
|
||||
const { onCopy, hasCopied } = useClipboard(interaction.qrCode);
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCopied) return;
|
||||
toast({
|
||||
title: l['form.button.copied.label'],
|
||||
status: 'success',
|
||||
variant: 'subtle',
|
||||
duration: 3000,
|
||||
});
|
||||
}, [hasCopied, l]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<VStack align="stretch" spacing={5}>
|
||||
{qrCodeForm && (
|
||||
<>
|
||||
{qrCodeForm.isCopyCodeBlock && (
|
||||
@ -65,17 +60,24 @@ export function QrCodeView() {
|
||||
id="qr-code-input"
|
||||
readOnly={true}
|
||||
></Input>
|
||||
<CopyToClipboardButton l={l} onClick={() => copyToClipboard()} />
|
||||
<Hr />
|
||||
<Button borderRadius="lg" colorScheme="brand" size="lg" onClick={onCopy}>
|
||||
{l['form.button.copy.label']}
|
||||
</Button>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<Instruction>{l['form.qr.code']}</Instruction>
|
||||
<Text color="bodyText" fontWeight="medium" textAlign="center">
|
||||
{l['form.qr.code']}
|
||||
</Text>
|
||||
<QRCode text={interaction.qrCode} />
|
||||
{initContext.redirectUrl && (
|
||||
<>
|
||||
<Hr />
|
||||
<Divider />
|
||||
<Button
|
||||
id="back-to-website-btn"
|
||||
borderRadius="lg"
|
||||
colorScheme="brand"
|
||||
size="lg"
|
||||
variant="link"
|
||||
onClick={() => window.open(initContext.redirectUrl, '_self')}
|
||||
>
|
||||
{l['form.button.back.to.website']}
|
||||
@ -84,6 +86,6 @@ export function QrCodeView() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Wrapper>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export function ViewContainer() {
|
||||
<>
|
||||
<Flex
|
||||
alignItems="stretch"
|
||||
background="gray.50"
|
||||
background="mainContainerBg"
|
||||
borderRadius="2xl"
|
||||
direction={['column', 'column', 'row']}
|
||||
gap={4}
|
||||
@ -41,9 +41,9 @@ export function ViewContainer() {
|
||||
<InfoContainer viewAmount={viewAmount}></InfoContainer>
|
||||
<ViewModelContext.Provider value={{ viewModel, viewAmount, goTo, forward, backward }}>
|
||||
<Box
|
||||
background="white"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
background="viewContainerBg"
|
||||
// border="1px solid"
|
||||
// borderColor="gray.200"
|
||||
borderRadius="xl"
|
||||
p={[4, 4, 6]}
|
||||
position="relative"
|
||||
|
@ -23,6 +23,8 @@ const Icon = styled.div`
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<{ $hasIcon?: boolean }>`
|
||||
background-color: ${({ theme }) => theme.input.backgroundColor};
|
||||
color: ${({ theme }) => theme.input.color};
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
@ -35,11 +37,11 @@ const StyledInput = styled.input<{ $hasIcon?: boolean }>`
|
||||
padding-left: ${({ $hasIcon }) => `${$hasIcon ? CONTENT_OFFSET + ICON_SIZE + TEXT_ICON_OFFSET : CONTENT_OFFSET}px`};
|
||||
padding-right: ${CONTENT_OFFSET}px;
|
||||
appearance: none;
|
||||
transition: border-color 0.3s;
|
||||
/* transition: border-color 0.3s; */
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.input.placeholder};
|
||||
opacity: 1;
|
||||
/* opacity: 1; */
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
|
Loading…
Reference in New Issue
Block a user