@wf-financing/ui 3.11.0 → 3.12.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.es.js +21827 -12280
  3. package/package.json +5 -3
  4. package/src/CtaWidget.tsx +31 -9
  5. package/src/api/ctaBanner.ts +1 -2
  6. package/src/components/banner/BannerActions.tsx +26 -0
  7. package/src/components/banner/BulletList.tsx +34 -21
  8. package/src/components/banner/CloseButton.tsx +20 -1
  9. package/src/components/banner/CtaBanner.snapshot.stories.tsx +10 -1
  10. package/src/components/banner/CtaBanner.tsx +22 -14
  11. package/src/components/banner/CtaMainText.tsx +23 -0
  12. package/src/components/modal/ConsentModal.snapshot.stories.tsx +28 -25
  13. package/src/components/modal/ConsentModal.tsx +18 -30
  14. package/src/components/modal/ConsentModalContent.tsx +63 -0
  15. package/src/components/modal/FundingSteps.tsx +16 -8
  16. package/src/components/modal/ModalFooter.tsx +13 -18
  17. package/src/components/modal/TemplateWithUrl.tsx +30 -0
  18. package/src/config/applicationVersion.ts +1 -0
  19. package/src/config/index.ts +1 -0
  20. package/src/hooks/index.ts +1 -0
  21. package/src/hooks/useCopy.ts +30 -0
  22. package/src/main.tsx +0 -1
  23. package/src/utils/buildBannerConfig.ts +26 -0
  24. package/src/utils/getBannerConfig.ts +44 -0
  25. package/src/utils/getModalItems.ts +19 -0
  26. package/src/utils/index.ts +5 -0
  27. package/src/utils/injectUrlInTemplate.ts +6 -0
  28. package/src/utils/replacePlaceholdersForMainText.ts +29 -0
  29. package/tsconfig.json +2 -1
  30. package/vite-env.d.ts +1 -0
  31. package/vite.config.mts +3 -0
  32. package/src/components/banner/BannerActionsDesktop.tsx +0 -13
  33. package/src/components/banner/CtaBannerContent.tsx +0 -37
  34. package/src/components/banner/FooterActions.tsx +0 -8
  35. package/src/components/banner/HeaderActions.tsx +0 -21
  36. package/src/components/banner/ProceedFundingButton.tsx +0 -63
  37. package/src/constants/index.ts +0 -1
  38. package/src/constants/modalItems.ts +0 -26
@@ -0,0 +1,30 @@
1
+ import ReactMarkdown from 'react-markdown';
2
+ import { Link, Text } from '@wayflyer/flyui';
3
+ import { Logger } from '@wf-financing/logger';
4
+
5
+ import { injectUrlInTemplate } from '../../utils';
6
+
7
+ type TemplateWithUrlProps = {
8
+ templateString: string;
9
+ };
10
+
11
+ export const TemplateWithUrl = ({ templateString }: TemplateWithUrlProps) => {
12
+ const termsUrl = 'https://wayflyer.com/en/privacy-notice';
13
+ const text = injectUrlInTemplate(templateString, termsUrl, 'termsUrl');
14
+
15
+ const handleOpenExternalLink = () => {
16
+ Logger.logEvent('cta_open_privacy_note');
17
+ window.open('https://wayflyer.com/en/privacy-notice', '_blank', 'noopener,noreferrer');
18
+ };
19
+
20
+ return (
21
+ <ReactMarkdown
22
+ components={{
23
+ a: ({ children }) => <Link onClick={handleOpenExternalLink} children={children} />,
24
+ p: ({ children }) => <Text size="sm" lineHeight="normal" children={children} />,
25
+ }}
26
+ >
27
+ {text}
28
+ </ReactMarkdown>
29
+ );
30
+ };
@@ -0,0 +1 @@
1
+ export const APPLICATION_VERSION: string = import.meta.env.VITE_APP_VERSION;
@@ -6,3 +6,4 @@ export { HEADLESS_SDK_URL } from './url';
6
6
  export { WHITELISTED_PARTNER_IDS } from './whitelistedPartnerIds';
7
7
  export type { PartnerId, PartnerTheme } from './whitelistedPartnerIds';
8
8
  export { STATIC_BASE_URL, DM_SANS_URL, MERRION_SANS_URL, MODAL_LOGO_IMAGE_URL } from './staticUrls';
