@wf-financing/ui 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wf-financing/ui",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "exports": {
5
5
  ".": {
6
6
  "import": "./dist/index.es.js",
@@ -22,7 +22,7 @@
22
22
  "@storybook/testing-library": "^0.2.2",
23
23
  "@vitest/browser": "^3.2.4",
24
24
  "chromatic": "^13.1.2",
25
- "eslint-plugin-storybook": "^9.0.15",
25
+ "eslint-plugin-storybook": "^9.1.10",
26
26
  "playwright": "^1.53.2",
27
27
  "storybook": "^9.0.15",
28
28
  "typescript": "^5.0.0",
@@ -32,16 +32,12 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@tanstack/react-query": "5.81.5",
35
- "@wayflyer/flyui-icons": "1.6.0",
36
35
  "framer-motion": "^12.23.0",
37
36
  "react-aria": "^3.41.1",
38
37
  "react-intl": "^6.2.5",
39
38
  "styled-components": "^6.1.19",
40
39
  "@wf-financing/embedded-types": "0.4.1"
41
40
  },
42
- "peerDependencies": {
43
- "@wayflyer/flyui": "204.4.1"
44
- },
45
41
  "publishConfig": {
46
42
  "access": "public"
47
43
  },
@@ -3,11 +3,11 @@ import { Flex } from '@wayflyer/flyui';
3
3
  import { CloseButton } from './CloseButton';
4
4
  import { ProceedFundingButton } from './ProceedFundingButton';
5
5
 
6
- export const BannerActionsDesktop = () => {
6
+ export const BannerActionsDesktop = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
7
7
  return (
8
8
  <Flex gap="4">
9
- <ProceedFundingButton />
10
- <CloseButton />
9
+ <ProceedFundingButton isOnDarkTheme={isOnDarkTheme} />
10
+ <CloseButton isOnDarkTheme={isOnDarkTheme} />
11
11
  </Flex>
12
12
  );
13
13
  };
@@ -1,12 +1,20 @@
1
- import { Flex, Text } from '@wayflyer/flyui';
1
+ import { Flex, Text, Icon } from '@wayflyer/flyui';
2
2
  import { IconCheck12Line } from '@wayflyer/flyui-icons/12/line';
3
3
 
