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