Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e25098b
refactor(contract-documents): move the contract-details helpers to co…
Sep 17, 2026
244968e
feat(contract-document): render the contract details and create the c…
Sep 17, 2026
a21c314
fix(contract-document): restore entered contract details on back, rew…
Sep 17, 2026
fd25db1
fix(contract-document): attach field labels to submission errors
Sep 17, 2026
9041028
fix(contract-document): keep field metadata reachable from an in-flig…
Sep 17, 2026
8beaef6
chore(contractor-onboarding): drop the doc comments orphaned by the move
Sep 17, 2026
f0b021a
fix(contract-document): fail the contract-details schema query on an …
Sep 17, 2026
86b40d4
fix(contract-document): prefill the service start date with today
Sep 17, 2026
47b8245
fix(contract-document): drop the start-date mismatch note from the st…
Sep 17, 2026
3c28ec8
fix(contract-document): expose query errors on the bag and block subm…
Sep 17, 2026
c366b27
refactor(contract-document): drop the unused form values type
Sep 17, 2026
0be33c4
docs(contract-document): document the error key and the standard cont…
Sep 17, 2026
c728705
test(contract-document): assert the disclaimer shows for a standard c…
Sep 17, 2026
920800d
fix(contract-document): report a failed employment fetch and keep tod…
Sep 22, 2026
f1bf337
docs(contract-document): any edit clears the AI misclassification war…
Sep 22, 2026
da2fcad
refactor(contractor-onboarding): re-export the moved contract-details…
Sep 22, 2026
6135135
fix(contract-document): keep the flow usable when the contract docume…
Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 79 additions & 24 deletions example/src/ContractDocument.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,83 @@
import { ContractDocumentFlow } from '@remoteoss/remote-flows';
import {
ContractDocumentFlow,
ContractDocumentForm,
ContractDocumentSubmitButton,
NormalizedFieldError,
} from '@remoteoss/remote-flows';
import { useState } from 'react';
import { AlertError } from './AlertError';
import { EmploymentIdForm } from './components/EmploymentIdForm';
import { RemoteFlows } from './RemoteFlows';
import './css/main.css';