9
+ export { APPLICATION_VERSION } from './applicationVersion';
@@ -6,3 +6,4 @@ export { useContinueHostedApplication } from './useContinueHostedApplication';
6
6
  export { useDismissCta } from './useDismissCta';
7
7
  export { useRemoveInerted } from './useRemoveInerted';
8
8
  export { usePreloadImage } from './usePreloadImage';
9
+ export { useCopy } from './useCopy.ts';
@@ -0,0 +1,30 @@
1
+ import { SdkOptionsType } from '@wf-financing/embedded-types';
2
+ import { useQuery, keepPreviousData } from '@tanstack/react-query';
3
+ import { loadAndConfigureCopy } from '@wf-financing/ui-assets';
4
+
5
+ import { usePartnerContext } from './usePartnerContext.ts';
6
+ import { APPLICATION_VERSION } from '../config';
7
+ import { getPartnerIdFromToken } from '../utils';
8
+
9
+ type Options = SdkOptionsType & {
10
+ language?: string;
11
+ cohort?: string;
12
+ };
13
+
14
+ export const useCopy = () => {
15
+ const { options, companyToken } = usePartnerContext();
16
+
17
+ const partnerId = getPartnerIdFromToken(companyToken);
18
+ // TODO: remove after extension of SdkOptionType
19
+ const copyOptions = options as Options;
20
+
21
+ return useQuery({
22
+ queryKey: ['partnerCopy'],
23
+ queryFn: () => loadAndConfigureCopy({ version: APPLICATION_VERSION, partnerId, options: copyOptions }),
24
+ placeholderData: keepPreviousData,
25
+ refetchOnWindowFocus: false,
26
+ refetchOnMount: false,
27
+ staleTime: 10 * 60 * 1000,
28
+ gcTime: 10 * 60 * 1000,
29
+ });
30
+ };
package/src/main.tsx CHANGED
@@ -26,7 +26,6 @@ export const mountToTarget = async (
26
26
 
27
27
  const hostEl = document.getElementById(savedTargetId as string);
28
28
  if (!hostEl) {
29
- Logger.logError(`Target element with id "${savedTargetId}" not found.`);
30
29
  throw new Error(`Target element with id "${savedTargetId}" not found.`);
31
30
  }
32
31
 
@@ -0,0 +1,26 @@
1
+ import { replacePlaceholdersForMainText } from './replacePlaceholdersForMainText.ts';
2
+
3
+ type CopySection = {
4
+ mainText: string;
5
+ buttonText: string;
6
+ bulletPoints?: string[];
7
+ };
8
+
9
+ export type BannerConfig = CopySection & {
10
+ buttonAction: () => void;
11
+ };
12
+
13
+ type BuildBannerConfig = (
14
+ copySection: CopySection,
15
+ buttonAction: () => void,
16
+ offer?: { currency: string; amount: number },
17
+ ) => BannerConfig;
18
+
19
+ export const buildBannerConfig: BuildBannerConfig = (copySection, buttonAction, offer) => ({
20
+ mainText: offer
21
+ ? replacePlaceholdersForMainText(offer.currency, offer.amount, copySection.mainText)
22
+ : copySection.mainText,
23
+ bulletPoints: copySection.bulletPoints,
24
+ buttonText: copySection.buttonText,
25
+ buttonAction: buttonAction,
26
+ });
@@ -0,0 +1,44 @@
1
+ import type { Copy } from '@wf-financing/ui-assets';
2
+ import { CtaStateType, ApplicationRequiredActions, CtaResponseType } from '@wf-financing/embedded-types';
3
+
4
+ import { buildBannerConfig, type BannerConfig } from './buildBannerConfig.ts';
5
+
6
+ type GetBannerConfig = (
7
+ cta: Exclude<CtaResponseType, null>,
8
+ copy: Copy,
9
+ handleIsModalOpen: () => void,
10
+ handleContinueHostedApplication: () => void,
11
+ ) => BannerConfig;
12
+
13
+ export const getBannerConfig: GetBannerConfig = (cta, copy, handleIsModalOpen, handleContinueHostedApplication) => {
14
+ const { state, data } = cta;
15
+ const {
16
+ genericOffer,
17
+ indicativeOffer,
18
+ completeApplication,
19
+ selectOffer,
20
+ completeKyc,
21
+ signContract,
22
+ provideAdditionalInfo,
23
+ } = copy;
24
+
25
+ switch (state) {
26
+ case CtaStateType.GENERIC_OFFER:
27
+ return buildBannerConfig(genericOffer, handleIsModalOpen);
28
+ case CtaStateType.INDICATIVE_OFFER:
29
+ return buildBannerConfig(indicativeOffer, handleIsModalOpen, data.offer);
30
+ case CtaStateType.CONTINUE_APPLICATION:
31
+ switch (cta.data.config.required_action) {
32
+ case ApplicationRequiredActions.COMPLETE_APPLICATION:
33
+ return buildBannerConfig(completeApplication, handleContinueHostedApplication);
34
+ case ApplicationRequiredActions.SELECT_OFFER:
35
+ return buildBannerConfig(selectOffer, handleContinueHostedApplication, data.offer);
36
+ case ApplicationRequiredActions.COMPLETE_KYC:
37
+ return buildBannerConfig(completeKyc, handleContinueHostedApplication);
38
+ case ApplicationRequiredActions.SIGN_CONTRACT:
39
+ return buildBannerConfig(signContract, handleContinueHostedApplication);
40
+ case ApplicationRequiredActions.PROVIDE_ADDITIONAL_INFO:
41
+ return buildBannerConfig(provideAdditionalInfo, handleContinueHostedApplication);
42
+ }
43
+ }
44
+ };
@@ -0,0 +1,19 @@
1
+ import {
2
+ IconArrowsCwHorizontal24Line,
3
+ IconCheckCircle24Line,
4
+ IconPersonCircle24Line,
5
+ } from '@wayflyer/flyui-icons/24/line';
6
+
7
+ type ModalDescriptions = {
8
+ title: string;
9
+ description: string;
10
+ };
11
+
12
+ export const getModalItems = (consentModalDescription: ModalDescriptions[]) => {
13
+ const modalIcons = [IconPersonCircle24Line, IconArrowsCwHorizontal24Line, IconCheckCircle24Line];
14
+
15
+ return consentModalDescription.map((description, id) => ({
16
+ ...description,
17
+ Icon: modalIcons[id],
18
+ }));
19
+ };
@@ -6,3 +6,8 @@ export { getPartnerThemeById } from './getPartnerThemeById';
6
6
  export { initializeHeadlessSdk } from './initializeHeadlessSdk';
7
7
  export { loadScriptAndInitializeSdk } from './loadScriptAndInitializeSdk';
8
8
  export { PartnerContext, type PartnerContextType } from './partnerContext';
9
+ export { replacePlaceholdersForMainText } from './replacePlaceholdersForMainText.ts';
10
+ export { getModalItems } from './getModalItems';
11
+ export { injectUrlInTemplate } from './injectUrlInTemplate';
12
+ export { buildBannerConfig, type BannerConfig } from './buildBannerConfig';
13
+ export { getBannerConfig } from './getBannerConfig';
@@ -0,0 +1,6 @@
1
+ export const injectUrlInTemplate = (template: string, url: string, placeholder: string) => {
2
+ const escaped = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ const regex = new RegExp(`\\(\\{${escaped}\\}\\)`, 'g');
4
+
5
+ return template.replace(regex, `(${url})`);
6
+ };
@@ -0,0 +1,29 @@
1
+ export const replacePlaceholdersForMainText = (currency: string, amount: number, templateString: string): string => {
2
+ const currencies = [
3
+ { code: 'USD', icon: '$', position: 'before' },
4
+ { code: 'GBP', icon: '£', position: 'before' },
5
+ { code: 'EUR', icon: '€', position: 'after' },
6
+ { code: 'SEK', icon: 'kr', position: 'after' },
7
+ { code: 'DKK', icon: 'kr', position: 'after' },
8
+ { code: 'AUD', icon: 'A$', position: 'before' },
9
+ { code: 'CAD', icon: 'C$', position: 'before' },
10
+ ];
11
+ const currencyWithIcon = currencies.find(({ code }) => currency === code);
12
+
13
+ const getConvertedAmount = () => {
14
+ if (!currencyWithIcon) return `${amount}${currency}`;
15
+
16
+ const { position, icon } = currencyWithIcon;
17
+
18
+ switch (position) {
19
+ case 'before':
20
+ return `${icon}${amount}`;
21
+ case 'after':
22
+ return `${amount}${icon}`;
23
+ default:
24
+ return `${icon}${amount}`;
25
+ }
26
+ };
27
+
28
+ return templateString.replace('{amount}', getConvertedAmount());
29
+ };
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "rootDir": ".",
5
- "baseUrl": "."
5
+ "baseUrl": ".",
6
+ "resolveJsonModule": true
6
7
  },
