From bb9b5a240a6562e63acfaef5e4d51ebf923e2b70 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Wed, 28 Jan 2026 11:42:04 +0000 Subject: [PATCH 01/10] initial commit --- src/flows/CreateCompany/CreateCompany.tsx | 28 ++ src/flows/CreateCompany/api.ts | 0 .../components/CreateCompanyForm.tsx | 97 +++++++ .../components/SelectCountryStep.tsx | 70 +++++ src/flows/CreateCompany/context.ts | 29 ++ src/flows/CreateCompany/hooks.ts | 270 ++++++++++++++++++ src/flows/CreateCompany/index.ts | 1 + src/flows/CreateCompany/types.ts | 52 ++++ src/flows/CreateCompany/utils.ts | 9 + 9 files changed, 556 insertions(+) create mode 100644 src/flows/CreateCompany/CreateCompany.tsx create mode 100644 src/flows/CreateCompany/api.ts create mode 100644 src/flows/CreateCompany/components/CreateCompanyForm.tsx create mode 100644 src/flows/CreateCompany/components/SelectCountryStep.tsx create mode 100644 src/flows/CreateCompany/context.ts create mode 100644 src/flows/CreateCompany/hooks.ts create mode 100644 src/flows/CreateCompany/index.ts create mode 100644 src/flows/CreateCompany/types.ts create mode 100644 src/flows/CreateCompany/utils.ts diff --git a/src/flows/CreateCompany/CreateCompany.tsx b/src/flows/CreateCompany/CreateCompany.tsx new file mode 100644 index 000000000..7ff131e28 --- /dev/null +++ b/src/flows/CreateCompany/CreateCompany.tsx @@ -0,0 +1,28 @@ + +import { useId, useRef, useMemo } from 'react'; +export const CreateCompanyFlow = ({ + render, + countryCode, + options, +}: CreateCompanyFlowProps) => { + const createCompanyBag = useCreateCompany({ + options, + countryCode, + }); + const formId = useId(); + // Store form's setValue method in ref to allow sibling components + return ( + + {render({ + createCompanyBag, + components: { + SelectCountryStep: SelectCountryStep, + BackButton: OnboardingBack, + SubmitButton: OnboardingSubmit, + }, + })} + + ); +}; diff --git a/src/flows/CreateCompany/api.ts b/src/flows/CreateCompany/api.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/flows/CreateCompany/components/CreateCompanyForm.tsx b/src/flows/CreateCompany/components/CreateCompanyForm.tsx new file mode 100644 index 000000000..e466bfd6e --- /dev/null +++ b/src/flows/CreateCompany/components/CreateCompanyForm.tsx @@ -0,0 +1,97 @@ +import { JSFFields } from '@/src/types/remoteFlows'; +import { JSONSchemaFormFields } from '@/src/components/form/JSONSchemaForm'; +import { Form } from '@/src/components/ui/form'; +import { useForm } from 'react-hook-form'; +import { useJsonSchemasValidationFormResolver } from '@/src/components/form/validationResolver'; +import { BasicInformationFormPayload } from '@/src/flows/Onboarding/types'; +import { Components } from '@/src/types/remoteFlows'; +import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; +import { useEffect } from 'react'; +import { PricingPlanFormPayload } from '@/src/flows/CreateCompany/types'; +import { CreateCompanyContractDetailsFormPayload } from '@/src/flows/CreateCompany/types'; + +type CreateCompanyFormProps = { + onSubmit: ( + payload: + | BasicInformationFormPayload + | PricingPlanFormPayload + | CreateCompanyContractDetailsFormPayload, + ) => Promise; + components?: Components; + fields?: JSFFields; + defaultValues: Record; +}; + +export function CreateCompanyForm({ + defaultValues, + onSubmit, + components, +}: CreateCompanyFormProps) { + const { formId, contractorOnboardingBag, formRef } = + useCreateCompanyContext(); + + const resolver = useJsonSchemasValidationFormResolver( + contractorOnboardingBag.handleValidation, + ); + + const form = useForm({ + resolver, + defaultValues, + shouldUnregister: false, + mode: 'onBlur', + }); + + // Register the form's setValue method with the context so other components can access it + useEffect(() => { + if (formRef?.setValue) { + formRef.setValue.current = form.setValue; + } + }, [form.setValue, formRef]); + + useEffect(() => { + // When the employmentId is set, + // we need to run the checkFieldUpdates to update fieldValues in useStepState + if (contractorOnboardingBag.employmentId) { + contractorOnboardingBag?.checkFieldUpdates(form.getValues()); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const subscription = form?.watch((values) => { + const isAnyFieldDirty = Object.keys(values).some( + (key) => + values[key as keyof unknown] !== defaultValues[key as keyof unknown], + ); + if (isAnyFieldDirty) { + contractorOnboardingBag?.checkFieldUpdates(values); + } + }); + return () => subscription?.unsubscribe(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSubmit = async (values: Record) => { + await onSubmit(values); + }; + + return ( +
+ + + + + ); +} diff --git a/src/flows/CreateCompany/components/SelectCountryStep.tsx b/src/flows/CreateCompany/components/SelectCountryStep.tsx new file mode 100644 index 000000000..42cf038e6 --- /dev/null +++ b/src/flows/CreateCompany/components/SelectCountryStep.tsx @@ -0,0 +1,70 @@ +// TODO: Correct types later +import { + SelectCountryFormPayload, + SelectCountrySuccess, +} from '@/src/flows/CreateCountry/types'; +import { NormalizedFieldError } from '@/src/lib/mutations'; +import { $TSFixMe } from '@/src/types/remoteFlows'; +import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; +import { CreateCompanyForm } from '@/src/flows/CreateCompany/components/CreateCompanyForm'; +import { handleStepError } from '@/src/lib/utils'; + +type SelectCountryStepProps = { + /* + * The function is called when the form is submitted. It receives the form values as an argument. + */ + onSubmit?: (payload: SelectCountryFormPayload) => void | Promise; + /* + * The function is called when the form submission is successful. + */ + onSuccess?: (data: SelectCountrySuccess) => void | Promise; + /* + * The function is called when an error occurs during form submission. + */ + onError?: ({ + error, + rawError, + fieldErrors, + }: { + error: Error; + rawError: Record; + fieldErrors: NormalizedFieldError[]; + }) => void; +}; + +export function SelectCountryStep({ + onSubmit, + onSuccess, + onError, +}: SelectCountryStepProps) { + const { createCompanyBag } = useCreateCompanyContext(); + const handleSubmit = async (payload: $TSFixMe) => { + try { + await onSubmit?.({ countryCode: payload.country }); + const response = await createCompanyBag.onSubmit(payload); + if (response?.data) { + await onSuccess?.(response?.data as SelectCountrySuccess); + createCompanyBag?.next(); + return; + } + } catch (error: unknown) { + const structuredError = handleStepError( + error, + createCompanyBag.meta?.fields?.select_country, + ); + + onError?.(structuredError); + } + }; + + const initialValues = + createCompanyBag.stepState.values?.select_country || + createCompanyBag.initialValues.select_country; + + return ( + + ); +} diff --git a/src/flows/CreateCompany/context.ts b/src/flows/CreateCompany/context.ts new file mode 100644 index 000000000..0f6b4ed88 --- /dev/null +++ b/src/flows/CreateCompany/context.ts @@ -0,0 +1,29 @@ +import type { useCreateCompany } from '@/src/flows/CreateCompany/hooks'; +import { createContext, useContext, RefObject } from 'react'; +import { UseFormSetValue } from 'react-hook-form'; +export const CreateCompanyContext = createContext<{ + formId: string | undefined; + createCompanyBag: ReturnType | null; + formRef?: { + setValue: RefObject> | undefined>; + }; +}>({ + formId: undefined, + createCompanyBag: null, + formRef: undefined, +}); + +export const useCreateCompanyContext = () => { + const context = useContext(CreateCompanyContext); + if (!context.formId || !context.createCompanyBag) { + throw new Error( + 'useCreateCompanyContext must be used within a CreateCompanyContextProvider', + ); + } + + return { + formId: context.formId, + createCompanyBag: context.createCompanyBag, + formRef: context.formRef, + } as const; +}; diff --git a/src/flows/CreateCompany/hooks.ts b/src/flows/CreateCompany/hooks.ts new file mode 100644 index 000000000..484b28c19 --- /dev/null +++ b/src/flows/CreateCompany/hooks.ts @@ -0,0 +1,270 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { FieldValues } from 'react-hook-form'; +import { JSFFields } from '@/src/types/remoteFlows'; +import { + getInitialValues, +} from '@/src/components/form/utils'; +import { ValidationResult } from '@remoteoss/remote-json-schema-form-kit'; +import { CreateCompanyFlowProps } from '@/src/flows/CreateCompany/types'; +import { + STEPS, +} from '@/src/flows/CreateCompany/utils'; +import { + useCountriesSchemaField, + useJSONSchemaForm, +} from '@/src/flows/Onboarding/api'; + +import { FlowOptions, JSFModify, JSONSchemaFormType } from '@/src/flows/types'; +import { Step, useStepState } from '@/src/flows/useStepState'; +import { createStructuredError, prettifyFormValues } from '@/src/lib/utils'; +import { JSFFieldset, Meta } from '@/src/types/remoteFlows'; + +type useCreateCompanyProps = Omit< + CreateCompanyFlowProps, + 'render' +>; + +const stepToFormSchemaMap: Record< + keyof typeof STEPS, + JSONSchemaFormType | null +> = { + select_country: null, + basic_information: 'employment_basic_information', +}; + +export const useCreateCompany = ({ + countryCode, + options, + initialValues: createCompanyInitialValues, +}: useCreateCompanyProps) => { + const [internalCountryCode, setInternalCountryCode] = useState( + countryCode || null, + ); + const fieldsMetaRef = useRef<{ + select_country: Meta; + basic_information: Meta; + }>({ + select_country: {}, + basic_information: {}, + }); + + const { + fieldValues, + stepState, + setFieldValues, + previousStep, + nextStep, + goToStep, + setStepValues, + } = useStepState( + STEPS as Record>, + ); + + + const { selectCountryForm, isLoading: isLoadingCountries } = + useCountriesSchemaField({ + jsfModify: options?.jsfModify?.select_country, + queryOptions: { + enabled: stepState.currentStep.name === 'select_country', + }, + }); + + const useJSONSchema = ({ + form, + options: jsonSchemaOptions = {}, + query = {}, + }: { + form: JSONSchemaFormType; + options?: { + jsfModify?: JSFModify; + queryOptions?: { enabled?: boolean }; + jsonSchemaVersion?: FlowOptions['jsonSchemaVersion']; + }; + query?: Record; + }) => { + const hasUserEnteredAnyValues = Object.keys(fieldValues).length > 0; + // when you write on the fields, the values are stored in the fieldValues state + // when values are stored in the stepState is when the user has navigated to the step + // and then we have the values from the server and the onboardingInitialValues that the user can inject, + const mergedFormValues = hasUserEnteredAnyValues + ? { + ...createCompanyInitialValues, + ...stepState.values?.[stepState.currentStep.name], // Restore values for the current step + ...fieldValues, + } + : { + ...createCompanyInitialValues, + }; + + return useJSONSchemaForm({ + countryCode: internalCountryCode as string, + form: form, + fieldValues: mergedFormValues, + query, + options: { + ...jsonSchemaOptions, + queryOptions: { + enabled: jsonSchemaOptions.queryOptions?.enabled ?? true, + }, + }, + }); + }; + + const stepFields: Record = useMemo( + () => ({ + select_country: selectCountryForm?.fields || [], + review: [], + }), + [ + selectCountryForm?.fields, + ], + ); + + const stepFieldsWithFlatFieldsets: Record< + keyof typeof STEPS, + JSFFieldset | null | undefined + > = { + select_country: null, + }; + + + const selectCountryInitialValues = useMemo( + () => + getInitialValues(stepFields.select_country, { + country: internalCountryCode || '', + }), + [stepFields.select_country, internalCountryCode], + ); + + + const initialValues = useMemo(() => { + return { + select_country: selectCountryInitialValues, + }; + }, [ + selectCountryInitialValues, + ]); + + const goTo = (step: keyof typeof STEPS) => { + goToStep(step); + }; + + const parseFormValues = async (values: FieldValues) => { + if (selectCountryForm && stepState.currentStep.name === 'select_country') { + return values; + } + return {}; + }; + + async function onSubmit(values: FieldValues) { + const currentStepName = stepState.currentStep.name; + if (currentStepName in fieldsMetaRef.current) { + fieldsMetaRef.current[ + currentStepName as keyof typeof fieldsMetaRef.current + ] = prettifyFormValues(values, stepFields[currentStepName]); + } + const parsedValues = await parseFormValues(values); + switch (stepState.currentStep.name) { + case 'select_country': { + setInternalCountryCode(parsedValues.country); + return Promise.resolve({ data: { countryCode: parsedValues.country } }); + } + + default: { + throw createStructuredError('Invalid step state'); + } + } + } + + const isLoading = + isLoadingCountries + + return { + /** + * Loading state indicating if the flow is loading data + */ + isLoading, + + /** + * Current state of the form fields for the current step. + */ + fieldValues, + + /** + * Current step state containing the current step and total number of steps + */ + stepState, + + /** + * Function to update the current form field values + * @param values - New form values to set + */ + checkFieldUpdates: setFieldValues, + + /** + * Function to handle going back to the previous step + * @returns {void} + */ + back: previousStep, + + /** + * Function to handle going to the next step + * @returns {void} + */ + next: nextStep, + + /** + * Function to handle going to a specific step + * @param step The step to go to. + * @returns {void} + */ + goTo: goTo, + + /** + * Function to handle form submission + * @param values - Form values to submit + * @returns Promise resolving to the mutation result + */ + onSubmit, + + /** + * Array of form fields from the onboarding schema + */ + fields: stepFields[stepState.currentStep.name], + + /** + * Fields metadata for each step + */ + meta: { + fields: fieldsMetaRef.current, + fieldsets: stepFieldsWithFlatFieldsets[stepState.currentStep.name], + }, + + /** + * Function to parse form values before submission + * @param values - Form values to parse + * @returns Parsed form values + */ + parseFormValues, + + /** + * Function to validate form values against the onboarding schema + * @param values - Form values to validate + * @returns Validation result or null if no schema is available + */ + handleValidation: async ( + values: FieldValues, + ): Promise => { + if (stepState.currentStep.name === 'select_country') { + return selectCountryForm.handleValidation(values); + } + + return null; + }, + + /** + * Initial form values + */ + initialValues, + }; +}; diff --git a/src/flows/CreateCompany/index.ts b/src/flows/CreateCompany/index.ts new file mode 100644 index 000000000..35f8e0720 --- /dev/null +++ b/src/flows/CreateCompany/index.ts @@ -0,0 +1 @@ +export {CreateCompanyFlow} from './CreateCompany'; diff --git a/src/flows/CreateCompany/types.ts b/src/flows/CreateCompany/types.ts new file mode 100644 index 000000000..506fb78ee --- /dev/null +++ b/src/flows/CreateCompany/types.ts @@ -0,0 +1,52 @@ +import { useCreateCompany } from '@/src/flows/CreateCompany/hooks'; +import { SelectCountryStep } from '@/src/flows/Onboarding/components/SelectCountryStep'; +import { FlowOptions, JSFModify } from '@/src/flows/types'; + +export type CreateCompanyRenderProps = { + /** + * The create company bag returned by the useCreateCompany hook. + * This bag contains all the methods and properties needed to handle the contractor onboarding flow. + * @see {@link useCreateCompany} + */ + createCompanyBag: ReturnType; + /** + * The components used in the contractor onboarding flow. + * @see {@link SelectCountryStep} + */ + components: { + SelectCountryStep: typeof SelectCountryStep; + }; +}; + +export type CreateCompanyFlowProps = { + /** + * The country code to use for the onboarding. + */ + countryCode?: string; + + + /** + * The render prop function with the params passed by the useCreateCompany hook and the components available to use for this flow + */ + render: ({ + createCompanyBag, + components, + }: CreateCompanyRenderProps) => React.ReactNode; + /** + * The options for the contractor onboarding flow. + */ + options?: Omit & { + jsfModify?: { + select_country?: JSFModify; + }; + }; + + /** + * Initial values to pre-populate the form fields. + * These are flat field values that will be automatically mapped to the correct step. + * Server data will override these values. This happens when you pass employmentId and the server returns an employment object. + */ + initialValues?: Record; +}; + + diff --git a/src/flows/CreateCompany/utils.ts b/src/flows/CreateCompany/utils.ts new file mode 100644 index 000000000..4ffc205a9 --- /dev/null +++ b/src/flows/CreateCompany/utils.ts @@ -0,0 +1,9 @@ +import { Step } from '@/src/flows/useStepState'; + +type StepKeys = + | 'select_country' + | 'review'; + +export const STEPS: Record> = { + select_country: { index: 0, name: 'select_country' }, +} as const; From 3d1574e0837a757fd6f0fd6f9dfb50858e94b1df Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Wed, 28 Jan 2026 12:31:57 +0000 Subject: [PATCH 02/10] create company --- example/flake.nix | 2 +- example/package-lock.json | 4 +- src/flows/CreateCompany/CreateCompany.tsx | 9 +-- .../components/CreateCompanyForm.tsx | 56 +++++++------------ .../components/SelectCountryStep.tsx | 2 +- src/flows/CreateCompany/hooks.ts | 56 +------------------ src/flows/CreateCompany/types.ts | 12 ++++ src/flows/CreateCompany/utils.ts | 1 + 8 files changed, 44 insertions(+), 98 deletions(-) diff --git a/example/flake.nix b/example/flake.nix index e9b30a68b..d784e7146 100644 --- a/example/flake.nix +++ b/example/flake.nix @@ -15,7 +15,7 @@ in { devShells.default = pkgs.mkShell { buildInputs = [ - pkgs.nodejs_20 or pkgs.nodejs_latest + pkgs.nodejs_24 or pkgs.nodejs_latest pkgs.playwright-driver.browsers pkgs.playwright ]; diff --git a/example/package-lock.json b/example/package-lock.json index 5e0c89a57..4b957616a 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -37,7 +37,7 @@ }, "..": { "name": "@remoteoss/remote-flows", - "version": "1.6.0", + "version": "1.7.0", "dependencies": { "@hookform/resolvers": "^4.1.3", "@radix-ui/react-accordion": "^1.2.12", @@ -54,7 +54,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@remoteoss/remote-json-schema-form-kit": "github:remoteoss/remote-json-schema-form-kit#update-types-kit", + "@remoteoss/remote-json-schema-form-kit": "github:remoteoss/remote-json-schema-form-kit#v0.0.5", "@tailwindcss/cli": "^4.1.17", "@tailwindcss/postcss": "^4.1.17", "@tanstack/react-query": "^5.90.10", diff --git a/src/flows/CreateCompany/CreateCompany.tsx b/src/flows/CreateCompany/CreateCompany.tsx index 7ff131e28..bc7379447 100644 --- a/src/flows/CreateCompany/CreateCompany.tsx +++ b/src/flows/CreateCompany/CreateCompany.tsx @@ -1,5 +1,9 @@ -import { useId, useRef, useMemo } from 'react'; +import { CreateCompanyFlowProps} from '@/src/flows/CreateCompany/types' +import { useCreateCompany} from '@/src/flows/CreateCompany/hooks' +import { CreateCompanyContext} from '@/src/flows/CreateCompany/context' +import { SelectCountryStep } from '@/src/flows/CreateCompany/components/SelectCountryStep'; +import { useId } from 'react'; export const CreateCompanyFlow = ({ render, countryCode, @@ -10,7 +14,6 @@ export const CreateCompanyFlow = ({ countryCode, }); const formId = useId(); - // Store form's setValue method in ref to allow sibling components return ( diff --git a/src/flows/CreateCompany/components/CreateCompanyForm.tsx b/src/flows/CreateCompany/components/CreateCompanyForm.tsx index e466bfd6e..4c2d28dac 100644 --- a/src/flows/CreateCompany/components/CreateCompanyForm.tsx +++ b/src/flows/CreateCompany/components/CreateCompanyForm.tsx @@ -3,19 +3,15 @@ import { JSONSchemaFormFields } from '@/src/components/form/JSONSchemaForm'; import { Form } from '@/src/components/ui/form'; import { useForm } from 'react-hook-form'; import { useJsonSchemasValidationFormResolver } from '@/src/components/form/validationResolver'; -import { BasicInformationFormPayload } from '@/src/flows/Onboarding/types'; +import { BasicInformationFormPayload } from '@/src/flows/CreateCompany/types'; import { Components } from '@/src/types/remoteFlows'; import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; import { useEffect } from 'react'; -import { PricingPlanFormPayload } from '@/src/flows/CreateCompany/types'; -import { CreateCompanyContractDetailsFormPayload } from '@/src/flows/CreateCompany/types'; type CreateCompanyFormProps = { onSubmit: ( payload: | BasicInformationFormPayload - | PricingPlanFormPayload - | CreateCompanyContractDetailsFormPayload, ) => Promise; components?: Components; fields?: JSFFields; @@ -27,11 +23,11 @@ export function CreateCompanyForm({ onSubmit, components, }: CreateCompanyFormProps) { - const { formId, contractorOnboardingBag, formRef } = + const { formId, createCompanyBag, formRef } = useCreateCompanyContext(); const resolver = useJsonSchemasValidationFormResolver( - contractorOnboardingBag.handleValidation, + createCompanyBag.handleValidation, ); const form = useForm({ @@ -48,37 +44,25 @@ export function CreateCompanyForm({ } }, [form.setValue, formRef]); - useEffect(() => { - // When the employmentId is set, - // we need to run the checkFieldUpdates to update fieldValues in useStepState - if (contractorOnboardingBag.employmentId) { - contractorOnboardingBag?.checkFieldUpdates(form.getValues()); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const subscription = form?.watch((values) => { - const isAnyFieldDirty = Object.keys(values).some( - (key) => - values[key as keyof unknown] !== defaultValues[key as keyof unknown], - ); - if (isAnyFieldDirty) { - contractorOnboardingBag?.checkFieldUpdates(values); - } - }); - return () => subscription?.unsubscribe(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleSubmit = async (values: Record) => { - await onSubmit(values); + const handleSubmit = async (values: Record) => { + await onSubmit( + { + company_owner_email: values.company_owner_email, + company_owner_name: values.company_owner_email, + country_code: values.company_owner_email, + desired_currency: values.company_owner_email, + name: values.company_owner_email, + phone_number: values.company_owner_email, + tax_number: values.company_owner_email, + tax_job_category: values.company_owner_email, + tax_servicing_countries: [values.company_owner_email], + }); }; return (
diff --git a/src/flows/CreateCompany/components/SelectCountryStep.tsx b/src/flows/CreateCompany/components/SelectCountryStep.tsx index 42cf038e6..6c9c41eb2 100644 --- a/src/flows/CreateCompany/components/SelectCountryStep.tsx +++ b/src/flows/CreateCompany/components/SelectCountryStep.tsx @@ -2,7 +2,7 @@ import { SelectCountryFormPayload, SelectCountrySuccess, -} from '@/src/flows/CreateCountry/types'; +} from '@/src/flows/Onboarding/types'; import { NormalizedFieldError } from '@/src/lib/mutations'; import { $TSFixMe } from '@/src/types/remoteFlows'; import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; diff --git a/src/flows/CreateCompany/hooks.ts b/src/flows/CreateCompany/hooks.ts index 484b28c19..dc7effb8d 100644 --- a/src/flows/CreateCompany/hooks.ts +++ b/src/flows/CreateCompany/hooks.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { FieldValues } from 'react-hook-form'; import { JSFFields } from '@/src/types/remoteFlows'; import { @@ -11,10 +11,8 @@ import { } from '@/src/flows/CreateCompany/utils'; import { useCountriesSchemaField, - useJSONSchemaForm, } from '@/src/flows/Onboarding/api'; -import { FlowOptions, JSFModify, JSONSchemaFormType } from '@/src/flows/types'; import { Step, useStepState } from '@/src/flows/useStepState'; import { createStructuredError, prettifyFormValues } from '@/src/lib/utils'; import { JSFFieldset, Meta } from '@/src/types/remoteFlows'; @@ -24,18 +22,9 @@ type useCreateCompanyProps = Omit< 'render' >; -const stepToFormSchemaMap: Record< - keyof typeof STEPS, - JSONSchemaFormType | null -> = { - select_country: null, - basic_information: 'employment_basic_information', -}; - export const useCreateCompany = ({ countryCode, options, - initialValues: createCompanyInitialValues, }: useCreateCompanyProps) => { const [internalCountryCode, setInternalCountryCode] = useState( countryCode || null, @@ -55,7 +44,6 @@ export const useCreateCompany = ({ previousStep, nextStep, goToStep, - setStepValues, } = useStepState( STEPS as Record>, ); @@ -69,47 +57,6 @@ export const useCreateCompany = ({ }, }); - const useJSONSchema = ({ - form, - options: jsonSchemaOptions = {}, - query = {}, - }: { - form: JSONSchemaFormType; - options?: { - jsfModify?: JSFModify; - queryOptions?: { enabled?: boolean }; - jsonSchemaVersion?: FlowOptions['jsonSchemaVersion']; - }; - query?: Record; - }) => { - const hasUserEnteredAnyValues = Object.keys(fieldValues).length > 0; - // when you write on the fields, the values are stored in the fieldValues state - // when values are stored in the stepState is when the user has navigated to the step - // and then we have the values from the server and the onboardingInitialValues that the user can inject, - const mergedFormValues = hasUserEnteredAnyValues - ? { - ...createCompanyInitialValues, - ...stepState.values?.[stepState.currentStep.name], // Restore values for the current step - ...fieldValues, - } - : { - ...createCompanyInitialValues, - }; - - return useJSONSchemaForm({ - countryCode: internalCountryCode as string, - form: form, - fieldValues: mergedFormValues, - query, - options: { - ...jsonSchemaOptions, - queryOptions: { - enabled: jsonSchemaOptions.queryOptions?.enabled ?? true, - }, - }, - }); - }; - const stepFields: Record = useMemo( () => ({ select_country: selectCountryForm?.fields || [], @@ -125,6 +72,7 @@ export const useCreateCompany = ({ JSFFieldset | null | undefined > = { select_country: null, + review: null }; diff --git a/src/flows/CreateCompany/types.ts b/src/flows/CreateCompany/types.ts index 506fb78ee..ddb1c6694 100644 --- a/src/flows/CreateCompany/types.ts +++ b/src/flows/CreateCompany/types.ts @@ -50,3 +50,15 @@ export type CreateCompanyFlowProps = { }; +export type BasicInformationFormPayload = { + company_owner_email: string; + company_owner_name: string; + country_code: string; + desired_currency: string; + name: string; + phone_number: string; + tax_number: string; + tax_job_category: string; + tax_servicing_countries: string[]; +}; + diff --git a/src/flows/CreateCompany/utils.ts b/src/flows/CreateCompany/utils.ts index 4ffc205a9..6a902ae21 100644 --- a/src/flows/CreateCompany/utils.ts +++ b/src/flows/CreateCompany/utils.ts @@ -6,4 +6,5 @@ type StepKeys = export const STEPS: Record> = { select_country: { index: 0, name: 'select_country' }, + review: { index: 1, name: 'review' }, } as const; From a62ea97533567f778fb21668eb78eb8300d5873f Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Wed, 28 Jan 2026 12:57:43 +0000 Subject: [PATCH 03/10] add create company --- example/src/App.tsx | 10 + example/src/CreateCompany.tsx | 197 ++++++++++++++++++ src/flows/CreateCompany/CreateCompany.tsx | 2 + .../components/CreateCompanySubmit.tsx | 28 +++ src/flows/CreateCompany/index.ts | 4 + src/flows/CreateCompany/types.ts | 9 +- src/index.tsx | 8 + 7 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 example/src/CreateCompany.tsx create mode 100644 src/flows/CreateCompany/components/CreateCompanySubmit.tsx diff --git a/example/src/App.tsx b/example/src/App.tsx index 3c23aaf62..1f44d4a07 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -44,7 +44,9 @@ import CostCalculatorWithReplaceableComponentsCode from './CostCalculatorWithRep import TerminationCode from './Termination?raw'; import ContractAmendmentCode from './ContractAmendment?raw'; import { ContractorOnboardingForm } from './ContractorOnboarding'; +import { CreateCompanyForm } from './CreateCompany'; import ContractorOnboardingCode from './ContractorOnboarding?raw'; +import CreateCompanyCode from './CreateCompany?raw'; const costCalculatorDemos = [ { @@ -148,6 +150,14 @@ const additionalDemos = [ component: ContractorOnboardingForm, sourceCode: ContractorOnboardingCode, }, + { + id: 'create-company', + title: 'Create Company', + description: 'Create a company', + component: CreateCompanyForm, + sourceCode: CreateCompanyCode, + }, + ]; const demoStructure = [ diff --git a/example/src/CreateCompany.tsx b/example/src/CreateCompany.tsx new file mode 100644 index 000000000..837757751 --- /dev/null +++ b/example/src/CreateCompany.tsx @@ -0,0 +1,197 @@ +import { + SelectCountrySuccess, + SelectCountryFormPayload, + NormalizedFieldError, + CreateCompanyFlow, + CreateCompanyRenderProps, + JSFCustomComponentProps, +} from '@remoteoss/remote-flows'; +import { + Card, + Tabs, + TabsTrigger, + TabsList, +} from '@remoteoss/remote-flows/internals'; +import React, { useState } from 'react'; +import { RemoteFlows } from './RemoteFlows'; +import { AlertError } from './AlertError'; +import './css/main.css'; +import './css/contractor-onboarding.css'; + +const Switcher = (props: JSFCustomComponentProps) => { + return ( + { + props.setValue(value); + }} + > + + {props.options?.map((option) => ( + + {option.label} + + ))} + + + ); +}; + +const STEPS = [ + 'Select Country', + 'Review', +]; + +type MultiStepFormProps = { + createCompanyBag: CreateCompanyRenderProps['createCompanyBag']; + components: CreateCompanyRenderProps['components']; +}; + +const MultiStepForm = ({ + components, + createCompanyBag, +}: MultiStepFormProps) => { + const { + SubmitButton, + BackButton, + SelectCountryStep, + } = components; + const [errors, setErrors] = useState<{ + apiError: string; + fieldErrors: NormalizedFieldError[]; + }>({ + apiError: '', + fieldErrors: [], + }); + + switch (createCompanyBag.stepState.currentStep.name) { + case 'select_country': + console.log("SELECTING CONUTRY") + console.log("components") + console.log(components) + return ( +
+ + console.log('payload', payload) + } + onSuccess={(response: SelectCountrySuccess) => + console.log('response', response) + } + onError={({ + error, + fieldErrors, + }: { + error: Error; + fieldErrors: NormalizedFieldError[]; + }) => setErrors({ apiError: error.message, fieldErrors })} + /> +
+ + Continue + +
+
+ ); + case 'review': { + return ( +
+
+ ); + } + } +}; + +const CreateCompanyRender = ({ + createCompanyBag, + components, +}: MultiStepFormProps) => { + const currentStepIndex = createCompanyBag.stepState.currentStep.index; + + return ( + <> +
+
    + {STEPS.map((step, index) => ( +
  • + {index + 1}. {step} +
  • + ))} +
+
+ + {createCompanyBag.isLoading ? ( +
+

Loading...

+
+ ) : ( + + )} + + ); +}; + +const Header = () => { + return ( +
+

Contractor Onboarding

+

Adding a new contractor is simple and fast.

+
+ ); +}; + +type CreateCompanyFormData = { + countryCode?: string; +}; + +export const CreateCompanyWithProps = ({ +}: CreateCompanyFormData) => { + return ( +
+ +
+
+ + + +
+
+
+ ); +}; + +export const CreateCompanyForm = () => { + const [formData] = useState({ + }); + const [showOnboarding, setShowOnboarding] = useState(false); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setShowOnboarding(true); + }; + + if (showOnboarding) { + return ; + } + + return ( +
+ +
+ ); +}; diff --git a/src/flows/CreateCompany/CreateCompany.tsx b/src/flows/CreateCompany/CreateCompany.tsx index bc7379447..5c9ecdc19 100644 --- a/src/flows/CreateCompany/CreateCompany.tsx +++ b/src/flows/CreateCompany/CreateCompany.tsx @@ -3,6 +3,7 @@ import { CreateCompanyFlowProps} from '@/src/flows/CreateCompany/types' import { useCreateCompany} from '@/src/flows/CreateCompany/hooks' import { CreateCompanyContext} from '@/src/flows/CreateCompany/context' import { SelectCountryStep } from '@/src/flows/CreateCompany/components/SelectCountryStep'; +import { CreateCompanySubmit } from '@/src/flows/CreateCompany/components/CreateCompanySubmit'; import { useId } from 'react'; export const CreateCompanyFlow = ({ render, @@ -22,6 +23,7 @@ export const CreateCompanyFlow = ({ createCompanyBag, components: { SelectCountryStep: SelectCountryStep, + SubmitButton: CreateCompanySubmit }, })}
diff --git a/src/flows/CreateCompany/components/CreateCompanySubmit.tsx b/src/flows/CreateCompany/components/CreateCompanySubmit.tsx new file mode 100644 index 000000000..343b73158 --- /dev/null +++ b/src/flows/CreateCompany/components/CreateCompanySubmit.tsx @@ -0,0 +1,28 @@ +import { ButtonHTMLAttributes, PropsWithChildren } from 'react'; +import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; +import { useFormFields } from '@/src/context'; + +export function CreateCompanySubmit({ + children, + ...props +}: PropsWithChildren< + ButtonHTMLAttributes & Record +>) { + const { formId } = useCreateCompanyContext(); + const { components } = useFormFields(); + + const CustomButton = components?.button; + if (!CustomButton) { + throw new Error(`Button component not found`); + } + + return ( + + {children} + + ); +} diff --git a/src/flows/CreateCompany/index.ts b/src/flows/CreateCompany/index.ts index 35f8e0720..031485cbc 100644 --- a/src/flows/CreateCompany/index.ts +++ b/src/flows/CreateCompany/index.ts @@ -1 +1,5 @@ export {CreateCompanyFlow} from './CreateCompany'; +export type { + CreateCompanyFlowProps, + CreateCompanyRenderProps, +} from './types'; diff --git a/src/flows/CreateCompany/types.ts b/src/flows/CreateCompany/types.ts index ddb1c6694..211268033 100644 --- a/src/flows/CreateCompany/types.ts +++ b/src/flows/CreateCompany/types.ts @@ -1,20 +1,23 @@ import { useCreateCompany } from '@/src/flows/CreateCompany/hooks'; import { SelectCountryStep } from '@/src/flows/Onboarding/components/SelectCountryStep'; import { FlowOptions, JSFModify } from '@/src/flows/types'; +import { CreateCompanySubmit } from '@/src/flows/CreateCompany/components/CreateCompanySubmit'; export type CreateCompanyRenderProps = { /** * The create company bag returned by the useCreateCompany hook. - * This bag contains all the methods and properties needed to handle the contractor onboarding flow. + * This bag contains all the methods and properties needed to handle the create company flow. * @see {@link useCreateCompany} */ createCompanyBag: ReturnType; /** - * The components used in the contractor onboarding flow. + * The components used in the create company flow. * @see {@link SelectCountryStep} */ components: { SelectCountryStep: typeof SelectCountryStep; + SubmitButton: typeof CreateCompanySubmit; + }; }; @@ -33,7 +36,7 @@ export type CreateCompanyFlowProps = { components, }: CreateCompanyRenderProps) => React.ReactNode; /** - * The options for the contractor onboarding flow. + * The options for the create company flow. */ options?: Omit & { jsfModify?: { diff --git a/src/index.tsx b/src/index.tsx index d2cda1d92..07b13b3ee 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -83,6 +83,14 @@ export type { export type { ContractPreviewStatementProps } from '@/src/flows/ContractorOnboarding/components/ContractPreviewStatement'; +export { CreateCompanyFlow } from '@/src/flows/CreateCompany'; +export type { + CreateCompanyFlowProps, + CreateCompanyRenderProps, +} from '@/src/flows/CreateCompany'; + + + export type * from '@/src/flows/CostCalculator/types'; export { useMagicLink } from '@/src/common/api'; From 8fad61ecfa18eb06d76ff014ba3312d509a36fe0 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Thu, 29 Jan 2026 09:54:28 +0000 Subject: [PATCH 04/10] add json schema for creating company --- .../components/CreateCompanyForm.tsx | 1 + src/flows/CreateCompany/hooks.ts | 77 ++++++++++++++++++- .../json-schemas/selectCountryStep.ts | 22 ++++++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/flows/CreateCompany/json-schemas/selectCountryStep.ts diff --git a/src/flows/CreateCompany/components/CreateCompanyForm.tsx b/src/flows/CreateCompany/components/CreateCompanyForm.tsx index 4c2d28dac..7e52acf77 100644 --- a/src/flows/CreateCompany/components/CreateCompanyForm.tsx +++ b/src/flows/CreateCompany/components/CreateCompanyForm.tsx @@ -45,6 +45,7 @@ export function CreateCompanyForm({ }, [form.setValue, formRef]); const handleSubmit = async (values: Record) => { + console.log("SUBMITTING", values) await onSubmit( { company_owner_email: values.company_owner_email, diff --git a/src/flows/CreateCompany/hooks.ts b/src/flows/CreateCompany/hooks.ts index dc7effb8d..cf0b65012 100644 --- a/src/flows/CreateCompany/hooks.ts +++ b/src/flows/CreateCompany/hooks.ts @@ -1,17 +1,25 @@ import { useMemo, useRef, useState } from 'react'; import { FieldValues } from 'react-hook-form'; +import { useQuery } from '@tanstack/react-query'; import { JSFFields } from '@/src/types/remoteFlows'; +import { Client } from '@/src/client/client'; import { getInitialValues, } from '@/src/components/form/utils'; +import { + FlowOptions, +} from '@/src/flows/types'; import { ValidationResult } from '@remoteoss/remote-json-schema-form-kit'; import { CreateCompanyFlowProps } from '@/src/flows/CreateCompany/types'; +import { createHeadlessForm } from '@/src/common/createHeadlessForm'; +import { useClient } from '@/src/context'; +import { selectCountryStepSchema } from '@/src/flows/CreateCompany/json-schemas/selectCountryStep'; import { STEPS, } from '@/src/flows/CreateCompany/utils'; import { - useCountriesSchemaField, -} from '@/src/flows/Onboarding/api'; + getSupportedCountry, +} from '@/src/client'; import { Step, useStepState } from '@/src/flows/useStepState'; import { createStructuredError, prettifyFormValues } from '@/src/lib/utils'; @@ -22,6 +30,69 @@ type useCreateCompanyProps = Omit< 'render' >; +const useCountries = (queryOptions?: { enabled?: boolean }) => { + const { client } = useClient(); + return useQuery({ + ...queryOptions, + queryKey: ['countries'], + retry: false, + queryFn: async () => { + const response = await getSupportedCountry({ + client: client as Client, + headers: { + Authorization: ``, + }, + }); + + // If response status is 404 or other error, throw an error to trigger isError + if (response.error || !response.data) { + throw new Error('Failed to fetch supported countries'); + } + + return response; + }, + select: ({ data }) => { + return ( + data?.data + ?.filter((country) => country.eor_onboarding) + .map((country) => { + return { + label: country.name, + value: country.code, + }; + }) || [] + ); + }, + }); +}; +const useCountriesSchemaField = ( + options?: Omit & { + queryOptions?: { enabled?: boolean }; + }, +) => { + const { data: countries, isLoading } = useCountries(options?.queryOptions); + + const selectCountryForm = createHeadlessForm( + selectCountryStepSchema.data.schema, + {}, + options, + ); + + if (countries) { + const countryField = selectCountryForm.fields.find( + (field) => field.name === 'country_code', + ); + if (countryField) { + countryField.options = countries; + } + } + + return { + isLoading, + selectCountryForm, + }; +}; + export const useCreateCompany = ({ countryCode, options, @@ -115,7 +186,7 @@ export const useCreateCompany = ({ switch (stepState.currentStep.name) { case 'select_country': { setInternalCountryCode(parsedValues.country); - return Promise.resolve({ data: { countryCode: parsedValues.country } }); + return Promise.resolve({ data: { countryCode: parsedValues.country_code } }); } default: { diff --git a/src/flows/CreateCompany/json-schemas/selectCountryStep.ts b/src/flows/CreateCompany/json-schemas/selectCountryStep.ts new file mode 100644 index 000000000..2e5f4fb2f --- /dev/null +++ b/src/flows/CreateCompany/json-schemas/selectCountryStep.ts @@ -0,0 +1,22 @@ +export const selectCountryStepSchema = { + data: { + version: 7, + schema: { + additionalProperties: false, + properties: { + country_code: { + title: 'Country', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'select', + }, + }, + }, + required: ['country_code'], + type: 'object', + 'x-jsf-order': ['country_code'], + }, + }, +}; From fde999c4e73f39ca6386fc134f6d81f32ae7468d Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Thu, 29 Jan 2026 13:59:05 +0000 Subject: [PATCH 05/10] create company --- example/api/get_token.js | 49 +++++++++++- example/api/proxy.js | 13 +++- example/src/CreateCompany.tsx | 3 - src/flows/CreateCompany/api.ts | 27 +++++++ .../components/CreateCompanyForm.tsx | 16 ++-- .../components/SelectCountryStep.tsx | 22 ++++-- src/flows/CreateCompany/hooks.ts | 56 ++++++++++++++ .../json-schemas/selectCountryStep.ts | 74 ++++++++++++++++++- src/flows/CreateCompany/types.ts | 14 ++++ 9 files changed, 251 insertions(+), 23 deletions(-) diff --git a/example/api/get_token.js b/example/api/get_token.js index 47cbb26a2..1435e02e9 100644 --- a/example/api/get_token.js +++ b/example/api/get_token.js @@ -47,6 +47,53 @@ async function fetchAccessToken() { return { accessToken: data.access_token, expiresIn: data.expires_in }; } +async function fetchClientCredentialsAccessToken() { + const { + VITE_CLIENT_ID, + VITE_CLIENT_SECRET, + VITE_REMOTE_GATEWAY, + VITE_REFRESH_TOKEN, + } = process.env; + + // for local development, we don't need a client secret + if ( + !VITE_CLIENT_ID || + (!VITE_CLIENT_SECRET && VITE_REMOTE_GATEWAY !== 'local') || + !VITE_REMOTE_GATEWAY || + !VITE_REFRESH_TOKEN + ) { + throw new Error( + 'Missing VITE_CLIENT_ID, VITE_CLIENT_SECRET, VITE_REMOTE_GATEWAY, or VITE_REFRESH_TOKEN', + ); + } + + const gatewayUrl = buildGatewayURL(); + + const encodedCredentials = Buffer.from( + `${VITE_CLIENT_ID}:${VITE_CLIENT_SECRET}`, + ).toString('base64'); + + const response = await fetch(`${gatewayUrl}/auth/oauth2/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${encodedCredentials}`, + }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const data = await response.json(); + return { accessToken: data.access_token, expiresIn: data.expires_in }; +} + + // Express route handler async function getToken(req, res) { const { NODE_ENV } = process.env; @@ -70,4 +117,4 @@ async function getToken(req, res) { } } -module.exports = { getToken, fetchAccessToken }; +module.exports = { getToken, fetchAccessToken, fetchClientCredentialsAccessToken }; diff --git a/example/api/proxy.js b/example/api/proxy.js index 30651aac3..c62829c9b 100644 --- a/example/api/proxy.js +++ b/example/api/proxy.js @@ -1,5 +1,5 @@ const axios = require('axios'); -const { fetchAccessToken } = require('./get_token.js'); +const { fetchClientCredentialsAccessToken, fetchAccessToken } = require('./get_token.js'); const { buildGatewayURL } = require('./utils.js'); /** @@ -38,9 +38,17 @@ async function createProxyRequest(path, method = 'GET', options = {}) { if (stream) { delete requestConfig.headers['Content-Type']; } + console.log("REQUIRES AUTH", requiresAuth) // Add authentication if required - if (requiresAuth) { + if (requiresAuth && path == '/v1/companies') { + console.log("COMPANIES") + const { accessToken } = await fetchClientCredentialsAccessToken(); + requestConfig.headers.Authorization = `Bearer ${accessToken}`; + + + } + else if (requiresAuth) { const { accessToken } = await fetchAccessToken(); requestConfig.headers.Authorization = `Bearer ${accessToken}`; } @@ -58,6 +66,7 @@ function createProxyMiddleware(requiresAuth = true) { const isMultipart = req.headers['content-type']?.includes( 'multipart/form-data', ); + console.log("PROXYING", req.originalUrl) try { const response = await createProxyRequest(req.originalUrl, req.method, { diff --git a/example/src/CreateCompany.tsx b/example/src/CreateCompany.tsx index 837757751..d468f225f 100644 --- a/example/src/CreateCompany.tsx +++ b/example/src/CreateCompany.tsx @@ -66,9 +66,6 @@ const MultiStepForm = ({ switch (createCompanyBag.stepState.currentStep.name) { case 'select_country': - console.log("SELECTING CONUTRY") - console.log("components") - console.log(components) return (
{ + const { client } = useClient(); + return useMutation({ + mutationFn: (payload: CreateCompanyParams) => { + console.log("FIRING REQUEST", payload) + return postCreateCompany({ + client: client as Client, + headers: { + Authorization: ``, + }, + body: payload, + }); + }, + }); +}; + + diff --git a/src/flows/CreateCompany/components/CreateCompanyForm.tsx b/src/flows/CreateCompany/components/CreateCompanyForm.tsx index 7e52acf77..7b2ea0451 100644 --- a/src/flows/CreateCompany/components/CreateCompanyForm.tsx +++ b/src/flows/CreateCompany/components/CreateCompanyForm.tsx @@ -49,14 +49,14 @@ export function CreateCompanyForm({ await onSubmit( { company_owner_email: values.company_owner_email, - company_owner_name: values.company_owner_email, - country_code: values.company_owner_email, - desired_currency: values.company_owner_email, - name: values.company_owner_email, - phone_number: values.company_owner_email, - tax_number: values.company_owner_email, - tax_job_category: values.company_owner_email, - tax_servicing_countries: [values.company_owner_email], + company_owner_name: values.company_owner_name, + country_code: values.country_code, + desired_currency: values.desired_currency, + name: values.name, + phone_number: values.phone_number, + tax_number: values.tax_number, + tax_job_category: values.tax_job_category, + tax_servicing_countries: [], }); }; diff --git a/src/flows/CreateCompany/components/SelectCountryStep.tsx b/src/flows/CreateCompany/components/SelectCountryStep.tsx index 6c9c41eb2..14e487842 100644 --- a/src/flows/CreateCompany/components/SelectCountryStep.tsx +++ b/src/flows/CreateCompany/components/SelectCountryStep.tsx @@ -1,8 +1,8 @@ // TODO: Correct types later import { - SelectCountryFormPayload, - SelectCountrySuccess, -} from '@/src/flows/Onboarding/types'; + CompanyBasicInfoFormPayload, + CompanyBasicInfoSuccess, +} from '@/src/flows/CreateCompany/types'; import { NormalizedFieldError } from '@/src/lib/mutations'; import { $TSFixMe } from '@/src/types/remoteFlows'; import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; @@ -13,11 +13,11 @@ type SelectCountryStepProps = { /* * The function is called when the form is submitted. It receives the form values as an argument. */ - onSubmit?: (payload: SelectCountryFormPayload) => void | Promise; + onSubmit?: (payload: CompanyBasicInfoFormPayload) => void | Promise; /* * The function is called when the form submission is successful. */ - onSuccess?: (data: SelectCountrySuccess) => void | Promise; + onSuccess?: (data: CompanyBasicInfoSuccess) => void | Promise; /* * The function is called when an error occurs during form submission. */ @@ -40,10 +40,18 @@ export function SelectCountryStep({ const { createCompanyBag } = useCreateCompanyContext(); const handleSubmit = async (payload: $TSFixMe) => { try { - await onSubmit?.({ countryCode: payload.country }); + console.log(payload) + await onSubmit?.({ countryCode: payload.country_code, + companyOwnerEmail: payload.company_owner_email, + companyOwnerName: payload.company_owner_name, + desiredCurrency: payload.desired_currency, + phoneNumber: payload.phone_number, + taxNumber: payload.tax_number, + taxJobCategory: payload.tax_job_category + }); const response = await createCompanyBag.onSubmit(payload); if (response?.data) { - await onSuccess?.(response?.data as SelectCountrySuccess); + await onSuccess?.(response?.data as CompanyBasicInfoSuccess); createCompanyBag?.next(); return; } diff --git a/src/flows/CreateCompany/hooks.ts b/src/flows/CreateCompany/hooks.ts index cf0b65012..12a57638f 100644 --- a/src/flows/CreateCompany/hooks.ts +++ b/src/flows/CreateCompany/hooks.ts @@ -11,6 +11,10 @@ import { } from '@/src/flows/types'; import { ValidationResult } from '@remoteoss/remote-json-schema-form-kit'; import { CreateCompanyFlowProps } from '@/src/flows/CreateCompany/types'; +import { + CreateCompanyParams, +} from '@/src/client'; + import { createHeadlessForm } from '@/src/common/createHeadlessForm'; import { useClient } from '@/src/context'; import { selectCountryStepSchema } from '@/src/flows/CreateCompany/json-schemas/selectCountryStep'; @@ -24,6 +28,10 @@ import { import { Step, useStepState } from '@/src/flows/useStepState'; import { createStructuredError, prettifyFormValues } from '@/src/lib/utils'; import { JSFFieldset, Meta } from '@/src/types/remoteFlows'; +import { mutationToPromise } from '@/src/lib/mutations'; +import { + useCreateCompanyRequest, +} from '@/src/flows/CreateCompany/api'; type useCreateCompanyProps = Omit< CreateCompanyFlowProps, @@ -93,10 +101,33 @@ const useCountriesSchemaField = ( }; }; + +function nowUtcFormatted() { + const now = new Date(); + + const pad = (n: number) => String(n).padStart(2, '0'); + + return ( + now.getUTCFullYear() + '-' + + pad(now.getUTCMonth() + 1) + '-' + + pad(now.getUTCDate()) + ' ' + + pad(now.getUTCHours()) + ':' + + pad(now.getUTCMinutes()) + ':' + + pad(now.getUTCSeconds()) + 'Z' + ); +} export const useCreateCompany = ({ countryCode, options, }: useCreateCompanyProps) => { + +const createCompanyMutation = useCreateCompanyRequest(); + +const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( + createCompanyMutation, +); + + const [internalCountryCode, setInternalCountryCode] = useState( countryCode || null, ); @@ -186,6 +217,31 @@ export const useCreateCompany = ({ switch (stepState.currentStep.name) { case 'select_country': { setInternalCountryCode(parsedValues.country); + console.log("PARSED") + console.log(parsedValues) + const payload: CreateCompanyParams = { + address_details: { + address: "1709 Broderick St", + address_line_2: "Flat number 124", + city: "San Francisco", + postal_code: "94101-3344", + state: "CA" + + }, + country_code: parsedValues.country_code, + company_owner_email: parsedValues.company_owner_email, + company_owner_name: parsedValues.company_owner_name, + desired_currency: parsedValues.desired_currency, + name: parsedValues.name, + phone_number: parsedValues.phone_number, + tax_number: parsedValues.tax_number, + terms_of_service_accepted_at: nowUtcFormatted() + }; + + + console.log("FIRING") + const response = await createCompanyMutationAsync(payload); + console.log("RESPONSE", response) return Promise.resolve({ data: { countryCode: parsedValues.country_code } }); } diff --git a/src/flows/CreateCompany/json-schemas/selectCountryStep.ts b/src/flows/CreateCompany/json-schemas/selectCountryStep.ts index 2e5f4fb2f..bcd8ba3d4 100644 --- a/src/flows/CreateCompany/json-schemas/selectCountryStep.ts +++ b/src/flows/CreateCompany/json-schemas/selectCountryStep.ts @@ -13,10 +13,80 @@ export const selectCountryStepSchema = { inputType: 'select', }, }, + company_owner_email: { + title: 'Company Owner Email', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'text', + }, + + }, + company_owner_name: { + title: 'Company Owner Name', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'text', + }, + + }, + desired_currency: { + title: 'Desired Currency', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'text', + }, + + }, + name: { + title: 'Name', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'text', + }, + + }, + phone_number: { + title: 'Phone Number', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'text', + }, + + }, + tax_number: { + title: 'Tax Number', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'text', + }, + + }, + tax_job_category: { + title: 'Tax Job Category', + description: '', + type: 'string', + oneOf: [], + 'x-jsf-presentation': { + inputType: 'text', + }, + + }, }, - required: ['country_code'], + required: ['country_code', 'company_owner_email'], type: 'object', - 'x-jsf-order': ['country_code'], + 'x-jsf-order': ['country_code', 'company_owner_email'], }, }, }; diff --git a/src/flows/CreateCompany/types.ts b/src/flows/CreateCompany/types.ts index 211268033..cd5cbcab6 100644 --- a/src/flows/CreateCompany/types.ts +++ b/src/flows/CreateCompany/types.ts @@ -3,6 +3,20 @@ import { SelectCountryStep } from '@/src/flows/Onboarding/components/SelectCount import { FlowOptions, JSFModify } from '@/src/flows/types'; import { CreateCompanySubmit } from '@/src/flows/CreateCompany/components/CreateCompanySubmit'; +type countryFormFields = { + countryCode: string; + companyOwnerEmail: string; + companyOwnerName: string; + desiredCurrency: string; + phoneNumber: string; + taxNumber: string; + taxJobCategory: string; +} + +export type CompanyBasicInfoFormPayload = countryFormFields; + +export type CompanyBasicInfoSuccess = countryFormFields; + export type CreateCompanyRenderProps = { /** * The create company bag returned by the useCreateCompany hook. From 2723cc3df4024b12c45dfbbd6bc67b3d339bdad5 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Tue, 3 Feb 2026 15:58:17 +0000 Subject: [PATCH 06/10] include credentials --- src/auth/createClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/auth/createClient.ts b/src/auth/createClient.ts index 58950e048..1b99c17cb 100644 --- a/src/auth/createClient.ts +++ b/src/auth/createClient.ts @@ -50,6 +50,7 @@ export function createClient( return createHeyApiClient({ ...clientConfig, + credentials: "include", // for fetch headers: { ...clientConfig.headers, ...(isValidProxy ? options?.proxy?.headers : {}), From 381dc14e3ad7aa07b01c20af920735c472ddf081 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Tue, 3 Feb 2026 17:15:17 +0000 Subject: [PATCH 07/10] wip --- example/api/proxy.js | 3 +-- example/package-lock.json | 4 +-- example/src/CreateCompany.tsx | 47 ++++++++++++++++++++++++++++++----- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/example/api/proxy.js b/example/api/proxy.js index c62829c9b..1c12ff650 100644 --- a/example/api/proxy.js +++ b/example/api/proxy.js @@ -41,8 +41,7 @@ async function createProxyRequest(path, method = 'GET', options = {}) { console.log("REQUIRES AUTH", requiresAuth) // Add authentication if required - if (requiresAuth && path == '/v1/companies') { - console.log("COMPANIES") + if (requiresAuth && path.startsWith('/v1/companies/')) { const { accessToken } = await fetchClientCredentialsAccessToken(); requestConfig.headers.Authorization = `Bearer ${accessToken}`; diff --git a/example/package-lock.json b/example/package-lock.json index 4b957616a..1b84632cb 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -37,7 +37,7 @@ }, "..": { "name": "@remoteoss/remote-flows", - "version": "1.7.0", + "version": "1.10.0", "dependencies": { "@hookform/resolvers": "^4.1.3", "@radix-ui/react-accordion": "^1.2.12", @@ -117,7 +117,7 @@ "vitest": "^4.0.13" }, "engines": { - "node": ">=24" + "node": ">=20" }, "peerDependencies": { "react": "^18.3.1", diff --git a/example/src/CreateCompany.tsx b/example/src/CreateCompany.tsx index d468f225f..e9486d1a7 100644 --- a/example/src/CreateCompany.tsx +++ b/example/src/CreateCompany.tsx @@ -5,6 +5,8 @@ import { CreateCompanyFlow, CreateCompanyRenderProps, JSFCustomComponentProps, + CompanyAddressDetailsFormPayload, + CompanyAddressDetailsSuccess, } from '@remoteoss/remote-flows'; import { Card, @@ -39,7 +41,7 @@ const Switcher = (props: JSFCustomComponentProps) => { const STEPS = [ 'Select Country', - 'Review', + 'Address Details', ]; type MultiStepFormProps = { @@ -53,8 +55,8 @@ const MultiStepForm = ({ }: MultiStepFormProps) => { const { SubmitButton, - BackButton, SelectCountryStep, + AddressDetailsStep, } = components; const [errors, setErrors] = useState<{ apiError: string; @@ -83,6 +85,7 @@ const MultiStepForm = ({ fieldErrors: NormalizedFieldError[]; }) => setErrors({ apiError: error.message, fieldErrors })} /> +
Continue @@ -90,12 +93,44 @@ const MultiStepForm = ({
); - case 'review': { + case 'address_details': return (
+ + console.log('address details payload', payload) + } + onSuccess={(response: CompanyAddressDetailsSuccess) => + console.log('address details response', response) + } + onError={({ + error, + fieldErrors, + }: { + error: Error; + fieldErrors: NormalizedFieldError[]; + }) => setErrors({ apiError: error.message, fieldErrors })} + /> + +
+ + + Complete + +
); - } + default: + return null; } }; @@ -137,8 +172,8 @@ const CreateCompanyRender = ({ const Header = () => { return (
-

Contractor Onboarding

-

Adding a new contractor is simple and fast.

+

Create Company

+

Create a new company and complete the address details.

); }; From 90c19980652dc5078e12063076e9a27d91e1e4c7 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Tue, 3 Feb 2026 17:15:28 +0000 Subject: [PATCH 08/10] wip --- src/flows/CreateCompany/CreateCompany.tsx | 2 + src/flows/CreateCompany/api.ts | 31 ++++ .../components/AddressDetailsStep.tsx | 69 ++++++++ .../components/CreateCompanyForm.tsx | 22 +-- .../components/SelectCountryStep.tsx | 1 - src/flows/CreateCompany/hooks.ts | 164 ++++++++++++++---- src/flows/CreateCompany/types.ts | 10 +- src/flows/CreateCompany/utils.ts | 4 +- 8 files changed, 249 insertions(+), 54 deletions(-) create mode 100644 src/flows/CreateCompany/components/AddressDetailsStep.tsx diff --git a/src/flows/CreateCompany/CreateCompany.tsx b/src/flows/CreateCompany/CreateCompany.tsx index 5c9ecdc19..3198bb4b6 100644 --- a/src/flows/CreateCompany/CreateCompany.tsx +++ b/src/flows/CreateCompany/CreateCompany.tsx @@ -3,6 +3,7 @@ import { CreateCompanyFlowProps} from '@/src/flows/CreateCompany/types' import { useCreateCompany} from '@/src/flows/CreateCompany/hooks' import { CreateCompanyContext} from '@/src/flows/CreateCompany/context' import { SelectCountryStep } from '@/src/flows/CreateCompany/components/SelectCountryStep'; +import { AddressDetailsStep } from '@/src/flows/CreateCompany/components/AddressDetailsStep'; import { CreateCompanySubmit } from '@/src/flows/CreateCompany/components/CreateCompanySubmit'; import { useId } from 'react'; export const CreateCompanyFlow = ({ @@ -23,6 +24,7 @@ export const CreateCompanyFlow = ({ createCompanyBag, components: { SelectCountryStep: SelectCountryStep, + AddressDetailsStep: AddressDetailsStep, SubmitButton: CreateCompanySubmit }, })} diff --git a/src/flows/CreateCompany/api.ts b/src/flows/CreateCompany/api.ts index b5b16dd91..3a3bea616 100644 --- a/src/flows/CreateCompany/api.ts +++ b/src/flows/CreateCompany/api.ts @@ -5,6 +5,8 @@ import { Client } from '@/src/client/client'; import { CreateCompanyParams, postCreateCompany, + patchUpdateCompany2, + UpdateCompanyParams, } from '@/src/client'; @@ -24,4 +26,33 @@ export const useCreateCompanyRequest = () => { }); }; +export const useUpdateCompanyRequest = () => { + const { client } = useClient(); + return useMutation({ + mutationFn: ({ + companyId, + payload, + jsonSchemaVersion, + }: { + companyId: string; + payload: UpdateCompanyParams; + jsonSchemaVersion?: number | 'latest'; + }) => { + return patchUpdateCompany2({ + client: client as Client, + headers: { + Authorization: ``, + }, + path: { + company_id: companyId, + }, + body: payload, + query: { + address_details_json_schema_version: jsonSchemaVersion, + }, + }); + }, + }); +}; + diff --git a/src/flows/CreateCompany/components/AddressDetailsStep.tsx b/src/flows/CreateCompany/components/AddressDetailsStep.tsx new file mode 100644 index 000000000..703e30c60 --- /dev/null +++ b/src/flows/CreateCompany/components/AddressDetailsStep.tsx @@ -0,0 +1,69 @@ +import { + CompanyAddressDetailsFormPayload, + CompanyAddressDetailsSuccess, +} from '@/src/flows/CreateCompany/types'; +import { NormalizedFieldError } from '@/src/lib/mutations'; +import { $TSFixMe } from '@/src/types/remoteFlows'; +import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; +import { CreateCompanyForm } from '@/src/flows/CreateCompany/components/CreateCompanyForm'; +import { handleStepError } from '@/src/lib/utils'; + +type AddressDetailsStepProps = { + /* + * The function is called when the form is submitted. It receives the form values as an argument. + */ + onSubmit?: (payload: CompanyAddressDetailsFormPayload) => void | Promise; + /* + * The function is called when the form submission is successful. + */ + onSuccess?: (data: CompanyAddressDetailsSuccess) => void | Promise; + /* + * The function is called when an error occurs during form submission. + */ + onError?: ({ + error, + rawError, + fieldErrors, + }: { + error: Error; + rawError: Record; + fieldErrors: NormalizedFieldError[]; + }) => void; +}; + +export function AddressDetailsStep({ + onSubmit, + onSuccess, + onError, +}: AddressDetailsStepProps) { + const { createCompanyBag } = useCreateCompanyContext(); + const handleSubmit = async (payload: $TSFixMe) => { + try { + await onSubmit?.(payload); + const response = await createCompanyBag.onSubmit(payload); + if (response?.data) { + await onSuccess?.(response?.data as CompanyAddressDetailsSuccess); + return; + } + } catch (error: unknown) { + const structuredError = handleStepError( + error, + createCompanyBag.meta?.fields?.address_details, + ); + + onError?.(structuredError); + } + }; + + const initialValues = + createCompanyBag.stepState.values?.address_details || + createCompanyBag.initialValues.address_details; + + return ( + + ); +} + diff --git a/src/flows/CreateCompany/components/CreateCompanyForm.tsx b/src/flows/CreateCompany/components/CreateCompanyForm.tsx index 7b2ea0451..62a54bb17 100644 --- a/src/flows/CreateCompany/components/CreateCompanyForm.tsx +++ b/src/flows/CreateCompany/components/CreateCompanyForm.tsx @@ -3,16 +3,12 @@ import { JSONSchemaFormFields } from '@/src/components/form/JSONSchemaForm'; import { Form } from '@/src/components/ui/form'; import { useForm } from 'react-hook-form'; import { useJsonSchemasValidationFormResolver } from '@/src/components/form/validationResolver'; -import { BasicInformationFormPayload } from '@/src/flows/CreateCompany/types'; import { Components } from '@/src/types/remoteFlows'; import { useCreateCompanyContext } from '@/src/flows/CreateCompany/context'; import { useEffect } from 'react'; type CreateCompanyFormProps = { - onSubmit: ( - payload: - | BasicInformationFormPayload - ) => Promise; + onSubmit: (payload: Record) => Promise; components?: Components; fields?: JSFFields; defaultValues: Record; @@ -44,20 +40,8 @@ export function CreateCompanyForm({ } }, [form.setValue, formRef]); - const handleSubmit = async (values: Record) => { - console.log("SUBMITTING", values) - await onSubmit( - { - company_owner_email: values.company_owner_email, - company_owner_name: values.company_owner_name, - country_code: values.country_code, - desired_currency: values.desired_currency, - name: values.name, - phone_number: values.phone_number, - tax_number: values.tax_number, - tax_job_category: values.tax_job_category, - tax_servicing_countries: [], - }); + const handleSubmit = async (values: Record) => { + await onSubmit(values); }; return ( diff --git a/src/flows/CreateCompany/components/SelectCountryStep.tsx b/src/flows/CreateCompany/components/SelectCountryStep.tsx index 14e487842..194f97d13 100644 --- a/src/flows/CreateCompany/components/SelectCountryStep.tsx +++ b/src/flows/CreateCompany/components/SelectCountryStep.tsx @@ -40,7 +40,6 @@ export function SelectCountryStep({ const { createCompanyBag } = useCreateCompanyContext(); const handleSubmit = async (payload: $TSFixMe) => { try { - console.log(payload) await onSubmit?.({ countryCode: payload.country_code, companyOwnerEmail: payload.company_owner_email, companyOwnerName: payload.company_owner_name, diff --git a/src/flows/CreateCompany/hooks.ts b/src/flows/CreateCompany/hooks.ts index 12a57638f..2b91521e4 100644 --- a/src/flows/CreateCompany/hooks.ts +++ b/src/flows/CreateCompany/hooks.ts @@ -13,6 +13,8 @@ import { ValidationResult } from '@remoteoss/remote-json-schema-form-kit'; import { CreateCompanyFlowProps } from '@/src/flows/CreateCompany/types'; import { CreateCompanyParams, + getShowFormCountry, + UpdateCompanyParams, } from '@/src/client'; import { createHeadlessForm } from '@/src/common/createHeadlessForm'; @@ -31,6 +33,7 @@ import { JSFFieldset, Meta } from '@/src/types/remoteFlows'; import { mutationToPromise } from '@/src/lib/mutations'; import { useCreateCompanyRequest, + useUpdateCompanyRequest, } from '@/src/flows/CreateCompany/api'; type useCreateCompanyProps = Omit< @@ -116,27 +119,76 @@ function nowUtcFormatted() { pad(now.getUTCSeconds()) + 'Z' ); } +const useAddressDetailsSchema = ({ + countryCode, + fieldValues, + options, +}: { + countryCode: string | null; + fieldValues: FieldValues; + options?: FlowOptions & { queryOptions?: { enabled?: boolean } }; +}) => { + const { client } = useClient(); + return useQuery({ + queryKey: ['company-address-details-schema', countryCode], + retry: false, + queryFn: async () => { + if (!countryCode) { + throw new Error('Country code is required'); + } + const response = await getShowFormCountry({ + client: client as Client, + headers: { + Authorization: ``, + }, + path: { + country_code: countryCode, + form: 'address_details', + }, + query: { + json_schema_version: options?.jsonSchemaVersion?.form_schema?.address_details || 'latest', + }, + }); + + if (response.error || !response.data) { + throw new Error('Failed to fetch address details schema'); + } + + return response; + }, + enabled: options?.queryOptions?.enabled && !!countryCode, + select: ({ data }) => { + const jsfSchema = data?.data || {}; + return createHeadlessForm(jsfSchema, fieldValues, options); + }, + }); +}; + export const useCreateCompany = ({ countryCode, options, }: useCreateCompanyProps) => { const createCompanyMutation = useCreateCompanyRequest(); +const updateCompanyMutation = useUpdateCompanyRequest(); const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( createCompanyMutation, ); - +const { mutateAsync: updateCompanyMutationAsync } = mutationToPromise( + updateCompanyMutation, +); const [internalCountryCode, setInternalCountryCode] = useState( countryCode || null, ); + const [createdCompanyId, setCreatedCompanyId] = useState(null); const fieldsMetaRef = useRef<{ select_country: Meta; - basic_information: Meta; + address_details: Meta; }>({ select_country: {}, - basic_information: {}, + address_details: {}, }); const { @@ -159,13 +211,30 @@ const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( }, }); + const { data: addressDetailsForm, isLoading: isLoadingAddressDetails } = + useAddressDetailsSchema({ + countryCode: internalCountryCode, + fieldValues: fieldValues.address_details || {}, + options: { + ...options, + jsfModify: options?.jsfModify?.address_details, + queryOptions: { + enabled: + stepState.currentStep.name === 'address_details' && + !!internalCountryCode && + !!createdCompanyId, + }, + }, + }); + const stepFields: Record = useMemo( () => ({ select_country: selectCountryForm?.fields || [], - review: [], + address_details: addressDetailsForm?.fields || [], }), [ selectCountryForm?.fields, + addressDetailsForm?.fields, ], ); @@ -174,7 +243,7 @@ const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( JSFFieldset | null | undefined > = { select_country: null, - review: null + address_details: addressDetailsForm?.meta?.['x-jsf-fieldsets'] || null, }; @@ -186,13 +255,20 @@ const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( [stepFields.select_country, internalCountryCode], ); + const addressDetailsInitialValues = useMemo( + () => + getInitialValues(stepFields.address_details, {}), + [stepFields.address_details], + ); const initialValues = useMemo(() => { return { select_country: selectCountryInitialValues, + address_details: addressDetailsInitialValues, }; }, [ selectCountryInitialValues, + addressDetailsInitialValues, ]); const goTo = (step: keyof typeof STEPS) => { @@ -203,6 +279,9 @@ const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( if (selectCountryForm && stepState.currentStep.name === 'select_country') { return values; } + if (addressDetailsForm && stepState.currentStep.name === 'address_details') { + return values; + } return {}; }; @@ -216,35 +295,55 @@ const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( const parsedValues = await parseFormValues(values); switch (stepState.currentStep.name) { case 'select_country': { - setInternalCountryCode(parsedValues.country); - console.log("PARSED") - console.log(parsedValues) - const payload: CreateCompanyParams = { - address_details: { - address: "1709 Broderick St", - address_line_2: "Flat number 124", - city: "San Francisco", - postal_code: "94101-3344", - state: "CA" - - }, - country_code: parsedValues.country_code, - company_owner_email: parsedValues.company_owner_email, - company_owner_name: parsedValues.company_owner_name, - desired_currency: parsedValues.desired_currency, - name: parsedValues.name, - phone_number: parsedValues.phone_number, - tax_number: parsedValues.tax_number, - terms_of_service_accepted_at: nowUtcFormatted() - }; - - - console.log("FIRING") + setInternalCountryCode(parsedValues.country_code); + const payload: CreateCompanyParams = { + country_code: parsedValues.country_code, + company_owner_email: parsedValues.company_owner_email, + company_owner_name: parsedValues.company_owner_name, + desired_currency: parsedValues.desired_currency, + name: parsedValues.name, + phone_number: parsedValues.phone_number, + tax_number: parsedValues.tax_number, + terms_of_service_accepted_at: nowUtcFormatted(), + }; + const response = await createCompanyMutationAsync(payload); - console.log("RESPONSE", response) + // Handle both CompanyResponse and CompanyWithTokensResponse structures + // CompanyResponse: { data: { company?: Company } } + // CompanyWithTokensResponse: { company?: Company; tokens?: OAuth2Tokens } + const responseData = response.data?.data; + let companyId: string | undefined; + if (responseData) { + if ('data' in responseData && responseData.data) { + // CompanyResponse structure: response.data.data.data.company.id + companyId = (responseData.data as { company?: { id: string } }).company?.id; + } else if ('company' in responseData) { + // CompanyWithTokensResponse structure: response.data.data.company.id + companyId = (responseData as { company?: { id: string } }).company?.id; + } + } + if (companyId) { + setCreatedCompanyId(companyId); + } return Promise.resolve({ data: { countryCode: parsedValues.country_code } }); } + case 'address_details': { + if (!createdCompanyId) { + throw createStructuredError('Company ID is required to update address details'); + } + const payload: UpdateCompanyParams = { + address_details: parsedValues, + }; + + const response = await updateCompanyMutationAsync({ + companyId: createdCompanyId, + payload, + jsonSchemaVersion: options?.jsonSchemaVersion?.form_schema?.address_details, + }); + return Promise.resolve({ data: response.data }); + } + default: { throw createStructuredError('Invalid step state'); } @@ -252,7 +351,7 @@ const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( } const isLoading = - isLoadingCountries + isLoadingCountries || isLoadingAddressDetails return { /** @@ -333,6 +432,9 @@ const { mutateAsync: createCompanyMutationAsync } = mutationToPromise( if (stepState.currentStep.name === 'select_country') { return selectCountryForm.handleValidation(values); } + if (stepState.currentStep.name === 'address_details' && addressDetailsForm) { + return addressDetailsForm.handleValidation(values); + } return null; }, diff --git a/src/flows/CreateCompany/types.ts b/src/flows/CreateCompany/types.ts index cd5cbcab6..e30db20d6 100644 --- a/src/flows/CreateCompany/types.ts +++ b/src/flows/CreateCompany/types.ts @@ -1,5 +1,6 @@ import { useCreateCompany } from '@/src/flows/CreateCompany/hooks'; -import { SelectCountryStep } from '@/src/flows/Onboarding/components/SelectCountryStep'; +import { SelectCountryStep } from '@/src/flows/CreateCompany/components/SelectCountryStep'; +import { AddressDetailsStep } from '@/src/flows/CreateCompany/components/AddressDetailsStep'; import { FlowOptions, JSFModify } from '@/src/flows/types'; import { CreateCompanySubmit } from '@/src/flows/CreateCompany/components/CreateCompanySubmit'; @@ -17,6 +18,10 @@ export type CompanyBasicInfoFormPayload = countryFormFields; export type CompanyBasicInfoSuccess = countryFormFields; +export type CompanyAddressDetailsFormPayload = Record; + +export type CompanyAddressDetailsSuccess = Record; + export type CreateCompanyRenderProps = { /** * The create company bag returned by the useCreateCompany hook. @@ -27,9 +32,11 @@ export type CreateCompanyRenderProps = { /** * The components used in the create company flow. * @see {@link SelectCountryStep} + * @see {@link AddressDetailsStep} */ components: { SelectCountryStep: typeof SelectCountryStep; + AddressDetailsStep: typeof AddressDetailsStep; SubmitButton: typeof CreateCompanySubmit; }; @@ -55,6 +62,7 @@ export type CreateCompanyFlowProps = { options?: Omit & { jsfModify?: { select_country?: JSFModify; + address_details?: JSFModify; }; }; diff --git a/src/flows/CreateCompany/utils.ts b/src/flows/CreateCompany/utils.ts index 6a902ae21..88c5e183d 100644 --- a/src/flows/CreateCompany/utils.ts +++ b/src/flows/CreateCompany/utils.ts @@ -2,9 +2,9 @@ import { Step } from '@/src/flows/useStepState'; type StepKeys = | 'select_country' - | 'review'; + | 'address_details'; export const STEPS: Record> = { select_country: { index: 0, name: 'select_country' }, - review: { index: 1, name: 'review' }, + address_details: { index: 1, name: 'address_details' }, } as const; From dfc95724bbfee5840269292485f76af46d0481c3 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Fri, 6 Feb 2026 13:43:11 +0000 Subject: [PATCH 09/10] feat: support empty auth --- src/auth/createClient.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/auth/createClient.ts b/src/auth/createClient.ts index 1b99c17cb..01788f972 100644 --- a/src/auth/createClient.ts +++ b/src/auth/createClient.ts @@ -25,7 +25,7 @@ function isValidUrl(url: string) { } export function createClient( - auth: () => Promise, + auth?: () => Promise, options?: Options, ) { const sessionRef = { @@ -50,7 +50,7 @@ export function createClient( return createHeyApiClient({ ...clientConfig, - credentials: "include", // for fetch + credentials: 'include', headers: { ...clientConfig.headers, ...(isValidProxy ? options?.proxy?.headers : {}), @@ -59,6 +59,10 @@ export function createClient( }, baseUrl: isValidProxy ? options.proxy?.url : baseUrl, auth: async () => { + if (!auth) { + return undefined; + } + function hasTokenExpired(expiresAt: number | undefined) { return !expiresAt || Date.now() + 60000 > expiresAt; } @@ -78,6 +82,7 @@ export function createClient( return undefined; } } + return sessionRef.current?.accessToken; }, }); From 283f3b467db29d881c57fdbe55c731a668cf7b4d Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Mon, 9 Feb 2026 11:38:30 +0000 Subject: [PATCH 10/10] wip --- example/api/proxy.js | 2 +- example/package-lock.json | 2 +- .../components/AddressDetailsStep.tsx | 7 ++ .../components/SelectCountryStep.tsx | 10 ++- src/flows/CreateCompany/hooks.ts | 68 ++++++++++++++++++- .../json-schemas/selectCountryStep.ts | 12 +--- src/flows/CreateCompany/types.ts | 2 - 7 files changed, 84 insertions(+), 19 deletions(-) diff --git a/example/api/proxy.js b/example/api/proxy.js index 1c12ff650..f36aa7356 100644 --- a/example/api/proxy.js +++ b/example/api/proxy.js @@ -41,7 +41,7 @@ async function createProxyRequest(path, method = 'GET', options = {}) { console.log("REQUIRES AUTH", requiresAuth) // Add authentication if required - if (requiresAuth && path.startsWith('/v1/companies/')) { + if (requiresAuth && (path.startsWith('/v1/countries') || path.startsWith('/v1/companies') || path.startsWith('/v1/company-currencies'))) { const { accessToken } = await fetchClientCredentialsAccessToken(); requestConfig.headers.Authorization = `Bearer ${accessToken}`; diff --git a/example/package-lock.json b/example/package-lock.json index 1b84632cb..c59c06d48 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -37,7 +37,7 @@ }, "..": { "name": "@remoteoss/remote-flows", - "version": "1.10.0", + "version": "1.11.0", "dependencies": { "@hookform/resolvers": "^4.1.3", "@radix-ui/react-accordion": "^1.2.12", diff --git a/src/flows/CreateCompany/components/AddressDetailsStep.tsx b/src/flows/CreateCompany/components/AddressDetailsStep.tsx index 703e30c60..f7b2b733f 100644 --- a/src/flows/CreateCompany/components/AddressDetailsStep.tsx +++ b/src/flows/CreateCompany/components/AddressDetailsStep.tsx @@ -45,6 +45,13 @@ export function AddressDetailsStep({ await onSuccess?.(response?.data as CompanyAddressDetailsSuccess); return; } + if (response?.error) { + const structuredError = handleStepError( + response, + createCompanyBag.meta?.fields?.address_details, + ); + onError?.(structuredError); + } } catch (error: unknown) { const structuredError = handleStepError( error, diff --git a/src/flows/CreateCompany/components/SelectCountryStep.tsx b/src/flows/CreateCompany/components/SelectCountryStep.tsx index 194f97d13..1a9d3fc03 100644 --- a/src/flows/CreateCompany/components/SelectCountryStep.tsx +++ b/src/flows/CreateCompany/components/SelectCountryStep.tsx @@ -45,8 +45,7 @@ export function SelectCountryStep({ companyOwnerName: payload.company_owner_name, desiredCurrency: payload.desired_currency, phoneNumber: payload.phone_number, - taxNumber: payload.tax_number, - taxJobCategory: payload.tax_job_category + taxNumber: payload.tax_number }); const response = await createCompanyBag.onSubmit(payload); if (response?.data) { @@ -54,6 +53,13 @@ export function SelectCountryStep({ createCompanyBag?.next(); return; } + if (response?.error) { + const structuredError = handleStepError( + response, + createCompanyBag.meta?.fields?.select_country, + ); + onError?.(structuredError); + } } catch (error: unknown) { const structuredError = handleStepError( error, diff --git a/src/flows/CreateCompany/hooks.ts b/src/flows/CreateCompany/hooks.ts index 2b91521e4..b573147a8 100644 --- a/src/flows/CreateCompany/hooks.ts +++ b/src/flows/CreateCompany/hooks.ts @@ -25,6 +25,7 @@ import { } from '@/src/flows/CreateCompany/utils'; import { getSupportedCountry, + getIndexCompanyCurrency, } from '@/src/client'; import { Step, useStepState } from '@/src/flows/useStepState'; @@ -76,12 +77,44 @@ const useCountries = (queryOptions?: { enabled?: boolean }) => { }, }); }; +const useCompanyCurrencies = (queryOptions?: { enabled?: boolean }) => { + const { client } = useClient(); + return useQuery({ + ...queryOptions, + queryKey: ['company-currencies'], + retry: false, + queryFn: async () => { + const response = await getIndexCompanyCurrency({ + client: client as Client, + headers: { + Authorization: ``, + }, + }); + + if (response.error || !response.data) { + throw new Error('Failed to fetch company currencies'); + } + + return response; + }, + select: ({ data }) => { + return ( + data?.data?.company_currencies.map((currency) => ({ + value: currency.code, + label: currency.code, + })) || [] + ); + }, + }); +}; + const useCountriesSchemaField = ( options?: Omit & { queryOptions?: { enabled?: boolean }; }, ) => { - const { data: countries, isLoading } = useCountries(options?.queryOptions); + const { data: countries, isLoading: isLoadingCountries } = useCountries(options?.queryOptions); + const { data: currencies, isLoading: isLoadingCurrencies } = useCompanyCurrencies(options?.queryOptions); const selectCountryForm = createHeadlessForm( selectCountryStepSchema.data.schema, @@ -98,8 +131,17 @@ const useCountriesSchemaField = ( } } + if (currencies) { + const currencyField = selectCountryForm.fields.find( + (field) => field.name === 'desired_currency', + ); + if (currencyField) { + currencyField.options = currencies; + } + } + return { - isLoading, + isLoading: isLoadingCountries || isLoadingCurrencies, selectCountryForm, }; }; @@ -308,6 +350,17 @@ const { mutateAsync: updateCompanyMutationAsync } = mutationToPromise( }; const response = await createCompanyMutationAsync(payload); + + // Check for errors from the mutation + if (response.error) { + return Promise.resolve({ + data: null, + error: response.error, + rawError: response.rawError, + fieldErrors: response.fieldErrors, + }); + } + // Handle both CompanyResponse and CompanyWithTokensResponse structures // CompanyResponse: { data: { company?: Company } } // CompanyWithTokensResponse: { company?: Company; tokens?: OAuth2Tokens } @@ -341,6 +394,17 @@ const { mutateAsync: updateCompanyMutationAsync } = mutationToPromise( payload, jsonSchemaVersion: options?.jsonSchemaVersion?.form_schema?.address_details, }); + + // Check for errors from the mutation + if (response.error) { + return Promise.resolve({ + data: null, + error: response.error, + rawError: response.rawError, + fieldErrors: response.fieldErrors, + }); + } + return Promise.resolve({ data: response.data }); } diff --git a/src/flows/CreateCompany/json-schemas/selectCountryStep.ts b/src/flows/CreateCompany/json-schemas/selectCountryStep.ts index bcd8ba3d4..30b1096fe 100644 --- a/src/flows/CreateCompany/json-schemas/selectCountryStep.ts +++ b/src/flows/CreateCompany/json-schemas/selectCountryStep.ts @@ -39,7 +39,7 @@ export const selectCountryStepSchema = { type: 'string', oneOf: [], 'x-jsf-presentation': { - inputType: 'text', + inputType: 'select', }, }, @@ -73,16 +73,6 @@ export const selectCountryStepSchema = { }, }, - tax_job_category: { - title: 'Tax Job Category', - description: '', - type: 'string', - oneOf: [], - 'x-jsf-presentation': { - inputType: 'text', - }, - - }, }, required: ['country_code', 'company_owner_email'], type: 'object', diff --git a/src/flows/CreateCompany/types.ts b/src/flows/CreateCompany/types.ts index e30db20d6..cc2845b1f 100644 --- a/src/flows/CreateCompany/types.ts +++ b/src/flows/CreateCompany/types.ts @@ -11,7 +11,6 @@ type countryFormFields = { desiredCurrency: string; phoneNumber: string; taxNumber: string; - taxJobCategory: string; } export type CompanyBasicInfoFormPayload = countryFormFields; @@ -83,7 +82,6 @@ export type BasicInformationFormPayload = { name: string; phone_number: string; tax_number: string; - tax_job_category: string; tax_servicing_countries: string[]; };