function CreateContractDocument({ employmentId }: { employmentId: string }) {
const [errors, setErrors] = useState<{
apiError: string;
fieldErrors: NormalizedFieldError[];
}>({ apiError: '', fieldErrors: [] });

return (
<ContractDocumentFlow
employmentId={employmentId}
render={(contractDocumentBag) => {
if (contractDocumentBag.isLoading) {
return <div>Loading…</div>;
return <div>Loading contract document...</div>;
}

const { steps, stepState } = contractDocumentBag;

return (
<div className='onboarding-form-container'>
<h2>Create contract document</h2>
<p>
{contractDocumentBag.employment?.full_name}
{contractDocumentBag.isContractorOfRecord &&
' (Contractor of Record)'}
</p>
<p>To be continued…</p>
</div>
<>
<div className='steps-navigation'>
<ul>
{steps.map((step) => (
<li
key={step.name}
className={`step-item ${step.name === stepState.currentStep.name ? 'active' : ''}`}
>
{step.index + 1}. {step.label}
</li>
))}
</ul>
</div>
<div className='card' style={{ marginBottom: '20px' }}>
<h1 className='heading'>
{stepState.currentStep.name === 'contract_details'
? 'Contract Details'
: 'Contract Preview'}
</h1>
{stepState.currentStep.name === 'contract_details' ? (
<>
<ContractDocumentForm
onError={({ error, fieldErrors }) =>
setErrors({ apiError: error.message, fieldErrors })
}
/>
<AlertError errors={errors} />
{contractDocumentBag.canSkipAiValidation && (
<p className='mt-3'>
AI validation detected potential compliance issues. Edit
the Services and Deliverables field above, or continue at
your own risk by submitting again.
</p>
)}
<ContractDocumentSubmitButton
className='submit-button mt-3'
onClick={() => setErrors({ apiError: '', fieldErrors: [] })}
>
{contractDocumentBag.canSkipAiValidation
? 'Continue anyway'
: 'Next Step'}
</ContractDocumentSubmitButton>
</>
) : (
<p>
Contract document {contractDocumentBag.contractDocumentId}. To
be continued…
</p>
)}
</div>
</>
);
}}
/>
Expand All @@ -32,19 +87,19 @@ function CreateContractDocument({ employmentId }: { employmentId: string }) {
export function ContractDocument() {
const [employmentId, setEmploymentId] = useState<string | null>(null);

if (employmentId) {
return (
<RemoteFlows proxy={{ url: window.location.origin }}>
<CreateContractDocument employmentId={employmentId} />
</RemoteFlows>
);
}

return (
<RemoteFlows proxy={{ url: window.location.origin }}>
<div style={{ width: 640, padding: 20, margin: '80px auto' }}>
{employmentId ? (
<CreateContractDocument employmentId={employmentId} />
) : (
<EmploymentIdForm
defaultValue={import.meta.env.VITE_CONTRACT_DOCUMENT_EMPLOYMENT_ID}
submitLabel='Create contract document'
onSubmit={setEmploymentId}
/>
)}
</div>
</RemoteFlows>
<EmploymentIdForm
defaultValue={import.meta.env.VITE_CONTRACT_DOCUMENT_EMPLOYMENT_ID}
submitLabel='Create contract document'
onSubmit={setEmploymentId}
/>
);
}
22 changes: 15 additions & 7 deletions src/common/api/contractor-contract-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,22 @@ const useContractorOnboardingDetailsSchema = ({
],
retry: false,
queryFn: async () => {
return getV1CountriesCountryCodeContractorContractDetails({
client: client as Client,
path: { country_code: countryCode },
query: {
json_schema_version: 1,
employment_id: employmentId,
const response = await getV1CountriesCountryCodeContractorContractDetails(
{
client: client as Client,
path: { country_code: countryCode },
query: {
json_schema_version: 1,
employment_id: employmentId,
},
},
});
);

if (response.error || !response.data) {
throw new Error('Failed to fetch contractor contract details schema');
}

return response;
},
enabled: options?.queryOptions?.enabled,
select: ({ data }) => {
Expand Down
35 changes: 33 additions & 2 deletions src/common/contract-documents/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { queryOptions, useQuery } from '@tanstack/react-query';
import { getV1EmploymentsEmploymentIdContractDocuments } from '@/src/client';
import { queryOptions, useMutation, useQuery } from '@tanstack/react-query';
import {
CreateContractDocument,
getV1EmploymentsEmploymentIdContractDocuments,
postV1ContractorsEmploymentsEmploymentIdContractDocuments,
} from '@/src/client';
import { Client } from '@/src/client/client';
import { useClient } from '@/src/context';

Expand Down Expand Up @@ -39,3 +43,30 @@ export const useGetContractDocuments = (
select: ({ data }) => data?.data?.contract_documents,
});
};

/**
* Saves the contractor details data
* @param employmentId - The employment ID
* @param payload - The payload
* @returns The contractor contract document
*/
export const useCreateContractorContractDocument = () => {
const { client } = useClient();
return useMutation({
mutationFn: async ({
employmentId,
payload,
}: {
employmentId: string;
payload: CreateContractDocument;
}) => {
return postV1ContractorsEmploymentsEmploymentIdContractDocuments({
client: client as Client,
body: payload,
path: {
employment_id: employmentId,
},
});
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Statement } from '@/src/components/form/Statement';
import {
contractorPlusProductIdentifier,
contractorStandardProductIdentifier,
} from '@/src/common/contract-documents/constants';

const CSA_DISCLAIMER = {
[contractorStandardProductIdentifier]:
' Remote provides its Contractor Services Agreement (CSA) “as is”; it does not constitute legal advice or create an attorney-client relationship with Remote. Remote is not liable for the content You enter herein and You are encouraged to seek independent legal advice tailored to the specific relationship between You and Your Contractor. By use of the CSA, You release Remote from liability for any claim for damages or losses which may arise related to Your use thereof.',
Comment thread
jordividaller marked this conversation as resolved.
Resolved
[contractorPlusProductIdentifier]:
'Remote provides its Contractor Services Agreement (CSA) “as is”; it does not constitute legal advice or create an attorney-client relationship with Remote. Remote is not liable for the content You enter herein and You are encouraged to seek independent legal advice tailored to the specific relationship between You and Your Contractor.',
};

export const StatementOfWorkDisclaimer = ({
subscription,
}: {
subscription: string;
}) => {
return (
<Statement
title='Contractor Services Agreement'
description={CSA_DISCLAIMER[subscription as keyof typeof CSA_DISCLAIMER]}
severity='info'
/>
);
};
14 changes: 14 additions & 0 deletions src/common/contract-documents/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const contractorStandardProductIdentifier =
'urn:remotecom:resource:product:contractor:standard:monthly';
export const contractorPlusProductIdentifier =
'urn:remotecom:resource:product:contractor:plus:monthly';
export const corProductIdentifier =
'urn:remotecom:resource:product:contractor:aor:monthly';

export const REMOTE_AI_ERROR_SOURCE = 'REMOTE_AI';

export const REMOTE_AI_SERVICES_AND_DELIVERABLES_ERROR_MESSAGE =
"You cannot control how a contractor completes the Services and Deliverables. This means, for example, that you can't specify their working hours or use of subcontractors. Please make sure this field includes only project names, descriptions, and deliverables. We do not allow hyperlinks.";

export const REMOTE_AI_SERVICES_AND_DELIVERABLES_COR_ERROR_MESSAGE =
'The content You have entered may not be consistent with the Contractor of Record terms. You are responsible before proceeding for ensuring that the language entered herein accurately reflects those terms and your relationship with Subcontractors.';
152 changes: 152 additions & 0 deletions src/common/contract-documents/jsfModify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { addYears, format } from 'date-fns';
import { FieldValues } from 'react-hook-form';
import { ChangeEvent } from 'react';

import { parseLocalDate } from '@/src/common/dates';
import { createStatementProperty } from '@/src/components/form/jsf-utils/createFields';
import {
contractorStandardProductIdentifier,
REMOTE_AI_SERVICES_AND_DELIVERABLES_COR_ERROR_MESSAGE,
REMOTE_AI_SERVICES_AND_DELIVERABLES_ERROR_MESSAGE,
} from '@/src/common/contract-documents/constants';
import { ContractorContractDetailsFormPayload } from '@/src/common/contract-documents/types';
import { JSFModify } from '@/src/flows/types';

const isStandardPricingPlan = (pricingPlan: string | undefined) => {
return pricingPlan === contractorStandardProductIdentifier;
};

const showBackDateWarning = (
isStandardPricingPlanSelected: boolean,
provisionalStartDate: string | undefined,
) => {
const isStartDateBackdated =
provisionalStartDate &&
// Compare full days omitting time of the day
provisionalStartDate < format(new Date(), 'yyyy-MM-dd');

if (!isStandardPricingPlanSelected && isStartDateBackdated) {
return createStatementProperty({
severity: 'warning',
description:
'Backdating the service start date is not supported in the selected Contractor Management plan.',
});
}

return undefined;
};

/**
* Handles changes to the services_and_deliverables field to clear AI warning state
*/
function onServicesAndDeliverablesChange(
_event: ChangeEvent<HTMLTextAreaElement>,
values: ContractorContractDetailsFormPayload & {
services_and_deliverables_ai_warning: string;
services_and_deliverables_error_skippable: boolean;
},
setValues: (
formValues: Partial<
ContractorContractDetailsFormPayload & {
services_and_deliverables_ai_warning: string;
services_and_deliverables_error_skippable: boolean;
}
>,
) => void,
) {
setValues({
...values,
services_and_deliverables_ai_warning: '',
services_and_deliverables_error_skippable: false,
});
}

/**
* Merges internal jsfModify modifications with user-provided options for contract_details step
* This abstracts the logic of applying internal field modifications (like dynamic descriptions)
* while preserving user customizations
*/
export const buildContractDetailsJsfModify = (
userJsfModify: JSFModify | undefined,
provisionalStartDateDescription: string | undefined,
selectedPricingPlan: string | undefined,
fieldValues: FieldValues,
isContractorOfRecord: boolean,
): JSFModify => {
const isStandardPricingPlanSelected =
isStandardPricingPlan(selectedPricingPlan);
const provisionalStartDate =
fieldValues?.service_duration?.provisional_start_date;
const statement = showBackDateWarning(
isStandardPricingPlanSelected,
provisionalStartDate,
);
const AiStatementWarning = createStatementProperty({
severity: 'warning',
title: 'Possible misclassification risk',
description: isContractorOfRecord
? REMOTE_AI_SERVICES_AND_DELIVERABLES_COR_ERROR_MESSAGE
: REMOTE_AI_SERVICES_AND_DELIVERABLES_ERROR_MESSAGE,
});
return {
...userJsfModify,
create: {
...userJsfModify?.create,
services_and_deliverables_ai_warning: {
type: 'string',
'x-jsf-presentation': {
inputType: 'hidden',
},
},
services_and_deliverables_error_skippable: {
type: 'boolean',
'x-jsf-presentation': {
inputType: 'hidden',
},
},
},
fields: {
...userJsfModify?.fields,
...{
'service_duration.provisional_start_date': {
description: provisionalStartDateDescription,
'x-jsf-presentation': {
minDate: !isStandardPricingPlanSelected
? format(new Date(), 'yyyy-MM-dd')
: undefined,
...statement,
},
},
'service_duration.expiration_date': {
'x-jsf-presentation': {
calculateDynamicProperties: (formValues: FieldValues) => {
const maxDate =
isContractorOfRecord &&
formValues.service_duration?.provisional_start_date
? addYears(
parseLocalDate(
formValues.service_duration.provisional_start_date,
),
1,
)
: undefined;
return {
maxDate: maxDate ? format(maxDate, 'yyyy-MM-dd') : undefined,
};
},
},
},
services_and_deliverables: {
onChange: onServicesAndDeliverablesChange,
'x-jsf-presentation': {
calculateDynamicProperties: (formValues: FieldValues) => ({
statement: formValues.services_and_deliverables_ai_warning
? AiStatementWarning.statement
: undefined,
}),
},
},
},
},
};
};
Loading
Loading