4
- export const BulletList = ({ items }: { items: string[] }) => (
4
+ export const BulletList = ({ items, isOnDarkTheme }: { items: string[]; isOnDarkTheme: boolean }) => (
5
5
  <Flex gap="4">
6
6
  {items.map((bullet_point: string) => (
7
7
  <Flex key={bullet_point} gap="1" align="center">
8
- <IconCheck12Line />
9
- <Text size="sm" fontWeight="medium" fontStyle="regular" lineHeight="tight">
8
+ <Icon color={isOnDarkTheme ? 'onDark' : 'default'}>
9
+ <IconCheck12Line />
10
+ </Icon>
11
+ <Text
12
+ size="sm"
13
+ fontWeight="medium"
14
+ fontStyle="regular"
15
+ lineHeight="tight"
16
+ color={isOnDarkTheme ? 'onDark' : 'default'}
17
+ >
10
18
  {bullet_point}
11
19
  </Text>
12
20
  </Flex>
@@ -3,7 +3,7 @@ import { IconX16Line } from '@wayflyer/flyui-icons/16/line';
3
3
 
4
4
  import { usePartnerContext, useDismissCta } from '../../hooks';
5
5
 
6
- export const CloseButton = () => {
6
+ export const CloseButton = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
7
7
  const { onWidgetClose: closeWidget } = usePartnerContext();
8
8
  const dismissCta = useDismissCta();
9
9
 
@@ -17,7 +17,7 @@ export const CloseButton = () => {
17
17
  };
18
18
 
19
19
  return (
20
- <Button onClick={handleCloseWidget} variant="Tertiary">
20
+ <Button onClick={handleCloseWidget} variant={isOnDarkTheme ? 'TertiaryOnDark' : 'Tertiary'}>
21
21
  <Flex padding="2">
22
22
  <IconX16Line />
23
23
  </Flex>
@@ -1,13 +1,11 @@
1
1
  import { Meta, StoryObj } from '@storybook/react';
2
- import { PartnerTheme } from '../../config';
3
- import { App } from '../../App';
4
2
  import { CtaBanner } from './CtaBanner';
3
+ import { PartnerContext } from '../../utils';
5
4
 
6
5
  const portalContainer = document.createElement('div');
7
6
  document.body.append(portalContainer);
8
7
 
9
8
  const defaultArgs = {
10
- theme: 'whiteLabel' as PartnerTheme,
11
9
  companyToken: 'demo-token',
12
10
  partnerCallback: () => {},
13
11
  onWidgetClose: () => {},
@@ -20,9 +18,9 @@ const defaultArgs = {
20
18
  type CtaBannerStoryArgs = typeof defaultArgs;
21
19
 
22
20
  const Template = (args: CtaBannerStoryArgs) => (
23
- <App {...args}>
21
+ <PartnerContext.Provider value={{ ...args }}>
24
22
  <CtaBanner />
25
- </App>
23
+ </PartnerContext.Provider>
26
24
  );
27
25
 
28
26
  const meta: Meta<typeof CtaBanner> = {
@@ -1,36 +1,42 @@
1
- import { Flex, Theme, useDetectDeviceSize, useTheme } from '@wayflyer/flyui';
1
+ import { Flex, useDetectDeviceSize, useTheme } from '@wayflyer/flyui';
2
2
  import { styled } from 'styled-components';
3
3
 
4
4
  import { CtaBannerContent } from './CtaBannerContent';
5
5
  import { FooterActions } from './FooterActions';
6
6
  import { HeaderActions } from './HeaderActions';
7
+ import { usePreloadImage } from '../../hooks';
8
+ import { MODAL_LOGO_IMAGE_URL, STATIC_BASE_URL } from '../../config';
7
9
 
8
- type BannerContainerPropTypes = {
9
- theme: Theme;
10
- $isMobile: boolean;
11
- };
10
+ const ON_DARK_THEMES = ['athena'];
12
11
 
13
- const BannerContainer = styled.aside<BannerContainerPropTypes>`
14
- padding: ${({ theme, $isMobile }) => theme.spacing($isMobile ? ['3', '3', '3', '4'] : ['4', '4', '4', '6'])};
15
- box-shadow: ${({ theme }) => theme.effects.shadow};
16
- border-radius: ${({ theme }) => theme.borderRadius(['md'])};
12
+ const BannerContainer = styled.aside`
13
+ padding: var(--sizes-spacing-4) var(--sizes-spacing-4) var(--sizes-spacing-4) var(--sizes-spacing-6);
14
+ box-shadow: var(--effects-shadow);
15
+ border-radius: var(--sizes-radius-md);
17
16
  display: flex;
18
- flex-direction: ${({ $isMobile }) => ($isMobile ? 'column' : 'row')};
19
- gap: ${({ $isMobile, theme }) => ($isMobile ? theme.spacing(['4']) : '0')};
20
- background-color: ${({ theme }) => theme.palette.utility.primaryBrand};
17
+ background-color: var(--palette-utility-primaryBrand);
18
+
19
+ @media (max-width: 640px) {
20
+ padding: var(--sizes-spacing-3) var(--sizes-spacing-3) var(--sizes-spacing-3) var(--sizes-spacing-4);
21
+ flex-direction: column;
22
+ gap: var(--sizes-spacing-4);
23
+ }
21
24
  `;
22
25
 
23
26
  export const CtaBanner = () => {
24
27
  const { isMobile, isTablet } = useDetectDeviceSize();
25
- const theme = useTheme();
28
+ const logoImageUrl = `${STATIC_BASE_URL}${MODAL_LOGO_IMAGE_URL}`;
29
+ const { themeName } = useTheme();
30
+ const isOnDarkTheme = ON_DARK_THEMES.includes(themeName);
31
+ usePreloadImage(logoImageUrl);
26
32
 
27
33
  return (
28
- <BannerContainer $isMobile={isMobile} theme={theme}>
34
+ <BannerContainer>
29
35
  <Flex gap="4" align="center" justify="space-between" width="100%">
30
- <CtaBannerContent isMobile={isMobile} isTablet={isTablet} />
31
- <HeaderActions />
36
+ <CtaBannerContent isMobile={isMobile} isTablet={isTablet} isOnDarkTheme={isOnDarkTheme} />
37
+ <HeaderActions isOnDarkTheme={isOnDarkTheme} />
32
38
  </Flex>
33
- <FooterActions />
39
+ <FooterActions isOnDarkTheme={isOnDarkTheme} />
34
40
  </BannerContainer>
35
41
  );
36
42
  };
@@ -1,4 +1,4 @@
1
- import { Flex, Text } from '@wayflyer/flyui';
1
+ import { Flex, Text, Heading } from '@wayflyer/flyui';
2
2
  import type { CtaResponseType } from '@wf-financing/embedded-types';
3
3
 
4
4
  import { useCtaBanner } from '../../hooks';
@@ -7,21 +7,28 @@ import { BulletList } from './BulletList';
7
7
  type CtaBannerContentProps = {
8
8
  isMobile: boolean;
9
9
  isTablet: boolean;
10
+ isOnDarkTheme: boolean;
10
11
  };
11
12
 
12
- export const CtaBannerContent = ({ isMobile, isTablet }: CtaBannerContentProps) => {
13
+ export const CtaBannerContent = ({ isMobile, isTablet, isOnDarkTheme }: CtaBannerContentProps) => {
13
14
  const sdk = useCtaBanner();
14
15
  const ctaData = sdk.data as CtaResponseType;
16
+ const TextComponent = isMobile ? Text : Heading;
15
17
 
16
18
  return (
17
19
  <>
18
20
  {!sdk.isLoading && (
19
21
  <Flex direction="column" gap="1">
20
- <Text size={isMobile ? 'base' : 'lg'} fontStyle="regular" fontWeight="medium" lineHeight="normal">
22
+ <TextComponent
23
+ size={isMobile ? 'base' : 'lg'}
24
+ fontWeight="medium"
25
+ lineHeight="normal"
26
+ color={isOnDarkTheme ? 'onDark' : 'default'}
27
+ >
21
28
  {ctaData?.data?.config?.text}
22
- </Text>
29
+ </TextComponent>
23
30
  {!(isTablet || isMobile) && ctaData?.data?.config?.bullet_points && (
24
- <BulletList items={ctaData?.data?.config?.bullet_points} />
31
+ <BulletList items={ctaData?.data?.config?.bullet_points} isOnDarkTheme={isOnDarkTheme} />
25
32
  )}
26
33
  </Flex>
27
34
  )}
@@ -1,8 +1,8 @@
1
1
  import { useDetectDeviceSize } from '@wayflyer/flyui';
2
2
  import { ProceedFundingButton } from './ProceedFundingButton';
3
3
 
4
- export const FooterActions = () => {
4
+ export const FooterActions = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
5
5
  const { isMobile } = useDetectDeviceSize();
6
6
 
7
- return isMobile ? <ProceedFundingButton /> : null;
7
+ return isMobile ? <ProceedFundingButton isOnDarkTheme={isOnDarkTheme} /> : null;
8
8
  };
@@ -2,8 +2,12 @@ import { useDetectDeviceSize } from '@wayflyer/flyui';
2
2
  import { BannerActionsDesktop } from './BannerActionsDesktop';
3
3
  import { CloseButton } from './CloseButton';
4
4
 
5
- export const HeaderActions = () => {
5
+ export const HeaderActions = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
6
6
  const { isMobile } = useDetectDeviceSize();
7
7
 
8
- return isMobile ? <CloseButton /> : <BannerActionsDesktop />;
8
+ return isMobile ? (
9
+ <CloseButton isOnDarkTheme={isOnDarkTheme} />
10
+ ) : (
11
+ <BannerActionsDesktop isOnDarkTheme={isOnDarkTheme} />
12
+ );
9
13
  };
@@ -8,16 +8,17 @@ import {
8
8
  } from '@wf-financing/embedded-types';
9
9
  import { useState } from 'react';
10
10
 
11
- import { useCtaBanner, useContinueHostedApplication } from '../../hooks';
11
+ import { useCtaBanner, useContinueHostedApplication, useInvalidateCta } from '../../hooks';
12
12
  import { ConsentModal } from '../modal/ConsentModal';
13
13
 
14
14
  type CtaResponseType = CtaGenericOfferType | CtaIndicativeOfferType | CtaContinueFundingType;
15
15
 
16
- export const ProceedFundingButton = () => {
16
+ export const ProceedFundingButton = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
17
17
  const [isModalOpen, setIsModalOpen] = useState(false);
18
18
  const sdkResponse = useCtaBanner();
19
19
  const sdk = sdkResponse.data as CtaResponseType;
20
20
  const continueHostedApplicationMutation = useContinueHostedApplication();
21
+ const invalidateCta = useInvalidateCta();
21
22
 
22
23
  if (!sdk) return null;
23
24
 
@@ -33,6 +34,7 @@ export const ProceedFundingButton = () => {
33
34
  onSuccess: (nextUrl: ContinueHostedApplicationResponseType) => {
34
35
  const { next } = nextUrl;
35
36
  window.open(next);
37
+ invalidateCta();
36
38
  },
37
39
  onError: (error) => {
38
40
  console.error('Failed to continue application', error);
@@ -47,7 +49,7 @@ export const ProceedFundingButton = () => {
47
49
 
48
50
  return (
49
51
  <>
50
- <Button variant="Primary" fullWidth onClick={handleContinueHostedApplication}>
52
+ <Button variant={isOnDarkTheme ? 'PrimaryOnDark' : 'Primary'} fullWidth onClick={handleContinueHostedApplication}>
51
53
  {config?.button_label}
52
54
  </Button>
53
55
  <ConsentModal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
@@ -6,6 +6,7 @@ import { useDetectSmallScreen } from '../../hooks';
6
6
  import { FundingSteps } from './FundingSteps';
7
7
  import { Modal } from './Modal';
8
8
  import { ModalFooter } from './ModalFooter';
9
+ import { STATIC_BASE_URL, MODAL_LOGO_IMAGE_URL } from '../../config';
9
10
 
10
11
  type ConsentModalProps = {
11
12
  isModalOpen: boolean;
@@ -26,7 +27,7 @@ export const ConsentModal = ({ isModalOpen, setIsModalOpen }: ConsentModalProps)
26
27
 
27
28
  return (
28
29
  <Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen}>
29
- {!isSmallScreen && <ImageContainer src="https://static.wayflyer.com/flyui-assets/logos/wayflyer-ef.png" />}
30
+ {!isSmallScreen && <ImageContainer src={`${STATIC_BASE_URL}${MODAL_LOGO_IMAGE_URL}`} />}
30
31
  <Flex direction="column" gap="8" padding={isReducedSpacing ? '4' : '6'}>
31
32
  <Flex direction="column" gap="6">
32
33
  <Text fontStyle="regular" fontWeight="medium" lineHeight="tight" size="2xl">
@@ -3,7 +3,7 @@ import { IconArrowOnSquareUpRight16Line } from '@wayflyer/flyui-icons/16/line';
3
3
  import { StartHostedApplicationResponseType } from '@wf-financing/embedded-types';
4
4
  import { FormattedMessage } from 'react-intl';
5
5
 
6
- import { useDetectSmallScreen, useStartHostedApplication } from '../../hooks';
6
+ import { useDetectSmallScreen, useStartHostedApplication, useInvalidateCta } from '../../hooks';
7
7
 
8
8
  type ModalFooterType = {
9
9
  setOpen: (isOpen: boolean) => void;
@@ -13,6 +13,7 @@ export const ModalFooter = ({ setOpen }: ModalFooterType) => {
13
13
  const { isMobile } = useDetectDeviceSize();
14
14
  const startHostedAppMutation = useStartHostedApplication();
15
15
  const isSmallScreen = useDetectSmallScreen();
16
+ const invalidateCta = useInvalidateCta();
16
17
 
17
18
  const handleStartApplication = () => {
18
19
  startHostedAppMutation.mutate(undefined, {
@@ -20,6 +21,7 @@ export const ModalFooter = ({ setOpen }: ModalFooterType) => {
20
21
  const { next } = nextUrl;
21
22
  setOpen(false);
22
23
  window.open(next);
24
+ invalidateCta();
23
25
  },
24
26
  onError: (error) => {
25
27
  console.error('Failed to start application', error);
@@ -1,3 +1,5 @@
1
+ import { STATIC_BASE_URL, DM_SANS_URL, MERRION_SANS_URL, MONTSERRAT_URL } from './staticUrls';
2
+
1
3
  export type FontParamsType = {
2
4
  fontFamily: string;
3
5
  fontUrl: string;
@@ -6,17 +8,24 @@ export type FontParamsType = {
6
8
 
7
9
  const dmSansParams = {
8
10
  fontFamily: 'DM Sans',
9
- fontUrl: 'https://static.wayflyer.com/flyui-assets/fonts/dm-sans/DMSans-VariableFont_opsz,wght.ttf',
11
+ fontUrl: `${STATIC_BASE_URL}${DM_SANS_URL}`,
10
12
  fallbackFontUrl: 'https://app.wayflyer.com/flyui-assets/fonts/dm-sans/DMSans-VariableFont_opsz,wght.ttf',
11
13
  };
12
14
 
13
15
  const merrionSansParams = {
14
16
  fontFamily: 'Merrion Sans',
15
- fontUrl: 'https://static.wayflyer.com/flyui-assets/fonts/merrion-sans/Merrion_Sans-Medium.woff2',
17
+ fontUrl: `${STATIC_BASE_URL}${MERRION_SANS_URL}`,
16
18
  fallbackFontUrl: 'https://app.wayflyer.com/flyui-assets/fonts/merrion-sans/Merrion_Sans-Medium.woff2',
17
19
  };
18
20
 
21
+ const montserratParams = {
22
+ fontFamily: 'Montserrat',
23
+ fontUrl: `${STATIC_BASE_URL}${MONTSERRAT_URL}`,
24
+ fallbackFontUrl: 'https://app.wayflyer.com/flyui-assets/fonts/montserrat/Montserrat-VariableFont_wght.ttf',
25
+ };
26
+
19
27
  export const FONT_PARAMS: Record<string, FontParamsType> = {
20
28
  dmSansParams,
21
29
  merrionSansParams,
30
+ montserratParams,
22
31
  };
@@ -5,3 +5,4 @@ export { WAYFLYER_HEADLESS_SDK_ID } from './scriptId';
5
5
  export { HEADLESS_SDK_URL } from './url';
6
6
  export { WHITELISTED_PARTNER_IDS } from './whitelistedPartnerIds';
7
7
  export type { PartnerId, PartnerTheme } from './whitelistedPartnerIds';
8
+ export { STATIC_BASE_URL, DM_SANS_URL, MERRION_SANS_URL, MODAL_LOGO_IMAGE_URL } from './staticUrls';
@@ -0,0 +1,5 @@
1
+ export const STATIC_BASE_URL = 'https://static.wayflyer.com/flyui-assets';
2
+ export const DM_SANS_URL = '/fonts/dm-sans/DMSans-VariableFont_opsz,wght.ttf';
3
+ export const MERRION_SANS_URL = 'fonts/dm-sans/DMSans-VariableFont_opsz,wght.ttf';
4
+ export const MONTSERRAT_URL = 'fonts/montserrat/Montserrat-VariableFont_wght.ttf';
5
+ export const MODAL_LOGO_IMAGE_URL = '/logos/wayflyer-ef.png';
@@ -10,5 +10,6 @@ export type PartnerId = (typeof WHITELISTED_PARTNER_IDS)[PartnerTheme];
10
10
  export const WHITELISTED_PARTNER_IDS = {
11
11
  whiteLabel: '2416e2af-c9f7-40ac-a2f0-f37dcfe59674',
12
12
  wayflyer: '1f309265-5324-4f8b-88d7-9b0d5c77a27c',
13
+ athena: '970df87a-e7d5-4942-9450-a77cf771dca3',
13
14
  defaultTheme: 'defaultTheme',
14
15
  } as const satisfies Record<PartnerTheme, string>;
@@ -5,3 +5,5 @@ export { useDetectSmallScreen } from './useDetectSmallScreen';
5
5
  export { useContinueHostedApplication } from './useContinueHostedApplication';
6
6
  export { useDismissCta } from './useDismissCta';
7
7
  export { useRemoveInerted } from './useRemoveInerted';
8
+ export { usePreloadImage } from './usePreloadImage';
9
+ export { useInvalidateCta } from './useInvalidateCta';
@@ -1,4 +1,4 @@
1
- import { useQuery } from '@tanstack/react-query';
1
+ import { useQuery, keepPreviousData } from '@tanstack/react-query';
2
2
 
3
3
  import { fetchCtaBanner } from '../api';
4
4
  import { usePartnerContext } from './usePartnerContext';
@@ -7,8 +7,11 @@ export const useCtaBanner = () => {
7
7
  const { companyToken, options } = usePartnerContext();
8
8
 
9
9
  return useQuery({
10
- queryKey: ['cta', companyToken, options],
10
+ queryKey: ['cta', companyToken],
11
11
  queryFn: () => fetchCtaBanner(companyToken, options),
12
12
  staleTime: Infinity,
13
+ refetchOnWindowFocus: false,
14
+ placeholderData: keepPreviousData,
15
+ enabled: !!companyToken,
13
16
  });
14
17
  };
@@ -0,0 +1,13 @@
1
+ import { useQueryClient } from '@tanstack/react-query';
2
+
3
+ import { usePartnerContext } from './index';
4
+
5
+ export const useInvalidateCta = () => {
6
+ const queryClient = useQueryClient();
7
+ const { companyToken } = usePartnerContext();
8
+
9
+ return () =>
10
+ queryClient.invalidateQueries({
11
+ queryKey: ['cta', companyToken],
12
+ });
13
+ };
@@ -0,0 +1,14 @@
1
+ import { useEffect } from 'react';
2
+
3
+ export function usePreloadImage(src: string) {
4
+ useEffect(() => {
5
+ if (!src) return;
6
+
7
+ const img = new Image();
8
+ img.decoding = 'async';
9
+ img.fetchPriority = 'high';
10
+ img.src = src;
11
+
12
+ img.decode();
13
+ }, [src]);
14
+ }
@@ -4,12 +4,15 @@ import { loadFont } from './loadFont';
4
4
  type ApplyFontsType = (shadow: ShadowRoot, partnerTheme: PartnerTheme) => Promise<void>;
5
5
 
6
6
  export const applyFont: ApplyFontsType = async (shadow, partnerTheme) => {
7
- const { dmSansParams, merrionSansParams } = FONT_PARAMS;
7
+ const { dmSansParams, merrionSansParams, montserratParams } = FONT_PARAMS;
8
8
 
9
9
  switch (partnerTheme) {
10
10
  case 'whiteLabel':
11
11
  await loadFont(shadow, dmSansParams);
12
12
  break;
13
+ case 'athena':
14
+ await loadFont(shadow, montserratParams);
15
+ break;
13
16
  case 'wayflyer':
14
17
  case 'defaultTheme':
15
18
  await loadFont(shadow, merrionSansParams);
package/vite.config.ts CHANGED
@@ -5,6 +5,9 @@ import { defineConfig } from 'vite';
5
5
 
6
6
  export default defineConfig(({ mode }) => ({
7
7
  plugins: [react()],
8
+ optimizeDeps: {
9
+ exclude: ['@wayflyer/*'],
10
+ },
8
11
  define: {
9
12
  'process.env': {},
10
13
  'process.env.NODE_ENV': JSON.stringify(mode),