diff --git a/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx b/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx index c815881bb2..dae9eafdaf 100644 --- a/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx +++ b/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx @@ -23,7 +23,7 @@ export const DateRangePicker = ({ onChange, value, className }: TDateRangePicker 'text-muted-foreground': !value, })} > - + {value?.from && value?.to && ( <> {formatDate(value.from, 'LLL dd, y')} - {formatDate(value.to, 'LLL dd, y')} diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Actions.tsx b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Actions.tsx index 6e3a7708e0..9fa249d16b 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Actions.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/Case.Actions.tsx @@ -53,7 +53,7 @@ export const Actions: FunctionComponent = ({ /> -
+

{ loadingPlaceholder={} fallback={CustomerProviderFallback} > - + + + ); + + // return ; }; (window as any).toggleDevmode = () => { diff --git a/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx b/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx index 42ddce49b3..415b031739 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx @@ -10,7 +10,13 @@ export const FormContainer = ({ children, header }: Props) => {
{header ?
{header}
: null} -
{children}
+
+ {children} +
); diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/Page/Page.tsx b/apps/kyb-app/src/components/organisms/DynamicUI/Page/Page.tsx index 75bbd3fa94..89db6e08bd 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/Page/Page.tsx +++ b/apps/kyb-app/src/components/organisms/DynamicUI/Page/Page.tsx @@ -5,7 +5,7 @@ import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateMa import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; import { useRuleExecutor } from '@/components/organisms/DynamicUI/hooks/useRuleExecutor'; import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines'; -import { UIElement, UIPage } from '@/domains/collection-flow'; +import { UIElementDefinition, UIPage } from '@/domains/collection-flow'; import { AnyChildren, AnyObject } from '@ballerine/ui'; import { useMemo } from 'react'; import { pageContext } from './page.context'; @@ -20,7 +20,7 @@ export interface PageProps { export const Page = ({ page, children }: PageProps) => { const { pages } = usePageResolverContext(); const definition = useMemo(() => { - const definition: UIElement = { + const definition: UIElementDefinition = { type: 'page', name: page.name, options: {}, diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts index 8c056a9c16..c31275c557 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts @@ -1,6 +1,6 @@ import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines'; import { findDocumentDefinitionById } from '@/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName'; -import { Document, UIElement, UIPage } from '@/domains/collection-flow'; +import { Document, UIElementDefinition, UIPage } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import { useMemo } from 'react'; @@ -9,7 +9,7 @@ export interface PageError { pageName: string; stateName: string; errors: ErrorField[]; - _elements: UIElement[]; + _elements: UIElementDefinition[]; } export const selectDirectors = (context: AnyObject) => diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/helpers/getDispatchableActions.ts b/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/helpers/getDispatchableActions.ts index 533404dc6f..ff2fb1305e 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/helpers/getDispatchableActions.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/helpers/getDispatchableActions.ts @@ -4,14 +4,15 @@ import { DocumentsRuleEngine } from '@/components/organisms/DynamicUI/rule-engin import { JmespathRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/jmespath.rule-engine'; import { JsonLogicRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/json-logic.rule-engine'; import { JsonSchemaRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine'; -import { Action, UIElement } from '@/domains/collection-flow'; +import { Action, UIElementDefinition, UIPage } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; export const getDispatchableActions = ( context: AnyObject, actions: Action[], - definition: UIElement, + definition: UIElementDefinition, state: UIState, + currentPage: UIPage, ) => { return actions.filter(action => { const engineManager = new EngineManager([ @@ -20,6 +21,7 @@ export const getDispatchableActions = ( new JsonSchemaRuleEngine(), new DocumentsRuleEngine(), new JmespathRuleEngine(), + // new IsStepValidRuleEngine(), ]); if (!action.dispatchOn.rules) return true; @@ -33,7 +35,7 @@ export const getDispatchableActions = ( // @ts-ignore rule?.type, ) - ?.validate(context, rule, definition, state).isValid, + ?.validate(context, rule, definition, state, currentPage).isValid, ) ); }); diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/useEventEmitter.ts b/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/useEventEmitter.ts index e8f26b52d5..3527910269 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/useEventEmitter.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/useEventEmitter.ts @@ -1,3 +1,4 @@ +import { usePageResolverContext } from '@/components/organisms/DynamicUI/PageResolver/hooks/usePageResolverContext'; import { useActionsHandlerContext } from '@/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useActionsHandlerContext'; import { getDispatchableActions } from '@/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/helpers/getDispatchableActions'; import { getTriggeredActions } from '@/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/helpers/getTriggeredActions'; @@ -5,15 +6,16 @@ import { useActionDispatcher } from '@/components/organisms/DynamicUI/StateManag import { UIEventType } from '@/components/organisms/DynamicUI/StateManager/components/ActionsHandler/hooks/useEventEmitterLogic/types'; import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import { useCallback } from 'react'; -export const useEventEmitterLogic = (elementDefinition: UIElement) => { +export const useEventEmitterLogic = (elementDefinition: UIElementDefinition) => { const { actions, dispatchAction } = useActionsHandlerContext(); const { stateApi } = useStateManagerContext(); const { state } = useDynamicUIContext(); const { getDispatch } = useActionDispatcher(actions, dispatchAction); + const { currentPage } = usePageResolverContext(); const emitEvent = useCallback( (type: UIEventType) => { @@ -33,6 +35,7 @@ export const useEventEmitterLogic = (elementDefinition: UIElement) => triggeredActions, elementDefinition, state, + currentPage!, ); console.info(`Dispatchable actions`, { @@ -52,7 +55,7 @@ export const useEventEmitterLogic = (elementDefinition: UIElement) => dispatch(action); }); }, - [elementDefinition, actions, stateApi, state, getDispatch], + [elementDefinition, actions, stateApi, state, currentPage, getDispatch], ); return emitEvent; diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic/useMachineLogic.ts b/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic/useMachineLogic.ts index 2594fb8c5c..5cc5139dc8 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic/useMachineLogic.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/StateManager/hooks/useMachineLogic/useMachineLogic.ts @@ -1,7 +1,7 @@ +import { isErrorWithMessage } from '@ballerine/common'; import { AnyObject } from '@ballerine/ui'; import { WorkflowBrowserSDK } from '@ballerine/workflow-browser-sdk'; import { useCallback, useMemo, useState } from 'react'; -import { isErrorWithMessage } from '@ballerine/common'; export interface StateMachineAPI { invokePlugin: (pluginName: string) => Promise; @@ -22,7 +22,7 @@ export const useMachineLogic = ( try { await machine.invokePlugin(pluginName); } catch (error) { - console.log('Failed to invoke plugin', isErrorWithMessage(error) ? error.message : error); + console.info('Failed to invoke plugin', isErrorWithMessage(error) ? error.message : error); } finally { setInvokingPlugin(false); } diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useRuleExecutor/useRuleExecutor.ts b/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useRuleExecutor/useRuleExecutor.ts index e4f4a46cb1..0aaab83917 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useRuleExecutor/useRuleExecutor.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useRuleExecutor/useRuleExecutor.ts @@ -1,20 +1,19 @@ -import { EngineManager } from '@/components/organisms/DynamicUI/StateManager/components/ActionsHandler/helpers/engine-manager'; +import { usePageResolverContext } from '@/components/organisms/DynamicUI/PageResolver/hooks/usePageResolverContext'; import { UIState } from '@/components/organisms/DynamicUI/hooks/useUIStateLogic/types'; -import { JsonLogicRuleEngine, RuleTestResult } from '@/components/organisms/DynamicUI/rule-engines'; -import { DocumentsRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/documents.rule-engine'; -import { JmespathRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/jmespath.rule-engine'; -import { JsonSchemaRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine'; -import { Rule, UIElement } from '@/domains/collection-flow'; +import { RuleTestResult } from '@/components/organisms/DynamicUI/rule-engines'; +import { executeRule } from '@/components/organisms/DynamicUI/rule-engines/utils/execute-rules'; +import { Rule, UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import { useEffect, useMemo, useRef, useState } from 'react'; export const useRuleExecutor = ( context: AnyObject, rules: Rule[], - definition: UIElement, + definition: UIElementDefinition, uiState: UIState, ) => { const uiStateRef = useRef(uiState); + const { currentPage } = usePageResolverContext(); useEffect(() => { uiStateRef.current = uiState; @@ -22,47 +21,38 @@ export const useRuleExecutor = ( const [executionResult, setExecutionResult] = useState([]); - const rulesManager = useMemo( - () => - new EngineManager([ - new JsonLogicRuleEngine(), - // @ts-ignore - new JsonSchemaRuleEngine(), - new JmespathRuleEngine(), - new DocumentsRuleEngine(), - ]), - [], - ); - const executeRules = useMemo( () => - (context: AnyObject, rules: Rule[], definition: UIElement, uiState: UIState) => { + ( + context: AnyObject, + rules: Rule[], + definition: UIElementDefinition, + uiState: UIState, + ) => { const executionResult = rules?.map(rule => { - const engine = rulesManager.getEngine(rule.type); - const ctx = { ...context }; //This hack is neeeded to filter out `empty` //TO DO: Find solution on how to define array items in schemas // ctx.documents = ctx?.documents.filter(Boolean); - return engine?.validate(ctx, rule, definition, uiState); + return executeRule(ctx, rule, definition, uiState, currentPage!); }) || []; // @ts-ignore setExecutionResult(executionResult); }, - [rulesManager], + [currentPage], ); useEffect(() => { executeRules(context, rules, definition, uiStateRef.current); - }, [context, rules, uiStateRef, definition, executeRules]); + }, [context, rules, uiStateRef, definition, currentPage, executeRules]); if (import.meta.env.MODE === 'development') { if (executionResult.length && executionResult.every(r => !r.isValid && r.errors?.length)) { - console.log('Rules execution result', executionResult); + console.info('Rules execution result', executionResult); } } diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useUIStateLogic/hooks/useUIElementsStateLogic/hooks/useUIElementToolsLogic/useUIElementToolsLogic.ts b/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useUIStateLogic/hooks/useUIElementsStateLogic/hooks/useUIElementToolsLogic/useUIElementToolsLogic.ts index e9c76b784e..75daa1102c 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useUIStateLogic/hooks/useUIElementsStateLogic/hooks/useUIElementToolsLogic/useUIElementToolsLogic.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/hooks/useUIStateLogic/hooks/useUIElementsStateLogic/hooks/useUIElementToolsLogic/useUIElementToolsLogic.ts @@ -27,7 +27,7 @@ export const useUIElementToolsLogic = (elementId: string) => { ); const elementState = useMemo( - () => state.elements[elementId] || null, + () => state?.elements?.[elementId] || null, [state.elements, elementId], ); diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/documents.rule-engine.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/documents.rule-engine.ts index 3e70945a86..6018189904 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/documents.rule-engine.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/documents.rule-engine.ts @@ -6,7 +6,13 @@ import { ErrorField, RuleEngine, } from '@/components/organisms/DynamicUI/rule-engines/rule-engine.abstract'; -import { Document, DocumentsValidatorRule, Rule, UIElement } from '@/domains/collection-flow'; +import { + Document, + DocumentsValidatorRule, + Rule, + UIElementDefinition, + UIPage, +} from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import get from 'lodash/get'; @@ -14,7 +20,13 @@ export class DocumentsRuleEngine implements RuleEngine { public readonly ENGINE_NAME = 'destination-engine'; private ruleManager = new EngineManager([new JmespathRuleEngine(), new JsonLogicRuleEngine()]); - validate(context: AnyObject, rule: unknown, definition: UIElement, state: UIState) { + validate( + context: AnyObject, + rule: unknown, + definition: UIElementDefinition, + state: UIState, + page: UIPage, + ) { if (this.isDestinationValidatorRule(rule)) { const errors: ErrorField[] = []; @@ -22,7 +34,7 @@ export class DocumentsRuleEngine implements RuleEngine { const isRequired = this.isRule(params.required) ? this.ruleManager ?.getEngine(params.required.type) - ?.validate(context, params.required, definition, state).isValid + ?.validate(context, params.required, definition, state, page).isValid : params.required; const document = ((context.documents || []) as Document[]).find( diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/is-step-valid.rule-engine.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/is-step-valid.rule-engine.ts new file mode 100644 index 0000000000..74372d3e1d --- /dev/null +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/is-step-valid.rule-engine.ts @@ -0,0 +1,50 @@ +import { UIState } from '@/components/organisms/DynamicUI/hooks/useUIStateLogic/types'; +import { + RuleEngine, + RuleTestResult, +} from '@/components/organisms/DynamicUI/rule-engines/rule-engine.abstract'; +import { validate } from '@/components/providers/Validator/hooks/useValidate'; +import { UIElementDefinition, UIPage, ValidContextRule } from '@/domains/collection-flow'; +import { transformV1UIElementsToV2UIElements } from '@/pages/CollectionFlowV2/helpers'; +import { AnyObject } from '@ballerine/ui'; +import jsonLogic from 'json-logic-js'; + +export class IsStepValidRuleEngine implements RuleEngine { + public readonly ENGINE_NAME = 'is-step-valid'; + + validate( + context: unknown, + _: unknown, + element: UIElementDefinition, + __: UIState, + uiPage: UIPage, + ): RuleTestResult { + console.info(`Executing rule engine ${this.ENGINE_NAME}`); + + const uiEelemntsV2 = transformV1UIElementsToV2UIElements(uiPage?.elements || []); + const validationErrors = validate(uiEelemntsV2, context as AnyObject); + + const result = { isValid: !validationErrors.length, errors: [] }; + + console.info(`Result of rule engine ${this.ENGINE_NAME}:`, { + isValid: !validationErrors.length, + errors: validationErrors, + }); + + return result; + } + + test(context: unknown, rule: unknown) { + if (this.isValidContextRule(rule)) { + return jsonLogic.apply(rule.value, context as AnyObject) as boolean; + } + + throw new Error(`Invalid rule provided to ${this.ENGINE_NAME}`); + } + + private isValidContextRule(rule: unknown): rule is ValidContextRule { + return ( + typeof rule === 'object' && rule !== null && 'type' in rule && rule.type === 'json-logic' + ); + } +} diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/jmespath.rule-engine.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/jmespath.rule-engine.ts index 9162d9fd82..298fdf3e54 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/jmespath.rule-engine.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/jmespath.rule-engine.ts @@ -3,7 +3,7 @@ import { RuleEngine, RuleTestResult, } from '@/components/organisms/DynamicUI/rule-engines/rule-engine.abstract'; -import { JMESPathRule, Rule, UIElement } from '@/domains/collection-flow'; +import { JMESPathRule, Rule, UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import jmespath from 'jmespath'; @@ -13,7 +13,7 @@ export class JmespathRuleEngine implements RuleEngine { validate( context: unknown, rule: Rule, - _: UIElement, + _: UIElementDefinition, uiState: UIState, ): RuleTestResult { const result = this.test({ ...(context as AnyObject), uiState }, rule); diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-logic.rule-engine.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-logic.rule-engine.ts index 8daf036774..7ff14fe4d7 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-logic.rule-engine.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-logic.rule-engine.ts @@ -3,7 +3,7 @@ import { RuleEngine, RuleTestResult, } from '@/components/organisms/DynamicUI/rule-engines/rule-engine.abstract'; -import { JSONLogicRule, UIElement } from '@/domains/collection-flow'; +import { JSONLogicRule, UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import jsonLogic from 'json-logic-js'; @@ -13,7 +13,7 @@ export class JsonLogicRuleEngine implements RuleEngine { validate( context: unknown, rule: unknown, - _: UIElement, + _: UIElementDefinition, uiState: UIState, ): RuleTestResult { const result = this.test( diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.ts index 0666931bde..b7375fa6fd 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.ts @@ -2,7 +2,7 @@ import { ErrorField, RuleEngine, } from '@/components/organisms/DynamicUI/rule-engines/rule-engine.abstract'; -import { Rule, UIElement } from '@/domains/collection-flow'; +import { Rule, UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import ajvErrors from 'ajv-errors'; import addFormats, { FormatName } from 'ajv-formats'; @@ -16,7 +16,7 @@ export class JsonSchemaRuleEngine implements RuleEngine { public readonly ENGINE_NAME = JsonSchemaRuleEngine.ENGINE_NAME; // @ts-ignore - validate(context: unknown, rule: Rule, definition: UIElement) { + validate(context: unknown, rule: Rule, definition: UIElementDefinition) { const validator = new Ajv({ allErrors: true, useDefaults: true }); addFormats(validator, { formats: JsonSchemaRuleEngine.ALLOWED_FORMATS, @@ -48,7 +48,7 @@ export class JsonSchemaRuleEngine implements RuleEngine { return validationResult; } - private extractErrorsWithFields(validator: Ajv, definition: UIElement) { + private extractErrorsWithFields(validator: Ajv, definition: UIElementDefinition) { const result = validator.errors?.map(error => { const erroredParams = Object.values(error.params) as string[]; const uniqueErroredParams = Array.from(new Set(erroredParams)); diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.unit.test.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.unit.test.ts index 24aa991d64..2bbfdc3508 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.unit.test.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine.unit.test.ts @@ -1,6 +1,6 @@ import { JsonSchemaRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine'; import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines/rule-engine.abstract'; -import { Rule, UIElement } from '@/domains/collection-flow'; +import { Rule, UIElementDefinition } from '@/domains/collection-flow'; describe('JsonSchemaRuleEngine', () => { let ruleEngine: JsonSchemaRuleEngine; @@ -66,7 +66,7 @@ describe('JsonSchemaRuleEngine', () => { const testResult = ruleEngine.validate(testData, rule, { valueDestination: 'foo', - } as UIElement); + } as UIElementDefinition); const expectedError: ErrorField = { type: 'error', @@ -118,7 +118,7 @@ describe('JsonSchemaRuleEngine', () => { bar: null, }; - const testResult = ruleEngine.validate(testData, rule, {} as UIElement); + const testResult = ruleEngine.validate(testData, rule, {} as UIElementDefinition); expect(testResult.isValid).toBe(false); expect(testResult.errors?.length).toBe(2); @@ -168,7 +168,7 @@ describe('JsonSchemaRuleEngine', () => { message: expect.any(String) as string, }; - const testResult = ruleEngine.validate(testData, rule, {} as UIElement); + const testResult = ruleEngine.validate(testData, rule, {} as UIElementDefinition); expect(testResult.errors?.length).toBe(1); expect(testResult.errors).toContainEqual(testError); @@ -202,7 +202,7 @@ describe('JsonSchemaRuleEngine', () => { }, }; - const testResult = ruleEngine.validate(testData, rule, {} as UIElement); + const testResult = ruleEngine.validate(testData, rule, {} as UIElementDefinition); expect(testResult.isValid).toBe(false); expect(testResult.errors?.length).toBe(1); @@ -246,7 +246,7 @@ describe('JsonSchemaRuleEngine', () => { }; const expectedFileId = 'fooList[0].foo'; - const testResult = ruleEngine.validate(testData, rule, {} as UIElement); + const testResult = ruleEngine.validate(testData, rule, {} as UIElementDefinition); expect(testResult.isValid).toBe(false); expect(testResult.errors?.length).toBe(1); @@ -281,7 +281,7 @@ describe('JsonSchemaRuleEngine', () => { const testData = {}; - const testResult = ruleEngine.validate(testData, rule, {} as UIElement); + const testResult = ruleEngine.validate(testData, rule, {} as UIElementDefinition); expect(testResult.isValid).toBe(false); expect(testResult.errors?.length).toBe(1); diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/rule-engine.abstract.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/rule-engine.abstract.ts index b5e464c9b9..2a9b0085f1 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/rule-engine.abstract.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/rule-engine.abstract.ts @@ -1,5 +1,5 @@ import { UIState } from '@/components/organisms/DynamicUI/hooks/useUIStateLogic/types'; -import { Rule, UIElement } from '@/domains/collection-flow'; +import { Rule, UIElementDefinition, UIPage } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; export type ErrorField = { @@ -21,8 +21,9 @@ export abstract class RuleEngine { abstract validate( context: unknown, rule: Rule, - definition: UIElement, + definition: UIElementDefinition, uiState: UIState, + uiPage: UIPage, ): RuleTestResult; abstract test(context: unknown, rule: Rule): boolean; diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/utils/execute-rules.ts b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/utils/execute-rules.ts index 401c8c30ed..2942974668 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/utils/execute-rules.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/rule-engines/utils/execute-rules.ts @@ -1,4 +1,5 @@ import { DocumentsRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/documents.rule-engine'; +import { IsStepValidRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/is-step-valid.rule-engine'; import { JmespathRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/jmespath.rule-engine'; import { JsonLogicRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/json-logic.rule-engine'; import { JsonSchemaRuleEngine } from '@/components/organisms/DynamicUI/rule-engines/json-schema.rule-engine'; @@ -11,6 +12,7 @@ const rulesManages = new EngineManager([ // @ts-ignore new JsonSchemaRuleEngine(), new JmespathRuleEngine(), + new IsStepValidRuleEngine(), new DocumentsRuleEngine(), ]); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/UIRenderer.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/UIRenderer.tsx index 87e266aaa0..961a7998cb 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/UIRenderer.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/UIRenderer.tsx @@ -1,14 +1,14 @@ import { baseElements } from '@/components/organisms/UIRenderer/base-elements'; import { ElementsMap } from '@/components/organisms/UIRenderer/types/elements.types'; import { generateBlocks } from '@/components/organisms/UIRenderer/utils/generateBlocks'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { BlocksComponent } from '@ballerine/blocks'; import { AnyObject } from '@ballerine/ui'; import { ComponentProps, FunctionComponent, useMemo } from 'react'; import { UiRendererContext } from './ui-renderer.context'; export interface UIRendererProps { - schema: UIElement[]; + schema: UIElementDefinition[]; elements?: ElementsMap; } diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/JSONForm.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/JSONForm.tsx index f3ab0e9638..2ac597fd23 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/JSONForm.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/JSONForm.tsx @@ -96,8 +96,11 @@ export const JSONForm: UIElementComponent = ({ defini
{ const { language } = useLanguageParam(); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/DocumentField.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/DocumentField.tsx index 8fd4a7b235..b5f5a7ad6c 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/DocumentField.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/DocumentField.tsx @@ -10,7 +10,7 @@ import { useFileRepository } from '@/components/organisms/UIRenderer/elements/JS import { UploadFileFn } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField/hooks/useFileUploading/types'; import { useUIElementErrors } from '@/components/organisms/UIRenderer/hooks/useUIElementErrors/useUIElementErrors'; import { useUIElementState } from '@/components/organisms/UIRenderer/hooks/useUIElementState'; -import { Document, UIElement } from '@/domains/collection-flow'; +import { Document, UIElementDefinition } from '@/domains/collection-flow'; import { fetchFile, uploadFile } from '@/domains/storage/storage.api'; import { collectionFlowFileStorage } from '@/pages/CollectionFlow/collection-flow.file-storage'; import { findDocumentSchemaByTypeAndCategory } from '@ballerine/common'; @@ -25,7 +25,7 @@ export interface DocumentFieldParams { } export const DocumentField = ( - props: RJSFInputProps & { definition: UIElement } & { + props: RJSFInputProps & { definition: UIElementDefinition } & { inputIndex: number | null; }, ) => { diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/FieldTemplate/FieldTemplate.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/FieldTemplate/FieldTemplate.tsx index b97d0d1e7c..f8ff9b9526 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/FieldTemplate/FieldTemplate.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/FieldTemplate/FieldTemplate.tsx @@ -9,7 +9,7 @@ import { findDefinitionByName } from '@/components/organisms/UIRenderer/elements import { getInputIndex } from '@/components/organisms/UIRenderer/elements/JSONForm/hocs/withDynamicUIInput'; import { useJSONFormDefinition } from '@/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/useJSONFormDefinition'; import { useUIElementProps } from '@/components/organisms/UIRenderer/hooks/useUIElementProps'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject, FieldLayout } from '@ballerine/ui'; export const FieldTemplate = (props: FieldTemplateProps) => { @@ -29,7 +29,7 @@ export const FieldTemplate = (props: FieldTemplateProps) => { const fieldDefinition = useMemo( () => findDefinitionByName(props.id.replace(/root_\d*_?/, ''), definition.elements || []) || - ({} as UIElement), + ({} as UIElementDefinition), [props.id, definition.elements], ); @@ -52,6 +52,7 @@ export const FieldTemplate = (props: FieldTemplateProps) => { return (
+ {/* @ts-ignore */}
); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/Multiselect/Multiselect.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/Multiselect/Multiselect.tsx index 9db1612738..9fdfa02314 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/Multiselect/Multiselect.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/Multiselect/Multiselect.tsx @@ -1,4 +1,4 @@ -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { Chip, MultiselectInputAdapter, @@ -20,7 +20,7 @@ export interface MultiselectParams { export const Multiselect = ({ definition, ...adapterProps -}: RJSFInputProps & { definition?: UIElement }) => { +}: RJSFInputProps & { definition?: UIElementDefinition }) => { const renderSelected: MultiSelectSelectedItemRenderer = useCallback( (params, option) => { return ( diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/RelationshipDropdown/RelationshipDropdown.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/RelationshipDropdown/RelationshipDropdown.tsx index 96bb089e5a..703089e6ec 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/RelationshipDropdown/RelationshipDropdown.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/RelationshipDropdown/RelationshipDropdown.tsx @@ -1,10 +1,10 @@ import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; import { relationshipOptions } from '@/components/organisms/UIRenderer/elements/JSONForm/components/RelationshipDropdown/relationship-options'; -import { UIElement } from '@/domains/collection-flow'; +import { UISchema } from '@/components/organisms/UIRenderer/types/ui-schema.types'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AutocompleteTextInputAdapter, RJSFInputProps } from '@ballerine/ui'; -import { useMemo } from 'react'; import get from 'lodash/get'; -import { UISchema } from '@/components/organisms/UIRenderer/types/ui-schema.types'; +import { useMemo } from 'react'; export interface RelationshipDropdownParams { companyNameDestination: string; @@ -13,7 +13,9 @@ export interface RelationshipDropdownParams { const COMPANY_NAME_PLACEHOLDER = '{company_name}'; export const RelationshipDropdown = ( - props: RJSFInputProps & { definition: UIElement } & { + props: RJSFInputProps & { + definition: UIElementDefinition; + } & { inputIndex: number | null; }, ) => { diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/StatePicker/StatePicker.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/StatePicker/StatePicker.tsx index 10977bbb3e..9a12af6671 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/StatePicker/StatePicker.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/components/StatePicker/StatePicker.tsx @@ -1,16 +1,16 @@ import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { getCountryStates } from '@/helpers/countries-data'; import { RJSFInputProps, TextInputAdapter } from '@ballerine/ui'; -import { useMemo } from 'react'; import get from 'lodash/get'; +import { useMemo } from 'react'; export interface StatePickerParams { countryCodePath: string; } export const StatePicker = ( - props: RJSFInputProps & { definition: UIElement }, + props: RJSFInputProps & { definition: UIElementDefinition }, ) => { const { schema: baseSchema, definition } = props; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createFormSchemaFromUIElements.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createFormSchemaFromUIElements.ts index a37476776c..65f6b2cde6 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createFormSchemaFromUIElements.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createFormSchemaFromUIElements.ts @@ -1,10 +1,10 @@ import { JSONFormElementBaseParams } from '@/components/organisms/UIRenderer/elements/JSONForm/JSONForm'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; export const createFormSchemaFromUIElements = ( - formElement: UIElement, + formElement: UIElementDefinition, ) => { const formSchema: RJSFSchema = { type: formElement.options?.jsonFormDefinition?.type === 'array' ? 'array' : 'object', @@ -21,30 +21,32 @@ export const createFormSchemaFromUIElements = ( if (formSchema.type === 'object') { formSchema.properties = {}; - (formElement.elements as Array>)?.forEach(uiElement => { - if (!uiElement.options?.jsonFormDefinition) return; - - const elementDefinition = { - ...uiElement.options.jsonFormDefinition, - title: uiElement.options.label, - description: uiElement.options.description, - }; - - if (!formSchema.properties) { - formSchema.properties = {}; - } - - formSchema.properties[uiElement.name] = elementDefinition; - - uiSchema[uiElement.name] = { - ...uiElement?.options?.uiSchema, - 'ui:label': - (uiElement.options?.uiSchema || {})['ui:label'] === undefined - ? Boolean(uiElement?.options?.label) - : (uiElement.options?.uiSchema || {})['ui:label'], - 'ui:placeholder': uiElement?.options?.hint, - }; - }); + (formElement.elements as Array>)?.forEach( + uiElement => { + if (!uiElement.options?.jsonFormDefinition) return; + + const elementDefinition = { + ...uiElement.options.jsonFormDefinition, + title: uiElement.options.label, + description: uiElement.options.description, + }; + + if (!formSchema.properties) { + formSchema.properties = {}; + } + + formSchema.properties[uiElement.name] = elementDefinition; + + uiSchema[uiElement.name] = { + ...uiElement?.options?.uiSchema, + 'ui:label': + (uiElement.options?.uiSchema || {})['ui:label'] === undefined + ? Boolean(uiElement?.options?.label) + : (uiElement.options?.uiSchema || {})['ui:label'], + 'ui:placeholder': uiElement?.options?.hint, + }; + }, + ); } if (formSchema.type === 'array') { @@ -61,28 +63,30 @@ export const createFormSchemaFromUIElements = ( 'ui:label': false, } as AnyObject; - (formElement.elements as Array>)?.forEach(uiElement => { - if (!uiElement.options?.jsonFormDefinition) return; - - const elementDefinition = { - ...uiElement.options.jsonFormDefinition, - title: uiElement.options.label, - description: uiElement.options.description, - }; - - if (!(formSchema.items as RJSFSchema)?.properties) { - (formSchema.items as RJSFSchema).properties = {}; - } - - // @ts-ignore - (formSchema.items as RJSFSchema).properties[uiElement.name] = elementDefinition; - - uiSchema.items[uiElement.name] = { - ...uiElement?.options?.uiSchema, - 'ui:label': Boolean(uiElement?.options?.label), - 'ui:placeholder': uiElement?.options?.hint, - }; - }); + (formElement.elements as Array>)?.forEach( + uiElement => { + if (!uiElement.options?.jsonFormDefinition) return; + + const elementDefinition = { + ...uiElement.options.jsonFormDefinition, + title: uiElement.options.label, + description: uiElement.options.description, + }; + + if (!(formSchema.items as RJSFSchema)?.properties) { + (formSchema.items as RJSFSchema).properties = {}; + } + + // @ts-ignore + (formSchema.items as RJSFSchema).properties[uiElement.name] = elementDefinition; + + uiSchema.items[uiElement.name] = { + ...uiElement?.options?.uiSchema, + 'ui:label': Boolean(uiElement?.options?.label), + 'ui:placeholder': uiElement?.options?.hint, + }; + }, + ); } return { diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createInitialFormData.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createInitialFormData.ts index 525fcc23e1..5e27c1b162 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createInitialFormData.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/createInitialFormData.ts @@ -1,10 +1,10 @@ import { JSONFormElementBaseParams } from '@/components/organisms/UIRenderer/elements/JSONForm/JSONForm'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import get from 'lodash/get'; export const createInitialFormData = ( - definition: UIElement, + definition: UIElementDefinition, context: AnyObject, ) => { let formData: AnyObject | AnyObject[] = {}; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts index 66fb3bdc57..781e2a38fd 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName.ts @@ -1,11 +1,11 @@ import { deserializeDocumentId } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField/helpers/serialize-document-id'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; export const findDefinitionByName = ( name: string, - elements: Array>, -): UIElement | undefined => { + elements: Array>, +): UIElementDefinition | undefined => { for (const element of elements) { if (element.name === name) { return element; @@ -25,8 +25,8 @@ export const findDefinitionByName = ( export const findDefinitionByDestinationPath = ( destination: string, - elements: Array>, -): UIElement | undefined => { + elements: Array>, +): UIElementDefinition | undefined => { for (const element of elements) { if (element.valueDestination === destination) { return element; @@ -46,8 +46,8 @@ export const findDefinitionByDestinationPath = ( export const findDocumentDefinitionById = ( id: string, - elements: Array>, -): UIElement | undefined => { + elements: Array>, +): UIElementDefinition | undefined => { for (const element of elements) { if ((element?.options?.documentData?.id as string) === deserializeDocumentId(id)) { return element; @@ -65,10 +65,10 @@ export const findDocumentDefinitionById = ( return undefined; }; -export const getAllDefinitions = (elements: Array>) => { - const items: Array> = []; +export const getAllDefinitions = (elements: Array>) => { + const items: Array> = []; - const run = (elements: Array>) => { + const run = (elements: Array>) => { for (const element of elements) { if (element.valueDestination) { items.push(element); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/hocs/withDynamicUIInput/withDynamicUIInput.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/hocs/withDynamicUIInput/withDynamicUIInput.tsx index c7a70b0ec8..54c6cfb7f2 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/hocs/withDynamicUIInput/withDynamicUIInput.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/hocs/withDynamicUIInput/withDynamicUIInput.tsx @@ -7,7 +7,7 @@ import { useUIElementErrors } from '@/components/organisms/UIRenderer/hooks/useU import { useUIElementHandlers } from '@/components/organisms/UIRenderer/hooks/useUIElementHandlers'; import { useUIElementProps } from '@/components/organisms/UIRenderer/hooks/useUIElementProps'; import { useUIElementState } from '@/components/organisms/UIRenderer/hooks/useUIElementState'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject, ErrorsList, RJSFInputAdapter, RJSFInputProps } from '@ballerine/ui'; import get from 'lodash/get'; import { useCallback, useMemo } from 'react'; @@ -37,7 +37,7 @@ const injectIndexToDestinationIfNeeded = (destination: string, index: number | n }; export type DynamicUIComponent = React.ComponentType< - TProps & { definition: UIElement } + TProps & { definition: UIElementDefinition } >; export const withDynamicUIInput = ( diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/JSONFormDefinitionProvider.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/JSONFormDefinitionProvider.tsx index d5f73161dd..03c2b934b2 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/JSONFormDefinitionProvider.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/JSONFormDefinitionProvider.tsx @@ -1,14 +1,14 @@ -import { UIElement } from '@/domains/collection-flow'; +import { JSONFormElementBaseParams } from '@/components/organisms/UIRenderer/elements/JSONForm/JSONForm'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyChildren } from '@ballerine/ui'; import { useMemo } from 'react'; import { jsonFormDefinitionContext } from './json-form-definition.context'; -import { JSONFormElementBaseParams } from '@/components/organisms/UIRenderer/elements/JSONForm/JSONForm'; const { Provider } = jsonFormDefinitionContext; interface Props { children: AnyChildren; - definition: UIElement; + definition: UIElementDefinition; } export const JSONFormDefinitionProvider = ({ children, definition }: Props) => { diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/json-form-definition.context.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/json-form-definition.context.ts index 942a6cc678..ed2ca73800 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/json-form-definition.context.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/JSONForm/providers/JSONFormDefinitionProvider/json-form-definition.context.ts @@ -1,9 +1,9 @@ import { JSONFormElementBaseParams } from '@/components/organisms/UIRenderer/elements/JSONForm/JSONForm'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { createContext } from 'react'; export interface JSONFormDefinitionContext { - definition: UIElement; + definition: UIElementDefinition; } export const jsonFormDefinitionContext = createContext( diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/StepperUI/StepperUI.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/elements/StepperUI/StepperUI.tsx index 7b2b09e6b4..0c19bc7536 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/StepperUI/StepperUI.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/StepperUI/StepperUI.tsx @@ -47,7 +47,7 @@ export const StepperUI = () => { return pages.map(page => { const stepStatus = computeStepStatus({ // @ts-ignore - uiElementState: uiState.elements[page.stateName], + uiElementState: uiState.elements?.[page.stateName], // @ts-ignore pageError: pageErrors?.[page.stateName], page, diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts b/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts index bfad2f76a6..13af46fb71 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/elements/SubmitButton/helpers.ts @@ -1,19 +1,19 @@ import { ARRAY_VALUE_INDEX_PLACEHOLDER } from '@/common/consts/consts'; import { DocumentFieldParams } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField'; -import { UIElement, UIPage } from '@/domains/collection-flow'; +import { UIElementDefinition, UIPage } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; export const getElementByValueDestination = ( destination: string, page: UIPage, -): UIElement | null => { +): UIElementDefinition | null => { const arrayIndexRegex = /[(\d)]/g; const isIndexValue = destination.match(arrayIndexRegex); const findByElementDefinitionByDestination = ( targetDestination: string, - elements: UIElement[], - ): UIElement | null => { + elements: UIElementDefinition[], + ): UIElementDefinition | null => { for (const element of elements) { if (element.valueDestination === targetDestination) return element; @@ -45,11 +45,11 @@ export const getElementByValueDestination = ( export const getDocumentElementByDocumentError = ( id: string, page: UIPage, -): UIElement | null => { +): UIElementDefinition | null => { const findElement = ( id: string, - elements: UIElement[], - ): UIElement | null => { + elements: UIElementDefinition[], + ): UIElementDefinition | null => { for (const element of elements) { //@ts-ignore if (element.options?.documentData?.id === id.replace('document-error-', '')) return element; diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useDataInsertionLogic/useDataInsertionLogic.ts b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useDataInsertionLogic/useDataInsertionLogic.ts index 74b6343d6a..c7e892fea8 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useDataInsertionLogic/useDataInsertionLogic.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useDataInsertionLogic/useDataInsertionLogic.ts @@ -8,12 +8,12 @@ import { ObjectInsertionStrategy } from '@/components/organisms/UIRenderer/hooks import { InsertStrategyRunner } from '@/components/organisms/UIRenderer/hooks/useDataInsertionLogic/insert-strategy-runner'; import { DefinitionInsertionParams } from '@/components/organisms/UIRenderer/hooks/useDataInsertionLogic/types'; import { useListElementsDisablerLogic } from '@/components/organisms/UIRenderer/hooks/useDataInsertionLogic/useElementsDisablerLogic'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { useRefValue } from '@/hooks/useRefValue'; import { useEffect, useMemo } from 'react'; export const useDataInsertionLogic = ( - definition: UIElement, + definition: UIElementDefinition, skip?: boolean, ) => { const { stateApi, payload } = useStateManagerContext(); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementErrors/useUIElementErrors.ts b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementErrors/useUIElementErrors.ts index 76a4ebf358..6f79301ead 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementErrors/useUIElementErrors.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementErrors/useUIElementErrors.ts @@ -1,12 +1,12 @@ import { usePageContext } from '@/components/organisms/DynamicUI/Page'; import { usePageResolverContext } from '@/components/organisms/DynamicUI/PageResolver/hooks/usePageResolverContext'; import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import { useMemo } from 'react'; export const useUIElementErrors = ( - definition: UIElement, + definition: UIElementDefinition, errorKeyFallback?: () => string, ): { warnings: ErrorField[]; validationErrors: ErrorField[] } => { const { errors: _errors, pageErrors: _pageErrors } = usePageContext(); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementHandlers/useUIElementHandlers.ts b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementHandlers/useUIElementHandlers.ts index 2fd6a4f1c8..c29d04b342 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementHandlers/useUIElementHandlers.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementHandlers/useUIElementHandlers.ts @@ -1,11 +1,11 @@ import { useEventEmitterLogic } from '@/components/organisms/DynamicUI/StateManager/components/ActionsHandler'; import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import set from 'lodash/set'; import { useCallback } from 'react'; -export const useUIElementHandlers = (definition: UIElement) => { +export const useUIElementHandlers = (definition: UIElementDefinition) => { const emitEvent = useEventEmitterLogic(definition); const { stateApi } = useStateManagerContext(); const { setContext, getContext } = stateApi; 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 2ef98f03b5..39feb46d0e 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 @@ -3,12 +3,12 @@ import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useD 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 { UIElementDefinition } from '@/domains/collection-flow'; import { AnyObject } from '@ballerine/ui'; import { useMemo } from 'react'; export const useUIElementProps = ( - definition: UIElement, + definition: UIElementDefinition, index: number | null = null, ) => { const { payload } = useStateManagerContext(); diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementState/useUIElementState.ts b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementState/useUIElementState.ts index 68e683f233..e68a21e6af 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementState/useUIElementState.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/hooks/useUIElementState/useUIElementState.ts @@ -1,9 +1,9 @@ import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; import { UIElementState } from '@/components/organisms/DynamicUI/hooks/useUIStateLogic/hooks/useUIElementsStateLogic/types'; -import { UIElement } from '@/domains/collection-flow'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { useMemo } from 'react'; -export const useUIElementState = (definition: UIElement) => { +export const useUIElementState = (definition: UIElementDefinition) => { const { state, helpers } = useDynamicUIContext(); const elementState: UIElementState = useMemo(() => { diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/types.ts b/apps/kyb-app/src/components/organisms/UIRenderer/types.ts index eb2e8cfc52..7d791fa023 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/types.ts +++ b/apps/kyb-app/src/components/organisms/UIRenderer/types.ts @@ -1,8 +1,8 @@ -import { Action, UIElement } from '@/domains/collection-flow'; +import { Action, UIElementDefinition } from '@/domains/collection-flow'; export interface UIElementComponentProps { params: T; - definition: UIElement; + definition: UIElementDefinition; actions: Action[]; } diff --git a/apps/kyb-app/src/components/organisms/UIRenderer/utils/generateBlocks.tsx b/apps/kyb-app/src/components/organisms/UIRenderer/utils/generateBlocks.tsx index a6ba09957b..549ac183fd 100644 --- a/apps/kyb-app/src/components/organisms/UIRenderer/utils/generateBlocks.tsx +++ b/apps/kyb-app/src/components/organisms/UIRenderer/utils/generateBlocks.tsx @@ -1,10 +1,13 @@ //@ts-nocheck -import { UIElement } from '@/domains/collection-flow'; -import { ElementsMap } from '../types/elements.types'; +import { UIElementDefinition } from '@/domains/collection-flow'; import { createBlocks } from '@ballerine/blocks'; import { v4 } from 'uuid'; +import { ElementsMap } from '../types/elements.types'; -export const generateBlocks = (schema: UIElement | UIElement[], elements: ElementsMap) => { +export const generateBlocks = ( + schema: UIElementDefinition | UIElementDefinition[], + elements: ElementsMap, +) => { let base = createBlocks().addBlock(); if (Array.isArray(schema)) { diff --git a/apps/kyb-app/src/components/organisms/VersionResolver/VersionResolver.tsx b/apps/kyb-app/src/components/organisms/VersionResolver/VersionResolver.tsx new file mode 100644 index 0000000000..fb987ee1c9 --- /dev/null +++ b/apps/kyb-app/src/components/organisms/VersionResolver/VersionResolver.tsx @@ -0,0 +1,33 @@ +import { LoadingScreen } from '@/pages/CollectionFlow/components/atoms/LoadingScreen'; +import { AnyChildren } from '@ballerine/ui'; +import { FunctionComponent, useEffect, useState } from 'react'; + +interface IVersionResolverProps { + version: number; + children: AnyChildren; +} + +export const VersionResolver: FunctionComponent = ({ + version, + children, +}) => { + const [isResolved, setResolved] = useState(false); + + useEffect(() => { + if (!version) return; + + if (version === 1) { + setResolved(true); + return; + } + + if (location.href.includes(`/v${version}/`)) { + setResolved(true); + return; + } + + location.href = `${location.href.replace(location.origin, `/v${version}/collection-flow`)}`; + }, [version]); + + return isResolved ? children : ; +}; diff --git a/apps/kyb-app/src/components/organisms/VersionResolver/index.ts b/apps/kyb-app/src/components/organisms/VersionResolver/index.ts new file mode 100644 index 0000000000..fa7f8b8e96 --- /dev/null +++ b/apps/kyb-app/src/components/organisms/VersionResolver/index.ts @@ -0,0 +1 @@ +export * from './VersionResolver'; diff --git a/apps/kyb-app/src/components/providers/Validator/Validator.tsx b/apps/kyb-app/src/components/providers/Validator/Validator.tsx new file mode 100644 index 0000000000..16b073fc29 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/Validator.tsx @@ -0,0 +1,41 @@ +import { useValidate } from '@/components/providers/Validator/hooks/useValidate'; +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { UIElementV2 } from '@/components/providers/Validator/types'; +import React, { FunctionComponent, useCallback, useMemo, useState } from 'react'; +import { TValidationErrors, validatorContext } from './validator.context'; + +const { Provider } = validatorContext; + +export interface IValidatorProps { + children: React.ReactNode | React.ReactNode[]; + context: unknown; + elements: UIElementV2[]; +} + +export const Validator: FunctionComponent = ({ children, elements, context }) => { + const validate = useValidate({ elements, context }); + const [validationErrors, setValiationErrors] = useState({}); + + console.log({ validationErrors }); + + const onValidate = useCallback(() => { + const errors = validate(); + const validationErrors = errors.reduce((acc, error) => { + const element = new UIElement(error.element, context, error.stack); + acc[element.getId()] = [...(acc[element.getId()] || []), error.message]; + + return acc; + }, {} as TValidationErrors); + + setValiationErrors(validationErrors); + + return Boolean(errors.length); + }, [validate, context]); + + const ctx = useMemo( + () => ({ validate: onValidate, errors: validationErrors }), + [validationErrors, onValidate], + ); + + return {children}; +}; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/index.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/index.ts new file mode 100644 index 0000000000..2ae47e498d --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/index.ts @@ -0,0 +1,3 @@ +export * from './useValidate'; +export * from './utils/format-value-destination-and-apply-stack-indexes'; +export * from './validate'; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/ui-element.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/ui-element.ts new file mode 100644 index 0000000000..4e42f0544d --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/ui-element.ts @@ -0,0 +1,79 @@ +import { testRule } from '@/components/organisms/DynamicUI/rule-engines/utils/execute-rules'; +import { formatValueDestinationAndApplyStackIndexes } from '@/components/providers/Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes'; +import { TValidationParams, UIElementV2 } from '@/components/providers/Validator/types'; +import { IDocumentValueValidatorParams } from '@/components/providers/Validator/value-validators/document.value.validator'; +import { IRequiredValueValidatorParams } from '@/components/providers/Validator/value-validators/required.value-validator'; +import { fieldElelements } from '@/pages/CollectionFlowV2/renderer-schema'; +import { AnyObject } from '@ballerine/ui'; +import get from 'lodash/get'; + +export class UIElement { + constructor(readonly element: UIElementV2, readonly context: unknown, readonly stack: number[]) {} + + getId() { + return this.formatId(this.element.id); + } + + getOriginId() { + return this.element.id; + } + + isRequired() { + const requiredParams = this.element.validation?.required as IRequiredValueValidatorParams; + const documentParams = this.element.validation?.document as IDocumentValueValidatorParams; + + const applyRules = requiredParams?.applyWhen || documentParams?.applyWhen || []; + + if (applyRules.length) { + const isShouldApplyRequired = applyRules.every(rule => + testRule(this.context as AnyObject, rule), + ); + + return Boolean(isShouldApplyRequired); + } else { + return Boolean(requiredParams?.required) || Boolean(documentParams?.documentId); + } + } + + private formatId(id: string) { + return `${id}${this.stack.join('.')}`; + } + + getValueDestination() { + return this.formatValueDestination(this.element.valueDestination); + } + + private formatValueDestination(valueDestination: string) { + return this.formatValueDestinationAndApplyStackIndexes(valueDestination); + } + + private formatValueDestinationAndApplyStackIndexes(valueDestination: string) { + return formatValueDestinationAndApplyStackIndexes(valueDestination, this.stack); + } + + getValue() { + const valueDestination = this.getValueDestination(); + + return get(this.context, valueDestination); + } + + getValidatorsParams() { + const validatorKeys = Object.keys(this.element.validation || {}); + + return validatorKeys.map(key => ({ + validator: key, + params: this.element.validation[key as keyof UIElementV2['validation']] as TValidationParams, + })); + } + + getDefinition() { + return this.element; + } + + getFieldType() { + if (this.element.element === 'fieldlist') return 'field-list'; + if (this.element.element in fieldElelements) return 'field'; + + return 'ui'; + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/useValidate.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/useValidate.ts new file mode 100644 index 0000000000..a36c7d1eea --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/useValidate.ts @@ -0,0 +1,24 @@ +import { UIElementV2 } from '@/components/providers/Validator/types'; +import { useCallback } from 'react'; +import { validate as _validate } from './validate'; + +interface IUseValidateParams { + elements: UIElementV2[]; + context: unknown; +} + +export interface IValidationError { + id: string; + message: string; + element: UIElementV2; + valueDestination: string; + stack: number[]; +} + +export const useValidate = ({ elements, context }: IUseValidateParams) => { + const validate = useCallback(() => { + return _validate(elements, context as object); + }, [elements, context]); + + return validate; +}; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts new file mode 100644 index 0000000000..3c5b32e485 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/utils/format-value-destination-and-apply-stack-indexes.ts @@ -0,0 +1,10 @@ +export const formatValueDestinationAndApplyStackIndexes = ( + valueDestination: string, + stack: number[], +) => { + stack.forEach((stackValue, index) => { + valueDestination = valueDestination.replace(`$${index}`, String(stackValue)); + }); + + return valueDestination; +}; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/validate.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/validate.ts new file mode 100644 index 0000000000..bba92367a0 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidate/validate.ts @@ -0,0 +1,98 @@ +import { testRule } from '@/components/organisms/DynamicUI/rule-engines/utils/execute-rules'; +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { IValidationError } from '@/components/providers/Validator/hooks/useValidate/useValidate'; +import { IBaseValueValidatorParams, UIElementV2 } from '@/components/providers/Validator/types'; +import { ValueValidatorManager } from '@/components/providers/Validator/value-validator-manager'; +import { AnyObject } from '@ballerine/ui'; + +export const validate = (elements: UIElementV2[], context: object) => { + const validatorManager = new ValueValidatorManager(); + let errors: IValidationError[] = []; + + const fieldValidationStrategy = ( + element: UIElement, + stack: number[] = [], + ): IValidationError[] => { + const value = element.getValue(); + const fieldContext = { + context, + stack, + }; + + const isShouldApplyValidation = ( + params: TParams, + context: AnyObject, + ) => { + const applyRules = params.applyWhen && params.applyWhen ? params.applyWhen : null; + + if (!applyRules) return true; + + return applyRules.length && applyRules.every(rule => testRule(context, rule)); + }; + + const validationErrors = element.getValidatorsParams().map(({ validator, params }) => { + try { + if (validator === 'required' || validator === 'document') { + const isRequired = element.isRequired(); + if (!isRequired && value === undefined) return; + + validatorManager.validate(value, validator as any, params, fieldContext); + } else { + if (!isShouldApplyValidation(params as unknown as IBaseValueValidatorParams, context)) + return; + + if (value === undefined) return; + + validatorManager.validate(value, validator as any, params, fieldContext); + } + } catch (error) { + return { + //@ts-ignore + message: error.message, + element: element.element, + id: element.getId(), + valueDestination: element.getValueDestination(), + stack, + }; + } + }); + + return validationErrors.filter(Boolean) as IValidationError[]; + }; + + const validateFn = (elements: UIElementV2[], context: object, stack: number[] = []) => { + for (let i = 0; i < elements.length; i++) { + const element = elements[i] as UIElementV2; + const uiElement = new UIElement(element, context, stack); + if (!element) continue; + + if (uiElement.getFieldType() === 'field') { + errors = [...errors, ...fieldValidationStrategy(uiElement, stack)]; + + continue; + } + + if (uiElement.getFieldType() === 'field-list') { + const value = uiElement.getValue() as any[]; + + errors = [...errors, ...fieldValidationStrategy(uiElement, stack)]; + + if (!Array.isArray(value)) continue; + + value?.forEach((_, index) => { + validateFn(element.children!, context, [...stack, index]); + }); + + continue; + } + + if (element.children) { + validateFn(element.children, context, stack); + } + } + }; + + validateFn(elements, context as any); + + return errors; +}; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidatedInput/index.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidatedInput/index.ts new file mode 100644 index 0000000000..b6fb03d1bc --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidatedInput/index.ts @@ -0,0 +1 @@ +export * from './useValidatedInput'; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidatedInput/useValidatedInput.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidatedInput/useValidatedInput.ts new file mode 100644 index 0000000000..7daa7ec242 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidatedInput/useValidatedInput.ts @@ -0,0 +1,8 @@ +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; + +export const useValidatedInput = (element: UIElement) => { + const { errors } = useValidator(); + + return errors[element.getId()]; +}; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidator/index.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidator/index.ts new file mode 100644 index 0000000000..df0ef89dfd --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidator/index.ts @@ -0,0 +1 @@ +export * from './useValidator'; diff --git a/apps/kyb-app/src/components/providers/Validator/hooks/useValidator/useValidator.ts b/apps/kyb-app/src/components/providers/Validator/hooks/useValidator/useValidator.ts new file mode 100644 index 0000000000..f4889599d5 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/hooks/useValidator/useValidator.ts @@ -0,0 +1,4 @@ +import { validatorContext } from '@/components/providers/Validator/validator.context'; +import { useContext } from 'react'; + +export const useValidator = () => useContext(validatorContext); diff --git a/apps/kyb-app/src/components/providers/Validator/index.ts b/apps/kyb-app/src/components/providers/Validator/index.ts new file mode 100644 index 0000000000..3ed72221f5 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/index.ts @@ -0,0 +1 @@ +export * from './Validator'; diff --git a/apps/kyb-app/src/components/providers/Validator/types.ts b/apps/kyb-app/src/components/providers/Validator/types.ts new file mode 100644 index 0000000000..77580a1dfb --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/types.ts @@ -0,0 +1,63 @@ +import { IDocumentValueValidatorParams } from '@/components/providers/Validator/value-validators/document.value.validator'; +import { IFormatValueValidatorParams } from '@/components/providers/Validator/value-validators/format.value.validator'; +import { IMaxLengthValueValidatorParams } from '@/components/providers/Validator/value-validators/max-length.value.validator'; +import { IMaximumValueValidatorParams } from '@/components/providers/Validator/value-validators/maximum.value.validator'; +import { IMinLengthValueValidatorParams } from '@/components/providers/Validator/value-validators/min-length.value.validator'; +import { IPatternValidatorParams } from '@/components/providers/Validator/value-validators/pattern.value.validator'; +import { IRequiredValueValidatorParams } from '@/components/providers/Validator/value-validators/required.value-validator'; +import { Rule } from '@/domains/collection-flow'; +import { AnyObject } from '@ballerine/ui'; + +export type TFormats = 'email'; + +export type TValidatorErrorMessage = string; + +export type TValidatorApplyRule = object; + +export type TValidationParams = + | IFormatValueValidatorParams + | IMaxLengthValueValidatorParams + | IMinLengthValueValidatorParams + | IMaximumValueValidatorParams + | IMinLengthValueValidatorParams + | IRequiredValueValidatorParams + | IPatternValidatorParams + | IDocumentValueValidatorParams; + +export type TValidators = + | 'required' + | 'minLength' + | 'maxLength' + | 'pattern' + | 'minimum' + | 'maximum' + | 'format' + | 'document'; + +export interface IBaseFieldParams { + label?: string; + placeholder?: string; + stack?: number[]; +} + +export interface UIElementV2 { + id: string; + element: string; + validation: Partial>; + options?: TFieldParams; + valueDestination: string; + children?: UIElementV2[]; + + availableOn?: Rule[]; + visibleOn?: Rule[]; +} + +export interface IBaseValueValidatorParams { + message?: string; + applyWhen?: Rule[]; +} + +export interface IFieldContext { + context: AnyObject; + stack: number[]; +} diff --git a/apps/kyb-app/src/components/providers/Validator/validator.context.ts b/apps/kyb-app/src/components/providers/Validator/validator.context.ts new file mode 100644 index 0000000000..427b6885d6 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/validator.context.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react'; + +type TIsValid = boolean; +type TFielName = string; + +export type TValidationErrors = Record; +export interface IValidatorContext { + validate: () => TIsValid; + errors: TValidationErrors; +} + +export const validatorContext = createContext({} as IValidatorContext); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validator-manager.ts b/apps/kyb-app/src/components/providers/Validator/value-validator-manager.ts new file mode 100644 index 0000000000..01aea4fa49 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validator-manager.ts @@ -0,0 +1,37 @@ +import { IBaseValueValidatorParams, IFieldContext } from '@/components/providers/Validator/types'; +import { DocumentValueValidator } from '@/components/providers/Validator/value-validators/document.value.validator'; +import { FormatValueValidator } from '@/components/providers/Validator/value-validators/format.value.validator'; +import { MaxLengthValueValidator } from '@/components/providers/Validator/value-validators/max-length.value.validator'; +import { MaximumValueValidator } from '@/components/providers/Validator/value-validators/maximum.value.validator'; +import { MinLengthValueValidator } from '@/components/providers/Validator/value-validators/min-length.value.validator'; +import { MinimumValueValidator } from '@/components/providers/Validator/value-validators/minimum.value.validator'; +import { PatternValueValidator } from '@/components/providers/Validator/value-validators/pattern.value.validator'; +import { RequiredValueValidator } from '@/components/providers/Validator/value-validators/required.value-validator'; + +const validatorsMap = { + required: RequiredValueValidator, + minLength: MinLengthValueValidator, + maxLength: MaxLengthValueValidator, + pattern: PatternValueValidator, + minimum: MinimumValueValidator, + maximum: MaximumValueValidator, + format: FormatValueValidator, + document: DocumentValueValidator, +}; + +export type TValidator = keyof typeof validatorsMap; + +export class ValueValidatorManager { + constructor(readonly validators: typeof validatorsMap = validatorsMap) {} + + validate( + value: unknown, + key: TValidator, + params: TValidatorParams, + fieldContext: IFieldContext, + ) { + const validator = new this.validators[key](params as any); + //@ts-ignore + return validator.validate(value, fieldContext); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/document.value.validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/document.value.validator.ts new file mode 100644 index 0000000000..6473cd72a0 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/document.value.validator.ts @@ -0,0 +1,42 @@ +import { formatValueDestinationAndApplyStackIndexes } from '@/components/providers/Validator/hooks/useValidate'; +import { IBaseValueValidatorParams, IFieldContext } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; +import { Document } from '@/domains/collection-flow'; +import get from 'lodash/get'; + +export interface IDocumentValueValidatorParams extends IBaseValueValidatorParams { + documentId: string; + pathToDocuments: string; + + // Page index to check file id from, defaults to 0 + pageIndex?: number; +} + +export class DocumentValueValidator extends ValueValidator { + type = 'document'; + + validate(_: unknown, fieldContext: IFieldContext): void { + const { pathToDocuments, documentId, pageIndex = 0 } = this.params; + + const documentsPath = this.getDocumentsPathWithIndexes(pathToDocuments, fieldContext); + const documents = get(fieldContext.context, documentsPath); + + const document = documents?.find((document: Document) => document.id === documentId); + + debugger; + + if (!document || !document.pages?.[pageIndex]?.ballerineFileId) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) return `Document is required.`; + + return this.params.message; + } + + private getDocumentsPathWithIndexes(documentsPath: string, fieldContext: IFieldContext) { + return formatValueDestinationAndApplyStackIndexes(documentsPath, fieldContext.stack || []); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/document.value.validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/document.value.validator.unit.test.ts new file mode 100644 index 0000000000..c932511519 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/document.value.validator.unit.test.ts @@ -0,0 +1,186 @@ +import { DocumentValueValidator } from './document.value.validator'; + +describe('DocumentValueValidator', () => { + describe('validation will fail', () => { + test('when document is not found', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'form.documents', + }); + + expect(() => validator.validate(null as any, {} as any)).toThrowError( + 'Document is required.', + ); + }); + + test('when document not found at specific page index', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'form.documents', + pageIndex: 1, + }); + + expect(() => validator.validate(null as any, {} as any)).toThrowError( + 'Document is required.', + ); + }); + + test('when document is not found in nested data structure', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'data.items[$0].documents', + }); + + expect(() => + validator.validate( + null as any, + { + stack: [1], + context: { + data: { + items: [ + { + documents: [], + }, + { + documents: [], + }, + ], + }, + }, + } as any, + ), + ).toThrowError('Document is required.'); + }); + + test('when document not found at specific page index in nested data structure', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'data.items[$0].documents', + pageIndex: 1, + }); + + expect(() => + validator.validate( + null as any, + { + stack: [1], + context: {}, + } as any, + ), + ).toThrowError('Document is required.'); + }); + }); + + describe('validation will pass', () => { + test('when document is found', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + }); + + const context = { + documents: [ + { + id: '123', + pages: [ + { + ballerineFileId: 'someFileId', + }, + ], + }, + ], + }; + + expect(() => validator.validate(null as any, { context, stack: [] })).not.toThrow(); + }); + + test('when document is found at specific page index', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + pageIndex: 1, + }); + + const context = { + documents: [ + { + id: '123', + pages: [ + {}, + { + ballerineFileId: 'someFileId', + }, + ], + }, + ], + }; + + expect(() => validator.validate(null as any, { context, stack: [] })).not.toThrow(); + }); + + test.each([ + ['single level nesting', 'data.items[$0].documents', [0]], + ['two levels of nesting', 'data.items[$0].subitems[$1].documents', [0, 1]], + [ + 'three levels of nesting', + 'data.items[$0].subitems[$1].subsubitems[$2].documents', + [0, 1, 1], + ], + ])('when document is found in nested data structure - %s', (_, pathToDocuments, stack) => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments, + }); + + const context = { + data: { + items: [ + { + documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], + subitems: [ + {}, + { + documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], + subsubitems: [ + {}, + { + documents: [{ id: '123', pages: [{ ballerineFileId: 'someFileId' }] }], + }, + ], + }, + ], + }, + ], + }, + }; + + expect(() => validator.validate(null as any, { context, stack })).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + }); + + expect(() => validator.validate(null as any, { context: {}, stack: [] })).toThrowError( + 'Document is required.', + ); + }); + + test('should return custom error message when message is provided', () => { + const validator = new DocumentValueValidator({ + documentId: '123', + pathToDocuments: 'documents', + message: 'Custom error message.', + }); + + expect(() => validator.validate(null as any, { context: {}, stack: [] })).toThrowError( + 'Custom error message.', + ); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/format.value.validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/format.value.validator.ts new file mode 100644 index 0000000000..16efc39129 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/format.value.validator.ts @@ -0,0 +1,29 @@ +import { IBaseValueValidatorParams, TFormats } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; +import EmailValidator from 'email-validator'; + +export interface IFormatValueValidatorParams extends IBaseValueValidatorParams { + format: TFormats; +} + +export class FormatValueValidator extends ValueValidator { + type = 'format'; + + validate(value: unknown): void { + if (this.params.format === 'email') { + if (!EmailValidator.validate(value as string)) { + throw new Error(this.getErrorMessage()); + } + + return; + } + + throw new Error(`Format ${this.params.format} is not supported.`); + } + + private getErrorMessage() { + if (!this.params.message) return 'Invalid format.'; + + return this.params.message.replace('{format}', this.params.format.toString()); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/format.value.validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/format.value.validator.unit.test.ts new file mode 100644 index 0000000000..47c440d222 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/format.value.validator.unit.test.ts @@ -0,0 +1,58 @@ +import { FormatValueValidator } from '@/components/providers/Validator/value-validators/format.value.validator'; + +describe('Format Value Validator', () => { + describe('validation will fail', () => { + test('when value does not match format', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + + test('when unsupported format is provided', () => { + //@ts-ignore + const validator = new FormatValueValidator({ format: 'unsupported', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('Format unsupported is not supported.'); + }); + }); + + describe('validation will pass', () => { + test('when value matches format', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('example@gmail.com')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new FormatValueValidator({ format: 'email' }); + + expect(() => validator.validate('abc')).toThrowError('Invalid format.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('should interpolate {format} with the provided value', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error {format}' }); + + expect(() => validator.validate('abc')).toThrowError('error email'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new FormatValueValidator({ format: 'email', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/max-length.value.validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/max-length.value.validator.ts new file mode 100644 index 0000000000..2bc9f8d018 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/max-length.value.validator.ts @@ -0,0 +1,26 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IMaxLengthValueValidatorParams extends IBaseValueValidatorParams { + maxLength: number; +} + +export class MaxLengthValueValidator extends ValueValidator { + type = 'maxLength'; + + validate(value: TValue): void { + if (value?.length === undefined || value.length > this.params.maxLength) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Maximum length is {maxLength}.`.replace( + '{maxLength}', + this.params.maxLength.toString(), + ); + + return this.params.message.replace('{maxLength}', this.params.maxLength.toString()); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/max-length.value.validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/max-length.value.validator.unit.test.ts new file mode 100644 index 0000000000..5f8b758cbb --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/max-length.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MaxLengthValueValidator } from '@/components/providers/Validator/value-validators/max-length.value.validator'; + +describe('Max Length Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is above maximum length', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('123456')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is below maximum length', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + + test('when value is equal to maximum length', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5 }); + + expect(() => validator.validate('123456')).toThrowError('Maximum length is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('123456')).toThrowError('error'); + }); + + test('should interpolate {maxLength} with the provided value', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error {maxLength}' }); + + expect(() => validator.validate('123456')).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MaxLengthValueValidator({ maxLength: 5, message: 'error' }); + + expect(() => validator.validate('123456')).toThrowError('error'); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/maximum.value.validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/maximum.value.validator.ts new file mode 100644 index 0000000000..55bf6ae8ce --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/maximum.value.validator.ts @@ -0,0 +1,23 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IMaximumValueValidatorParams extends IBaseValueValidatorParams { + maximum: number; +} + +export class MaximumValueValidator extends ValueValidator { + type = 'maximum'; + + validate(value: TValue): void { + if (typeof value !== 'number' || value > this.params.maximum) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Maximum value is {maximum}.`.replace('{maximum}', this.params.maximum.toString()); + + return this.params.message.replace('{maximum}', this.params.maximum.toString()); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/maximum.value.validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/maximum.value.validator.unit.test.ts new file mode 100644 index 0000000000..4cceba2b16 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/maximum.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MaximumValueValidator } from '@/components/providers/Validator/value-validators/maximum.value.validator'; + +describe('Maximum Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is above maximum', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(6)).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is below maximum', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(4)).not.toThrow(); + }); + + test('when value is equal to maximum', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(5)).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MaximumValueValidator({ maximum: 5 }); + + expect(() => validator.validate(6)).toThrowError('Maximum value is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(6)).toThrowError('error'); + }); + + test('should interpolate {maximum} with the provided value', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error {maximum}' }); + + expect(() => validator.validate(6)).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MaximumValueValidator({ maximum: 5, message: 'error' }); + + expect(() => validator.validate(6)).toThrowError('error'); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/min-length.value.validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/min-length.value.validator.ts new file mode 100644 index 0000000000..04eb9ff5b3 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/min-length.value.validator.ts @@ -0,0 +1,26 @@ +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; +import { IBaseValueValidatorParams } from '../types'; + +export interface IMinLengthValueValidatorParams extends IBaseValueValidatorParams { + minLength: number; +} + +export class MinLengthValueValidator extends ValueValidator { + type = 'minLength'; + + validate(value: TValue): void { + if (value?.length === undefined || value.length < this.params.minLength) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Minimum length is {minLength}.`.replace( + '{minLength}', + this.params.minLength.toString(), + ); + + return this.params.message.replace('{minLength}', this.params.minLength.toString()); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/min-length.value.validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/min-length.value.validator.unit.test.ts new file mode 100644 index 0000000000..68e7f607de --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/min-length.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MinLengthValueValidator } from '@/components/providers/Validator/value-validators/min-length.value.validator'; + +describe('MinLength Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is below minimum length', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('1234')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is above minimum length', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + + test('when value is equal to minimum length', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('12345')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MinLengthValueValidator({ minLength: 5 }); + + expect(() => validator.validate('1234')).toThrowError('Minimum length is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('1234')).toThrowError('error'); + }); + + test('should interpolate {minLength} with the provided value', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error {minLength}' }); + + expect(() => validator.validate('1234')).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MinLengthValueValidator({ minLength: 5, message: 'error' }); + + expect(() => validator.validate('1234')).toThrowError('error'); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/minimum.value.validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/minimum.value.validator.ts new file mode 100644 index 0000000000..1e08f15ba4 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/minimum.value.validator.ts @@ -0,0 +1,23 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IMinimumValueValidatorParams extends IBaseValueValidatorParams { + minimum: number; +} + +export class MinimumValueValidator extends ValueValidator { + type = 'minimum'; + + validate(value: TValue): void { + if (typeof value !== 'number' || value < this.params.minimum) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Minimum value is {minimum}.`.replace('{minimum}', this.params.minimum.toString()); + + return this.params.message.replace('{minimum}', this.params.minimum.toString()); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/minimum.value.validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/minimum.value.validator.unit.test.ts new file mode 100644 index 0000000000..61bc62bd29 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/minimum.value.validator.unit.test.ts @@ -0,0 +1,63 @@ +import { MinimumValueValidator } from '@/components/providers/Validator/value-validators/minimum.value.validator'; + +describe('MinimumValueValidator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is below minimum', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(4)).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is above minimum', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(5)).not.toThrow(); + }); + + test('when value is equal to minimum', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(5)).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new MinimumValueValidator({ minimum: 5 }); + + expect(() => validator.validate(4)).toThrowError('Minimum value is 5.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(4)).toThrowError('error'); + }); + + test('should interpolate {minimum} with the provided value', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error {minimum}' }); + + expect(() => validator.validate(4)).toThrowError('error 5'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new MinimumValueValidator({ minimum: 5, message: 'error' }); + + expect(() => validator.validate(4)).toThrowError('error'); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/pattern.value.validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/pattern.value.validator.ts new file mode 100644 index 0000000000..b2cd0302e1 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/pattern.value.validator.ts @@ -0,0 +1,23 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IPatternValidatorParams extends IBaseValueValidatorParams { + pattern: string; +} + +export class PatternValueValidator extends ValueValidator { + type = 'pattern'; + + validate(value: unknown) { + if (!new RegExp(this.params.pattern).test(value as string)) { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) + return `Value must match {pattern}.`.replace('{pattern}', this.params.pattern); + + return this.params.message.replace('{pattern}', this.params.pattern); + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/pattern.value.validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/pattern.value.validator.unit.test.ts new file mode 100644 index 0000000000..b305bcb250 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/pattern.value.validator.unit.test.ts @@ -0,0 +1,54 @@ +import { PatternValueValidator } from '@/components/providers/Validator/value-validators/pattern.value.validator'; + +describe('Pattern Value Validator', () => { + describe('validation will fail', () => { + test('when value does not match pattern', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value matches pattern', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('123')).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$' }); + + expect(() => validator.validate('abc')).toThrowError('Value must match ^[0-9]+$.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + + test('should interpolate {pattern} with the provided value', () => { + const validator = new PatternValueValidator({ + pattern: '^[0-9]+$', + message: 'error {pattern}', + }); + + expect(() => validator.validate('abc')).toThrowError('error ^[0-9]+$'); + }); + + test('error message should stay same if interlopation tag is not present', () => { + const validator = new PatternValueValidator({ pattern: '^[0-9]+$', message: 'error' }); + + expect(() => validator.validate('abc')).toThrowError('error'); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/required.value-validator.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/required.value-validator.ts new file mode 100644 index 0000000000..bdaaeb20f6 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/required.value-validator.ts @@ -0,0 +1,22 @@ +import { IBaseValueValidatorParams } from '@/components/providers/Validator/types'; +import { ValueValidator } from '@/components/providers/Validator/value-validators/value-validator.abstract'; + +export interface IRequiredValueValidatorParams extends IBaseValueValidatorParams { + required: boolean; +} + +export class RequiredValueValidator extends ValueValidator { + type = 'required'; + + validate(value: unknown) { + if (value === undefined || value === null || value === '') { + throw new Error(this.getErrorMessage()); + } + } + + private getErrorMessage() { + if (!this.params.message) return `Value is required.`; + + return this.params.message; + } +} diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/required.value-validator.unit.test.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/required.value-validator.unit.test.ts new file mode 100644 index 0000000000..a752045689 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/required.value-validator.unit.test.ts @@ -0,0 +1,57 @@ +import { RequiredValueValidator } from '@/components/providers/Validator/value-validators/required.value-validator'; + +describe('Required Value Validator', () => { + describe('validation will fail', () => { + test('when value is undefined', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(undefined as any)).toThrowError('error'); + }); + + test('when value is empty string', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate('')).toThrowError('error'); + }); + + test('when value is null', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(null as any)).toThrowError('error'); + }); + }); + + describe('validation will pass', () => { + test('when value is not empty string', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate('value')).not.toThrow(); + }); + + test('when value is not null', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(0)).not.toThrow(); + }); + + test('when value is not undefined', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate(false)).not.toThrow(); + }); + }); + + describe('validator error messages', () => { + test('should return default error message when message is not provided', () => { + const validator = new RequiredValueValidator({ required: true }); + + expect(() => validator.validate('')).toThrowError('Value is required.'); + }); + + test('should return custom error message when message is provided', () => { + const validator = new RequiredValueValidator({ message: 'error', required: true }); + + expect(() => validator.validate('')).toThrowError('error'); + }); + }); +}); diff --git a/apps/kyb-app/src/components/providers/Validator/value-validators/value-validator.abstract.ts b/apps/kyb-app/src/components/providers/Validator/value-validators/value-validator.abstract.ts new file mode 100644 index 0000000000..d45f4d9b13 --- /dev/null +++ b/apps/kyb-app/src/components/providers/Validator/value-validators/value-validator.abstract.ts @@ -0,0 +1,9 @@ +import { IFieldContext } from '@/components/providers/Validator/types'; + +export abstract class ValueValidator { + abstract type: string; + + constructor(readonly params: TParams) {} + + abstract validate(value: unknown, fieldContext: IFieldContext): void; +} 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 5f50526ace..925054215d 100644 --- a/apps/kyb-app/src/domains/collection-flow/types/index.ts +++ b/apps/kyb-app/src/domains/collection-flow/types/index.ts @@ -1,5 +1,5 @@ import { ITheme } from '@/common/types/settings'; -import { Action, Rule, UIElement } from '@/domains/collection-flow/types/ui-schema.types'; +import { Action, Rule, UIElementDefinition } from '@/domains/collection-flow/types/ui-schema.types'; import { AnyObject } from '@ballerine/ui'; import { RJSFSchema, UiSchema } from '@rjsf/utils'; @@ -127,7 +127,7 @@ export interface UIPage { name: string; number: number; stateName: string; - elements: Array>; + elements: Array>; actions: Action[]; pageValidation?: Rule[]; } @@ -156,6 +156,7 @@ export interface UISchema { definition: AnyObject; extensions: AnyObject; }; + version: number; uiOptions?: UIOptions; } diff --git a/apps/kyb-app/src/domains/collection-flow/types/ui-schema.types.ts b/apps/kyb-app/src/domains/collection-flow/types/ui-schema.types.ts index e50271a16f..73014e022c 100644 --- a/apps/kyb-app/src/domains/collection-flow/types/ui-schema.types.ts +++ b/apps/kyb-app/src/domains/collection-flow/types/ui-schema.types.ts @@ -1,3 +1,4 @@ +import { UIElementV2 } from '@/components/providers/Validator/types'; import { AnyObject } from '@ballerine/ui'; export type UIElementType = string; @@ -27,6 +28,10 @@ export interface JMESPathRule extends BaseRule { value: string; } +export interface ValidContextRule extends BaseRule { + value: AnyObject; +} + export interface IRule extends BaseRule { value: string; persistStateRule?: boolean; @@ -53,7 +58,7 @@ export type Rule = JSONLogicRule | JMESPathRule | DocumentsValidatorRule; export type UIElementDestination = string; -export interface UIElement { +export interface UIElementDefinition { name: string; type: UIElementType; availableOn?: Rule[]; @@ -62,5 +67,6 @@ export interface UIElement { required?: boolean; options: TElementParams; valueDestination?: UIElementDestination; - elements?: UIElement[]; + elements?: UIElementDefinition[]; + validation?: UIElementV2['validation']; } diff --git a/apps/kyb-app/src/hooks/useFlowTracking/useFlowTracking.ts b/apps/kyb-app/src/hooks/useFlowTracking/useFlowTracking.ts index bc9416fe79..4c7feb98e5 100644 --- a/apps/kyb-app/src/hooks/useFlowTracking/useFlowTracking.ts +++ b/apps/kyb-app/src/hooks/useFlowTracking/useFlowTracking.ts @@ -3,13 +3,13 @@ import { useCallback } from 'react'; export const useFlowTracking = () => { const trackExit = useCallback(() => { const event = 'ballerine.collection-flow.back-button-pressed'; - console.log(`Sending event: ${event}`); + console.info(`Sending event: ${event}`); window.parent.postMessage(event, '*'); }, []); const trackFinish = useCallback(() => { const event = 'ballerine.collection-flow.finish-button-pressed'; - console.log(`Sending event: ${event}`); + console.info(`Sending event: ${event}`); window.parent.postMessage('ballerine.collection-flow.finish-button-pressed', '*'); }, []); diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/CollectionFlowV2.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/CollectionFlowV2.tsx new file mode 100644 index 0000000000..e3a14c5c86 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/CollectionFlowV2.tsx @@ -0,0 +1,261 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { StepperProgress } from '@/common/components/atoms/StepperProgress'; +import { ProgressBar } from '@/common/components/molecules/ProgressBar'; +import { AppShell } from '@/components/layouts/AppShell'; +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'; +import { StepperUI } from '@/components/organisms/UIRenderer/elements/StepperUI'; +import { useCustomer } from '@/components/providers/CustomerProvider'; +import { Validator } from '@/components/providers/Validator'; +import { UIElementV2 } from '@/components/providers/Validator/types'; +import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; +import { prepareInitialUIState } from '@/helpers/prepareInitialUIState'; +import { useFlowContextQuery } from '@/hooks/useFlowContextQuery'; +import { useLanguageParam } from '@/hooks/useLanguageParam/useLanguageParam'; +import { withSessionProtected } from '@/hooks/useSessionQuery/hocs/withSessionProtected'; +import { useUISchemasQuery } from '@/hooks/useUISchemasQuery'; +import { Approved } from '@/pages/CollectionFlow/components/pages/Approved'; +import { Rejected } from '@/pages/CollectionFlow/components/pages/Rejected'; +import { Success } from '@/pages/CollectionFlow/components/pages/Success'; +import { StackProvider } from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider'; +import { rendererSchema } from '@/pages/CollectionFlowV2/renderer-schema'; +import { Renderer } from '@ballerine/ui'; +import set from 'lodash/set'; +import ReactJson from 'react-json-view'; + +// TODO: Find a way to make this work via the workflow-browser-sdk `subscribe` method. +export const useCompleteLastStep = () => { + const { stateApi, state } = useStateManagerContext(); + const { language } = useLanguageParam(); + const { data: schema } = useUISchemasQuery(language); + const { refetch } = useFlowContextQuery(); + const elements = schema?.uiSchema?.elements; + const isPendingSync = useRef(false); + + useEffect(() => { + (async () => { + if (state !== 'finish') return; + + const { data: context } = await refetch(); + + if ( + !context || + context?.flowConfig?.stepsProgress?.[elements?.at(-1)?.stateName ?? '']?.isCompleted || + isPendingSync.current + ) { + return; + } + + set(context, `flowConfig.stepsProgress.${elements?.at(-1)?.stateName}.isCompleted`, true); + await stateApi.invokePlugin('sync_workflow_runtime'); + isPendingSync.current = true; + })(); + }, [elements, refetch, state, stateApi]); +}; + +export const CollectionFlowV2 = withSessionProtected(() => { + const { language } = useLanguageParam(); + const { data: schema } = useUISchemasQuery(language); + const { data: context } = useFlowContextQuery(); + const { customer } = useCustomer(); + const { t } = useTranslation(); + + const elements = schema?.uiSchema?.elements; + const definition = schema?.definition.definition; + + const pageErrors = usePageErrors(context ?? {}, elements || []); + const isRevision = useMemo( + () => pageErrors.some(error => error.errors?.some(error => error.type === 'warning')), + [pageErrors], + ); + + const filteredNonEmptyErrors = pageErrors?.filter(pageError => !!pageError.errors.length); + + // @ts-ignore + const initialContext: CollectionFlowContext | null = useMemo(() => { + const appState = + filteredNonEmptyErrors?.[0]?.stateName || + context?.flowConfig?.appState || + elements?.at(0)?.stateName; + + if (!appState) return null; + + return { + ...context, + flowConfig: { + ...context?.flowConfig, + appState, + }, + state: appState, + }; + }, [context, elements, filteredNonEmptyErrors]); + + const initialUIState = useMemo(() => { + return prepareInitialUIState(elements || [], context || {}, isRevision); + }, [elements, context, isRevision]); + + // Breadcrumbs now using scrollIntoView method to make sure that breadcrumb is always in viewport. + // Due to dynamic dimensions of logo it doesnt work well if scroll happens before logo is loaded. + // This workaround is needed to wait for logo to be loaded so scrollIntoView will work with correct dimensions of page. + const [isLogoLoaded, setLogoLoaded] = useState(customer?.logoImageUri ? false : true); + + useEffect(() => { + if (!customer?.logoImageUri) return; + + // Resseting loaded state in case of logo change + setLogoLoaded(false); + }, [customer?.logoImageUri]); + + if (initialContext?.flowConfig?.appState === 'approved') return ; + + if (initialContext?.flowConfig?.appState == 'rejected') return ; + + return definition && context ? ( + + + {({ state, stateApi, payload }) => + state === 'finish' ? ( + + ) : ( + + {({ currentPage }) => { + return currentPage ? ( + + + { + tools.setElementCompleted(prevState, true); + + set( + stateApi.getContext(), + `flowConfig.stepsProgress.${prevState}.isCompleted`, + true, + ); + await stateApi.invokePlugin('sync_workflow_runtime'); + }} + > + + + +
+
+
+ +
+ +
+
+
+ {customer?.logoImageUri && ( + setLogoLoaded(true)} + /> + )} +
+
+ {isLogoLoaded ? : null} +
+
+ {customer?.displayName && ( +
+ { + t('contact', { + companyName: customer.displayName, + }) as string + } +
+ )} + +
+
+
+
+ + + {localStorage.getItem('devmode') ? ( +
+ DEBUG +
+ {currentPage + ? currentPage.stateName + : 'Page not found and state ' + state} +
+
+ + +
+
+ ) : null} +
+
+ page?.stateName === state) ?? + 0) + 1 + } + totalSteps={elements?.length ?? 0} + /> + +
+
+ + {localStorage.getItem('devmode') ? ( +
+
+ +
+ +
+ ) : ( + + )} +
+
+
+
+
+
+
+
+
+
+ ) : null; + }} +
+ ) + } +
+
+ ) : null; +}); diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/SubmitButton.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/SubmitButton.tsx new file mode 100644 index 0000000000..1c9cb97d69 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/SubmitButton.tsx @@ -0,0 +1,64 @@ +import { usePageResolverContext } from '@/components/organisms/DynamicUI/PageResolver/hooks/usePageResolverContext'; +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { useDynamicUIContext } from '@/components/organisms/DynamicUI/hooks/useDynamicUIContext'; +import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; +import { useFlowTracking } from '@/hooks/useFlowTracking'; +import { useTouched } from '@/pages/CollectionFlowV2/hocs/withConnectedField'; +import { useIsDisabledState } from '@/pages/CollectionFlowV2/hooks/useDisabledState'; +import { useIsLoadingState } from '@/pages/CollectionFlowV2/hooks/useIsLoadingState'; +import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; +import { IUIComponentProps } from '@/pages/CollectionFlowV2/types'; +import { Button, createTestId } from '@ballerine/ui'; +import { FunctionComponent, useCallback, useMemo } from 'react'; + +export interface ISubmitButtonOptions { + text: string; +} + +export const SubmitButton: FunctionComponent> = ({ + definition, + uiElementProps, + stack, + options, +}) => { + const { text } = options; + const { isPluginLoading, payload } = useStateManagerContext(); + const uiElement = useUIElement(definition, payload); + const { onClick } = uiElementProps; + + const { state } = useDynamicUIContext(); + const isLoading = useIsLoadingState(uiElement); + const isDisabled = useIsDisabledState(uiElement, payload); + + const { currentPage, pages } = usePageResolverContext(); + + const { errors, validate } = useValidator(); + const isValid = useMemo(() => !Object.values(errors).length, [errors]); + + const { touchPageElements } = useTouched(uiElement, currentPage!); + + const { trackFinish } = useFlowTracking(); + + const handleClick = useCallback(() => { + touchPageElements(); + validate(); + onClick(); + + const isFinishPage = currentPage?.name === pages.at(-1)?.name; + + if (isFinishPage && isValid) { + trackFinish(); + } + }, [currentPage, pages, state, isValid, touchPageElements, onClick, trackFinish, validate]); + + return ( + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/helpers.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/helpers.ts new file mode 100644 index 0000000000..13af46fb71 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/helpers.ts @@ -0,0 +1,67 @@ +import { ARRAY_VALUE_INDEX_PLACEHOLDER } from '@/common/consts/consts'; +import { DocumentFieldParams } from '@/components/organisms/UIRenderer/elements/JSONForm/components/DocumentField'; +import { UIElementDefinition, UIPage } from '@/domains/collection-flow'; +import { AnyObject } from '@ballerine/ui'; + +export const getElementByValueDestination = ( + destination: string, + page: UIPage, +): UIElementDefinition | null => { + const arrayIndexRegex = /[(\d)]/g; + const isIndexValue = destination.match(arrayIndexRegex); + + const findByElementDefinitionByDestination = ( + targetDestination: string, + elements: UIElementDefinition[], + ): UIElementDefinition | null => { + for (const element of elements) { + if (element.valueDestination === targetDestination) return element; + + if (element.elements) { + const foundElement = findByElementDefinitionByDestination( + targetDestination, + element.elements, + ); + if (foundElement) return foundElement; + } + } + + return null; + }; + + if (isIndexValue) { + const originArrayDestinationPath = destination.replace( + /\[(\d+)\]/, + `[${ARRAY_VALUE_INDEX_PLACEHOLDER}]`, + ); + + const element = findByElementDefinitionByDestination(originArrayDestinationPath, page.elements); + return element; + } + + return findByElementDefinitionByDestination(destination, page.elements); +}; + +export const getDocumentElementByDocumentError = ( + id: string, + page: UIPage, +): UIElementDefinition | null => { + const findElement = ( + id: string, + elements: UIElementDefinition[], + ): UIElementDefinition | null => { + for (const element of elements) { + //@ts-ignore + if (element.options?.documentData?.id === id.replace('document-error-', '')) return element; + + if (element.elements) { + const foundInElements = findElement(id, element.elements); + if (foundInElements) return foundInElements; + } + } + + return null; + }; + + return findElement(id, page.elements); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/index.ts new file mode 100644 index 0000000000..bcd94ff82e --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/controls/SubmitButton/index.ts @@ -0,0 +1 @@ +export * from './SubmitButton'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors/FieldErrors.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors/FieldErrors.tsx new file mode 100644 index 0000000000..b8b1556110 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors/FieldErrors.tsx @@ -0,0 +1,25 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { useValidatedInput } from '@/components/providers/Validator/hooks/useValidatedInput'; +import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; +import { UIElementV2 } from '@/components/providers/Validator/types'; +import { useStack } from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider'; +import { useTouched } from '@/pages/CollectionFlowV2/hocs/withConnectedField'; +import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; +import { ErrorsList } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface IFieldErrorsProps { + definition: UIElementV2; + stack?: number[]; +} + +export const FieldErrors: FunctionComponent = ({ definition }) => { + const { payload } = useStateManagerContext(); + const { stack } = useStack(); + const uiElement = useUIElement(definition, payload, stack); + const { isTouched } = useTouched(uiElement); + const errors = useValidatedInput(uiElement); + const { errors: _validationErrors } = useValidator(); + + return isTouched && ; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors/index.ts new file mode 100644 index 0000000000..42dab91f50 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors/index.ts @@ -0,0 +1 @@ +export * from './FieldErrors'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout/FieldLayout.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout/FieldLayout.tsx new file mode 100644 index 0000000000..feaf03aaac --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout/FieldLayout.tsx @@ -0,0 +1,41 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { UIElementV2 } from '@/components/providers/Validator/types'; +import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; +import { ctw, Label } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; + +export interface IFieldLayoutBaseParams { + label?: string; +} + +export interface IFieldLayoutProps { + definition: UIElementV2; + stack?: number[]; + children: React.ReactNode[] | React.ReactNode; + className?: string; +} + +export const FieldLayout: FunctionComponent> = ({ + definition, + stack, + children, + className, +}) => { + const { payload } = useStateManagerContext(); + const uiElement = useUIElement(definition, payload, stack); + const isRequired = useMemo(() => uiElement.isRequired(), [uiElement]); + const { label } = definition.options || {}; + + return ( +
+
+ {label && ( + + )} +
+
{children}
+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout/index.ts new file mode 100644 index 0000000000..8a886329ed --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout/index.ts @@ -0,0 +1 @@ +export * from './FieldLayout'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/AutocompleteField/AutocompleteField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/AutocompleteField/AutocompleteField.tsx new file mode 100644 index 0000000000..3ca556c7a3 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/AutocompleteField/AutocompleteField.tsx @@ -0,0 +1,35 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { AutocompleteInput, createTestId } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface IAutocompleteFieldOption { + label: string; + value: string; +} + +export interface IAutocompleteFieldOptions { + placeholder?: string; + options: IAutocompleteFieldOption[]; +} + +export const AutocompleteField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value, onChange, onBlur, disabled } = fieldProps; + const { options = [], placeholder = '' } = _options; + + return ( + + onChange(event.target.value || '')} + onBlur={onBlur} + /> + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/AutocompleteField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/AutocompleteField/index.ts new file mode 100644 index 0000000000..e17490b84b --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/AutocompleteField/index.ts @@ -0,0 +1 @@ +export * from './AutocompleteField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxField/CheckboxField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxField/CheckboxField.tsx new file mode 100644 index 0000000000..618ed66e38 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxField/CheckboxField.tsx @@ -0,0 +1,35 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { Checkbox, createTestId } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface ICheckboxFieldOptions { + label: string; +} + +export const CheckboxField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { value, onChange, onBlur, disabled } = fieldProps; + const { label } = options; + + return ( + + { + onChange(Boolean(e)); + }} + onBlur={onBlur} + /> + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxField/index.ts new file mode 100644 index 0000000000..032e30e3a8 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxField/index.ts @@ -0,0 +1 @@ +export * from './CheckboxField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxList/CheckboxList.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxList/CheckboxList.tsx new file mode 100644 index 0000000000..ffb97b34bf --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxList/CheckboxList.tsx @@ -0,0 +1,53 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { Checkbox, createTestId, ctw } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface ICheckboxListOption { + label: string; + value: string; +} + +export interface ICheckboxListFieldOptions { + options: ICheckboxListOption[]; +} + +export const CheckboxListField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value = [], onChange, disabled } = fieldProps; + const { options = [] } = _options || {}; + + return ( + +
+ {options.map(option => ( + + ))} +
+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxList/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxList/index.ts new file mode 100644 index 0000000000..e6d5425198 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CheckboxList/index.ts @@ -0,0 +1 @@ +export * from './CheckboxList'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CountryField/CountryField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CountryField/CountryField.tsx new file mode 100644 index 0000000000..1696fed2b4 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CountryField/CountryField.tsx @@ -0,0 +1,39 @@ +import { getCountries } from '@/helpers/countries-data'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; + +export interface ICountryFieldOptions { + placeholder?: string; +} + +export const CountryField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { placeholder = '' } = options || {}; + + const dropdownOptions = useMemo(() => { + return getCountries().map(country => ({ + value: country.const, + label: country.title, + })); + }, []); + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CountryField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CountryField/index.ts new file mode 100644 index 0000000000..7b6770fb65 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/CountryField/index.ts @@ -0,0 +1 @@ +export * from './CountryField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DateField/DateField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DateField/DateField.tsx new file mode 100644 index 0000000000..3cbffd78a3 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DateField/DateField.tsx @@ -0,0 +1,65 @@ +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { + createTestId, + DatePickerChangeEvent, + DatePickerInput, + DatePickerValue, + isValidDate, +} from '@ballerine/ui'; +import { FunctionComponent, useCallback } from 'react'; + +export interface IDateFieldOptions { + disableFuture?: boolean; + disablePast?: boolean; + outputFormat?: 'date' | 'iso'; +} + +const defaultOptions: IDateFieldOptions = { + disableFuture: false, + disablePast: false, + outputFormat: undefined, +}; + +export const DateField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options = defaultOptions, stack }) => { + const { onBlur, onChange, value, disabled } = fieldProps; + const { + disableFuture = defaultOptions.disableFuture, + disablePast = defaultOptions.disablePast, + outputFormat = defaultOptions.outputFormat, + } = options; + + const handleChange = useCallback( + (event: DatePickerChangeEvent) => { + const dateValue = event.target.value; + + if (dateValue === null) return onChange(null); + + if (!isValidDate(dateValue)) return; + + onChange(dateValue); + }, + [onChange], + ); + + return ( + + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DateField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DateField/index.ts new file mode 100644 index 0000000000..0e91777402 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DateField/index.ts @@ -0,0 +1 @@ +export * from './DateField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/DocumentField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/DocumentField.tsx new file mode 100644 index 0000000000..cb066b3958 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/DocumentField.tsx @@ -0,0 +1,92 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { FileUploaderField } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField'; +import { useFileRepository } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField/hooks/useFileRepository'; +import { useValidator } from '@/components/providers/Validator/hooks/useValidator'; +import { Document } from '@/domains/collection-flow'; +import { fetchFile } from '@/domains/storage/storage.api'; +import { collectionFlowFileStorage } from '@/pages/CollectionFlow/collection-flow.file-storage'; +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { useDocument } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument'; +import { useDocumentUpload } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload'; +import { useDocuments } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments'; +import { useEventEmmitter } from '@/pages/CollectionFlowV2/hocs/withConnectedField'; +import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { ErrorsList } from '@ballerine/ui'; +import { FunctionComponent, useCallback, useLayoutEffect } from 'react'; + +export type TDocumentFieldValueType = string | undefined; +export interface IDocumentFieldParams { + initialData: Partial; + pageIndex?: number; +} + +export const DocumentField: FunctionComponent< + IFieldComponentProps +> = ({ definition, stack, options, fieldProps }) => { + const { payload } = useStateManagerContext(); + const uiElement = useUIElement(definition, payload, stack); + console.log('doc id', uiElement.getId()); + const documents = useDocuments(uiElement); + const { fileId, setDocument, clearDocument } = useDocument({ + documents, + params: options, + uiElement, + }); + const { fileUploader, isUploading, uploadError } = useDocumentUpload(); + const { validate } = useValidator(); + + const emitEvent = useEventEmmitter(uiElement); + + const { onBlur, disabled } = fieldProps; + useFileRepository(collectionFlowFileStorage, fileId); + + useLayoutEffect(() => { + if (!fileId) return; + + const persistedFile = collectionFlowFileStorage.getFileById(fileId); + + if (persistedFile) return; + + void fetchFile(fileId).then(file => { + const createdFile = new File([''], file.fileNameInBucket || file.fileNameOnDisk || '', { + type: 'text/plain', + }); + + collectionFlowFileStorage.registerFile(fileId, createdFile); + }); + }, [fileId]); + + const handleChange = useCallback( + (fileId: string) => { + setDocument(fileId); + emitEvent('onChange'); + validate(); + }, + [setDocument, emitEvent, validate], + ); + + return ( + + void} + testId={uiElement.getId()} + onChange={handleChange} + /> + {/* {!!warnings.length && err.message)} />} + {isTouched && !!validationErrors.length && ( + error.message)} /> + )} */} + {uploadError && } + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument/index.ts new file mode 100644 index 0000000000..75d28b795e --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument/index.ts @@ -0,0 +1 @@ +export * from './useDocument'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument/useDocument.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument/useDocument.ts new file mode 100644 index 0000000000..9033fa6400 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocument/useDocument.ts @@ -0,0 +1,98 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { Document } from '@/domains/collection-flow'; +import { IDocumentFieldParams } from '@/pages/CollectionFlowV2/components/ui/fields/DocumentField/DocumentField'; +import remove from 'lodash/remove'; +import set from 'lodash/set'; +import { useCallback, useMemo } from 'react'; + +interface IUseDocumentParams { + documents: Document[]; + params: IDocumentFieldParams; + uiElement: UIElement; +} + +export const useDocument = ({ documents, params, uiElement }: IUseDocumentParams) => { + const { stateApi } = useStateManagerContext(); + + const document = useMemo(() => { + return documents?.find(document => document.id === params.initialData.id); + }, [documents, params]); + + const setDocument = useCallback( + (fileId: string) => { + const { initialData, pageIndex = 0 } = params; + + if (!document) { + const newDocument = structuredClone(initialData); + if (!newDocument.pages) { + newDocument.pages = []; + } + + newDocument.pages[pageIndex] = { ballerineFileId: fileId }; + + const newDocuments = [...documents, newDocument]; + + const context = stateApi.getContext(); + set(context, uiElement.getValueDestination(), newDocuments); + + stateApi.setContext(context); + } else { + document.pages![pageIndex] = { ballerineFileId: fileId }; + + const newDocuments = documents.map(document => { + if (document.id === params.initialData.id) { + return { ...document }; + } + + return document; + }); + + const context = stateApi.getContext(); + set(context, uiElement.getValueDestination(), newDocuments); + + stateApi.setContext(context); + } + }, + [params, uiElement, document, documents, stateApi], + ); + + const clearDocument = useCallback(() => { + if (!document) return; + + const newDocument = structuredClone(document); + const context = stateApi.getContext(); + + // removing document page at pageIndex + remove(newDocument?.pages || [], (page, index) => index === (params.pageIndex || 0)); + + // if no pages left, remove the document from documents array + if (!newDocument?.pages?.length) { + const newDocuments = remove(documents, document => document.id === newDocument.id); + + set(context, uiElement.getValueDestination(), newDocuments); + + stateApi.setContext(context); + } else { + set( + context, + uiElement.getValueDestination(), + documents.map(document => { + if (document.id === newDocument.id) { + return newDocument; + } + + return document; + }), + ); + + stateApi.setContext(context); + } + }, [params, document, documents, uiElement, stateApi]); + + const fileId = useMemo(() => { + return document?.pages?.[params.pageIndex || 0]?.ballerineFileId; + }, [document, params]); + + return { fileId, setDocument, clearDocument }; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload/index.ts new file mode 100644 index 0000000000..b067342cd0 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload/index.ts @@ -0,0 +1 @@ +export * from './useDocumentUpload'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts new file mode 100644 index 0000000000..a847b8f468 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocumentUpload/useDocumentUpload.ts @@ -0,0 +1,46 @@ +import { UploadFileFn } from '@/components/organisms/UIRenderer/elements/JSONForm/components/FileUploaderField/hooks/useFileUploading/types'; +import { uploadFile } from '@/domains/storage/storage.api'; +import { HTTPError } from 'ky'; +import { useCallback, useState } from 'react'; + +interface IUseDocumentUploadState { + uploadError: Error | null; + isUploading: boolean; +} + +export const useDocumentUpload = () => { + const [state, setState] = useState({ + uploadError: null, + isUploading: false, + }); + + const fileUploader: UploadFileFn = useCallback(async (file: File) => { + setState(prevState => ({ ...prevState, isUploading: true })); + + try { + const { id: fileId } = await uploadFile({ file }); + + setState(prevState => ({ ...prevState, uploadError: null })); + + return { fileId }; + } catch (error) { + if (error instanceof HTTPError) { + console.error('Error uploading file', error); + setState(prevState => ({ ...prevState, uploadError: error })); + + throw error; + } + + setState(prevState => ({ ...prevState, uploadError: new Error('Failed to upload file.') })); + + throw error; + } finally { + setState(prevState => ({ ...prevState, isUploading: false })); + } + }, []); + + return { + ...state, + fileUploader, + }; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments/index.ts new file mode 100644 index 0000000000..f6ff11ffd5 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments/index.ts @@ -0,0 +1 @@ +export * from './useDocuments'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments/useDocuments.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments/useDocuments.ts new file mode 100644 index 0000000000..f5ad224769 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/hooks/useDocuments/useDocuments.ts @@ -0,0 +1,10 @@ +import { UIElement } from '@/components/providers/Validator/hooks/useValidate/ui-element'; +import { useMemo } from 'react'; + +export const useDocuments = (uiElement: UIElement) => { + const documents = useMemo(() => { + return uiElement.getValue() || []; + }, [uiElement]); + + return documents; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/index.ts new file mode 100644 index 0000000000..4aa75d4e29 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/DocumentField/index.ts @@ -0,0 +1 @@ +export * from './DocumentField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/FieldList.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/FieldList.tsx new file mode 100644 index 0000000000..4e37a4ca88 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/FieldList.tsx @@ -0,0 +1,48 @@ +import { useFieldList } from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList'; +import { + StackProvider, + useStack, +} from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider'; +import { rendererSchema } from '@/pages/CollectionFlowV2/renderer-schema'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { AnyObject, Button, Renderer } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export type TFieldListValueType = T[]; + +export interface IFieldListOptions { + defaultValue: AnyObject; + addButtonLabel?: string; + removeButtonLabel?: string; +} + +export const FieldList: FunctionComponent< + IFieldComponentProps, IFieldListOptions> +> = props => { + const { stack } = useStack(); + const { definition, options } = props; + const { addButtonLabel = 'Add Item', removeButtonLabel = 'Remove' } = options || {}; + const { items, addItem, removeItem } = useFieldList(props); + + return ( +
+ {items.map((item, index) => { + return ( +
+
+ removeItem(index)}> + {removeButtonLabel} + +
+ + + +
+ ); + })} +
+ +
+
+ ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList/index.ts new file mode 100644 index 0000000000..dbf39b30fa --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList/index.ts @@ -0,0 +1 @@ +export * from './useFieldList'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList/useFieldList.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList/useFieldList.ts new file mode 100644 index 0000000000..b467719a58 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/hooks/useFieldList/useFieldList.ts @@ -0,0 +1,49 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { + IFieldListOptions, + TFieldListValueType, +} from '@/pages/CollectionFlowV2/components/ui/fields/FieldList/FieldList'; +import { useUIElement } from '@/pages/CollectionFlowV2/hooks/useUIElement'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import set from 'lodash/set'; +import { useCallback, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +export const useFieldList = ({ + definition, + stack, + fieldProps, + options, +}: IFieldComponentProps, IFieldListOptions>) => { + const { payload, stateApi } = useStateManagerContext(); + const uiElement = useUIElement(definition, payload, stack); + + const items = useMemo(() => (uiElement.getValue() as { _id: string }[]) || [], [uiElement]); + + const addItem = useCallback(() => { + const valueDestination = uiElement.getValueDestination(); + + const newValue = [...items, { _id: uuidv4(), ...options?.defaultValue }]; + set(payload, valueDestination, newValue); + + stateApi.setContext(payload); + }, [uiElement, items, stateApi]); + + const removeItem = useCallback( + (index: number) => { + if (!Array.isArray(items)) return; + + const newValue = items.filter((_, i) => i !== index); + set(payload, uiElement.getValueDestination(), newValue); + + stateApi.setContext(payload); + }, + [uiElement, items, stateApi], + ); + + return { + items, + addItem, + removeItem, + }; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/index.ts new file mode 100644 index 0000000000..9fb4cd2987 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/index.ts @@ -0,0 +1 @@ +export * from './FieldList'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/StackProvider.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/StackProvider.tsx new file mode 100644 index 0000000000..e5339bbad3 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/StackProvider.tsx @@ -0,0 +1,13 @@ +import { FunctionComponent, useMemo } from 'react'; +import { StackProviderContext } from './context/stack-provider-context'; + +export interface IStackProviderProps { + stack?: number[]; + children: React.ReactNode | React.ReactNode[]; +} + +export const StackProvider: FunctionComponent = ({ stack, children }) => { + const context = useMemo(() => ({ stack }), [stack]); + + return {children}; +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/context/stack-provider-context.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/context/stack-provider-context.ts new file mode 100644 index 0000000000..970d1d4200 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/context/stack-provider-context.ts @@ -0,0 +1,6 @@ +import { createContext } from 'react'; +import { IStackProviderContext } from '../types'; + +export const StackProviderContext = createContext({ + stack: [], +}); diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/hooks/useStack/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/hooks/useStack/index.ts new file mode 100644 index 0000000000..9c9318428b --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/hooks/useStack/index.ts @@ -0,0 +1 @@ +export * from './useStack'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.ts new file mode 100644 index 0000000000..3a8bcbda58 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/hooks/useStack/useStack.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { StackProviderContext } from '../../context/stack-provider-context'; + +export const useStack = () => useContext(StackProviderContext); diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/index.ts new file mode 100644 index 0000000000..c7972e4730 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/index.ts @@ -0,0 +1,2 @@ +export * from './hooks/useStack'; +export * from './StackProvider'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/types/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/types/index.ts new file mode 100644 index 0000000000..ae6896c9a7 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/FieldList/providers/StackProvider/types/index.ts @@ -0,0 +1,3 @@ +export interface IStackProviderContext { + stack?: number[]; +} diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/IndustriesField/IndustriesField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/IndustriesField/IndustriesField.tsx new file mode 100644 index 0000000000..9a7f556ebe --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/IndustriesField/IndustriesField.tsx @@ -0,0 +1,45 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface IIndustriesFieldOptions { + placeholder?: string; +} + +export const IndustriesField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { placeholder } = options; + const { value, disabled, onChange, onBlur } = fieldProps; + + const { t } = useTranslation(); + + const translatedIndustries = t('industries', { returnObjects: true }) as string[]; + + const dropdownOptions = useMemo( + () => + translatedIndustries.map(industry => ({ + label: industry, + value: industry, + })), + [translatedIndustries], + ); + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/IndustriesField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/IndustriesField/index.ts new file mode 100644 index 0000000000..9d8a407ae9 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/IndustriesField/index.ts @@ -0,0 +1 @@ +export * from './IndustriesField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/LocaleField/LocaleField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/LocaleField/LocaleField.tsx new file mode 100644 index 0000000000..f43f80e68d --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/LocaleField/LocaleField.tsx @@ -0,0 +1,51 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface ILocaleFieldOptions { + placeholder?: string; +} + +export const LocaleField: FunctionComponent> = ({ + fieldProps, + definition, + options, + stack, +}) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { placeholder = '' } = options || {}; + + const { t } = useTranslation(); + + const dropdownOptions = useMemo( + () => + ( + t('languages', { returnObjects: true }) as Array<{ + const: string; + title: string; + }> + ).map(({ const: constValue, title }) => ({ + label: title, + value: constValue, + })), + [t], + ); + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/LocaleField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/LocaleField/index.ts new file mode 100644 index 0000000000..4a2e5cb805 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/LocaleField/index.ts @@ -0,0 +1 @@ +export * from './LocaleField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/MCCField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/MCCField.tsx new file mode 100644 index 0000000000..34dbe397a4 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/MCCField.tsx @@ -0,0 +1,35 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { mccOptions } from '@/pages/CollectionFlowV2/components/ui/fields/MCCField/options'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export interface IMCCFieldOptions { + placeholder?: string; +} + +export const MCCField: FunctionComponent> = ({ + fieldProps, + definition, + options, + stack, +}) => { + const { placeholder } = options; + const { value, disabled, onChange, onBlur } = fieldProps; + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/index.ts new file mode 100644 index 0000000000..017388ab08 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/index.ts @@ -0,0 +1 @@ +export * from './MCCField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/options.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/options.ts new file mode 100644 index 0000000000..7e443150e8 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MCCField/options.ts @@ -0,0 +1,3941 @@ +export const mccOptions = [ + { + mcc: '0742', + description: 'Veterinary Services', + }, + { + mcc: '0763', + description: 'Agricultural Co-operatives', + }, + { + mcc: '0780', + description: 'Horticultural Services, Landscaping Services', + }, + { + mcc: '1520', + description: 'General Contractors-Residential and Commercial', + }, + { + mcc: '1711', + description: + 'Air Conditioning Contractors – Sales and Installation, Heating Contractors – Sales, Service, Installation', + }, + { + mcc: '1731', + description: 'Electrical Contractors', + }, + { + mcc: '1740', + description: + 'Insulation – Contractors, Masonry, Stonework Contractors, Plastering Contractors, Stonework and Masonry Contractors, Tile Settings Contractors', + }, + { + mcc: '1750', + description: 'Carpentry Contractors', + }, + { + mcc: '1761', + description: 'Roofing – Contractors, Sheet Metal Work – Contractors, Siding – Contractors', + }, + { + mcc: '1771', + description: 'Contractors – Concrete Work', + }, + { + mcc: '1799', + description: 'Contractors – Special Trade, Not Elsewhere Classified', + }, + { + mcc: '2741', + description: 'Miscellaneous Publishing and Printing', + }, + { + mcc: '2791', + description: 'Typesetting, Plate Making, & Related Services', + }, + { + mcc: '2842', + description: 'Specialty Cleaning, Polishing, and Sanitation Preparations', + }, + { + mcc: '3000', + description: 'UNITED AIRLINES', + }, + { + mcc: '3001', + description: 'AMERICAN AIRLINES', + }, + { + mcc: '3002', + description: 'PAN AMERICAN', + }, + { + mcc: '3003', + description: 'Airlines', + }, + { + mcc: '3004', + description: 'TRANS WORLD AIRLINES', + }, + { + mcc: '3005', + description: 'BRITISH AIRWAYS', + }, + { + mcc: '3006', + description: 'JAPAN AIRLINES', + }, + { + mcc: '3007', + description: 'AIR FRANCE', + }, + { + mcc: '3008', + description: 'LUFTHANSA', + }, + { + mcc: '3009', + description: 'AIR CANADA', + }, + { + mcc: '3010', + description: 'KLM (ROYAL DUTCH AIRLINES)', + }, + { + mcc: '3011', + description: 'AEORFLOT', + }, + { + mcc: '3012', + description: 'QANTAS', + }, + { + mcc: '3013', + description: 'ALITALIA', + }, + { + mcc: '3014', + description: 'SAUDIA ARABIAN AIRLINES', + }, + { + mcc: '3015', + description: 'SWISSAIR', + }, + { + mcc: '3016', + description: 'SAS', + }, + { + mcc: '3017', + description: 'SOUTH AFRICAN AIRWAYS', + }, + { + mcc: '3018', + description: 'VARIG (BRAZIL)', + }, + { + mcc: '3019', + description: 'Airlines', + }, + { + mcc: '3020', + description: 'AIR-INDIA', + }, + { + mcc: '3021', + description: 'AIR ALGERIE', + }, + { + mcc: '3022', + description: 'PHILIPPINE AIRLINES', + }, + { + mcc: '3023', + description: 'MEXICANA', + }, + { + mcc: '3024', + description: 'PAKISTAN INTERNATIONAL', + }, + { + mcc: '3025', + description: 'AIR NEW ZEALAND', + }, + { + mcc: '3026', + description: 'Airlines', + }, + { + mcc: '3027', + description: 'UTA/INTERAIR', + }, + { + mcc: '3028', + description: 'AIR MALTA', + }, + { + mcc: '3029', + description: 'SABENA', + }, + { + mcc: '3030', + description: 'AEROLINEAS ARGENTINAS', + }, + { + mcc: '3031', + description: 'OLYMPIC AIRWAYS', + }, + { + mcc: '3032', + description: 'EL AL', + }, + { + mcc: '3033', + description: 'ANSETT AIRLINES', + }, + { + mcc: '3034', + description: 'AUSTRAINLIAN AIRLINES', + }, + { + mcc: '3035', + description: 'TAP (PORTUGAL)', + }, + { + mcc: '3036', + description: 'VASP (BRAZIL)', + }, + { + mcc: '3037', + description: 'EGYPTAIR', + }, + { + mcc: '3038', + description: 'KUWAIT AIRLINES', + }, + { + mcc: '3039', + description: 'AVIANCA', + }, + { + mcc: '3040', + description: 'GULF AIR (BAHRAIN)', + }, + { + mcc: '3041', + description: 'BALKAN-BULGARIAN AIRLINES', + }, + { + mcc: '3042', + description: 'FINNAIR', + }, + { + mcc: '3043', + description: 'AER LINGUS', + }, + { + mcc: '3044', + description: 'AIR LANKA', + }, + { + mcc: '3045', + description: 'NIGERIA AIRWAYS', + }, + { + mcc: '3046', + description: 'CRUZEIRO DO SUL (BRAZIJ)', + }, + { + mcc: '3047', + description: 'THY (TURKEY)', + }, + { + mcc: '3048', + description: 'ROYAL AIR MAROC', + }, + { + mcc: '3049', + description: 'TUNIS AIR', + }, + { + mcc: '3050', + description: 'ICELANDAIR', + }, + { + mcc: '3051', + description: 'AUSTRIAN AIRLINES', + }, + { + mcc: '3052', + description: 'LANCHILE', + }, + { + mcc: '3053', + description: 'AVIACO (SPAIN)', + }, + { + mcc: '3054', + description: 'LADECO (CHILE)', + }, + { + mcc: '3055', + description: 'LAB (BOLIVIA)', + }, + { + mcc: '3056', + description: 'QUEBECAIRE', + }, + { + mcc: '3057', + description: 'EASTWEST AIRLINES (AUSTRALIA)', + }, + { + mcc: '3058', + description: 'DELTA', + }, + { + mcc: '3059', + description: 'Airlines', + }, + { + mcc: '3060', + description: 'NORTHWEST', + }, + { + mcc: '3061', + description: 'CONTINENTAL', + }, + { + mcc: '3062', + description: 'WESTERN', + }, + { + mcc: '3063', + description: 'US AIR', + }, + { + mcc: '3064', + description: 'Airlines', + }, + { + mcc: '3065', + description: 'AIRINTER', + }, + { + mcc: '3066', + description: 'SOUTHWEST', + }, + { + mcc: '3067', + description: 'Airlines', + }, + { + mcc: '3068', + description: 'Airlines', + }, + { + mcc: '3069', + description: 'SUN COUNTRY AIRLINES', + }, + { + mcc: '3070', + description: 'Airlines', + }, + { + mcc: '3071', + description: 'AIR BRITISH COLUBIA', + }, + { + mcc: '3072', + description: 'Airlines', + }, + { + mcc: '3073', + description: 'Airlines', + }, + { + mcc: '3074', + description: 'Airlines', + }, + { + mcc: '3075', + description: 'SINGAPORE AIRLINES', + }, + { + mcc: '3076', + description: 'AEROMEXICO', + }, + { + mcc: '3077', + description: 'THAI AIRWAYS', + }, + { + mcc: '3078', + description: 'CHINA AIRLINES', + }, + { + mcc: '3079', + description: 'Airlines', + }, + { + mcc: '3080', + description: 'Airlines', + }, + { + mcc: '3081', + description: 'NORDAIR', + }, + { + mcc: '3082', + description: 'KOREAN AIRLINES', + }, + { + mcc: '3083', + description: 'AIR AFRIGUE', + }, + { + mcc: '3084', + description: 'EVA AIRLINES', + }, + { + mcc: '3085', + description: 'MIDWEST EXPRESS AIRLINES, INC.', + }, + { + mcc: '3086', + description: 'Airlines', + }, + { + mcc: '3087', + description: 'METRO AIRLINES', + }, + { + mcc: '3088', + description: 'CROATIA AIRLINES', + }, + { + mcc: '3089', + description: 'TRANSAERO', + }, + { + mcc: '3090', + description: 'Airlines', + }, + { + mcc: '3091', + description: 'Airlines', + }, + { + mcc: '3092', + description: 'Airlines', + }, + { + mcc: '3093', + description: 'Airlines', + }, + { + mcc: '3094', + description: 'ZAMBIA AIRWAYS', + }, + { + mcc: '3095', + description: 'Airlines', + }, + { + mcc: '3096', + description: 'AIR ZIMBABWE', + }, + { + mcc: '3097', + description: 'Airlines', + }, + { + mcc: '3098', + description: 'Airlines', + }, + { + mcc: '3099', + description: 'CATHAY PACIFIC', + }, + { + mcc: '3100', + description: 'MALAYSIAN AIRLINE SYSTEM', + }, + { + mcc: '3101', + description: 'Airlines', + }, + { + mcc: '3102', + description: 'IBERIA', + }, + { + mcc: '3103', + description: 'GARUDA (INDONESIA)', + }, + { + mcc: '3104', + description: 'Airlines', + }, + { + mcc: '3105', + description: 'Airlines', + }, + { + mcc: '3106', + description: 'BRAATHENS S.A.F.E. (NORWAY)', + }, + { + mcc: '3107', + description: 'Airlines', + }, + { + mcc: '3108', + description: 'Airlines', + }, + { + mcc: '3109', + description: 'Airlines', + }, + { + mcc: '3110', + description: 'WINGS AIRWAYS', + }, + { + mcc: '3111', + description: 'BRITISH MIDLAND', + }, + { + mcc: '3112', + description: 'WINDWARD ISLAND', + }, + { + mcc: '3113', + description: 'Airlines', + }, + { + mcc: '3114', + description: 'Airlines', + }, + { + mcc: '3115', + description: 'Airlines', + }, + { + mcc: '3116', + description: 'Airlines', + }, + { + mcc: '3117', + description: 'VIASA', + }, + { + mcc: '3118', + description: 'VALLEY AIRLINES', + }, + { + mcc: '3119', + description: 'Airlines', + }, + { + mcc: '3120', + description: 'Airlines', + }, + { + mcc: '3121', + description: 'Airlines', + }, + { + mcc: '3122', + description: 'Airlines', + }, + { + mcc: '3123', + description: 'Airlines', + }, + { + mcc: '3124', + description: 'Airlines', + }, + { + mcc: '3125', + description: 'TAN', + }, + { + mcc: '3126', + description: 'TALAIR', + }, + { + mcc: '3127', + description: 'TACA INTERNATIONAL', + }, + { + mcc: '3128', + description: 'Airlines', + }, + { + mcc: '3129', + description: 'SURINAM AIRWAYS', + }, + { + mcc: '3130', + description: 'SUN WORLD INTERNATIONAL', + }, + { + mcc: '3131', + description: 'Airlines', + }, + { + mcc: '3132', + description: 'Airlines', + }, + { + mcc: '3133', + description: 'SUNBELT AIRLINES', + }, + { + mcc: '3134', + description: 'Airlines', + }, + { + mcc: '3135', + description: 'SUDAN AIRWAYS', + }, + { + mcc: '3136', + description: 'Airlines', + }, + { + mcc: '3137', + description: 'SINGLETON', + }, + { + mcc: '3138', + description: 'SIMMONS AIRLINES', + }, + { + mcc: '3139', + description: 'Airlines', + }, + { + mcc: '3140', + description: 'Airlines', + }, + { + mcc: '3141', + description: 'Airlines', + }, + { + mcc: '3142', + description: 'Airlines', + }, + { + mcc: '3143', + description: 'SCENIC AIRLINES', + }, + { + mcc: '3144', + description: 'VIRGIN ATLANTIC', + }, + { + mcc: '3145', + description: 'SAN JUAN AIRLINES', + }, + { + mcc: '3146', + description: 'LUXAIR', + }, + { + mcc: '3147', + description: 'Airlines', + }, + { + mcc: '3148', + description: 'Airlines', + }, + { + mcc: '3149', + description: 'Airlines', + }, + { + mcc: '3150', + description: 'Airlines', + }, + { + mcc: '3151', + description: 'AIR ZAIRE', + }, + { + mcc: '3152', + description: 'Airlines', + }, + { + mcc: '3153', + description: 'Airlines', + }, + { + mcc: '3154', + description: 'PRINCEVILLE', + }, + { + mcc: '3155', + description: 'Airlines', + }, + { + mcc: '3156', + description: 'Airlines', + }, + { + mcc: '3157', + description: 'Airlines', + }, + { + mcc: '3158', + description: 'Airlines', + }, + { + mcc: '3159', + description: 'PBA', + }, + { + mcc: '3160', + description: 'Airlines', + }, + { + mcc: '3161', + description: 'ALL NIPPON AIRWAYS', + }, + { + mcc: '3162', + description: 'Airlines', + }, + { + mcc: '3163', + description: 'Airlines', + }, + { + mcc: '3164', + description: 'NORONTAIR', + }, + { + mcc: '3165', + description: 'NEW YORK HELICOPTER', + }, + { + mcc: '3166', + description: 'Airlines', + }, + { + mcc: '3167', + description: 'Airlines', + }, + { + mcc: '3168', + description: 'Airlines', + }, + { + mcc: '3169', + description: 'Airlines', + }, + { + mcc: '3170', + description: 'NOUNT COOK', + }, + { + mcc: '3171', + description: 'CANADIAN AIRLINES INTERNATIONAL', + }, + { + mcc: '3172', + description: 'NATIONAIR', + }, + { + mcc: '3173', + description: 'Airlines', + }, + { + mcc: '3174', + description: 'Airlines', + }, + { + mcc: '3175', + description: 'Airlines', + }, + { + mcc: '3176', + description: 'METROFLIGHT AIRLINES', + }, + { + mcc: '3177', + description: 'Airlines', + }, + { + mcc: '3178', + description: 'MESA AIR', + }, + { + mcc: '3179', + description: 'Airlines', + }, + { + mcc: '3180', + description: 'Airlines', + }, + { + mcc: '3181', + description: 'MALEV', + }, + { + mcc: '3182', + description: 'LOT (POLAND)', + }, + { + mcc: '3183', + description: 'Airlines', + }, + { + mcc: '3184', + description: 'LIAT', + }, + { + mcc: '3185', + description: 'LAV (VENEZUELA)', + }, + { + mcc: '3186', + description: 'LAP (PARAGUAY)', + }, + { + mcc: '3187', + description: 'LACSA (COSTA RICA)', + }, + { + mcc: '3188', + description: 'Airlines', + }, + { + mcc: '3189', + description: 'Airlines', + }, + { + mcc: '3190', + description: 'JUGOSLAV AIR', + }, + { + mcc: '3191', + description: 'ISLAND AIRLINES', + }, + { + mcc: '3192', + description: 'IRAN AIR', + }, + { + mcc: '3193', + description: 'INDIAN AIRLINES', + }, + { + mcc: '3194', + description: 'Airlines', + }, + { + mcc: '3195', + description: 'Airlines', + }, + { + mcc: '3196', + description: 'HAWAIIAN AIR', + }, + { + mcc: '3197', + description: 'HAVASU AIRLINES', + }, + { + mcc: '3198', + description: 'Airlines', + }, + { + mcc: '3199', + description: 'Airlines', + }, + { + mcc: '3200', + description: 'FUYANA AIRWAYS', + }, + { + mcc: '3201', + description: 'Airlines', + }, + { + mcc: '3202', + description: 'Airlines', + }, + { + mcc: '3203', + description: 'GOLDEN PACIFIC AIR', + }, + { + mcc: '3204', + description: 'FREEDOM AIR', + }, + { + mcc: '3205', + description: 'Airlines', + }, + { + mcc: '3206', + description: 'Airlines', + }, + { + mcc: '3207', + description: 'Airlines', + }, + { + mcc: '3208', + description: 'Airlines', + }, + { + mcc: '3209', + description: 'Airlines', + }, + { + mcc: '3210', + description: 'Airlines', + }, + { + mcc: '3211', + description: 'Airlines', + }, + { + mcc: '3212', + description: 'DOMINICANA', + }, + { + mcc: '3213', + description: 'Airlines', + }, + { + mcc: '3214', + description: 'Airlines', + }, + { + mcc: '3215', + description: 'DAN AIR SERVICES', + }, + { + mcc: '3216', + description: 'CUMBERLAND AIRLINES', + }, + { + mcc: '3217', + description: 'CSA', + }, + { + mcc: '3218', + description: 'CROWN AIR', + }, + { + mcc: '3219', + description: 'COPA', + }, + { + mcc: '3220', + description: 'COMPANIA FAUCETT', + }, + { + mcc: '3221', + description: 'TRANSPORTES AEROS MILITARES ECCUATORANOS', + }, + { + mcc: '3222', + description: 'COMMAND AIRWAYS', + }, + { + mcc: '3223', + description: 'COMAIR', + }, + { + mcc: '3224', + description: 'Airlines', + }, + { + mcc: '3225', + description: 'Airlines', + }, + { + mcc: '3226', + description: 'Airlines', + }, + { + mcc: '3227', + description: 'Airlines', + }, + { + mcc: '3228', + description: 'CAYMAN AIRWAYS', + }, + { + mcc: '3229', + description: 'SAETA SOCIAEDAD ECUATORIANOS DE TRANSPORTES AEREOS', + }, + { + mcc: '3230', + description: 'Airlines', + }, + { + mcc: '3231', + description: 'SASHA SERVICIO AERO DE HONDURAS', + }, + { + mcc: '3232', + description: 'Airlines', + }, + { + mcc: '3233', + description: 'CAPITOL AIR', + }, + { + mcc: '3234', + description: 'BWIA', + }, + { + mcc: '3235', + description: 'BROKWAY AIR', + }, + { + mcc: '3236', + description: 'Airlines', + }, + { + mcc: '3237', + description: 'Airlines', + }, + { + mcc: '3238', + description: 'BEMIDJI AIRLINES', + }, + { + mcc: '3239', + description: 'BAR HARBOR AIRLINES', + }, + { + mcc: '3240', + description: 'BAHAMASAIR', + }, + { + mcc: '3241', + description: 'AVIATECA (GUATEMALA)', + }, + { + mcc: '3242', + description: 'AVENSA', + }, + { + mcc: '3243', + description: 'AUSTRIAN AIR SERVICE', + }, + { + mcc: '3244', + description: 'Airlines', + }, + { + mcc: '3245', + description: 'Airlines', + }, + { + mcc: '3246', + description: 'Airlines', + }, + { + mcc: '3247', + description: 'Airlines', + }, + { + mcc: '3248', + description: 'Airlines', + }, + { + mcc: '3249', + description: 'Airlines', + }, + { + mcc: '3250', + description: 'Airlines', + }, + { + mcc: '3251', + description: 'ALOHA AIRLINES', + }, + { + mcc: '3252', + description: 'ALM', + }, + { + mcc: '3253', + description: 'AMERICA WEST', + }, + { + mcc: '3254', + description: 'TRUMP AIRLINE', + }, + { + mcc: '3255', + description: 'Airlines', + }, + { + mcc: '3256', + description: 'ALASKA AIRLINES', + }, + { + mcc: '3257', + description: 'Airlines', + }, + { + mcc: '3258', + description: 'Airlines', + }, + { + mcc: '3259', + description: 'AMERICAN TRANS AIR', + }, + { + mcc: '3260', + description: 'Airlines', + }, + { + mcc: '3261', + description: 'AIR CHINA', + }, + { + mcc: '3262', + description: 'RENO AIR, INC.', + }, + { + mcc: '3263', + description: 'Airlines', + }, + { + mcc: '3264', + description: 'Airlines', + }, + { + mcc: '3265', + description: 'Airlines', + }, + { + mcc: '3266', + description: 'AIR SEYCHELLES', + }, + { + mcc: '3267', + description: 'AIR PANAMA', + }, + { + mcc: '3268', + description: 'Airlines', + }, + { + mcc: '3269', + description: 'Airlines', + }, + { + mcc: '3270', + description: 'Airlines', + }, + { + mcc: '3271', + description: 'Airlines', + }, + { + mcc: '3272', + description: 'Airlines', + }, + { + mcc: '3273', + description: 'Airlines', + }, + { + mcc: '3274', + description: 'Airlines', + }, + { + mcc: '3275', + description: 'Airlines', + }, + { + mcc: '3276', + description: 'Airlines', + }, + { + mcc: '3277', + description: 'Airlines', + }, + { + mcc: '3278', + description: 'Airlines', + }, + { + mcc: '3279', + description: 'Airlines', + }, + { + mcc: '3280', + description: 'AIR JAMAICA', + }, + { + mcc: '3281', + description: 'Airlines', + }, + { + mcc: '3282', + description: 'AIR DJIBOUTI', + }, + { + mcc: '3283', + description: 'Airlines', + }, + { + mcc: '3284', + description: 'AERO VIRGIN ISLANDS', + }, + { + mcc: '3285', + description: 'AERO PERU', + }, + { + mcc: '3286', + description: 'AEROLINEAS NICARAGUENSIS', + }, + { + mcc: '3287', + description: 'AERO COACH AVAIATION', + }, + { + mcc: '3288', + description: 'Airlines', + }, + { + mcc: '3289', + description: 'Airlines', + }, + { + mcc: '3290', + description: 'Airlines', + }, + { + mcc: '3291', + description: 'ARIANA AFGHAN', + }, + { + mcc: '3292', + description: 'CYPRUS AIRWAYS', + }, + { + mcc: '3293', + description: 'ECUATORIANA', + }, + { + mcc: '3294', + description: 'ETHIOPIAN AIRLINES', + }, + { + mcc: '3295', + description: 'KENYA AIRLINES', + }, + { + mcc: '3296', + description: 'Airlines', + }, + { + mcc: '3297', + description: 'Airlines', + }, + { + mcc: '3298', + description: 'AIR MAURITIUS', + }, + { + mcc: '3299', + description: 'WIDERO’S FLYVESELSKAP', + }, + { + mcc: '3351', + description: 'AFFILIATED AUTO RENTAL', + }, + { + mcc: '3352', + description: 'AMERICAN INTL RENT-A-CAR', + }, + { + mcc: '3353', + description: 'BROOKS RENT-A-CAR', + }, + { + mcc: '3354', + description: 'ACTION AUTO RENTAL', + }, + { + mcc: '3355', + description: 'Car Rental', + }, + { + mcc: '3356', + description: 'Car Rental', + }, + { + mcc: '3357', + description: 'HERTZ RENT-A-CAR', + }, + { + mcc: '3358', + description: 'Car Rental', + }, + { + mcc: '3359', + description: 'PAYLESS CAR RENTAL', + }, + { + mcc: '3360', + description: 'SNAPPY CAR RENTAL', + }, + { + mcc: '3361', + description: 'AIRWAYS RENT-A-CAR', + }, + { + mcc: '3362', + description: 'ALTRA AUTO RENTAL', + }, + { + mcc: '3363', + description: 'Car Rental', + }, + { + mcc: '3364', + description: 'AGENCY RENT-A-CAR', + }, + { + mcc: '3365', + description: 'Car Rental', + }, + { + mcc: '3366', + description: 'BUDGET RENT-A-CAR', + }, + { + mcc: '3367', + description: 'Car Rental', + }, + { + mcc: '3368', + description: 'HOLIDAY RENT-A-WRECK', + }, + { + mcc: '3369', + description: 'Car Rental', + }, + { + mcc: '3370', + description: 'RENT-A-WRECK', + }, + { + mcc: '3371', + description: 'Car Rental', + }, + { + mcc: '3372', + description: 'Car Rental', + }, + { + mcc: '3373', + description: 'Car Rental', + }, + { + mcc: '3374', + description: 'Car Rental', + }, + { + mcc: '3375', + description: 'Car Rental', + }, + { + mcc: '3376', + description: 'AJAX RENT-A-CAR', + }, + { + mcc: '3377', + description: 'Car Rental', + }, + { + mcc: '3378', + description: 'Car Rental', + }, + { + mcc: '3379', + description: 'Car Rental', + }, + { + mcc: '3380', + description: 'Car Rental', + }, + { + mcc: '3381', + description: 'EUROP CAR', + }, + { + mcc: '3382', + description: 'Car Rental', + }, + { + mcc: '3383', + description: 'Car Rental', + }, + { + mcc: '3384', + description: 'Car Rental', + }, + { + mcc: '3385', + description: 'TROPICAL RENT-A-CAR', + }, + { + mcc: '3386', + description: 'SHOWCASE RENTAL CARS', + }, + { + mcc: '3387', + description: 'ALAMO RENT-A-CAR', + }, + { + mcc: '3388', + description: 'Car Rental', + }, + { + mcc: '3389', + description: 'AVIS RENT-A-CAR', + }, + { + mcc: '3390', + description: 'DOLLAR RENT-A-CAR', + }, + { + mcc: '3391', + description: 'EUROPE BY CAR', + }, + { + mcc: '3392', + description: 'Car Rental', + }, + { + mcc: '3393', + description: 'NATIONAL CAR RENTAL', + }, + { + mcc: '3394', + description: 'KEMWELL GROUP RENT-A-CAR', + }, + { + mcc: '3395', + description: 'THRIFTY RENT-A-CAR', + }, + { + mcc: '3396', + description: 'TILDEN TENT-A-CAR', + }, + { + mcc: '3397', + description: 'Car Rental', + }, + { + mcc: '3398', + description: 'ECONO-CAR RENT-A-CAR', + }, + { + mcc: '3399', + description: 'Car Rental', + }, + { + mcc: '3400', + description: 'AUTO HOST COST CAR RENTALS', + }, + { + mcc: '3401', + description: 'Car Rental', + }, + { + mcc: '3402', + description: 'Car Rental', + }, + { + mcc: '3403', + description: 'Car Rental', + }, + { + mcc: '3404', + description: 'Car Rental', + }, + { + mcc: '3405', + description: 'ENTERPRISE RENT-A-CAR', + }, + { + mcc: '3406', + description: 'Car Rental', + }, + { + mcc: '3407', + description: 'Car Rental', + }, + { + mcc: '3408', + description: 'Car Rental', + }, + { + mcc: '3409', + description: 'GENERAL RENT-A-CAR', + }, + { + mcc: '3410', + description: 'Car Rental', + }, + { + mcc: '3411', + description: 'Car Rental', + }, + { + mcc: '3412', + description: 'A-1 RENT-A-CAR', + }, + { + mcc: '3413', + description: 'Car Rental', + }, + { + mcc: '3414', + description: 'GODFREY NATL RENT-A-CAR', + }, + { + mcc: '3415', + description: 'Car Rental', + }, + { + mcc: '3416', + description: 'Car Rental', + }, + { + mcc: '3417', + description: 'Car Rental', + }, + { + mcc: '3418', + description: 'Car Rental', + }, + { + mcc: '3419', + description: 'ALPHA RENT-A-CAR', + }, + { + mcc: '3420', + description: 'ANSA INTL RENT-A-CAR', + }, + { + mcc: '3421', + description: 'ALLSTAE RENT-A-CAR', + }, + { + mcc: '3422', + description: 'Car Rental', + }, + { + mcc: '3423', + description: 'AVCAR RENT-A-CAR', + }, + { + mcc: '3424', + description: 'Car Rental', + }, + { + mcc: '3425', + description: 'AUTOMATE RENT-A-CAR', + }, + { + mcc: '3426', + description: 'Car Rental', + }, + { + mcc: '3427', + description: 'AVON RENT-A-CAR', + }, + { + mcc: '3428', + description: 'CAREY RENT-A-CAR', + }, + { + mcc: '3429', + description: 'INSURANCE RENT-A-CAR', + }, + { + mcc: '3430', + description: 'MAJOR RENT-A-CAR', + }, + { + mcc: '3431', + description: 'REPLACEMENT RENT-A-CAR', + }, + { + mcc: '3432', + description: 'RESERVE RENT-A-CAR', + }, + { + mcc: '3433', + description: 'UGLY DUCKLING RENT-A-CAR', + }, + { + mcc: '3434', + description: 'USA RENT-A-CAR', + }, + { + mcc: '3435', + description: 'VALUE RENT-A-CAR', + }, + { + mcc: '3436', + description: 'AUTOHANSA RENT-A-CAR', + }, + { + mcc: '3437', + description: 'CITE RENT-A-CAR', + }, + { + mcc: '3438', + description: 'INTERENT RENT-A-CAR', + }, + { + mcc: '3439', + description: 'MILLEVILLE RENT-A-CAR', + }, + { + mcc: '3440', + description: 'VIA ROUTE RENT-A-CAR', + }, + { + mcc: '3441', + description: 'Car Rental', + }, + { + mcc: '3501', + description: 'HOLIDAY INNS, HOLIDAY INN EXPRESS', + }, + { + mcc: '3502', + description: 'BEST WESTERN HOTELS', + }, + { + mcc: '3503', + description: 'SHERATON HOTELS', + }, + { + mcc: '3504', + description: 'HILTON HOTELS', + }, + { + mcc: '3505', + description: 'FORTE HOTELS', + }, + { + mcc: '3506', + description: 'GOLDEN TULIP HOTELS', + }, + { + mcc: '3507', + description: 'FRIENDSHIP INNS', + }, + { + mcc: '3508', + description: 'QUALITY INNS, QUALITY SUITES', + }, + { + mcc: '3509', + description: 'MARRIOTT HOTELS', + }, + { + mcc: '3510', + description: 'DAYS INN, DAYSTOP', + }, + { + mcc: '3511', + description: 'ARABELLA HOTELS', + }, + { + mcc: '3512', + description: 'INTER-CONTINENTAL HOTELS', + }, + { + mcc: '3513', + description: 'WESTIN HOTELS', + }, + { + mcc: '3514', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3515', + description: 'RODEWAY INNS', + }, + { + mcc: '3516', + description: 'LA QUINTA MOTOR INNS', + }, + { + mcc: '3517', + description: 'AMERICANA HOTELS', + }, + { + mcc: '3518', + description: 'SOL HOTELS', + }, + { + mcc: '3519', + description: 'PULLMAN INTERNATIONAL HOTELS', + }, + { + mcc: '3520', + description: 'MERIDIEN HOTELS', + }, + { + mcc: '3521', + description: 'CREST HOTELS (see FORTE HOTELS)', + }, + { + mcc: '3522', + description: 'TOKYO HOTEL', + }, + { + mcc: '3523', + description: 'PENNSULA HOTEL', + }, + { + mcc: '3524', + description: 'WELCOMGROUP HOTELS', + }, + { + mcc: '3525', + description: 'DUNFEY HOTELS', + }, + { + mcc: '3526', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3527', + description: 'DOWNTOWNER-PASSPORT HOTEL', + }, + { + mcc: '3528', + description: 'RED LION HOTELS, RED LION INNS', + }, + { + mcc: '3529', + description: 'CP HOTELS', + }, + { + mcc: '3530', + description: 'RENAISSANCE HOTELS, STOUFFER HOTELS', + }, + { + mcc: '3531', + description: 'ASTIR HOTELS', + }, + { + mcc: '3532', + description: 'SUN ROUTE HOTELS', + }, + { + mcc: '3533', + description: 'HOTEL IBIS', + }, + { + mcc: '3534', + description: 'SOUTHERN PACIFIC HOTELS', + }, + { + mcc: '3535', + description: 'HILTON INTERNATIONAL', + }, + { + mcc: '3536', + description: 'AMFAC HOTELS', + }, + { + mcc: '3537', + description: 'ANA HOTEL', + }, + { + mcc: '3538', + description: 'CONCORDE HOTELS', + }, + { + mcc: '3539', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3540', + description: 'IBEROTEL HOTELS', + }, + { + mcc: '3541', + description: 'HOTEL OKURA', + }, + { + mcc: '3542', + description: 'ROYAL HOTELS', + }, + { + mcc: '3543', + description: 'FOUR SEASONS HOTELS', + }, + { + mcc: '3544', + description: 'CIGA HOTELS', + }, + { + mcc: '3545', + description: 'SHANGRI-LA INTERNATIONAL', + }, + { + mcc: '3546', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3547', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3548', + description: 'HOTELES MELIA', + }, + { + mcc: '3549', + description: 'AUBERGE DES GOVERNEURS', + }, + { + mcc: '3550', + description: 'REGAL 8 INNS', + }, + { + mcc: '3551', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3552', + description: 'COAST HOTELS', + }, + { + mcc: '3553', + description: 'PARK INNS INTERNATIONAL', + }, + { + mcc: '3554', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3555', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3556', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3557', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3558', + description: 'JOLLY HOTELS', + }, + { + mcc: '3559', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3560', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3561', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3562', + description: 'COMFORT INNS', + }, + { + mcc: '3563', + description: 'JOURNEY’S END MOTLS', + }, + { + mcc: '3564', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3565', + description: 'RELAX INNS', + }, + { + mcc: '3566', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3567', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3568', + description: 'LADBROKE HOTELS', + }, + { + mcc: '3569', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3570', + description: 'FORUM HOTELS', + }, + { + mcc: '3571', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3572', + description: 'MIYAKO HOTELS', + }, + { + mcc: '3573', + description: 'SANDMAN HOTELS', + }, + { + mcc: '3574', + description: 'VENTURE INNS', + }, + { + mcc: '3575', + description: 'VAGABOND HOTELS', + }, + { + mcc: '3576', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3577', + description: 'MANDARIN ORIENTAL HOTEL', + }, + { + mcc: '3578', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3579', + description: 'HOTEL MERCURE', + }, + { + mcc: '3580', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3581', + description: 'DELTA HOTEL', + }, + { + mcc: '3582', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3583', + description: 'SAS HOTELS', + }, + { + mcc: '3584', + description: 'PRINCESS HOTELS INTERNATIONAL', + }, + { + mcc: '3585', + description: 'HUNGAR HOTELS', + }, + { + mcc: '3586', + description: 'SOKOS HOTELS', + }, + { + mcc: '3587', + description: 'DORAL HOTELS', + }, + { + mcc: '3588', + description: 'HELMSLEY HOTELS', + }, + { + mcc: '3589', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3590', + description: 'FAIRMONT HOTELS', + }, + { + mcc: '3591', + description: 'SONESTA HOTELS', + }, + { + mcc: '3592', + description: 'OMNI HOTELS', + }, + { + mcc: '3593', + description: 'CUNARD HOTELS', + }, + { + mcc: '3594', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3595', + description: 'HOSPITALITY INTERNATIONAL', + }, + { + mcc: '3596', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3597', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3598', + description: 'REGENT INTERNATIONAL HOTELS', + }, + { + mcc: '3599', + description: 'PANNONIA HOTELS', + }, + { + mcc: '3600', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3601', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3602', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3603', + description: 'NOAH’S HOTELS', + }, + { + mcc: '3604', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3605', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3606', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3607', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3608', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3609', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3610', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3611', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3612', + description: 'MOVENPICK HOTELS', + }, + { + mcc: '3613', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3614', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3615', + description: 'TRAVELODGE', + }, + { + mcc: '3616', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3617', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3618', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3619', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3620', + description: 'TELFORD INTERNATIONAL', + }, + { + mcc: '3621', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3622', + description: 'MERLIN HOTELS', + }, + { + mcc: '3623', + description: 'DORINT HOTELS', + }, + { + mcc: '3624', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3625', + description: 'HOTLE UNIVERSALE', + }, + { + mcc: '3626', + description: 'PRINCE HOTELS', + }, + { + mcc: '3627', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3628', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3629', + description: 'DAN HOTELS', + }, + { + mcc: '3630', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3631', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3632', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3633', + description: 'RANK HOTELS', + }, + { + mcc: '3634', + description: 'SWISSOTEL', + }, + { + mcc: '3635', + description: 'RESO HOTELS', + }, + { + mcc: '3636', + description: 'SAROVA HOTELS', + }, + { + mcc: '3637', + description: 'RAMADA INNS, RAMADA LIMITED', + }, + { + mcc: '3638', + description: 'HO JO INN, HOWARD JOHNSON', + }, + { + mcc: '3639', + description: 'MOUNT CHARLOTTE THISTLE', + }, + { + mcc: '3640', + description: 'HYATT HOTEL', + }, + { + mcc: '3641', + description: 'SOFITEL HOTELS', + }, + { + mcc: '3642', + description: 'NOVOTEL HOTELS', + }, + { + mcc: '3643', + description: 'STEIGENBERGER HOTELS', + }, + { + mcc: '3644', + description: 'ECONO LODGES', + }, + { + mcc: '3645', + description: 'QUEENS MOAT HOUSES', + }, + { + mcc: '3646', + description: 'SWALLOW HOTELS', + }, + { + mcc: '3647', + description: 'HUSA HOTELS', + }, + { + mcc: '3648', + description: 'DE VERE HOTELS', + }, + { + mcc: '3649', + description: 'RADISSON HOTELS', + }, + { + mcc: '3650', + description: 'RED ROOK INNS', + }, + { + mcc: '3651', + description: 'IMPERIAL LONDON HOTEL', + }, + { + mcc: '3652', + description: 'EMBASSY HOTELS', + }, + { + mcc: '3653', + description: 'PENTA HOTELS', + }, + { + mcc: '3654', + description: 'LOEWS HOTELS', + }, + { + mcc: '3655', + description: 'SCANDIC HOTELS', + }, + { + mcc: '3656', + description: 'SARA HOTELS', + }, + { + mcc: '3657', + description: 'OBEROI HOTELS', + }, + { + mcc: '3658', + description: 'OTANI HOTELS', + }, + { + mcc: '3659', + description: 'TAJ HOTELS INTERNATIONAL', + }, + { + mcc: '3660', + description: 'KNIGHTS INNS', + }, + { + mcc: '3661', + description: 'METROPOLE HOTELS', + }, + { + mcc: '3662', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3663', + description: 'HOTELES EL PRESIDENTS', + }, + { + mcc: '3664', + description: 'FLAG INN', + }, + { + mcc: '3665', + description: 'HAMPTON INNS', + }, + { + mcc: '3666', + description: 'STAKIS HOTELS', + }, + { + mcc: '3667', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3668', + description: 'MARITIM HOTELS', + }, + { + mcc: '3669', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3670', + description: 'ARCARD HOTELS', + }, + { + mcc: '3671', + description: 'ARCTIA HOTELS', + }, + { + mcc: '3672', + description: 'CAMPANIEL HOTELS', + }, + { + mcc: '3673', + description: 'IBUSZ HOTELS', + }, + { + mcc: '3674', + description: 'RANTASIPI HOTELS', + }, + { + mcc: '3675', + description: 'INTERHOTEL CEDOK', + }, + { + mcc: '3676', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3677', + description: 'CLIMAT DE FRANCE HOTELS', + }, + { + mcc: '3678', + description: 'CUMULUS HOTELS', + }, + { + mcc: '3679', + description: 'DANUBIUS HOTEL', + }, + { + mcc: '3680', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3681', + description: 'ADAMS MARK HOTELS', + }, + { + mcc: '3682', + description: 'ALLSTAR INNS', + }, + { + mcc: '3683', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3684', + description: 'BUDGET HOST INNS', + }, + { + mcc: '3685', + description: 'BUDGETEL HOTELS', + }, + { + mcc: '3686', + description: 'SUISSE CHALETS', + }, + { + mcc: '3687', + description: 'CLARION HOTELS', + }, + { + mcc: '3688', + description: 'COMPRI HOTELS', + }, + { + mcc: '3689', + description: 'CONSORT HOTELS', + }, + { + mcc: '3690', + description: 'COURTYARD BY MARRIOTT', + }, + { + mcc: '3691', + description: 'DILLION INNS', + }, + { + mcc: '3692', + description: 'DOUBLETREE HOTELS', + }, + { + mcc: '3693', + description: 'DRURY INNS', + }, + { + mcc: '3694', + description: 'ECONOMY INNS OF AMERICA', + }, + { + mcc: '3695', + description: 'EMBASSY SUITES', + }, + { + mcc: '3696', + description: 'EXEL INNS', + }, + { + mcc: '3697', + description: 'FARFIELD HOTELS', + }, + { + mcc: '3698', + description: 'HARLEY HOTELS', + }, + { + mcc: '3699', + description: 'MIDWAY MOTOR LODGE', + }, + { + mcc: '3700', + description: 'MOTEL 6', + }, + { + mcc: '3701', + description: 'GUEST QUARTERS (Formally PICKETT SUITE HOTELS)', + }, + { + mcc: '3702', + description: 'THE REGISTRY HOTELS', + }, + { + mcc: '3703', + description: 'RESIDENCE INNS', + }, + { + mcc: '3704', + description: 'ROYCE HOTELS', + }, + { + mcc: '3705', + description: 'SANDMAN INNS', + }, + { + mcc: '3706', + description: 'SHILO INNS', + }, + { + mcc: '3707', + description: 'SHONEY’S INNS', + }, + { + mcc: '3708', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3709', + description: 'SUPER8 MOTELS', + }, + { + mcc: '3710', + description: 'THE RITZ CARLTON HOTELS', + }, + { + mcc: '3711', + description: 'FLAG INNS (AUSRALIA)', + }, + { + mcc: '3712', + description: 'GOLDEN CHAIN HOTEL', + }, + { + mcc: '3713', + description: 'QUALITY PACIFIC HOTEL', + }, + { + mcc: '3714', + description: 'FOUR SEASONS HOTEL (AUSTRALIA)', + }, + { + mcc: '3715', + description: 'FARIFIELD INN', + }, + { + mcc: '3716', + description: 'CARLTON HOTELS', + }, + { + mcc: '3717', + description: 'CITY LODGE HOTELS', + }, + { + mcc: '3718', + description: 'KAROS HOTELS', + }, + { + mcc: '3719', + description: 'PROTEA HOTELS', + }, + { + mcc: '3720', + description: 'SOUTHERN SUN HOTELS', + }, + { + mcc: '3721', + description: 'HILTON CONRAD', + }, + { + mcc: '3722', + description: 'WYNDHAM HOTEL AND RESORTS', + }, + { + mcc: '3723', + description: 'RICA HOTELS', + }, + { + mcc: '3724', + description: 'INER NOR HOTELS', + }, + { + mcc: '3725', + description: 'SEAINES PLANATION', + }, + { + mcc: '3726', + description: 'RIO SUITES', + }, + { + mcc: '3727', + description: 'BROADMOOR HOTEL', + }, + { + mcc: '3728', + description: 'BALLY’S HOTEL AND CASINO', + }, + { + mcc: '3729', + description: 'JOHN ASCUAGA’S NUGGET', + }, + { + mcc: '3730', + description: 'MGM GRAND HOTEL', + }, + { + mcc: '3731', + description: 'HARRAH’S HOTELS AND CASINOS', + }, + { + mcc: '3732', + description: 'OPRYLAND HOTEL', + }, + { + mcc: '3733', + description: 'BOCA RATON RESORT', + }, + { + mcc: '3734', + description: 'HARVEY/BRISTOL HOTELS', + }, + { + mcc: '3735', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3736', + description: 'COLORADO BELLE/EDGEWATER RESORT', + }, + { + mcc: '3737', + description: 'RIVIERA HOTEL AND CASINO', + }, + { + mcc: '3738', + description: 'TROPICANA RESORT AND CASINO', + }, + { + mcc: '3739', + description: 'WOODSIDE HOTELS AND RESORTS', + }, + { + mcc: '3740', + description: 'TOWNPLACE SUITES', + }, + { + mcc: '3741', + description: 'MILLENIUM BROADWAY HOTEL', + }, + { + mcc: '3742', + description: 'CLUB MED', + }, + { + mcc: '3743', + description: 'BILTMORE HOTEL AND SUITES', + }, + { + mcc: '3744', + description: 'CAREFREE RESORTS', + }, + { + mcc: '3745', + description: 'ST. REGIS HOTEL', + }, + { + mcc: '3746', + description: 'THE ELIOT HOTEL', + }, + { + mcc: '3747', + description: 'CLUBCORP/CLUB RESORTS', + }, + { + mcc: '3748', + description: 'WELESLEY INNS', + }, + { + mcc: '3749', + description: 'THE BEVERLY HILLS HOTEL', + }, + { + mcc: '3750', + description: 'CROWNE PLAZA HOTELS', + }, + { + mcc: '3751', + description: 'HOMEWOOD SUITES', + }, + { + mcc: '3752', + description: 'PEABODY HOTELS', + }, + { + mcc: '3753', + description: 'GREENBRIAH RESORTS', + }, + { + mcc: '3754', + description: 'AMELIA ISLAND PLANATION', + }, + { + mcc: '3755', + description: 'THE HOMESTEAD', + }, + { + mcc: '3756', + description: 'SOUTH SEAS RESORTS', + }, + { + mcc: '3757', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3758', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3759', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3760', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3761', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3762', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3763', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3764', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3765', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3766', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3767', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3768', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3769', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3770', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3771', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3772', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3773', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3774', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3775', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3776', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3777', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3778', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3779', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3780', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3781', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3782', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3783', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3784', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3785', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3786', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3787', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3788', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3789', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3790', + description: 'Hotels/Motels/Inns/Resorts', + }, + { + mcc: '3816', + description: 'Home2Suites', + }, + { + mcc: '3835', + description: '* MASTERS ECONOMY INNS', + }, + { + mcc: '4011', + description: 'Railroads', + }, + { + mcc: '4111', + description: + 'Local/Suburban Commuter Passenger Transportation – Railroads, Feries, Local Water Transportation.', + }, + { + mcc: '4112', + description: 'Passenger Railways', + }, + { + mcc: '4119', + description: 'Ambulance Services', + }, + { + mcc: '4121', + description: 'Taxicabs and Limousines', + }, + { + mcc: '4131', + description: 'Bus Lines, Including Charters, Tour Buses', + }, + { + mcc: '4214', + description: + 'Motor Freight Carriers, Moving and Storage Companies, Trucking – Local/Long Distance, Delivery Services – Local', + }, + { + mcc: '4215', + description: 'Courier Services – Air or Ground, Freight forwarders', + }, + { + mcc: '4225', + description: 'Public warehousing, Storage', + }, + { + mcc: '4411', + description: 'Cruise and Steamship Lines', + }, + { + mcc: '4457', + description: 'Boat Rentals and Leases', + }, + { + mcc: '4468', + description: 'Marinas, Marine Service, and Supplies', + }, + { + mcc: '4511', + description: 'Airlines, Air Carriers ( not listed elsewhere)', + }, + { + mcc: '4582', + description: 'Airports, Airport Terminals, Flying Fields', + }, + { + mcc: '4722', + description: 'Travel Agencies and Tour Operations', + }, + { + mcc: '4723', + description: 'Package Tour Operators (For use in Germany only)', + }, + { + mcc: '4784', + description: 'Toll and Bridge Fees', + }, + { + mcc: '4789', + description: 'Transportation Services, Not elsewhere classified)', + }, + { + mcc: '4812', + description: 'Telecommunications Equipment including telephone sales', + }, + { + mcc: '4814', + description: 'Fax services, Telecommunication Services', + }, + { + mcc: '4815', + description: 'VisaPhone', + }, + { + mcc: '4816', + description: 'Computer Network Services', + }, + { + mcc: '4821', + description: 'Telegraph services', + }, + { + mcc: '4829', + description: 'Money Orders – Wire Transfer', + }, + { + mcc: '4899', + description: 'Cable and other pay television (previously Cable Services)', + }, + { + mcc: '4900', + description: 'Electric, Gas, Sanitary and Water Utilities', + }, + { + mcc: '5013', + description: 'Motor vehicle supplies and new parts', + }, + { + mcc: '5021', + description: 'Office and Commercial Furniture', + }, + { + mcc: '5039', + description: 'Construction Materials, Not Elsewhere Classified', + }, + { + mcc: '5044', + description: 'Office, Photographic, Photocopy, and Microfilm Equipment', + }, + { + mcc: '5045', + description: 'Computers, Computer Peripheral Equipment, Software', + }, + { + mcc: '5046', + description: 'Commercial Equipment, Not Elsewhere Classified', + }, + { + mcc: '5047', + description: 'Medical, Dental Ophthalmic, Hospital Equipment and Supplies', + }, + { + mcc: '5051', + description: 'Metal Service Centers and Offices', + }, + { + mcc: '5065', + description: 'Electrical Parts and Equipment', + }, + { + mcc: '5072', + description: 'Hardware Equipment and Supplies', + }, + { + mcc: '5074', + description: 'Plumbing and Heating Equipment and Supplies', + }, + { + mcc: '5085', + description: 'Industrial Supplies, Not Elsewhere Classified', + }, + { + mcc: '5094', + description: 'Precious Stones and Metals, Watches and Jewelry', + }, + { + mcc: '5099', + description: 'Durable Goods, Not Elsewhere Classified', + }, + { + mcc: '5111', + description: 'Stationery, Office Supplies, Printing, and Writing Paper', + }, + { + mcc: '5122', + description: 'Drugs, Drug Proprietors, and Druggist’s Sundries', + }, + { + mcc: '5131', + description: 'Piece Goods, Notions, and Other Dry Goods', + }, + { + mcc: '5137', + description: 'Men’s Women’s and Children’s Uniforms and Commercial Clothing', + }, + { + mcc: '5139', + description: 'Commercial Footwear', + }, + { + mcc: '5169', + description: 'Chemicals and Allied Products, Not Elsewhere Classified', + }, + { + mcc: '5172', + description: 'Petroleum and Petroleum Products', + }, + { + mcc: '5192', + description: 'Books, Periodicals, and Newspapers', + }, + { + mcc: '5193', + description: 'Florists’ Supplies, Nursery Stock and Flowers', + }, + { + mcc: '5198', + description: 'Paints, Varnishes, and Supplies', + }, + { + mcc: '5199', + description: 'Non-durable Goods, Not Elsewhere Classified', + }, + { + mcc: '5200', + description: 'Home Supply Warehouse Stores', + }, + { + mcc: '5211', + description: 'Lumber and Building Materials Stores', + }, + { + mcc: '5231', + description: 'Glass, Paint, and Wallpaper Stores', + }, + { + mcc: '5251', + description: 'Hardware Stores', + }, + { + mcc: '5261', + description: 'Nurseries – Lawn and Garden Supply Store', + }, + { + mcc: '5271', + description: 'Mobile Home Dealers', + }, + { + mcc: '5300', + description: 'Wholesale Clubs', + }, + { + mcc: '5309', + description: 'Duty Free Store', + }, + { + mcc: '5310', + description: 'Discount Stores', + }, + { + mcc: '5311', + description: 'Department Stores', + }, + { + mcc: '5331', + description: 'Variety Stores', + }, + { + mcc: '5399', + description: 'Misc. General Merchandise', + }, + { + mcc: '5411', + description: 'Grocery Stores, Supermarkets', + }, + { + mcc: '5422', + description: 'Meat Provisioners – Freezer and Locker', + }, + { + mcc: '5441', + description: 'Candy, Nut, and Confectionery Stores', + }, + { + mcc: '5451', + description: 'Dairy Products Stores', + }, + { + mcc: '5462', + description: 'Bakeries', + }, + { + mcc: '5499', + description: 'Misc. Food Stores – Convenience Stores and Specialty Markets', + }, + { + mcc: '5511', + description: 'Car and Truck Dealers (New and Used) Sales, Service, Repairs, Parts, and Leasing', + }, + { + mcc: '5521', + description: 'Automobile and Truck Dealers (Used Only)', + }, + { + mcc: '5531', + description: 'Automobile Supply Stores', + }, + { + mcc: '5532', + description: 'Automotive Tire Stores', + }, + { + mcc: '5533', + description: 'Automotive Parts, Accessories Stores', + }, + { + mcc: '5541', + description: 'Service Stations ( with or without ancillary services)', + }, + { + mcc: '5542', + description: 'Automated Fuel Dispensers', + }, + { + mcc: '5551', + description: 'Boat Dealers', + }, + { + mcc: '5561', + description: 'Recreational and Utility Trailers, Camp Dealers', + }, + { + mcc: '5571', + description: 'Motorcycle Dealers', + }, + { + mcc: '5592', + description: 'Motor Home Dealers', + }, + { + mcc: '5598', + description: 'Snowmobile Dealers', + }, + { + mcc: '5599', + description: 'Miscellaneous Auto Dealers ', + }, + { + mcc: '5611', + description: 'Men’s and Boy’s Clothing and Accessories Stores', + }, + { + mcc: '5621', + description: 'Women’s Ready-to-Wear Stores', + }, + { + mcc: '5631', + description: 'Women’s Accessory and Specialty Shops', + }, + { + mcc: '5641', + description: 'Children’s and Infant’s Wear Stores', + }, + { + mcc: '5651', + description: 'Family Clothing Stores', + }, + { + mcc: '5655', + description: 'Sports Apparel, Riding Apparel Stores', + }, + { + mcc: '5661', + description: 'Shoe Stores', + }, + { + mcc: '5681', + description: 'Furriers and Fur Shops', + }, + { + mcc: '5691', + description: 'Men’s and Women’s Clothing Stores', + }, + { + mcc: '5697', + description: 'Tailors, Seamstress, Mending, and Alterations', + }, + { + mcc: '5698', + description: 'Wig and Toupee Stores', + }, + { + mcc: '5699', + description: 'Miscellaneous Apparel and Accessory Shops', + }, + { + mcc: '5712', + description: 'Furniture, Home Furnishings, and Equipment Stores, ExceptAppliances', + }, + { + mcc: '5713', + description: 'Floor Covering Stores', + }, + { + mcc: '5714', + description: 'Drapery, Window Covering and Upholstery Stores', + }, + { + mcc: '5718', + description: 'Fireplace, Fireplace Screens, and Accessories Stores', + }, + { + mcc: '5719', + description: 'Miscellaneous Home Furnishing Specialty Stores', + }, + { + mcc: '5722', + description: 'Household Appliance Stores', + }, + { + mcc: '5732', + description: 'Electronic Sales', + }, + { + mcc: '5733', + description: 'Music Stores, Musical Instruments, Piano Sheet Music', + }, + { + mcc: '5734', + description: 'Computer Software Stores', + }, + { + mcc: '5735', + description: 'Record Shops', + }, + { + mcc: '5811', + description: 'Caterers', + }, + { + mcc: '5812', + description: 'Eating places and Restaurants', + }, + { + mcc: '5813', + description: + 'Drinking Places (Alcoholic Beverages), Bars, Taverns, Cocktail lounges, Nightclubs and Discotheques', + }, + { + mcc: '5814', + description: 'Fast Food Restaurants', + }, + { + mcc: '5815', + description: 'Digital Goods: Media, Books, Movies, Music', + }, + { + mcc: '5816', + description: 'Digital Goods: Games', + }, + { + mcc: '5817', + description: 'Digital Goods: Applications (Excludes Games)', + }, + { + mcc: '5818', + description: 'Digital Goods: Large Digital Goods Merchant', + }, + { + mcc: '5832', + description: 'Antique Shops – Sales, Repairs, and Restoration Services', + }, + { + mcc: '5912', + description: 'Drug Stores and Pharmacies', + }, + { + mcc: '5921', + description: 'Package Stores – Beer, Wine, and Liquor', + }, + { + mcc: '5931', + description: 'Used Merchandise and Secondhand Stores', + }, + { + mcc: '5932', + description: 'Antique Shops', + }, + { + mcc: '5933', + description: 'Pawn Shops and Salvage Yards', + }, + { + mcc: '5935', + description: 'Wrecking and Salvage Yards', + }, + { + mcc: '5937', + description: 'Antique Reproductions', + }, + { + mcc: '5940', + description: 'Bicycle Shops – Sales and Service', + }, + { + mcc: '5941', + description: 'Sporting Goods Stores', + }, + { + mcc: '5942', + description: 'Book Stores', + }, + { + mcc: '5943', + description: 'Stationery Stores, Office and School Supply Stores', + }, + { + mcc: '5944', + description: 'Watch, Clock, Jewelry, and Silverware Stores', + }, + { + mcc: '5945', + description: 'Hobby, Toy, and Game Shops', + }, + { + mcc: '5946', + description: 'Camera and Photographic Supply Stores', + }, + { + mcc: '5947', + description: 'Card Shops, Gift, Novelty, and Souvenir Shops', + }, + { + mcc: '5948', + description: 'Leather Goods Stores', + }, + { + mcc: '5949', + description: 'Sewing, Needle, Fabric, and Price Goods Stores', + }, + { + mcc: '5950', + description: 'Glassware/Crystal Stores', + }, + { + mcc: '5960', + description: 'Direct Marketing- Insurance Service', + }, + { + mcc: '5961', + description: + 'Mail Order Houses Including Catalog Order Stores, Book/Record Clubs (No longer permitted for U.S. original presentments)', + }, + { + mcc: '5962', + description: 'Direct Marketing – Travel Related Arrangements Services', + }, + { + mcc: '5963', + description: 'Door-to-Door Sales', + }, + { + mcc: '5964', + description: 'Direct Marketing – Catalog Merchant', + }, + { + mcc: '5965', + description: 'Direct Marketing – Catalog and Catalog and Retail Merchant', + }, + { + mcc: '5966', + description: 'Direct Marketing- Outbound Telemarketing Merchant', + }, + { + mcc: '5967', + description: 'Direct Marketing – Inbound Teleservices Merchant', + }, + { + mcc: '5968', + description: 'Direct Marketing – Continuity/Subscription Merchant', + }, + { + mcc: '5969', + description: 'Direct Marketing – Not Elsewhere Classified', + }, + { + mcc: '5970', + description: 'Artist’s Supply and Craft Shops', + }, + { + mcc: '5971', + description: 'Art Dealers and Galleries', + }, + { + mcc: '5972', + description: 'Stamp and Coin Stores – Philatelic and Numismatic Supplies', + }, + { + mcc: '5973', + description: 'Religious Goods Stores', + }, + { + mcc: '5975', + description: 'Hearing Aids – Sales, Service, and Supply Stores', + }, + { + mcc: '5976', + description: 'Orthopedic Goods Prosthetic Devices', + }, + { + mcc: '5977', + description: 'Cosmetic Stores', + }, + { + mcc: '5978', + description: 'Typewriter Stores – Sales, Rental, Service', + }, + { + mcc: '5983', + description: 'Fuel – Fuel Oil, Wood, Coal, Liquefied Petroleum', + }, + { + mcc: '5992', + description: 'Florists', + }, + { + mcc: '5993', + description: 'Cigar Stores and Stands', + }, + { + mcc: '5994', + description: 'News Dealers and Newsstands', + }, + { + mcc: '5995', + description: 'Pet Shops, Pet Foods, and Supplies Stores', + }, + { + mcc: '5996', + description: 'Swimming Pools – Sales, Service, and Supplies', + }, + { + mcc: '5997', + description: 'Electric Razor Stores – Sales and Service', + }, + { + mcc: '5998', + description: 'Tent and Awning Shops', + }, + { + mcc: '5999', + description: 'Miscellaneous and Specialty Retail Stores', + }, + { + mcc: '6010', + description: 'Financial Institutions – Manual Cash Disbursements', + }, + { + mcc: '6011', + description: 'Financial Institutions – Manual Cash Disbursements', + }, + { + mcc: '6012', + description: 'Financial Institutions – Merchandise and Services', + }, + { + mcc: '6051', + description: + 'Non-Financial Institutions – Foreign Currency, Money Orders (not wire transfer) and Travelers Cheques', + }, + { + mcc: '6211', + description: 'Security Brokers/Dealers', + }, + { + mcc: '6300', + description: 'Insurance Sales, Underwriting, and Premiums', + }, + { + mcc: '6381', + description: 'Insurance Premiums, (no longer valid for first presentment work)', + }, + { + mcc: '6399', + description: 'Insurance, Not Elsewhere Classified ( no longer valid forfirst presentment work)', + }, + { + mcc: '6513', + description: 'Real Estate Agents and Managers - Rentals', + }, + { + mcc: '7011', + description: + 'Lodging – Hotels, Motels, Resorts, Central Reservation Services (not elsewhere classified)', + }, + { + mcc: '7012', + description: 'Timeshares', + }, + { + mcc: '7032', + description: 'Sporting and Recreational Camps', + }, + { + mcc: '7033', + description: 'Trailer Parks and Camp Grounds', + }, + { + mcc: '7210', + description: 'Laundry, Cleaning, and Garment Services', + }, + { + mcc: '7211', + description: 'Laundry – Family and Commercial', + }, + { + mcc: '7216', + description: 'Dry Cleaners', + }, + { + mcc: '7217', + description: 'Carpet and Upholstery Cleaning', + }, + { + mcc: '7221', + description: 'Photographic Studios', + }, + { + mcc: '7230', + description: 'Barber and Beauty Shops', + }, + { + mcc: '7251', + description: 'Shop Repair Shops and Shoe Shine Parlors, and Hat Cleaning Shops', + }, + { + mcc: '7261', + description: 'Funeral Service and Crematories', + }, + { + mcc: '7273', + description: 'Dating and Escort Services', + }, + { + mcc: '7276', + description: 'Tax Preparation Service', + }, + { + mcc: '7277', + description: 'Counseling Service – Debt, Marriage, Personal', + }, + { + mcc: '7278', + description: 'Buying/Shopping Services, Clubs', + }, + { + mcc: '7296', + description: 'Clothing Rental – Costumes, Formal Wear, Uniforms', + }, + { + mcc: '7297', + description: 'Massage Parlors', + }, + { + mcc: '7298', + description: 'Health and Beauty Shops', + }, + { + mcc: '7299', + description: 'Miscellaneous Personal Services ( not elsewhere classifies)', + }, + { + mcc: '7311', + description: 'Advertising Services', + }, + { + mcc: '7321', + description: 'Consumer Credit Reporting Agencies', + }, + { + mcc: '7332', + description: 'Blueprinting and Photocopying Services', + }, + { + mcc: '7333', + description: 'Commercial Photography, Art and Graphics', + }, + { + mcc: '7338', + description: 'Quick Copy, Reproduction and Blueprinting Services', + }, + { + mcc: '7339', + description: 'Stenographic and Secretarial Support Services', + }, + { + mcc: '7342', + description: 'Exterminating and Disinfecting Services', + }, + { + mcc: '7349', + description: 'Cleaning and Maintenance, Janitorial Services', + }, + { + mcc: '7361', + description: 'Employment Agencies, Temporary Help Services', + }, + { + mcc: '7372', + description: 'Computer Programming, Integrated Systems Design and Data Processing Services', + }, + { + mcc: '7375', + description: 'Information Retrieval Services', + }, + { + mcc: '7379', + description: 'Computer Maintenance and Repair Services, Not Elsewhere Classified', + }, + { + mcc: '7392', + description: 'Management, Consulting, and Public Relations Services', + }, + { + mcc: '7393', + description: 'Protective and Security Services – Including Armored Carsand Guard Dogs', + }, + { + mcc: '7394', + description: + 'Equipment Rental and Leasing Services, Tool Rental, Furniture Rental, and Appliance Rental', + }, + { + mcc: '7395', + description: 'Photofinishing Laboratories, Photo Developing', + }, + { + mcc: '7399', + description: 'Business Services, Not Elsewhere Classified', + }, + { + mcc: '7511', + description: 'Truck Stop', + }, + { + mcc: '7512', + description: 'Car Rental Companies ( Not Listed Below)', + }, + { + mcc: '7513', + description: 'Truck and Utility Trailer Rentals', + }, + { + mcc: '7519', + description: 'Motor Home and Recreational Vehicle Rentals', + }, + { + mcc: '7523', + description: 'Automobile Parking Lots and Garages', + }, + { + mcc: '7531', + description: 'Automotive Body Repair Shops', + }, + { + mcc: '7534', + description: 'Tire Re-treading and Repair Shops', + }, + { + mcc: '7535', + description: 'Paint Shops – Automotive', + }, + { + mcc: '7538', + description: 'Automotive Service Shops', + }, + { + mcc: '7542', + description: 'Car Washes', + }, + { + mcc: '7549', + description: 'Towing Services', + }, + { + mcc: '7622', + description: 'Radio Repair Shops', + }, + { + mcc: '7623', + description: 'Air Conditioning and Refrigeration Repair Shops', + }, + { + mcc: '7629', + description: 'Electrical And Small Appliance Repair Shops', + }, + { + mcc: '7631', + description: 'Watch, Clock, and Jewelry Repair', + }, + { + mcc: '7641', + description: 'Furniture, Furniture Repair, and Furniture Refinishing', + }, + { + mcc: '7692', + description: 'Welding Repair', + }, + { + mcc: '7699', + description: 'Repair Shops and Related Services –Miscellaneous', + }, + { + mcc: '7800', + description: 'Government-Owned Lotteries', + }, + { + mcc: '7801', + description: 'Government-Licensed On-Line Casinos (On-Line Gambling)', + }, + { + mcc: '7802', + description: 'Government-Licensed Horse/Dog Racing', + }, + { + mcc: '7829', + description: 'Motion Pictures and Video Tape Production and Distribution', + }, + { + mcc: '7832', + description: 'Motion Picture Theaters', + }, + { + mcc: '7841', + description: 'Video Tape Rental Stores', + }, + { + mcc: '7911', + description: 'Dance Halls, Studios and Schools', + }, + { + mcc: '7922', + description: 'Theatrical Producers (Except Motion Pictures), Ticket Agencies', + }, + { + mcc: '7929', + description: 'Bands, Orchestras, and Miscellaneous Entertainers (Not Elsewhere Classified)', + }, + { + mcc: '7932', + description: 'Billiard and Pool Establishments', + }, + { + mcc: '7933', + description: 'Bowling Alleys', + }, + { + mcc: '7941', + description: + 'Commercial Sports, Athletic Fields, Professional Sport Clubs, and Sport Promoters', + }, + { + mcc: '7991', + description: 'Tourist Attractions and Exhibits', + }, + { + mcc: '7992', + description: 'Golf Courses – Public', + }, + { + mcc: '7993', + description: 'Video Amusement Game Supplies', + }, + { + mcc: '7994', + description: 'Video Game Arcades/Establishments', + }, + { + mcc: '7995', + description: + 'Betting (including Lottery Tickets, Casino Gaming Chips, Off-track Betting and Wagers at Race Tracks)', + }, + { + mcc: '7996', + description: 'Amusement Parks, Carnivals, Circuses, Fortune Tellers', + }, + { + mcc: '7997', + description: + 'Membership Clubs (Sports, Recreation, Athletic), Country Clubs, and Private Golf Courses', + }, + { + mcc: '7998', + description: 'Aquariums, Sea-aquariums, Dolphinariums', + }, + { + mcc: '7999', + description: 'Recreation Services (Not Elsewhere Classified)', + }, + { + mcc: '8011', + description: 'Doctors and Physicians (Not Elsewhere Classified)', + }, + { + mcc: '8021', + description: 'Dentists and Orthodontists', + }, + { + mcc: '8031', + description: 'Osteopaths', + }, + { + mcc: '8041', + description: 'Chiropractors', + }, + { + mcc: '8042', + description: 'Optometrists and Ophthalmologists', + }, + { + mcc: '8043', + description: 'Opticians, Opticians Goods and Eyeglasses', + }, + { + mcc: '8044', + description: 'Opticians, Optical Goods, and Eyeglasses (no longer validfor first presentments)', + }, + { + mcc: '8049', + description: 'Podiatrists and Chiropodists', + }, + { + mcc: '8050', + description: 'Nursing and Personal Care Facilities', + }, + { + mcc: '8062', + description: 'Hospitals', + }, + { + mcc: '8071', + description: 'Medical and Dental Laboratories', + }, + { + mcc: '8099', + description: 'Medical Services and Health Practitioners (Not Elsewhere Classified)', + }, + { + mcc: '8111', + description: 'Legal Services and Attorneys', + }, + { + mcc: '8211', + description: 'Elementary and Secondary Schools', + }, + { + mcc: '8220', + description: 'Colleges, Junior Colleges, Universities, and ProfessionalSchools', + }, + { + mcc: '8241', + description: 'Correspondence Schools', + }, + { + mcc: '8244', + description: 'Business and Secretarial Schools', + }, + { + mcc: '8249', + description: 'Vocational Schools and Trade Schools', + }, + { + mcc: '8299', + description: 'Schools and Educational Services ( Not Elsewhere Classified)', + }, + { + mcc: '8351', + description: 'Child Care Services', + }, + { + mcc: '8398', + description: 'Charitable and Social Service Organizations', + }, + { + mcc: '8641', + description: 'Civic, Fraternal, and Social Associations', + }, + { + mcc: '8651', + description: 'Political Organizations', + }, + { + mcc: '8661', + description: 'Religious Organizations', + }, + { + mcc: '8675', + description: 'Automobile Associations', + }, + { + mcc: '8699', + description: 'Membership Organizations ( Not Elsewhere Classified)', + }, + { + mcc: '8734', + description: 'Testing Laboratories ( non-medical)', + }, + { + mcc: '8911', + description: 'Architectural – Engineering and Surveying Services', + }, + { + mcc: '8931', + description: 'Accounting, Auditing, and Bookkeeping Services', + }, + { + mcc: '8999', + description: 'Professional Services ( Not Elsewhere Defined)', + }, + { + mcc: '9211', + description: 'Court Costs, including Alimony and Child Support', + }, + { + mcc: '9222', + description: 'Fines', + }, + { + mcc: '9223', + description: 'Bail and Bond Payments', + }, + { + mcc: '9311', + description: 'Tax Payments', + }, + { + mcc: '9399', + description: 'Government Services ( Not Elsewhere Classified)', + }, + { + mcc: '9402', + description: 'Postal Services – Government Only', + }, + { + mcc: '9405', + description: 'Intra – Government Transactions', + }, + { + mcc: '9700', + description: 'Automated Referral Service ( For Visa Only)', + }, + { + mcc: '9701', + description: 'Visa Credential Service ( For Visa Only)', + }, + { + mcc: '9702', + description: 'GCAS Emergency Services ( For Visa Only)', + }, + { + mcc: '9950', + description: 'Intra – Company Purchases ( For Visa Only)', + }, +].map(item => ({ + value: item.mcc, + label: item.description, +})); diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MultiselectField/MultiselectField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MultiselectField/MultiselectField.tsx new file mode 100644 index 0000000000..c61f16b916 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MultiselectField/MultiselectField.tsx @@ -0,0 +1,71 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { + Chip, + createTestId, + MultiSelect, + MultiSelectSelectedItemRenderer, + MultiSelectValue, +} from '@ballerine/ui'; +import { X } from 'lucide-react'; +import { FunctionComponent, useCallback, useMemo } from 'react'; + +export interface IMultiselectFieldOption { + label: string; + value: string; +} + +export interface IMultiselectFieldOptions { + options: IMultiselectFieldOption[]; + placeholder?: string; +} + +export const MultiselectField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { options, placeholder } = _options || {}; + + const multiselectOptions = useMemo( + () => + options.map(option => ({ + title: option.label, + value: option.value, + })), + [options], + ); + + const handleChange = useCallback( + (selected: MultiSelectValue[]) => { + onChange(selected); + }, + [onChange], + ); + + const renderSelected: MultiSelectSelectedItemRenderer = useCallback((params, option) => { + return ( + + + } + /> + + ); + }, []); + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MultiselectField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MultiselectField/index.ts new file mode 100644 index 0000000000..2009deaf54 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/MultiselectField/index.ts @@ -0,0 +1 @@ +export * from './MultiselectField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/NationalityField/NationalityField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/NationalityField/NationalityField.tsx new file mode 100644 index 0000000000..d414d32e31 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/NationalityField/NationalityField.tsx @@ -0,0 +1,46 @@ +import { getNationalities } from '@/helpers/countries-data'; +import { useLanguageParam } from '@/hooks/useLanguageParam/useLanguageParam'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import { FunctionComponent, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface INationalityFieldOptions { + placeholder?: string; +} + +export const NationalityField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options, stack }) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { placeholder = '' } = options || {}; + + const { language } = useLanguageParam(); + const { t } = useTranslation(); + + const nationalities = useMemo(() => getNationalities(language, t), [language, t]); + + const dropdownOptions = useMemo(() => { + return nationalities.map(({ const: constValue, title }) => ({ + label: title, + value: constValue, + })); + }, [nationalities]); + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/NationalityField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/NationalityField/index.ts new file mode 100644 index 0000000000..4600526100 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/NationalityField/index.ts @@ -0,0 +1 @@ +export * from './NationalityField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/PhoneField/PhoneField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/PhoneField/PhoneField.tsx new file mode 100644 index 0000000000..14b5a280c5 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/PhoneField/PhoneField.tsx @@ -0,0 +1,42 @@ +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, PhoneNumberInput } from '@ballerine/ui'; +import { FunctionComponent } from 'react'; + +export type TPhoneFieldValueType = string | undefined; + +export interface IPhoneFieldOptions { + defaultCountry?: string; + enableSearch?: boolean; +} + +const defaultOptions: IPhoneFieldOptions = { + defaultCountry: 'us', + enableSearch: true, +}; + +export const PhoneField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, options = defaultOptions, stack, definition }) => { + const { + defaultCountry = defaultOptions.defaultCountry, + enableSearch = defaultOptions.enableSearch, + } = options; + const { disabled, value, onBlur, onChange } = fieldProps; + + return ( + + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/PhoneField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/PhoneField/index.ts new file mode 100644 index 0000000000..dab745108a --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/PhoneField/index.ts @@ -0,0 +1 @@ +export * from './PhoneField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/RelationshipField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/RelationshipField.tsx new file mode 100644 index 0000000000..ff5a7b2543 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/RelationshipField.tsx @@ -0,0 +1,50 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { relationshipOptions } from '@/pages/CollectionFlowV2/components/ui/fields/RelationshipField/relationship-options'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import get from 'lodash/get'; +import { FunctionComponent, useMemo } from 'react'; + +export interface IRelationshipFieldOptions { + placeholder?: string; + companyNameDestination?: string; + companyName?: string; +} + +const COMPANY_NAME_PLACEHOLDER = '{company_name}'; + +export const RelationshipField: FunctionComponent< + IFieldComponentProps +> = ({ fieldProps, definition, options: _options, stack }) => { + const { value, onChange, onBlur } = fieldProps; + const { + placeholder = '', + companyNameDestination = '', + companyName: _companyName = 'N/A', + } = _options || {}; + + const { payload } = useStateManagerContext(); + const dropdownOptions = useMemo(() => { + const companyName = get(payload, companyNameDestination, _companyName); + + return relationshipOptions.map(option => ({ + label: option.label.replace(COMPANY_NAME_PLACEHOLDER, companyName), + value: option.value.replace(COMPANY_NAME_PLACEHOLDER, companyName), + })); + }, [payload, companyNameDestination, _companyName]); + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/index.ts new file mode 100644 index 0000000000..8040c8b47c --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/index.ts @@ -0,0 +1 @@ +export * from './RelationshipField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/relationship-options.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/relationship-options.ts new file mode 100644 index 0000000000..2623f49949 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/RelationshipField/relationship-options.ts @@ -0,0 +1,31 @@ +export const relationshipOptions = [ + { label: 'Parent of {company_name}', value: 'Parent of {company_name}' }, + { label: 'Subsidiary of {company_name}', value: 'Subsidiary of {company_name}' }, + { label: 'Affiliate with {company_name}', value: 'Affiliate with {company_name}' }, + { + label: 'Joint Venture Partner with {company_name}', + value: 'Joint Venture Partner with {company_name}', + }, + { + label: 'Majority Shareholder in {company_name}', + value: 'Majority Shareholder in {company_name}', + }, + { + label: 'Minority Shareholder in {company_name}', + value: 'Minority Shareholder in {company_name}', + }, + { label: 'Strategic Partner to {company_name}', value: 'Strategic Partner to {company_name}' }, + { + label: 'Institutional Investor in {company_name}', + value: 'Institutional Investor in {company_name}', + }, + { label: 'Angel Investor in {company_name}', value: 'Angel Investor in {company_name}' }, + { + label: 'Venture Capital Investor in {company_name}', + value: 'Venture Capital Investor in {company_name}', + }, + { + label: 'Private Equity Investor in {company_name}', + value: 'Private Equity Investor in {company_name}', + }, +]; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/StateField/StateField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/StateField/StateField.tsx new file mode 100644 index 0000000000..8780abe507 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/StateField/StateField.tsx @@ -0,0 +1,57 @@ +import { useStateManagerContext } from '@/components/organisms/DynamicUI/StateManager/components/StateProvider'; +import { getCountryStates } from '@/helpers/countries-data'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, DropdownInput } from '@ballerine/ui'; +import get from 'lodash/get'; +import { FunctionComponent, useMemo } from 'react'; + +export interface IStateFieldOptions { + placeholder?: string; + countryCodePath?: string; + countryCode?: string; +} + +export const StateField: FunctionComponent> = ({ + fieldProps, + definition, + options = {}, + stack, +}) => { + const { value, disabled, onChange, onBlur } = fieldProps; + const { + placeholder = 'Select State', + countryCodePath = '', + countryCode: _defaultCountryCode = 'US', + } = options; + + const { payload } = useStateManagerContext(); + const dropdownOptions = useMemo(() => { + const countryCode = get(payload, countryCodePath, _defaultCountryCode) as string; + + return ( + countryCode + ? getCountryStates(countryCode).map(state => ({ title: state.name, const: state.isoCode })) + : [] + ).map(option => ({ + label: option.title, + value: option.const, + })); + }, [payload, countryCodePath, _defaultCountryCode]); + + return ( + + + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/StateField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/StateField/index.ts new file mode 100644 index 0000000000..03bc19f8b7 --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/StateField/index.ts @@ -0,0 +1 @@ +export * from './StateField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TagsField/TagsField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TagsField/TagsField.tsx new file mode 100644 index 0000000000..e453b3d31f --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TagsField/TagsField.tsx @@ -0,0 +1,45 @@ +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { Tag, TagInput } from 'emblor'; +import { FunctionComponent, useMemo, useState } from 'react'; + +export interface ITagsFieldOptions { + placeholder?: string; +} + +export const TagsField: FunctionComponent> = ({ + fieldProps, + definition, + options, + stack, +}) => { + const { value, onBlur, onChange } = fieldProps; + const { placeholder } = options || {}; + + const [activeTagIndex, setActiveTagIndex] = useState(null); + + const tags = useMemo(() => { + if (!Array.isArray(value)) return []; + + return value.map((tag, index) => { + return { + id: String(index), + text: String(tag), + } satisfies Tag; + }); + }, [value]); + + return ( + + onChange((tags as Tag[]).map(tag => tag.text))} + tags={tags} + activeTagIndex={activeTagIndex} + setActiveTagIndex={setActiveTagIndex} + placeholder={placeholder} + addTagsOnBlur + /> + + ); +}; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TagsField/index.ts b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TagsField/index.ts new file mode 100644 index 0000000000..ba8821d39e --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TagsField/index.ts @@ -0,0 +1 @@ +export * from './TagsField'; diff --git a/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TextField/TextField.tsx b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TextField/TextField.tsx new file mode 100644 index 0000000000..fcc8e20f1c --- /dev/null +++ b/apps/kyb-app/src/pages/CollectionFlowV2/components/ui/fields/TextField/TextField.tsx @@ -0,0 +1,53 @@ +import { FieldErrors } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldErrors'; +import { FieldLayout } from '@/pages/CollectionFlowV2/components/ui/field-parts/FieldLayout'; +import { serializeTextFieldValue } from '@/pages/CollectionFlowV2/components/ui/fields/TextField/helpers'; +import { IFieldComponentProps } from '@/pages/CollectionFlowV2/types'; +import { createTestId, Input, TextArea } from '@ballerine/ui'; +import { FunctionComponent, useCallback, useMemo } from 'react'; + +export type TTextFieldValueType = string | number | undefined; +export type TTextFieldOptions = { + valueType: 'integer' | 'number' | 'string'; + style: 'text' | 'textarea'; + placeholder?: string; +}; + +export const TextField: FunctionComponent< + IFieldComponentProps +> = ({ options = {} as TTextFieldOptions, definition, stack, fieldProps }) => { + const { valueType = 'string', style = 'text', placeholder } = options; + const { onChange, onBlur, value, disabled } = fieldProps; + const testId = useMemo(() => createTestId(definition, stack), [definition, stack]); + + const handleChange = useCallback( + (event: React.ChangeEvent) => { + const serializedValue = serializeTextFieldValue(event.target.value, valueType); + + onChange(serializedValue); + }, + [onChange, valueType], + ); + + const inputProps = { + value: value || '', + placeholder, + disabled, + onChange: handleChange, + onBlur, + }; + + return ( + + {style === 'textarea' ? ( +