Add Build In metadata icon (#329)

This commit is contained in:
Ildar Galeev 2024-07-25 19:58:55 +07:00 committed by GitHub
parent 0bfc635965
commit f09a49ae52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 116 additions and 125 deletions

View File

@ -49,7 +49,6 @@ export type {
ServiceProviderMetadataField,
ServiceProviderMetadataForm,
PaymentSessionInfoMetadata,
ServiceProviderIconMetadata,
MetadataTextLocalization,
ServiceProviderMetadataSelect,
ServiceProviderTitleMetadata,

View File

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

View File

@ -14,7 +14,7 @@ export function Pane(props: PaneProps) {
borderColor="chakra-border-color"
borderRadius="lg"
cursor="pointer"
p={2}
p={4}
spacing={2}
{...rest}
>

View File

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

View 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} />;
}

View 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} />;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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