From ab53d160a7879c1b3db3aa24e8cebfa5e630c8c8 Mon Sep 17 00:00:00 2001 From: Steve Jensen Date: Fri, 26 Jun 2026 15:18:36 -0600 Subject: [PATCH 1/2] feat(Contractor.Documents): add document list, W-9 signing, and signer flow Add standalone contractor document signing: - useContractorDocumentsList data hook + DocumentsList component - useContractorSignatureForm dynamic W-9 hook (get/getPdf/sign) with classification radio synthesis, conditional LLC/other fields, and 0/1 wire mapping on submit - SignatureForm component reusing the shared DocumentViewer - ContractorDocumentSigner robot3 orchestrator composing the two - masked SSN/EIN handled as redacted: empty input, mask shown as placeholder, omitted from the sign payload unless replaced - dedicated i18n namespaces, events, MSW W-9 mocks, tests, and stories - sdk-app registry regenerated so the dev app injects contractorId WIP: SignatureForm masked-SSN placeholder test (getByLabelText) still failing; to be resolved before review. Co-authored-by: Cursor --- sdk-app/src/generated-registry-data.ts | 15 +- .../ContractorDocumentSigner.test.tsx | 47 ++ .../Documents/ContractorDocumentSigner.tsx | 68 +++ .../DocumentsList/DocumentsList.test.tsx | 78 +++ .../Documents/DocumentsList/DocumentsList.tsx | 131 +++++ .../Documents/DocumentsList/index.ts | 7 + .../useContractorDocumentsList/index.ts | 6 + .../useContractorDocumentsList.tsx | 86 ++++ .../SignatureForm/SignatureForm.test.tsx | 165 ++++++ .../Documents/SignatureForm/SignatureForm.tsx | 338 +++++++++++++ .../Documents/SignatureForm/index.ts | 2 + .../contractorSignatureFormSchema.test.ts | 118 +++++ .../contractorSignatureFormSchema.ts | 104 ++++ .../useContractorSignatureForm/fields.tsx | 91 ++++ .../useContractorSignatureForm/index.ts | 38 ++ .../useContractorSignatureForm.tsx | 290 +++++++++++ .../w9Fields.test.ts | 256 ++++++++++ .../useContractorSignatureForm/w9Fields.ts | 469 ++++++++++++++++++ .../Documents/documentSignerStateMachine.tsx | 41 ++ .../Contractor/Documents/index.stories.tsx | 33 ++ src/components/Contractor/Documents/index.ts | 13 + .../Contractor/Documents/stateMachine.ts | 61 +++ .../exports/contractorOnboarding.ts | 6 + src/i18n/en/Contractor.DocumentsList.json | 13 + src/i18n/en/Contractor.SignatureForm.json | 113 +++++ src/index.ts | 43 ++ src/partner-hook-utils/types.ts | 2 + src/shared/constants.ts | 3 + src/test/mocks/apis/contractor_documents.ts | 179 +++++++ src/test/mocks/handlers.ts | 2 + src/types/i18next.d.ts | 128 ++++- 31 files changed, 2935 insertions(+), 11 deletions(-) create mode 100644 src/components/Contractor/Documents/ContractorDocumentSigner.test.tsx create mode 100644 src/components/Contractor/Documents/ContractorDocumentSigner.tsx create mode 100644 src/components/Contractor/Documents/DocumentsList/DocumentsList.test.tsx create mode 100644 src/components/Contractor/Documents/DocumentsList/DocumentsList.tsx create mode 100644 src/components/Contractor/Documents/DocumentsList/index.ts create mode 100644 src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/index.ts create mode 100644 src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/useContractorDocumentsList.tsx create mode 100644 src/components/Contractor/Documents/SignatureForm/SignatureForm.test.tsx create mode 100644 src/components/Contractor/Documents/SignatureForm/SignatureForm.tsx create mode 100644 src/components/Contractor/Documents/SignatureForm/index.ts create mode 100644 src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.test.ts create mode 100644 src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.ts create mode 100644 src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/fields.tsx create mode 100644 src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/index.ts create mode 100644 src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/useContractorSignatureForm.tsx create mode 100644 src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.test.ts create mode 100644 src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.ts create mode 100644 src/components/Contractor/Documents/documentSignerStateMachine.tsx create mode 100644 src/components/Contractor/Documents/index.stories.tsx create mode 100644 src/components/Contractor/Documents/index.ts create mode 100644 src/components/Contractor/Documents/stateMachine.ts create mode 100644 src/i18n/en/Contractor.DocumentsList.json create mode 100644 src/i18n/en/Contractor.SignatureForm.json create mode 100644 src/test/mocks/apis/contractor_documents.ts diff --git a/sdk-app/src/generated-registry-data.ts b/sdk-app/src/generated-registry-data.ts index c0ea73b1f..dbb7410b5 100644 --- a/sdk-app/src/generated-registry-data.ts +++ b/sdk-app/src/generated-registry-data.ts @@ -27,13 +27,16 @@ export const ENTITY_REQUIREMENTS: Record = { 'ContractorManagement.PaymentSummary': ['companyId'], 'ContractorManagement.PaymentsList': ['companyId'], 'ContractorOnboarding.Address': ['contractorId'], + 'ContractorOnboarding.ContractorDocumentSigner': ['contractorId'], 'ContractorOnboarding.ContractorList': ['companyId'], 'ContractorOnboarding.ContractorProfile': ['companyId'], 'ContractorOnboarding.ContractorSubmit': ['contractorId'], + 'ContractorOnboarding.DocumentsList': ['contractorId'], 'ContractorOnboarding.Landing': ['contractorId', 'companyId'], 'ContractorOnboarding.NewHireReport': ['contractorId'], 'ContractorOnboarding.OnboardingFlow': ['companyId'], 'ContractorOnboarding.PaymentMethod': ['contractorId'], + 'ContractorOnboarding.SignatureForm': ['contractorId'], 'EmployeeManagement.Compensation': ['employeeId'], 'EmployeeManagement.CompensationAddAnotherJobForm': ['employeeId'], 'EmployeeManagement.CompensationAddJobForm': ['employeeId'], @@ -137,6 +140,7 @@ export const ADDITIONAL_REQUIRED_PROPS: Record = { 'ContractorManagement.PaymentHistory': ['paymentId'], 'ContractorManagement.PaymentStatement': ['paymentGroupId', 'contractorUuid'], 'ContractorManagement.PaymentSummary': ['paymentGroupId'], + 'ContractorOnboarding.SignatureForm': ['documentUuid'], 'EmployeeManagement.CompensationEditForm': ['jobId'], 'EmployeeManagement.DocumentManager': ['formId'], 'EmployeeOnboarding.Compensation': ['startDate'], @@ -151,14 +155,5 @@ export const ADDITIONAL_REQUIRED_PROPS: Record = { 'TimeOff.PolicySettings': ['policyId'], 'TimeOff.PolicySettingsPresentation': ['accrualMethod', 'onContinue', 'onBack'], 'TimeOff.TimeOffPolicyDetail': ['policyId'], - 'TimeOff.TimeOffPolicyDetailPresentation': [ - 'title', - 'onBack', - 'backLabel', - 'selectedTabId', - 'onTabChange', - 'employees', - 'removeDialog', - 'policyDetails', - ], + 'TimeOff.TimeOffPolicyDetailPresentation': ['title', 'onBack', 'backLabel', 'selectedTabId', 'onTabChange', 'employees', 'removeDialog', 'policyDetails'], } diff --git a/src/components/Contractor/Documents/ContractorDocumentSigner.test.tsx b/src/components/Contractor/Documents/ContractorDocumentSigner.test.tsx new file mode 100644 index 000000000..5db3adc40 --- /dev/null +++ b/src/components/Contractor/Documents/ContractorDocumentSigner.test.tsx @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { ContractorDocumentSigner } from './ContractorDocumentSigner' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { renderWithProviders } from '@/test-utils/renderWithProviders' + +describe('ContractorDocumentSigner orchestration', () => { + beforeEach(() => { + setupApiTestMocks() + }) + + const contractorId = 'contractor-123' + + it('opens the signature form for the chosen document and returns on Back', async () => { + const user = userEvent.setup() + renderWithProviders() + + await screen.findByText('Documents') + await user.click(screen.getByRole('button', { name: 'Sign document' })) + + await screen.findByRole('button', { name: 'Sign' }) + await user.click(screen.getByRole('button', { name: 'Back' })) + + await screen.findByRole('button', { name: 'Continue' }) + }) + + it('returns to the list after a document is signed', async () => { + const user = userEvent.setup() + renderWithProviders() + + await screen.findByText('Documents') + await user.click(screen.getByRole('button', { name: 'Sign document' })) + + await screen.findByRole('button', { name: 'Sign' }) + await user.click(screen.getByRole('radio', { name: 'C-Corporation' })) + await user.type(screen.getByLabelText('Signature'), 'Klay Thompson') + await user.click( + screen.getByRole('checkbox', { name: 'I agree to electronically sign this form.' }), + ) + await user.click(screen.getByRole('button', { name: 'Sign' })) + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/Contractor/Documents/ContractorDocumentSigner.tsx b/src/components/Contractor/Documents/ContractorDocumentSigner.tsx new file mode 100644 index 000000000..fcba99702 --- /dev/null +++ b/src/components/Contractor/Documents/ContractorDocumentSigner.tsx @@ -0,0 +1,68 @@ +import { createMachine } from 'robot3' +import { useMemo } from 'react' +import { + DocumentsListContextual, + type ContractorDocumentSignerContextInterface, +} from './documentSignerStateMachine' +import { contractorDocumentSignerMachine } from './stateMachine' +import { Flow } from '@/components/Flow/Flow' +import { BaseComponent, type BaseComponentInterface } from '@/components/Base' + +/** + * Props for {@link ContractorDocumentSigner}. + * + * @public + */ +export interface ContractorDocumentSignerProps extends BaseComponentInterface { + /** The associated contractor identifier. */ + contractorId: string +} + +/** + * Standalone flow for a contractor to review and sign their documents (W-9). + * + * @remarks + * Lists the contractor's documents and routes through the signing UI for each + * one, returning to the list after a document is signed or the user navigates + * back. Composes {@link DocumentsList} and {@link SignatureForm}. + * + * | Event | Description | Data | + * | ----- | ----------- | ---- | + * | `contractor/documents/view` | Fired when a document's "Sign" action is selected from the list | `{ uuid: string; title?: string }` | + * | `contractor/documents/sign` | Fired after a document is successfully signed | The signed document | + * | `contractor/documents/done` | Fired when the contractor completes the documents step | — | + * | `CANCEL` | Fired when the user navigates back from the signature form to the list | — | + * + * @components + * - {@link DocumentsList} + * - {@link SignatureForm} + * + * @param props - See {@link ContractorDocumentSignerProps}. + * @returns The contractor document signing flow. + * @public + */ +export function ContractorDocumentSigner(props: ContractorDocumentSignerProps) { + return ( + + + + ) +} + +function Root({ contractorId, onEvent }: ContractorDocumentSignerProps) { + const machine = useMemo( + () => + createMachine( + 'list', + contractorDocumentSignerMachine, + (initialContext: ContractorDocumentSignerContextInterface) => ({ + ...initialContext, + component: DocumentsListContextual, + contractorId, + }), + ), + [contractorId], + ) + + return +} diff --git a/src/components/Contractor/Documents/DocumentsList/DocumentsList.test.tsx b/src/components/Contractor/Documents/DocumentsList/DocumentsList.test.tsx new file mode 100644 index 000000000..83b3b1819 --- /dev/null +++ b/src/components/Contractor/Documents/DocumentsList/DocumentsList.test.tsx @@ -0,0 +1,78 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { HttpResponse } from 'msw' +import { DocumentsList } from './DocumentsList' +import { server } from '@/test/mocks/server' +import { + handleGetContractorDocuments, + W9_DOCUMENT_UUID, +} from '@/test/mocks/apis/contractor_documents' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { contractorEvents } from '@/shared/constants' +import { renderWithProviders } from '@/test-utils/renderWithProviders' + +describe('Contractor DocumentsList', () => { + beforeEach(() => { + setupApiTestMocks() + }) + + const contractorId = 'contractor-123' + + it('renders the documents with their signing status', async () => { + renderWithProviders() + + await screen.findByText('Documents') + expect(screen.getByRole('button', { name: 'Sign document' })).toBeInTheDocument() + expect(screen.getByText('Complete')).toBeInTheDocument() + }) + + it('keeps Continue disabled while a document still needs signing', async () => { + renderWithProviders() + + await screen.findByText('Documents') + expect(screen.getByRole('button', { name: 'Continue' })).toBeDisabled() + }) + + it('emits the view event with the document identity when Sign is selected', async () => { + const onEvent = vi.fn() + const user = userEvent.setup() + renderWithProviders() + + await screen.findByText('Documents') + await user.click(screen.getByRole('button', { name: 'Sign document' })) + + expect(onEvent).toHaveBeenCalledWith(contractorEvents.CONTRACTOR_VIEW_DOCUMENT_TO_SIGN, { + uuid: W9_DOCUMENT_UUID, + title: 'W-9', + }) + }) + + it('enables Continue and emits done once every document is signed', async () => { + server.use( + handleGetContractorDocuments(() => + HttpResponse.json([ + { + uuid: W9_DOCUMENT_UUID, + title: 'W-9', + name: 'taxpayer_identification_form_w_9', + requires_signing: false, + signed_at: '2025-01-01T00:00:00Z', + }, + ]), + ), + ) + const onEvent = vi.fn() + const user = userEvent.setup() + renderWithProviders() + + await screen.findByText('Documents') + const continueButton = screen.getByRole('button', { name: 'Continue' }) + await waitFor(() => { + expect(continueButton).toBeEnabled() + }) + + await user.click(continueButton) + expect(onEvent).toHaveBeenCalledWith(contractorEvents.CONTRACTOR_DOCUMENTS_DONE) + }) +}) diff --git a/src/components/Contractor/Documents/DocumentsList/DocumentsList.tsx b/src/components/Contractor/Documents/DocumentsList/DocumentsList.tsx new file mode 100644 index 000000000..a783b053c --- /dev/null +++ b/src/components/Contractor/Documents/DocumentsList/DocumentsList.tsx @@ -0,0 +1,131 @@ +import { useTranslation } from 'react-i18next' +import type { Document } from '@gusto/embedded-api-v-2025-11-15/models/components/document' +import { useContractorDocumentsList } from './useContractorDocumentsList' +import { BaseComponent, type BaseComponentInterface } from '@/components/Base/Base' +import { BaseLayout } from '@/components/Base' +import { useBase } from '@/components/Base/useBase' +import { useI18n, useComponentDictionary } from '@/i18n' +import { ActionsLayout, Flex } from '@/components/Common' +import { DocumentList } from '@/components/Common/DocumentList' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { contractorEvents } from '@/shared/constants' + +/** + * Props for {@link DocumentsList}. + * + * @public + */ +export interface DocumentsListProps extends BaseComponentInterface<'Contractor.DocumentsList'> { + /** The associated contractor identifier. */ + contractorId: string +} + +/** + * Returns whether a document still needs the contractor's signature. + * + * @remarks + * A document is outstanding when it requires signing and has not yet been + * signed (`signedAt` is unset). + */ +function requiresSignature(document: Document): boolean { + return Boolean(document.requiresSigning) && !document.signedAt +} + +/** + * Lists a contractor's documents and lets the contractor open each one for signing. + * + * @remarks + * Fetches the contractor's documents via {@link useContractorDocumentsList} and + * renders them in a table. The Continue action is disabled until every document + * that requires signing has been signed. + * + * | Event | Description | Data | + * | ----- | ----------- | ---- | + * | `contractor/documents/view` | Fired when a document's "Sign" action is selected | `{ uuid?: string; title?: string }` | + * | `contractor/documents/done` | Fired when all required documents are signed and the user continues | — | + * + * @param props - See {@link DocumentsListProps}. + * @returns The contractor documents list. + * @public + */ +export function DocumentsList(props: DocumentsListProps) { + return ( + + + + ) +} + +function Root({ contractorId, className, dictionary }: DocumentsListProps) { + useComponentDictionary('Contractor.DocumentsList', dictionary) + useI18n('Contractor.DocumentsList') + const { t } = useTranslation('Contractor.DocumentsList') + const { onEvent } = useBase() + const Components = useComponentContext() + + const result = useContractorDocumentsList({ contractorId }) + + if (result.isLoading) { + return + } + + const { documents } = result.data + const hasError = result.errorHandling.errors.length > 0 + const hasSignedAllDocuments = documents.every(document => !requiresSignature(document)) + + const handleRequestSign = (uuid: string) => { + const document = documents.find(candidate => candidate.uuid === uuid) + onEvent(contractorEvents.CONTRACTOR_VIEW_DOCUMENT_TO_SIGN, { + uuid, + title: document?.title, + }) + } + + const handleContinue = () => { + onEvent(contractorEvents.CONTRACTOR_DOCUMENTS_DONE) + } + + return ( +
+ + + + {t('title')} + {t('subtitle')} + + + ({ + uuid: document.uuid ?? '', + title: document.title, + description: document.description, + requires_signing: requiresSignature(document), + }))} + onRequestSign={form => { + handleRequestSign(form.uuid) + }} + withError={hasError} + label={t('documentListLabel')} + columnLabels={{ + form: t('documentColumnLabel'), + action: t('statusColumnLabel'), + }} + statusLabels={{ + signCta: t('signDocumentCta'), + notSigned: t('notSigned'), + complete: t('signed'), + }} + emptyStateLabel={t('emptyTitle')} + errorLabel={t('errorTitle')} + /> + + + + {t('continueCta')} + + + + +
+ ) +} diff --git a/src/components/Contractor/Documents/DocumentsList/index.ts b/src/components/Contractor/Documents/DocumentsList/index.ts new file mode 100644 index 000000000..a4c620075 --- /dev/null +++ b/src/components/Contractor/Documents/DocumentsList/index.ts @@ -0,0 +1,7 @@ +export { DocumentsList, type DocumentsListProps } from './DocumentsList' +export { + useContractorDocumentsList, + type UseContractorDocumentsListParams, + type UseContractorDocumentsListReady, + type UseContractorDocumentsListResult, +} from './useContractorDocumentsList' diff --git a/src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/index.ts b/src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/index.ts new file mode 100644 index 000000000..a4057f30d --- /dev/null +++ b/src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/index.ts @@ -0,0 +1,6 @@ +export { + useContractorDocumentsList, + type UseContractorDocumentsListParams, + type UseContractorDocumentsListReady, + type UseContractorDocumentsListResult, +} from './useContractorDocumentsList' diff --git a/src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/useContractorDocumentsList.tsx b/src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/useContractorDocumentsList.tsx new file mode 100644 index 000000000..faffb899e --- /dev/null +++ b/src/components/Contractor/Documents/DocumentsList/useContractorDocumentsList/useContractorDocumentsList.tsx @@ -0,0 +1,86 @@ +import { useContractorDocumentsGetAll } from '@gusto/embedded-api-v-2025-11-15/react-query/contractorDocumentsGetAll' +import type { Document } from '@gusto/embedded-api-v-2025-11-15/models/components/document' +import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler' +import type { BaseHookReady, HookLoadingResult } from '@/partner-hook-utils/types' + +/** + * Parameters for {@link useContractorDocumentsList}. + * + * @public + */ +export interface UseContractorDocumentsListParams { + /** The associated contractor identifier. */ + contractorId: string +} + +/** + * Ready-state shape returned by {@link useContractorDocumentsList} once the documents have loaded. + * + * @public + */ +export type UseContractorDocumentsListReady = BaseHookReady< + { documents: Document[] }, + { isFetching: boolean } +> + +/** + * Result of {@link useContractorDocumentsList} — a discriminated union of loading and ready states. + * + * @public + */ +export type UseContractorDocumentsListResult = HookLoadingResult | UseContractorDocumentsListReady + +/** + * Standalone data hook for a contractor's documents. + * + * @remarks + * Wraps the `contractorDocumentsGetAll` query in the standard + * {@link BaseHookReady} shape. Read-only — viewing or signing a document is + * handled by the screen the parent routes to, so this hook exposes no actions. + * + * @param params - See {@link UseContractorDocumentsListParams}. + * @returns A {@link HookLoadingResult} while loading, or the ready state with `data.documents` once loaded. + * @public + * + * @example + * ```tsx + * import { useContractorDocumentsList } from '@gusto/embedded-react-sdk' + * + * function ContractorDocuments({ contractorId }: { contractorId: string }) { + * const result = useContractorDocumentsList({ contractorId }) + * + * if (result.isLoading) return
Loading...
+ * + * return ( + *
    + * {result.data.documents.map(doc => ( + *
  • {doc.title}
  • + * ))} + *
+ * ) + * } + * ``` + */ +export function useContractorDocumentsList({ + contractorId, +}: UseContractorDocumentsListParams): UseContractorDocumentsListResult { + // staleTime: Infinity — the SDK QueryClient invalidates on any mutation + // success, so a long stale time avoids redundant refetches on remount. + const documentsQuery = useContractorDocumentsGetAll( + { contractorUuid: contractorId }, + { staleTime: Infinity }, + ) + + const errorHandling = composeErrorHandler([documentsQuery]) + + if (documentsQuery.isLoading) { + return { isLoading: true, errorHandling } + } + + return { + isLoading: false, + data: { documents: documentsQuery.data?.documents ?? [] }, + status: { isFetching: documentsQuery.isFetching }, + errorHandling, + } +} diff --git a/src/components/Contractor/Documents/SignatureForm/SignatureForm.test.tsx b/src/components/Contractor/Documents/SignatureForm/SignatureForm.test.tsx new file mode 100644 index 000000000..b438c12da --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/SignatureForm.test.tsx @@ -0,0 +1,165 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { HttpResponse, type HttpResponseResolver } from 'msw' +import { SignatureForm } from './SignatureForm' +import { server } from '@/test/mocks/server' +import { + handleSignContractorDocument, + W9_DOCUMENT_UUID, +} from '@/test/mocks/apis/contractor_documents' +import { setupApiTestMocks } from '@/test/mocks/apiServer' +import { contractorEvents, componentEvents } from '@/shared/constants' +import { renderWithProviders } from '@/test-utils/renderWithProviders' + +interface SignRequestBody { + fields: Array<{ key: string; value: string }> + agree: boolean +} + +describe('Contractor SignatureForm', () => { + beforeEach(() => { + setupApiTestMocks() + }) + + const contractorId = 'contractor-123' + + function renderForm(onEvent = vi.fn()) { + renderWithProviders( + , + ) + return onEvent + } + + it('prefills editable inputs from the document fields', async () => { + renderForm() + + await screen.findByRole('button', { name: 'Sign' }) + expect(screen.getByLabelText('Entity or individual name (1)')).toHaveValue('Klay Thompson') + }) + + it('shows the masked SSN as a placeholder instead of seeding it as the value', async () => { + renderForm() + + await screen.findByRole('button', { name: 'Sign' }) + const ssnInput = screen.getByLabelText('Social Security Number (SSN)') + expect(ssnInput).toHaveValue('') + expect(ssnInput).toHaveAttribute('placeholder', 'XXX-XX-3123') + }) + + it('does not send the masked SSN back in the sign payload', async () => { + let signBody: SignRequestBody | null = null + const signResolver = vi.fn(async ({ request }) => { + signBody = (await request.json()) as SignRequestBody + return HttpResponse.json({ uuid: W9_DOCUMENT_UUID, signed_at: '2025-06-26T00:00:00Z' }) + }) + server.use(handleSignContractorDocument(signResolver)) + + const user = userEvent.setup() + renderForm() + + await screen.findByRole('button', { name: 'Sign' }) + await user.click(screen.getByRole('radio', { name: 'C-Corporation' })) + await user.type(screen.getByLabelText('Signature'), 'Klay Thompson') + await user.click( + screen.getByRole('checkbox', { name: 'I agree to electronically sign this form.' }), + ) + await user.click(screen.getByRole('button', { name: 'Sign' })) + + await waitFor(() => { + expect(signResolver).toHaveBeenCalledTimes(1) + }) + expect(signBody!.fields.find(f => f.key === 'ssn')).toBeUndefined() + }) + + it('renders the tax classification as a single radio group', async () => { + renderForm() + + await screen.findByRole('button', { name: 'Sign' }) + expect(screen.getByRole('radio', { name: 'C-Corporation' })).toBeInTheDocument() + expect(screen.getByRole('radio', { name: 'LLC' })).toBeInTheDocument() + expect(screen.queryByRole('checkbox', { name: 'C-Corporation' })).not.toBeInTheDocument() + }) + + it('reveals the LLC code select only after the LLC classification is chosen', async () => { + const user = userEvent.setup() + renderForm() + + await screen.findByRole('button', { name: 'Sign' }) + expect(screen.queryByLabelText('LLC tax classification code')).not.toBeInTheDocument() + + await user.click(screen.getByRole('radio', { name: 'LLC' })) + expect(screen.getByLabelText('LLC tax classification code')).toBeInTheDocument() + }) + + it('blocks submission and skips the API when consent is not given', async () => { + const signResolver = vi.fn(() => + HttpResponse.json({ uuid: W9_DOCUMENT_UUID, signed_at: '2025-01-01T00:00:00Z' }), + ) + server.use(handleSignContractorDocument(signResolver)) + + const user = userEvent.setup() + renderForm() + + await screen.findByRole('button', { name: 'Sign' }) + await user.click(screen.getByRole('radio', { name: 'C-Corporation' })) + await user.click(screen.getByRole('button', { name: 'Sign' })) + + expect( + await screen.findByText('You must agree to electronically sign this form.'), + ).toBeInTheDocument() + expect(signResolver).not.toHaveBeenCalled() + }) + + it('signs the document and emits the signed document, mapping the classification to the W-9 wire format', async () => { + let signBody: SignRequestBody | null = null + const signResolver = vi.fn(async ({ request }) => { + signBody = (await request.json()) as SignRequestBody + return HttpResponse.json({ + uuid: W9_DOCUMENT_UUID, + title: 'W-9', + requires_signing: false, + signed_at: '2025-06-26T00:00:00Z', + }) + }) + server.use(handleSignContractorDocument(signResolver)) + + const onEvent = renderForm() + const user = userEvent.setup() + + await screen.findByRole('button', { name: 'Sign' }) + await user.click(screen.getByRole('radio', { name: 'C-Corporation' })) + await user.type(screen.getByLabelText('Signature'), 'Klay Thompson') + await user.click( + screen.getByRole('checkbox', { name: 'I agree to electronically sign this form.' }), + ) + await user.click(screen.getByRole('button', { name: 'Sign' })) + + await waitFor(() => { + expect(signResolver).toHaveBeenCalledTimes(1) + }) + + expect(signBody).toMatchObject({ agree: true }) + expect(signBody!.fields).toContainEqual({ key: 'c_corporation', value: '1' }) + expect(signBody!.fields).toContainEqual({ key: 'signature_text', value: 'Klay Thompson' }) + + expect(onEvent).toHaveBeenCalledWith( + contractorEvents.CONTRACTOR_SIGN_DOCUMENT, + expect.objectContaining({ uuid: W9_DOCUMENT_UUID }), + ) + }) + + it('emits CANCEL when navigating back', async () => { + const onEvent = renderForm() + const user = userEvent.setup() + + await screen.findByRole('button', { name: 'Back' }) + await user.click(screen.getByRole('button', { name: 'Back' })) + + expect(onEvent).toHaveBeenCalledWith(componentEvents.CANCEL) + }) +}) diff --git a/src/components/Contractor/Documents/SignatureForm/SignatureForm.tsx b/src/components/Contractor/Documents/SignatureForm/SignatureForm.tsx new file mode 100644 index 000000000..5a065a0ef --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/SignatureForm.tsx @@ -0,0 +1,338 @@ +import { Trans, useTranslation } from 'react-i18next' +import { useWatch } from 'react-hook-form' +import { + useContractorSignatureForm, + AGREE_FIELD, + LLC_CLASSIFICATION_FIELD, + LLC_CLASSIFICATION_OPTION, + OTHER_CLASSIFICATION_OPTION, + OTHER_TEXT_FIELD, + TAX_CLASSIFICATION_FIELD, + type ContractorSignatureSection, + type UseContractorSignatureFormReady, +} from './useContractorSignatureForm' +import { useI18n, useComponentDictionary } from '@/i18n' +import type { BaseComponentInterface } from '@/components/Base/Base' +import { BaseComponent, BaseLayout } from '@/components/Base' +import { useBase } from '@/components/Base/useBase' +import { ActionsLayout, Flex } from '@/components/Common' +import { Form as FormLayout } from '@/components/Common/Form' +import { DocumentViewer } from '@/components/Common/DocumentViewer' +import { SDKFormProvider } from '@/partner-hook-utils/form/SDKFormProvider' +import { contractorEvents, componentEvents } from '@/shared/constants' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' + +/** + * Props for {@link SignatureForm}. + * + * @public + */ +export interface SignatureFormProps extends BaseComponentInterface<'Contractor.SignatureForm'> { + /** The UUID of the contractor document to sign. */ + documentUuid: string + /** The associated contractor identifier. */ + contractorId: string +} + +/** + * Standalone form for signing an individual contractor document (W-9). + * + * @remarks + * Lower-level building block used internally by `ContractorDocumentSigner` for + * its signing view. Use this component directly when you need full control over + * navigation between the document list and the signature form. + * + * | Event | Description | Data | + * | ----- | ----------- | ---- | + * | `contractor/documents/sign` | Fired when the document is successfully signed | The signed document | + * | `CANCEL` | Fired when the user navigates back from the signature form | — | + * + * @param props - See {@link SignatureFormProps}. + * @returns The rendered signature form. + * @public + */ +export function SignatureForm(props: SignatureFormProps) { + return ( + + + + ) +} + +function Root({ documentUuid, dictionary }: SignatureFormProps) { + useComponentDictionary('Contractor.SignatureForm', dictionary) + useI18n('Contractor.SignatureForm') + const { t } = useTranslation('Contractor.SignatureForm') + const { onEvent } = useBase() + const Components = useComponentContext() + + const hookResult = useContractorSignatureForm({ documentUuid }) + + if (hookResult.isLoading) { + return + } + + const { document, pdfUrl, sections, hasFields } = hookResult.data + const { isPending } = hookResult.status + + const handleFormSubmit = async () => { + const result = await hookResult.actions.onSubmit() + if (result) { + onEvent(contractorEvents.CONTRACTOR_SIGN_DOCUMENT, result.data) + } + } + + const handleBack = () => { + onEvent(componentEvents.CANCEL) + } + + return ( + + + + +
+ + + {document.title ?? t('signatureRequired')} + + {t('instructions')} + {pdfUrl && ( + + + ), + }} + /> + + )} + +
+ + + + {sections.map(section => ( + + ))} + + + + + + {t('backCta')} + + + {hasFields ? t('signCta') : t('acknowledgeCta')} + + +
+
+
+
+ ) +} + +interface SectionFieldsProps { + section: ContractorSignatureSection + hookResult: UseContractorSignatureFormReady +} + +function SectionFields({ section, hookResult }: SectionFieldsProps) { + const { t } = useTranslation('Contractor.SignatureForm') + const Components = useComponentContext() + const { Fields } = hookResult.form + const copy = useSignatureFormCopy() + + const classification = useWatch({ + control: hookResult.form.hookFormInternals.formMethods.control, + name: TAX_CLASSIFICATION_FIELD, + }) + + const isFieldVisible = (name: string): boolean => { + if (name === LLC_CLASSIFICATION_FIELD) return classification === LLC_CLASSIFICATION_OPTION + if (name === OTHER_TEXT_FIELD) return classification === OTHER_CLASSIFICATION_OPTION + return true + } + + const requiredMessages = { + REQUIRED: t('validation.required'), + AGREE_REQUIRED: t('validation.agreeRequired'), + } + + return ( + + {section.section !== 'classification' && ( + + {copy.sectionHeadings[section.section]} + + {copy.sectionInstructions[section.section]} + + {section.section === 'tin' && ( + + {t('sectionInstructions.tinSecondary')} + + )} + + )} + + {section.section === 'certification' && } + + {section.fieldNames.filter(isFieldVisible).map(name => { + const Field = Fields[name] + if (!Field) return null + + const isClassification = name === TAX_CLASSIFICATION_FIELD + const isLlc = name === LLC_CLASSIFICATION_FIELD + const redactedPlaceholder = hookResult.form.fieldsMetadata[name]?.placeholder + + return ( + copy.taxClassificationOptions[value] ?? value + : isLlc + ? value => copy.llcClassificationOptions[value] ?? value + : undefined + } + /> + ) + })} + + ) +} + +function CertificationDeclaration() { + const { t } = useTranslation('Contractor.SignatureForm') + const Components = useComponentContext() + const points = Object.values(t('certificationPoints', { returnObjects: true })) + + return ( + + {t('certificationIntro')} +
    + {points.map((point, index) => ( +
  1. + {point} +
  2. + ))} +
+
+ ) +} + +/** + * Resolves the W-9 signing form copy into lookup maps keyed by field name and + * option value. Each entry uses a static translation key so the typed `t` + * surface stays sound while still allowing dynamic, API-driven field lists. + */ +function useSignatureFormCopy() { + const { t } = useTranslation('Contractor.SignatureForm') + + const fieldLabels: Record = { + name: t('fields.name.label'), + business_name: t('fields.business_name.label'), + taxClassification: t('fields.taxClassification.label'), + llcClassificationCode: t('fields.llcClassificationCode.label'), + other_text: t('fields.other_text.label'), + foreign_partners: t('fields.foreign_partners.label'), + exempt_payee_code: t('fields.exempt_payee_code.label'), + exemption_from_FATCA: t('fields.exemption_from_FATCA.label'), + home_address_street_1: t('fields.home_address_street_1.label'), + home_address_street_2: t('fields.home_address_street_2.label'), + home_address_city: t('fields.home_address_city.label'), + home_address_state: t('fields.home_address_state.label'), + home_address_zip: t('fields.home_address_zip.label'), + account_number: t('fields.account_number.label'), + company_name: t('fields.company_name.label'), + ssn: t('fields.ssn.label'), + ein: t('fields.ein.label'), + signature_text: t('fields.signature_text.label'), + } + + const fieldDescriptions: Record = { + name: t('fields.name.description'), + business_name: t('fields.business_name.description'), + taxClassification: t('fields.taxClassification.description'), + llcClassificationCode: t('fields.llcClassificationCode.description'), + foreign_partners: t('fields.foreign_partners.description'), + exemption_from_FATCA: t('fields.exemption_from_FATCA.description'), + } + + const sectionHeadings: Record = { + exemptions: t('sections.exemptions'), + address: t('sections.address'), + tin: t('sections.tin'), + certification: t('sections.certification'), + } + + const sectionInstructions: Record = { + exemptions: t('sectionInstructions.exemptions'), + address: t('sectionInstructions.address'), + tin: t('sectionInstructions.tin'), + certification: t('sectionInstructions.certification'), + } + + const taxClassificationOptions: Record = { + individual_proprietor: t('options.taxClassification.individual_proprietor'), + c_corporation: t('options.taxClassification.c_corporation'), + s_corporation: t('options.taxClassification.s_corporation'), + partnership: t('options.taxClassification.partnership'), + trust_estate: t('options.taxClassification.trust_estate'), + limited_liability_company: t('options.taxClassification.limited_liability_company'), + other: t('options.taxClassification.other'), + } + + const llcClassificationOptions: Record = { + c: t('options.llcClassificationCode.c'), + s: t('options.llcClassificationCode.s'), + p: t('options.llcClassificationCode.p'), + } + + return { + fieldLabels, + fieldDescriptions, + sectionHeadings, + sectionInstructions, + taxClassificationOptions, + llcClassificationOptions, + llcPlaceholder: t('options.llcClassificationCode.placeholder'), + } +} + +interface AgreeFieldProps { + hookResult: UseContractorSignatureFormReady +} + +function AgreeField({ hookResult }: AgreeFieldProps) { + const { t } = useTranslation('Contractor.SignatureForm') + const Agree = hookResult.form.Fields[AGREE_FIELD] + if (!Agree) return null + + return ( + + ) +} diff --git a/src/components/Contractor/Documents/SignatureForm/index.ts b/src/components/Contractor/Documents/SignatureForm/index.ts new file mode 100644 index 000000000..5bd7d36cc --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/index.ts @@ -0,0 +1,2 @@ +export { SignatureForm, type SignatureFormProps } from './SignatureForm' +export * from './useContractorSignatureForm' diff --git a/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.test.ts b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.test.ts new file mode 100644 index 000000000..ffa9df293 --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from 'vitest' +import type { Document } from '@gusto/embedded-api-v-2025-11-15/models/components/document' +import { + createContractorSignatureFormSchema, + ContractorSignatureFormErrorCodes, +} from './contractorSignatureFormSchema' +import { + buildW9Defaults, + buildW9FieldDescriptors, + LLC_CLASSIFICATION_FIELD, + TAX_CLASSIFICATION_FIELD, + type ContractorSignatureFormData, +} from './w9Fields' + +function w9Document(): Document { + return { + uuid: 'doc-1', + name: 'taxpayer_identification_form_w_9', + title: 'W-9', + requiresSigning: true, + fields: [ + { key: 'name', value: 'Klay Thompson', dataType: 'full_name', required: true }, + { key: 'individual_proprietor', value: null, dataType: 'checkbox', required: true }, + { key: 'c_corporation', value: null, dataType: 'checkbox', required: true }, + { key: 'limited_liability_company', value: null, dataType: 'checkbox', required: true }, + { key: 'other', value: null, dataType: 'checkbox', required: true }, + { + key: 'home_address_street_1', + value: '525 7th street', + dataType: 'home_address_street_1', + required: true, + }, + { + key: 'home_address_city', + value: 'New York', + dataType: 'home_address_city', + required: true, + }, + { key: 'home_address_state', value: 'NY', dataType: 'home_address_state', required: true }, + { key: 'home_address_zip', value: '10022', dataType: 'home_address_zip', required: true }, + { key: 'ssn', value: 'XXX-XX-3123', dataType: 'ssn', required: true }, + { key: 'ein', value: 'N/A', dataType: 'ein', required: true }, + { key: 'signature_text', value: null, dataType: 'signature', required: true }, + { key: 'date', value: '9/17/2025', dataType: 'date', required: true }, + ], + } +} + +const descriptors = buildW9FieldDescriptors(w9Document()) +const schema = createContractorSignatureFormSchema(descriptors) + +function validValues( + overrides: Partial = {}, +): ContractorSignatureFormData { + return { + ...buildW9Defaults(w9Document(), descriptors), + [TAX_CLASSIFICATION_FIELD]: 'c_corporation', + signature_text: 'Klay Thompson', + agree: true, + ...overrides, + } +} + +function errorCodesByPath(values: ContractorSignatureFormData): Record { + const result = schema.safeParse(values) + if (result.success) return {} + return Object.fromEntries( + result.error.issues.map(issue => [String(issue.path[0]), issue.message]), + ) +} + +describe('createContractorSignatureFormSchema', () => { + it('accepts a fully populated, agreed-to submission', () => { + expect(schema.safeParse(validValues()).success).toBe(true) + }) + + it('requires the agree checkbox', () => { + expect(errorCodesByPath(validValues({ agree: false }))).toMatchObject({ + agree: ContractorSignatureFormErrorCodes.AGREE_REQUIRED, + }) + }) + + it('requires the federal tax classification', () => { + expect(errorCodesByPath(validValues({ [TAX_CLASSIFICATION_FIELD]: '' }))).toMatchObject({ + [TAX_CLASSIFICATION_FIELD]: ContractorSignatureFormErrorCodes.REQUIRED, + }) + }) + + it('requires prefilled-but-cleared required fields', () => { + expect(errorCodesByPath(validValues({ name: '', signature_text: '' }))).toMatchObject({ + name: ContractorSignatureFormErrorCodes.REQUIRED, + signature_text: ContractorSignatureFormErrorCodes.REQUIRED, + }) + }) + + it('requires the LLC code only while the LLC classification is selected', () => { + expect( + errorCodesByPath(validValues({ [TAX_CLASSIFICATION_FIELD]: 'limited_liability_company' })), + ).toMatchObject({ + [LLC_CLASSIFICATION_FIELD]: ContractorSignatureFormErrorCodes.REQUIRED, + }) + + expect( + schema.safeParse( + validValues({ + [TAX_CLASSIFICATION_FIELD]: 'limited_liability_company', + [LLC_CLASSIFICATION_FIELD]: 'c', + }), + ).success, + ).toBe(true) + }) + + it('treats the Other free-text field as optional', () => { + expect(schema.safeParse(validValues({ [TAX_CLASSIFICATION_FIELD]: 'other' })).success).toBe( + true, + ) + }) +}) diff --git a/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.ts b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.ts new file mode 100644 index 000000000..cb1a156f2 --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/contractorSignatureFormSchema.ts @@ -0,0 +1,104 @@ +import { z } from 'zod' +import { + LLC_CLASSIFICATION_FIELD, + LLC_CLASSIFICATION_OPTION, + TAX_CLASSIFICATION_FIELD, + type ContractorSignatureFormData, + type W9FieldDescriptor, +} from './w9Fields' + +/** + * Validation error codes produced by the contractor signature form schema. + * + * @remarks + * Use these constants as the keys in a field's `validationMessages` prop to map + * an error code to a user-facing message. + * + * @public + */ +export const ContractorSignatureFormErrorCodes = { + /** A required field was left empty. */ + REQUIRED: 'REQUIRED', + /** The electronic-signature consent checkbox was not checked. */ + AGREE_REQUIRED: 'AGREE_REQUIRED', +} as const + +/** + * Union of validation error code strings emitted by the contractor signature + * form schema. + * + * @public + */ +export type ContractorSignatureFormErrorCode = + (typeof ContractorSignatureFormErrorCodes)[keyof typeof ContractorSignatureFormErrorCodes] + +function isEmpty(value: unknown): boolean { + if (value === undefined || value === null) return true + if (typeof value === 'string' && value.trim() === '') return true + return false +} + +/** + * Builds a Zod schema for the W-9 signing form from its field descriptors. + * + * @remarks + * Every field validates as `z.unknown()` with emptiness enforced in + * `superRefine`, mirroring the dynamic state-taxes form pattern. The LLC + * classification code is required only while the LLC classification is + * selected, and `agree` must be checked. + * + * @param descriptors - The descriptors produced by `buildW9FieldDescriptors`. + * @returns A Zod schema typed to {@link ContractorSignatureFormData}. + * @public + */ +export function createContractorSignatureFormSchema( + descriptors: W9FieldDescriptor[], +): z.ZodType { + const shape: Record = { agree: z.unknown() } + for (const descriptor of descriptors) { + shape[descriptor.name] = z.unknown() + } + + const baseSchema = z.object(shape) + + const refined = baseSchema.superRefine((data, ctx) => { + const values = data as Record + const rawClassification = values[TAX_CLASSIFICATION_FIELD] + const classification = typeof rawClassification === 'string' ? rawClassification : '' + + for (const descriptor of descriptors) { + if ( + descriptor.visibleWhenClassification && + descriptor.visibleWhenClassification !== classification + ) { + continue + } + + // A redacted field already has a value on file, so an empty input is valid. + if (descriptor.hasRedactedValue) { + continue + } + + const isLlcRequired = + descriptor.name === LLC_CLASSIFICATION_FIELD && classification === LLC_CLASSIFICATION_OPTION + + if ((descriptor.isRequired || isLlcRequired) && isEmpty(values[descriptor.name])) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [descriptor.name], + message: ContractorSignatureFormErrorCodes.REQUIRED, + }) + } + } + + if (values.agree !== true) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['agree'], + message: ContractorSignatureFormErrorCodes.AGREE_REQUIRED, + }) + } + }) + + return refined as unknown as z.ZodType +} diff --git a/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/fields.tsx b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/fields.tsx new file mode 100644 index 000000000..d0a55debb --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/fields.tsx @@ -0,0 +1,91 @@ +import type { ComponentType, ReactNode } from 'react' +import type { ContractorSignatureFormErrorCode } from './contractorSignatureFormSchema' +import type { W9FieldDescriptor } from './w9Fields' +import { TextInputHookField } from '@/partner-hook-utils/form/fields/TextInputHookField' +import { CheckboxHookField } from '@/partner-hook-utils/form/fields/CheckboxHookField' +import { RadioGroupHookField } from '@/partner-hook-utils/form/fields/RadioGroupHookField' +import { SelectHookField } from '@/partner-hook-utils/form/fields/SelectHookField' +import type { ValidationMessages } from '@/partner-hook-utils/types' + +/** + * Props accepted by a W-9 field component exposed on + * `useContractorSignatureForm`'s `form.Fields`. + * + * @public + */ +export interface ContractorSignatureFieldProps { + /** Visible label rendered above the field. */ + label: string + /** Optional helper text rendered below the field. */ + description?: ReactNode + /** Custom error text keyed by validation error code. */ + validationMessages?: ValidationMessages + /** Placeholder text; used by text and select fields. */ + placeholder?: string + /** Maps an option value to its display label; used by radio and select fields. */ + getOptionLabel?: (value: string) => string +} + +/** + * A W-9 field component pre-bound to its form-field name. + * + * @public + */ +export type ContractorSignatureBoundField = ComponentType + +/** + * Map of W-9 form-field name to its bound field component. + * + * @public + */ +export type ContractorSignatureFields = Record + +/** + * The form-field name of the electronic-signature consent checkbox. + * + * @public + */ +export const AGREE_FIELD = 'agree' + +function boundField( + name: string, + variant: W9FieldDescriptor['variant'], +): ContractorSignatureBoundField { + switch (variant) { + case 'checkbox': + return function BoundCheckbox(props: ContractorSignatureFieldProps) { + return + } + case 'radio': + return function BoundRadioGroup(props: ContractorSignatureFieldProps) { + return + } + case 'select': + return function BoundSelect({ placeholder, ...props }: ContractorSignatureFieldProps) { + return + } + default: + return function BoundTextInput(props: ContractorSignatureFieldProps) { + return + } + } +} + +/** + * Builds the map of bound field components for a W-9 signing form, including + * the always-present `agree` consent checkbox. + * + * @param descriptors - The descriptors produced by `buildW9FieldDescriptors`. + * @returns A {@link ContractorSignatureFields} map keyed by form-field name. + * @public + */ +export function buildContractorSignatureFields( + descriptors: W9FieldDescriptor[], +): ContractorSignatureFields { + const fields: ContractorSignatureFields = {} + for (const descriptor of descriptors) { + fields[descriptor.name] = boundField(descriptor.name, descriptor.variant) + } + fields[AGREE_FIELD] = boundField(AGREE_FIELD, 'checkbox') + return fields +} diff --git a/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/index.ts b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/index.ts new file mode 100644 index 000000000..51cb0bbbc --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/index.ts @@ -0,0 +1,38 @@ +export { + useContractorSignatureForm, + type UseContractorSignatureFormProps, + type UseContractorSignatureFormReady, + type UseContractorSignatureFormResult, + type ContractorSignatureSection, +} from './useContractorSignatureForm' +export { + ContractorSignatureFormErrorCodes, + type ContractorSignatureFormErrorCode, + createContractorSignatureFormSchema, +} from './contractorSignatureFormSchema' +export { + buildContractorSignatureFields, + AGREE_FIELD, + type ContractorSignatureFields, + type ContractorSignatureBoundField, + type ContractorSignatureFieldProps, +} from './fields' +export { + W9_DOCUMENT_NAME, + TAX_CLASSIFICATION_FIELD, + LLC_CLASSIFICATION_FIELD, + TAX_CLASSIFICATION_OPTION_KEYS, + LLC_CLASSIFICATION_CODES, + LLC_CLASSIFICATION_OPTION, + OTHER_CLASSIFICATION_OPTION, + OTHER_TEXT_FIELD, + isW9Document, + buildW9FieldDescriptors, + buildW9Defaults, + serializeW9Fields, + type ContractorSignatureFormData, + type TaxClassificationOptionKey, + type W9FieldDescriptor, + type W9FieldVariant, + type W9Section, +} from './w9Fields' diff --git a/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/useContractorSignatureForm.tsx b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/useContractorSignatureForm.tsx new file mode 100644 index 000000000..f74cc872c --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/useContractorSignatureForm.tsx @@ -0,0 +1,290 @@ +import { useMemo } from 'react' +import { useForm } from 'react-hook-form' +import type { UseFormProps } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import type { Document } from '@gusto/embedded-api-v-2025-11-15/models/components/document' +import type { DocumentSigned } from '@gusto/embedded-api-v-2025-11-15/models/components/documentsigned' +import { useContractorDocumentsGet } from '@gusto/embedded-api-v-2025-11-15/react-query/contractorDocumentsGet' +import { useContractorDocumentsGetPdf } from '@gusto/embedded-api-v-2025-11-15/react-query/contractorDocumentsGetPdf' +import { useContractorDocumentsSignMutation } from '@gusto/embedded-api-v-2025-11-15/react-query/contractorDocumentsSign' +import { + buildW9Defaults, + buildW9FieldDescriptors, + serializeW9Fields, + TAX_CLASSIFICATION_OPTION_KEYS, + LLC_CLASSIFICATION_CODES, + LLC_CLASSIFICATION_FIELD, + TAX_CLASSIFICATION_FIELD, + type ContractorSignatureFormData, + type W9FieldDescriptor, + type W9Section, +} from './w9Fields' +import { createContractorSignatureFormSchema } from './contractorSignatureFormSchema' +import { + AGREE_FIELD, + buildContractorSignatureFields, + type ContractorSignatureFields, +} from './fields' +import { useHookFormInternals } from '@/partner-hook-utils/form/useHookFormInternals' +import { createGetFormSubmissionValues } from '@/partner-hook-utils/form/getFormSubmissionValues' +import { composeErrorHandler } from '@/partner-hook-utils/composeErrorHandler' +import type { + BaseFormHookReady, + FieldsMetadata, + HookLoadingResult, + HookSubmitResult, +} from '@/partner-hook-utils/types' +import { useBaseSubmit } from '@/components/Base/useBaseSubmit' +import { SDKInternalError } from '@/types/sdkError' + +/** + * Props for {@link useContractorSignatureForm}. + * + * @public + */ +export interface UseContractorSignatureFormProps { + /** UUID of the contractor document to sign. */ + documentUuid: string + /** When validation runs. Passed through to react-hook-form; defaults to `'onSubmit'`. */ + validationMode?: UseFormProps['mode'] + /** Auto-focus the first invalid field on submit. Defaults to `true`; set to `false` when using `composeSubmitHandler`. */ + shouldFocusError?: boolean +} + +/** + * A section of the W-9 signing form along with the form-field names to render + * within it, in order. + * + * @public + */ +export interface ContractorSignatureSection { + /** The section identifier (e.g. `'address'`, `'tin'`). */ + section: W9Section + /** Form-field names to render in this section, in order. */ + fieldNames: string[] +} + +/** + * Ready-state shape returned by {@link useContractorSignatureForm} once the + * document metadata has loaded. + * + * @public + */ +export interface UseContractorSignatureFormReady extends BaseFormHookReady< + FieldsMetadata, + ContractorSignatureFormData, + ContractorSignatureFields +> { + /** Loaded data — the document being signed and a preview PDF URL. */ + data: { + /** The document entity fetched from the API. */ + document: Document + /** URL to the document's PDF, or `null` when unavailable. */ + pdfUrl: string | null + /** Ordered sections describing how to group fields when rendering. */ + sections: ContractorSignatureSection[] + /** Whether the document carries signable fields (vs. acknowledge-only). */ + hasFields: boolean + } + /** Submit-state flags. */ + status: { + /** `true` while the sign mutation is in flight. */ + isPending: boolean + /** Always `'create'`; the hook always submits as a signing operation. */ + mode: 'create' + } + /** Imperative actions exposed by the hook. */ + actions: { + /** Validates the form and submits the signature. Resolves with the signed document on success, or `undefined` on validation or API failure. */ + onSubmit: () => Promise | undefined> + } +} + +/** + * Result of {@link useContractorSignatureForm} — a discriminated union on `isLoading`. + * + * @public + */ +export type UseContractorSignatureFormResult = HookLoadingResult | UseContractorSignatureFormReady + +const SECTION_ORDER: W9Section[] = [ + 'classification', + 'exemptions', + 'address', + 'tin', + 'certification', +] + +function buildSections(descriptors: W9FieldDescriptor[]): ContractorSignatureSection[] { + const grouped = new Map() + for (const descriptor of descriptors) { + const names = grouped.get(descriptor.section) ?? [] + names.push(descriptor.name) + grouped.set(descriptor.section, names) + } + return SECTION_ORDER.filter(section => grouped.has(section)).map(section => ({ + section, + fieldNames: grouped.get(section)!, + })) +} + +function buildFieldsMetadata(descriptors: W9FieldDescriptor[]): FieldsMetadata { + const metadata: FieldsMetadata = { + [AGREE_FIELD]: { name: AGREE_FIELD, isRequired: true }, + } + + for (const descriptor of descriptors) { + if (descriptor.name === TAX_CLASSIFICATION_FIELD) { + metadata[descriptor.name] = { + name: descriptor.name, + isRequired: descriptor.isRequired, + options: TAX_CLASSIFICATION_OPTION_KEYS.map(key => ({ value: key, label: key })), + entries: [...TAX_CLASSIFICATION_OPTION_KEYS], + } + continue + } + if (descriptor.name === LLC_CLASSIFICATION_FIELD) { + metadata[descriptor.name] = { + name: descriptor.name, + isRequired: true, + options: LLC_CLASSIFICATION_CODES.map(code => ({ value: code, label: code })), + entries: [...LLC_CLASSIFICATION_CODES], + } + continue + } + metadata[descriptor.name] = { + name: descriptor.name, + isRequired: descriptor.isRequired && !descriptor.hasRedactedValue, + hasRedactedValue: descriptor.hasRedactedValue, + placeholder: descriptor.placeholder, + } + } + + return metadata +} + +/** + * Headless hook for signing a contractor document — displays the document PDF + * and collects the document's fields plus a typed signature and consent. + * + * @remarks + * The fields are driven by the document returned from the API. Today the only + * signable contractor document is the W-9, whose federal tax classification + * checkboxes are presented as a single required radio group with conditional + * LLC-code and "Other" sub-fields; on submit the selection is mapped back to + * the W-9 wire format. Pre-filled values (name, address, TIN, etc.) are + * editable inputs. `data.sections` describes how to group `form.Fields` under + * headings; consult `form.fieldsMetadata` for per-field required flags and + * select/radio options. + * + * @param props - See {@link UseContractorSignatureFormProps}. + * @returns A {@link HookLoadingResult} while loading, or a {@link UseContractorSignatureFormReady} once loaded. + * @public + */ +export function useContractorSignatureForm({ + documentUuid, + validationMode = 'onSubmit', + shouldFocusError = true, +}: UseContractorSignatureFormProps): UseContractorSignatureFormResult { + const documentQuery = useContractorDocumentsGet({ documentUuid }) + // PDF failures are intentionally excluded from the page error surface; the + // viewer degrades gracefully when the URL is unavailable. + const pdfQuery = useContractorDocumentsGetPdf({ documentUuid }) + + const document = documentQuery.data?.document + + const descriptors = useMemo(() => (document ? buildW9FieldDescriptors(document) : []), [document]) + const schema = useMemo(() => createContractorSignatureFormSchema(descriptors), [descriptors]) + const defaultValues = useMemo( + () => (document ? buildW9Defaults(document, descriptors) : { agree: false }), + [document, descriptors], + ) + const Fields = useMemo(() => buildContractorSignatureFields(descriptors), [descriptors]) + const fieldsMetadata = useMemo(() => buildFieldsMetadata(descriptors), [descriptors]) + const sections = useMemo(() => buildSections(descriptors), [descriptors]) + + const formMethods = useForm({ + resolver: zodResolver(schema), + mode: validationMode, + shouldFocusError, + defaultValues, + values: defaultValues, + resetOptions: { keepDirtyValues: true }, + }) + + const signMutation = useContractorDocumentsSignMutation() + const isPending = signMutation.isPending + + const { + baseSubmitHandler, + error: submitError, + setError: setSubmitError, + } = useBaseSubmit('ContractorSignatureForm') + + const errorHandling = composeErrorHandler([documentQuery], { submitError, setSubmitError }) + + const onSubmit = async (): Promise | undefined> => { + let submitResult: HookSubmitResult | undefined + + await new Promise(resolve => { + void formMethods.handleSubmit( + async (data: ContractorSignatureFormData) => { + await baseSubmitHandler(data, async payload => { + if (!document) { + throw new SDKInternalError('Document must be loaded before signing') + } + + const result = await signMutation.mutateAsync({ + request: { + documentUuid, + requestBody: { + fields: serializeW9Fields(document, descriptors, payload), + agree: payload.agree, + }, + }, + }) + + const signedDocument = result.documentSigned + + if (!signedDocument) { + throw new SDKInternalError('Contractor document signing failed') + } + + submitResult = { mode: 'create', data: signedDocument } + }) + resolve() + }, + () => { + resolve() + }, + )() + }) + + return submitResult + } + + const hookFormInternals = useHookFormInternals(formMethods) + + if (documentQuery.isLoading || !document) { + return { isLoading: true, errorHandling } + } + + return { + isLoading: false, + data: { + document, + pdfUrl: pdfQuery.data?.documentPdf?.documentUrl ?? null, + sections, + hasFields: descriptors.length > 0, + }, + status: { isPending, mode: 'create' }, + actions: { onSubmit }, + errorHandling, + form: { + Fields, + fieldsMetadata, + hookFormInternals, + getFormSubmissionValues: createGetFormSubmissionValues(formMethods, schema), + }, + } +} diff --git a/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.test.ts b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.test.ts new file mode 100644 index 000000000..cdfb896f9 --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.test.ts @@ -0,0 +1,256 @@ +import { describe, expect, it } from 'vitest' +import type { Document } from '@gusto/embedded-api-v-2025-11-15/models/components/document' +import { + buildW9Defaults, + buildW9FieldDescriptors, + serializeW9Fields, + TAX_CLASSIFICATION_FIELD, + LLC_CLASSIFICATION_FIELD, + isW9Document, + type ContractorSignatureFormData, +} from './w9Fields' + +function w9Document(overrides: Partial = {}): Document { + return { + uuid: 'doc-1', + name: 'taxpayer_identification_form_w_9', + title: 'W-9', + requiresSigning: true, + fields: [ + { key: 'name', value: 'Klay Thompson', dataType: 'full_name', required: true }, + { key: 'business_name', value: null, dataType: 'text', required: false }, + { key: 'individual_proprietor', value: null, dataType: 'checkbox', required: true }, + { key: 'c_corporation', value: null, dataType: 'checkbox', required: true }, + { key: 's_corporation', value: null, dataType: 'checkbox', required: true }, + { key: 'partnership', value: null, dataType: 'checkbox', required: true }, + { key: 'trust_estate', value: null, dataType: 'checkbox', required: true }, + { key: 'limited_liability_company', value: null, dataType: 'checkbox', required: true }, + { key: 'tax_classification', value: null, dataType: 'text', required: false }, + { key: 'exempt_payee_code', value: null, dataType: 'text', required: false }, + { key: 'exemption_from_FATCA', value: null, dataType: 'checkbox', required: true }, + { key: 'other', value: null, dataType: 'text', required: false }, + { key: 'other_text', value: null, dataType: 'text', required: false }, + { key: 'foreign_partners', value: null, dataType: 'checkbox', required: true }, + { + key: 'home_address_street_1', + value: '525 7th street', + dataType: 'home_address_street_1', + required: true, + }, + { + key: 'home_address_street_2', + value: '', + dataType: 'home_address_street_2', + required: true, + }, + { + key: 'home_address_city', + value: 'New York', + dataType: 'home_address_city', + required: true, + }, + { key: 'home_address_state', value: 'NY', dataType: 'home_address_state', required: true }, + { key: 'home_address_zip', value: '10022', dataType: 'home_address_zip', required: true }, + { + key: 'company_name', + value: 'Entercross Systems', + dataType: 'company_name', + required: true, + }, + { key: 'account_number', value: null, dataType: 'text', required: false }, + { key: 'ssn', value: 'XXX-XX-3123', dataType: 'ssn', required: true }, + { key: 'ein', value: 'N/A', dataType: 'ein', required: true }, + { key: 'signature_text', value: null, dataType: 'signature', required: true }, + { key: 'date', value: '9/17/2025', dataType: 'date', required: true }, + ], + ...overrides, + } +} + +describe('isW9Document', () => { + it('identifies the W-9 by name', () => { + expect(isW9Document(w9Document())).toBe(true) + expect(isW9Document(w9Document({ name: 'contractor_handbook' }))).toBe(false) + }) +}) + +describe('buildW9FieldDescriptors', () => { + it('synthesizes a single required classification radio from the checkbox keys', () => { + const descriptors = buildW9FieldDescriptors(w9Document()) + const classification = descriptors.find(d => d.name === TAX_CLASSIFICATION_FIELD) + expect(classification).toMatchObject({ + name: TAX_CLASSIFICATION_FIELD, + variant: 'radio', + section: 'classification', + isRequired: true, + }) + }) + + it('does not render the seven classification checkboxes as standalone fields', () => { + const names = buildW9FieldDescriptors(w9Document()).map(d => d.name) + expect(names).not.toContain('individual_proprietor') + expect(names).not.toContain('c_corporation') + expect(names).not.toContain('limited_liability_company') + }) + + it('marks gws-flows-optional fields optional regardless of the API required flag', () => { + const descriptors = buildW9FieldDescriptors(w9Document()) + const byName = Object.fromEntries(descriptors.map(d => [d.name, d])) + expect(byName.business_name).toMatchObject({ isRequired: false }) + expect(byName.account_number).toMatchObject({ isRequired: false }) + expect(byName.company_name).toMatchObject({ isRequired: false }) + expect(byName.home_address_street_2).toMatchObject({ isRequired: false }) + }) + + it('keeps API-required fields required', () => { + const byName = Object.fromEntries(buildW9FieldDescriptors(w9Document()).map(d => [d.name, d])) + expect(byName.name).toMatchObject({ isRequired: true }) + expect(byName.ssn).toMatchObject({ isRequired: true }) + expect(byName.signature_text).toMatchObject({ isRequired: true }) + }) + + it('groups fields into ordered sections', () => { + const sections = buildW9FieldDescriptors(w9Document()).map(d => d.section) + expect(sections).toContain('classification') + expect(sections).toContain('exemptions') + expect(sections).toContain('address') + expect(sections).toContain('tin') + expect(sections).toContain('certification') + }) +}) + +describe('buildW9Defaults', () => { + it('seeds editable inputs from the API values', () => { + const descriptors = buildW9FieldDescriptors(w9Document()) + const defaults = buildW9Defaults(w9Document(), descriptors) + expect(defaults).toMatchObject({ + name: 'Klay Thompson', + home_address_street_1: '525 7th street', + home_address_city: 'New York', + ein: 'N/A', + company_name: 'Entercross Systems', + agree: false, + [TAX_CLASSIFICATION_FIELD]: '', + }) + }) + + it('seeds an empty input for a masked (redacted) SSN', () => { + const descriptors = buildW9FieldDescriptors(w9Document()) + const defaults = buildW9Defaults(w9Document(), descriptors) + expect(defaults.ssn).toBe('') + }) + + it('preselects the classification whose checkbox value is set', () => { + const document = w9Document({ + fields: w9Document().fields?.map(field => + field.key === 's_corporation' ? { ...field, value: '1' } : field, + ), + }) + const descriptors = buildW9FieldDescriptors(document) + const defaults = buildW9Defaults(document, descriptors) + expect(defaults[TAX_CLASSIFICATION_FIELD]).toBe('s_corporation') + }) +}) + +describe('serializeW9Fields', () => { + const descriptors = buildW9FieldDescriptors(w9Document()) + + function baseValues( + overrides: Partial = {}, + ): ContractorSignatureFormData { + return { + ...buildW9Defaults(w9Document(), descriptors), + [TAX_CLASSIFICATION_FIELD]: 'c_corporation', + signature_text: 'Klay Thompson', + agree: true, + ...overrides, + } + } + + it('maps the chosen classification to value "1"', () => { + const fields = serializeW9Fields(w9Document(), descriptors, baseValues()) + expect(fields).toContainEqual({ key: 'c_corporation', value: '1' }) + }) + + it('only sends the chosen classification, not the unchosen ones', () => { + const fields = serializeW9Fields(w9Document(), descriptors, baseValues()) + const classificationKeys = fields.filter(f => f.value === '1').map(f => f.key) + expect(classificationKeys).toEqual(['c_corporation']) + }) + + it('sends the LLC code under the tax_classification key when LLC is selected', () => { + const fields = serializeW9Fields( + w9Document(), + descriptors, + baseValues({ + [TAX_CLASSIFICATION_FIELD]: 'limited_liability_company', + [LLC_CLASSIFICATION_FIELD]: 'p', + }), + ) + expect(fields).toContainEqual({ key: 'limited_liability_company', value: '1' }) + expect(fields).toContainEqual({ key: 'tax_classification', value: 'p' }) + }) + + it('includes other_text only when the Other classification is selected', () => { + const withOther = serializeW9Fields( + w9Document(), + descriptors, + baseValues({ [TAX_CLASSIFICATION_FIELD]: 'other', other_text: 'Custom entity' }), + ) + expect(withOther).toContainEqual({ key: 'other_text', value: 'Custom entity' }) + + const withoutOther = serializeW9Fields(w9Document(), descriptors, baseValues()) + expect(withoutOther.find(f => f.key === 'other_text')).toBeUndefined() + }) + + it('serializes checkboxes to "1"/"0"', () => { + const checked = serializeW9Fields( + w9Document(), + descriptors, + baseValues({ foreign_partners: true, exemption_from_FATCA: false }), + ) + expect(checked).toContainEqual({ key: 'foreign_partners', value: '1' }) + expect(checked).toContainEqual({ key: 'exemption_from_FATCA', value: '0' }) + }) + + it('sets the date field to today in ISO format', () => { + const today = new Date().toISOString().slice(0, 10) + const fields = serializeW9Fields(w9Document(), descriptors, baseValues()) + expect(fields).toContainEqual({ key: 'date', value: today }) + }) + + it('passes edited non-sensitive values through as editable inputs', () => { + const fields = serializeW9Fields(w9Document(), descriptors, baseValues({ name: 'Edited Name' })) + expect(fields).toContainEqual({ key: 'name', value: 'Edited Name' }) + expect(fields).toContainEqual({ key: 'ein', value: 'N/A' }) + }) + + it('omits an untouched redacted SSN so the mask is never sent back', () => { + const fields = serializeW9Fields(w9Document(), descriptors, baseValues({ ssn: '' })) + expect(fields.find(f => f.key === 'ssn')).toBeUndefined() + }) + + it('sends a replacement SSN when the contractor enters one', () => { + const fields = serializeW9Fields(w9Document(), descriptors, baseValues({ ssn: '123-45-6789' })) + expect(fields).toContainEqual({ key: 'ssn', value: '123-45-6789' }) + }) +}) + +describe('redacted (masked) fields', () => { + it('flags a masked SSN as redacted and surfaces the mask as a placeholder', () => { + const descriptors = buildW9FieldDescriptors(w9Document()) + const ssn = descriptors.find(d => d.name === 'ssn') + expect(ssn).toMatchObject({ + name: 'ssn', + hasRedactedValue: true, + placeholder: 'XXX-XX-3123', + isRequired: true, + }) + }) + + it('does not flag an unmasked sensitive value (EIN "N/A") as redacted', () => { + const descriptors = buildW9FieldDescriptors(w9Document()) + const ein = descriptors.find(d => d.name === 'ein') + expect(ein).toMatchObject({ name: 'ein', hasRedactedValue: false }) + }) +}) diff --git a/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.ts b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.ts new file mode 100644 index 000000000..d739af71e --- /dev/null +++ b/src/components/Contractor/Documents/SignatureForm/useContractorSignatureForm/w9Fields.ts @@ -0,0 +1,469 @@ +import type { + Document, + Fields as DocumentField, +} from '@gusto/embedded-api-v-2025-11-15/models/components/document' + +/** + * The `name` of the W-9 document — the only contractor document type that + * supports signing today. + * + * @public + */ +export const W9_DOCUMENT_NAME = 'taxpayer_identification_form_w_9' + +/** + * Form-field name for the synthesized federal tax classification radio group. + * + * @public + */ +export const TAX_CLASSIFICATION_FIELD = 'taxClassification' + +/** + * Form-field name for the synthesized LLC tax classification code select. + * + * @public + */ +export const LLC_CLASSIFICATION_FIELD = 'llcClassificationCode' + +/** + * Ordered classification option keys backing the {@link TAX_CLASSIFICATION_FIELD} + * radio group. Each maps to a W-9 checkbox field on the underlying document. + * + * @public + */ +export const TAX_CLASSIFICATION_OPTION_KEYS = [ + 'individual_proprietor', + 'c_corporation', + 's_corporation', + 'partnership', + 'trust_estate', + 'limited_liability_company', + 'other', +] as const + +/** + * A single federal tax classification option key. + * + * @public + */ +export type TaxClassificationOptionKey = (typeof TAX_CLASSIFICATION_OPTION_KEYS)[number] + +/** + * The classification option that reveals the LLC tax classification select. + * + * @public + */ +export const LLC_CLASSIFICATION_OPTION: TaxClassificationOptionKey = 'limited_liability_company' + +/** + * The classification option that reveals the "Other" free-text field. + * + * @public + */ +export const OTHER_CLASSIFICATION_OPTION: TaxClassificationOptionKey = 'other' + +/** + * Ordered LLC tax classification code options. + * + * @public + */ +export const LLC_CLASSIFICATION_CODES = ['c', 's', 'p'] as const + +/** + * The W-9 `other_text` API field key, revealed when "Other" is selected. + * + * @public + */ +export const OTHER_TEXT_FIELD = 'other_text' + +/** + * The W-9 `date` API field key, populated automatically at submit time. + * + * @public + */ +export const SIGNED_DATE_FIELD = 'date' + +/** + * Section a W-9 field belongs to, used to group fields under headings when + * rendering the signing form. + * + * @public + */ +export type W9Section = 'classification' | 'exemptions' | 'address' | 'tin' | 'certification' + +/** + * Visual input variant for a W-9 field. + * + * @public + */ +export type W9FieldVariant = 'text' | 'checkbox' | 'radio' | 'select' + +/** + * A render-ready descriptor for a single W-9 form field. + * + * @public + */ +export interface W9FieldDescriptor { + /** react-hook-form field name (also the i18n label sub-key). */ + name: string + /** Underlying API field key for pass-through fields; absent for synthesized fields. */ + apiKey?: string + /** Input variant used to pick the bound field component. */ + variant: W9FieldVariant + /** Section this field is grouped under. */ + section: W9Section + /** Whether the field must have a value for the form to submit. */ + isRequired: boolean + /** When set, the field only renders while the classification radio holds this option. */ + visibleWhenClassification?: TaxClassificationOptionKey + /** + * Whether the API returned a masked value for this field (e.g. a redacted + * SSN/EIN). Redacted fields seed an empty input, surface the mask as a + * placeholder, are exempt from required validation, and are omitted from the + * sign payload unless the contractor types a replacement. + */ + hasRedactedValue?: boolean + /** The masked value to display as a placeholder for a redacted field. */ + placeholder?: string +} + +/** API field keys whose values are sensitive and arrive masked when on file. */ +const SENSITIVE_FIELD_KEYS = new Set(['ssn', 'ein']) + +/** Whether a value is a server-side mask (e.g. `XXX-XX-1111`) rather than real input. */ +function isMaskedValue(value: string | null | undefined): boolean { + return typeof value === 'string' && /[X•*]/.test(value) +} + +/** Whether a document field carries a redacted (masked) sensitive value. */ +function hasRedactedValue(field: DocumentField | undefined): boolean { + if (!field) return false + const isSensitive = + SENSITIVE_FIELD_KEYS.has(field.key ?? '') || + field.dataType === 'ssn' || + field.dataType === 'ein' + return isSensitive && isMaskedValue(field.value) +} + +/** + * The W-9 layout plan, in render order. Pass-through entries carry the API + * field key; synthesized entries (`taxClassification`, `llcClassificationCode`) + * do not. The actual rendered set is filtered to fields present on the document. + */ +const W9_PLAN: Array<{ + name: string + apiKey?: string + variant: W9FieldVariant + section: W9Section + optional?: boolean + visibleWhenClassification?: TaxClassificationOptionKey +}> = [ + { name: 'name', apiKey: 'name', variant: 'text', section: 'classification' }, + { + name: 'business_name', + apiKey: 'business_name', + variant: 'text', + section: 'classification', + optional: true, + }, + { name: TAX_CLASSIFICATION_FIELD, variant: 'radio', section: 'classification' }, + { + name: LLC_CLASSIFICATION_FIELD, + variant: 'select', + section: 'classification', + optional: true, + visibleWhenClassification: LLC_CLASSIFICATION_OPTION, + }, + { + name: OTHER_TEXT_FIELD, + apiKey: OTHER_TEXT_FIELD, + variant: 'text', + section: 'classification', + optional: true, + visibleWhenClassification: OTHER_CLASSIFICATION_OPTION, + }, + { + name: 'foreign_partners', + apiKey: 'foreign_partners', + variant: 'checkbox', + section: 'classification', + optional: true, + }, + { + name: 'exempt_payee_code', + apiKey: 'exempt_payee_code', + variant: 'text', + section: 'exemptions', + optional: true, + }, + { + name: 'exemption_from_FATCA', + apiKey: 'exemption_from_FATCA', + variant: 'checkbox', + section: 'exemptions', + optional: true, + }, + { + name: 'home_address_street_1', + apiKey: 'home_address_street_1', + variant: 'text', + section: 'address', + }, + { + name: 'home_address_street_2', + apiKey: 'home_address_street_2', + variant: 'text', + section: 'address', + optional: true, + }, + { name: 'home_address_city', apiKey: 'home_address_city', variant: 'text', section: 'address' }, + { name: 'home_address_state', apiKey: 'home_address_state', variant: 'text', section: 'address' }, + { name: 'home_address_zip', apiKey: 'home_address_zip', variant: 'text', section: 'address' }, + { + name: 'account_number', + apiKey: 'account_number', + variant: 'text', + section: 'address', + optional: true, + }, + { + name: 'company_name', + apiKey: 'company_name', + variant: 'text', + section: 'address', + optional: true, + }, + { name: 'ssn', apiKey: 'ssn', variant: 'text', section: 'tin' }, + { name: 'ein', apiKey: 'ein', variant: 'text', section: 'tin' }, + { name: 'signature_text', apiKey: 'signature_text', variant: 'text', section: 'certification' }, +] + +function indexFields(document: Document): Map { + const byKey = new Map() + for (const field of document.fields ?? []) { + if (field.key) byKey.set(field.key, field) + } + return byKey +} + +/** + * Whether the document is a W-9 with signable fields. + * + * @public + */ +export function isW9Document(document: Document): boolean { + return document.name === W9_DOCUMENT_NAME +} + +/** + * Builds the ordered, render-ready W-9 field descriptors for a document. + * + * @remarks + * Pass-through fields are included only when present on the document; their + * `isRequired` flag is driven by the API `required` flag unless the W-9 layout + * marks them optional. The classification radio is included when any of the + * classification checkbox fields are present. + * + * @param document - The W-9 document returned by the API. + * @returns The ordered list of field descriptors to render. + * @public + */ +export function buildW9FieldDescriptors(document: Document): W9FieldDescriptor[] { + const byKey = indexFields(document) + const hasClassification = TAX_CLASSIFICATION_OPTION_KEYS.some(key => byKey.has(key)) + + const descriptors: W9FieldDescriptor[] = [] + for (const entry of W9_PLAN) { + if (entry.apiKey) { + const field = byKey.get(entry.apiKey) + if (!field) continue + const redacted = hasRedactedValue(field) + descriptors.push({ + name: entry.name, + apiKey: entry.apiKey, + variant: entry.variant, + section: entry.section, + isRequired: entry.optional ? false : Boolean(field.required), + visibleWhenClassification: entry.visibleWhenClassification, + hasRedactedValue: redacted, + placeholder: redacted ? (field.value ?? undefined) : undefined, + }) + continue + } + + if (entry.name === TAX_CLASSIFICATION_FIELD) { + if (!hasClassification) continue + descriptors.push({ + name: entry.name, + variant: entry.variant, + section: entry.section, + isRequired: true, + }) + continue + } + + if (entry.name === LLC_CLASSIFICATION_FIELD) { + if (!hasClassification) continue + descriptors.push({ + name: entry.name, + variant: entry.variant, + section: entry.section, + isRequired: false, + visibleWhenClassification: LLC_CLASSIFICATION_OPTION, + }) + } + } + + return descriptors +} + +/** Coerces an API checkbox value (`'1'`, `'true'`) to a boolean. */ +function toBoolean(value: string | null | undefined): boolean { + return value === '1' || value === 'true' +} + +/** + * The shape of values managed by the W-9 signing form. + * + * @public + */ +export type ContractorSignatureFormData = Record & { + /** Electronic-signature consent; must be checked to submit. */ + agree: boolean +} + +/** + * Builds default form values from a document's fields. + * + * @remarks + * Pass-through values are seeded from the API `value`. The classification radio + * defaults to whichever classification checkbox is already set (`'1'`). + * + * @param document - The W-9 document returned by the API. + * @param descriptors - The descriptors produced by {@link buildW9FieldDescriptors}. + * @returns The default form values. + * @public + */ +export function buildW9Defaults( + document: Document, + descriptors: W9FieldDescriptor[], +): ContractorSignatureFormData { + const byKey = indexFields(document) + const defaults: ContractorSignatureFormData = { agree: false } + + for (const descriptor of descriptors) { + if (descriptor.name === TAX_CLASSIFICATION_FIELD) { + const selected = TAX_CLASSIFICATION_OPTION_KEYS.find(key => toBoolean(byKey.get(key)?.value)) + defaults[descriptor.name] = selected ?? '' + continue + } + if (descriptor.name === LLC_CLASSIFICATION_FIELD) { + defaults[descriptor.name] = '' + continue + } + // A masked value must not seed the input — it would be echoed back as the + // new value on submit. The mask surfaces as a placeholder instead. + if (descriptor.hasRedactedValue) { + defaults[descriptor.name] = '' + continue + } + const field = descriptor.apiKey ? byKey.get(descriptor.apiKey) : undefined + if (descriptor.variant === 'checkbox') { + defaults[descriptor.name] = toBoolean(field?.value) + } else { + defaults[descriptor.name] = field?.value ?? '' + } + } + + return defaults +} + +/** + * A single `{ key, value }` pair sent in the sign request. + * + * @public + */ +export interface SignFieldValue { + /** The API field key. */ + key: string + /** The serialized field value. */ + value: string +} + +/** + * Serializes W-9 form values into the `{ key, value }` field array expected by + * the sign API. + * + * @remarks + * Applies the W-9 mapping: the chosen classification key is sent with value + * `'1'`; when the LLC classification is chosen, the selected LLC code is sent + * under the `tax_classification` key. Checkboxes serialize to `'1'`/`'0'`. The + * `other_text` and `llcClassificationCode` values are only included for their + * respective classifications. The `date` field, when present on the document, + * is set to today's date. + * + * @param document - The W-9 document being signed. + * @param descriptors - The descriptors produced by {@link buildW9FieldDescriptors}. + * @param values - The validated form values. + * @returns The ordered field array for the sign request body. + * @public + */ +export function serializeW9Fields( + document: Document, + descriptors: W9FieldDescriptor[], + values: ContractorSignatureFormData, +): SignFieldValue[] { + const byKey = indexFields(document) + const fields: SignFieldValue[] = [] + + const classification = String(values[TAX_CLASSIFICATION_FIELD] ?? '') + + for (const descriptor of descriptors) { + if ( + descriptor.name === TAX_CLASSIFICATION_FIELD || + descriptor.name === LLC_CLASSIFICATION_FIELD + ) { + continue + } + if (!descriptor.apiKey) continue + + // Conditionally-revealed fields are only submitted when their controlling + // classification is active — matching the rendered form. + if ( + descriptor.visibleWhenClassification && + descriptor.visibleWhenClassification !== classification + ) { + continue + } + + const value = values[descriptor.name] + + // A redacted field left untouched is omitted so the masked value isn't sent + // back as a replacement; the server keeps the value already on file. + if (descriptor.hasRedactedValue && (value === undefined || value === null || value === '')) { + continue + } + + if (descriptor.variant === 'checkbox') { + fields.push({ key: descriptor.apiKey, value: value ? '1' : '0' }) + } else { + fields.push({ key: descriptor.apiKey, value: String(value ?? '') }) + } + } + + if (classification) { + fields.push({ key: classification, value: '1' }) + if (classification === LLC_CLASSIFICATION_OPTION) { + fields.push({ + key: 'tax_classification', + value: String(values[LLC_CLASSIFICATION_FIELD] ?? ''), + }) + } + } + + if (byKey.has(SIGNED_DATE_FIELD)) { + fields.push({ key: SIGNED_DATE_FIELD, value: new Date().toISOString().slice(0, 10) }) + } + + return fields +} diff --git a/src/components/Contractor/Documents/documentSignerStateMachine.tsx b/src/components/Contractor/Documents/documentSignerStateMachine.tsx new file mode 100644 index 000000000..5e9c36b6b --- /dev/null +++ b/src/components/Contractor/Documents/documentSignerStateMachine.tsx @@ -0,0 +1,41 @@ +import type { DocumentSigned } from '@gusto/embedded-api-v-2025-11-15/models/components/documentsigned' +import { DocumentsList } from './DocumentsList' +import { SignatureForm } from './SignatureForm' +import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow' +import type { contractorEvents, componentEvents } from '@/shared/constants' +import { ensureRequired } from '@/helpers/ensureRequired' + +/** @internal */ +export type EventPayloads = { + [contractorEvents.CONTRACTOR_VIEW_DOCUMENT_TO_SIGN]: { uuid: string; title?: string } + [contractorEvents.CONTRACTOR_SIGN_DOCUMENT]: DocumentSigned + [contractorEvents.CONTRACTOR_DOCUMENTS_DONE]: undefined + [componentEvents.CANCEL]: undefined +} + +/** @internal */ +export interface ContractorDocumentSignerContextInterface extends FlowContextInterface { + contractorId: string + documentUuid?: string +} + +/** @internal */ +export function DocumentsListContextual() { + const { contractorId, onEvent } = useFlow() + + return +} + +/** @internal */ +export function SignatureFormContextual() { + const { contractorId, documentUuid, onEvent } = + useFlow() + + return ( + + ) +} diff --git a/src/components/Contractor/Documents/index.stories.tsx b/src/components/Contractor/Documents/index.stories.tsx new file mode 100644 index 000000000..831f3745a --- /dev/null +++ b/src/components/Contractor/Documents/index.stories.tsx @@ -0,0 +1,33 @@ +import { fn } from 'storybook/test' +import { ContractorDocumentSigner } from './ContractorDocumentSigner' +import { DocumentsList } from './DocumentsList' +import { SignatureForm } from './SignatureForm' + +export default { + title: 'Domain/Contractor/Documents', +} + +const contractorId = 'mock-contractor-id' +const documentUuid = 'mock-document-uuid' + +export const DocumentSigner = () => ( + +) + +export const List = () => ( + +) + +export const Signature = () => ( + +) diff --git a/src/components/Contractor/Documents/index.ts b/src/components/Contractor/Documents/index.ts new file mode 100644 index 000000000..a9ce91d4a --- /dev/null +++ b/src/components/Contractor/Documents/index.ts @@ -0,0 +1,13 @@ +export { + ContractorDocumentSigner, + type ContractorDocumentSignerProps, +} from './ContractorDocumentSigner' +export { DocumentsList, type DocumentsListProps } from './DocumentsList' +export { SignatureForm, type SignatureFormProps } from './SignatureForm' +export { + useContractorDocumentsList, + type UseContractorDocumentsListParams, + type UseContractorDocumentsListReady, + type UseContractorDocumentsListResult, +} from './DocumentsList' +export * from './SignatureForm/useContractorSignatureForm' diff --git a/src/components/Contractor/Documents/stateMachine.ts b/src/components/Contractor/Documents/stateMachine.ts new file mode 100644 index 000000000..fb816bf39 --- /dev/null +++ b/src/components/Contractor/Documents/stateMachine.ts @@ -0,0 +1,61 @@ +import { state, transition, reduce, state as final } from 'robot3' +import type { + ContractorDocumentSignerContextInterface, + EventPayloads, +} from './documentSignerStateMachine' +import { DocumentsListContextual, SignatureFormContextual } from './documentSignerStateMachine' +import { contractorEvents, componentEvents } from '@/shared/constants' +import type { MachineEventType, MachineTransition } from '@/types/Helpers' + +/** @internal */ +export const contractorDocumentSignerMachine = { + list: state( + transition( + contractorEvents.CONTRACTOR_VIEW_DOCUMENT_TO_SIGN, + 'signatureForm', + reduce( + ( + ctx: ContractorDocumentSignerContextInterface, + ev: MachineEventType< + EventPayloads, + typeof contractorEvents.CONTRACTOR_VIEW_DOCUMENT_TO_SIGN + >, + ): ContractorDocumentSignerContextInterface => ({ + ...ctx, + documentUuid: ev.payload.uuid, + component: SignatureFormContextual, + }), + ), + ), + transition(contractorEvents.CONTRACTOR_DOCUMENTS_DONE, 'done'), + ), + signatureForm: state( + transition( + contractorEvents.CONTRACTOR_SIGN_DOCUMENT, + 'list', + reduce( + ( + ctx: ContractorDocumentSignerContextInterface, + ): ContractorDocumentSignerContextInterface => ({ + ...ctx, + documentUuid: undefined, + component: DocumentsListContextual, + }), + ), + ), + transition( + componentEvents.CANCEL, + 'list', + reduce( + ( + ctx: ContractorDocumentSignerContextInterface, + ): ContractorDocumentSignerContextInterface => ({ + ...ctx, + documentUuid: undefined, + component: DocumentsListContextual, + }), + ), + ), + ), + done: final(), +} diff --git a/src/components/Contractor/exports/contractorOnboarding.ts b/src/components/Contractor/exports/contractorOnboarding.ts index f8aaa8b7b..0fd795ab3 100644 --- a/src/components/Contractor/exports/contractorOnboarding.ts +++ b/src/components/Contractor/exports/contractorOnboarding.ts @@ -14,3 +14,9 @@ export type { PaymentMethodProps } from '../PaymentMethod/types' export { NewHireReport } from '../NewHireReport/NewHireReport' export type { NewHireReportProps } from '../NewHireReport/types' export { ContractorSubmit, type ContractorSubmitProps } from '../Submit/Submit' +export { + ContractorDocumentSigner, + type ContractorDocumentSignerProps, +} from '../Documents/ContractorDocumentSigner' +export { DocumentsList, type DocumentsListProps } from '../Documents/DocumentsList' +export { SignatureForm, type SignatureFormProps } from '../Documents/SignatureForm' diff --git a/src/i18n/en/Contractor.DocumentsList.json b/src/i18n/en/Contractor.DocumentsList.json new file mode 100644 index 000000000..b60af042d --- /dev/null +++ b/src/i18n/en/Contractor.DocumentsList.json @@ -0,0 +1,13 @@ +{ + "title": "Documents", + "subtitle": "Please review and sign your documents.", + "documentListLabel": "Documents", + "documentColumnLabel": "Name", + "statusColumnLabel": "Status", + "signDocumentCta": "Sign document", + "notSigned": "Not signed", + "signed": "Complete", + "emptyTitle": "No documents found", + "errorTitle": "Could not load your documents, try again later.", + "continueCta": "Continue" +} diff --git a/src/i18n/en/Contractor.SignatureForm.json b/src/i18n/en/Contractor.SignatureForm.json new file mode 100644 index 000000000..582dae9fb --- /dev/null +++ b/src/i18n/en/Contractor.SignatureForm.json @@ -0,0 +1,113 @@ +{ + "signatureRequired": "Signature required", + "instructions": "See the IRS Form W-9 for instructions on completing this form.", + "downloadPrompt": "Download document", + "viewDocumentCta": "View document", + "sections": { + "exemptions": "Exemptions", + "address": "Address", + "tin": "Taxpayer Identification Number (TIN)", + "certification": "Certification" + }, + "sectionInstructions": { + "exemptions": "See section 4 of IRS Form W-9 for instructions.", + "address": "See sections 5-7 of IRS Form W-9 for instructions.", + "tin": "For individuals, this is generally your social security number (SSN). For other entities, this will be your employer identification number (EIN).", + "tinSecondary": "See Part 1 of the IRS Form W-9 for instructions.", + "certification": "See Part 2 of the IRS Form W-9 for instructions." + }, + "certificationIntro": "Under penalties of perjury, I certify that:", + "certificationPoints": [ + "The number shown on this form is my correct taxpayer identification number (or I am waiting for a number to be issued to me);", + "I am not subject to backup withholding because (a) I am exempt from backup withholding, or (b) I have not been notified by the Internal Revenue Service (IRS) that I am subject to backup withholding as a result of a failure to report all interest or dividends, or (c) the IRS has notified me that I am no longer subject to backup withholding;", + "I am a U.S. citizen or other U.S. person (defined below);", + "The FATCA code(s) entered on this form (if any) indicating that I am exempt from FATCA reporting is correct." + ], + "fields": { + "name": { + "label": "Entity or individual name (1)", + "description": "Your individual or business name. For a sole proprietor or disregarded entity, enter the owner's name here, and enter the business/disregarded entity's name in the Business name field below." + }, + "business_name": { + "label": "Business name (2)", + "description": "The name of your business, if different than above." + }, + "taxClassification": { + "label": "Federal tax classification (3)", + "description": "Select the type of business you are." + }, + "llcClassificationCode": { + "label": "LLC tax classification code", + "description": "Select the appropriate code for the LLC, unless it is a disregarded entity. A disregarded entity should instead check the appropriate selection for the tax classification of its owner." + }, + "other_text": { + "label": "Other" + }, + "foreign_partners": { + "label": "Foreign partners/owners/beneficiaries (3b)", + "description": "If you selected \u201cPartnership\u201d, \u201cTrust/Estate\u201d or checked \u201cLLC\u201d and entered \u201cP\u201d as the tax classification, and you are providing this form to a partnership, trust, or estate in which you have an ownership interest, check this box if you have any foreign partners, owners, or beneficiaries." + }, + "exempt_payee_code": { + "label": "Exempt payee code" + }, + "exemption_from_FATCA": { + "label": "Exemption from FATCA", + "description": "This is an exemption from the Foreign Account Tax Compliance Act." + }, + "home_address_street_1": { + "label": "Street 1" + }, + "home_address_street_2": { + "label": "Street 2" + }, + "home_address_city": { + "label": "City" + }, + "home_address_state": { + "label": "State" + }, + "home_address_zip": { + "label": "Zip code" + }, + "account_number": { + "label": "Account numbers" + }, + "company_name": { + "label": "Requester's name and address" + }, + "ssn": { + "label": "Social Security Number (SSN)" + }, + "ein": { + "label": "Employer Identification Number (EIN)" + }, + "signature_text": { + "label": "Signature" + } + }, + "options": { + "taxClassification": { + "individual_proprietor": "Individual/sole proprietor", + "c_corporation": "C-Corporation", + "s_corporation": "S-Corporation", + "partnership": "Partnership", + "trust_estate": "Trust/estate", + "limited_liability_company": "LLC", + "other": "Other" + }, + "llcClassificationCode": { + "placeholder": "Select...", + "c": "C = C-Corporation", + "s": "S = S-Corporation", + "p": "P = Partnership" + } + }, + "agreeLabel": "I agree to electronically sign this form.", + "validation": { + "required": "This field is required.", + "agreeRequired": "You must agree to electronically sign this form." + }, + "backCta": "Back", + "signCta": "Sign", + "acknowledgeCta": "Acknowledge" +} diff --git a/src/index.ts b/src/index.ts index c2f127e49..05a10b34e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -684,3 +684,46 @@ export type { SignatureFieldProps, ConfirmSignatureFieldProps, } from '@/components/Company/DocumentSigner/shared/useSignCompanyForm' + +// Domain data hooks - Contractor +export { useContractorDocumentsList } from '@/components/Contractor/Documents/DocumentsList/useContractorDocumentsList' +export type { + UseContractorDocumentsListParams, + UseContractorDocumentsListResult, + UseContractorDocumentsListReady, +} from '@/components/Contractor/Documents/DocumentsList/useContractorDocumentsList' + +export { + useContractorSignatureForm, + ContractorSignatureFormErrorCodes, + createContractorSignatureFormSchema, + buildContractorSignatureFields, + AGREE_FIELD, + W9_DOCUMENT_NAME, + TAX_CLASSIFICATION_FIELD, + LLC_CLASSIFICATION_FIELD, + TAX_CLASSIFICATION_OPTION_KEYS, + LLC_CLASSIFICATION_CODES, + LLC_CLASSIFICATION_OPTION, + OTHER_CLASSIFICATION_OPTION, + OTHER_TEXT_FIELD, + isW9Document, + buildW9FieldDescriptors, + buildW9Defaults, + serializeW9Fields, +} from '@/components/Contractor/Documents/SignatureForm/useContractorSignatureForm' +export type { + UseContractorSignatureFormProps, + UseContractorSignatureFormResult, + UseContractorSignatureFormReady, + ContractorSignatureSection, + ContractorSignatureFormErrorCode, + ContractorSignatureFields, + ContractorSignatureBoundField, + ContractorSignatureFieldProps, + ContractorSignatureFormData, + TaxClassificationOptionKey, + W9FieldDescriptor, + W9FieldVariant, + W9Section, +} from '@/components/Contractor/Documents/SignatureForm/useContractorSignatureForm' diff --git a/src/partner-hook-utils/types.ts b/src/partner-hook-utils/types.ts index 79e559be4..47a6d69ce 100644 --- a/src/partner-hook-utils/types.ts +++ b/src/partner-hook-utils/types.ts @@ -22,6 +22,8 @@ export interface FieldMetadata { isDisabled?: boolean /** Whether the server returned a redacted placeholder instead of the real value. */ hasRedactedValue?: boolean + /** Placeholder text a hook supplies for the field (e.g. a masked value to display while the input is empty). */ + placeholder?: string /** ISO date string lower bound for date picker fields. Set by hooks; consumed by DatePickerHookField. */ minDate?: string | null /** ISO date string upper bound for date picker fields. Set by hooks; consumed by DatePickerHookField. */ diff --git a/src/shared/constants.ts b/src/shared/constants.ts index c7c69c307..cc4567e77 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -236,6 +236,9 @@ export const contractorEvents = { CONTRACTOR_ONBOARDING_STATUS_UPDATED: 'contractor/onboardingStatus/updated', CONTRACTOR_INVITE_CONTRACTOR: 'contractor/invite/selfOnboarding', CONTRACTOR_ONBOARDING_CONTINUE: 'contractor/onboarding/continue', + CONTRACTOR_VIEW_DOCUMENT_TO_SIGN: 'contractor/documents/view', + CONTRACTOR_SIGN_DOCUMENT: 'contractor/documents/sign', + CONTRACTOR_DOCUMENTS_DONE: 'contractor/documents/done', } as const /** diff --git a/src/test/mocks/apis/contractor_documents.ts b/src/test/mocks/apis/contractor_documents.ts new file mode 100644 index 000000000..b4d645326 --- /dev/null +++ b/src/test/mocks/apis/contractor_documents.ts @@ -0,0 +1,179 @@ +import type { HttpResponseResolver } from 'msw' +import { http, HttpResponse } from 'msw' +import { API_BASE_URL } from '@/test/constants' + +/** + * Realistic W-9 `fields` fixture mirroring the shape returned by the contractor + * document API (snake_case, pre-SDK parsing). Sourced from the gws-flows W-9 + * request spec. + */ +export const w9DocumentFields = [ + { key: 'name', value: 'Klay Thompson', data_type: 'full_name', required: true, page_number: 0 }, + { key: 'business_name', value: null, data_type: 'text', required: false, page_number: 0 }, + { + key: 'individual_proprietor', + value: null, + data_type: 'checkbox', + required: true, + page_number: 0, + }, + { key: 'c_corporation', value: null, data_type: 'checkbox', required: true, page_number: 0 }, + { key: 's_corporation', value: null, data_type: 'checkbox', required: true, page_number: 0 }, + { key: 'partnership', value: null, data_type: 'checkbox', required: true, page_number: 0 }, + { key: 'trust_estate', value: null, data_type: 'checkbox', required: true, page_number: 0 }, + { + key: 'limited_liability_company', + value: null, + data_type: 'checkbox', + required: true, + page_number: 0, + }, + { key: 'tax_classification', value: null, data_type: 'text', required: false, page_number: 0 }, + { key: 'exempt_payee_code', value: null, data_type: 'text', required: false, page_number: 0 }, + { + key: 'exemption_from_FATCA', + value: null, + data_type: 'checkbox', + required: true, + page_number: 0, + }, + { key: 'other', value: null, data_type: 'text', required: false, page_number: 0 }, + { key: 'other_text', value: null, data_type: 'text', required: false, page_number: 0 }, + { key: 'foreign_partners', value: null, data_type: 'checkbox', required: true, page_number: 0 }, + { + key: 'home_address_street_1', + value: '525 7th street', + data_type: 'home_address_street_1', + required: true, + page_number: 0, + }, + { + key: 'home_address_street_2', + value: '', + data_type: 'home_address_street_2', + required: true, + page_number: 0, + }, + { + key: 'home_address_city', + value: 'New York', + data_type: 'home_address_city', + required: true, + page_number: 0, + }, + { + key: 'home_address_state', + value: 'NY', + data_type: 'home_address_state', + required: true, + page_number: 0, + }, + { + key: 'home_address_zip', + value: '10022', + data_type: 'home_address_zip', + required: true, + page_number: 0, + }, + { + key: 'company_name', + value: 'Entercross Systems', + data_type: 'company_name', + required: true, + page_number: 0, + }, + { key: 'account_number', value: null, data_type: 'text', required: false, page_number: 0 }, + { key: 'ssn', value: 'XXX-XX-3123', data_type: 'ssn', required: true, page_number: 0 }, + { key: 'ein', value: 'N/A', data_type: 'ein', required: true, page_number: 0 }, + { key: 'signature_text', value: null, data_type: 'signature', required: true, page_number: 0 }, + { key: 'date', value: '9/17/2025', data_type: 'date', required: true, page_number: 0 }, +] + +export const W9_DOCUMENT_UUID = 'w9-document-uuid' +export const SIGNED_DOCUMENT_UUID = 'signed-document-uuid' + +/** A signable W-9 document (unsigned), including pages and fields. */ +export function buildW9Document(overrides: Record = {}) { + return { + uuid: W9_DOCUMENT_UUID, + title: 'W-9', + name: 'taxpayer_identification_form_w_9', + recipient_type: 'Contractor', + requires_signing: true, + signed_at: null, + description: 'Request for Taxpayer Identification Number and Certification', + pages: [{ page_number: 0, image_url: 'https://gusto-test.docs/page-0.png' }], + fields: w9DocumentFields, + ...overrides, + } +} + +/** The contractor documents list response (raw array). */ +export function buildContractorDocumentsList(overrides?: Array>) { + return ( + overrides ?? [ + { + uuid: W9_DOCUMENT_UUID, + title: 'W-9', + name: 'taxpayer_identification_form_w_9', + requires_signing: true, + signed_at: null, + }, + { + uuid: SIGNED_DOCUMENT_UUID, + title: 'Contractor handbook', + name: 'contractor_handbook', + requires_signing: false, + signed_at: '2025-01-01T00:00:00Z', + }, + ] + ) +} + +export function handleGetContractorDocuments(resolver: HttpResponseResolver) { + return http.get(`${API_BASE_URL}/v1/contractors/:contractor_uuid/documents`, resolver) +} + +export function handleGetContractorDocument(resolver: HttpResponseResolver) { + return http.get(`${API_BASE_URL}/v1/documents/:document_uuid`, resolver) +} + +export function handleGetContractorDocumentPdf(resolver: HttpResponseResolver) { + return http.get(`${API_BASE_URL}/v1/documents/:document_uuid/pdf`, resolver) +} + +export function handleSignContractorDocument(resolver: HttpResponseResolver) { + return http.put(`${API_BASE_URL}/v1/documents/:document_uuid/sign`, resolver) +} + +export const getContractorDocuments = handleGetContractorDocuments(() => + HttpResponse.json(buildContractorDocumentsList()), +) + +export const getContractorDocument = handleGetContractorDocument(() => + HttpResponse.json(buildW9Document()), +) + +export const getContractorDocumentPdf = handleGetContractorDocumentPdf(({ params }) => + HttpResponse.json({ + uuid: params.document_uuid, + document_url: 'https://gusto-test.com/docs/w9.pdf', + }), +) + +export const signContractorDocument = handleSignContractorDocument(({ params }) => + HttpResponse.json({ + uuid: params.document_uuid, + title: 'W-9', + name: 'taxpayer_identification_form_w_9', + requires_signing: false, + signed_at: new Date().toISOString(), + }), +) + +export default [ + getContractorDocuments, + getContractorDocument, + getContractorDocumentPdf, + signContractorDocument, +] diff --git a/src/test/mocks/handlers.ts b/src/test/mocks/handlers.ts index 1a4da2435..c6a3156c9 100644 --- a/src/test/mocks/handlers.ts +++ b/src/test/mocks/handlers.ts @@ -20,6 +20,7 @@ import ContractorPaymentMethodHandlers from './apis/contractor_payment_method' import ContractorNewHireReportHandlers from './apis/contractor_new_hire_report' import contractorAddressHandlers from './apis/contractor_address' import ContractorHandlers from './apis/contractors' +import ContractorDocumentsHandlers from './apis/contractor_documents' import ContractorPaymentGroupsHandlers from './apis/contractor_payment_groups' import WireInRequestsHandlers from './apis/wire_in_requests' import InformationRequestsHandlers from './apis/information_requests' @@ -68,6 +69,7 @@ export const handlers = [ ...ContractorNewHireReportHandlers, ...contractorAddressHandlers, ...ContractorHandlers, + ...ContractorDocumentsHandlers, ...ContractorPaymentGroupsHandlers, ...WireInRequestsHandlers, ...InformationRequestsHandlers, diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts index 34cf1f473..fcc75e6c3 100644 --- a/src/types/i18next.d.ts +++ b/src/types/i18next.d.ts @@ -933,6 +933,19 @@ export interface ContractorContractorList{ "continueCta":string; "progressBarCta":string; }; +export interface ContractorDocumentsList{ +"title":string; +"subtitle":string; +"documentListLabel":string; +"documentColumnLabel":string; +"statusColumnLabel":string; +"signDocumentCta":string; +"notSigned":string; +"signed":string; +"emptyTitle":string; +"errorTitle":string; +"continueCta":string; +}; export interface ContractorLanding{ "landingSubtitle":string; "landingDescription":string; @@ -1302,6 +1315,119 @@ export interface ContractorProfile{ "updating":string; }; }; +export interface ContractorSignatureForm{ +"signatureRequired":string; +"instructions":string; +"downloadPrompt":string; +"viewDocumentCta":string; +"sections":{ +"exemptions":string; +"address":string; +"tin":string; +"certification":string; +}; +"sectionInstructions":{ +"exemptions":string; +"address":string; +"tin":string; +"tinSecondary":string; +"certification":string; +}; +"certificationIntro":string; +"certificationPoints":{ +"0":string; +"1":string; +"2":string; +"3":string; +}; +"fields":{ +"name":{ +"label":string; +"description":string; +}; +"business_name":{ +"label":string; +"description":string; +}; +"taxClassification":{ +"label":string; +"description":string; +}; +"llcClassificationCode":{ +"label":string; +"description":string; +}; +"other_text":{ +"label":string; +}; +"foreign_partners":{ +"label":string; +"description":string; +}; +"exempt_payee_code":{ +"label":string; +}; +"exemption_from_FATCA":{ +"label":string; +"description":string; +}; +"home_address_street_1":{ +"label":string; +}; +"home_address_street_2":{ +"label":string; +}; +"home_address_city":{ +"label":string; +}; +"home_address_state":{ +"label":string; +}; +"home_address_zip":{ +"label":string; +}; +"account_number":{ +"label":string; +}; +"company_name":{ +"label":string; +}; +"ssn":{ +"label":string; +}; +"ein":{ +"label":string; +}; +"signature_text":{ +"label":string; +}; +}; +"options":{ +"taxClassification":{ +"individual_proprietor":string; +"c_corporation":string; +"s_corporation":string; +"partnership":string; +"trust_estate":string; +"limited_liability_company":string; +"other":string; +}; +"llcClassificationCode":{ +"placeholder":string; +"c":string; +"s":string; +"p":string; +}; +}; +"agreeLabel":string; +"validation":{ +"required":string; +"agreeRequired":string; +}; +"backCta":string; +"signCta":string; +"acknowledgeCta":string; +}; export interface ContractorSubmit{ "heading":string; "doneTitle":string; @@ -4048,6 +4174,6 @@ export interface common{ interface CustomTypeOptions { defaultNS: 'common'; - resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.Landing': ContractorLanding, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.BankFormBody': EmployeeBankFormBody, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DeductionsForm': EmployeeDeductionsForm, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.FederalTaxesView': EmployeeFederalTaxesView, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.Compensation': EmployeeManagementCompensation, 'Employee.Management.Deductions': EmployeeManagementDeductions, 'Employee.Management.Documents': EmployeeManagementDocuments, 'Employee.Management.FederalTaxes': EmployeeManagementFederalTaxes, 'Employee.Management.HomeAddress': EmployeeManagementHomeAddress, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.Paystubs': EmployeeManagementPaystubs, 'Employee.Management.Profile': EmployeeManagementProfile, 'Employee.Management.StateTaxes': EmployeeManagementStateTaxes, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.SplitPaymentsFormBody': EmployeeSplitPaymentsFormBody, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.StateTaxesView': EmployeeStateTaxesView, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } + resources: { 'Company.Addresses': CompanyAddresses, 'Company.AssignSignatory': CompanyAssignSignatory, 'Company.BankAccount': CompanyBankAccount, 'Company.DocumentList': CompanyDocumentList, 'Company.FederalTaxes': CompanyFederalTaxes, 'Company.Industry': CompanyIndustry, 'Company.Locations': CompanyLocations, 'Company.OnboardingOverview': CompanyOnboardingOverview, 'Company.PaySchedule': CompanyPaySchedule, 'Company.SignatureForm': CompanySignatureForm, 'Company.StateTaxes': CompanyStateTaxes, 'Company.TimeOff.CreateTimeOffPolicy': CompanyTimeOffCreateTimeOffPolicy, 'Company.TimeOff.EmployeeTable': CompanyTimeOffEmployeeTable, 'Company.TimeOff.HolidayPolicy': CompanyTimeOffHolidayPolicy, 'Company.TimeOff.PolicyDetail': CompanyTimeOffPolicyDetail, 'Company.TimeOff.SelectEmployees': CompanyTimeOffSelectEmployees, 'Company.TimeOff.SelectPolicyType': CompanyTimeOffSelectPolicyType, 'Company.TimeOff.TimeOffPolicies': CompanyTimeOffTimeOffPolicies, 'Company.TimeOff.TimeOffPolicyDetails': CompanyTimeOffTimeOffPolicyDetails, 'Company.TimeOff.TimeOffRequests': CompanyTimeOffTimeOffRequests, 'Contractor.Address': ContractorAddress, 'Contractor.ContractorList': ContractorContractorList, 'Contractor.DocumentsList': ContractorDocumentsList, 'Contractor.Landing': ContractorLanding, 'Contractor.NewHireReport': ContractorNewHireReport, 'Contractor.PaymentMethod': ContractorPaymentMethod, 'Contractor.Payments.CreatePayment': ContractorPaymentsCreatePayment, 'Contractor.Payments.PaymentHistory': ContractorPaymentsPaymentHistory, 'Contractor.Payments.PaymentStatement': ContractorPaymentsPaymentStatement, 'Contractor.Payments.PaymentSummary': ContractorPaymentsPaymentSummary, 'Contractor.Payments.PaymentsList': ContractorPaymentsPaymentsList, 'Contractor.Profile': ContractorProfile, 'Contractor.SignatureForm': ContractorSignatureForm, 'Contractor.Submit': ContractorSubmit, 'Employee.BankAccount': EmployeeBankAccount, 'Employee.BankFormBody': EmployeeBankFormBody, 'Employee.Compensation': EmployeeCompensation, 'Employee.Dashboard': EmployeeDashboard, 'Employee.Deductions': EmployeeDeductions, 'Employee.DeductionsForm': EmployeeDeductionsForm, 'Employee.DocumentManager': EmployeeDocumentManager, 'Employee.DocumentSigner': EmployeeDocumentSigner, 'Employee.EmployeeDocuments': EmployeeEmployeeDocuments, 'Employee.EmployeeList': EmployeeEmployeeList, 'Employee.EmploymentEligibility': EmployeeEmploymentEligibility, 'Employee.FederalTaxes': EmployeeFederalTaxes, 'Employee.FederalTaxesView': EmployeeFederalTaxesView, 'Employee.HomeAddress': EmployeeHomeAddress, 'Employee.I9SignatureForm': EmployeeI9SignatureForm, 'Employee.Landing': EmployeeLanding, 'Employee.Management.Compensation': EmployeeManagementCompensation, 'Employee.Management.Deductions': EmployeeManagementDeductions, 'Employee.Management.Documents': EmployeeManagementDocuments, 'Employee.Management.FederalTaxes': EmployeeManagementFederalTaxes, 'Employee.Management.HomeAddress': EmployeeManagementHomeAddress, 'Employee.Management.PaymentMethod': EmployeeManagementPaymentMethod, 'Employee.Management.PaymentMethodBankForm': EmployeeManagementPaymentMethodBankForm, 'Employee.Management.PaymentMethodSplitForm': EmployeeManagementPaymentMethodSplitForm, 'Employee.Management.Paystubs': EmployeeManagementPaystubs, 'Employee.Management.Profile': EmployeeManagementProfile, 'Employee.Management.StateTaxes': EmployeeManagementStateTaxes, 'Employee.Management.WorkAddress': EmployeeManagementWorkAddress, 'Employee.ManagementEmployeeList': EmployeeManagementEmployeeList, 'Employee.OnboardingSummary': EmployeeOnboardingSummary, 'Employee.PaySchedules': EmployeePaySchedules, 'Employee.PaymentMethod': EmployeePaymentMethod, 'Employee.Profile': EmployeeProfile, 'Employee.SplitPaycheck': EmployeeSplitPaycheck, 'Employee.SplitPaymentsFormBody': EmployeeSplitPaymentsFormBody, 'Employee.StateTaxes': EmployeeStateTaxes, 'Employee.StateTaxesView': EmployeeStateTaxesView, 'Employee.Terminations.TerminateEmployee': EmployeeTerminationsTerminateEmployee, 'Employee.Terminations.TerminationFlow': EmployeeTerminationsTerminationFlow, 'Employee.Terminations.TerminationSummary': EmployeeTerminationsTerminationSummary, 'InformationRequests.InformationRequestForm': InformationRequestsInformationRequestForm, 'InformationRequests.InformationRequestList': InformationRequestsInformationRequestList, 'InformationRequests': InformationRequests, 'Payroll.Common': PayrollCommon, 'Payroll.ConfirmWireDetailsBanner': PayrollConfirmWireDetailsBanner, 'Payroll.ConfirmWireDetailsForm': PayrollConfirmWireDetailsForm, 'Payroll.Dismissal': PayrollDismissal, 'Payroll.EmployeeSelection': PayrollEmployeeSelection, 'Payroll.GrossUpModal': PayrollGrossUpModal, 'Payroll.OffCycle': PayrollOffCycle, 'Payroll.OffCycleCreation': PayrollOffCycleCreation, 'Payroll.OffCycleDeductionsSetting': PayrollOffCycleDeductionsSetting, 'Payroll.OffCyclePayPeriodDateForm': PayrollOffCyclePayPeriodDateForm, 'Payroll.OffCycleReasonSelection': PayrollOffCycleReasonSelection, 'Payroll.OffCycleTaxWithholding': PayrollOffCycleTaxWithholding, 'Payroll.PayrollBlocker': PayrollPayrollBlocker, 'Payroll.PayrollConfiguration': PayrollPayrollConfiguration, 'Payroll.PayrollEditEmployee': PayrollPayrollEditEmployee, 'Payroll.PayrollFlow': PayrollPayrollFlow, 'Payroll.PayrollHistory': PayrollPayrollHistory, 'Payroll.PayrollLanding': PayrollPayrollLanding, 'Payroll.PayrollList': PayrollPayrollList, 'Payroll.PayrollOverview': PayrollPayrollOverview, 'Payroll.PayrollReceipts': PayrollPayrollReceipts, 'Payroll.RecoveryCasesList': PayrollRecoveryCasesList, 'Payroll.RecoveryCasesResubmit': PayrollRecoveryCasesResubmit, 'Payroll.Transition': PayrollTransition, 'Payroll.TransitionCreation': PayrollTransitionCreation, 'Payroll.TransitionPayrollAlert': PayrollTransitionPayrollAlert, 'Payroll.WireInstructions': PayrollWireInstructions, 'common': common, } }; } \ No newline at end of file From 47a44ad2df949d2032f2caf32897811da3d3be06 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Jun 2026 21:21:28 +0000 Subject: [PATCH 2/2] chore: update derived files --- .reports/embedded-react-sdk.api.md | 192 ++++++- docs/appendix/endpoint-inventory.json | 54 ++ docs/appendix/endpoint-reference.md | 8 + docs/reference/component-inventory.md | 2 +- docs/reference/contractor/hooks/index.mdx | 2 +- .../hooks/use-contractor-documents-list.md | 94 ++++ .../hooks/use-contractor-signature-form.md | 518 ++++++++++++++++++ docs/reference/contractor/index.mdx | 4 +- .../reference/contractor/onboarding/blocks.md | 101 ++++ docs/reference/contractor/onboarding/index.md | 3 + docs/reference/events.md | 3 + docs/reference/index.md | 9 +- docs/reference/utilities.md | 3 + 13 files changed, 984 insertions(+), 9 deletions(-) create mode 100644 docs/reference/contractor/hooks/use-contractor-documents-list.md create mode 100644 docs/reference/contractor/hooks/use-contractor-signature-form.md diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md index a61c09b99..b2d2f80f8 100644 --- a/.reports/embedded-react-sdk.api.md +++ b/.reports/embedded-react-sdk.api.md @@ -25,6 +25,8 @@ import { ContractorType as ContractorType_2 } from '@gusto/embedded-api-v-2025-1 import { Control } from 'react-hook-form'; import { CustomTypeOptions } from 'i18next'; import { default as default_2 } from 'react'; +import { Document as Document_2 } from '@gusto/embedded-api-v-2025-11-15/models/components/document'; +import { DocumentSigned } from '@gusto/embedded-api-v-2025-11-15/models/components/documentsigned'; import { Employee } from '@gusto/embedded-api-v-2025-11-15/models/components/employee'; import { EmployeeAddress } from '@gusto/embedded-api-v-2025-11-15/models/components/employeeaddress'; import { EmployeeBankAccount } from '@gusto/embedded-api-v-2025-11-15/models/components/employeebankaccount'; @@ -147,6 +149,9 @@ export { AfterSuccessContext } export { AfterSuccessHook } +// @public +export const AGREE_FIELD = "agree"; + // @public export interface AlertProps { action?: ReactNode; @@ -376,6 +381,15 @@ export interface BreadcrumbsProps { onClick?: (id: string) => void; } +// @public +export function buildContractorSignatureFields(descriptors: W9FieldDescriptor[]): ContractorSignatureFields; + +// @public +export function buildW9Defaults(document: Document_2, descriptors: W9FieldDescriptor[]): ContractorSignatureFormData; + +// @public +export function buildW9FieldDescriptors(document: Document_2): W9FieldDescriptor[]; + // @public export interface ButtonIconProps extends ButtonProps { 'aria-label': string; @@ -895,6 +909,9 @@ export const componentEvents: { readonly CONTRACTOR_ONBOARDING_STATUS_UPDATED: "contractor/onboardingStatus/updated"; readonly CONTRACTOR_INVITE_CONTRACTOR: "contractor/invite/selfOnboarding"; readonly CONTRACTOR_ONBOARDING_CONTINUE: "contractor/onboarding/continue"; + readonly CONTRACTOR_VIEW_DOCUMENT_TO_SIGN: "contractor/documents/view"; + readonly CONTRACTOR_SIGN_DOCUMENT: "contractor/documents/sign"; + readonly CONTRACTOR_DOCUMENTS_DONE: "contractor/documents/done"; readonly PAY_SCHEDULE_CREATE: "paySchedule/create"; readonly PAY_SCHEDULE_CREATED: "paySchedule/created"; readonly PAY_SCHEDULE_UPDATE: "paySchedule/update"; @@ -1283,6 +1300,14 @@ export interface ContractorDetailsSubmitOptions { companyId?: string; } +// @public +function ContractorDocumentSigner(props: ContractorDocumentSignerProps): JSX; + +// @public +interface ContractorDocumentSignerProps extends BaseComponentInterface { + contractorId: string; +} + // @public export function ContractorEinField(props: ContractorEinFieldProps): JSX; @@ -1371,7 +1396,13 @@ declare namespace ContractorOnboarding { NewHireReport, NewHireReportProps, ContractorSubmit, - ContractorSubmitProps + ContractorSubmitProps, + ContractorDocumentSigner, + ContractorDocumentSignerProps, + DocumentsList, + DocumentsListProps, + SignatureForm_3 as SignatureForm, + SignatureFormProps_3 as SignatureFormProps } } @@ -1405,6 +1436,41 @@ export type ContractorSelfOnboardingFieldProps = HookFieldProps; +// @public +export type ContractorSignatureBoundField = ComponentType; + +// @public +export interface ContractorSignatureFieldProps { + description?: ReactNode; + getOptionLabel?: (value: string) => string; + label: string; + placeholder?: string; + validationMessages?: ValidationMessages; +} + +// @public +export type ContractorSignatureFields = Record; + +// @public +export type ContractorSignatureFormData = Record & { + agree: boolean; +}; + +// @public +export type ContractorSignatureFormErrorCode = (typeof ContractorSignatureFormErrorCodes)[keyof typeof ContractorSignatureFormErrorCodes]; + +// @public +export const ContractorSignatureFormErrorCodes: { + readonly REQUIRED: "REQUIRED"; + readonly AGREE_REQUIRED: "AGREE_REQUIRED"; +}; + +// @public +export interface ContractorSignatureSection { + fieldNames: string[]; + section: W9Section; +} + // @public export function ContractorSsnField(props: ContractorSsnFieldProps): JSX; @@ -1538,6 +1604,9 @@ state: z.ZodString; zip: z.ZodString; }>; +// @public +export function createContractorSignatureFormSchema(descriptors: W9FieldDescriptor[]): z.ZodType; + // Warning: (ae-forgotten-export) The symbol "DeductionFormSchemaOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-internal-missing-underscore) The name "createDeductionFormSchema" should be prefixed with an underscore because the declaration is marked as @internal // @@ -2099,6 +2168,14 @@ interface DocumentSignerProps_2 extends BaseComponentInterface<'Company.Document signatoryId?: string; } +// @public +function DocumentsList(props: DocumentsListProps): JSX; + +// @public +interface DocumentsListProps extends BaseComponentInterface<'Contractor.DocumentsList'> { + contractorId: string; +} + // @public interface DocumentsProps extends BaseComponentInterface<'Employee.Management.Documents'> { employeeId: string; @@ -2555,6 +2632,7 @@ export interface FieldMetadata { maxDate?: string | null; minDate?: string | null; name: string; + placeholder?: string; } // @public @@ -3020,6 +3098,9 @@ interface InviteSignatoryProps extends BaseComponentInterface<'Company.AssignSig defaultValues?: InviteSignatoryDefaultValues; } +// @public +export function isW9Document(document: Document_2): boolean; + // @public export type JobErrorCode = (typeof JobErrorCodes)[keyof typeof JobErrorCodes]; @@ -3155,6 +3236,15 @@ export interface LinkProps extends Pick, children?: ReactNode; } +// @public +export const LLC_CLASSIFICATION_CODES: readonly ["c", "s", "p"]; + +// @public +export const LLC_CLASSIFICATION_FIELD = "llcClassificationCode"; + +// @public +export const LLC_CLASSIFICATION_OPTION: TaxClassificationOptionKey; + // @public export interface LoadingSpinnerProps extends Pick, 'className' | 'id' | 'aria-label'> { size?: 'lg' | 'sm'; @@ -3557,6 +3647,12 @@ export function OrderNumberField(props: OrderNumberFieldProps): JSX; // @public export type OrderNumberFieldProps = HookFieldProps>; +// @public +export const OTHER_CLASSIFICATION_OPTION: TaxClassificationOptionKey; + +// @public +export const OTHER_TEXT_FIELD = "other_text"; + // @public export function OtherIncomeField(props: OtherIncomeFieldProps): JSX; @@ -4451,6 +4547,11 @@ interface SelfOnboardingFlowProps extends BaseComponentInterface { withEmployeeI9?: boolean; } +// Warning: (ae-forgotten-export) The symbol "SignFieldValue" needs to be exported by the entry point index.d.ts +// +// @public +export function serializeW9Fields(document: Document_2, descriptors: W9FieldDescriptor[], values: ContractorSignatureFormData): SignFieldValue[]; + // @public export interface SharedFieldLayoutProps extends DataAttributes { description?: ReactNode; @@ -4475,6 +4576,9 @@ function SignatureForm(props: SignatureFormProps): JSX; // @public function SignatureForm_2(props: SignatureFormProps_2): JSX; +// @public +function SignatureForm_3(props: SignatureFormProps_3): JSX; + // @public interface SignatureFormProps extends BaseComponentInterface<'Employee.DocumentSigner'> { employeeId: string; @@ -4489,6 +4593,12 @@ interface SignatureFormProps_2 extends BaseComponentInterface<'Company.Signature formId: string; } +// @public +interface SignatureFormProps_3 extends BaseComponentInterface<'Contractor.SignatureForm'> { + contractorId: string; + documentUuid: string; +} + // @public export type SignCompanyFormData = { signature: string; confirmSignature: boolean; }; @@ -4860,6 +4970,15 @@ export interface TabsProps { tabs: TabProps[]; } +// @public +export const TAX_CLASSIFICATION_FIELD = "taxClassification"; + +// @public +export const TAX_CLASSIFICATION_OPTION_KEYS: readonly ["individual_proprietor", "c_corporation", "s_corporation", "partnership", "trust_estate", "limited_liability_company", "other"]; + +// @public +export type TaxClassificationOptionKey = (typeof TAX_CLASSIFICATION_OPTION_KEYS)[number]; + // @public function TerminateEmployee(props: TerminateEmployeeProps): JSX; @@ -5291,6 +5410,54 @@ export type UseContractorDetailsFormSharedProps = { shouldFocusError?: boolean; }; +// @public +export function useContractorDocumentsList(input: UseContractorDocumentsListParams): UseContractorDocumentsListResult; + +// @public +export interface UseContractorDocumentsListParams { + contractorId: string; +} + +// @public +export type UseContractorDocumentsListReady = BaseHookReady<{ + documents: Document_2[]; +}, { + isFetching: boolean; +}>; + +// @public +export type UseContractorDocumentsListResult = HookLoadingResult | UseContractorDocumentsListReady; + +// @public +export function useContractorSignatureForm(input: UseContractorSignatureFormProps): UseContractorSignatureFormResult; + +// @public +export interface UseContractorSignatureFormProps { + documentUuid: string; + shouldFocusError?: boolean; + validationMode?: UseFormProps['mode']; +} + +// @public +export interface UseContractorSignatureFormReady extends BaseFormHookReady { + actions: { + onSubmit: () => Promise | undefined>; + }; + data: { + document: Document_2; + pdfUrl: string | null; + sections: ContractorSignatureSection[]; + hasFields: boolean; + }; + status: { + isPending: boolean; + mode: 'create'; + }; +} + +// @public +export type UseContractorSignatureFormResult = HookLoadingResult | UseContractorSignatureFormReady; + // @public export function useCurrentHomeAddressForm(props: UseCurrentHomeAddressFormProps): UseHomeAddressFormResult; @@ -5778,6 +5945,27 @@ interface ViewHolidayScheduleProps extends BaseComponentInterface<'Company.TimeO companyId: string; } +// @public +export const W9_DOCUMENT_NAME = "taxpayer_identification_form_w_9"; + +// @public +export interface W9FieldDescriptor { + apiKey?: string; + hasRedactedValue?: boolean; + isRequired: boolean; + name: string; + placeholder?: string; + section: W9Section; + variant: W9FieldVariant; + visibleWhenClassification?: TaxClassificationOptionKey; +} + +// @public +export type W9FieldVariant = 'text' | 'checkbox' | 'radio' | 'select'; + +// @public +export type W9Section = 'classification' | 'exemptions' | 'address' | 'tin' | 'certification'; + // @public export const WageType: { readonly Fixed: "Fixed"; @@ -5893,7 +6081,7 @@ export type ZipValidation = (typeof HomeAddressErrorCodes)['REQUIRED' | 'INVALID // Warnings were encountered during analysis: // -// dist/partner-hook-utils/types.d.ts:270:13 - (ae-forgotten-export) The symbol "FieldElementRegistry" needs to be exported by the entry point index.d.ts +// dist/partner-hook-utils/types.d.ts:272:13 - (ae-forgotten-export) The symbol "FieldElementRegistry" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/docs/appendix/endpoint-inventory.json b/docs/appendix/endpoint-inventory.json index 20cdcf314..d2a3101ae 100644 --- a/docs/appendix/endpoint-inventory.json +++ b/docs/appendix/endpoint-inventory.json @@ -1791,6 +1791,60 @@ "contractorUuid" ] }, + "ContractorOnboarding.ContractorDocumentSigner": { + "endpoints": [ + { + "method": "GET", + "path": "/v1/contractors/:contractorUuid/documents" + }, + { + "method": "GET", + "path": "/v1/documents/:documentUuid" + }, + { + "method": "GET", + "path": "/v1/documents/:documentUuid/pdf" + }, + { + "method": "PUT", + "path": "/v1/documents/:documentUuid/sign" + } + ], + "variables": [ + "contractorUuid", + "documentUuid" + ] + }, + "ContractorOnboarding.DocumentsList": { + "endpoints": [ + { + "method": "GET", + "path": "/v1/contractors/:contractorUuid/documents" + } + ], + "variables": [ + "contractorUuid" + ] + }, + "ContractorOnboarding.SignatureForm": { + "endpoints": [ + { + "method": "GET", + "path": "/v1/documents/:documentUuid" + }, + { + "method": "GET", + "path": "/v1/documents/:documentUuid/pdf" + }, + { + "method": "PUT", + "path": "/v1/documents/:documentUuid/sign" + } + ], + "variables": [ + "documentUuid" + ] + }, "ContractorManagement.PaymentsList": { "endpoints": [ { diff --git a/docs/appendix/endpoint-reference.md b/docs/appendix/endpoint-reference.md index 5194611d4..5fc55cb67 100644 --- a/docs/appendix/endpoint-reference.md +++ b/docs/appendix/endpoint-reference.md @@ -328,6 +328,14 @@ import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json' | **ContractorOnboarding.ContractorSubmit** | GET | `/v1/contractors/:contractorUuid` | | | GET | `/v1/contractors/:contractorUuid/onboarding_status` | | | PUT | `/v1/contractors/:contractorUuid/onboarding_status` | +| **ContractorOnboarding.ContractorDocumentSigner** | GET | `/v1/contractors/:contractorUuid/documents` | +| | GET | `/v1/documents/:documentUuid` | +| | GET | `/v1/documents/:documentUuid/pdf` | +| | PUT | `/v1/documents/:documentUuid/sign` | +| **ContractorOnboarding.DocumentsList** | GET | `/v1/contractors/:contractorUuid/documents` | +| **ContractorOnboarding.SignatureForm** | GET | `/v1/documents/:documentUuid` | +| | GET | `/v1/documents/:documentUuid/pdf` | +| | PUT | `/v1/documents/:documentUuid/sign` | ## ContractorManagement components diff --git a/docs/reference/component-inventory.md b/docs/reference/component-inventory.md index be7c5365b..618b849ad 100644 --- a/docs/reference/component-inventory.md +++ b/docs/reference/component-inventory.md @@ -1144,7 +1144,7 @@ Renders body text as `

`, ``, `

`, or `
`, with size, weight, al
 
 | Property | Type | Default value | Description |
 | ------ | ------ | ------ | ------ |
-| `as?` | `"div"` \| `"span"` \| `"p"` \| `"pre"` | `'p'` | HTML element to render the text as |
+| `as?` | `"div"` \| `"p"` \| `"span"` \| `"pre"` | `'p'` | HTML element to render the text as |
 | `children?` | `ReactNode` | | Content to be displayed |
 | `size?` | `"xs"` \| `"sm"` \| `"md"` \| `"lg"` | `'md'` | Size variant of the text |
 | `textAlign?` | `"center"` \| `"start"` \| `"end"` | `undefined` | Text alignment within the container |
diff --git a/docs/reference/contractor/hooks/index.mdx b/docs/reference/contractor/hooks/index.mdx
index 33a6b3305..a06e1b147 100644
--- a/docs/reference/contractor/hooks/index.mdx
+++ b/docs/reference/contractor/hooks/index.mdx
@@ -12,4 +12,4 @@ custom_edit_url: null
 
 # Hooks
 
-
+
diff --git a/docs/reference/contractor/hooks/use-contractor-documents-list.md b/docs/reference/contractor/hooks/use-contractor-documents-list.md
new file mode 100644
index 000000000..293ebc59d
--- /dev/null
+++ b/docs/reference/contractor/hooks/use-contractor-documents-list.md
@@ -0,0 +1,94 @@
+---
+# Autogenerated by TypeDoc from TSDoc comments in the source code.
+# To update content: edit TSDoc comments in src/.
+# To update structure: edit docs-site/typedoc.config.ts or docs-site/plugins/typedoc-custom/.
+# Then run `npm run docs:api:generate` to regenerate.
+title: useContractorDocumentsList
+description: useContractorDocumentsList reference.
+generated_by: typedoc
+custom_edit_url: null
+---
+
+# useContractorDocumentsList
+
+## Hooks
+
+
+
+### useContractorDocumentsList()
+
+> **useContractorDocumentsList**(`params`): [`UseContractorDocumentsListResult`](#usecontractordocumentslistresult)
+
+Standalone data hook for a contractor's documents.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `params` | [`UseContractorDocumentsListParams`](#usecontractordocumentslistparams) | See [UseContractorDocumentsListParams](#usecontractordocumentslistparams). |
+
+#### Returns
+
+[`UseContractorDocumentsListResult`](#usecontractordocumentslistresult)
+
+A [HookLoadingResult](../../utilities.md#hookloadingresult) while loading, or the ready state with `data.documents` once loaded.
+
+#### Remarks
+
+Wraps the `contractorDocumentsGetAll` query in the standard
+[BaseHookReady](../../utilities.md#basehookready) shape. Read-only — viewing or signing a document is
+handled by the screen the parent routes to, so this hook exposes no actions.
+
+#### Example
+
+```tsx
+import { useContractorDocumentsList } from '@gusto/embedded-react-sdk'
+
+function ContractorDocuments({ contractorId }: { contractorId: string }) {
+  const result = useContractorDocumentsList({ contractorId })
+
+  if (result.isLoading) return 
Loading...
+ + return ( +
    + {result.data.documents.map(doc => ( +
  • {doc.title}
  • + ))} +
+ ) +} +``` + +## Interfaces + + + +### UseContractorDocumentsListParams + +Parameters for [useContractorDocumentsList](#usecontractordocumentslist). + +#### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `contractorId` | `string` | The associated contractor identifier. | + +## Type Aliases + + + +### UseContractorDocumentsListReady + +> **UseContractorDocumentsListReady** = [`BaseHookReady`](../../utilities.md#basehookready)\<\{ `documents`: `Document`[]; \}, \{ `isFetching`: `boolean`; \}\> + +Ready-state shape returned by [useContractorDocumentsList](#usecontractordocumentslist) once the documents have loaded. + +*** + + + +### UseContractorDocumentsListResult + +> **UseContractorDocumentsListResult** = [`HookLoadingResult`](../../utilities.md#hookloadingresult) \| [`UseContractorDocumentsListReady`](#usecontractordocumentslistready) + +Result of [useContractorDocumentsList](#usecontractordocumentslist) — a discriminated union of loading and ready states. diff --git a/docs/reference/contractor/hooks/use-contractor-signature-form.md b/docs/reference/contractor/hooks/use-contractor-signature-form.md new file mode 100644 index 000000000..2bcfae06d --- /dev/null +++ b/docs/reference/contractor/hooks/use-contractor-signature-form.md @@ -0,0 +1,518 @@ +--- +# Autogenerated by TypeDoc from TSDoc comments in the source code. +# To update content: edit TSDoc comments in src/. +# To update structure: edit docs-site/typedoc.config.ts or docs-site/plugins/typedoc-custom/. +# Then run `npm run docs:api:generate` to regenerate. +title: useContractorSignatureForm +description: useContractorSignatureForm reference. +generated_by: typedoc +custom_edit_url: null +--- + +# useContractorSignatureForm + +## Form Hooks + + + +### useContractorSignatureForm() + +> **useContractorSignatureForm**(`props`): [`UseContractorSignatureFormResult`](#usecontractorsignatureformresult) + +Headless hook for signing a contractor document — displays the document PDF +and collects the document's fields plus a typed signature and consent. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `props` | [`UseContractorSignatureFormProps`](#usecontractorsignatureformprops) | See [UseContractorSignatureFormProps](#usecontractorsignatureformprops). | + +#### Returns + +[`UseContractorSignatureFormResult`](#usecontractorsignatureformresult) + +A [HookLoadingResult](../../utilities.md#hookloadingresult) while loading, or a [UseContractorSignatureFormReady](#usecontractorsignatureformready) once loaded. + +#### Remarks + +The fields are driven by the document returned from the API. Today the only +signable contractor document is the W-9, whose federal tax classification +checkboxes are presented as a single required radio group with conditional +LLC-code and "Other" sub-fields; on submit the selection is mapped back to +the W-9 wire format. Pre-filled values (name, address, TIN, etc.) are +editable inputs. `data.sections` describes how to group `form.Fields` under +headings; consult `form.fieldsMetadata` for per-field required flags and +select/radio options. + +## Functions + + + +### buildContractorSignatureFields() + +> **buildContractorSignatureFields**(`descriptors`): [`ContractorSignatureFields`](#contractorsignaturefields) + +Builds the map of bound field components for a W-9 signing form, including +the always-present `agree` consent checkbox. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `descriptors` | [`W9FieldDescriptor`](#w9fielddescriptor)[] | The descriptors produced by `buildW9FieldDescriptors`. | + +#### Returns + +[`ContractorSignatureFields`](#contractorsignaturefields) + +A [ContractorSignatureFields](#contractorsignaturefields) map keyed by form-field name. + +*** + + + +### buildW9Defaults() + +> **buildW9Defaults**(`document`, `descriptors`): [`ContractorSignatureFormData`](#contractorsignatureformdata) + +Builds default form values from a document's fields. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `document` | `Document` | The W-9 document returned by the API. | +| `descriptors` | [`W9FieldDescriptor`](#w9fielddescriptor)[] | The descriptors produced by [buildW9FieldDescriptors](#buildw9fielddescriptors). | + +#### Returns + +[`ContractorSignatureFormData`](#contractorsignatureformdata) + +The default form values. + +#### Remarks + +Pass-through values are seeded from the API `value`. The classification radio +defaults to whichever classification checkbox is already set (`'1'`). + +*** + + + +### buildW9FieldDescriptors() + +> **buildW9FieldDescriptors**(`document`): [`W9FieldDescriptor`](#w9fielddescriptor)[] + +Builds the ordered, render-ready W-9 field descriptors for a document. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `document` | `Document` | The W-9 document returned by the API. | + +#### Returns + +[`W9FieldDescriptor`](#w9fielddescriptor)[] + +The ordered list of field descriptors to render. + +#### Remarks + +Pass-through fields are included only when present on the document; their +`isRequired` flag is driven by the API `required` flag unless the W-9 layout +marks them optional. The classification radio is included when any of the +classification checkbox fields are present. + +*** + + + +### createContractorSignatureFormSchema() + +> **createContractorSignatureFormSchema**(`descriptors`): `ZodType`\<[`ContractorSignatureFormData`](#contractorsignatureformdata), [`ContractorSignatureFormData`](#contractorsignatureformdata)\> + +Builds a Zod schema for the W-9 signing form from its field descriptors. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `descriptors` | [`W9FieldDescriptor`](#w9fielddescriptor)[] | The descriptors produced by `buildW9FieldDescriptors`. | + +#### Returns + +`ZodType`\<[`ContractorSignatureFormData`](#contractorsignatureformdata), [`ContractorSignatureFormData`](#contractorsignatureformdata)\> + +A Zod schema typed to [ContractorSignatureFormData](#contractorsignatureformdata). + +#### Remarks + +Every field validates as `z.unknown()` with emptiness enforced in +`superRefine`, mirroring the dynamic state-taxes form pattern. The LLC +classification code is required only while the LLC classification is +selected, and `agree` must be checked. + +*** + + + +### isW9Document() + +> **isW9Document**(`document`): `boolean` + +Whether the document is a W-9 with signable fields. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `document` | `Document` | + +#### Returns + +`boolean` + +*** + + + +### serializeW9Fields() + +> **serializeW9Fields**(`document`, `descriptors`, `values`): `SignFieldValue`[] + +Serializes W-9 form values into the `{ key, value }` field array expected by +the sign API. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `document` | `Document` | The W-9 document being signed. | +| `descriptors` | [`W9FieldDescriptor`](#w9fielddescriptor)[] | The descriptors produced by [buildW9FieldDescriptors](#buildw9fielddescriptors). | +| `values` | [`ContractorSignatureFormData`](#contractorsignatureformdata) | The validated form values. | + +#### Returns + +`SignFieldValue`[] + +The ordered field array for the sign request body. + +#### Remarks + +Applies the W-9 mapping: the chosen classification key is sent with value +`'1'`; when the LLC classification is chosen, the selected LLC code is sent +under the `tax_classification` key. Checkboxes serialize to `'1'`/`'0'`. The +`other_text` and `llcClassificationCode` values are only included for their +respective classifications. The `date` field, when present on the document, +is set to today's date. + +## Variables + + + +### AGREE\_FIELD + +> `const` **AGREE\_FIELD**: `"agree"` = `'agree'` + +The form-field name of the electronic-signature consent checkbox. + +*** + + + +### ContractorSignatureFormErrorCodes + +> `const` **ContractorSignatureFormErrorCodes**: `object` + +Validation error codes produced by the contractor signature form schema. + +#### Type Declaration + +| Name | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `AGREE_REQUIRED` | `"AGREE_REQUIRED"` | `'AGREE_REQUIRED'` | The electronic-signature consent checkbox was not checked. | +| `REQUIRED` | `"REQUIRED"` | `'REQUIRED'` | A required field was left empty. | + +#### Remarks + +Use these constants as the keys in a field's `validationMessages` prop to map +an error code to a user-facing message. + +*** + + + +### LLC\_CLASSIFICATION\_CODES + +> `const` **LLC\_CLASSIFICATION\_CODES**: readonly \[`"c"`, `"s"`, `"p"`\] + +Ordered LLC tax classification code options. + +*** + + + +### LLC\_CLASSIFICATION\_FIELD + +> `const` **LLC\_CLASSIFICATION\_FIELD**: `"llcClassificationCode"` = `'llcClassificationCode'` + +Form-field name for the synthesized LLC tax classification code select. + +*** + + + +### LLC\_CLASSIFICATION\_OPTION + +> `const` **LLC\_CLASSIFICATION\_OPTION**: [`TaxClassificationOptionKey`](#taxclassificationoptionkey) = `'limited_liability_company'` + +The classification option that reveals the LLC tax classification select. + +*** + + + +### OTHER\_CLASSIFICATION\_OPTION + +> `const` **OTHER\_CLASSIFICATION\_OPTION**: [`TaxClassificationOptionKey`](#taxclassificationoptionkey) = `'other'` + +The classification option that reveals the "Other" free-text field. + +*** + + + +### OTHER\_TEXT\_FIELD + +> `const` **OTHER\_TEXT\_FIELD**: `"other_text"` = `'other_text'` + +The W-9 `other_text` API field key, revealed when "Other" is selected. + +*** + + + +### TAX\_CLASSIFICATION\_FIELD + +> `const` **TAX\_CLASSIFICATION\_FIELD**: `"taxClassification"` = `'taxClassification'` + +Form-field name for the synthesized federal tax classification radio group. + +*** + + + +### TAX\_CLASSIFICATION\_OPTION\_KEYS + +> `const` **TAX\_CLASSIFICATION\_OPTION\_KEYS**: readonly \[`"individual_proprietor"`, `"c_corporation"`, `"s_corporation"`, `"partnership"`, `"trust_estate"`, `"limited_liability_company"`, `"other"`\] + +Ordered classification option keys backing the [TAX\_CLASSIFICATION\_FIELD](#tax_classification_field) +radio group. Each maps to a W-9 checkbox field on the underlying document. + +*** + + + +### W9\_DOCUMENT\_NAME + +> `const` **W9\_DOCUMENT\_NAME**: `"taxpayer_identification_form_w_9"` = `'taxpayer_identification_form_w_9'` + +The `name` of the W-9 document — the only contractor document type that +supports signing today. + +## Interfaces + + + +### ContractorSignatureFieldProps + +Props accepted by a W-9 field component exposed on +`useContractorSignatureForm`'s `form.Fields`. + +#### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `label` | `string` | Visible label rendered above the field. | +| `description?` | `ReactNode` | Optional helper text rendered below the field. | +| `getOptionLabel?` | (`value`) => `string` | Maps an option value to its display label; used by radio and select fields. | +| `placeholder?` | `string` | Placeholder text; used by text and select fields. | +| `validationMessages?` | [`ValidationMessages`](../../utilities.md#validationmessages)\<[`ContractorSignatureFormErrorCode`](#contractorsignatureformerrorcode)\> | Custom error text keyed by validation error code. | + +*** + + + +### ContractorSignatureSection + +A section of the W-9 signing form along with the form-field names to render +within it, in order. + +#### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `fieldNames` | `string`[] | Form-field names to render in this section, in order. | +| `section` | [`W9Section`](#w9section) | The section identifier (e.g. `'address'`, `'tin'`). | + +*** + + + +### UseContractorSignatureFormProps + +Props for [useContractorSignatureForm](#usecontractorsignatureform). + +#### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `documentUuid` | `string` | UUID of the contractor document to sign. | +| `shouldFocusError?` | `boolean` | Auto-focus the first invalid field on submit. Defaults to `true`; set to `false` when using `composeSubmitHandler`. | +| `validationMode?` | `"onChange"` \| `"onBlur"` \| `"onSubmit"` \| `"onTouched"` \| `"all"` | When validation runs. Passed through to react-hook-form; defaults to `'onSubmit'`. | + +*** + + + +### UseContractorSignatureFormReady + +Ready-state shape returned by [useContractorSignatureForm](#usecontractorsignatureform) once the +document metadata has loaded. + +#### Extends + +- [`BaseFormHookReady`](../../utilities.md#baseformhookready)\<[`FieldsMetadata`](../../utilities.md#fieldsmetadata), [`ContractorSignatureFormData`](#contractorsignatureformdata), [`ContractorSignatureFields`](#contractorsignaturefields)\> + +#### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `actions` | `object` | Imperative actions exposed by the hook. | +| `actions.onSubmit` | () => `Promise`\<[`HookSubmitResult`](../../utilities.md#hooksubmitresult)\<`DocumentSigned`\> \| `undefined`\> | Validates the form and submits the signature. Resolves with the signed document on success, or `undefined` on validation or API failure. | +| `data` | `object` | Loaded data — the document being signed and a preview PDF URL. | +| `data.document` | `Document` | The document entity fetched from the API. | +| `data.hasFields` | `boolean` | Whether the document carries signable fields (vs. acknowledge-only). | +| `data.pdfUrl` | `string` \| `null` | URL to the document's PDF, or `null` when unavailable. | +| `data.sections` | [`ContractorSignatureSection`](#contractorsignaturesection)[] | Ordered sections describing how to group fields when rendering. | +| `errorHandling` | [`HookErrorHandling`](../../utilities.md#hookerrorhandling) | Error state and recovery actions. | +| `form` | `object` | Form bindings: pre-bound field components, per-field metadata, submission values, and react-hook-form internals. | +| `form.Fields` | `TFields` | - | +| `form.fieldsMetadata` | [`FieldsMetadata`](../../utilities.md#fieldsmetadata) | - | +| `form.getFormSubmissionValues` | () => `Record`\<`string`, `unknown`\> \| `undefined` | - | +| `form.hookFormInternals` | [`HookFormInternals`](../../utilities.md#hookforminternals)\<[`ContractorSignatureFormData`](#contractorsignatureformdata)\> | - | +| `isLoading` | `false` | Always `false` in this branch; discriminates from [HookLoadingResult](../../utilities.md#hookloadingresult). | +| `status` | `object` | Submit-state flags. | +| `status.isPending` | `boolean` | `true` while the sign mutation is in flight. | +| `status.mode` | `"create"` | Always `'create'`; the hook always submits as a signing operation. | + +*** + + + +### W9FieldDescriptor + +A render-ready descriptor for a single W-9 form field. + +#### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `isRequired` | `boolean` | Whether the field must have a value for the form to submit. | +| `name` | `string` | react-hook-form field name (also the i18n label sub-key). | +| `section` | [`W9Section`](#w9section) | Section this field is grouped under. | +| `variant` | [`W9FieldVariant`](#w9fieldvariant) | Input variant used to pick the bound field component. | +| `apiKey?` | `string` | Underlying API field key for pass-through fields; absent for synthesized fields. | +| `hasRedactedValue?` | `boolean` | Whether the API returned a masked value for this field (e.g. a redacted SSN/EIN). Redacted fields seed an empty input, surface the mask as a placeholder, are exempt from required validation, and are omitted from the sign payload unless the contractor types a replacement. | +| `placeholder?` | `string` | The masked value to display as a placeholder for a redacted field. | +| `visibleWhenClassification?` | `"other"` \| `"individual_proprietor"` \| `"c_corporation"` \| `"s_corporation"` \| `"partnership"` \| `"trust_estate"` \| `"limited_liability_company"` | When set, the field only renders while the classification radio holds this option. | + +## Type Aliases + + + +### ContractorSignatureBoundField + +> **ContractorSignatureBoundField** = `ComponentType`\<[`ContractorSignatureFieldProps`](#contractorsignaturefieldprops)\> + +A W-9 field component pre-bound to its form-field name. + +*** + + + +### ContractorSignatureFields + +> **ContractorSignatureFields** = `Record`\<`string`, [`ContractorSignatureBoundField`](#contractorsignatureboundfield)\> + +Map of W-9 form-field name to its bound field component. + +*** + + + +### ContractorSignatureFormData + +> **ContractorSignatureFormData** = `Record`\<`string`, `string` \| `boolean`\> & `object` + +The shape of values managed by the W-9 signing form. + +#### Type Declaration + +| Name | Type | Description | +| ------ | ------ | ------ | +| `agree` | `boolean` | Electronic-signature consent; must be checked to submit. | + +*** + + + +### ContractorSignatureFormErrorCode + +> **ContractorSignatureFormErrorCode** = *typeof* [`ContractorSignatureFormErrorCodes`](#contractorsignatureformerrorcodes)\[keyof *typeof* [`ContractorSignatureFormErrorCodes`](#contractorsignatureformerrorcodes)\] + +Union of validation error code strings emitted by the contractor signature +form schema. + +*** + + + +### TaxClassificationOptionKey + +> **TaxClassificationOptionKey** = *typeof* [`TAX_CLASSIFICATION_OPTION_KEYS`](#tax_classification_option_keys)\[`number`\] + +A single federal tax classification option key. + +*** + + + +### UseContractorSignatureFormResult + +> **UseContractorSignatureFormResult** = [`HookLoadingResult`](../../utilities.md#hookloadingresult) \| [`UseContractorSignatureFormReady`](#usecontractorsignatureformready) + +Result of [useContractorSignatureForm](#usecontractorsignatureform) — a discriminated union on `isLoading`. + +*** + + + +### W9FieldVariant + +> **W9FieldVariant** = `"text"` \| `"checkbox"` \| `"radio"` \| `"select"` + +Visual input variant for a W-9 field. + +*** + + + +### W9Section + +> **W9Section** = `"classification"` \| `"exemptions"` \| `"address"` \| `"tin"` \| `"certification"` + +Section a W-9 field belongs to, used to group fields under headings when +rendering the signing form. diff --git a/docs/reference/contractor/index.mdx b/docs/reference/contractor/index.mdx index 2af4e34be..91b9a2902 100644 --- a/docs/reference/contractor/index.mdx +++ b/docs/reference/contractor/index.mdx @@ -14,7 +14,7 @@ custom_edit_url: null ## 📁 ContractorOnboarding - + ## 📁 ContractorManagement @@ -22,4 +22,4 @@ custom_edit_url: null ## 🪝 Hooks - + diff --git a/docs/reference/contractor/onboarding/blocks.md b/docs/reference/contractor/onboarding/blocks.md index 5d8a2c997..ce2ae9050 100644 --- a/docs/reference/contractor/onboarding/blocks.md +++ b/docs/reference/contractor/onboarding/blocks.md @@ -53,6 +53,44 @@ function MyComponent() { } ``` + + +## ContractorDocumentSigner + +Standalone flow for a contractor to review and sign their documents (W-9). + +### ContractorDocumentSignerProps + + + +Props for [ContractorDocumentSigner](#contractordocumentsigner). + +| Property | Type | Description | +| ------ | ------ | ------ | +| `contractorId` | `string` | The associated contractor identifier. | +| `onEvent` | [`OnEventType`](../../index.md#oneventtype)\<[`EventType`](../../events.md#eventtype), `unknown`\> | Callback invoked each time the component emits an event — user interactions, successful API responses, step transitions, or errors. Receives the event type constant and an optional payload whose shape varies by event. See the [Event Handling guide](https://docs.gusto.com/embedded-payroll/docs/event-handling) and each component's event table for the full list of emitted events. | +| `dictionary?` | `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`common`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyAddresses`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyAssignSignatory`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyBankAccount`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyDocumentList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyFederalTaxes`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyIndustry`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyLocations`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyOnboardingOverview`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyPaySchedule`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanySignatureForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyStateTaxes`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffCreateTimeOffPolicy`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffEmployeeTable`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffHolidayPolicy`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffPolicyDetail`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffSelectEmployees`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffSelectPolicyType`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffTimeOffPolicies`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffTimeOffPolicyDetails`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`CompanyTimeOffTimeOffRequests`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorAddress`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorContractorList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorDocumentsList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorLanding`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorNewHireReport`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorPaymentMethod`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorPaymentsCreatePayment`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorPaymentsPaymentHistory`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorPaymentsPaymentStatement`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorPaymentsPaymentSummary`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorPaymentsPaymentsList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorProfile`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorSignatureForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorSubmit`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeBankAccount`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeBankFormBody`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeCompensation`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeDashboard`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeDeductions`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeDeductionsForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeDocumentManager`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeDocumentSigner`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeEmployeeDocuments`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeEmployeeList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeEmploymentEligibility`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeFederalTaxes`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeFederalTaxesView`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeHomeAddress`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeI9SignatureForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeLanding`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementCompensation`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementDeductions`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementDocuments`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementFederalTaxes`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementHomeAddress`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementPaymentMethod`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementPaymentMethodBankForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementPaymentMethodSplitForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementPaystubs`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementProfile`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementStateTaxes`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementWorkAddress`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeManagementEmployeeList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeOnboardingSummary`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeePaySchedules`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeePaymentMethod`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeProfile`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeSplitPaycheck`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeSplitPaymentsFormBody`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeStateTaxes`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeStateTaxesView`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeTerminationsTerminateEmployee`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeTerminationsTerminationFlow`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`EmployeeTerminationsTerminationSummary`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`InformationRequestsInformationRequestForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`InformationRequestsInformationRequestList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`InformationRequests`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollCommon`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollConfirmWireDetailsBanner`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollConfirmWireDetailsForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollDismissal`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollEmployeeSelection`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollGrossUpModal`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollOffCycle`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollOffCycleCreation`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollOffCycleDeductionsSetting`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollOffCyclePayPeriodDateForm`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollOffCycleReasonSelection`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollOffCycleTaxWithholding`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollBlocker`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollConfiguration`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollEditEmployee`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollFlow`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollHistory`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollLanding`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollOverview`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollPayrollReceipts`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollRecoveryCasesList`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollRecoveryCasesResubmit`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollTransition`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollTransitionCreation`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollTransitionPayrollAlert`\>\> \| `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`PayrollWireInstructions`\>\> | Overrides for the component's i18n strings. Supply a partial object whose keys match the component's resource namespace — any omitted keys fall back to SDK defaults. See the [Translation guide](https://docs.gusto.com/embedded-payroll/docs/translation) for details. | + +_Inherits `children`, `className`, `defaultValues`, `FallbackComponent`, `LoaderComponent` from [BaseComponentInterface](../../index.md#basecomponentinterface)._ + +### Remarks + +Lists the contractor's documents and routes through the signing UI for each +one, returning to the list after a document is signed or the user navigates +back. Composes [DocumentsList](#documentslist) and [SignatureForm](#signatureform). + +| Event | Description | Data | +| ----- | ----------- | ---- | +| `contractor/documents/view` | Fired when a document's "Sign" action is selected from the list | `{ uuid: string; title?: string }` | +| `contractor/documents/sign` | Fired after a document is successfully signed | The signed document | +| `contractor/documents/done` | Fired when the contractor completes the documents step | — | +| `CANCEL` | Fired when the user navigates back from the signature form to the list | — | + +### Components + +- [DocumentsList](#documentslist) +- [SignatureForm](#signatureform) + ## ContractorList @@ -147,6 +185,37 @@ _Inherits `children`, `className`, `defaultValues`, `FallbackComponent`, `Loader | `contractor/invite/selfOnboarding` | The invite action was triggered for a self-onboarding contractor. | `{ contractorId: string }` | | `contractor/submit/done` | The submission step finished — fired after a successful status update, after an invite, or when the contractor was already onboarded. | `{ message: string }`, optionally with `onboardingStatus` when the contractor was already completed. | + + +## DocumentsList + +Lists a contractor's documents and lets the contractor open each one for signing. + +### DocumentsListProps + + + +Props for [DocumentsList](#documentslist). + +| Property | Type | Description | +| ------ | ------ | ------ | +| `contractorId` | `string` | The associated contractor identifier. | +| `onEvent` | [`OnEventType`](../../index.md#oneventtype)\<[`EventType`](../../events.md#eventtype), `unknown`\> | Callback invoked each time the component emits an event — user interactions, successful API responses, step transitions, or errors. Receives the event type constant and an optional payload whose shape varies by event. See the [Event Handling guide](https://docs.gusto.com/embedded-payroll/docs/event-handling) and each component's event table for the full list of emitted events. | +| `dictionary?` | `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorDocumentsList`\>\> | Overrides for the component's i18n strings. Supply a partial object whose keys match the component's resource namespace — any omitted keys fall back to SDK defaults. See the [Translation guide](https://docs.gusto.com/embedded-payroll/docs/translation) for details. | + +_Inherits `children`, `className`, `defaultValues`, `FallbackComponent`, `LoaderComponent` from [BaseComponentInterface](../../index.md#basecomponentinterface)._ + +### Remarks + +Fetches the contractor's documents via [useContractorDocumentsList](../hooks/use-contractor-documents-list.md#usecontractordocumentslist) and +renders them in a table. The Continue action is disabled until every document +that requires signing has been signed. + +| Event | Description | Data | +| ----- | ----------- | ---- | +| `contractor/documents/view` | Fired when a document's "Sign" action is selected | `{ uuid?: string; title?: string }` | +| `contractor/documents/done` | Fired when all required documents are signed and the user continues | — | + ## Landing @@ -266,6 +335,38 @@ function PaymentMethodStep() { } ``` + + +## SignatureForm + +Standalone form for signing an individual contractor document (W-9). + +### SignatureFormProps + + + +Props for [SignatureForm](#signatureform). + +| Property | Type | Description | +| ------ | ------ | ------ | +| `contractorId` | `string` | The associated contractor identifier. | +| `documentUuid` | `string` | The UUID of the contractor document to sign. | +| `onEvent` | [`OnEventType`](../../index.md#oneventtype)\<[`EventType`](../../events.md#eventtype), `unknown`\> | Callback invoked each time the component emits an event — user interactions, successful API responses, step transitions, or errors. Receives the event type constant and an optional payload whose shape varies by event. See the [Event Handling guide](https://docs.gusto.com/embedded-payroll/docs/event-handling) and each component's event table for the full list of emitted events. | +| `dictionary?` | `Record`\<`"en"`, [`DeepPartial`](../../index.md#deeppartial)\<`ContractorSignatureForm`\>\> | Overrides for the component's i18n strings. Supply a partial object whose keys match the component's resource namespace — any omitted keys fall back to SDK defaults. See the [Translation guide](https://docs.gusto.com/embedded-payroll/docs/translation) for details. | + +_Inherits `children`, `className`, `defaultValues`, `FallbackComponent`, `LoaderComponent` from [BaseComponentInterface](../../index.md#basecomponentinterface)._ + +### Remarks + +Lower-level building block used internally by `ContractorDocumentSigner` for +its signing view. Use this component directly when you need full control over +navigation between the document list and the signature form. + +| Event | Description | Data | +| ----- | ----------- | ---- | +| `contractor/documents/sign` | Fired when the document is successfully signed | The signed document | +| `CANCEL` | Fired when the user navigates back from the signature form | — | + ## Utility types diff --git a/docs/reference/contractor/onboarding/index.md b/docs/reference/contractor/onboarding/index.md index 55bf8096f..183b65491 100644 --- a/docs/reference/contractor/onboarding/index.md +++ b/docs/reference/contractor/onboarding/index.md @@ -23,9 +23,12 @@ custom_edit_url: null | Component | Description | | --------- | ----------- | | [Address](blocks.md#address) | Form for collecting and updating a contractor's mailing address. Renders a business or home address title based on the contractor type. | +| [ContractorDocumentSigner](blocks.md#contractordocumentsigner) | Standalone flow for a contractor to review and sign their documents (W-9). | | [ContractorList](blocks.md#contractorlist) | Lists a company's contractors with controls to add, edit, delete, and continue onboarding. | | [ContractorProfile](blocks.md#contractorprofile) | Form for creating or editing a contractor profile, supporting both individual and business contractor types. | | [ContractorSubmit](blocks.md#contractorsubmit) | Finalizes contractor onboarding by updating the onboarding status, and in the self-onboarding flow can trigger an invitation to the contractor. | +| [DocumentsList](blocks.md#documentslist) | Lists a contractor's documents and lets the contractor open each one for signing. | | [Landing](blocks.md#landing) | Landing page for the contractor self-onboarding flow. Displays a welcome message and the list of onboarding steps the contractor needs to complete. | | [NewHireReport](blocks.md#newhirereport) | Collects new hire reporting information for a contractor and persists it to the contractor record. | | [PaymentMethod](blocks.md#paymentmethod) | Manages a contractor's payment method, capturing a bank account for direct deposit or recording check as the payment method. | +| [SignatureForm](blocks.md#signatureform) | Standalone form for signing an individual contractor document (W-9). | diff --git a/docs/reference/events.md b/docs/reference/events.md index d7b433b61..6428026bb 100644 --- a/docs/reference/events.md +++ b/docs/reference/events.md @@ -67,6 +67,7 @@ Catalog of every event key that an SDK component can emit through `onEvent`. | `CONTRACTOR_CREATE` | `"contractor/create"` | `'contractor/create'` | | `CONTRACTOR_CREATED` | `"contractor/created"` | `'contractor/created'` | | `CONTRACTOR_DELETED` | `"contractor/deleted"` | `'contractor/deleted'` | +| `CONTRACTOR_DOCUMENTS_DONE` | `"contractor/documents/done"` | `'contractor/documents/done'` | | `CONTRACTOR_INVITE_CONTRACTOR` | `"contractor/invite/selfOnboarding"` | `'contractor/invite/selfOnboarding'` | | `CONTRACTOR_NEW_HIRE_REPORT_DONE` | `"contractor/newHireReport/done"` | `'contractor/newHireReport/done'` | | `CONTRACTOR_NEW_HIRE_REPORT_UPDATED` | `"contractor/newHireReport/updated"` | `'contractor/newHireReport/updated'` | @@ -88,9 +89,11 @@ Catalog of every event key that an SDK component can emit through `onEvent`. | `CONTRACTOR_PAYMENT_VIEW_DETAILS` | `"contractor/payments/view/details"` | `'contractor/payments/view/details'` | | `CONTRACTOR_PROFILE_DONE` | `"contractor/profile/done"` | `'contractor/profile/done'` | | `CONTRACTOR_SELF_ONBOARDING_START` | `"contractor/selfOnboarding/start"` | `'contractor/selfOnboarding/start'` | +| `CONTRACTOR_SIGN_DOCUMENT` | `"contractor/documents/sign"` | `'contractor/documents/sign'` | | `CONTRACTOR_SUBMIT_DONE` | `"contractor/submit/done"` | `'contractor/submit/done'` | | `CONTRACTOR_UPDATE` | `"contractor/update"` | `'contractor/update'` | | `CONTRACTOR_UPDATED` | `"contractor/updated"` | `'contractor/updated'` | +| `CONTRACTOR_VIEW_DOCUMENT_TO_SIGN` | `"contractor/documents/view"` | `'contractor/documents/view'` | | `DISMISSAL_PAY_PERIOD_SELECTED` | `"dismissal/payPeriod/selected"` | `'dismissal/payPeriod/selected'` | | `EMPLOYEE_BANK_ACCOUNT_CREATE` | `"employee/bankAccount/create"` | `'employee/bankAccount/create'` | | `EMPLOYEE_BANK_ACCOUNT_CREATED` | `"employee/bankAccount/created"` | `'employee/bankAccount/created'` | diff --git a/docs/reference/index.md b/docs/reference/index.md index 1939a9ac6..f31218304 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -371,6 +371,9 @@ shape mixed into every public SDK feature component. - [`PaymentMethodProps`](contractor/onboarding/blocks.md#paymentmethodprops) - [`NewHireReportProps`](contractor/onboarding/blocks.md#newhirereportprops) - [`ContractorSubmitProps`](contractor/onboarding/blocks.md#contractorsubmitprops) +- [`ContractorDocumentSignerProps`](contractor/onboarding/blocks.md#contractordocumentsignerprops) +- [`DocumentsListProps`](contractor/onboarding/blocks.md#documentslistprops) +- [`SignatureFormProps`](contractor/onboarding/blocks.md#signatureformprops) - [`PaymentFlowProps`](contractor/management/payment-flow.md#paymentflowprops) - [`PaymentsListProps`](contractor/management/blocks.md#paymentslistprops) - [`CreatePaymentProps`](contractor/management/blocks.md#createpaymentprops) @@ -452,7 +455,7 @@ you do not supply fall back to the SDK's built-in React Aria implementations. | `children?` | `ReactNode` | The application tree that should have access to the SDK. | | `components?` | `Partial`\<[`ComponentsContextType`](component-inventory.md#componentscontexttype)\> | Partial component overrides. Any component you do not supply uses the SDK's default React Aria implementation. | | `currency?` | `string` | ISO 4217 currency code used for monetary formatting. Defaults to `'USD'`. | -| `dictionary?` | `Record`\<`"en"`, `Partial`\<\{ `common`: `common`; `Company.Addresses`: `CompanyAddresses`; `Company.AssignSignatory`: `CompanyAssignSignatory`; `Company.BankAccount`: `CompanyBankAccount`; `Company.DocumentList`: `CompanyDocumentList`; `Company.FederalTaxes`: `CompanyFederalTaxes`; `Company.Industry`: `CompanyIndustry`; `Company.Locations`: `CompanyLocations`; `Company.OnboardingOverview`: `CompanyOnboardingOverview`; `Company.PaySchedule`: `CompanyPaySchedule`; `Company.SignatureForm`: `CompanySignatureForm`; `Company.StateTaxes`: `CompanyStateTaxes`; `Company.TimeOff.CreateTimeOffPolicy`: `CompanyTimeOffCreateTimeOffPolicy`; `Company.TimeOff.EmployeeTable`: `CompanyTimeOffEmployeeTable`; `Company.TimeOff.HolidayPolicy`: `CompanyTimeOffHolidayPolicy`; `Company.TimeOff.PolicyDetail`: `CompanyTimeOffPolicyDetail`; `Company.TimeOff.SelectEmployees`: `CompanyTimeOffSelectEmployees`; `Company.TimeOff.SelectPolicyType`: `CompanyTimeOffSelectPolicyType`; `Company.TimeOff.TimeOffPolicies`: `CompanyTimeOffTimeOffPolicies`; `Company.TimeOff.TimeOffPolicyDetails`: `CompanyTimeOffTimeOffPolicyDetails`; `Company.TimeOff.TimeOffRequests`: `CompanyTimeOffTimeOffRequests`; `Contractor.Address`: `ContractorAddress`; `Contractor.ContractorList`: `ContractorContractorList`; `Contractor.Landing`: `ContractorLanding`; `Contractor.NewHireReport`: `ContractorNewHireReport`; `Contractor.PaymentMethod`: `ContractorPaymentMethod`; `Contractor.Payments.CreatePayment`: `ContractorPaymentsCreatePayment`; `Contractor.Payments.PaymentHistory`: `ContractorPaymentsPaymentHistory`; `Contractor.Payments.PaymentsList`: `ContractorPaymentsPaymentsList`; `Contractor.Payments.PaymentStatement`: `ContractorPaymentsPaymentStatement`; `Contractor.Payments.PaymentSummary`: `ContractorPaymentsPaymentSummary`; `Contractor.Profile`: `ContractorProfile`; `Contractor.Submit`: `ContractorSubmit`; `Employee.BankAccount`: `EmployeeBankAccount`; `Employee.BankFormBody`: `EmployeeBankFormBody`; `Employee.Compensation`: `EmployeeCompensation`; `Employee.Dashboard`: `EmployeeDashboard`; `Employee.Deductions`: `EmployeeDeductions`; `Employee.DeductionsForm`: `EmployeeDeductionsForm`; `Employee.DocumentManager`: `EmployeeDocumentManager`; `Employee.DocumentSigner`: `EmployeeDocumentSigner`; `Employee.EmployeeDocuments`: `EmployeeEmployeeDocuments`; `Employee.EmployeeList`: `EmployeeEmployeeList`; `Employee.EmploymentEligibility`: `EmployeeEmploymentEligibility`; `Employee.FederalTaxes`: `EmployeeFederalTaxes`; `Employee.FederalTaxesView`: `EmployeeFederalTaxesView`; `Employee.HomeAddress`: `EmployeeHomeAddress`; `Employee.I9SignatureForm`: `EmployeeI9SignatureForm`; `Employee.Landing`: `EmployeeLanding`; `Employee.Management.Compensation`: `EmployeeManagementCompensation`; `Employee.Management.Deductions`: `EmployeeManagementDeductions`; `Employee.Management.Documents`: `EmployeeManagementDocuments`; `Employee.Management.FederalTaxes`: `EmployeeManagementFederalTaxes`; `Employee.Management.HomeAddress`: `EmployeeManagementHomeAddress`; `Employee.Management.PaymentMethod`: `EmployeeManagementPaymentMethod`; `Employee.Management.PaymentMethodBankForm`: `EmployeeManagementPaymentMethodBankForm`; `Employee.Management.PaymentMethodSplitForm`: `EmployeeManagementPaymentMethodSplitForm`; `Employee.Management.Paystubs`: `EmployeeManagementPaystubs`; `Employee.Management.Profile`: `EmployeeManagementProfile`; `Employee.Management.StateTaxes`: `EmployeeManagementStateTaxes`; `Employee.Management.WorkAddress`: `EmployeeManagementWorkAddress`; `Employee.ManagementEmployeeList`: `EmployeeManagementEmployeeList`; `Employee.OnboardingSummary`: `EmployeeOnboardingSummary`; `Employee.PaymentMethod`: `EmployeePaymentMethod`; `Employee.PaySchedules`: `EmployeePaySchedules`; `Employee.Profile`: `EmployeeProfile`; `Employee.SplitPaycheck`: `EmployeeSplitPaycheck`; `Employee.SplitPaymentsFormBody`: `EmployeeSplitPaymentsFormBody`; `Employee.StateTaxes`: `EmployeeStateTaxes`; `Employee.StateTaxesView`: `EmployeeStateTaxesView`; `Employee.Terminations.TerminateEmployee`: `EmployeeTerminationsTerminateEmployee`; `Employee.Terminations.TerminationFlow`: `EmployeeTerminationsTerminationFlow`; `Employee.Terminations.TerminationSummary`: `EmployeeTerminationsTerminationSummary`; `InformationRequests`: `InformationRequests`; `InformationRequests.InformationRequestForm`: `InformationRequestsInformationRequestForm`; `InformationRequests.InformationRequestList`: `InformationRequestsInformationRequestList`; `Payroll.Common`: `PayrollCommon`; `Payroll.ConfirmWireDetailsBanner`: `PayrollConfirmWireDetailsBanner`; `Payroll.ConfirmWireDetailsForm`: `PayrollConfirmWireDetailsForm`; `Payroll.Dismissal`: `PayrollDismissal`; `Payroll.EmployeeSelection`: `PayrollEmployeeSelection`; `Payroll.GrossUpModal`: `PayrollGrossUpModal`; `Payroll.OffCycle`: `PayrollOffCycle`; `Payroll.OffCycleCreation`: `PayrollOffCycleCreation`; `Payroll.OffCycleDeductionsSetting`: `PayrollOffCycleDeductionsSetting`; `Payroll.OffCyclePayPeriodDateForm`: `PayrollOffCyclePayPeriodDateForm`; `Payroll.OffCycleReasonSelection`: `PayrollOffCycleReasonSelection`; `Payroll.OffCycleTaxWithholding`: `PayrollOffCycleTaxWithholding`; `Payroll.PayrollBlocker`: `PayrollPayrollBlocker`; `Payroll.PayrollConfiguration`: `PayrollPayrollConfiguration`; `Payroll.PayrollEditEmployee`: `PayrollPayrollEditEmployee`; `Payroll.PayrollFlow`: `PayrollPayrollFlow`; `Payroll.PayrollHistory`: `PayrollPayrollHistory`; `Payroll.PayrollLanding`: `PayrollPayrollLanding`; `Payroll.PayrollList`: `PayrollPayrollList`; `Payroll.PayrollOverview`: `PayrollPayrollOverview`; `Payroll.PayrollReceipts`: `PayrollPayrollReceipts`; `Payroll.RecoveryCasesList`: `PayrollRecoveryCasesList`; `Payroll.RecoveryCasesResubmit`: `PayrollRecoveryCasesResubmit`; `Payroll.Transition`: `PayrollTransition`; `Payroll.TransitionCreation`: `PayrollTransitionCreation`; `Payroll.TransitionPayrollAlert`: `PayrollTransitionPayrollAlert`; `Payroll.WireInstructions`: `PayrollWireInstructions`; \}\>\> | Translation overrides keyed by language and i18next namespace. Strings supplied here replace the SDK defaults for the matching keys. | +| `dictionary?` | `Record`\<`"en"`, `Partial`\<\{ `common`: `common`; `Company.Addresses`: `CompanyAddresses`; `Company.AssignSignatory`: `CompanyAssignSignatory`; `Company.BankAccount`: `CompanyBankAccount`; `Company.DocumentList`: `CompanyDocumentList`; `Company.FederalTaxes`: `CompanyFederalTaxes`; `Company.Industry`: `CompanyIndustry`; `Company.Locations`: `CompanyLocations`; `Company.OnboardingOverview`: `CompanyOnboardingOverview`; `Company.PaySchedule`: `CompanyPaySchedule`; `Company.SignatureForm`: `CompanySignatureForm`; `Company.StateTaxes`: `CompanyStateTaxes`; `Company.TimeOff.CreateTimeOffPolicy`: `CompanyTimeOffCreateTimeOffPolicy`; `Company.TimeOff.EmployeeTable`: `CompanyTimeOffEmployeeTable`; `Company.TimeOff.HolidayPolicy`: `CompanyTimeOffHolidayPolicy`; `Company.TimeOff.PolicyDetail`: `CompanyTimeOffPolicyDetail`; `Company.TimeOff.SelectEmployees`: `CompanyTimeOffSelectEmployees`; `Company.TimeOff.SelectPolicyType`: `CompanyTimeOffSelectPolicyType`; `Company.TimeOff.TimeOffPolicies`: `CompanyTimeOffTimeOffPolicies`; `Company.TimeOff.TimeOffPolicyDetails`: `CompanyTimeOffTimeOffPolicyDetails`; `Company.TimeOff.TimeOffRequests`: `CompanyTimeOffTimeOffRequests`; `Contractor.Address`: `ContractorAddress`; `Contractor.ContractorList`: `ContractorContractorList`; `Contractor.DocumentsList`: `ContractorDocumentsList`; `Contractor.Landing`: `ContractorLanding`; `Contractor.NewHireReport`: `ContractorNewHireReport`; `Contractor.PaymentMethod`: `ContractorPaymentMethod`; `Contractor.Payments.CreatePayment`: `ContractorPaymentsCreatePayment`; `Contractor.Payments.PaymentHistory`: `ContractorPaymentsPaymentHistory`; `Contractor.Payments.PaymentsList`: `ContractorPaymentsPaymentsList`; `Contractor.Payments.PaymentStatement`: `ContractorPaymentsPaymentStatement`; `Contractor.Payments.PaymentSummary`: `ContractorPaymentsPaymentSummary`; `Contractor.Profile`: `ContractorProfile`; `Contractor.SignatureForm`: `ContractorSignatureForm`; `Contractor.Submit`: `ContractorSubmit`; `Employee.BankAccount`: `EmployeeBankAccount`; `Employee.BankFormBody`: `EmployeeBankFormBody`; `Employee.Compensation`: `EmployeeCompensation`; `Employee.Dashboard`: `EmployeeDashboard`; `Employee.Deductions`: `EmployeeDeductions`; `Employee.DeductionsForm`: `EmployeeDeductionsForm`; `Employee.DocumentManager`: `EmployeeDocumentManager`; `Employee.DocumentSigner`: `EmployeeDocumentSigner`; `Employee.EmployeeDocuments`: `EmployeeEmployeeDocuments`; `Employee.EmployeeList`: `EmployeeEmployeeList`; `Employee.EmploymentEligibility`: `EmployeeEmploymentEligibility`; `Employee.FederalTaxes`: `EmployeeFederalTaxes`; `Employee.FederalTaxesView`: `EmployeeFederalTaxesView`; `Employee.HomeAddress`: `EmployeeHomeAddress`; `Employee.I9SignatureForm`: `EmployeeI9SignatureForm`; `Employee.Landing`: `EmployeeLanding`; `Employee.Management.Compensation`: `EmployeeManagementCompensation`; `Employee.Management.Deductions`: `EmployeeManagementDeductions`; `Employee.Management.Documents`: `EmployeeManagementDocuments`; `Employee.Management.FederalTaxes`: `EmployeeManagementFederalTaxes`; `Employee.Management.HomeAddress`: `EmployeeManagementHomeAddress`; `Employee.Management.PaymentMethod`: `EmployeeManagementPaymentMethod`; `Employee.Management.PaymentMethodBankForm`: `EmployeeManagementPaymentMethodBankForm`; `Employee.Management.PaymentMethodSplitForm`: `EmployeeManagementPaymentMethodSplitForm`; `Employee.Management.Paystubs`: `EmployeeManagementPaystubs`; `Employee.Management.Profile`: `EmployeeManagementProfile`; `Employee.Management.StateTaxes`: `EmployeeManagementStateTaxes`; `Employee.Management.WorkAddress`: `EmployeeManagementWorkAddress`; `Employee.ManagementEmployeeList`: `EmployeeManagementEmployeeList`; `Employee.OnboardingSummary`: `EmployeeOnboardingSummary`; `Employee.PaymentMethod`: `EmployeePaymentMethod`; `Employee.PaySchedules`: `EmployeePaySchedules`; `Employee.Profile`: `EmployeeProfile`; `Employee.SplitPaycheck`: `EmployeeSplitPaycheck`; `Employee.SplitPaymentsFormBody`: `EmployeeSplitPaymentsFormBody`; `Employee.StateTaxes`: `EmployeeStateTaxes`; `Employee.StateTaxesView`: `EmployeeStateTaxesView`; `Employee.Terminations.TerminateEmployee`: `EmployeeTerminationsTerminateEmployee`; `Employee.Terminations.TerminationFlow`: `EmployeeTerminationsTerminationFlow`; `Employee.Terminations.TerminationSummary`: `EmployeeTerminationsTerminationSummary`; `InformationRequests`: `InformationRequests`; `InformationRequests.InformationRequestForm`: `InformationRequestsInformationRequestForm`; `InformationRequests.InformationRequestList`: `InformationRequestsInformationRequestList`; `Payroll.Common`: `PayrollCommon`; `Payroll.ConfirmWireDetailsBanner`: `PayrollConfirmWireDetailsBanner`; `Payroll.ConfirmWireDetailsForm`: `PayrollConfirmWireDetailsForm`; `Payroll.Dismissal`: `PayrollDismissal`; `Payroll.EmployeeSelection`: `PayrollEmployeeSelection`; `Payroll.GrossUpModal`: `PayrollGrossUpModal`; `Payroll.OffCycle`: `PayrollOffCycle`; `Payroll.OffCycleCreation`: `PayrollOffCycleCreation`; `Payroll.OffCycleDeductionsSetting`: `PayrollOffCycleDeductionsSetting`; `Payroll.OffCyclePayPeriodDateForm`: `PayrollOffCyclePayPeriodDateForm`; `Payroll.OffCycleReasonSelection`: `PayrollOffCycleReasonSelection`; `Payroll.OffCycleTaxWithholding`: `PayrollOffCycleTaxWithholding`; `Payroll.PayrollBlocker`: `PayrollPayrollBlocker`; `Payroll.PayrollConfiguration`: `PayrollPayrollConfiguration`; `Payroll.PayrollEditEmployee`: `PayrollPayrollEditEmployee`; `Payroll.PayrollFlow`: `PayrollPayrollFlow`; `Payroll.PayrollHistory`: `PayrollPayrollHistory`; `Payroll.PayrollLanding`: `PayrollPayrollLanding`; `Payroll.PayrollList`: `PayrollPayrollList`; `Payroll.PayrollOverview`: `PayrollPayrollOverview`; `Payroll.PayrollReceipts`: `PayrollPayrollReceipts`; `Payroll.RecoveryCasesList`: `PayrollRecoveryCasesList`; `Payroll.RecoveryCasesResubmit`: `PayrollRecoveryCasesResubmit`; `Payroll.Transition`: `PayrollTransition`; `Payroll.TransitionCreation`: `PayrollTransitionCreation`; `Payroll.TransitionPayrollAlert`: `PayrollTransitionPayrollAlert`; `Payroll.WireInstructions`: `PayrollWireInstructions`; \}\>\> | Translation overrides keyed by language and i18next namespace. Strings supplied here replace the SDK defaults for the matching keys. | | `lng?` | `string` | Active i18next language. Defaults to `'en'`. | | `LoaderComponent?` | (`__namedParameters`) => `Element` | Loading indicator rendered while SDK queries are pending. Overrides the SDK default spinner. | | `locale?` | `string` | BCP 47 locale used for number, date, and currency formatting throughout the SDK. Defaults to `'en-US'`. | @@ -480,7 +483,7 @@ Props for [GustoProviderCustomUIAdapter](#gustoprovidercustomuiadapter). | `config` | [`APIConfig`](#apiconfig) | API client configuration, including the proxy `baseUrl`, request hooks, and observability. See [APIConfig](#apiconfig). | | `children?` | `ReactNode` | The application tree that should have access to the SDK. | | `currency?` | `string` | ISO 4217 currency code used for monetary formatting. Defaults to `'USD'`. | -| `dictionary?` | `Record`\<`"en"`, `Partial`\<\{ `common`: `common`; `Company.Addresses`: `CompanyAddresses`; `Company.AssignSignatory`: `CompanyAssignSignatory`; `Company.BankAccount`: `CompanyBankAccount`; `Company.DocumentList`: `CompanyDocumentList`; `Company.FederalTaxes`: `CompanyFederalTaxes`; `Company.Industry`: `CompanyIndustry`; `Company.Locations`: `CompanyLocations`; `Company.OnboardingOverview`: `CompanyOnboardingOverview`; `Company.PaySchedule`: `CompanyPaySchedule`; `Company.SignatureForm`: `CompanySignatureForm`; `Company.StateTaxes`: `CompanyStateTaxes`; `Company.TimeOff.CreateTimeOffPolicy`: `CompanyTimeOffCreateTimeOffPolicy`; `Company.TimeOff.EmployeeTable`: `CompanyTimeOffEmployeeTable`; `Company.TimeOff.HolidayPolicy`: `CompanyTimeOffHolidayPolicy`; `Company.TimeOff.PolicyDetail`: `CompanyTimeOffPolicyDetail`; `Company.TimeOff.SelectEmployees`: `CompanyTimeOffSelectEmployees`; `Company.TimeOff.SelectPolicyType`: `CompanyTimeOffSelectPolicyType`; `Company.TimeOff.TimeOffPolicies`: `CompanyTimeOffTimeOffPolicies`; `Company.TimeOff.TimeOffPolicyDetails`: `CompanyTimeOffTimeOffPolicyDetails`; `Company.TimeOff.TimeOffRequests`: `CompanyTimeOffTimeOffRequests`; `Contractor.Address`: `ContractorAddress`; `Contractor.ContractorList`: `ContractorContractorList`; `Contractor.Landing`: `ContractorLanding`; `Contractor.NewHireReport`: `ContractorNewHireReport`; `Contractor.PaymentMethod`: `ContractorPaymentMethod`; `Contractor.Payments.CreatePayment`: `ContractorPaymentsCreatePayment`; `Contractor.Payments.PaymentHistory`: `ContractorPaymentsPaymentHistory`; `Contractor.Payments.PaymentsList`: `ContractorPaymentsPaymentsList`; `Contractor.Payments.PaymentStatement`: `ContractorPaymentsPaymentStatement`; `Contractor.Payments.PaymentSummary`: `ContractorPaymentsPaymentSummary`; `Contractor.Profile`: `ContractorProfile`; `Contractor.Submit`: `ContractorSubmit`; `Employee.BankAccount`: `EmployeeBankAccount`; `Employee.BankFormBody`: `EmployeeBankFormBody`; `Employee.Compensation`: `EmployeeCompensation`; `Employee.Dashboard`: `EmployeeDashboard`; `Employee.Deductions`: `EmployeeDeductions`; `Employee.DeductionsForm`: `EmployeeDeductionsForm`; `Employee.DocumentManager`: `EmployeeDocumentManager`; `Employee.DocumentSigner`: `EmployeeDocumentSigner`; `Employee.EmployeeDocuments`: `EmployeeEmployeeDocuments`; `Employee.EmployeeList`: `EmployeeEmployeeList`; `Employee.EmploymentEligibility`: `EmployeeEmploymentEligibility`; `Employee.FederalTaxes`: `EmployeeFederalTaxes`; `Employee.FederalTaxesView`: `EmployeeFederalTaxesView`; `Employee.HomeAddress`: `EmployeeHomeAddress`; `Employee.I9SignatureForm`: `EmployeeI9SignatureForm`; `Employee.Landing`: `EmployeeLanding`; `Employee.Management.Compensation`: `EmployeeManagementCompensation`; `Employee.Management.Deductions`: `EmployeeManagementDeductions`; `Employee.Management.Documents`: `EmployeeManagementDocuments`; `Employee.Management.FederalTaxes`: `EmployeeManagementFederalTaxes`; `Employee.Management.HomeAddress`: `EmployeeManagementHomeAddress`; `Employee.Management.PaymentMethod`: `EmployeeManagementPaymentMethod`; `Employee.Management.PaymentMethodBankForm`: `EmployeeManagementPaymentMethodBankForm`; `Employee.Management.PaymentMethodSplitForm`: `EmployeeManagementPaymentMethodSplitForm`; `Employee.Management.Paystubs`: `EmployeeManagementPaystubs`; `Employee.Management.Profile`: `EmployeeManagementProfile`; `Employee.Management.StateTaxes`: `EmployeeManagementStateTaxes`; `Employee.Management.WorkAddress`: `EmployeeManagementWorkAddress`; `Employee.ManagementEmployeeList`: `EmployeeManagementEmployeeList`; `Employee.OnboardingSummary`: `EmployeeOnboardingSummary`; `Employee.PaymentMethod`: `EmployeePaymentMethod`; `Employee.PaySchedules`: `EmployeePaySchedules`; `Employee.Profile`: `EmployeeProfile`; `Employee.SplitPaycheck`: `EmployeeSplitPaycheck`; `Employee.SplitPaymentsFormBody`: `EmployeeSplitPaymentsFormBody`; `Employee.StateTaxes`: `EmployeeStateTaxes`; `Employee.StateTaxesView`: `EmployeeStateTaxesView`; `Employee.Terminations.TerminateEmployee`: `EmployeeTerminationsTerminateEmployee`; `Employee.Terminations.TerminationFlow`: `EmployeeTerminationsTerminationFlow`; `Employee.Terminations.TerminationSummary`: `EmployeeTerminationsTerminationSummary`; `InformationRequests`: `InformationRequests`; `InformationRequests.InformationRequestForm`: `InformationRequestsInformationRequestForm`; `InformationRequests.InformationRequestList`: `InformationRequestsInformationRequestList`; `Payroll.Common`: `PayrollCommon`; `Payroll.ConfirmWireDetailsBanner`: `PayrollConfirmWireDetailsBanner`; `Payroll.ConfirmWireDetailsForm`: `PayrollConfirmWireDetailsForm`; `Payroll.Dismissal`: `PayrollDismissal`; `Payroll.EmployeeSelection`: `PayrollEmployeeSelection`; `Payroll.GrossUpModal`: `PayrollGrossUpModal`; `Payroll.OffCycle`: `PayrollOffCycle`; `Payroll.OffCycleCreation`: `PayrollOffCycleCreation`; `Payroll.OffCycleDeductionsSetting`: `PayrollOffCycleDeductionsSetting`; `Payroll.OffCyclePayPeriodDateForm`: `PayrollOffCyclePayPeriodDateForm`; `Payroll.OffCycleReasonSelection`: `PayrollOffCycleReasonSelection`; `Payroll.OffCycleTaxWithholding`: `PayrollOffCycleTaxWithholding`; `Payroll.PayrollBlocker`: `PayrollPayrollBlocker`; `Payroll.PayrollConfiguration`: `PayrollPayrollConfiguration`; `Payroll.PayrollEditEmployee`: `PayrollPayrollEditEmployee`; `Payroll.PayrollFlow`: `PayrollPayrollFlow`; `Payroll.PayrollHistory`: `PayrollPayrollHistory`; `Payroll.PayrollLanding`: `PayrollPayrollLanding`; `Payroll.PayrollList`: `PayrollPayrollList`; `Payroll.PayrollOverview`: `PayrollPayrollOverview`; `Payroll.PayrollReceipts`: `PayrollPayrollReceipts`; `Payroll.RecoveryCasesList`: `PayrollRecoveryCasesList`; `Payroll.RecoveryCasesResubmit`: `PayrollRecoveryCasesResubmit`; `Payroll.Transition`: `PayrollTransition`; `Payroll.TransitionCreation`: `PayrollTransitionCreation`; `Payroll.TransitionPayrollAlert`: `PayrollTransitionPayrollAlert`; `Payroll.WireInstructions`: `PayrollWireInstructions`; \}\>\> | Translation overrides keyed by language and i18next namespace. Strings supplied here replace the SDK defaults for the matching keys. | +| `dictionary?` | `Record`\<`"en"`, `Partial`\<\{ `common`: `common`; `Company.Addresses`: `CompanyAddresses`; `Company.AssignSignatory`: `CompanyAssignSignatory`; `Company.BankAccount`: `CompanyBankAccount`; `Company.DocumentList`: `CompanyDocumentList`; `Company.FederalTaxes`: `CompanyFederalTaxes`; `Company.Industry`: `CompanyIndustry`; `Company.Locations`: `CompanyLocations`; `Company.OnboardingOverview`: `CompanyOnboardingOverview`; `Company.PaySchedule`: `CompanyPaySchedule`; `Company.SignatureForm`: `CompanySignatureForm`; `Company.StateTaxes`: `CompanyStateTaxes`; `Company.TimeOff.CreateTimeOffPolicy`: `CompanyTimeOffCreateTimeOffPolicy`; `Company.TimeOff.EmployeeTable`: `CompanyTimeOffEmployeeTable`; `Company.TimeOff.HolidayPolicy`: `CompanyTimeOffHolidayPolicy`; `Company.TimeOff.PolicyDetail`: `CompanyTimeOffPolicyDetail`; `Company.TimeOff.SelectEmployees`: `CompanyTimeOffSelectEmployees`; `Company.TimeOff.SelectPolicyType`: `CompanyTimeOffSelectPolicyType`; `Company.TimeOff.TimeOffPolicies`: `CompanyTimeOffTimeOffPolicies`; `Company.TimeOff.TimeOffPolicyDetails`: `CompanyTimeOffTimeOffPolicyDetails`; `Company.TimeOff.TimeOffRequests`: `CompanyTimeOffTimeOffRequests`; `Contractor.Address`: `ContractorAddress`; `Contractor.ContractorList`: `ContractorContractorList`; `Contractor.DocumentsList`: `ContractorDocumentsList`; `Contractor.Landing`: `ContractorLanding`; `Contractor.NewHireReport`: `ContractorNewHireReport`; `Contractor.PaymentMethod`: `ContractorPaymentMethod`; `Contractor.Payments.CreatePayment`: `ContractorPaymentsCreatePayment`; `Contractor.Payments.PaymentHistory`: `ContractorPaymentsPaymentHistory`; `Contractor.Payments.PaymentsList`: `ContractorPaymentsPaymentsList`; `Contractor.Payments.PaymentStatement`: `ContractorPaymentsPaymentStatement`; `Contractor.Payments.PaymentSummary`: `ContractorPaymentsPaymentSummary`; `Contractor.Profile`: `ContractorProfile`; `Contractor.SignatureForm`: `ContractorSignatureForm`; `Contractor.Submit`: `ContractorSubmit`; `Employee.BankAccount`: `EmployeeBankAccount`; `Employee.BankFormBody`: `EmployeeBankFormBody`; `Employee.Compensation`: `EmployeeCompensation`; `Employee.Dashboard`: `EmployeeDashboard`; `Employee.Deductions`: `EmployeeDeductions`; `Employee.DeductionsForm`: `EmployeeDeductionsForm`; `Employee.DocumentManager`: `EmployeeDocumentManager`; `Employee.DocumentSigner`: `EmployeeDocumentSigner`; `Employee.EmployeeDocuments`: `EmployeeEmployeeDocuments`; `Employee.EmployeeList`: `EmployeeEmployeeList`; `Employee.EmploymentEligibility`: `EmployeeEmploymentEligibility`; `Employee.FederalTaxes`: `EmployeeFederalTaxes`; `Employee.FederalTaxesView`: `EmployeeFederalTaxesView`; `Employee.HomeAddress`: `EmployeeHomeAddress`; `Employee.I9SignatureForm`: `EmployeeI9SignatureForm`; `Employee.Landing`: `EmployeeLanding`; `Employee.Management.Compensation`: `EmployeeManagementCompensation`; `Employee.Management.Deductions`: `EmployeeManagementDeductions`; `Employee.Management.Documents`: `EmployeeManagementDocuments`; `Employee.Management.FederalTaxes`: `EmployeeManagementFederalTaxes`; `Employee.Management.HomeAddress`: `EmployeeManagementHomeAddress`; `Employee.Management.PaymentMethod`: `EmployeeManagementPaymentMethod`; `Employee.Management.PaymentMethodBankForm`: `EmployeeManagementPaymentMethodBankForm`; `Employee.Management.PaymentMethodSplitForm`: `EmployeeManagementPaymentMethodSplitForm`; `Employee.Management.Paystubs`: `EmployeeManagementPaystubs`; `Employee.Management.Profile`: `EmployeeManagementProfile`; `Employee.Management.StateTaxes`: `EmployeeManagementStateTaxes`; `Employee.Management.WorkAddress`: `EmployeeManagementWorkAddress`; `Employee.ManagementEmployeeList`: `EmployeeManagementEmployeeList`; `Employee.OnboardingSummary`: `EmployeeOnboardingSummary`; `Employee.PaymentMethod`: `EmployeePaymentMethod`; `Employee.PaySchedules`: `EmployeePaySchedules`; `Employee.Profile`: `EmployeeProfile`; `Employee.SplitPaycheck`: `EmployeeSplitPaycheck`; `Employee.SplitPaymentsFormBody`: `EmployeeSplitPaymentsFormBody`; `Employee.StateTaxes`: `EmployeeStateTaxes`; `Employee.StateTaxesView`: `EmployeeStateTaxesView`; `Employee.Terminations.TerminateEmployee`: `EmployeeTerminationsTerminateEmployee`; `Employee.Terminations.TerminationFlow`: `EmployeeTerminationsTerminationFlow`; `Employee.Terminations.TerminationSummary`: `EmployeeTerminationsTerminationSummary`; `InformationRequests`: `InformationRequests`; `InformationRequests.InformationRequestForm`: `InformationRequestsInformationRequestForm`; `InformationRequests.InformationRequestList`: `InformationRequestsInformationRequestList`; `Payroll.Common`: `PayrollCommon`; `Payroll.ConfirmWireDetailsBanner`: `PayrollConfirmWireDetailsBanner`; `Payroll.ConfirmWireDetailsForm`: `PayrollConfirmWireDetailsForm`; `Payroll.Dismissal`: `PayrollDismissal`; `Payroll.EmployeeSelection`: `PayrollEmployeeSelection`; `Payroll.GrossUpModal`: `PayrollGrossUpModal`; `Payroll.OffCycle`: `PayrollOffCycle`; `Payroll.OffCycleCreation`: `PayrollOffCycleCreation`; `Payroll.OffCycleDeductionsSetting`: `PayrollOffCycleDeductionsSetting`; `Payroll.OffCyclePayPeriodDateForm`: `PayrollOffCyclePayPeriodDateForm`; `Payroll.OffCycleReasonSelection`: `PayrollOffCycleReasonSelection`; `Payroll.OffCycleTaxWithholding`: `PayrollOffCycleTaxWithholding`; `Payroll.PayrollBlocker`: `PayrollPayrollBlocker`; `Payroll.PayrollConfiguration`: `PayrollPayrollConfiguration`; `Payroll.PayrollEditEmployee`: `PayrollPayrollEditEmployee`; `Payroll.PayrollFlow`: `PayrollPayrollFlow`; `Payroll.PayrollHistory`: `PayrollPayrollHistory`; `Payroll.PayrollLanding`: `PayrollPayrollLanding`; `Payroll.PayrollList`: `PayrollPayrollList`; `Payroll.PayrollOverview`: `PayrollPayrollOverview`; `Payroll.PayrollReceipts`: `PayrollPayrollReceipts`; `Payroll.RecoveryCasesList`: `PayrollRecoveryCasesList`; `Payroll.RecoveryCasesResubmit`: `PayrollRecoveryCasesResubmit`; `Payroll.Transition`: `PayrollTransition`; `Payroll.TransitionCreation`: `PayrollTransitionCreation`; `Payroll.TransitionPayrollAlert`: `PayrollTransitionPayrollAlert`; `Payroll.WireInstructions`: `PayrollWireInstructions`; \}\>\> | Translation overrides keyed by language and i18next namespace. Strings supplied here replace the SDK defaults for the matching keys. | | `lng?` | `string` | Active i18next language. Defaults to `'en'`. | | `LoaderComponent?` | (`__namedParameters`) => `Element` | Loading indicator rendered while SDK queries are pending. Overrides the SDK default spinner. | | `locale?` | `string` | BCP 47 locale used for number, date, and currency formatting throughout the SDK. Defaults to `'en-US'`. | @@ -507,7 +510,7 @@ Shared configuration props accepted by [GustoProvider](#gustoprovider) and [Gust | `components` | [`ComponentsContextType`](component-inventory.md#componentscontexttype) | Complete map of UI components the SDK renders. Required because this adapter ships no defaults. | | `config` | [`APIConfig`](#apiconfig) | API client configuration, including the proxy `baseUrl`, request hooks, and observability. See [APIConfig](#apiconfig). | | `currency?` | `string` | ISO 4217 currency code used for monetary formatting. Defaults to `'USD'`. | -| `dictionary?` | `Record`\<`"en"`, `Partial`\<\{ `common`: `common`; `Company.Addresses`: `CompanyAddresses`; `Company.AssignSignatory`: `CompanyAssignSignatory`; `Company.BankAccount`: `CompanyBankAccount`; `Company.DocumentList`: `CompanyDocumentList`; `Company.FederalTaxes`: `CompanyFederalTaxes`; `Company.Industry`: `CompanyIndustry`; `Company.Locations`: `CompanyLocations`; `Company.OnboardingOverview`: `CompanyOnboardingOverview`; `Company.PaySchedule`: `CompanyPaySchedule`; `Company.SignatureForm`: `CompanySignatureForm`; `Company.StateTaxes`: `CompanyStateTaxes`; `Company.TimeOff.CreateTimeOffPolicy`: `CompanyTimeOffCreateTimeOffPolicy`; `Company.TimeOff.EmployeeTable`: `CompanyTimeOffEmployeeTable`; `Company.TimeOff.HolidayPolicy`: `CompanyTimeOffHolidayPolicy`; `Company.TimeOff.PolicyDetail`: `CompanyTimeOffPolicyDetail`; `Company.TimeOff.SelectEmployees`: `CompanyTimeOffSelectEmployees`; `Company.TimeOff.SelectPolicyType`: `CompanyTimeOffSelectPolicyType`; `Company.TimeOff.TimeOffPolicies`: `CompanyTimeOffTimeOffPolicies`; `Company.TimeOff.TimeOffPolicyDetails`: `CompanyTimeOffTimeOffPolicyDetails`; `Company.TimeOff.TimeOffRequests`: `CompanyTimeOffTimeOffRequests`; `Contractor.Address`: `ContractorAddress`; `Contractor.ContractorList`: `ContractorContractorList`; `Contractor.Landing`: `ContractorLanding`; `Contractor.NewHireReport`: `ContractorNewHireReport`; `Contractor.PaymentMethod`: `ContractorPaymentMethod`; `Contractor.Payments.CreatePayment`: `ContractorPaymentsCreatePayment`; `Contractor.Payments.PaymentHistory`: `ContractorPaymentsPaymentHistory`; `Contractor.Payments.PaymentsList`: `ContractorPaymentsPaymentsList`; `Contractor.Payments.PaymentStatement`: `ContractorPaymentsPaymentStatement`; `Contractor.Payments.PaymentSummary`: `ContractorPaymentsPaymentSummary`; `Contractor.Profile`: `ContractorProfile`; `Contractor.Submit`: `ContractorSubmit`; `Employee.BankAccount`: `EmployeeBankAccount`; `Employee.BankFormBody`: `EmployeeBankFormBody`; `Employee.Compensation`: `EmployeeCompensation`; `Employee.Dashboard`: `EmployeeDashboard`; `Employee.Deductions`: `EmployeeDeductions`; `Employee.DeductionsForm`: `EmployeeDeductionsForm`; `Employee.DocumentManager`: `EmployeeDocumentManager`; `Employee.DocumentSigner`: `EmployeeDocumentSigner`; `Employee.EmployeeDocuments`: `EmployeeEmployeeDocuments`; `Employee.EmployeeList`: `EmployeeEmployeeList`; `Employee.EmploymentEligibility`: `EmployeeEmploymentEligibility`; `Employee.FederalTaxes`: `EmployeeFederalTaxes`; `Employee.FederalTaxesView`: `EmployeeFederalTaxesView`; `Employee.HomeAddress`: `EmployeeHomeAddress`; `Employee.I9SignatureForm`: `EmployeeI9SignatureForm`; `Employee.Landing`: `EmployeeLanding`; `Employee.Management.Compensation`: `EmployeeManagementCompensation`; `Employee.Management.Deductions`: `EmployeeManagementDeductions`; `Employee.Management.Documents`: `EmployeeManagementDocuments`; `Employee.Management.FederalTaxes`: `EmployeeManagementFederalTaxes`; `Employee.Management.HomeAddress`: `EmployeeManagementHomeAddress`; `Employee.Management.PaymentMethod`: `EmployeeManagementPaymentMethod`; `Employee.Management.PaymentMethodBankForm`: `EmployeeManagementPaymentMethodBankForm`; `Employee.Management.PaymentMethodSplitForm`: `EmployeeManagementPaymentMethodSplitForm`; `Employee.Management.Paystubs`: `EmployeeManagementPaystubs`; `Employee.Management.Profile`: `EmployeeManagementProfile`; `Employee.Management.StateTaxes`: `EmployeeManagementStateTaxes`; `Employee.Management.WorkAddress`: `EmployeeManagementWorkAddress`; `Employee.ManagementEmployeeList`: `EmployeeManagementEmployeeList`; `Employee.OnboardingSummary`: `EmployeeOnboardingSummary`; `Employee.PaymentMethod`: `EmployeePaymentMethod`; `Employee.PaySchedules`: `EmployeePaySchedules`; `Employee.Profile`: `EmployeeProfile`; `Employee.SplitPaycheck`: `EmployeeSplitPaycheck`; `Employee.SplitPaymentsFormBody`: `EmployeeSplitPaymentsFormBody`; `Employee.StateTaxes`: `EmployeeStateTaxes`; `Employee.StateTaxesView`: `EmployeeStateTaxesView`; `Employee.Terminations.TerminateEmployee`: `EmployeeTerminationsTerminateEmployee`; `Employee.Terminations.TerminationFlow`: `EmployeeTerminationsTerminationFlow`; `Employee.Terminations.TerminationSummary`: `EmployeeTerminationsTerminationSummary`; `InformationRequests`: `InformationRequests`; `InformationRequests.InformationRequestForm`: `InformationRequestsInformationRequestForm`; `InformationRequests.InformationRequestList`: `InformationRequestsInformationRequestList`; `Payroll.Common`: `PayrollCommon`; `Payroll.ConfirmWireDetailsBanner`: `PayrollConfirmWireDetailsBanner`; `Payroll.ConfirmWireDetailsForm`: `PayrollConfirmWireDetailsForm`; `Payroll.Dismissal`: `PayrollDismissal`; `Payroll.EmployeeSelection`: `PayrollEmployeeSelection`; `Payroll.GrossUpModal`: `PayrollGrossUpModal`; `Payroll.OffCycle`: `PayrollOffCycle`; `Payroll.OffCycleCreation`: `PayrollOffCycleCreation`; `Payroll.OffCycleDeductionsSetting`: `PayrollOffCycleDeductionsSetting`; `Payroll.OffCyclePayPeriodDateForm`: `PayrollOffCyclePayPeriodDateForm`; `Payroll.OffCycleReasonSelection`: `PayrollOffCycleReasonSelection`; `Payroll.OffCycleTaxWithholding`: `PayrollOffCycleTaxWithholding`; `Payroll.PayrollBlocker`: `PayrollPayrollBlocker`; `Payroll.PayrollConfiguration`: `PayrollPayrollConfiguration`; `Payroll.PayrollEditEmployee`: `PayrollPayrollEditEmployee`; `Payroll.PayrollFlow`: `PayrollPayrollFlow`; `Payroll.PayrollHistory`: `PayrollPayrollHistory`; `Payroll.PayrollLanding`: `PayrollPayrollLanding`; `Payroll.PayrollList`: `PayrollPayrollList`; `Payroll.PayrollOverview`: `PayrollPayrollOverview`; `Payroll.PayrollReceipts`: `PayrollPayrollReceipts`; `Payroll.RecoveryCasesList`: `PayrollRecoveryCasesList`; `Payroll.RecoveryCasesResubmit`: `PayrollRecoveryCasesResubmit`; `Payroll.Transition`: `PayrollTransition`; `Payroll.TransitionCreation`: `PayrollTransitionCreation`; `Payroll.TransitionPayrollAlert`: `PayrollTransitionPayrollAlert`; `Payroll.WireInstructions`: `PayrollWireInstructions`; \}\>\> | Translation overrides keyed by language and i18next namespace. Strings supplied here replace the SDK defaults for the matching keys. | +| `dictionary?` | `Record`\<`"en"`, `Partial`\<\{ `common`: `common`; `Company.Addresses`: `CompanyAddresses`; `Company.AssignSignatory`: `CompanyAssignSignatory`; `Company.BankAccount`: `CompanyBankAccount`; `Company.DocumentList`: `CompanyDocumentList`; `Company.FederalTaxes`: `CompanyFederalTaxes`; `Company.Industry`: `CompanyIndustry`; `Company.Locations`: `CompanyLocations`; `Company.OnboardingOverview`: `CompanyOnboardingOverview`; `Company.PaySchedule`: `CompanyPaySchedule`; `Company.SignatureForm`: `CompanySignatureForm`; `Company.StateTaxes`: `CompanyStateTaxes`; `Company.TimeOff.CreateTimeOffPolicy`: `CompanyTimeOffCreateTimeOffPolicy`; `Company.TimeOff.EmployeeTable`: `CompanyTimeOffEmployeeTable`; `Company.TimeOff.HolidayPolicy`: `CompanyTimeOffHolidayPolicy`; `Company.TimeOff.PolicyDetail`: `CompanyTimeOffPolicyDetail`; `Company.TimeOff.SelectEmployees`: `CompanyTimeOffSelectEmployees`; `Company.TimeOff.SelectPolicyType`: `CompanyTimeOffSelectPolicyType`; `Company.TimeOff.TimeOffPolicies`: `CompanyTimeOffTimeOffPolicies`; `Company.TimeOff.TimeOffPolicyDetails`: `CompanyTimeOffTimeOffPolicyDetails`; `Company.TimeOff.TimeOffRequests`: `CompanyTimeOffTimeOffRequests`; `Contractor.Address`: `ContractorAddress`; `Contractor.ContractorList`: `ContractorContractorList`; `Contractor.DocumentsList`: `ContractorDocumentsList`; `Contractor.Landing`: `ContractorLanding`; `Contractor.NewHireReport`: `ContractorNewHireReport`; `Contractor.PaymentMethod`: `ContractorPaymentMethod`; `Contractor.Payments.CreatePayment`: `ContractorPaymentsCreatePayment`; `Contractor.Payments.PaymentHistory`: `ContractorPaymentsPaymentHistory`; `Contractor.Payments.PaymentsList`: `ContractorPaymentsPaymentsList`; `Contractor.Payments.PaymentStatement`: `ContractorPaymentsPaymentStatement`; `Contractor.Payments.PaymentSummary`: `ContractorPaymentsPaymentSummary`; `Contractor.Profile`: `ContractorProfile`; `Contractor.SignatureForm`: `ContractorSignatureForm`; `Contractor.Submit`: `ContractorSubmit`; `Employee.BankAccount`: `EmployeeBankAccount`; `Employee.BankFormBody`: `EmployeeBankFormBody`; `Employee.Compensation`: `EmployeeCompensation`; `Employee.Dashboard`: `EmployeeDashboard`; `Employee.Deductions`: `EmployeeDeductions`; `Employee.DeductionsForm`: `EmployeeDeductionsForm`; `Employee.DocumentManager`: `EmployeeDocumentManager`; `Employee.DocumentSigner`: `EmployeeDocumentSigner`; `Employee.EmployeeDocuments`: `EmployeeEmployeeDocuments`; `Employee.EmployeeList`: `EmployeeEmployeeList`; `Employee.EmploymentEligibility`: `EmployeeEmploymentEligibility`; `Employee.FederalTaxes`: `EmployeeFederalTaxes`; `Employee.FederalTaxesView`: `EmployeeFederalTaxesView`; `Employee.HomeAddress`: `EmployeeHomeAddress`; `Employee.I9SignatureForm`: `EmployeeI9SignatureForm`; `Employee.Landing`: `EmployeeLanding`; `Employee.Management.Compensation`: `EmployeeManagementCompensation`; `Employee.Management.Deductions`: `EmployeeManagementDeductions`; `Employee.Management.Documents`: `EmployeeManagementDocuments`; `Employee.Management.FederalTaxes`: `EmployeeManagementFederalTaxes`; `Employee.Management.HomeAddress`: `EmployeeManagementHomeAddress`; `Employee.Management.PaymentMethod`: `EmployeeManagementPaymentMethod`; `Employee.Management.PaymentMethodBankForm`: `EmployeeManagementPaymentMethodBankForm`; `Employee.Management.PaymentMethodSplitForm`: `EmployeeManagementPaymentMethodSplitForm`; `Employee.Management.Paystubs`: `EmployeeManagementPaystubs`; `Employee.Management.Profile`: `EmployeeManagementProfile`; `Employee.Management.StateTaxes`: `EmployeeManagementStateTaxes`; `Employee.Management.WorkAddress`: `EmployeeManagementWorkAddress`; `Employee.ManagementEmployeeList`: `EmployeeManagementEmployeeList`; `Employee.OnboardingSummary`: `EmployeeOnboardingSummary`; `Employee.PaymentMethod`: `EmployeePaymentMethod`; `Employee.PaySchedules`: `EmployeePaySchedules`; `Employee.Profile`: `EmployeeProfile`; `Employee.SplitPaycheck`: `EmployeeSplitPaycheck`; `Employee.SplitPaymentsFormBody`: `EmployeeSplitPaymentsFormBody`; `Employee.StateTaxes`: `EmployeeStateTaxes`; `Employee.StateTaxesView`: `EmployeeStateTaxesView`; `Employee.Terminations.TerminateEmployee`: `EmployeeTerminationsTerminateEmployee`; `Employee.Terminations.TerminationFlow`: `EmployeeTerminationsTerminationFlow`; `Employee.Terminations.TerminationSummary`: `EmployeeTerminationsTerminationSummary`; `InformationRequests`: `InformationRequests`; `InformationRequests.InformationRequestForm`: `InformationRequestsInformationRequestForm`; `InformationRequests.InformationRequestList`: `InformationRequestsInformationRequestList`; `Payroll.Common`: `PayrollCommon`; `Payroll.ConfirmWireDetailsBanner`: `PayrollConfirmWireDetailsBanner`; `Payroll.ConfirmWireDetailsForm`: `PayrollConfirmWireDetailsForm`; `Payroll.Dismissal`: `PayrollDismissal`; `Payroll.EmployeeSelection`: `PayrollEmployeeSelection`; `Payroll.GrossUpModal`: `PayrollGrossUpModal`; `Payroll.OffCycle`: `PayrollOffCycle`; `Payroll.OffCycleCreation`: `PayrollOffCycleCreation`; `Payroll.OffCycleDeductionsSetting`: `PayrollOffCycleDeductionsSetting`; `Payroll.OffCyclePayPeriodDateForm`: `PayrollOffCyclePayPeriodDateForm`; `Payroll.OffCycleReasonSelection`: `PayrollOffCycleReasonSelection`; `Payroll.OffCycleTaxWithholding`: `PayrollOffCycleTaxWithholding`; `Payroll.PayrollBlocker`: `PayrollPayrollBlocker`; `Payroll.PayrollConfiguration`: `PayrollPayrollConfiguration`; `Payroll.PayrollEditEmployee`: `PayrollPayrollEditEmployee`; `Payroll.PayrollFlow`: `PayrollPayrollFlow`; `Payroll.PayrollHistory`: `PayrollPayrollHistory`; `Payroll.PayrollLanding`: `PayrollPayrollLanding`; `Payroll.PayrollList`: `PayrollPayrollList`; `Payroll.PayrollOverview`: `PayrollPayrollOverview`; `Payroll.PayrollReceipts`: `PayrollPayrollReceipts`; `Payroll.RecoveryCasesList`: `PayrollRecoveryCasesList`; `Payroll.RecoveryCasesResubmit`: `PayrollRecoveryCasesResubmit`; `Payroll.Transition`: `PayrollTransition`; `Payroll.TransitionCreation`: `PayrollTransitionCreation`; `Payroll.TransitionPayrollAlert`: `PayrollTransitionPayrollAlert`; `Payroll.WireInstructions`: `PayrollWireInstructions`; \}\>\> | Translation overrides keyed by language and i18next namespace. Strings supplied here replace the SDK defaults for the matching keys. | | `lng?` | `string` | Active i18next language. Defaults to `'en'`. | | `LoaderComponent?` | (`__namedParameters`) => `Element` | Loading indicator rendered while SDK queries are pending. Overrides the SDK default spinner. | | `locale?` | `string` | BCP 47 locale used for number, date, and currency formatting throughout the SDK. Defaults to `'en-US'`. | diff --git a/docs/reference/utilities.md b/docs/reference/utilities.md index b5a6e8a1d..3c5398785 100644 --- a/docs/reference/utilities.md +++ b/docs/reference/utilities.md @@ -249,6 +249,7 @@ parsed values (or `undefined` if invalid). - [`UseSignEmployeeFormReady`](employee/hooks/use-sign-employee-form.md#usesignemployeeformready) - [`UsePayScheduleFormReady`](company/hooks/use-pay-schedule-form.md#usepayscheduleformready) - [`UseSignCompanyFormReady`](company/hooks/use-sign-company-form.md#usesigncompanyformready) +- [`UseContractorSignatureFormReady`](contractor/hooks/use-contractor-signature-form.md#usecontractorsignatureformready) #### Type Parameters @@ -405,6 +406,7 @@ components to render labels, inline validation, and bounded date pickers. | `isRequired?` | `boolean` | Whether the field must have a value for the form to submit. | | `maxDate?` | `string` \| `null` | ISO date string upper bound for date picker fields. Set by hooks; consumed by DatePickerHookField. | | `minDate?` | `string` \| `null` | ISO date string lower bound for date picker fields. Set by hooks; consumed by DatePickerHookField. | +| `placeholder?` | `string` | Placeholder text a hook supplies for the field (e.g. a masked value to display while the input is empty). | *** @@ -442,6 +444,7 @@ from so callers can read additional attributes off the originating record. | `isRequired?` | `boolean` | Whether the field must have a value for the form to submit. | | `maxDate?` | `string` \| `null` | ISO date string upper bound for date picker fields. Set by hooks; consumed by DatePickerHookField. | | `minDate?` | `string` \| `null` | ISO date string lower bound for date picker fields. Set by hooks; consumed by DatePickerHookField. | +| `placeholder?` | `string` | Placeholder text a hook supplies for the field (e.g. a masked value to display while the input is empty). | ***