7
8
  "include": ["**/*.ts", "**/*.tsx"],
8
9
  "exclude": ["dist", "tests", "playground", "node_modules"]
package/vite-env.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/vite.config.mts CHANGED
@@ -4,6 +4,8 @@ import { defineConfig, type ConfigEnv, type UserConfig, type PluginOption } from
4
4
  import { visualizer } from 'rollup-plugin-visualizer';
5
5
  import inject from '@rollup/plugin-inject';
6
6
 
7
+ import pkg from './package.json';
8
+
7
9
  const visualizerOptions = {
8
10
  'analyze-html': { emitFile: true, filename: `bundle-stats/index.html`, gzipSize: true },
9
11
  'analyze-json': { emitFile: true, filename: `bundle-stats/stats.json`, template: 'raw-data', gzipSize: true },
@@ -18,6 +20,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
18
20
  define: {
19
21
  'process.env': {},
20
22
  'process.env.NODE_ENV': JSON.stringify(mode),
23
+ 'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version),
21
24
  },
22
25
  server: {
23
26
  host: true,
@@ -1,13 +0,0 @@
1
- import { Flex } from '@wayflyer/flyui';
2
-
3
- import { CloseButton } from './CloseButton';
4
- import { ProceedFundingButton } from './ProceedFundingButton';
5
-
6
- export const BannerActionsDesktop = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
7
- return (
8
- <Flex gap="4">
9
- <ProceedFundingButton isOnDarkTheme={isOnDarkTheme} />
10
- <CloseButton isOnDarkTheme={isOnDarkTheme} />
11
- </Flex>
12
- );
13
- };
@@ -1,37 +0,0 @@
1
- import { Flex, Text, Heading } from '@wayflyer/flyui';
2
- import type { CtaResponseType } from '@wf-financing/embedded-types';
3
-
4
- import { useCtaBanner } from '../../hooks';
5
- import { BulletList } from './BulletList';
6
-
7
- type CtaBannerContentProps = {
8
- isMobile: boolean;
9
- isTablet: boolean;
10
- isOnDarkTheme: boolean;
11
- };
12
-
13
- export const CtaBannerContent = ({ isMobile, isTablet, isOnDarkTheme }: CtaBannerContentProps) => {
14
- const sdk = useCtaBanner();
15
- const ctaData = sdk.data as CtaResponseType;
16
- const TextComponent = isMobile ? Text : Heading;
17
-
18
- return (
19
- <>
20
- {!sdk.isLoading && (
21
- <Flex direction="column" gap="1">
22
- <TextComponent
23
- size={isMobile ? 'base' : 'lg'}
24
- fontWeight="medium"
25
- lineHeight="normal"
26
- color={isOnDarkTheme ? 'onDark' : 'default'}
27
- >
28
- {ctaData?.data?.config?.text}
29
- </TextComponent>
30
- {!(isTablet || isMobile) && ctaData?.data?.config?.bullet_points && (
31
- <BulletList items={ctaData?.data?.config?.bullet_points} isOnDarkTheme={isOnDarkTheme} />
32
- )}
33
- </Flex>
34
- )}
35
- </>
36
- );
37
- };
@@ -1,8 +0,0 @@
1
- import { useDetectDeviceSize } from '@wayflyer/flyui';
2
- import { ProceedFundingButton } from './ProceedFundingButton';
3
-
4
- export const FooterActions = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
5
- const { isMobile } = useDetectDeviceSize();
6
-
7
- return isMobile ? <ProceedFundingButton isOnDarkTheme={isOnDarkTheme} /> : null;
8
- };
@@ -1,21 +0,0 @@
1
- import { useDetectDeviceSize } from '@wayflyer/flyui';
2
- import styled from 'styled-components';
3
- import { BannerActionsDesktop } from './BannerActionsDesktop';
4
- import { CloseButton } from './CloseButton';
5
-
6
- const MobileButtonContainer = styled.div`
7
- margin-top: calc(0px - var(--sizes-spacing-3));
8
- margin-right: calc(0px - var(--sizes-spacing-2));
9
- `;
10
-
11
- export const HeaderActions = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
12
- const { isMobile } = useDetectDeviceSize();
13
-
14
- return isMobile ? (
15
- <MobileButtonContainer>
16
- <CloseButton isOnDarkTheme={isOnDarkTheme} />
17
- </MobileButtonContainer>
18
- ) : (
19
- <BannerActionsDesktop isOnDarkTheme={isOnDarkTheme} />
20
- );
21
- };
@@ -1,63 +0,0 @@
1
- import { Button } from '@wayflyer/flyui';
2
- import {
3
- ContinueHostedApplicationResponseType,
4
- CtaContinueFundingType,
5
- CtaGenericOfferType,
6
- CtaIndicativeOfferType,
7
- CtaStateType,
8
- } from '@wf-financing/embedded-types';
9
- import { Logger } from '@wf-financing/logger';
10
- import { useState } from 'react';
11
-
12
- import { useContinueHostedApplication, useCtaBanner } from '../../hooks';
13
- import { ConsentModal } from '../modal/ConsentModal';
14
-
15
- type CtaResponseType = CtaGenericOfferType | CtaIndicativeOfferType | CtaContinueFundingType;
16
-
17
- export const ProceedFundingButton = ({ isOnDarkTheme }: { isOnDarkTheme: boolean }) => {
18
- const [isModalOpen, setIsModalOpen] = useState(false);
19
- const sdkResponse = useCtaBanner();
20
- const sdk = sdkResponse.data as CtaResponseType;
21
- const { mutate, isPending: isLoading } = useContinueHostedApplication();
22
-
23
- if (!sdk) return null;
24
-
25
- const {
26
- state,
27
- data: { config },
28
- } = sdk;
29
-
30
- const handleContinueHostedApplication = () => {
31
- switch (state) {
32
- case CtaStateType.CONTINUE_APPLICATION:
33
- Logger.logEvent('cta_continue_hosted_application');
34
- mutate(undefined, {
35
- onSuccess: (nextUrl: ContinueHostedApplicationResponseType) => {
36
- const { next } = nextUrl;
37
- window.open(next);
38
- },
39
- onError: (error) => {
40
- Logger.logError(`Failed to continue application ${error.message}`);
41
- },
42
- });
43
- break;
44
- default:
45
- setIsModalOpen(true);
46
- break;
47
- }
48
- };
49
-
50
- return (
51
- <>
52
- <Button
53
- variant={isOnDarkTheme ? 'PrimaryOnDark' : 'Primary'}
54
- fullWidth
55
- onClick={handleContinueHostedApplication}
56
- loading={isLoading}
57
- >
58
- {config?.button_label}
59
- </Button>
60
- <ConsentModal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
61
- </>
62
- );
63
- };
@@ -1 +0,0 @@
1
- export { MODAL_ITEMS } from './modalItems';
@@ -1,26 +0,0 @@
1
- import {
2
- IconArrowsCwHorizontal24Line,
3
- IconCheckCircle24Line,
4
- IconPersonCircle24Line,
5
- } from '@wayflyer/flyui-icons/24/line';
6
-
7
- export const MODAL_ITEMS = [
8
- {
9
- id: 'step1',
10
- title: { id: 'ctaModal.step1.title' },
11
- subtitle: { id: 'ctaModal.step1.subtitle' },
12
- Icon: IconPersonCircle24Line,
13
- },
14
- {
15
- id: 'step2',
16
- title: { id: 'ctaModal.step2.title' },
17
- subtitle: { id: 'ctaModal.step2.subtitle' },
18
- Icon: IconArrowsCwHorizontal24Line,
19
- },
20
- {
21
- id: 'step3',
22
- title: { id: 'ctaModal.step3.title' },
23
- subtitle: { id: 'ctaModal.step3.subtitle' },
24
- Icon: IconCheckCircle24Line,
25
- },
26
- ];