mirror of
https://github.com/valitydev/checkout.git
synced 2024-11-06 02:25:18 +00:00
Add Build In metadata icon (#329)
This commit is contained in:
parent
0bfc635965
commit
f09a49ae52
@ -49,7 +49,6 @@ export type {
|
||||
ServiceProviderMetadataField,
|
||||
ServiceProviderMetadataForm,
|
||||
PaymentSessionInfoMetadata,
|
||||
ServiceProviderIconMetadata,
|
||||
MetadataTextLocalization,
|
||||
ServiceProviderMetadataSelect,
|
||||
ServiceProviderTitleMetadata,
|
||||
|
@ -41,12 +41,20 @@ export type ServiceProviderMetadataSelect = {
|
||||
index?: number;
|
||||
};
|
||||
|
||||
export type ServiceProviderIconMetadata = {
|
||||
export type ServiceProviderMetadataImage = {
|
||||
type?: 'image';
|
||||
src: string;
|
||||
width: string;
|
||||
height: string;
|
||||
};
|
||||
|
||||
export type ServiceProviderMetadataBuildInIcon = {
|
||||
type: 'buildInIcon';
|
||||
name: 'HiCreditCard' | 'HiCash' | 'HiLibrary';
|
||||
};
|
||||
|
||||
export type ServiceProviderMetadataLogo = ServiceProviderMetadataImage | ServiceProviderMetadataBuildInIcon;
|
||||
|
||||
export type ServiceProviderTitleMetadata = {
|
||||
icon: 'wallets' | 'online-banking' | 'bank-card';
|
||||
localization?: MetadataTextLocalization;
|
||||
@ -87,7 +95,7 @@ export type Addon = PinikleAddon;
|
||||
|
||||
export type CheckoutServiceProviderMetadata = {
|
||||
form?: ServiceProviderMetadataForm;
|
||||
logo?: ServiceProviderIconMetadata;
|
||||
logo?: ServiceProviderMetadataLogo;
|
||||
title?: ServiceProviderTitleMetadata;
|
||||
contactInfo?: ServiceProviderContactInfo;
|
||||
userInteraction?: UserInteractionMetadata;
|
||||
|
@ -14,7 +14,7 @@ export function Pane(props: PaneProps) {
|
||||
borderColor="chakra-border-color"
|
||||
borderRadius="lg"
|
||||
cursor="pointer"
|
||||
p={2}
|
||||
p={4}
|
||||
spacing={2}
|
||||
{...rest}
|
||||
>
|
||||
|
@ -2,7 +2,7 @@ import { Center, CenterProps, useColorModeValue } from '@chakra-ui/react';
|
||||
|
||||
export function PaneLogoBox(props: CenterProps) {
|
||||
const { children, ...rest } = props;
|
||||
const bgColor = useColorModeValue('white', 'gray.100');
|
||||
const bgColor = useColorModeValue('white', 'gray.200');
|
||||
|
||||
return (
|
||||
<Center bgColor={bgColor} borderRadius="md" height={16} p={4} userSelect="none" {...rest}>
|
||||
|
29
src/common/components/Pane/PaneMetadataBuildInIcon.tsx
Normal file
29
src/common/components/Pane/PaneMetadataBuildInIcon.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { HiCash, HiCreditCard, HiLibrary, HiQuestionMarkCircle } from 'react-icons/hi';
|
||||
|
||||
import { ServiceProviderMetadataBuildInIcon } from 'checkout/backend/payments/serviceProviderMetadata';
|
||||
import { isNil } from 'checkout/utils';
|
||||
|
||||
import { PaneLogo } from './PaneLogo';
|
||||
|
||||
export type PaneMetadataBuildInIconProps = {
|
||||
logo: ServiceProviderMetadataBuildInIcon;
|
||||
};
|
||||
|
||||
export function PaneMetadataBuildInIcon({ logo: { name } }: PaneMetadataBuildInIconProps) {
|
||||
const buildInIcons = {
|
||||
HiCreditCard: HiCreditCard,
|
||||
HiCash: HiCash,
|
||||
HiLibrary: HiLibrary,
|
||||
};
|
||||
const icon = buildInIcons[name];
|
||||
|
||||
if (isNil(icon)) {
|
||||
console.error(
|
||||
`Build in icon: ${name} is not found. Supported icon list: [${Object.keys(
|
||||
buildInIcons,
|
||||
)}]. Default icon will be used.`,
|
||||
);
|
||||
}
|
||||
|
||||
return <PaneLogo as={icon || HiQuestionMarkCircle} />;
|
||||
}
|
10
src/common/components/Pane/PaneMetadataImageLogo.tsx
Normal file
10
src/common/components/Pane/PaneMetadataImageLogo.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { ImageProps, Image } from '@chakra-ui/react';
|
||||
|
||||
import { ServiceProviderMetadataImage } from 'checkout/backend/payments/serviceProviderMetadata';
|
||||
|
||||
export type PaneMetadataImageLogoProps = { logo: ServiceProviderMetadataImage } & ImageProps;
|
||||
|
||||
export function PaneMetadataImageLogo(props: PaneMetadataImageLogoProps) {
|
||||
const { logo, ...rest } = props;
|
||||
return <Image height={logo.height} src={logo.src} width={logo.width} {...rest} />;
|
||||
}
|
@ -1,10 +1,47 @@
|
||||
import { ImageProps, Image } from '@chakra-ui/react';
|
||||
import { useMemo } from 'react';
|
||||
import { HiQuestionMarkCircle } from 'react-icons/hi';
|
||||
|
||||
import { ServiceProviderIconMetadata } from 'checkout/backend/payments/serviceProviderMetadata';
|
||||
import {
|
||||
ServiceProviderMetadataImage,
|
||||
ServiceProviderMetadataLogo,
|
||||
} from 'checkout/backend/payments/serviceProviderMetadata';
|
||||
import { isNil } from 'checkout/utils';
|
||||
|
||||
export type PaneMetadataLogoProps = { logo: ServiceProviderIconMetadata } & ImageProps;
|
||||
import { PaneLogo } from './PaneLogo';
|
||||
import { PaneMetadataBuildInIcon } from './PaneMetadataBuildInIcon';
|
||||
import { PaneMetadataImageLogo } from './PaneMetadataImageLogo';
|
||||
|
||||
export function PaneMetadataLogo(props: PaneMetadataLogoProps) {
|
||||
const { logo, ...rest } = props;
|
||||
return <Image height={logo.height} src={logo.src} width={logo.width} {...rest} />;
|
||||
export type PaneMetadataLogoProps = {
|
||||
logo: ServiceProviderMetadataLogo;
|
||||
};
|
||||
|
||||
const isServiceProviderMetadataImage = (logo: ServiceProviderMetadataLogo): logo is ServiceProviderMetadataImage => {
|
||||
return isNil(logo.type) || logo.type === 'image';
|
||||
};
|
||||
|
||||
export function PaneMetadataLogo({ logo }: PaneMetadataLogoProps) {
|
||||
const metadataLogo = useMemo(() => {
|
||||
if (isServiceProviderMetadataImage(logo)) {
|
||||
const requiredImage: Required<ServiceProviderMetadataImage> = {
|
||||
...logo,
|
||||
type: 'image',
|
||||
};
|
||||
return requiredImage;
|
||||
}
|
||||
if (logo.type === 'buildInIcon') {
|
||||
return logo;
|
||||
}
|
||||
console.error('ServiceProvider metadata logo is unsupported', logo);
|
||||
return null;
|
||||
}, [logo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNil(metadataLogo) && <PaneLogo as={HiQuestionMarkCircle} />}
|
||||
{!isNil(metadataLogo) && metadataLogo.type === 'image' && <PaneMetadataImageLogo logo={metadataLogo} />}
|
||||
{!isNil(metadataLogo) && metadataLogo.type === 'buildInIcon' && (
|
||||
<PaneMetadataBuildInIcon logo={metadataLogo} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -3,5 +3,7 @@ export { PaneText } from './PaneText';
|
||||
export { PaneLogoBox } from './PaneLogoBox';
|
||||
export { PaneLogo } from './PaneLogo';
|
||||
export { PaneMetadataLogo } from './PaneMetadataLogo';
|
||||
export { PaneMetadataImageLogo } from './PaneMetadataImageLogo';
|
||||
|
||||
export type { PaneMetadataLogoProps } from './PaneMetadataLogo';
|
||||
export type { PaneMetadataImageLogoProps } from './PaneMetadataImageLogo';
|
||||
|
@ -1,8 +1,8 @@
|
||||
export { ErrorAlert } from './ErrorAlert';
|
||||
export { BackwardBox } from './BackwardBox';
|
||||
export { GlobalSpinner } from './GlobalSpinner';
|
||||
export { Pane, PaneLogo, PaneLogoBox, PaneText, PaneMetadataLogo } from './Pane';
|
||||
export { Pane, PaneLogo, PaneLogoBox, PaneText, PaneMetadataLogo, PaneMetadataImageLogo } from './Pane';
|
||||
export { StatusInputRightElement } from './StatusInputRightElement';
|
||||
|
||||
export type { PaneMetadataLogoProps } from './Pane';
|
||||
export type { PaneMetadataImageLogoProps, PaneMetadataLogoProps } from './Pane';
|
||||
export type { StatusInputRightElementProps } from './StatusInputRightElement';
|
||||
|
@ -32,7 +32,6 @@ export { detectLocale } from './detectLocale';
|
||||
export { createRegExpForMetaPattern } from './createRegExpForMetaPattern';
|
||||
export { formatCard } from './formatCard';
|
||||
export { truncate } from './truncate';
|
||||
export { isEmojiSupported } from './isEmojiSupported';
|
||||
|
||||
export type { URLParams } from './getUrlParams';
|
||||
export type { CountrySubdivision, Country } from './countries';
|
||||
|
@ -1,51 +0,0 @@
|
||||
import { isEmojiSupported } from './isEmojiSupported';
|
||||
|
||||
describe('isEmojiSupported', () => {
|
||||
let ctx;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the canvas context
|
||||
ctx = {
|
||||
fillStyle: '',
|
||||
fillRect: jest.fn(),
|
||||
textBaseline: '',
|
||||
font: '',
|
||||
fillText: jest.fn(),
|
||||
getImageData: jest.fn(),
|
||||
};
|
||||
// Setup to return a fake image data array
|
||||
ctx.getImageData.mockReturnValue({
|
||||
data: new Uint8ClampedArray(32 * 32 * 4).fill(0), // All pixels transparent
|
||||
});
|
||||
// Mock canvas creation
|
||||
document.createElement = jest.fn().mockReturnValue({
|
||||
getContext: () => ctx,
|
||||
width: 32,
|
||||
height: 32,
|
||||
});
|
||||
});
|
||||
|
||||
test('should detect supported emoji by checking non-transparent pixels', () => {
|
||||
// Mocking the context to simulate an emoji being supported (non-transparent pixels)
|
||||
ctx.getImageData.mockReturnValue({
|
||||
data: new Uint8ClampedArray(32 * 32 * 4).fill(255), // All pixels non-transparent
|
||||
});
|
||||
|
||||
expect(isEmojiSupported('😊')).toBe(true);
|
||||
});
|
||||
|
||||
test('should detect unsupported emoji by checking all transparent pixels', () => {
|
||||
// Using the initial all-transparent setup by default
|
||||
|
||||
expect(isEmojiSupported('😊')).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle null canvas context gracefully', () => {
|
||||
// Simulate canvas context not being available
|
||||
(document.createElement as any).mockReturnValue({
|
||||
getContext: () => null,
|
||||
});
|
||||
|
||||
expect(isEmojiSupported('😊')).toBe(false);
|
||||
});
|
||||
});
|
@ -1,37 +0,0 @@
|
||||
import { isNil } from './isNil';
|
||||
|
||||
// Tests flag emoji
|
||||
export function isEmojiSupported(emoji: string): boolean {
|
||||
if (isNil(emoji)) {
|
||||
return false;
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvas.height = 32; // The size should be large enough to hold the emoji
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return false; // In case the browser doesn't support canvas
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'white'; // Background color
|
||||
ctx.fillRect(0, 0, 32, 32); // Fill the background
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.font = '32px Arial'; // A common font that may not support the emoji
|
||||
|
||||
ctx.fillText(emoji, 0, 0); // Draw the emoji on the canvas
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
|
||||
let isSupported = false;
|
||||
|
||||
// Loop through pixel data; every four items in the array represent the RGBA values of a pixel
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
const alpha = imageData[i + 3]; // Get the alpha value of the pixel
|
||||
if (alpha > 0) {
|
||||
// Check if the pixel is not completely transparent
|
||||
isSupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
import { Flex, Menu, MenuButton, MenuItem, MenuList, Text } from '@chakra-ui/react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { HiChevronDown } from 'react-icons/hi';
|
||||
|
||||
import { isEmojiSupported } from 'checkout/utils';
|
||||
|
||||
const localeInfo = {
|
||||
ar: {
|
||||
flag: '🇸🇦',
|
||||
@ -59,17 +57,14 @@ export type LocaleSelectorProps = {
|
||||
|
||||
export function LocaleSelector({ initLocaleCode, onLocaleChange }: LocaleSelectorProps) {
|
||||
const [activeLocaleCode, setActiveLocaleCode] = useState<string>(initLocaleCode);
|
||||
const isEmojiAvailable = useMemo(() => isEmojiSupported('🏳️'), []);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton color="white">
|
||||
<Flex alignItems="center" gap="1">
|
||||
{isEmojiAvailable && (
|
||||
<Text as="span" fontSize="md">
|
||||
{localeInfo[activeLocaleCode]?.flag}
|
||||
</Text>
|
||||
)}
|
||||
<Text as="span" fontSize="md">
|
||||
{localeInfo[activeLocaleCode]?.flag}
|
||||
</Text>
|
||||
<Text color="white" fontSize="md" fontWeight="bold">
|
||||
{localeInfo[activeLocaleCode]?.short || activeLocaleCode}
|
||||
</Text>
|
||||
@ -86,11 +81,9 @@ export function LocaleSelector({ initLocaleCode, onLocaleChange }: LocaleSelecto
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" gap="3">
|
||||
{isEmojiAvailable && (
|
||||
<Text as="span" fontSize="xl">
|
||||
{flag}
|
||||
</Text>
|
||||
)}
|
||||
<Text as="span" fontSize="xl">
|
||||
{flag}
|
||||
</Text>
|
||||
<Text fontSize="md">{long}</Text>
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useContext } from 'react';
|
||||
import { HiCash } from 'react-icons/hi';
|
||||
import { HiQuestionMarkCircle } from 'react-icons/hi';
|
||||
|
||||
import { Pane, PaneLogoBox, PaneLogo, PaneText, PaneMetadataLogo } from 'checkout/components';
|
||||
import { PaymentModelContext } from 'checkout/contexts';
|
||||
@ -21,7 +21,8 @@ export function PaymentTerminalPane({ onClick, provider }: PaymentTerminalPanePr
|
||||
return (
|
||||
<Pane onClick={onClick}>
|
||||
<PaneLogoBox>
|
||||
{isNil(logo) && <PaneLogo as={HiCash} />} {!isNil(logo) && <PaneMetadataLogo logo={logo} />}
|
||||
{isNil(logo) && <PaneLogo as={HiQuestionMarkCircle} />}
|
||||
{!isNil(logo) && <PaneMetadataLogo logo={logo} />}
|
||||
</PaneLogoBox>
|
||||
<PaneText>{serviceProvider?.brandName}</PaneText>
|
||||
</Pane>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HiCash } from 'react-icons/hi';
|
||||
import { HiViewGrid } from 'react-icons/hi';
|
||||
|
||||
import { Pane, PaneLogoBox, PaneLogo, PaneText } from 'checkout/components';
|
||||
|
||||
@ -11,7 +11,7 @@ export function TerminalSelectorPane({ onClick, category }: TerminalSelectorPane
|
||||
return (
|
||||
<Pane onClick={onClick}>
|
||||
<PaneLogoBox>
|
||||
<PaneLogo as={HiCash} />
|
||||
<PaneLogo as={HiViewGrid} />
|
||||
</PaneLogoBox>
|
||||
<PaneText>{category}</PaneText>
|
||||
</Pane>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { VStack, SimpleGrid, Input, Flex, Text } from '@chakra-ui/react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { HiCash } from 'react-icons/hi';
|
||||
import { HiQuestionMarkCircle } from 'react-icons/hi';
|
||||
|
||||
import { BackwardBox, Pane, PaneLogo, PaneLogoBox, PaneMetadataLogo, PaneText } from 'checkout/components';
|
||||
import { LocaleContext, PaymentModelContext, ViewModelContext } from 'checkout/contexts';
|
||||
@ -52,7 +52,8 @@ export function TerminalSelectorView() {
|
||||
{pageItems.map(({ logo, brandName, viewId }, i) => (
|
||||
<Pane key={i} onClick={() => forward(viewId)}>
|
||||
<PaneLogoBox>
|
||||
{isNil(logo) && <PaneLogo as={HiCash} />} {!isNil(logo) && <PaneMetadataLogo logo={logo} />}
|
||||
{isNil(logo) && <PaneLogo as={HiQuestionMarkCircle} />}
|
||||
{!isNil(logo) && <PaneMetadataLogo logo={logo} />}
|
||||
</PaneLogoBox>
|
||||
<PaneText>{brandName}</PaneText>
|
||||
</Pane>
|
||||
|
@ -3,9 +3,9 @@ import { useCallback, useReducer } from 'react';
|
||||
import {
|
||||
CheckoutServiceProviderMetadata,
|
||||
METADATA_NAMESPACE,
|
||||
ServiceProviderIconMetadata,
|
||||
ServiceProviderMetadataLogo,
|
||||
ServiceProviderMetadata,
|
||||
} from 'checkout/backend/payments';
|
||||
} from 'checkout/backend/payments/serviceProviderMetadata';
|
||||
import { TerminalServiceProvider } from 'checkout/paymentModel';
|
||||
|
||||
import { TerminalSelectorItem } from '../types';
|
||||
@ -17,7 +17,7 @@ type ServiceProviderPage = {
|
||||
export type GridItem = {
|
||||
viewId: string;
|
||||
brandName: string;
|
||||
logo: ServiceProviderIconMetadata | null;
|
||||
logo: ServiceProviderMetadataLogo | null;
|
||||
};
|
||||
|
||||
const toPages = (gridItems: GridItem[], itemsOnPage: number): ServiceProviderPage[] => {
|
||||
|
Loading…
Reference in New Issue
Block a user