diff --git a/apps/kyb-app/public/poweredby-white.svg b/apps/kyb-app/public/poweredby-white.svg new file mode 100644 index 0000000000..9f6f5a025d --- /dev/null +++ b/apps/kyb-app/public/poweredby-white.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/apps/kyb-app/src/__tests/providers/TestProvider/TestProvider.tsx b/apps/kyb-app/src/__tests/providers/TestProvider/TestProvider.tsx index 570e0bfcb0..275c2d4c55 100644 --- a/apps/kyb-app/src/__tests/providers/TestProvider/TestProvider.tsx +++ b/apps/kyb-app/src/__tests/providers/TestProvider/TestProvider.tsx @@ -1,11 +1,9 @@ import { Head } from '@/Head'; -import { SettingsProvider } from '@/common/providers/SettingsProvider/SettingsProvider'; import { ThemeProvider } from '@/common/providers/ThemeProvider'; import { queryClient } from '@/common/utils/query-client'; import { AnyChildren } from '@ballerine/ui'; import { QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; -import settingsJson from '../../../../settings.json'; interface TestProviderProps { children: AnyChildren; @@ -16,9 +14,7 @@ export const TestProvider = ({ children }: TestProviderProps) => { - - {children} - + {children} ); diff --git a/apps/kyb-app/src/common/components/molecules/ProgressBar/ProgressBar.tsx b/apps/kyb-app/src/common/components/molecules/ProgressBar/ProgressBar.tsx index c5e7a2af98..2eeb06b3e5 100644 --- a/apps/kyb-app/src/common/components/molecules/ProgressBar/ProgressBar.tsx +++ b/apps/kyb-app/src/common/components/molecules/ProgressBar/ProgressBar.tsx @@ -1,10 +1,10 @@ -import styles from './ProgressBar.module.css'; import { Chip } from '@/common/components/atoms/Chip'; import { LoadingSpinner } from '@/common/components/atoms/LoadingSpinner'; -import { Check } from 'lucide-react'; -import { ctw } from '@ballerine/ui'; import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; +import { ctw } from '@ballerine/ui'; +import { Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +import styles from './ProgressBar.module.css'; interface Props { className?: string; diff --git a/apps/kyb-app/src/common/providers/ThemeProvider/ThemeProvider.tsx b/apps/kyb-app/src/common/providers/ThemeProvider/ThemeProvider.tsx index 4f96c3f45f..c20b048f5a 100644 --- a/apps/kyb-app/src/common/providers/ThemeProvider/ThemeProvider.tsx +++ b/apps/kyb-app/src/common/providers/ThemeProvider/ThemeProvider.tsx @@ -1,18 +1,44 @@ -import { useLayoutEffect } from 'react'; -import { transformThemeToInlineStyles } from '@/utils/transform-theme-to-inline-styles'; +import { APP_LANGUAGE_QUERY_KEY } from '@/common/consts/consts'; +import { IThemeContext } from '@/common/providers/ThemeProvider/types'; import { ITheme } from '@/common/types/settings'; +import { useUISchemasQuery } from '@/hooks/useUISchemasQuery'; +import { transformThemeToInlineStyles } from '@/utils/transform-theme-to-inline-styles'; +import { useLayoutEffect, useMemo } from 'react'; +import defaultTheme from '../../../../theme.json'; +import { themeContext } from './theme.context'; +const { Provider } = themeContext; interface Props { - theme: ITheme; children: React.ReactNode | React.ReactNode[]; } -export const ThemeProvider = ({ theme, children }: Props) => { +export const ThemeProvider = ({ children }: Props) => { + const language = new URLSearchParams(window.location.search).get(APP_LANGUAGE_QUERY_KEY) || 'en'; + const { data: uiSchema, isLoading, error } = useUISchemasQuery(language); + + const theme = useMemo(() => { + if (isLoading) return null; + + if (error) { + console.warn('Failed to load theme', error); + + return defaultTheme.theme; + } + + if (!uiSchema?.uiSchema?.theme) return defaultTheme.theme; + + return uiSchema.uiSchema.theme; + }, [uiSchema, isLoading, error]); + + const context = useMemo(() => ({ themeDefinition: theme } as IThemeContext), [theme]); + useLayoutEffect(() => { - document - .getElementsByTagName('html')[0] - ?.setAttribute('style', transformThemeToInlineStyles(theme)); - }); + if (theme) { + document + .getElementsByTagName('html')[0] + ?.setAttribute('style', transformThemeToInlineStyles(theme as ITheme)); + } + }, [theme]); - return <>{children}; + return {children}; }; diff --git a/apps/kyb-app/src/common/providers/ThemeProvider/index.ts b/apps/kyb-app/src/common/providers/ThemeProvider/index.ts index 8abd195f69..87dbc115ad 100644 --- a/apps/kyb-app/src/common/providers/ThemeProvider/index.ts +++ b/apps/kyb-app/src/common/providers/ThemeProvider/index.ts @@ -1 +1,2 @@ export * from './ThemeProvider'; +export * from './useTheme'; diff --git a/apps/kyb-app/src/common/providers/ThemeProvider/theme.context.ts b/apps/kyb-app/src/common/providers/ThemeProvider/theme.context.ts new file mode 100644 index 0000000000..c4feb5780c --- /dev/null +++ b/apps/kyb-app/src/common/providers/ThemeProvider/theme.context.ts @@ -0,0 +1,4 @@ +import { IThemeContext } from '@/common/providers/ThemeProvider/types'; +import { createContext } from 'react'; + +export const themeContext = createContext({} as IThemeContext); diff --git a/apps/kyb-app/src/common/providers/ThemeProvider/types.ts b/apps/kyb-app/src/common/providers/ThemeProvider/types.ts index bf1e1bc14a..1dc99678a7 100644 --- a/apps/kyb-app/src/common/providers/ThemeProvider/types.ts +++ b/apps/kyb-app/src/common/providers/ThemeProvider/types.ts @@ -1,3 +1,5 @@ -import { ISettings } from '@/common/types/settings'; +import { ITheme } from '@/common/types/settings'; -export type ThemeContext = ISettings['theme']; +export interface IThemeContext { + themeDefinition: ITheme; +} diff --git a/apps/kyb-app/src/common/providers/ThemeProvider/useTheme.ts b/apps/kyb-app/src/common/providers/ThemeProvider/useTheme.ts new file mode 100644 index 0000000000..907885a8e0 --- /dev/null +++ b/apps/kyb-app/src/common/providers/ThemeProvider/useTheme.ts @@ -0,0 +1,4 @@ +import { themeContext } from '@/common/providers/ThemeProvider/theme.context'; +import { useContext } from 'react'; + +export const useTheme = () => useContext(themeContext); diff --git a/apps/kyb-app/src/common/types/settings.ts b/apps/kyb-app/src/common/types/settings.ts index b583d95f21..10a6d85725 100644 --- a/apps/kyb-app/src/common/types/settings.ts +++ b/apps/kyb-app/src/common/types/settings.ts @@ -1,6 +1,7 @@ export interface ITheme { - pallete: Record; - elements: Record; + logo?: string; + palette: Record; + elements: Record>; } export interface ISettings { diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/Breadcrumbs.Label.tsx b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/Breadcrumbs.Label.tsx index e1f6dcdd14..2b4b1950b6 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/Breadcrumbs.Label.tsx +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/Breadcrumbs.Label.tsx @@ -1,13 +1,17 @@ +import { pickLabelProps } from '@/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-label-props'; import { BreadcrumbsLabelProps } from '@/components/atoms/Stepper/components/atoms/Breadcrumbs/types'; import { ctw } from '@ballerine/ui'; +import { useMemo } from 'react'; export const Label = ({ active, text, state }: BreadcrumbsLabelProps) => { + const labelProps = useMemo(() => pickLabelProps(state, active), [active, state]); + return ( {text} diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Inner.tsx b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Inner.tsx index dee44dfd67..a41f53c4e0 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Inner.tsx +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Inner.tsx @@ -6,7 +6,7 @@ export const Inner = ({ className, icon }: BreadcrumbsInnerProps) => { const { props } = useBreadcrumbElementLogic('inner'); return ( -
+
{icon || props.icon ? (
{icon || props.icon}
) : null} diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Outer.tsx b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Outer.tsx index 6f589d5f9f..16bcc58406 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Outer.tsx +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Outer.tsx @@ -4,5 +4,9 @@ import { BreadcrumbsOuterProps } from '@/components/atoms/Stepper/components/ato export const Outer = ({ className, children }: BreadcrumbsOuterProps) => { const { props } = useBreadcrumbElementLogic('outer'); - return
{children}
; + return ( +
+ {children} +
+ ); }; diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Wrapper.tsx b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Wrapper.tsx index 815478dfae..feddf453ed 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Wrapper.tsx +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/components/elements/Wrapper.tsx @@ -5,5 +5,13 @@ import { ctw } from '@ballerine/ui'; export const Wrapper = ({ className, children }: BreadcrumbsWrapperProps) => { const { props } = useBreadcrumbElementLogic('wrapper'); - return
{children}
; + return ( +
+ {children} +
+ ); }; diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-inner-props.ts b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-inner-props.ts index c0ecddcfe1..4ffdc5f7e9 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-inner-props.ts +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-inner-props.ts @@ -8,6 +8,7 @@ export const pickInnerProps: ElementPropsPicker = (state, const props: BreadcrumbsInnerProps = { className: ctw(themeParams.className, { [themeParams.activeClassName || '']: active }), icon: active ? themeParams.icon : themeParams.activeIcon || themeParams.icon, + style: active ? themeParams.activeStyles : themeParams.styles, }; return props; diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-label-props.ts b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-label-props.ts new file mode 100644 index 0000000000..7897c00382 --- /dev/null +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-label-props.ts @@ -0,0 +1,39 @@ +import { BreadcrumbState } from '@/components/atoms/Stepper/components/atoms/Breadcrumbs/types'; +import { CSSProperties } from 'react'; + +export const pickLabelProps = (state: BreadcrumbState, active: boolean) => { + const propsMap: Record = { + idle: { + style: { + color: active + ? 'var(--stepper-breadcrumbs-idle-label-text-color)' + : 'var(--stepper-breadcrumbs-idle-label-text-active-color)', + opacity: active + ? 'var(--stepper-breadcrumbs-idle-label-text-opacity)' + : 'var(--stepper-breadcrumbs-idle-label-text-active-opacity)', + }, + }, + warning: { + style: { + color: active + ? 'var(--stepper-breadcrumbs-warning-label-text-color)' + : 'var(--stepper-breadcrumbs-warning-label-text-active-color)', + opacity: active + ? 'var(--stepper-breadcrumbs-warning-label-text-opacity)' + : 'var(--stepper-breadcrumbs-warning-label-text-active-opacity)', + }, + }, + completed: { + style: { + color: active + ? 'var(--stepper-breadcrumbs-completed-label-text-color)' + : 'var(--stepper-breadcrumbs-completed-label-text-active-color)', + opacity: active + ? 'var(--stepper-breadcrumbs-completed-label-text-opacity)' + : 'var(--stepper-breadcrumbs-completed-label-text-active-opacity)', + }, + }, + }; + + return propsMap[state]; +}; diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-outer-props.ts b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-outer-props.ts index 5b47671642..46cb9c584d 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-outer-props.ts +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-outer-props.ts @@ -11,6 +11,7 @@ export const pickOuterProps: ElementPropsPicker = ( const props: BreadcrumbsOuterProps = { className: ctw(themeParams.className, { [themeParams.activeClassName || '']: active }), + style: active ? themeParams.activeStyles : themeParams.styles, }; return props; diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-wrapper.props.ts b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-wrapper.props.ts index 9605068744..3d32f8642c 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-wrapper.props.ts +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/helpers/pick-wrapper.props.ts @@ -11,6 +11,7 @@ export const pickWrapperProps: ElementPropsPicker = ( const props: BreadcrumbsWrapperProps = { className: ctw(themeParams.className, { [themeParams.activeClassName || '']: active }), + style: active ? themeParams.activeStyles : themeParams.styles, }; return props; diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/base-theme.tsx b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/base-theme.tsx index b5a2f29432..49b7f304e9 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/base-theme.tsx +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/base-theme.tsx @@ -14,11 +14,21 @@ export const baseBreadcrumbTheme: BreadcrumbTheme = { }, outer: { className: outerCommonClassName, - activeClassName: ctw('border-[#007AFF33]'), + styles: { + borderColor: 'var(--stepper-breadcrumbs-idle-outer-border-color)', + }, + activeStyles: { + borderColor: 'var(--stepper-breadcrumbs-idle-outer-active-border-color)', + }, }, wrapper: { className: ctw(wrapperCommonClassName, 'border'), - activeClassName: ctw('border-[#007AFF]'), + styles: { + borderColor: 'var(--stepper-breadcrumbs-idle-wrapper-border-color)', + }, + activeStyles: { + borderColor: 'var(--stepper-breadcrumbs-idle-wrapper-active-border-color)', + }, }, }, warning: { @@ -28,7 +38,12 @@ export const baseBreadcrumbTheme: BreadcrumbTheme = { }, outer: { className: outerCommonClassName, - activeClassName: 'border-[#FF8A0055]', + styles: { + borderColor: 'var(--stepper-breadcrumbs-warning-outer-border-color)', + }, + activeStyles: { + borderColor: 'var(--stepper-breadcrumbs-warning-outer-active-border-color)', + }, }, wrapper: { className: wrapperCommonClassName, @@ -41,11 +56,19 @@ export const baseBreadcrumbTheme: BreadcrumbTheme = { }, outer: { className: outerCommonClassName, - activeClassName: 'border-[#00BD5933]', + styles: { + borderColor: 'var(--stepper-breadcrumbs-completed-outer-border-color)', + }, + activeStyles: { + borderColor: 'var(--stepper-breadcrumbs-completed-outer-active-border-color)', + }, }, wrapper: { className: wrapperCommonClassName, - activeClassName: ctw('border-[1px] border-[#20B064]'), + activeClassName: ctw('border-[1px]'), + activeStyles: { + borderColor: 'var(--stepper-breadcrumbs-completed-wrapper-active-border-color)', + }, }, }, }; diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/common.ts b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/common.ts index 3dae3204b8..d66d68d43b 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/common.ts +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/theme/common.ts @@ -1,4 +1,4 @@ import { ctw } from '@ballerine/ui'; -export const outerCommonClassName = ctw('rounded-full', 'border-[2px] border-transparent'); +export const outerCommonClassName = ctw('rounded-full', 'border-[2px]'); export const wrapperCommonClassName = ctw('box-border', 'w-[12px]', 'h-[12px]', 'rounded-full'); diff --git a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/types.ts b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/types.ts index 7773a2d465..a9b42984c1 100644 --- a/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/types.ts +++ b/apps/kyb-app/src/components/atoms/Stepper/components/atoms/Breadcrumbs/types.ts @@ -1,4 +1,5 @@ import { AnyChildren } from '@ballerine/ui'; +import React from 'react'; export type BreadcrumbState = 'idle' | 'warning' | 'completed'; export type BreadcrumbElements = 'wrapper' | 'outer' | 'inner'; @@ -6,16 +7,19 @@ export type BreadcrumbElements = 'wrapper' | 'outer' | 'inner'; export interface BreadcrumbsOuterProps { className?: string; children?: AnyChildren; + style?: React.CSSProperties; } export interface BreadcrumbsInnerProps { className?: string; icon?: React.ReactNode; + style?: React.CSSProperties; } export interface BreadcrumbsWrapperProps { className?: string; children?: AnyChildren; + style?: React.CSSProperties; } export interface BreadcrumbContext { @@ -30,6 +34,8 @@ export interface BreadcrumbContext { export interface InnerThemeSettings { className: string; activeClassName?: string; + styles?: React.CSSProperties; + activeStyles?: React.CSSProperties; icon?: React.ReactNode; activeIcon?: React.ReactNode; } @@ -37,11 +43,15 @@ export interface InnerThemeSettings { export interface OuterThemeSettings { className: string; activeClassName?: string; + styles?: React.CSSProperties; + activeStyles?: React.CSSProperties; } export interface WrapperThemeSettings { className: string; activeClassName?: string; + styles?: React.CSSProperties; + activeStyles?: React.CSSProperties; } export interface BreadcrumbElementSettings { diff --git a/apps/kyb-app/src/components/layouts/AppShell/Content.tsx b/apps/kyb-app/src/components/layouts/AppShell/Content.tsx index b579611e19..f3767cc137 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/Content.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/Content.tsx @@ -5,5 +5,14 @@ interface Props { } export const Content = ({ children }: Props) => { - return
{children}
; + return ( +
+ {children} +
+ ); }; diff --git a/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx b/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx index 5e27e99a09..42ddce49b3 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx @@ -8,7 +8,7 @@ interface Props { export const FormContainer = ({ children, header }: Props) => { return ( -
+
{header ?
{header}
: null}
{children}
diff --git a/apps/kyb-app/src/components/layouts/AppShell/LanguagePicker.tsx b/apps/kyb-app/src/components/layouts/AppShell/LanguagePicker.tsx index da4391ea75..6f2f2a47cc 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/LanguagePicker.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/LanguagePicker.tsx @@ -1,12 +1,12 @@ -import { useMemo } from 'react'; import i18next from 'i18next'; +import { useMemo } from 'react'; -import { GlobeIcon } from '@/common/icons'; -import { DropdownInput } from '@ballerine/ui'; -import { useUISchemasQuery } from '@/hooks/useUISchemasQuery'; import { LoadingSpinner } from '@/common/components/atoms/LoadingSpinner'; +import { GlobeIcon } from '@/common/icons'; import { useLanguageParam } from '@/hooks/useLanguageParam/useLanguageParam'; import { useLanguageQuery } from '@/hooks/useLanguageQuery'; +import { useUISchemasQuery } from '@/hooks/useUISchemasQuery'; +import { DropdownInput } from '@ballerine/ui'; const countryCodeToLanguage = { en: 'English', @@ -41,7 +41,14 @@ export const LanguagePicker = () => { props={{ item: { variant: 'inverted' }, content: { className: 'w-[204px] p-1', align: 'start' }, - trigger: { icon: , className: 'px-3 gap-x-2 bg-black/5' }, + trigger: { + icon: ( + + + + ), + className: 'px-3 gap-x-2 bg-primary text-primary-foreground', + }, }} onChange={selectedLanguage => { updateLanguage(selectedLanguage); diff --git a/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx b/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx index 5b7cf5b9fb..bc669538b4 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx @@ -23,10 +23,12 @@ export const Navigation = () => { const onPrevious = useCallback(() => { if (!isFirstStep) { stateApi.sendEvent('PREVIOUS'); + return; } exit(); + return; }, [stateApi, exit]); @@ -34,7 +36,7 @@ export const Navigation = () => { return ( diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementProps/helpers.ts b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementProps/helpers.ts new file mode 100644 index 0000000000..3e481aaa81 --- /dev/null +++ b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementProps/helpers.ts @@ -0,0 +1,32 @@ +import { Rule } from '@/domains/collection-flow'; + +export const injectIndexesAtRulesPaths = (rules: Rule[], index: number | null = null) => { + if (index === null) return rules; + + if (!Array.isArray(rules)) return rules; + + const result = rules.map(rule => { + if (rule.type === 'json-logic') { + const stringValue = JSON.stringify(rule.value); + const newValue = JSON.parse( + stringValue.replace(/\[({INDEX})\]/g, (match, p1, offset, string) => { + const before = string[offset - 1]; + const after = string[offset + match.length]; + const prefix = before && before !== '.' ? '.' : ''; + const suffix = after && after !== '.' ? '.' : ''; + + return `${prefix}${index}${suffix}`; + }), + ); + + return { + ...rule, + value: newValue, + }; + } + + return rule; + }); + + return result; +}; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementProps/useUIElementProps.ts b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementProps/useUIElementProps.ts index 98d42724df..2c9d53aec6 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementProps/useUIElementProps.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementProps/useUIElementProps.ts @@ -1,26 +1,38 @@ import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; import { useRuleExecutor } from '@/components/organisms/DynamicUI/hooks/useRuleExecutor'; +import { injectIndexesAtRulesPaths } from '@/components/organisms/UIRenderer/hooks/useUIElementProps/helpers'; import { useUIElementState } from '@/components/organisms/UIRenderer/hooks/useUIElementState'; import { UIElement } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import { useMemo } from 'react'; -export const useUIElementProps = (definition: UIElement) => { +export const useUIElementProps = ( + definition: UIElement, + index: number | null = null, +) => { const { payload } = useStateManagerContext(); const { state } = useDynamicUIContext(); + const availabilityRules = useMemo(() => { + return injectIndexesAtRulesPaths(definition.availableOn || [], index); + }, [definition, index]); + + const visibilityRules = useMemo(() => { + return injectIndexesAtRulesPaths(definition.visibleOn || [], index); + }, [definition, index]); + const [availabilityTestResulsts, visibilityTestResults] = [ useRuleExecutor( payload, // @ts-ignore - definition.availableOn, + availabilityRules, definition, state, ), useRuleExecutor( payload, // @ts-ignore - definition.visibleOn, + visibilityRules, definition, state, ), diff --git a/apps/kyb-app/src/domains/collection-flow/types/index.ts b/apps/kyb-app/src/domains/collection-flow/types/index.ts index 2d9940ff74..5f50526ace 100644 --- a/apps/kyb-app/src/domains/collection-flow/types/index.ts +++ b/apps/kyb-app/src/domains/collection-flow/types/index.ts @@ -1,3 +1,4 @@ +import { ITheme } from '@/common/types/settings'; import { Action, Rule, UIElement } from '@/domains/collection-flow/types/ui-schema.types'; import { AnyObject } from '@ballerine/ui'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; @@ -148,6 +149,7 @@ export interface UISchema { config: UISchemaConfig; uiSchema: { elements: UIPage[]; + theme: ITheme; }; definition: { definitionType: string; diff --git a/apps/kyb-app/src/main.tsx b/apps/kyb-app/src/main.tsx index 3c94d508b0..79a670fec0 100644 --- a/apps/kyb-app/src/main.tsx +++ b/apps/kyb-app/src/main.tsx @@ -1,20 +1,18 @@ import '@total-typescript/ts-reset'; -import { SettingsProvider } from '@/common/providers/SettingsProvider/SettingsProvider'; import { ThemeProvider } from '@/common/providers/ThemeProvider/ThemeProvider'; import { queryClient } from '@/common/utils/query-client'; +import { initializeMonitoring } from '@/initialize-monitoring/initialize-monitoring'; +import { initializeSessionRecording } from '@/initialize-session-recording/initialize-session-recording'; import '@ballerine/ui/dist/style.css'; import { QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { HelmetProvider } from 'react-helmet-async'; -import settingsJson from '../settings.json'; import { App } from './App'; import { Head } from './Head'; import './i18next'; import './index.css'; -import { initializeMonitoring } from '@/initialize-monitoring/initialize-monitoring'; -import { initializeSessionRecording } from '@/initialize-session-recording/initialize-session-recording'; initializeMonitoring(); @@ -25,11 +23,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - + + + , diff --git a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx index 8a60222a63..3f10278d2b 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx @@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next'; import { StepperProgress } from '@/common/components/atoms/StepperProgress'; import { ProgressBar } from '@/common/components/molecules/ProgressBar'; +import { useTheme } from '@/common/providers/ThemeProvider'; import { AppShell } from '@/components/layouts/AppShell'; +import { PoweredByLogo } from '@/components/molecules/PoweredByLogo'; import { DynamicUI, State } from '@/components/organisms/DynamicUI'; import { usePageErrors } from '@/components/organisms/DynamicUI/Page/hooks/usePageErrors'; import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; @@ -89,6 +91,7 @@ export const CollectionFlow = withSessionProtected(() => { const { data: context } = useFlowContextQuery(); const { customer } = useCustomer(); const { t } = useTranslation(); + const { themeDefinition } = useTheme(); const elements = schema?.uiSchema?.elements; const definition = schema?.definition.definition; @@ -189,7 +192,7 @@ export const CollectionFlow = withSessionProtected(() => { {customer?.logoImageUri && ( setLogoLoaded(true)} @@ -209,7 +212,8 @@ export const CollectionFlow = withSessionProtected(() => { }
)} - + {/* */} +
diff --git a/apps/kyb-app/src/utils/transform-theme-to-inline-styles.ts b/apps/kyb-app/src/utils/transform-theme-to-inline-styles.ts index f5dd1a2b00..92782c3b8e 100644 --- a/apps/kyb-app/src/utils/transform-theme-to-inline-styles.ts +++ b/apps/kyb-app/src/utils/transform-theme-to-inline-styles.ts @@ -1,20 +1,28 @@ import { ITheme } from '@/common/types/settings'; -function createInlineVariable(key: string, value: string): string { +const createInlineVariable = (key: string, value: string) => { return `--${key}: ${value};`; -} +}; export const transformThemeToInlineStyles = (theme: ITheme): string => { let styles = ''; - Object.entries(theme.pallete).forEach(([variableKey, value]) => { + Object.entries(theme.palette).forEach(([variableKey, value]) => { styles += createInlineVariable(variableKey, value.color); styles += createInlineVariable(`${variableKey}-foreground`, value.foreground); }); - Object.entries(theme.elements).forEach(([variableKey, value]) => { - styles += createInlineVariable(variableKey, value); - }); + const buildInlineVariableForElements = (elements: ITheme['elements'], path?: string) => { + Object.entries(elements).forEach(([variableKey, value]) => { + if (typeof value === 'string') { + styles += createInlineVariable(`${path ? `${path}-` : ''}${variableKey}`, value); + } else { + buildInlineVariableForElements(value, `${path ? `${path}-` : ''}${variableKey}`); + } + }); + }; + + buildInlineVariableForElements(theme.elements); return styles; }; diff --git a/apps/kyb-app/tailwind.config.cjs b/apps/kyb-app/tailwind.config.cjs index 27a22a551f..50b11dd3df 100644 --- a/apps/kyb-app/tailwind.config.cjs +++ b/apps/kyb-app/tailwind.config.cjs @@ -23,28 +23,32 @@ module.exports = { background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', + DEFAULT: 'var(--primary)', + foreground: 'var(--primary-foreground)', }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', + DEFAULT: 'var(--secondary)', + foreground: 'var(--secondary-foreground)', }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', + DEFAULT: 'var(--destructive)', + foreground: 'var(--destructive-foreground)', }, success: { - DEFAULT: 'hsl(var(--success))', - foreground: 'hsl(var(--success-foreground))', + DEFAULT: 'var(--success)', + foreground: 'var(--success-foreground)', }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', + DEFAULT: 'var(--muted)', + foreground: 'var(--muted-foreground)', }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', + DEFAULT: 'var(--accent)', + foreground: 'var(--accent-foreground)', + }, + controls: { + DEFAULT: 'var(--controls)', + foreground: 'var(--controls-foreground)', }, }, fontFamily: { diff --git a/apps/kyb-app/theme.json b/apps/kyb-app/theme.json new file mode 100644 index 0000000000..5f9d2b036e --- /dev/null +++ b/apps/kyb-app/theme.json @@ -0,0 +1,64 @@ +{ + "theme": { + "palette": { + "primary": { + "color": "hsl(0, 0%, 100%)", + "foreground": "hsl(0, 0%, 0%)" + }, + "secondary": { + "color": "hsl(226, 100%, 97%)", + "foreground": "hsl(0, 0%, 0%)" + }, + "controls": { + "color": "hsl(0, 0%, 0%)", + "foreground": "hsl(0, 0%, 100%)" + }, + "stepper": { + "breadcrumbs": {} + } + }, + "elements": { + "ring": "215 20.2% 65.1%", + "border": "214.3 31.8% 91.4%", + "input": "214.3 31.8% 91.4%", + "radius": "0.5rem", + "stepper": { + "breadcrumbs": { + "idle": { + "inner": {}, + "outer": { + "border-color": "transparent", + "active-border-color": "#007AFF33" + }, + "wrapper": { + "border-color": "rgb(226, 232, 240)", + "active-border-color": "#007AFF" + }, + "label": {} + }, + "warning": { + "inner": { + "border-color": "#FFB35A" + }, + "outer": { + "border-color": "transparent", + "active-border-color": "#FF8A0055" + } + }, + "completed": { + "inner": { + "border-color": "#00BD59" + }, + "outer": { + "border-color": "transparent", + "active-border-color": "#00BD5933" + }, + "wrapper": { + "active-border-color": "#20B064" + } + } + } + } + } + } +} diff --git a/services/workflows-service/prisma/migrations/20241001141655_add_theme_to_ui_definition/migration.sql b/services/workflows-service/prisma/migrations/20241001141655_add_theme_to_ui_definition/migration.sql new file mode 100644 index 0000000000..b1014a8938 --- /dev/null +++ b/services/workflows-service/prisma/migrations/20241001141655_add_theme_to_ui_definition/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "UiDefinition" ADD COLUMN "theme" JSONB; diff --git a/services/workflows-service/prisma/schema.prisma b/services/workflows-service/prisma/schema.prisma index e61505067e..3808c42c08 100644 --- a/services/workflows-service/prisma/schema.prisma +++ b/services/workflows-service/prisma/schema.prisma @@ -419,7 +419,7 @@ enum UiDefinitionContext { model UiDefinition { id String @id @default(cuid()) - crossEnvKey String? @unique + crossEnvKey String? @unique workflowDefinitionId String uiContext UiDefinitionContext @@ -431,6 +431,7 @@ model UiDefinition { definition Json? // Frontend UI xstate definition uiSchema Json // JSON schmea of how to render UI + theme Json? // Theme for the UI schemaOptions Json? // Document Schemas, rejectionReasons: {}, documenTypes: {}, documenCateogries: {} uiOptions Json? // how the view will look, overall locales Json? // Locales for the UI diff --git a/services/workflows-service/src/collection-flow/collection-flow.service.ts b/services/workflows-service/src/collection-flow/collection-flow.service.ts index e27e365314..7e62f39a07 100644 --- a/services/workflows-service/src/collection-flow/collection-flow.service.ts +++ b/services/workflows-service/src/collection-flow/collection-flow.service.ts @@ -87,7 +87,7 @@ export class CollectionFlowService { projectIds, ); - const uiDefintion = await this.uiDefinitionService.getByWorkflowDefinitionId( + const uiDefinition = await this.uiDefinitionService.getByWorkflowDefinitionId( workflowDefinition.id, 'collection_flow' as const, projectIds, @@ -95,7 +95,7 @@ export class CollectionFlowService { ); const translationService = new TranslationService( - this.getTranslationServiceResources(uiDefintion), + this.getTranslationServiceResources(uiDefinition), ); await translationService.init(); @@ -103,19 +103,20 @@ export class CollectionFlowService { return { id: workflowDefinition.id, config: workflowDefinition.config, - uiOptions: uiDefintion.uiOptions, + uiOptions: uiDefinition.uiOptions, uiSchema: { // @ts-expect-error - error from Prisma types fix elements: this.traverseUiSchema( // @ts-expect-error - error from Prisma types fix - uiDefintion.uiSchema.elements, + uiDefinition.uiSchema.elements, context, language, translationService, ) as UiSchemaStep[], + theme: uiDefinition.theme, }, - definition: uiDefintion.definition - ? (uiDefintion.definition as unknown as UiDefDefinition) + definition: uiDefinition.definition + ? (uiDefinition.definition as unknown as UiDefDefinition) : undefined, }; } diff --git a/websites/docs/src/content/docs/en/collection-flow/theming.mdx b/websites/docs/src/content/docs/en/collection-flow/theming.mdx index 7e11ff97ad..4335ca6dd0 100644 --- a/websites/docs/src/content/docs/en/collection-flow/theming.mdx +++ b/websites/docs/src/content/docs/en/collection-flow/theming.mdx @@ -39,7 +39,7 @@ Will be used in case if UIDefinition doesnt have theme. ```json { - "pallete": { + "palette": { "primary": { "color": "0, 0%, 100%", "foreground": "0, 0%, 0%" @@ -61,7 +61,7 @@ Will be used in case if UIDefinition doesnt have theme. ```json { - "pallete": { + "palette": { "primary": { "color": "0, 0%, 0%", "foreground": "0, 0%, 100%" @@ -86,7 +86,7 @@ Will be used in case if UIDefinition doesnt have theme. ```json { - "pallete": { + "palette": { "primary": { "color": "0, 0%, 100%", "foreground": "93, 100%, 36%"