Custom theme config (#315)

This commit is contained in:
Ildar Galeev 2024-07-12 18:47:33 +07:00 committed by GitHub
parent 8a8923d417
commit daa3fcdf73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 251 additions and 151 deletions

View File

@ -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">

View File

@ -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>
</>
);
}

View File

@ -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>
)}

View File

@ -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"

View File

@ -29,6 +29,7 @@ it('should return resolved init config', () => {
phoneNumber: null,
redirectUrl: null,
skipUserInteraction: false,
theme: null,
};
expect(actual).toEqual(expected);
});

View File

@ -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),
};
};

View File

@ -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 = {

View File

@ -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,

View File

@ -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,

View File

@ -20,7 +20,9 @@ export type Theme = {
border: string;
};
input: {
backgroundColor: string;
border: string;
color: string;
placeholder: string;
error: string;
focus: string;

View File

@ -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;

View File

@ -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} />

View File

@ -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')}

View File

@ -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} />}

View File

@ -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')}

View File

@ -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)}

View File

@ -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>

View File

@ -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>
)}

View File

@ -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>

View File

@ -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} />
</>

View File

@ -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>
);

View File

@ -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>
))}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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']}

View File

@ -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>
);

View File

@ -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']}

View File

@ -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)}

View File

@ -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>

View File

@ -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>

View File

@ -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()}

View File

@ -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')}

View File

@ -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',

View File

@ -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>
);
}

View File

@ -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"

View File

@ -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 {