diff --git a/src/common/backend/payments/index.ts b/src/common/backend/payments/index.ts index 7709da9b..62da3e77 100644 --- a/src/common/backend/payments/index.ts +++ b/src/common/backend/payments/index.ts @@ -49,7 +49,6 @@ export type { ServiceProviderMetadataField, ServiceProviderMetadataForm, PaymentSessionInfoMetadata, - ServiceProviderIconMetadata, MetadataTextLocalization, ServiceProviderMetadataSelect, ServiceProviderTitleMetadata, diff --git a/src/common/backend/payments/serviceProviderMetadata.ts b/src/common/backend/payments/serviceProviderMetadata.ts index a7794825..da86600d 100644 --- a/src/common/backend/payments/serviceProviderMetadata.ts +++ b/src/common/backend/payments/serviceProviderMetadata.ts @@ -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; diff --git a/src/common/components/Pane/Pane.tsx b/src/common/components/Pane/Pane.tsx index a2d01dee..ac177ab3 100644 --- a/src/common/components/Pane/Pane.tsx +++ b/src/common/components/Pane/Pane.tsx @@ -14,7 +14,7 @@ export function Pane(props: PaneProps) { borderColor="chakra-border-color" borderRadius="lg" cursor="pointer" - p={2} + p={4} spacing={2} {...rest} > diff --git a/src/common/components/Pane/PaneLogoBox.tsx b/src/common/components/Pane/PaneLogoBox.tsx index 08ebdaca..699890af 100644 --- a/src/common/components/Pane/PaneLogoBox.tsx +++ b/src/common/components/Pane/PaneLogoBox.tsx @@ -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 (
diff --git a/src/common/components/Pane/PaneMetadataBuildInIcon.tsx b/src/common/components/Pane/PaneMetadataBuildInIcon.tsx new file mode 100644 index 00000000..e28d799f --- /dev/null +++ b/src/common/components/Pane/PaneMetadataBuildInIcon.tsx @@ -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 ; +} diff --git a/src/common/components/Pane/PaneMetadataImageLogo.tsx b/src/common/components/Pane/PaneMetadataImageLogo.tsx new file mode 100644 index 00000000..2d6f22d7 --- /dev/null +++ b/src/common/components/Pane/PaneMetadataImageLogo.tsx @@ -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 ; +} diff --git a/src/common/components/Pane/PaneMetadataLogo.tsx b/src/common/components/Pane/PaneMetadataLogo.tsx index 6691d687..52904903 100644 --- a/src/common/components/Pane/PaneMetadataLogo.tsx +++ b/src/common/components/Pane/PaneMetadataLogo.tsx @@ -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 ; +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 = { + ...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) && } + {!isNil(metadataLogo) && metadataLogo.type === 'image' && } + {!isNil(metadataLogo) && metadataLogo.type === 'buildInIcon' && ( + + )} + + ); } diff --git a/src/common/components/Pane/index.ts b/src/common/components/Pane/index.ts index 35e75f2c..20e35046 100644 --- a/src/common/components/Pane/index.ts +++ b/src/common/components/Pane/index.ts @@ -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'; diff --git a/src/common/components/index.ts b/src/common/components/index.ts index 1f46bfc8..db50c87a 100644 --- a/src/common/components/index.ts +++ b/src/common/components/index.ts @@ -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'; diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 999cdfc0..c679e9c1 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -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'; diff --git a/src/common/utils/isEmojiSupported.test.ts b/src/common/utils/isEmojiSupported.test.ts deleted file mode 100644 index 24e0e331..00000000 --- a/src/common/utils/isEmojiSupported.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/common/utils/isEmojiSupported.ts b/src/common/utils/isEmojiSupported.ts deleted file mode 100644 index a424cf0c..00000000 --- a/src/common/utils/isEmojiSupported.ts +++ /dev/null @@ -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; -} diff --git a/src/components/ViewContainer/LocaleSelector.tsx b/src/components/ViewContainer/LocaleSelector.tsx index c58566e2..c546e689 100644 --- a/src/components/ViewContainer/LocaleSelector.tsx +++ b/src/components/ViewContainer/LocaleSelector.tsx @@ -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(initLocaleCode); - const isEmojiAvailable = useMemo(() => isEmojiSupported('🏳️'), []); return ( - {isEmojiAvailable && ( - - {localeInfo[activeLocaleCode]?.flag} - - )} + + {localeInfo[activeLocaleCode]?.flag} + {localeInfo[activeLocaleCode]?.short || activeLocaleCode} @@ -86,11 +81,9 @@ export function LocaleSelector({ initLocaleCode, onLocaleChange }: LocaleSelecto }} > - {isEmojiAvailable && ( - - {flag} - - )} + + {flag} + {long} diff --git a/src/components/ViewContainer/PaymentMethodSelectorView/PaymentTerminalPane.tsx b/src/components/ViewContainer/PaymentMethodSelectorView/PaymentTerminalPane.tsx index b6cfb6de..8a29e5d1 100644 --- a/src/components/ViewContainer/PaymentMethodSelectorView/PaymentTerminalPane.tsx +++ b/src/components/ViewContainer/PaymentMethodSelectorView/PaymentTerminalPane.tsx @@ -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 ( - {isNil(logo) && } {!isNil(logo) && } + {isNil(logo) && } + {!isNil(logo) && } {serviceProvider?.brandName} diff --git a/src/components/ViewContainer/PaymentMethodSelectorView/TerminalSelectorPane.tsx b/src/components/ViewContainer/PaymentMethodSelectorView/TerminalSelectorPane.tsx index 1f73fe4c..874a38ff 100644 --- a/src/components/ViewContainer/PaymentMethodSelectorView/TerminalSelectorPane.tsx +++ b/src/components/ViewContainer/PaymentMethodSelectorView/TerminalSelectorPane.tsx @@ -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 ( - + {category} diff --git a/src/components/ViewContainer/TerminalSelectorView/TerminalSelectorView.tsx b/src/components/ViewContainer/TerminalSelectorView/TerminalSelectorView.tsx index 7b66c313..c5ab776c 100644 --- a/src/components/ViewContainer/TerminalSelectorView/TerminalSelectorView.tsx +++ b/src/components/ViewContainer/TerminalSelectorView/TerminalSelectorView.tsx @@ -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) => ( forward(viewId)}> - {isNil(logo) && } {!isNil(logo) && } + {isNil(logo) && } + {!isNil(logo) && } {brandName} diff --git a/src/components/ViewContainer/TerminalSelectorView/useGrigPages.ts b/src/components/ViewContainer/TerminalSelectorView/useGrigPages.ts index c8c98ce1..4471efb0 100644 --- a/src/components/ViewContainer/TerminalSelectorView/useGrigPages.ts +++ b/src/components/ViewContainer/TerminalSelectorView/useGrigPages.ts @@ -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[] => {