From 6a430bf383216c9054df646badc4a2a0b2b70c6e Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Mon, 1 Jun 2026 15:28:26 -0700 Subject: [PATCH 1/6] ci: complete Docusaurus publishing pipeline Adds the release-snapshot + post-NPM-publish bits that pair with the Docusaurus site stood up in #1943: - ci.yaml: docs-build job validates the site compiles (onBrokenLinks / onBrokenAnchors: throw) at PR time rather than at release time. - prepare-release.yaml: snapshots docs for the released version via docusaurus docs:version and folds the snapshot into the release commit (idempotent so workflow reruns on the same release PR are safe). - publish-docs.yaml: fires on successful Publish to NPM; builds the site and pushes it to Gusto/embedded-sdk-docs gh-pages. Gated on the repo variable DOCS_PUBLISH_ENABLED so it lands dormant; activation runbook (GitHub App install, secrets, var, Pages enablement) is in the file header. - docusaurus.config.ts: points url at the obfuscated *.pages.github.io subdomain GitHub auto-assigns to internal repos as an interim home; swap to https://sdk.gusto.com when SDK-831 provisions the custom domain. - docs-site/versions.json + versioned_{docs,sidebars}/version-0.46.3: one-time bootstrap of the version dropdown at the currently released version. Subsequent releases self-snapshot via prepare-release.yaml. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yaml | 20 + .github/workflows/prepare-release.yaml | 23 + .github/workflows/publish-docs.yaml | 99 + docs-site/docusaurus.config.ts | 6 +- docs-site/versioned_docs/.gitkeep | 0 .../version-0.46.3/chameleon.jpg | Bin 0 -> 93386 bytes .../component-adapter-faq.md | 129 + .../component-adapter-types.md | 39 + .../component-adapter/component-adapter.md | 20 + .../component-adapter/component-inventory.md | 707 +++++ .../how-the-component-adapter-works.md | 101 + .../setting-up-your-component-adapter.md | 258 ++ .../build-pathways-sdk-flows-api.md | 22 + .../component-types.md | 17 + .../deciding-to-build-with-the-sdk.md | 29 + .../getting-started/authentication.md | 74 + .../getting-started/getting-started.md | 78 + .../proxy-security-partner-guidance.md | 87 + .../version-0.46.3/hooks/hooks.md | 923 +++++++ .../hooks/jobs-and-compensations.md | 209 ++ .../version-0.46.3/hooks/useBankForm.md | 269 ++ .../hooks/useChildSupportGarnishmentForm.md | 210 ++ .../hooks/useCompensationForm.md | 509 ++++ .../version-0.46.3/hooks/useDeductionForm.md | 186 ++ .../hooks/useEmployeeDetailsForm.md | 609 +++++ .../hooks/useEmployeeStateTaxesForm.md | 420 +++ .../hooks/useFederalTaxesForm.md | 462 ++++ .../version-0.46.3/hooks/useJobForm.md | 338 +++ .../hooks/usePayScheduleForm.md | 582 ++++ .../hooks/usePaymentMethodForm.md | 227 ++ .../hooks/useSignCompanyForm.md | 306 +++ .../hooks/useSignEmployeeForm.md | 712 +++++ .../hooks/useSplitPaymentsForm.md | 294 ++ .../hooks/useWorkAddressForm.md | 387 +++ .../integration-guide/composition.md | 60 + .../integration-guide/customizing-sdk-ui.md | 92 + .../integration-guide/error-handling.md | 81 + .../integration-guide/event-handling.md | 60 + .../integration-guide/event-types.md | 109 + .../integration-guide/integration-guide.md | 17 + .../observability-examples.md | 251 ++ .../integration-guide/observability.md | 337 +++ .../providing-your-own-data.md | 48 + .../integration-guide/request-interceptors.md | 76 + .../integration-guide/routing.md | 296 ++ .../integration-guide/translation.md | 58 + .../integration-guide/versioning.md | 26 + .../reference/endpoint-inventory.json | 2422 +++++++++++++++++ .../reference/endpoint-reference.md | 504 ++++ .../reference/jobs-and-compensations.md | 764 ++++++ .../reference/proxy-examples.md | 118 + .../version-0.46.3/theming/theme-variables.md | 94 + .../version-0.46.3/theming/theming-guide.md | 72 + .../version-0.46.3/theming/theming.md | 14 + .../what-is-the-gep-react-sdk.md | 18 + .../workflows-overview/company-onboarding.md | 549 ++++ .../contractor-onboarding.md | 225 ++ .../workflows-overview/contractor-payments.md | 248 ++ .../workflows-overview/dismissal-payroll.md | 132 + .../workflows-overview/employee-dashboard.md | 400 +++ .../employee-onboarding.md | 406 +++ .../employee-self-onboarding.md | 254 ++ .../employee-termination.md | 250 ++ .../information-requests.md | 142 + .../workflows-overview/off-cycle-payroll.md | 173 ++ .../workflows-overview/run-payroll.md | 441 +++ .../workflows-overview/time-off.md | 590 ++++ .../workflows-overview/transition-payroll.md | 177 ++ .../workflows-overview/workflows-overview.md | 55 + docs-site/versioned_sidebars/.gitkeep | 0 .../version-0.46.3-sidebars.json | 123 + docs-site/versions.json | 1 + 72 files changed, 18034 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish-docs.yaml create mode 100644 docs-site/versioned_docs/.gitkeep create mode 100644 docs-site/versioned_docs/version-0.46.3/chameleon.jpg create mode 100644 docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter-faq.md create mode 100644 docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter-types.md create mode 100644 docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter.md create mode 100644 docs-site/versioned_docs/version-0.46.3/component-adapter/component-inventory.md create mode 100644 docs-site/versioned_docs/version-0.46.3/component-adapter/how-the-component-adapter-works.md create mode 100644 docs-site/versioned_docs/version-0.46.3/component-adapter/setting-up-your-component-adapter.md create mode 100644 docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/build-pathways-sdk-flows-api.md create mode 100644 docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/component-types.md create mode 100644 docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/deciding-to-build-with-the-sdk.md create mode 100644 docs-site/versioned_docs/version-0.46.3/getting-started/authentication.md create mode 100644 docs-site/versioned_docs/version-0.46.3/getting-started/getting-started.md create mode 100644 docs-site/versioned_docs/version-0.46.3/getting-started/proxy-security-partner-guidance.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/hooks.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/jobs-and-compensations.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useBankForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useChildSupportGarnishmentForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useCompensationForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useDeductionForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeDetailsForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeStateTaxesForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useFederalTaxesForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useJobForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/usePayScheduleForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/usePaymentMethodForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useSignCompanyForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useSignEmployeeForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useSplitPaymentsForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/hooks/useWorkAddressForm.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/composition.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/customizing-sdk-ui.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/error-handling.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/event-handling.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/event-types.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/integration-guide.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/observability-examples.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/observability.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/providing-your-own-data.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/request-interceptors.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/routing.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/translation.md create mode 100644 docs-site/versioned_docs/version-0.46.3/integration-guide/versioning.md create mode 100644 docs-site/versioned_docs/version-0.46.3/reference/endpoint-inventory.json create mode 100644 docs-site/versioned_docs/version-0.46.3/reference/endpoint-reference.md create mode 100644 docs-site/versioned_docs/version-0.46.3/reference/jobs-and-compensations.md create mode 100644 docs-site/versioned_docs/version-0.46.3/reference/proxy-examples.md create mode 100644 docs-site/versioned_docs/version-0.46.3/theming/theme-variables.md create mode 100644 docs-site/versioned_docs/version-0.46.3/theming/theming-guide.md create mode 100644 docs-site/versioned_docs/version-0.46.3/theming/theming.md create mode 100644 docs-site/versioned_docs/version-0.46.3/what-is-the-gep-react-sdk.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/company-onboarding.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/contractor-onboarding.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/contractor-payments.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/dismissal-payroll.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/employee-dashboard.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/employee-onboarding/employee-onboarding.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/employee-onboarding/employee-self-onboarding.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/employee-termination.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/information-requests.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/off-cycle-payroll.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/run-payroll.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/time-off.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/transition-payroll.md create mode 100644 docs-site/versioned_docs/version-0.46.3/workflows-overview/workflows-overview.md create mode 100644 docs-site/versioned_sidebars/.gitkeep create mode 100644 docs-site/versioned_sidebars/version-0.46.3-sidebars.json create mode 100644 docs-site/versions.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4e0e3f502..766a4bb18 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -167,6 +167,26 @@ jobs: - name: Build Storybook run: npm run build-storybook + # Docs build job: validates the Docusaurus site compiles, catching broken + # links/anchors (onBrokenLinks: 'throw') at PR time rather than at release + # time. Uses its own node_modules under docs-site/ — not the root cache. + docs-build: + needs: setup + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + + - name: Install docs dependencies + working-directory: docs-site + run: npm ci + + - name: Build docs + run: npm run docs:build + # SDK app build job: validates the dev app's vite config and entry compile sdk-app-build: needs: setup diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 9372da679..fe4f1e5e1 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -51,6 +51,29 @@ jobs: npx release-it --ci fi + - name: Install docs dependencies + working-directory: docs-site + run: npm ci + + - name: Snapshot docs for this version + working-directory: docs-site + run: | + VERSION=$(node -p "require('../package.json').version") + # Idempotent: re-running the workflow on the same release PR should + # not error on an existing snapshot. + rm -rf "versioned_docs/version-$VERSION" "versioned_sidebars/version-$VERSION-sidebars.json" + if [ -f versions.json ]; then + node -e "const v=require('./versions.json').filter(x=>x!=='$VERSION');require('fs').writeFileSync('./versions.json',JSON.stringify(v,null,2)+'\n')" + fi + npx docusaurus docs:version "$VERSION" + + - name: Fold docs snapshot into the release commit + run: | + git add docs-site/versions.json docs-site/versioned_docs docs-site/versioned_sidebars + if ! git diff --cached --quiet; then + git commit --amend --no-edit + fi + - name: Push branch and open PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml new file mode 100644 index 000000000..15f41f628 --- /dev/null +++ b/.github/workflows/publish-docs.yaml @@ -0,0 +1,99 @@ +name: Publish Docs + +# Fires after a successful NPM publish on main so the docs version on the +# public site always matches what was actually released. Lives in its own +# workflow file (rather than appended to publish.yaml) so a docs failure +# does not fail the NPM publish, and vice versa. +# +# Activation gate: the `publish-docs` job is gated on the repo variable +# DOCS_PUBLISH_ENABLED == 'true'. While that variable is unset, every +# release skips this job cleanly (no failed-check noise). To turn the +# pipeline on: +# 1. Provision the cross-repo publishing GitHub App and install it on +# Gusto/embedded-sdk-docs with `contents: write` + `metadata: read`. +# 2. Add repo secrets DOCS_PUBLISH_APP_ID and DOCS_PUBLISH_APP_PRIVATE_KEY +# to Gusto/embedded-react-sdk. +# 3. Add repo variable DOCS_PUBLISH_ENABLED=true. +# 4. Enable GitHub Pages on Gusto/embedded-sdk-docs (branch: gh-pages, /). +# The manual seed of Gusto/embedded-sdk-docs keeps the live site current +# until step 4 is done. + +on: + workflow_run: + workflows: [Publish to NPM] + types: [completed] + branches: [main] + workflow_dispatch: + +concurrency: + group: publish-docs + cancel-in-progress: false + +jobs: + publish-docs: + # Dormant until DOCS_PUBLISH_ENABLED is set (see header comment). + # workflow_dispatch still honors the gate so the job can't be triggered + # by hand into a missing-secrets failure either. + if: | + vars.DOCS_PUBLISH_ENABLED == 'true' && + (github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success') + runs-on: + group: gusto-ubuntu-default + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + + - name: Install docs dependencies + working-directory: docs-site + run: npm ci + + - name: Build docs + working-directory: docs-site + run: npm run build + + - name: Mint GitHub App token for target repo + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.DOCS_PUBLISH_APP_ID }} + private-key: ${{ secrets.DOCS_PUBLISH_APP_PRIVATE_KEY }} + owner: Gusto + repositories: embedded-sdk-docs + + - name: Checkout target repo (gh-pages branch) + uses: actions/checkout@v6 + with: + repository: Gusto/embedded-sdk-docs + ref: gh-pages + token: ${{ steps.app-token.outputs.token }} + path: target-repo + + - name: Sync built site into target repo + run: | + # Preserve .git, .nojekyll, and any CNAME; replace everything else. + find target-repo -mindepth 1 -maxdepth 1 \ + ! -name .git ! -name .nojekyll ! -name CNAME -exec rm -rf {} + + cp -R docs-site/build/. target-repo/ + # .nojekyll is required so GH Pages does not run Jekyll, which + # strips the `_`-prefixed asset directories Docusaurus emits. + touch target-repo/.nojekyll + + - name: Commit and push + working-directory: target-repo + run: | + git config user.name "gusto-docs-publisher[bot]" + git config user.email "gusto-docs-publisher[bot]@users.noreply.github.com" + if [ -z "$(git status --porcelain)" ]; then + echo "No changes to publish." + exit 0 + fi + SDK_VERSION=$(node -p "require('../package.json').version") + git add -A + git commit -m "docs: publish v$SDK_VERSION ($GITHUB_SHA)" + git push origin gh-pages diff --git a/docs-site/docusaurus.config.ts b/docs-site/docusaurus.config.ts index cbbd7cfaa..003804bc9 100644 --- a/docs-site/docusaurus.config.ts +++ b/docs-site/docusaurus.config.ts @@ -7,8 +7,12 @@ const config: Config = { tagline: 'Embedded Payroll React SDK Documentation', favicon: 'img/favicon.svg', - url: 'http://localhost', + // Interim: site is served at the obfuscated *.pages.github.io subdomain + // GitHub auto-assigns to private/internal repos. Swap to https://sdk.gusto.com + // once the custom domain is provisioned (SDK-831). + url: 'https://expert-adventure-1q5rvvv.pages.github.io', baseUrl: '/', + trailingSlash: false, onBrokenLinks: 'throw', onBrokenAnchors: 'throw', diff --git a/docs-site/versioned_docs/.gitkeep b/docs-site/versioned_docs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docs-site/versioned_docs/version-0.46.3/chameleon.jpg b/docs-site/versioned_docs/version-0.46.3/chameleon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8fc7659149d93f2da43ce4a994b6fd62df4078a5 GIT binary patch literal 93386 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 oKZ;bAX_;nId8 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 , + TextInput: props => , +} +``` + +### Implementing Components + +#### How do I implement components with complex behavior like ComboBox or DatePicker? + +For complex components, you have a few options: + +1. Use a third-party library that provides similar functionality +2. Implement a simplified version that meets your specific needs +3. Use the SDK's default implementation for complex components while customizing simpler ones + +Instead of creating these complex components from scratch, we recommend referencing our default implementations to understand how we've structured them: + +- [ComboBox implementation](https://github.com/Gusto/embedded-react-sdk/blob/main/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx) +- [DatePicker implementation](https://github.com/Gusto/embedded-react-sdk/blob/main/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx) + +These implementations demonstrate how to properly handle accessibility, state management, and the various props required by each component. + +To understand the expected behavior of complex components, you can also refer to their interface definitions: + +- [ComboBox on GitHub](https://github.com/Gusto/embedded-react-sdk/blob/main/src/components/Common/UI/ComboBox/ComboBox.tsx) +- [DatePicker on GitHub](https://github.com/Gusto/embedded-react-sdk/blob/main/src/components/Common/UI/DatePicker/DatePicker.tsx) + +#### My custom component isn't working correctly. What should I check? + +1. **Ensure you've implemented all required props**: Each component has a specific set of props that it expects to receive and use. Make sure your component handles all of these props correctly. + +2. **Check event handlers**: Pay special attention to event handlers like `onChange` and `onBlur`. The SDK expects these to be called with specific parameters. + +3. **Verify accessibility**: The SDK's default components are built with accessibility in mind. Ensure your custom components maintain these accessibility features. + +4. **Use the right types**: Make sure you're importing and using the correct prop types from the SDK. + +5. **Debug with React DevTools**: Use React DevTools to inspect the props being passed to your components and compare with what you're expecting. + +#### How can I ensure my custom components maintain accessibility features? + +**Important**: When using the Component Adapter with your own custom components, you are responsible for ensuring accessibility compliance. The SDK's default components are built with accessibility in mind, but this accessibility is not automatically transferred to your custom implementations. + +When implementing custom components, pay attention to: + +- Proper labeling of form controls +- Appropriate ARIA attributes +- Keyboard navigation +- Focus management +- Color contrast + +The prop interfaces include properties like `aria-describedby`, `isInvalid`, and others that support accessibility. Make sure your custom implementations use these props correctly. + +Study the default components to understand how they handle accessibility concerns. Check your own design system's accessibility guidelines and components, which likely have built-in accessibility features you can leverage. + +### Troubleshooting + +#### I'm getting errors about missing components. What's wrong? + +If you're seeing errors about missing components and you're using `GustoProviderCustomUIAdapter`, you must implement all components that your integration needs. With the custom provider, you're responsible for providing every component used by the SDK in your adapter. + +We recommend using `GustoProvider` instead if you only want to customize a few components. The `GustoProvider` automatically includes all default components and allows you to override just the specific components you want to customize, requiring less work from your end. + +You can check which components are being used by examining the SDK's source code or by adding console logs to your adapter implementation. + +#### My form values aren't being captured correctly. What might be wrong? + +This often happens when the `onChange` handler in your custom form components isn't being called with the correct parameters. The SDK expects specific value formats from each component's onChange handler: + +- Checkbox: `onChange(boolean)` +- DatePicker: `onChange(Date | null)` +- NumberInput: `onChange(number)` +- Select: `onChange(string)` +- TextInput: `onChange(string)` + +Make sure your components are calling these handlers with the expected data types. + +#### How can I test my component adapter? + +You can create unit tests for your custom components using testing libraries like Vitest and React Testing Library. Test that your components: + +1. Render correctly with various prop combinations +2. Call event handlers with the correct parameters +3. Handle state changes appropriately +4. Maintain accessibility + +For examples of how the SDK tests its components, you can look at the test files located alongside each component in the UI directory. For instance, check out [Button.test.tsx](https://github.com/Gusto/embedded-react-sdk/blob/main/src/components/Common/UI/Button/Button.test.tsx), [TextInput.test.tsx](https://github.com/Gusto/embedded-react-sdk/blob/main/src/components/Common/UI/TextInput/TextInput.test.tsx), and other test files in the [UI component directories](https://github.com/Gusto/embedded-react-sdk/tree/main/src/components/Common/UI). + +#### Can I contribute my component adapter back to the project? + +If you've created a component adapter for a popular UI library (like Material UI, Chakra UI, etc.), we'd love to hear about it! While the Gusto Embedded React SDK doesn't directly maintain adapters for third-party libraries, we can help guide other users to community solutions. + +Contact your Gusto Embedded representative if you'd like to share your adapter implementation with other partners. + +[Back to Component Adapter Overview](./component-adapter.md) diff --git a/docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter-types.md b/docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter-types.md new file mode 100644 index 000000000..857028f8d --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter-types.md @@ -0,0 +1,39 @@ +--- +title: Component Adapter Types +order: 4 +--- + +## Component Adapter Types + +The Component Adapter system uses TypeScript interfaces to ensure type safety and consistent behavior. This document provides links to the type definitions you'll need when implementing custom components. + +### Core Types + +- [Component Inventory](./component-inventory.md) - Individual component prop interfaces +- [`ComponentsContextType`](https://github.com/Gusto/embedded-react-sdk/blob/main/src/contexts/ComponentAdapter/useComponentContext.ts) - The main interface defining all customizable UI components +- [`GustoProviderCustomUIAdapterProps`](https://github.com/Gusto/embedded-react-sdk/blob/main/src/contexts/GustoProvider/GustoProviderCustomUIAdapter.tsx) - Props for the custom UI adapter + +### Importing Types + +All types are exported from the SDK package: + +```typescript +import type { + ComponentsContextType, + ButtonProps, + TextInputProps, + // ... other types as needed +} from '@gusto/embedded-react-sdk' +``` + +### Type Safety + +The Component Adapter system leverages TypeScript to ensure type safety: + +1. **Compile-time checking**: TypeScript will flag any missing or incorrect props in your component implementations +2. **IDE support**: You get autocomplete and documentation for all required props +3. **Type inference**: TypeScript can infer the types of your event handlers and other callback functions + +For implementation examples and getting started guidance, see the [Setup Guide](./setting-up-your-component-adapter.md). + +[Back to Component Adapter Overview](./component-adapter.md) diff --git a/docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter.md b/docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter.md new file mode 100644 index 000000000..9a0b84aea --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/component-adapter/component-adapter.md @@ -0,0 +1,20 @@ +--- +title: Component Adapter +order: 5 +--- + +## Component Adapter + +The Component Adapter system provides a powerful way to customize the UI components used throughout the Gusto Embedded React SDK. This feature allows you to replace the default SDK UI components with your own UI components while maintaining all the SDK's functionality. + +> Component adapters are powerful but can be complicated to set up and involve higher maintenance overhead. It is recommended to start with [theming](../theming/theming-guide.md) and then only use component adapters as needed. + +The Component Adapter provides a "bring your own UI" approach, enabling seamless integration with your existing design system while maintaining all the business logic and functionality of the SDK. + +| Section | Description | +| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| [How the Component Adapter Works](./how-the-component-adapter-works.md) | Learn about the architecture and principles behind the Component Adapter system | +| [Setting Up Your Component Adapter](./setting-up-your-component-adapter.md) | Step-by-step instructions for creating and implementing your own Component Adapter | +| [Component Adapter Types](./component-adapter-types.md) | Details about the TypeScript interfaces used by the Component Adapter system | +| [Component Inventory](./component-inventory.md) | Complete inventory of customizable UI components with prop documentation | +| [FAQ](./component-adapter-faq.md) | Answers to common questions about using the Component Adapter | diff --git a/docs-site/versioned_docs/version-0.46.3/component-adapter/component-inventory.md b/docs-site/versioned_docs/version-0.46.3/component-adapter/component-inventory.md new file mode 100644 index 000000000..82cac25b7 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/component-adapter/component-inventory.md @@ -0,0 +1,707 @@ +# Component Inventory + +- [AlertProps](#alertprops) +- [BadgeProps](#badgeprops) +- [BannerProps](#bannerprops) +- [BaseListProps](#baselistprops) +- [BoxHeaderProps](#boxheaderprops) +- [BoxProps](#boxprops) +- [BreadcrumbsProps](#breadcrumbsprops) + - [Breadcrumb](#breadcrumb) +- [ButtonIconProps](#buttoniconprops) +- [ButtonProps](#buttonprops) +- [CalendarPreviewProps](#calendarpreviewprops) +- [CardProps](#cardprops) +- [CheckboxGroupProps](#checkboxgroupprops) + - [CheckboxGroupOption](#checkboxgroupoption) +- [CheckboxProps](#checkboxprops) +- [ComboBoxProps](#comboboxprops) + - [ComboBoxOption](#comboboxoption) +- [DatePickerProps](#datepickerprops) +- [DescriptionListProps](#descriptionlistprops) + - [DescriptionListItem](#descriptionlistitem) +- [DialogProps](#dialogprops) +- [FileInputProps](#fileinputprops) +- [HeadingProps](#headingprops) +- [LinkProps](#linkprops) +- [LoadingSpinnerProps](#loadingspinnerprops) +- [MenuProps](#menuprops) + - [MenuItem](#menuitem) +- [ModalProps](#modalprops) +- [NumberInputProps](#numberinputprops) +- [OrderedListProps](#orderedlistprops) +- [PaginationControlProps](#paginationcontrolprops) +- [PaginationItemsPerPage](#paginationitemsperpage) +- [PayrollLoadingProps](#payrollloadingprops) +- [ProgressBarProps](#progressbarprops) +- [RadioGroupProps](#radiogroupprops) + - [RadioGroupOption](#radiogroupoption) +- [RadioProps](#radioprops) +- [SelectProps](#selectprops) + - [SelectOption](#selectoption) +- [SwitchProps](#switchprops) +- [TableProps](#tableprops) + - [TableData](#tabledata) + - [TableRow](#tablerow) +- [TabsProps](#tabsprops) + - [TabProps](#tabprops) +- [TextAreaProps](#textareaprops) +- [TextInputProps](#textinputprops) +- [TextProps](#textprops) +- [UnorderedListProps](#unorderedlistprops) + +## AlertProps + +| Prop | Type | Required | Description | +| ------------------------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **status** | `"info" \| "success" \| "warning" \| "error"` | No | The visual status that the alert should convey | +| **label** | `string` | Yes | The label text for the alert | +| **children** | `React.ReactNode` | No | Optional children to be rendered inside the alert | +| **action** | `React.ReactNode` | No | Optional action node (e.g. a Button) rendered inline beside the label, before the dismiss button. Use this for compact alerts that need a single call-to-action next to the heading (e.g. a "Review" button summarising details available in a modal). Multi-line supporting copy should still pass through `children`. | +| **icon** | `React.ReactNode` | No | Optional custom icon component to override the default icon | +| **className** | `string` | No | CSS className to be applied | +| **onDismiss** | `() => void` | No | Optional callback function called when the dismiss button is clicked | +| **disableScrollIntoView** | `boolean` | No | Whether to disable scrolling the alert into view and focusing it on mount. Set to true when using inside modals. | + +## BadgeProps + +| Prop | Type | Required | Description | +| -------------------- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- | +| **children** | `React.ReactNode` | Yes | Content to be displayed inside the badge | +| **status** | `"info" \| "success" \| "warning" \| "error"` | No | Visual style variant of the badge | +| **onDismiss** | `() => void` | No | Optional callback when the dismiss button is clicked. When provided, a dismiss button is rendered inside the badge. | +| **dismissAriaLabel** | `string` | No | Accessible label for the dismiss button | +| **isDisabled** | `boolean` | No | Whether the badge interaction is disabled | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **aria-label** | `string` | No | Defines a string value that labels the current element. | + +## BannerProps + +| Prop | Type | Required | Description | +| -------------- | ---------------------- | -------- | ------------------------------------------------------- | +| **title** | `React.ReactNode` | Yes | Title content displayed in the colored header section | +| **children** | `React.ReactNode` | Yes | Content to be displayed in the main content area | +| **status** | `"warning" \| "error"` | No | Visual status variant of the banner | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **aria-label** | `string` | No | Defines a string value that labels the current element. | + +## BaseListProps + +| Prop | Type | Required | Description | +| -------------------- | ------------------- | -------- | ----------------------------------------- | +| **items** | `React.ReactNode[]` | Yes | The list items to render | +| **className** | `string` | No | Optional custom class name | +| **aria-label** | `string` | No | Accessibility label for the list | +| **aria-labelledby** | `string` | No | ID of an element that labels this list | +| **aria-describedby** | `string` | No | ID of an element that describes this list | + +## BoxHeaderProps + +| Prop | Type | Required | Description | +| ---------------- | ---------------------------------------------- | -------- | ----------- | +| **title** | `React.ReactNode` | Yes | - | +| **description** | `React.ReactNode` | No | - | +| **action** | `React.ReactNode` | No | - | +| **headingLevel** | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | No | - | + +## BoxProps + +| Prop | Type | Required | Description | +| --------------- | ----------------- | -------- | ----------- | +| **children** | `React.ReactNode` | Yes | - | +| **header** | `React.ReactNode` | No | - | +| **footer** | `React.ReactNode` | No | - | +| **withPadding** | `boolean` | No | - | +| **className** | `string` | No | - | + +## BreadcrumbsProps + +| Prop | Type | Required | Description | +| ----------------------- | --------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **breadcrumbs** | [Breadcrumb](#breadcrumb)[] | Yes | Array of breadcrumbs | +| **currentBreadcrumbId** | `string` | No | Current breadcrumb id | +| **aria-label** | `string` | No | Accessibility label for the breadcrumbs | +| **className** | `string` | No | Additional CSS class name for the breadcrumbs container | +| **onClick** | `(id: string) => void` | No | Event handler for breadcrumb navigation | +| **isSmallContainer** | `boolean` | No | Passed to the breadcrumbs when the container size is small (640px and below) At this size, the breadcrumb typically does not have sufficient size to render completely. In our implementation, we switch to a condensed mobile version of the breadcrumbs | + +### Breadcrumb + +| Prop | Type | Required | Description | +| --------------- | ----------------- | -------- | --------------------------------------------------------------------------------------------------- | +| **id** | `string` | Yes | - | +| **label** | `React.ReactNode` | Yes | - | +| **isClickable** | `boolean` | No | When false, the breadcrumb is rendered as plain text even if onClick is provided. Defaults to true. | + +## ButtonIconProps + +| Prop | Type | Required | Description | +| -------------------- | --------------------------------------------------- | -------- | --------------------------------------------------------------------- | +| **buttonRef** | `Ref` | No | React ref for the button element | +| **variant** | `"error" \| "primary" \| "secondary" \| "tertiary"` | No | Visual style variant of the button | +| **isLoading** | `boolean` | No | Shows a loading spinner and disables the button | +| **isDisabled** | `boolean` | No | Disables the button and prevents interaction | +| **icon** | `React.ReactNode` | No | Optional leading icon rendered before children | +| **children** | `React.ReactNode` | No | Content to be rendered inside the button | +| **onBlur** | `(e: React.FocusEvent) => void` | No | Handler for blur events | +| **onFocus** | `(e: React.FocusEvent) => void` | No | Handler for focus events | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **onKeyDown** | `React.KeyboardEventHandler` | No | - | +| **onKeyUp** | `React.KeyboardEventHandler` | No | - | +| **aria-label** | `string` | No | Defines a string value that labels the current element. | +| **aria-labelledby** | `string` | No | Identifies the element (or elements) that labels the current element. | +| **aria-describedby** | `string` | No | Identifies the element (or elements) that describes the object. | +| **title** | `string` | No | - | +| **name** | `string` | No | - | +| **type** | `"submit" \| "reset" \| "button"` | No | - | +| **onClick** | `React.MouseEventHandler` | No | - | +| **form** | `string` | No | - | +| **tabIndex** | `number` | No | - | + +## ButtonProps + +| Prop | Type | Required | Description | +| -------------------- | --------------------------------------------------- | -------- | --------------------------------------------------------------------- | +| **buttonRef** | `Ref` | No | React ref for the button element | +| **variant** | `"error" \| "primary" \| "secondary" \| "tertiary"` | No | Visual style variant of the button | +| **isLoading** | `boolean` | No | Shows a loading spinner and disables the button | +| **isDisabled** | `boolean` | No | Disables the button and prevents interaction | +| **icon** | `React.ReactNode` | No | Optional leading icon rendered before children | +| **children** | `React.ReactNode` | No | Content to be rendered inside the button | +| **onBlur** | `(e: React.FocusEvent) => void` | No | Handler for blur events | +| **onFocus** | `(e: React.FocusEvent) => void` | No | Handler for focus events | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **onKeyDown** | `React.KeyboardEventHandler` | No | - | +| **onKeyUp** | `React.KeyboardEventHandler` | No | - | +| **aria-label** | `string` | No | Defines a string value that labels the current element. | +| **aria-labelledby** | `string` | No | Identifies the element (or elements) that labels the current element. | +| **aria-describedby** | `string` | No | Identifies the element (or elements) that describes the object. | +| **title** | `string` | No | - | +| **name** | `string` | No | - | +| **type** | `"submit" \| "reset" \| "button"` | No | - | +| **onClick** | `React.MouseEventHandler` | No | - | +| **form** | `string` | No | - | +| **tabIndex** | `number` | No | - | + +## CalendarPreviewProps + +| Prop | Type | Required | Description | +| ------------------ | ---------------------------------------------------------------------------- | -------- | --------------------------------------------------------- | +| **highlightDates** | `{ date: Date; highlightColor: "primary" \| "secondary"; label: string; }[]` | No | Array of dates to highlight with custom colors and labels | +| **dateRange** | `{ start: Date; end: Date; label: string; }` | Yes | Date range to display in the calendar preview | + +## CardProps + +| Prop | Type | Required | Description | +| ------------- | ----------------- | -------- | -------------------------------------------------------------------------------- | +| **children** | `React.ReactNode` | Yes | Content to be displayed inside the card | +| **menu** | `React.ReactNode` | No | Optional menu component to be displayed on the right side of the card | +| **className** | `string` | No | CSS className to be applied | +| **action** | `React.ReactNode` | No | Optional action element (e.g., checkbox, radio) to be displayed on the left side | + +## CheckboxGroupProps + +| Prop | Type | Required | Description | +| --------------------------- | --------------------------------------------- | -------- | ---------------------------------------------------------------------- | +| **isInvalid** | `boolean` | No | Indicates if the checkbox group is in an invalid state | +| **isDisabled** | `boolean` | No | Disables all checkbox options in the group | +| **options** | [CheckboxGroupOption](#checkboxgroupoption)[] | Yes | Array of checkbox options to display | +| **value** | `string[]` | No | Array of currently selected values | +| **onChange** | `(value: string[]) => void` | No | Callback when selection changes | +| **inputRef** | `Ref` | No | React ref for the first checkbox input element | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | + +### CheckboxGroupOption + +| Prop | Type | Required | Description | +| --------------- | ----------------- | -------- | --------------------------------------------------- | +| **label** | `React.ReactNode` | Yes | Label text or content for the checkbox option | +| **value** | `string` | Yes | Value of the option that will be passed to onChange | +| **isDisabled** | `boolean` | No | Disables this specific checkbox option | +| **description** | `React.ReactNode` | No | Optional description text for the checkbox option | + +## CheckboxProps + +| Prop | Type | Required | Description | +| --------------------------- | ------------------------------- | -------- | ---------------------------------------------------------------------- | +| **value** | `boolean` | No | Current checked state of the checkbox | +| **onChange** | `(value: boolean) => void` | No | Callback when checkbox state changes | +| **inputRef** | `Ref` | No | React ref for the checkbox input element | +| **isInvalid** | `boolean` | No | Indicates if the checkbox is in an invalid state | +| **isDisabled** | `boolean` | No | Disables the checkbox and prevents interaction | +| **onBlur** | `() => void` | No | Handler for blur events | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | + +## ComboBoxProps + +| Prop | Type | Required | Description | +| --------------------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| **isDisabled** | `boolean` | No | Disables the combo box and prevents interaction | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **label** | `string` | Yes | Label text for the combo box field | +| **onChange** | `(value: string) => void` | No | Callback when selection changes | +| **onBlur** | `() => void` | No | Handler for blur events | +| **options** | [ComboBoxOption](#comboboxoption)[] | Yes | Array of options to display in the dropdown | +| **value** | `null \| string` | No | Currently selected value | +| **inputRef** | `Ref` | No | React ref for the combo box input element | +| **allowsCustomValue** | `boolean` | No | Allows the user to type any value, not just options in the list. The options list becomes a suggestion helper rather than a strict constraint. | +| **portalContainer** | `HTMLElement` | No | Element to use as the portal container for the dropdown popover. Overrides the default SDK root container from context. | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | +| **placeholder** | `string` | No | - | + +### ComboBoxOption + +| Prop | Type | Required | Description | +| --------- | -------- | -------- | --------------------------------------------------- | +| **label** | `string` | Yes | Display text for the option | +| **value** | `string` | Yes | Value of the option that will be passed to onChange | + +## DatePickerProps + +| Prop | Type | Required | Description | +| --------------------------- | ------------------------------- | -------- | --------------------------------------------------------------------------------------------- | +| **inputRef** | `Ref` | No | React ref for the date input element | +| **isDisabled** | `boolean` | No | Disables the date picker and prevents interaction | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **onChange** | `(value: Date \| null) => void` | No | Callback when selected date changes | +| **onBlur** | `() => void` | No | Handler for blur events | +| **label** | `string` | Yes | Label text for the date picker field | +| **value** | `null \| Date` | No | Currently selected date value | +| **placeholder** | `string` | No | Placeholder text when no date is selected | +| **portalContainer** | `HTMLElement` | No | Element to use as the portal container | +| **minDate** | `Date` | No | Minimum selectable date. Dates before this will be disabled. | +| **maxDate** | `Date` | No | Maximum selectable date. Dates after this will be disabled. | +| **isDateDisabled** | `(date: Date) => boolean` | No | Callback to determine if a specific date should be disabled. Return true to disable the date. | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | + +## DescriptionListProps + +| Prop | Type | Required | Description | +| ------------------ | --------------------------------------------- | -------- | ----------- | +| **items** | [DescriptionListItem](#descriptionlistitem)[] | Yes | - | +| **layout** | `"stacked" \| "horizontal"` | No | - | +| **showSeparators** | `boolean` | No | - | +| **className** | `string` | No | - | + +### DescriptionListItem + +| Prop | Type | Required | Description | +| --------------- | -------------------------------------- | -------- | ----------- | +| **term** | `React.ReactNode \| React.ReactNode[]` | Yes | - | +| **description** | `React.ReactNode \| React.ReactNode[]` | Yes | - | + +## DialogProps + +| Prop | Type | Required | Description | +| ------------------------------ | ----------------- | -------- | --------------------------------------------------------------------------------- | +| **isOpen** | `boolean` | No | Controls whether the dialog is open or closed | +| **onClose** | `() => void` | No | Callback function called when the dialog should be closed | +| **onPrimaryActionClick** | `() => void` | No | Callback function called when the primary action button is clicked | +| **isDestructive** | `boolean` | No | Whether the primary action is destructive (changes button style to error variant) | +| **isPrimaryActionLoading** | `boolean` | No | Whether the primary action button is in loading state | +| **primaryActionLabel** | `string` | Yes | Text label for the primary action button | +| **closeActionLabel** | `string` | Yes | Text label for the close/cancel action button | +| **title** | `React.ReactNode` | No | Optional title content to be displayed at the top of the dialog | +| **children** | `React.ReactNode` | No | Optional children content to be rendered in the dialog body | +| **shouldCloseOnBackdropClick** | `boolean` | No | Whether clicking the backdrop should close the dialog | + +## FileInputProps + +| Prop | Type | Required | Description | +| -------------------- | ------------------------------ | -------- | -------------------------------------------------- | +| **id** | `string` | No | ID for the file input element | +| **value** | `null \| File` | Yes | Currently selected file | +| **onChange** | `(file: File \| null) => void` | Yes | Callback when file selection changes | +| **onBlur** | `() => void` | No | Handler for blur events | +| **accept** | `string[]` | No | Accepted file types (MIME types or extensions) | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **isDisabled** | `boolean` | No | Disables the input and prevents interaction | +| **className** | `string` | No | Additional CSS class name | +| **aria-describedby** | `string` | No | Aria-describedby attribute for accessibility | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | + +## HeadingProps + +| Prop | Type | Required | Description | +| ------------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------- | +| **as** | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | Yes | The HTML heading element to render (h1-h6) | +| **styledAs** | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | No | Optional visual style to apply, independent of the semantic heading level | +| **textAlign** | `"start" \| "center" \| "end"` | No | Text alignment within the heading | +| **children** | `React.ReactNode` | No | Content to be displayed inside the heading | +| **className** | `string` | No | - | +| **id** | `string` | No | - | + +## LinkProps + +| Prop | Type | Required | Description | +| -------------------- | ------------------------------------------------------ | -------- | --------------------------------------------------------------------- | +| **href** | `string` | No | - | +| **target** | `"_self" \| "_blank" \| "_parent" \| "_top" \| string` | No | - | +| **rel** | `string` | No | - | +| **download** | `any` | No | - | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **onKeyDown** | `React.KeyboardEventHandler` | No | - | +| **onKeyUp** | `React.KeyboardEventHandler` | No | - | +| **aria-label** | `string` | No | Defines a string value that labels the current element. | +| **aria-labelledby** | `string` | No | Identifies the element (or elements) that labels the current element. | +| **aria-describedby** | `string` | No | Identifies the element (or elements) that describes the object. | +| **title** | `string` | No | - | +| **children** | `React.ReactNode` | No | Content to be displayed inside the link | + +## LoadingSpinnerProps + +| Prop | Type | Required | Description | +| -------------- | --------------------- | -------- | ------------------------------------------------------- | +| **size** | `"lg" \| "sm"` | No | Size of the spinner | +| **style** | `"inline" \| "block"` | No | Display style of the spinner | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **aria-label** | `string` | No | Defines a string value that labels the current element. | + +## MenuProps + +| Prop | Type | Required | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------- | +| **triggerRef** | `RefObject` | No | Reference to the element that triggers the menu | +| **items** | [MenuItem](#menuitem)[] | No | Array of menu items to display | +| **isOpen** | `boolean` | No | Controls whether the menu is currently open | +| **onClose** | `() => void` | No | Callback when the menu is closed | +| **aria-label** | `string` | Yes | Accessible label describing the menu's purpose | +| **portalContainer** | `HTMLElement` | No | Element to use as the portal container for the menu popover. Overrides the default SDK root container from context. | +| **placement** | `"top" \| "top start" \| "top end" \| "bottom" \| "bottom start" \| "bottom end" \| "left" \| "right"` | No | Controls the placement of the menu popover relative to the trigger | + +### MenuItem + +| Prop | Type | Required | Description | +| -------------- | ----------------- | -------- | ----------------------------------------------- | +| **label** | `string` | Yes | Text label for the menu item | +| **icon** | `React.ReactNode` | No | Optional icon to display before the label | +| **onClick** | `() => void` | Yes | Callback function when the menu item is clicked | +| **isDisabled** | `boolean` | No | Disables the menu item and prevents interaction | +| **href** | `string` | No | Optional URL to navigate to when clicked | + +## ModalProps + +| Prop | Type | Required | Description | +| ------------------------------ | ----------------- | -------- | -------------------------------------------------------- | +| **isOpen** | `boolean` | No | Controls whether the modal is open or closed | +| **onClose** | `() => void` | No | Callback function called when the modal should be closed | +| **shouldCloseOnBackdropClick** | `boolean` | No | Whether clicking the backdrop should close the modal | +| **children** | `React.ReactNode` | No | Main content to be rendered in the modal body | +| **footer** | `React.ReactNode` | No | Footer content to be rendered at the bottom of the modal | +| **containerRef** | `RefObject` | No | Optional ref to the backdrop container | + +## NumberInputProps + +| Prop | Type | Required | Description | +| --------------------------- | -------------------------------------- | -------- | ---------------------------------------------------------------------- | +| **format** | `"currency" \| "decimal" \| "percent"` | No | Format type for the number input | +| **inputRef** | `Ref` | No | React ref for the number input element | +| **value** | `number` | No | Current value of the number input | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **isDisabled** | `boolean` | No | Disables the number input and prevents interaction | +| **onChange** | `(value: number) => void` | No | Callback when number input value changes | +| **onBlur** | `() => void` | No | Handler for blur events | +| **adornmentStart** | `React.ReactNode` | No | Element to display at the start of the input | +| **adornmentEnd** | `React.ReactNode` | No | Element to display at the end of the input | +| **minimumFractionDigits** | `number` | No | Minimum number of decimal places to display | +| **maximumFractionDigits** | `number` | No | Maximum number of decimal places to display | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | +| **placeholder** | `string` | No | - | +| **min** | `string \| number` | No | - | +| **max** | `string \| number` | No | - | + +## OrderedListProps + +The props for this component are defined in [BaseListProps](#baselistprops). + +## PaginationControlProps + +| Prop | Type | Required | Description | +| ---------------------------- | ---------------------------------------------------------------- | -------- | ----------- | +| **handleFirstPage** | `() => void` | Yes | - | +| **handlePreviousPage** | `() => void` | Yes | - | +| **handleNextPage** | `() => void` | Yes | - | +| **handleLastPage** | `() => void` | Yes | - | +| **handleItemsPerPageChange** | `(n: [PaginationItemsPerPage](#paginationitemsperpage)) => void` | Yes | - | +| **currentPage** | `number` | Yes | - | +| **totalPages** | `number` | Yes | - | +| **totalCount** | `number` | No | - | +| **itemsPerPage** | `5 \| 10 \| 25 \| 50` | No | - | +| **isFetching** | `boolean` | No | - | + +### PaginationItemsPerPage + +```typescript +type PaginationItemsPerPage = 5 | 10 | 25 | 50 +``` + +## PayrollLoadingProps + +| Prop | Type | Required | Description | +| --------------- | ----------------- | -------- | ----------- | +| **title** | `React.ReactNode` | Yes | - | +| **description** | `React.ReactNode` | No | - | + +## ProgressBarProps + +| Prop | Type | Required | Description | +| --------------- | ----------------------------- | -------- | -------------------------------------------------------- | +| **totalSteps** | `number` | Yes | Total number of steps in the progress sequence | +| **currentStep** | `number` | Yes | Current step in the progress sequence | +| **className** | `string` | No | Additional CSS class name for the progress bar container | +| **label** | `string` | Yes | Accessible label describing the progress bar's purpose | +| **cta** | `React.ComponentType \| null` | No | Component to render as the progress bar's CTA | + +## RadioGroupProps + +| Prop | Type | Required | Description | +| --------------------------- | --------------------------------------- | -------- | ---------------------------------------------------------------------- | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **isDisabled** | `boolean` | No | Disables all radio options in the group | +| **options** | [RadioGroupOption](#radiogroupoption)[] | Yes | Array of radio options to display | +| **value** | `null \| string` | No | Currently selected value | +| **defaultValue** | `string` | No | Initially selected value | +| **onChange** | `(value: string) => void` | No | Callback when selection changes | +| **inputRef** | `Ref` | No | React ref for the first radio input element | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | + +### RadioGroupOption + +| Prop | Type | Required | Description | +| --------------- | ----------------- | -------- | --------------------------------------------------- | +| **label** | `React.ReactNode` | Yes | Label text or content for the radio option | +| **value** | `string` | Yes | Value of the option that will be passed to onChange | +| **isDisabled** | `boolean` | No | Disables this specific radio option | +| **description** | `React.ReactNode` | No | Optional description text for the radio option | + +## RadioProps + +| Prop | Type | Required | Description | +| --------------------------- | ------------------------------------------- | -------- | ---------------------------------------------------------------------- | +| **value** | `boolean` | No | Current checked state of the radio button | +| **onChange** | `(checked: boolean) => void` | No | Callback when radio button state changes | +| **inputRef** | `Ref` | No | React ref for the radio input element | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **isDisabled** | `boolean` | No | Disables the radio button and prevents interaction | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | +| **onBlur** | `React.FocusEventHandler` | No | - | + +## SelectProps + +| Prop | Type | Required | Description | +| --------------------------- | -------------------------------- | -------- | ---------------------------------------------------------------------- | +| **isDisabled** | `boolean` | No | Disables the select and prevents interaction | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **label** | `string` | Yes | Label text for the select field | +| **onChange** | `(value: string) => void` | No | Callback when selection changes | +| **onBlur** | `() => void` | No | Handler for blur events | +| **options** | [SelectOption](#selectoption)[] | Yes | Array of options to display in the select dropdown | +| **placeholder** | `string` | No | Placeholder text when no option is selected | +| **value** | `null \| string` | No | Currently selected value | +| **inputRef** | `Ref` | No | React ref for the select button element | +| **portalContainer** | `HTMLElement` | No | Element to use as the portal container | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | + +### SelectOption + +| Prop | Type | Required | Description | +| --------- | -------- | -------- | --------------------------------------------------- | +| **value** | `string` | Yes | Value of the option that will be passed to onChange | +| **label** | `string` | Yes | Display text for the option | + +## SwitchProps + +| Prop | Type | Required | Description | +| --------------------------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ | +| **onBlur** | `() => void` | No | Handler for blur events | +| **onChange** | `(checked: boolean) => void` | No | Callback when switch state changes | +| **value** | `boolean` | No | Current checked state of the switch | +| **inputRef** | `Ref` | No | React ref for the switch input element | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **isDisabled** | `boolean` | No | Disables the switch and prevents interaction | +| **className** | `string` | No | Additional CSS class name for the switch container | +| **label** | `string` | Yes | Label text for the switch | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **id** | `string` | No | - | +| **name** | `string` | No | - | +| **aria-controls** | `string` | No | Identifies the element (or elements) whose contents or presence are controlled by the current element. | + +## TableProps + +| Prop | Type | Required | Description | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------- | +| **headers** | [TableData](#tabledata)[] | Yes | Array of header cells for the table | +| **rows** | [TableRow](#tablerow)[] | Yes | Array of rows to be displayed in the table | +| **footer** | [TableData](#tabledata)[] | No | Array of footer cells for the table | +| **emptyState** | `React.ReactNode` | No | Content to display when the table has no rows | +| **isWithinBox** | `boolean` | No | Removes borders and background for use inside a Box component | +| **hasCheckboxColumn** | `boolean` | No | Whether the first column contains checkboxes (affects which column gets leading variant) | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **aria-label** | `string` | No | Defines a string value that labels the current element. | +| **aria-labelledby** | `string` | No | Identifies the element (or elements) that labels the current element. | +| **aria-describedby** | `string` | No | Identifies the element (or elements) that describes the object. | +| **role** | `"form" \| "button" \| "alert" \| "alertdialog" \| "application" \| "article" \| "banner" \| "cell" \| "checkbox" \| "columnheader" \| "combobox" \| "complementary" \| "contentinfo" \| "definition" \| "dialog" \| "directory" \| "document" \| "feed" \| "figure" \| "grid" \| "gridcell" \| "group" \| "heading" \| "img" \| "link" \| "list" \| "listbox" \| "listitem" \| "log" \| "main" \| "marquee" \| "math" \| "menu" \| "menubar" \| "menuitem" \| "menuitemcheckbox" \| "menuitemradio" \| "navigation" \| "none" \| "note" \| "option" \| "presentation" \| "progressbar" \| "radio" \| "radiogroup" \| "region" \| "row" \| "rowgroup" \| "rowheader" \| "scrollbar" \| "search" \| "searchbox" \| "separator" \| "slider" \| "spinbutton" \| "status" \| "switch" \| "tab" \| "table" \| "tablist" \| "tabpanel" \| "term" \| "textbox" \| "timer" \| "toolbar" \| "tooltip" \| "tree" \| "treegrid" \| "treeitem" \| string` | No | - | + +### TableData + +| Prop | Type | Required | Description | +| ----------- | ----------------- | -------- | ----------------------------------------- | +| **key** | `string` | Yes | Unique identifier for the table cell | +| **content** | `React.ReactNode` | Yes | Content to be displayed in the table cell | + +### TableRow + +| Prop | Type | Required | Description | +| -------- | ------------------------- | -------- | ----------------------------------------- | +| **key** | `string` | Yes | Unique identifier for the table row | +| **data** | [TableData](#tabledata)[] | Yes | Array of cells to be displayed in the row | + +## TabsProps + +| Prop | Type | Required | Description | +| --------------------- | ----------------------- | -------- | ----------------------------------- | +| **tabs** | [TabProps](#tabprops)[] | Yes | Array of tab configuration objects | +| **selectedId** | `string` | No | Currently selected tab id | +| **onSelectionChange** | `(id: string) => void` | Yes | Callback when tab selection changes | +| **aria-label** | `string` | No | Accessible label for the tabs | +| **aria-labelledby** | `string` | No | ID of element that labels the tabs | +| **className** | `string` | No | Additional CSS class name | + +### TabProps + +| Prop | Type | Required | Description | +| -------------- | ----------------- | -------- | ----------------------------------- | +| **id** | `string` | Yes | Unique identifier for the tab | +| **label** | `React.ReactNode` | Yes | Label to display in the tab button | +| **content** | `React.ReactNode` | Yes | Content to display in the tab panel | +| **isDisabled** | `boolean` | No | Whether the tab is disabled | + +## TextAreaProps + +| Prop | Type | Required | Description | +| --------------------------- | ---------------------------------- | -------- | ---------------------------------------------------------------------- | +| **inputRef** | `Ref` | No | React ref for the textarea element | +| **value** | `string` | No | Current value of the textarea | +| **onChange** | `(value: string) => void` | No | Callback when textarea value changes | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **isDisabled** | `boolean` | No | Disables the textarea and prevents interaction | +| **onBlur** | `() => void` | No | Handler for blur events | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | +| **placeholder** | `string` | No | - | +| **rows** | `number` | No | - | +| **cols** | `number` | No | - | +| **aria-describedby** | `string` | No | Identifies the element (or elements) that describes the object. | + +## TextInputProps + +| Prop | Type | Required | Description | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------- | +| **inputRef** | `Ref` | No | React ref for the input element | +| **value** | `string` | No | Current value of the input | +| **onChange** | `(value: string) => void` | No | Callback when input value changes | +| **isInvalid** | `boolean` | No | Indicates that the field has an error | +| **isDisabled** | `boolean` | No | Disables the input and prevents interaction | +| **onBlur** | `() => void` | No | Handler for blur events | +| **adornmentStart** | `React.ReactNode` | No | Element to display at the start of the input | +| **adornmentEnd** | `React.ReactNode` | No | Element to display at the end of the input | +| **description** | `React.ReactNode` | No | Optional description text for the field | +| **errorMessage** | `string` | No | Error message to display when the field is invalid | +| **isRequired** | `boolean` | No | Indicates if the field is required | +| **label** | `React.ReactNode` | Yes | Label text for the field | +| **shouldVisuallyHideLabel** | `boolean` | No | Hides the label visually while keeping it accessible to screen readers | +| **className** | `string` | No | - | +| **id** | `string` | No | - | +| **name** | `string` | No | - | +| **type** | `"number" \| "submit" \| "reset" \| "button" \| "checkbox" \| "radio" \| "search" \| "color" \| "date" \| "datetime-local" \| "email" \| "file" \| "hidden" \| "image" \| "month" \| "password" \| "range" \| "tel" \| "text" \| "time" \| "url" \| "week" \| string` | No | - | +| **placeholder** | `string` | No | - | +| **min** | `string \| number` | No | - | +| **max** | `string \| number` | No | - | +| **maxLength** | `number` | No | - | +| **aria-labelledby** | `string` | No | Identifies the element (or elements) that labels the current element. | +| **aria-describedby** | `string` | No | Identifies the element (or elements) that describes the object. | + +## TextProps + +| Prop | Type | Required | Description | +| ------------- | ----------------------------------------------- | -------- | ----------------------------------- | +| **as** | `"p" \| "span" \| "div" \| "pre"` | No | HTML element to render the text as | +| **size** | `"lg" \| "sm" \| "xs" \| "md"` | No | Size variant of the text | +| **textAlign** | `"start" \| "center" \| "end"` | No | Text alignment within the container | +| **weight** | `"regular" \| "medium" \| "semibold" \| "bold"` | No | Font weight of the text | +| **children** | `React.ReactNode` | No | Content to be displayed | +| **variant** | `"supporting" \| "leading"` | No | Visual style variant of the text | +| **className** | `string` | No | - | +| **id** | `string` | No | - | + +## UnorderedListProps + +The props for this component are defined in [BaseListProps](#baselistprops). diff --git a/docs-site/versioned_docs/version-0.46.3/component-adapter/how-the-component-adapter-works.md b/docs-site/versioned_docs/version-0.46.3/component-adapter/how-the-component-adapter-works.md new file mode 100644 index 000000000..1235ec0fb --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/component-adapter/how-the-component-adapter-works.md @@ -0,0 +1,101 @@ +--- +title: How the Component Adapter Works +order: 2 +--- + +## How the Component Adapter Works + +1. You create mappings that connect the SDK props to your UI components +2. You provide these mappings to either: + - `GustoProvider` (recommended): Includes default React Aria components and allows overriding specific ones + - `GustoProviderCustomUIAdapter`: For complete UI control without React Aria dependencies +3. Your custom components are used by the SDK instead of the default components + +### Choosing a Provider + +The SDK offers two providers for different use cases: + +#### GustoProvider (Recommended) + +```tsx +import { GustoProvider, type ButtonProps, type TextInputProps } from '@gusto/embedded-react-sdk' +import { MyButton } from 'my-codebase/src/my-components/MyButton' + +function ButtonAdapter({ + isLoading = false, + isDisabled = false, + buttonRef, + onClick, + children, + ...props +}: ButtonProps) { + return ( + + {children} + + ) +}, + +function App() { + return ( + + + + ) +} +``` + +- Includes React Aria default components out of the box +- Allows overriding specific components while keeping defaults for others +- Best choice for most applications +- Simpler to implement when you only need to customize some components + +#### GustoProviderCustomUIAdapter + +```tsx +import { GustoProviderCustomUIAdapter } from '@gusto/embedded-react-sdk' + +function App() { + return ( + + + + ) +} +``` + +- Requires implementing all needed components +- No React Aria dependencies included +- Better for tree-shaking and bundle size optimization +- Ideal when you need complete control over the UI implementation + +### Default Components + +The SDK provides a set of default components implemented with React Aria for accessibility. These are used when no custom components are provided. You can view the default implementations here: + +- [Default Component Adapter](https://github.com/Gusto/embedded-react-sdk/blob/main/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx) +- [UI Components Directory](https://github.com/Gusto/embedded-react-sdk/tree/main/src/components/Common/UI) + +### Benefits + +This architecture provides several key benefits: + +1. **Consistent look and feel**: Your entire application can use a consistent design system +2. **Framework flexibility**: You can use any React-compatible UI framework or library +3. **Implement once**: Once you have implemented your adapters, any SDK features you add down the road will automatically use your provided custom UI for free + +[Back to Component Adapter Overview](./component-adapter.md) diff --git a/docs-site/versioned_docs/version-0.46.3/component-adapter/setting-up-your-component-adapter.md b/docs-site/versioned_docs/version-0.46.3/component-adapter/setting-up-your-component-adapter.md new file mode 100644 index 000000000..9327c6f13 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/component-adapter/setting-up-your-component-adapter.md @@ -0,0 +1,258 @@ +--- +title: Setting Up Your Component Adapter +order: 3 +--- + +## Setting Up Your Component Adapter + +This guide will walk you through the process of creating and implementing your own Component Adapter for the Gusto Embedded React SDK. + +### 1. Create Your Custom Component Implementations + +Each component must implement the required props interface defined by the SDK. For example, if you're creating a custom TextInput, it must accept all the props defined in the `TextInputProps` interface ([View interface on GitHub](https://github.com/Gusto/embedded-react-sdk/blob/main/src/components/Common/UI/TextInput/TextInputTypes.ts)). + +The component types extend basic HTML element props, so your implementations can accept and forward any standard HTML attributes to the underlying HTML elements. For example: + +```tsx +import type { TextInputProps } from '@gusto/embedded-react-sdk' + +const MyCustomTextInput = ({ + label, + description, + errorMessage, + isRequired, + isDisabled, + isInvalid, + id, + name, + value, + placeholder, + onChange, + onBlur, + inputRef, + shouldVisuallyHideLabel, + ...props // Additional HTML input props are passed through +}: TextInputProps) => { + // Your custom implementation here + // Or you could supply an existing text input component from your codebase + // Or you could supply a component from an external UI library + return ( +
+ + onChange && onChange(e.target.value)} + {...props} // Spread additional HTML props + /> + {errorMessage &&
{errorMessage}
} +
+ ) +} +``` + +Make sure your component implementation: + +- Handles all required props correctly +- Maintains accessibility features +- Follows your design system guidelines +- Properly passes event handlers +- Forwards additional HTML attributes to the appropriate element + +For a complete reference of all component types and their props, see the [Component Inventory](./component-inventory.md). + +To learn more about how each component should be implemented, you can reference the default implementations in the SDK ([View on GitHub](https://github.com/Gusto/embedded-react-sdk/tree/main/src/components/Common/UI)). + +### 2. Create Your Component Adapter Object + +Create an object that implements the `ComponentsContextType` interface ([View interface on GitHub](https://github.com/Gusto/embedded-react-sdk/blob/main/src/contexts/ComponentAdapter/useComponentContext.ts)) with your custom components: + +```tsx +import type { ComponentsContextType } from '@gusto/embedded-react-sdk' + +const myCustomComponents: ComponentsContextType = { + Alert: props => , + Badge: props => , + Button: props => , + ButtonIcon: props => , + CalendarPreview: props => , + Card: props => , + Checkbox: props => , + CheckboxGroup: props => , + ComboBox: props => , + DatePicker: props => , + Heading: props => , + Link: props => , + Menu: props => , + NumberInput: props => , + OrderedList: props => , + ProgressBar: props => , + Radio: props => , + RadioGroup: props => , + Select: props => , + Switch: props => , + Table: props => , + Text: props => , + TextInput: props => , + UnorderedList: props => , +} +``` + +### 3. Choose Your Provider + +The SDK offers two ways to provide your custom components, each suited for different needs: + +#### Option A: Using GustoProvider (Recommended) + +The `GustoProvider` is the recommended approach for most applications. Using the `GustoProvider`, you are able to supply only the components you want to override and then the rest fall back to default React Aria components: + +```tsx +import { GustoProvider } from '@gusto/embedded-react-sdk' + +function App() { + return ( + + {/* Your application components */} + + + ) +} +``` + +Benefits of using `GustoProvider`: + +- Only need to implement the components you want to customize +- Includes accessible React Aria components as defaults +- Simpler integration path +- Best choice for most applications + +#### Option B: Using GustoProviderCustomUIAdapter + +If you need complete control over the UI implementation or want to optimize bundle size through tree-shaking, use the `GustoProviderCustomUIAdapter`: + +```tsx +import { GustoProviderCustomUIAdapter } from '@gusto/embedded-react-sdk' + +function App() { + return ( + + {/* Your application components */} + + + ) +} +``` + +Benefits of using `GustoProviderCustomUIAdapter`: + +- Complete control over component implementation +- No React Aria dependencies included +- Better for tree-shaking and bundle size optimization +- Ideal when you want to fully customize the UI layer + +Choose this option when you: + +- Want to implement all UI components yourself +- Need to minimize bundle size +- Don't want React Aria as a dependency +- Have a complete design system you want to use + +### 4. Testing Your Implementation + +After implementing your Component Adapter, it's a good practice to: + +1. Test all components with various props and states +2. Verify that event handlers work as expected +3. Check accessibility features +4. Test across different browsers and devices + +### Complete Example + +Here's an example showing how to customize a few common components using Material UI with `GustoProvider`. For a full list of customizable components and their props, see the [Component Inventory](./component-inventory.md). + +```tsx +import { GustoProvider } from '@gusto/embedded-react-sdk' +import TextField from '@mui/material/TextField' +import Button from '@mui/material/Button' + +// Create Material UI implementations for the components you want to customize +const materialUIComponents = { + // TextInput implementation + TextInput: ({ + label, + description, + errorMessage, + isRequired, + isDisabled, + isInvalid, + id, + name, + value, + placeholder, + onChange, + onBlur, + ...props + }) => ( + onChange && onChange(e.target.value)} + onBlur={onBlur} + fullWidth + margin="normal" + {...props} + /> + ), + + // Button implementation + Button: ({ children, isDisabled, isLoading, onClick, variant = 'primary', ...props }) => { + const muiVariant = + variant === 'primary' ? 'contained' : variant === 'secondary' ? 'outlined' : 'text' + + return ( + + ) + }, + + // Other components can be customized as needed... +} + +function App() { + return ( + + + + ) +} +``` + +Other helpful resources setting up component adapters: + +- [Component Inventory](./component-inventory.md) +- [Default Component Implementations](https://github.com/Gusto/embedded-react-sdk/blob/main/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx) + +[Back to Component Adapter Overview](./component-adapter.md) diff --git a/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/build-pathways-sdk-flows-api.md b/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/build-pathways-sdk-flows-api.md new file mode 100644 index 000000000..4cf80786d --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/build-pathways-sdk-flows-api.md @@ -0,0 +1,22 @@ +--- +title: Build Pathways - SDK, Flows, & API +order: 0 +--- + +## How do I decide how I want to build payroll on GEP? + +Payroll is a very comprehensive product, involving multiple workflows and UI aspects that must be built in order for your customers to: + +1. Provide all the inputs into payroll (e.g. employees, addresses, pay schedules, etc.) +2. Run the various types of payrolls (e.g. regular, off cycle, dismissal etc.) +3. Manage payroll and the associated data afterward. + +An approximation of the most important workflows in a comprehensive payroll product can be found in our Flows list - [see here and try them out](https://docs.gusto.com/embedded-payroll/docs/flow-types)! + +Of course, this can be very time consuming to build. To cover this ground, GEP offers _three_ approaches to building your payroll product, which you can mix-and-match for various aspects of your application - depending on your needs for each workflow: + +| Approach | Pros | Cons | Documentation | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Custom (API only) | Full coverage for all of GEP’s capabilities
Full customization of your application | Requires you to build the UI
Requires understanding of individual endpoints | [GEP API Reference](https://docs.gusto.com/embedded-payroll/reference) | +| Flows via iframe | Fastest way to deploy any given workflow - all of the needed logic and API linkage already built in | Limited customizability beyond basic look and feel | [List of available Flows via iframe](https://docs.gusto.com/embedded-payroll/docs/flow-types)
[Flow customization options](https://docs.gusto.com/embedded-payroll/docs/customize-flows) | +| React SDK | Strikes the balance between Custom (API only) and Flows - enables more customization than Flows, and abstracts away logic around API endpoints | Workflows are limited but planned on our developer roadmap | See the other sections of this guide! | diff --git a/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/component-types.md b/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/component-types.md new file mode 100644 index 000000000..13a07a84a --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/component-types.md @@ -0,0 +1,17 @@ +--- +title: Component Types +order: 1 +--- + +## SDK Component Types and “Altitude” + +Given that a UI “component” can be anything ranging from a table to an entire end-to-end workflow reflecting the various steps of a user journey (e.g. employee onboarding), we wanted to provide more optionality for our developers by building a mix of components in terms of complexity/granularity. + +For each domain in the payroll application (e.g. run payroll, company onboarding, etc.), we include: + +- **“Workflow”** SDK components function as a full, off-the-shelf-ready solutions that can either serve as the entirety or the starting point for your build. This component would still have many of the customization and flexibility of the individual SDK components, but would lack the following capabilities: + - Cannot re-order or remove fields + - Has less flexibility to support A/B testing due to rigidity of the sub-components +- **“Building Block”** SDK components that represent the individual pieces of the Workflow above. Using these would provide more customization and flexibility, but would be more time-consuming compared to the ready-built Workflow above. + +While we generally recommend using the Workflow approach, your build needs may vary and we recommend discussing with a dedicated specialist from our side to determine what makes sense given your goals and your current application. diff --git a/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/deciding-to-build-with-the-sdk.md b/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/deciding-to-build-with-the-sdk.md new file mode 100644 index 000000000..d9f33ec04 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/deciding-to-build-with-the-sdk/deciding-to-build-with-the-sdk.md @@ -0,0 +1,29 @@ +--- +title: Deciding to build with the SDK +order: 1 +--- + +## How do I know if the GEP React SDK is right for me? + +While it’s possible to mix and match the build approach _across_ the domain of your payroll application, you would still generally want to match your needs to the best approach out of the possible build options (SDK, Flows, or build your own UI) for any given domain within the payroll application. As our SDK components are still in an early phase of development, many components may not yet be available via SDK (discuss with your Gusto point of contact if you’d like to inquire about a specific component). + +Here are some considerations when deciding whether or not to use the React SDK for any given payroll workflow for your application: + +**You should use the React SDK if…** + +- If you already use React in your application, or are open to introducing React into your stack ([Learn how to add React to existing projects](https://react.dev/learn/add-react-to-an-existing-project)) +- You want to use pre-built components that abstract API endpoint complexity +- You already capture info required for a particular workflow (e.g. company onboarding) +- You want to customize components to your application’s theme and layout +- You’re just starting out with building embedded payroll on GEP, or the components you want to use are currently available via the SDK library + +**You may benefit from a different build approach if…** + +- You need to deploy a proof of concept as soon as possible, even if the resulting application feels inconsistent in look and feel and non-integrated with the rest of your application (e.g. data that you already have may be requested again). + - If your number one priority is speed, you may need to use [Flows](https://docs.gusto.com/embedded-payroll/docs/flows-intro) instead, as it is still the fastest way to deploy any given workflow. +- You have very deep customization needs that may not be met by the React SDK (if in doubt, ask your Gusto Embedded point of contact!). You may need to create a custom, API-only build to accommodate your requirements, but we would still highly recommend using SDK or Flow components where possible to reduce your build workload. +- The SDK component you need currently does not yet exist: we are actively working on several components to cover the rest of our application - for what’s currently available, see the Workflows Overview section and reach out to your Gusto Embedded point of contact for more information! + +### How can I learn more about React SDK? + +As a Gusto Embedded partner, we will provide consultation with a GEP Solutions Architect to ensure we can find the right solution for you. We recommend starting with the documentation sections provided, and please reach out to your Gusto Embedded representative to learn more! diff --git a/docs-site/versioned_docs/version-0.46.3/getting-started/authentication.md b/docs-site/versioned_docs/version-0.46.3/getting-started/authentication.md new file mode 100644 index 000000000..71ba093e0 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/getting-started/authentication.md @@ -0,0 +1,74 @@ +--- +title: Authentication +order: 0 +--- + +## Authentication + +To get started, you'll need to create a way to properly create and retrieve access tokens on behalf of your authenticated user from your application. + +Since there are a vast number of ways this might work for a partner, what we can suggest to get up and running is to implement a proxy server that handles translating requests from the SDK to the Gusto Embedded API. + +### How access tokens work + +To maximize compatibility with a wide range of partner security postures and implementations, the Gusto Embedded React SDK does not include built-in OAuth2 token acquisition and refresh flows but instead is built to fit into a partner's existing flows. + +The most simple implementation is one where a partner has a backend service that acquires OAuth2 tokens from the Gusto Embedded API on behalf of an authenticated user and then proxies calls to the Gusto Embedded API using those tokens. The following provides a high level graphical representation of how that configuration would look: + +![](https://files.readme.io/161c4c0c0952486a811a18c71d959a8bd74ca4884f2fc1abe39737c988f3a05f-image.png) + +The `` can receive a `baseUrl` that can be configured with the address of your backend proxy service and can also be used if necessary to pass along vendor authentication credentials. + +```jsx react +import { GustoProvider } from '@gusto/embedded-react-sdk' + +function App() { + return Your app here! +} + +export default App +``` + +### Adding Headers to Requests + +The SDK provides two ways to add headers to API requests: + +#### 1. Static Headers via Config (Simple) + +For static headers like API keys or simple authentication tokens, you can pass them directly in the config: + +```jsx react +import { GustoProvider } from '@gusto/embedded-react-sdk' + +function App() { + return ( + + Your app here! + + ) +} +``` + +This approach automatically applies the headers to all API requests without requiring custom hooks. + +#### 2. Dynamic Headers via Hooks (Advanced) + +For dynamic headers that need to be computed at request time (like refreshing tokens), use the hooks approach described in the [Request Interceptors guide](../integration-guide/request-interceptors.md). + +### Securing your proxy + +Beyond authentication, your proxy server should also enforce authorization -- controlling which users can perform which actions through the Gusto API. For detailed guidance on implementing endpoint allowlisting, role-based access control, and other proxy security practices, see the [Securing your proxy](./getting-started.md#securing-your-proxy) section in the Getting Started guide and the [Proxy Security: Partner Guidance](./proxy-security-partner-guidance.md). + +### How components utilize the API + +Components which are part of the Gusto Embedded SDK utilize the `baseUrl` to call the appropriate public Gusto API endpoint. These components are custom built in order to abstract complicated API interactions. The baseUrl serves as a foundational element within the Embedded SDK framework, directing its components to the correct public Gusto API endpoint. + +For example, the `` component accesses a list of employees via the documented Gusto Embedded API endpoint at `/v1/companies//employees`. When your proxy server receives an inbound request, it can verify authentication & authorization, then after it's been identified as valid, retrieve or create an OAuth2 token, and finally use that token create a request to Gusto Embedded API's endpoint at `/v1/companies//employees` , returning the result received at that endpoint. diff --git a/docs-site/versioned_docs/version-0.46.3/getting-started/getting-started.md b/docs-site/versioned_docs/version-0.46.3/getting-started/getting-started.md new file mode 100644 index 000000000..19546a13a --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/getting-started/getting-started.md @@ -0,0 +1,78 @@ +--- +title: Getting Started +order: 2 +--- + +## CodeSandbox + +[We have a demo environment in CodeSandbox](https://codesandbox.io/p/devbox/happy-ardinghelli-nzpslw?file=%2Fsrc%2FApp.jsx). You can view the project setup there which puts together what you are going to see in this guide with a working example. + +## Installing the SDK + +To get started with the Gusto Embedded React SDK, first install it from NPM via the package manager of your choice. You can see the SDK published to NPM here at [@gusto/embedded-react-sdk.](https://www.npmjs.com/package/@gusto/embedded-react-sdk) + +In this case, installing via NPM: + +``` +npm i @gusto/embedded-react-sdk +``` + +## Configuring the API provider + +The `GustoProvider` is used to configure the SDK at the application level. It must wrap the top-level component tree (typically at the root of the application) to ensure SDK components have access to the necessary configurations. + +```jsx +import { GustoProvider } from '@gusto/embedded-react-sdk' + +function App() { + return ... +} + +export default App +``` + +The `baseUrl` property is configured with the address of your backend proxy which is detailed further in the following section. + +## Configuring a backend proxy + +When building with the React SDK, a backend proxy is required. React SDK components do not make calls to the Gusto Embedded API directly. Instead, the `baseUrl` configuration defines the URL of your proxy server. This proxy layer gives you complete control over requests sent to Gusto, which is essential for: + +1. Authentication +2. Providing the user IP address for form signing operations + +The React SDK is designed to mirror the [Gusto Embedded API Reference](https://docs.gusto.com/embedded-payroll/reference/whats-new-in-v2025-06-15) with a 1:1 mapping of endpoints. The SDK maintains consistent naming conventions, parameters, and response structures with the Gusto API. + +Your proxy server simply needs to forward any incoming SDK requests to the corresponding Embedded API endpoints. The proxy's main task is adding the necessary authentication headers before forwarding the request onwards. Since the SDK requests are already in the Embedded API format, no extra endpoint mapping or request transformation is required. + +### Using the proxy for authentication + +The proxy layer allows for authentication. The recommended approach is to use a backend service that acquires OAuth2 tokens from the Gusto Embedded API for authenticated users and proxies API calls using those tokens. Learn more about configuring this and setting up authentication in the `Authentication` section. + +### Using the proxy to provide the user IP address + +Some UI workflows require users to sign forms, which need the user's IP address for security purposes. To prevent vulnerabilities such as IP address spoofing, this information must be provided by your proxy server rather than collected client-side. + +Your proxy server can provide the IP address by adding the `x-gusto-client-ip` header with the user IP address to all forwarded requests on the backend. By setting this header once in your proxy it will be configured for all form signing operations. + +### Securing your proxy + +Your proxy is also the authorization layer between your users and the Gusto API. Because users can make API requests outside the SDK UI, authorization must be enforced server-side. The Gusto API provides application-level protections (scopes, company-bound tokens, rate limits), but user-level authorization is your responsibility. + +At a minimum, your proxy should: + +- **Authenticate every request** -- verify the user's session, not just at login +- **Allowlist endpoints** -- only forward requests to API endpoints your app actually uses +- **Validate resource ownership** -- ensure users can only access their own resources (e.g., their own employee ID) +- **Log proxied requests** -- maintain audit logs for monitoring and incident response + +For detailed guidance, endpoint allowlist tooling, and runnable examples, see the [Proxy Security: Partner Guidance](./proxy-security-partner-guidance.md). + +## Including styles + +The Gusto Embedded React SDK ships with preliminary styles for the UI components as a baseline. Those can be included by importing the following stylesheet: + +```typescript +import '@gusto/embedded-react-sdk/style.css' +``` + +See [Customizing SDK UI](../integration-guide/customizing-sdk-ui.md) for complete guidance on UI customization and making the SDK take on the look and feel of your application. diff --git a/docs-site/versioned_docs/version-0.46.3/getting-started/proxy-security-partner-guidance.md b/docs-site/versioned_docs/version-0.46.3/getting-started/proxy-security-partner-guidance.md new file mode 100644 index 000000000..7a0e19478 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/getting-started/proxy-security-partner-guidance.md @@ -0,0 +1,87 @@ +--- +title: 'Proxy Security: Partner Guidance' +--- + +# Proxy Security: Partner Guidance + +The Gusto API enforces application-level protections (scopes, company-bound tokens, rate limits). Your proxy enforces user-level authorization. Both layers are necessary -- UI-level restrictions alone are not sufficient since users can make API requests directly. + +## What to do + +1. **Authenticate every request.** Verify the user's session on every proxied request, not just at login. +2. **Allowlist endpoints.** Only forward requests to API endpoints your application actually uses. Deny everything else. +3. **Validate resource ownership.** Verify that the authenticated user is authorized to access the resource in the URL (e.g., an employee should only reach their own employee ID). +4. **Log proxied requests.** Maintain audit logs for security monitoring and incident response. + +## Getting the endpoint list + +Every SDK component makes a known set of API calls. Paths use named parameters (`:companyId`, `:employeeId`, etc.) that you replace with session values. The more you resolve, the tighter the allowlist: + +| What you resolve | Use case | +| ---------------------------- | --------------------------------------------------- | +| Nothing | Generic allowlisting, no user scoping | +| `:companyId` only | Admin who can access any employee in their company | +| `:companyId` + `:employeeId` | Self-service employee, restricted to their own data | + +### Option A: JSON endpoint inventory (recommended, any platform) + +The SDK ships a machine-readable JSON file listing every block and flow with their endpoints and required variables. It is auto-generated from source on every build and verified in CI. + +Import it from the package: + +```typescript +import inventory from '@gusto/embedded-react-sdk/endpoint-inventory.json' +``` + +Or load the file directly from `node_modules/@gusto/embedded-react-sdk/docs/reference/endpoint-inventory.json` in any language. + +The JSON structure: + +```json +{ + "blocks": { + "Employee.FederalTaxes": { + "endpoints": [ + { "method": "GET", "path": "/v1/employees/:employeeId/federal_taxes" }, + { "method": "PUT", "path": "/v1/employees/:employeeId/federal_taxes" } + ], + "variables": ["employeeId"] + } + }, + "flows": { + "Employee.SelfOnboardingFlow": { + "blocks": ["Employee.Landing", "Employee.Profile", "..."], + "endpoints": [ + { "method": "GET", "path": "/v1/employees/:employeeId" }, + { "method": "GET", "path": "/v1/companies/:companyId" } + ], + "variables": ["companyId", "employeeId"] + } + } +} +``` + +Look up the flows or blocks your app uses, substitute `:param` placeholders with session values, and use the result as your allowlist. + +### Option B: Static reference + +See the [endpoint reference tables](../reference/endpoint-reference.md) for a human-readable list. Copy the method + path pairs for the components you use and substitute `:param` placeholders with session values at runtime. + +## FAQ + +**Can an authenticated employee access another employee's data?** +Not if you substitute the employee ID -- the resulting paths only match that employee's endpoints. + +**How do we restrict some admins from running payroll?** +Only include payroll endpoints for roles that need them, or request removal of the `payrolls:run` scope entirely. + +**How do we keep the allowlist up to date?** +The JSON inventory is auto-derived on every build and verified in CI. Upgrading the SDK automatically reflects endpoint changes. + +## Further reading + +- [Proxy examples with role-based access](../reference/proxy-examples.md) +- [Endpoint reference tables](../reference/endpoint-reference.md) +- [Securing your proxy](./getting-started.md#securing-your-proxy) -- Getting Started +- [Gusto API Scopes](https://docs.gusto.com/embedded-payroll/docs/scopes) +- [Gusto Embedded API Reference](https://docs.gusto.com/embedded-payroll/reference) diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/hooks.md b/docs-site/versioned_docs/version-0.46.3/hooks/hooks.md new file mode 100644 index 000000000..b3f1bbf6e --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/hooks.md @@ -0,0 +1,923 @@ +--- +title: Hooks +order: 1 +--- + +# Hooks + +Hooks give you full control over form rendering while the SDK manages data fetching, validation, submission, and error handling. Each hook returns pre-bound field components, metadata, and actions — you supply the layout and labels. + +> Hooks are an experimental feature. APIs may change between minor versions during 0.x.x releases. + +## Available Hooks + +| Hook | Description | Reference | +| -------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| `useEmployeeDetailsForm` | Create or update employee profile fields (name, email, SSN, date of birth, self-onboarding) | [useEmployeeDetailsForm](./useEmployeeDetailsForm.md) | +| `useJobForm` | Create or update an employee's job (title, hire date, S-Corp 2% shareholder, WA workers' comp) | [useJobForm](./useJobForm.md) | +| `useCompensationForm` | Create or update a compensation row on a job (FLSA status, pay rate, payment unit, effective date) | [useCompensationForm](./useCompensationForm.md) | +| `useWorkAddressForm` | Create or update an employee's work address (company location select, effective date) | [useWorkAddressForm](./useWorkAddressForm.md) | +| `useFederalTaxesForm` | Update an employee's federal tax (W-4) withholding information | [useFederalTaxesForm](./useFederalTaxesForm.md) | +| `useEmployeeStateTaxesForm` | Update an employee's state tax withholding answers (dynamic, per-state question groups) | [useEmployeeStateTaxesForm](./useEmployeeStateTaxesForm.md) | +| `usePayScheduleForm` | Create or update a company pay schedule (frequency, pay dates, pay period calendar preview) | [usePayScheduleForm](./usePayScheduleForm.md) | +| `useSignCompanyForm` | Sign a company form (PDF viewer, typed signature, confirmation checkbox) | [useSignCompanyForm](./useSignCompanyForm.md) | +| `useSignEmployeeForm` | Sign an employee form (signature, confirmation, I-9 preparer/translator sections) | [useSignEmployeeForm](./useSignEmployeeForm.md) | +| `useBankForm` | Create a bank account for an employee (nickname, routing/account number, account type) | [useBankForm](./useBankForm.md) | +| `usePaymentMethodForm` | Switch an employee's payment method between Direct Deposit and Check | [usePaymentMethodForm](./usePaymentMethodForm.md) | +| `useSplitPaymentsForm` | Split a paycheck across multiple bank accounts (Percentage or Fixed amount, with reordering) | [useSplitPaymentsForm](./useSplitPaymentsForm.md) | +| `useDeductionForm` | Create or update a non-child-support deduction (court-ordered garnishment or post-tax custom) | [useDeductionForm](./useDeductionForm.md) | +| `useChildSupportGarnishmentForm` | Create or update a child-support garnishment (agency-keyed required attributes, county selection) | [useChildSupportGarnishmentForm](./useChildSupportGarnishmentForm.md) | + +--- + +## Getting Started + +All hooks are exported from `@gusto/embedded-react-sdk`. Your app must be wrapped in `GustoProvider`. + +```tsx +import { GustoProvider, useEmployeeDetailsForm } from '@gusto/embedded-react-sdk' + +function App() { + return ( + + + + ) +} + +function EmployeeForm({ companyId }: { companyId: string }) { + const employeeDetails = useEmployeeDetailsForm({ companyId }) + + if (employeeDetails.isLoading) { + return
Loading...
+ } + + const { Fields } = employeeDetails.form + + return ( +
{ + e.preventDefault() + await employeeDetails.actions.onSubmit() + }} + > + + + + + ) +} +``` + +### Key concepts + +1. **Call the hook** with the required identifiers (`companyId`, `employeeId`, etc.) +2. **Check `isLoading`** — the hook fetches server data before the form is ready +3. **Connect fields** — pass `formHookResult` as a prop to each field, or wrap fields in `SDKFormProvider` for context-based connection (see [Connecting Fields to the Form](#connecting-fields-to-the-form)) +4. **Render `Fields`** — each field is a pre-bound component that handles validation, error display, and metadata automatically +5. **Call `onSubmit`** — the hook handles API mutations, error normalization, and returns the saved entity + +--- + +## Connecting Fields to the Form + +Every Field component needs access to form state — react-hook-form's control, field metadata (required/disabled), and error messages. There are two ways to provide this connection: + +### Option A: `formHookResult` prop (explicit) + +Pass the hook result directly to each field. No wrapper component needed. + +```tsx +const { Fields } = employeeDetails.form + + + + +``` + +Each field reads metadata, form control, and error state directly from the prop. This is the most flexible approach — fields can be placed anywhere in your component tree and interleaved freely with fields from other hooks. + +### Option B: `SDKFormProvider` (context) + +Wrap fields in `SDKFormProvider` and they pick up form state from context automatically. + +```tsx +import { SDKFormProvider } from '@gusto/embedded-react-sdk' + +function EmployeeFormSection() { + // ...employeeDetails from useEmployeeDetailsForm + + const { Fields } = employeeDetails.form + + return ( + + + + + + ) +} +``` + +Fields inside an `SDKFormProvider` don't need the `formHookResult` prop — the provider injects form state via React context. This is convenient when all fields from a single hook are grouped together. + +### Choosing an approach + +Both approaches produce identical validation, API payloads, and behavior. The difference is purely in how fields discover their form state. + +| | `formHookResult` prop | `SDKFormProvider` | +| --------------------- | ------------------------------------------------------------------- | ----------------------------------------------- | +| **Best for** | Interleaving fields from multiple hooks; maximum layout flexibility | Grouping fields from a single hook together | +| **Boilerplate** | Each field receives the prop | One wrapper, fields are clean | +| **Interleaving** | Fields from different hooks can be placed in any order | Fields must stay within their provider boundary | +| **API error syncing** | Handled automatically per-field | Handled automatically via the provider | + +#### Side-by-side: two hooks on one screen + +The same `employeeDetails` + `compensation` form, written each way: + +```tsx +// Option A: formHookResult prop — fields can be interleaved freely +const EmployeeDetailsFields = employeeDetails.form.Fields +const CompensationFields = compensation.form.Fields + + + + + +``` + +```tsx +// Option B: SDKFormProvider — one wrapper per hook, fields stay grouped + + + + + + + + + +``` + +You can also mix approaches on the same page — for example, `SDKFormProvider` for one hook's fields that are grouped together, and `formHookResult` props for another hook's fields that are scattered. See [Composing Multiple Hooks](#composing-multiple-hooks) for the full submit-handler wiring around either layout. + +> **Avoid passing `formHookResult` to fields via props that are already inside an `SDKFormProvider`.** When both are present on the same field, the prop takes precedence and the provider's context is ignored, which may lead to unexpected behavior. + +--- + +## Field Rendering and Custom UI + +### Component Adapter integration + +By default, every field component renders through the SDK's [Component Adapter](../component-adapter/component-adapter.md). If you've configured a Component Adapter for your app (e.g., mapping to your own design system), hook fields will automatically render using your custom components. If no adapter is configured, fields render using the SDK's built-in React Aria-driven components. + +This means hooks inherit whatever UI customization you've already set up at the `GustoProvider` level -- no extra configuration needed. + +### Overriding a single field with `FieldComponent` + +If you need a specific field to render differently without changing your global Component Adapter, most fields accept a `FieldComponent` prop. This lets you swap the UI for a single field by providing your own component that conforms to the expected props interface. + +The `FieldComponent` receives the same props the underlying UI primitive expects (`TextInputProps`, `SelectProps`, `NumberInputProps`, etc.) -- including `value`, `onChange`, `onBlur`, error state, and accessibility attributes. You don't need any react-hook-form knowledge; the hook field handles all form binding and passes clean UI props to your component. + +```tsx +import type { TextInputProps } from '@gusto/embedded-react-sdk' + +function MyCustomTextInput(props: TextInputProps) { + return ( +
+ + props.onChange?.(e.target.value)} + onBlur={props.onBlur} + disabled={props.isDisabled} + required={props.isRequired} + /> + {props.errorMessage && {props.errorMessage}} +
+ ) +} + +// Then in your form: +; +``` + +This is useful when you want to use a third-party input library for one field, add custom styling, or render a completely different control while still getting the hook's validation, error handling, and form binding for free. + +The `FieldComponent` prop is available on all field types: `TextInputProps`, `SelectProps`, `NumberInputProps`, `CheckboxProps`, `DatePickerProps`, `RadioGroupProps`, and `SwitchProps`. Import the corresponding prop type from `@gusto/embedded-react-sdk` for type-safe implementations. + +--- + +## Data + +Every form hook returns a `data` object when ready. This contains the entities fetched by the hook — the primary entity being edited plus any supporting data needed for the form. + +```tsx +if (!employeeDetails.isLoading) { + const { employee } = employeeDetails.data + // employee is the loaded Employee entity (or null in create mode) +} +``` + +The shape of `data` varies by hook — see each hook's reference page for details: + +- `useEmployeeDetailsForm` — `{ employee }` +- `useJobForm` — `{ currentJob, jobs, employee, currentWorkAddress, showTwoPercentShareholder, showStateWc }` +- `useCompensationForm` — `{ compensation, currentJob, minimumWages, minimumEffectiveDate, maximumEffectiveDate, hasPendingFutureCompensation }` (plus `status.willDeleteSecondaryJobs` for the reactive carve-out flag) +- `useWorkAddressForm` — `{ workAddress, workAddresses, companyLocations }` +- `usePayScheduleForm` — `{ paySchedule, payPeriodPreview, payPreviewLoading, paymentSpeedDays }` +- `useSignCompanyForm` — `{ companyForm, pdfUrl }` + +--- + +## Required Fields + +Hooks let you declare which form fields are required beyond the built-in defaults. Each hook has built-in requiredness rules based on the form mode (create vs. update), and you can override optional fields to be required. + +The API varies by hook. Some hooks use `requiredFields` (flat array or per-mode object), while newer hooks use `optionalFieldsToRequire` with type-safe, mode-aware overrides. + +### `requiredFields` (useEmployeeDetailsForm, useWorkAddressForm) + +Pass a flat array (applies to both modes) or an object with per-mode arrays: + +```tsx +// Flat array: same requirements for both create and update +useEmployeeDetailsForm({ + companyId, + requiredFields: ['email', 'dateOfBirth'], +}) + +// Per-mode object: different requirements per mode +useEmployeeDetailsForm({ + companyId, + requiredFields: { + create: ['email'], + update: ['ssn', 'dateOfBirth'], + }, +}) +``` + +### `optionalFieldsToRequire` (useJobForm, useCompensationForm) + +Override specific fields that are optional in a given mode to be required. The type constrains which fields can be listed per mode — only fields that are actually optional in that mode are allowed: + +```tsx +useJobForm({ + employeeId, + jobId, + optionalFieldsToRequire: { + update: ['title'], + }, +}) + +useCompensationForm({ + employeeId, + jobId, + compensationId, + optionalFieldsToRequire: { + update: ['rate'], + }, +}) +``` + +Each hook's reference page documents which fields are available to require and which are required by default in each mode. See: + +- [useEmployeeDetailsForm required fields](./useEmployeeDetailsForm.md#required-fields) +- [useJobForm configurable required fields](./useJobForm.md#configurable-required-fields) +- [useCompensationForm configurable required fields](./useCompensationForm.md#configurable-required-fields) +- [useWorkAddressForm required fields](./useWorkAddressForm.md#required-fields) +- [usePayScheduleForm configurable required fields](./usePayScheduleForm.md#configurable-required-fields) + +--- + +## Default Values + +All form hooks accept a `defaultValues` prop to pre-fill the form. Pass a partial object matching the hook's form data shape — any fields you omit use built-in fallbacks (typically empty strings or `false`). + +```tsx +useEmployeeDetailsForm({ + companyId, + defaultValues: { + firstName: 'Jane', + email: 'jane@acme.com', + }, +}) + +useJobForm({ + employeeId, + defaultValues: { + title: 'Software Engineer', + hireDate: '2025-01-15', + }, +}) + +useCompensationForm({ + defaultValues: { + rate: 85000, + paymentUnit: 'Year', + flsaStatus: 'Exempt', + effectiveDate: '2025-01-15', + }, +}) +``` + +### Resolution order + +In **create mode** (no existing entity), `defaultValues` populate the form directly. In **update mode**, server data always takes precedence — `defaultValues` only fill in fields the server doesn't provide. + +Each hook's reference page documents the full form data shape accepted by `defaultValues`: + +- [useEmployeeDetailsForm form data](./useEmployeeDetailsForm.md#employeedetailsformdata) +- [useJobForm form data](./useJobForm.md#jobformdata) +- [useCompensationForm form data](./useCompensationForm.md#compensationformdata) +- [useWorkAddressForm form data](./useWorkAddressForm.md#workaddressformdata) +- [usePayScheduleForm form data](./usePayScheduleForm.md#payscheduleformdata) +- [useSignCompanyForm form data](./useSignCompanyForm.md#signcompanyformdata) + +--- + +## Loading States + +Every hook returns a discriminated union on `isLoading`. While server data is being fetched, only `isLoading` and `errorHandling` are available: + +```tsx +const employeeDetails = useEmployeeDetailsForm({ companyId, employeeId }) + +// Loading branch — no form data yet +if (employeeDetails.isLoading) { + return +} + +// Ready branch — TypeScript narrows to the full return type +const { data, form, actions, status, errorHandling } = employeeDetails +``` + +The loading state is also where you first encounter errors — if a data-fetching query fails, the hook stays in the loading branch but `errorHandling.errors` will be populated. See [Error Handling](#error-handling) below. + +--- + +## Error Handling + +All hooks return an `errorHandling` object in **both** loading and ready states. This ensures you can always display errors and offer recovery actions, even when data never loaded. + +```typescript +interface HookErrorHandling { + errors: SDKError[] + retryQueries: () => void + clearSubmitError: () => void +} +``` + +### Multi-hook screens + +When a screen pulls from more than one SDK hook (or mixes SDK hooks with additional `@gusto/embedded-api-v-2025-11-15` queries), combine their error state into one banner and one retry/dismiss flow using `composeErrorHandler` / `composeSubmitHandler`. See [Composing Multiple Hooks](#composing-multiple-hooks). + +### SDKError shape + +```typescript +interface SDKError { + category: 'api_error' | 'validation_error' | 'network_error' | 'internal_error' + message: string + httpStatus?: number + fieldErrors: SDKFieldError[] + raw?: unknown +} + +interface SDKFieldError { + field: string + category: string + message: string + metadata?: Record +} +``` + +### Error categories and recommended actions + +| Category | What happened | What you should do | +| ------------------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `api_error` | HTTP error from the Gusto API (422, 404, 409, etc.) | Display `error.message`. For 422 responses, check `error.fieldErrors` for inline field-level messages. For 404/409, show a contextual message to the user. | +| `validation_error` | Client-side schema validation failed before the request was sent | This is likely an SDK bug. Display a generic error and report to Gusto. | +| `network_error` | Network connectivity failure (timeout, connection refused) | Show retry UI using `errorHandling.retryQueries()`. Suggest the user check their connection. | +| `internal_error` | Unexpected SDK runtime error | Display a generic error and report to Gusto. | + +### Recovery actions + +- **`retryQueries()`** — Retries all failed data-fetching queries. Dependent queries automatically re-trigger when their dependencies resolve. +- **`clearSubmitError()`** — Clears the most recent form submission error from state. + +Explicit **`query` vs `submit` labels** on each `SDKError` are not part of the type today; infer recovery from **`retryQueries`** (fetch) vs **`clearSubmitError`** (submit). A future revision may add structured discrimination. + +### Example: error display with retry + +```tsx +function EmployeeForm({ companyId }: { companyId: string }) { + const employeeDetails = useEmployeeDetailsForm({ companyId }) + + if (employeeDetails.isLoading) { + const { errors, retryQueries } = employeeDetails.errorHandling + + if (errors.length > 0) { + return ( +
+

Failed to load employee data.

+
    + {errors.map((error, i) => ( +
  • {error.message}
  • + ))} +
+ +
+ ) + } + + return + } + + // ... render form +} +``` + +### Handling submit errors + +Submit errors (from API mutations) are also collected into `errorHandling.errors`. After a failed submission, you can display the error and let the user correct their input: + +```tsx +const { errors, clearSubmitError } = employeeDetails.errorHandling + +{ + errors.length > 0 && ( +
+ {errors.map((error, i) => ( +

{error.message}

+ ))} +
+ ) +} +``` + +Field-level API errors (e.g., 422 responses with `fieldErrors`) are automatically synced to the corresponding form fields so they appear inline alongside client-side validation errors. When using `SDKFormProvider`, the provider handles this syncing via context. When using the `formHookResult` prop, each field resolves errors directly from `formHookResult.errorHandling.errors` — no provider is needed. + +For a deeper look at the SDK's error architecture, see [Error Handling in the React SDK](../integration-guide/error-handling.md) and [Observability](../integration-guide/observability.md). + +--- + +## Submit Handler + +Each hook's `actions.onSubmit` is an async function that validates the form, calls the appropriate API mutations, and returns the result. + +```typescript +interface HookSubmitResult { + mode: 'create' | 'update' + data: T +} +``` + +`onSubmit` accepts optional callbacks that fire after each mutation step. This is useful for telemetry logging or reacting to individual API call results: + +```tsx +const result = await employeeDetails.actions.onSubmit({ + onEmployeeCreated: employee => { + console.log('Created:', employee.uuid) + }, + onEmployeeUpdated: employee => { + console.log('Updated:', employee.uuid) + }, +}) + +if (result) { + // result.mode is 'create' or 'update' + // result.data is the saved Employee entity + navigate(`/employees/${result.data.uuid}`) +} +``` + +If validation fails, `onSubmit` returns `undefined` and the form fields display their error messages. If a mutation fails, the error is captured in `errorHandling.errors`. + +### Checking pending state and mode + +Use `status.isPending` to disable the submit button while mutations are in flight, and `status.mode` to adapt your UI based on whether the hook is creating or updating: + +```tsx +

{employeeDetails.status.mode === 'create' ? 'Add Employee' : 'Edit Employee'}

+ + +``` + +`status.mode` is `'create'` when no existing entity was loaded (e.g., no `employeeId` was provided) and `'update'` when editing an existing record. + +--- + +## Validation Messages + +Each field component accepts a `validationMessages` prop that maps error codes to human-readable strings. Error codes are defined as typed constants, and TypeScript enforces that you provide a message for every code the field can produce. + +```tsx +import { EmployeeDetailsErrorCodes } from '@gusto/embedded-react-sdk' + + + + +``` + +If you omit `validationMessages`, validation still runs and the field is marked as invalid, but the displayed text falls back to the raw error code (e.g., `REQUIRED`, `INVALID_EMAIL`, `INVALID_AMOUNT`). Always supply `validationMessages` for production UI so you control the user-facing copy. + +Error codes for each hook are exported alongside the hook: + +- `EmployeeDetailsErrorCodes` — see [useEmployeeDetailsForm field reference](./useEmployeeDetailsForm.md#fields-reference) +- `JobErrorCodes` — see [useJobForm field reference](./useJobForm.md#fields-reference) +- `CompensationErrorCodes` — see [useCompensationForm field reference](./useCompensationForm.md#fields-reference) +- `WorkAddressErrorCodes` — see [useWorkAddressForm field reference](./useWorkAddressForm.md#fields-reference) +- `PayScheduleErrorCodes` — see [usePayScheduleForm field reference](./usePayScheduleForm.md#fields-reference) +- `SignCompanyFormErrorCodes` — see [useSignCompanyForm field reference](./useSignCompanyForm.md#fields-reference) + +--- + +## Composing Multiple Hooks + +A screen that combines multiple SDK hooks, or mixes SDK hooks with additional `@gusto/embedded-api-v-2025-11-15` queries, produces multiple `errorHandling` objects and (for form screens) multiple submit flows. Two small helpers stitch them together: + +- **`composeErrorHandler([sources])`** — merges many error sources into a single `HookErrorHandling`. +- **`composeSubmitHandler([forms], onAllValid)`** — coordinates validation and ordered submits across forms, and returns `{ handleSubmit, errorHandling }` where `errorHandling` is built from those forms via `composeErrorHandler` under the hood. + +### Combining data fetches with `composeErrorHandler` + +Use `composeErrorHandler` to produce a single `errorHandling` bag for any screen that reads from multiple sources. It accepts any mix of: + +- **SDK hook results** — objects with an `errorHandling` property (e.g., `useEmployeeDetailsForm`, `useCompensationForm`, or the return value of `composeSubmitHandler`). +- **`@gusto/embedded-api-v-2025-11-15` React Query results** — objects with `error` and `refetch` properties. + +```tsx +import { composeErrorHandler, useEmployeeDetailsForm } from '@gusto/embedded-react-sdk' +import { useEmployeeFormsList } from '@gusto/embedded-api-v-2025-11-15/react-query/employeeFormsList' + +function EmployeeProfileView({ companyId, employeeId }: { companyId: string; employeeId: string }) { + const employeeDetails = useEmployeeDetailsForm({ companyId, employeeId }) + const formsListQuery = useEmployeeFormsList({ employeeId }) + + const errorHandling = composeErrorHandler([employeeDetails, formsListQuery]) + + if (errorHandling.errors.length > 0) { + return ( +
+ {errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} + +
+ ) + } + + // ...render +} +``` + +`employeeDetails` is an SDK hook result (its `errorHandling` is delegated into), while `formsListQuery` is a raw `@gusto/embedded-api-v-2025-11-15` query (its `error` is normalized and its `refetch` is wired into `retryQueries`). The same call works for any combination of the two shapes. + +The returned `errorHandling` has the same shape as any SDK hook's `errorHandling`: + +- `errors: SDKError[]` — fetch errors from all sources. +- `retryQueries()` — refetches every failed query and delegates into nested hooks so their retries fire too. +- `clearSubmitError()` — clears submit errors across any nested hook results passed in. + +### Combining forms with `composeSubmitHandler` + +When multiple forms sit on the same page (e.g., employee details and compensation side by side), use `composeSubmitHandler` to coordinate validation, focus, and ordered submission across all of them. It returns both pieces you typically need: + +- **`handleSubmit`** — a form event handler that validates every form in parallel, focuses the first invalid field across forms (in array order) if any fail, and calls your `onAllValid` callback only when every form passes. +- **`errorHandling`** — a combined `HookErrorHandling` built from the forms via `composeErrorHandler` internally. No need to call `composeErrorHandler` yourself for the common case. + +```tsx +const { handleSubmit, errorHandling } = composeSubmitHandler( + [employeeDetails, compensation], + async () => { + await employeeDetails.actions.onSubmit() + await compensation.actions.onSubmit() + }, +) +``` + +If the same screen also has extra `@gusto/embedded-api-v-2025-11-15` queries that should feed the same error banner, pass the `composeSubmitHandler` result back into `composeErrorHandler` alongside those queries — the result already satisfies `composeErrorHandler`'s input shape: + +```tsx +const submitResult = composeSubmitHandler([employeeDetails, compensation], onAllValid) + +const errorHandling = composeErrorHandler([submitResult, extraQuery]) +``` + +### Setup + +Each form hook must be initialized with `shouldFocusError: false` so that react-hook-form's per-form focus is disabled and `composeSubmitHandler` can manage cross-form focus instead. + +Both connection approaches work with composition. Choose the one that fits your layout. + +#### Grouped layout with `SDKFormProvider` + +When fields from each hook are grouped into their own sections, `SDKFormProvider` keeps things clean: + +```tsx +import { + useEmployeeDetailsForm, + useCompensationForm, + composeSubmitHandler, + SDKFormProvider, +} from '@gusto/embedded-react-sdk' + +function OnboardingPage({ companyId, employeeId }: { companyId: string; employeeId: string }) { + const employeeDetails = useEmployeeDetailsForm({ + companyId, + employeeId, + shouldFocusError: false, + }) + + const compensation = useCompensationForm({ + employeeId, + shouldFocusError: false, + }) + + if (employeeDetails.isLoading || compensation.isLoading) { + return + } + + const EmployeeDetailsFields = employeeDetails.form.Fields + const CompensationFields = compensation.form.Fields + + const { handleSubmit, errorHandling } = composeSubmitHandler( + [employeeDetails, compensation], + async () => { + await employeeDetails.actions.onSubmit() + await compensation.actions.onSubmit() + }, + ) + + return ( +
+ {errorHandling.errors.length > 0 && ( +
+ {errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + +

Employee Details

+ + +
+ + +

Compensation

+ + +
+ + +
+ ) +} +``` + +Each `SDKFormProvider` scopes field metadata and error syncing to its respective hook. The outer `
` element uses the composed submit handler, and the combined `errorHandling` drives a single banner covering fetch failures from either hook and submit failures from any of the `onSubmit` calls. + +#### Interleaved layout with `formHookResult` prop + +When you want to mix fields from different hooks in any order — for example, placing job title next to first name, or grouping fields by theme rather than domain — use the `formHookResult` prop. There are no provider boundaries to manage, so fields can go anywhere: + +```tsx +import { + useEmployeeDetailsForm, + useCompensationForm, + useWorkAddressForm, + composeSubmitHandler, +} from '@gusto/embedded-react-sdk' + +function OnboardingPage({ companyId, employeeId }: { companyId: string; employeeId: string }) { + const employeeDetails = useEmployeeDetailsForm({ + companyId, + employeeId, + shouldFocusError: false, + }) + + const compensation = useCompensationForm({ + employeeId, + shouldFocusError: false, + }) + + const workAddress = useWorkAddressForm({ + companyId, + employeeId, + shouldFocusError: false, + }) + + if (employeeDetails.isLoading || compensation.isLoading || workAddress.isLoading) { + return + } + + const EmployeeDetailsFields = employeeDetails.form.Fields + const CompensationFields = compensation.form.Fields + const WorkAddressFields = workAddress.form.Fields + + const { handleSubmit, errorHandling } = composeSubmitHandler( + [employeeDetails, compensation, workAddress], + async () => { + await employeeDetails.actions.onSubmit() + await compensation.actions.onSubmit() + await workAddress.actions.onSubmit() + }, + ) + + return ( + + {errorHandling.errors.length > 0 && ( +
+ {errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + +
+

Who

+ + + + +
+ +
+

Role and Location

+ + + + +
+ + + + ) +} +``` + +Fields from `employeeDetails`, `compensation`, and `workAddress` are freely interleaved — each field knows which hook it belongs to via its `formHookResult` prop. Validation, error handling, and submission all work identically to the provider-based approach. + +### Composing Job + Compensation + +Jobs and compensations are separate entities in the Gusto API and most product flows compose both hooks on the same screen. See [Working with Jobs and Compensations](./jobs-and-compensations.md) for the full onboarding-stub-fill and steady-state-edit patterns. + +--- + +### Submit-time entity ID resolution + +In a create flow, the employee doesn't exist yet — so `useCompensationForm` and `useWorkAddressForm` can't receive an `employeeId` at init time. Both hooks accept `employeeId` as optional in their props and allow it to be provided at submit time via the `options` parameter: + +```tsx +function CreateOnboardingPage({ companyId }: { companyId: string }) { + const employeeDetails = useEmployeeDetailsForm({ + companyId, + shouldFocusError: false, + }) + + const compensation = useCompensationForm({ + shouldFocusError: false, + }) + + const workAddress = useWorkAddressForm({ + companyId, + shouldFocusError: false, + }) + + // ...loading checks... + + const { handleSubmit } = composeSubmitHandler( + [employeeDetails, compensation, workAddress], + async () => { + const employeeResult = await employeeDetails.actions.onSubmit() + if (!employeeResult) return + + const newEmployeeId = employeeResult.data.uuid + + await compensation.actions.onSubmit({ employeeId: newEmployeeId }) + await workAddress.actions.onSubmit(undefined, { employeeId: newEmployeeId }) + }, + ) + + // ...render forms... +} +``` + +When `employeeId` is omitted from props, the hooks skip data fetching and render in create mode with empty defaults. The ID is resolved at submit time, avoiding re-render cycles that would tear down the form UI. + +### Handling submission failures + +`composeSubmitHandler` takes care of client-side validation — your `onAllValid` callback only runs when every form passes. However, API mutations inside the callback can still fail. When they do, `onSubmit` returns `undefined` (it never throws) and the error is automatically captured in `errorHandling.errors` for display. + +Early return when a subsequent call depends on data from a prior call: + +```tsx +const { handleSubmit, errorHandling } = composeSubmitHandler( + [employeeDetails, compensation, workAddress], + async () => { + const employeeResult = await employeeDetails.actions.onSubmit() + if (!employeeResult) return + + const newEmployeeId = employeeResult.data.uuid + + await compensation.actions.onSubmit({ employeeId: newEmployeeId }) + await workAddress.actions.onSubmit(undefined, { employeeId: newEmployeeId }) + }, +) +``` + +Here `compensation` and `workAddress` both need the employee ID, so if employee creation fails there's nothing to pass and no reason to continue. The user will see the error from `errorHandling.errors` and can retry. + +For independent submissions where one doesn't depend on the other's result, continuing after a failure is a valid choice — it depends on your product requirements. + +--- + +## Reading Form Values + +Each hook exposes `form.getFormSubmissionValues()` — a synchronous function that returns the current form values parsed through the hook's Zod validation schema. The returned object matches exactly what `onSubmit` would receive: all preprocessing transforms (e.g., string-to-number coercion) are applied. + +Returns `undefined` when the current form state is invalid (empty required fields, failed cross-field rules, etc.). It never throws. + +```tsx +const values = employeeDetails.form.getFormSubmissionValues() + +if (values) { + console.log(values.firstName, values.lastName) +} +``` + +This is particularly useful when you need to share values across form submissions. For example, when the work address form captures an effective date that the compensation form needs as its start date, you can read the value from one form and pass it to the other's submit options: + +```tsx +const workAddress = useWorkAddressForm({ companyId, shouldFocusError: false }) +const compensation = useCompensationForm({ + withStartDateField: false, + shouldFocusError: false, +}) + +// ...loading checks... + +const { handleSubmit } = composeSubmitHandler( + [employeeDetails, workAddress, compensation], + async () => { + const employeeResult = await employeeDetails.actions.onSubmit() + if (!employeeResult) return + + const newEmployeeId = employeeResult.data.uuid + const workAddressValues = workAddress.form.getFormSubmissionValues() + + await workAddress.actions.onSubmit(undefined, { employeeId: newEmployeeId }) + await compensation.actions.onSubmit({ + employeeId: newEmployeeId, + startDate: workAddressValues?.effectiveDate, + }) + }, +) +``` + +`getFormSubmissionValues` has no side effects — it doesn't trigger re-renders, mutate form state, or update validation errors. It's a pure read from react-hook-form's internal store followed by Zod schema parsing. + +--- + +## Advanced: Hook Form Internals + +Each hook exposes `form.hookFormInternals` which provides direct access to the underlying react-hook-form `formMethods` (`UseFormReturn`). This is an escape hatch for advanced use cases that aren't covered by the hook's built-in API. + +```tsx +const { formMethods } = employeeDetails.form.hookFormInternals + +formMethods.watch('email') +formMethods.setValue('firstName', 'Jane') +formMethods.trigger('ssn') +``` + +Use this when you need to: + +- Watch specific fields for reactive UI updates outside of the SDK fields +- Programmatically set or reset field values +- Trigger validation on specific fields manually +- Access form state like `isDirty`, `isValid`, or `dirtyFields` + +In most cases the built-in Fields, `onSubmit`, and `getFormSubmissionValues` are sufficient. Reach for `hookFormInternals` only when you need fine-grained form control that the hook doesn't expose directly. diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/jobs-and-compensations.md b/docs-site/versioned_docs/version-0.46.3/hooks/jobs-and-compensations.md new file mode 100644 index 000000000..3e1b0d7f8 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/jobs-and-compensations.md @@ -0,0 +1,209 @@ +--- +title: Working with Jobs and Compensations +order: 5 +--- + +# Working with Jobs and Compensations + +Jobs and compensations are separate entities in the Gusto API: + +- A **job** (`POST /v1/employees/:id/jobs`, `PUT /v1/jobs/:id`) carries title, hire date, S-Corp 2% shareholder flag, and Washington state workers' comp fields. Modeled by [`useJobForm`](./useJobForm.md). +- A **compensation** (`POST /v1/jobs/:jobId/compensations`, `PUT /v1/compensations/:id`) carries FLSA status, pay rate, payment unit, and effective date. Modeled by [`useCompensationForm`](./useCompensationForm.md). + +Most product flows compose both hooks on the same screen. This page covers the two patterns you'll reach for and how to wire them up with [`composeSubmitHandler`](./hooks.md#composing-multiple-hooks). + +--- + +## Onboarding stub-fill (POST job → PUT auto-created stub) + +When a job is created, the API auto-creates a stub compensation with `rate: 0`. Onboarding flows replace that stub with the real values via PUT. `useCompensationForm.actions.onSubmit` accepts `{ jobId, compensationId, compensationVersion }` for this exact case: + +```tsx +import { + useJobForm, + useCompensationForm, + composeSubmitHandler, + SDKFormProvider, +} from '@gusto/embedded-react-sdk' + +function OnboardingCompensationPage({ employeeId }: { employeeId: string }) { + const jobForm = useJobForm({ employeeId, shouldFocusError: false }) + const compensationForm = useCompensationForm({ employeeId, shouldFocusError: false }) + + if (jobForm.isLoading || compensationForm.isLoading) return + + const JobFields = jobForm.form.Fields + const CompFields = compensationForm.form.Fields + + const { handleSubmit, errorHandling } = composeSubmitHandler( + [jobForm, compensationForm], + async () => { + const jobResult = await jobForm.actions.onSubmit() + if (!jobResult || jobResult.mode !== 'create') return + + const job = jobResult.data + const compensationId = job.currentCompensationUuid + const stub = job.compensations?.find(c => c.uuid === compensationId) + + await compensationForm.actions.onSubmit({ + jobId: job.uuid, + compensationId, + compensationVersion: stub?.version, + }) + }, + ) + + return ( +
+ {errorHandling.errors.length > 0 && ( +
+ {errorHandling.errors.map((e, i) => ( +

{e.message}

+ ))} +
+ )} + + + + + {JobFields.TwoPercentShareholder && ( + + )} + {JobFields.StateWcCovered && } + {JobFields.StateWcClassCode && ( + + )} + + + + {CompFields.FlsaStatus && ( + + )} + + + + {CompFields.AdjustForMinimumWage && ( + + )} + {CompFields.MinimumWageId && ( + + )} + + + +
+ ) +} +``` + +`composeSubmitHandler` validates both forms in parallel — if either fails, the chain short-circuits before any network I/O. Inside `onAllValid`, thread the new IDs and the stub's version into `useCompensationForm` to PUT it. + +### Driving hireDate / effectiveDate from external context + +To submit `hireDate` and `effectiveDate` without rendering input fields for them, set `withHireDateField: false` and `withEffectiveDateField: false` and supply the values via submit options. The schemas drop those fields from validation and `Fields.HireDate` / `Fields.EffectiveDate` become `undefined`, so the TypeScript build flags any accidental renders. + +```tsx +const jobForm = useJobForm({ + employeeId, + withHireDateField: false, + shouldFocusError: false, +}) +const compensationForm = useCompensationForm({ + employeeId, + withEffectiveDateField: false, + shouldFocusError: false, +}) + +const { handleSubmit } = composeSubmitHandler([jobForm, compensationForm], async () => { + const jobResult = await jobForm.actions.onSubmit({ employeeId, hireDate: startDate }) + if (!jobResult) return + + const job = jobResult.data + const stub = job.compensations?.find(c => c.uuid === job.currentCompensationUuid) + await compensationForm.actions.onSubmit({ + jobId: job.uuid, + compensationId: job.currentCompensationUuid ?? undefined, + compensationVersion: stub?.version, + effectiveDate: startDate, + }) +}) +``` + +When `withEffectiveDateField: false`, `useCompensationForm` is strictly options-only — `effective_date` is omitted from the PUT body unless you supply it via submit options. Omit `effectiveDate` from the options entirely to leave the existing date untouched. + +--- + +## Steady-state edit (job + compensation already exist) + +When both records exist (a page that edits the current compensation), pass IDs to both hooks and submit them in parallel. The hooks own their own version handling: + +```tsx +function CompensationEditPage({ + employeeId, + jobId, + compensationId, +}: { + employeeId: string + jobId: string + compensationId: string +}) { + const jobForm = useJobForm({ employeeId, jobId, shouldFocusError: false }) + const compensationForm = useCompensationForm({ + employeeId, + jobId, + compensationId, + shouldFocusError: false, + }) + + if (jobForm.isLoading || compensationForm.isLoading) return + + const { handleSubmit, errorHandling } = composeSubmitHandler( + [jobForm, compensationForm], + async () => { + await jobForm.actions.onSubmit() + await compensationForm.actions.onSubmit() + }, + ) + + return ( +
+ {compensationForm.status.willDeleteSecondaryJobs && ( + Saving will delete this employee's secondary jobs. + )} + + {/* ...fields, errorHandling banner, submit button */} +
+ ) +} +``` + +`compensationForm.status.willDeleteSecondaryJobs` is a reactive flag that flips to `true` when the form is positioned on the carve-out branch: update mode, loaded compensation is `Nonexempt`, the form's `flsaStatus` was just changed to a non-`Nonexempt` value, and the employee has at least one secondary job. While the flag is on, the hook also locks the `effectiveDate` field — it forces the form value to today and renders the field disabled (via `fieldsMetadata.effectiveDate.isDisabled`). Reverting `flsaStatus` back to `Nonexempt` restores the prior date. You can render the disabled `Fields.EffectiveDate` as-is or skip it entirely and rely on the inline warning. The hook tracks form state via `useWatch` internally — a render-time read of the flag is enough. See [derived helpers](./useCompensationForm.md#derived-helpers) for the full breakdown. diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useBankForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useBankForm.md new file mode 100644 index 000000000..013d6cf59 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useBankForm.md @@ -0,0 +1,269 @@ +--- +title: useBankForm +order: 7 +--- + +# useBankForm + +Creates an employee bank account — nickname, routing number, account number, and account type. Creating a bank account also updates the employee's payment method on the Gusto API. + +```tsx +import { useBankForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +--- + +## Props + +`useBankForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `employeeId` | `string` | No | — | The UUID of the employee. For composed create flows where the id isn't known until a prior form submits, omit and supply at submit time via `BankFormSubmitOptions.employeeId`. | +| `optionalFieldsToRequire` | `BankFormOptionalFieldsToRequire` | No | — | Override optional fields to be required. Today every field is required by default, so this is reserved for future schema expansion. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. `accountType` defaults to `'Checking'` when not supplied. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | When validation runs. Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### BankFormData + +The shape of `defaultValues`: + +```typescript +interface BankFormData { + name: string // Account nickname + routingNumber: string // 9-digit routing number + accountNumber: string // 1–17 digit account number + accountType: 'Checking' | 'Savings' +} +``` + +The constant `ACCOUNT_TYPES` (`['Checking', 'Savings']`) is exported for convenience. + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +`useBankForm` does not fetch any server data, so in practice it transitions to the ready state immediately. The loading branch exists to keep the return shape consistent with other form hooks. + +### Ready state + +```typescript +{ + isLoading: false + data: Record + status: { + isPending: boolean + mode: 'create' + } + actions: { + onSubmit: (options?: BankFormSubmitOptions) => + Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: BankFormFields + fieldsMetadata: BankFormFieldsMetadata + hookFormInternals: { + formMethods: UseFormReturn + } + getFormSubmissionValues: () => BankFormOutputs | undefined + } +} +``` + +### Submit options + +```typescript +interface BankFormSubmitOptions { + /** Override the `employeeId` configured at hook construction. Useful when the employee is created in the same submit chain. */ + employeeId?: string +} +``` + +```tsx +await bankForm.actions.onSubmit({ employeeId: newEmployeeId }) +``` + +`onSubmit` POSTs `/v1/employees/:id/bank_accounts` and returns `HookSubmitResult` with `mode: 'create'` and `data` set to the newly created bank account. Returns `undefined` if validation fails or the mutation errors (errors are captured in `errorHandling.errors`). + +--- + +## Fields Reference + +All fields accept `label` (required) and `description` (optional). Fields with validation accept `validationMessages` mapping error codes to display strings. + +### Error Codes + +```typescript +const BankFormErrorCodes = { + REQUIRED: 'REQUIRED', + INVALID_ROUTING_NUMBER: 'INVALID_ROUTING_NUMBER', + INVALID_ACCOUNT_NUMBER: 'INVALID_ACCOUNT_NUMBER', +} as const +``` + +| Field | Input type | Required by default | Error codes | Conditional availability | +| --------------- | ------------------- | ------------------- | ------------------------------------ | ----------------------------------------------------- | +| `Name` | Text input | Yes | `REQUIRED` | Always rendered. | +| `RoutingNumber` | Text input | Yes | `REQUIRED`, `INVALID_ROUTING_NUMBER` | Always rendered. Validates against `/^[0-9]{9}$/`. | +| `AccountNumber` | Text input | Yes | `REQUIRED`, `INVALID_ACCOUNT_NUMBER` | Always rendered. Validates against `/^[0-9]{1,17}$/`. | +| `AccountType` | Radio (two options) | Yes (has a default) | `REQUIRED` | Always rendered. Defaults to `'Checking'`. | + +`AccountType` always carries a non-empty default, so `REQUIRED` won't fire in practice and `validationMessages` can be omitted on that field. Supply `getOptionLabel` to translate the `Checking` / `Savings` labels in your UI. + +--- + +## Usage Examples + +### With `SDKFormProvider` (context) + +```tsx +import { + useBankForm, + SDKFormProvider, + type UseBankFormReady, + type AccountType, +} from '@gusto/embedded-react-sdk' + +function AddBankAccountPage({ employeeId }: { employeeId: string }) { + const bankForm = useBankForm({ employeeId }) + + if (bankForm.isLoading) { + return
Loading...
+ } + + return +} + +function AddBankAccountFormReady({ bankForm }: { bankForm: UseBankFormReady }) { + const { Fields } = bankForm.form + + const handleSubmit = async () => { + const result = await bankForm.actions.onSubmit() + if (result) { + console.log('Created bank account', result.data.uuid) + } + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > +

Add bank account

+ + {bankForm.errorHandling.errors.length > 0 && ( +
+ {bankForm.errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + + + + (type === 'Checking' ? 'Checking' : 'Savings')} + /> + + + +
+ ) +} +``` + +### With `formHookResult` prop + +The same form using prop-based field connection. No `SDKFormProvider` wrapper needed: + +```tsx +import { useBankForm, type UseBankFormReady } from '@gusto/embedded-react-sdk' + +function AddBankAccountPage({ employeeId }: { employeeId: string }) { + const bankForm = useBankForm({ employeeId }) + + if (bankForm.isLoading) { + return
Loading...
+ } + + return +} + +function AddBankAccountFormReady({ bankForm }: { bankForm: UseBankFormReady }) { + const { Fields } = bankForm.form + + return ( +
{ + e.preventDefault() + void bankForm.actions.onSubmit() + }} + > + + + + + + + + ) +} +``` + +Both examples produce identical validation, error handling, and API behavior. diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useChildSupportGarnishmentForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useChildSupportGarnishmentForm.md new file mode 100644 index 000000000..d173a501b --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useChildSupportGarnishmentForm.md @@ -0,0 +1,210 @@ +--- +title: useChildSupportGarnishmentForm +order: 8 +--- + +# useChildSupportGarnishmentForm + +Creates or updates a child-support garnishment. Unlike standard garnishments, child support requires agency-specific attributes (case number, order number, remittance number) that vary by state, plus an optional county selection when the state has multiple counties. The hook loads the agency catalog from the Gusto API, derives which attributes the selected state requires, and exposes the right Fields conditionally. + +```tsx +import { useChildSupportGarnishmentForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +--- + +## Props + +`useChildSupportGarnishmentForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------ | -------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------- | +| `employeeId` | `string` | Yes | — | The UUID of the employee. | +| `garnishmentId` | `string` | No | — | When present → **update** mode. When absent → **create** mode. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data takes precedence on update. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### ChildSupportGarnishmentFormData + +```typescript +interface ChildSupportGarnishmentFormData { + state: string // state code, e.g. 'AK' + fipsCode: string // county FIPS code; auto-filled when the state has a single all-counties code + caseNumber: string + orderNumber: string + remittanceNumber: string + payPeriodMaximum: number // currency + amount: number // percentage 0–100 + paymentPeriod: 'Every week' | 'Every other week' | 'Twice per month' | 'Monthly' +} +``` + +--- + +## Return Type + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +interface UseChildSupportGarnishmentFormReady { + isLoading: false + data: { + /** Agency entries for the `State` select; raw records for getOptionLabel translation. */ + agencies: Array<{ state: string; name: string; manualPaymentRequired?: boolean }> + /** Counties for the currently selected state. Empty when no state is selected. */ + counties: Array<{ fipsCode: string; county: string | null }> + /** The garnishment loaded for update; `null` in create mode. */ + deduction: Garnishment | null + } + status: { + isPending: boolean + mode: 'create' | 'update' + /** The full agency record matching the currently selected `state`. */ + selectedAgency: Agencies | null + /** Convenient for surfacing a warning alert above the form. */ + isManualPaymentRequired: boolean + /** Which `required_attributes` keys the selected agency declares. */ + requiredAttrKeys: ReadonlySet<'case_number' | 'order_number' | 'remittance_number'> + } + actions: { + onSubmit: () => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: ChildSupportGarnishmentFormFields + fieldsMetadata: ChildSupportGarnishmentFormFieldsMetadata + hookFormInternals: HookFormInternals + getFormSubmissionValues: () => ChildSupportGarnishmentFormData | undefined + } +} +``` + +--- + +## Fields Reference + +| Field | Input type | Required by default | Error codes | Conditional availability | +| ------------------ | ----------- | ------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------- | +| `State` | Select | Yes | `REQUIRED` | Always | +| `FipsCode` | Select | Yes | `REQUIRED` | Only when the selected agency has multiple counties (single all-counties codes auto-fill silently) | +| `CaseNumber` | TextInput | Yes (when present) | `REQUIRED` | Only when `status.requiredAttrKeys.has('case_number')` | +| `OrderNumber` | TextInput | Yes (when present) | `REQUIRED` | Only when `status.requiredAttrKeys.has('order_number')` | +| `RemittanceNumber` | TextInput | Yes (when present) | `REQUIRED` | Only when `status.requiredAttrKeys.has('remittance_number')` | +| `PayPeriodMaximum` | NumberInput | Yes | `REQUIRED`, `NEGATIVE_AMOUNT` | Always | +| `Amount` | NumberInput | Yes | `REQUIRED`, `PERCENT_OUT_OF_RANGE` | Always (percentage 0–100) | +| `PaymentPeriod` | Select | Yes | `REQUIRED` | Always | + +> Schema-level validation for the three agency-attribute fields (`CaseNumber`, `OrderNumber`, `RemittanceNumber`) is intentionally permissive — the Gusto API enforces presence. The hook surfaces `status.requiredAttrKeys` so consumers know which to mark required in the UI; the Fields' presence on `form.Fields` follows the same rule. + +--- + +## Usage Example + +```tsx +import { useChildSupportGarnishmentForm, SDKFormProvider } from '@gusto/embedded-react-sdk' + +function ChildSupportPage({ + employeeId, + garnishmentId, +}: { + employeeId: string + garnishmentId?: string +}) { + const form = useChildSupportGarnishmentForm({ employeeId, garnishmentId }) + + if (form.isLoading) return

Loading…

+ + const { Fields } = form.form + const { selectedAgency, isManualPaymentRequired } = form.status + + const handleSubmit = async () => { + const result = await form.actions.onSubmit() + if (result) { + // result.mode is 'create' or 'update'; result.data is the saved Garnishment + } + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > + entry.name} + validationMessages={{ REQUIRED: 'Required' }} + /> + {isManualPaymentRequired &&

Manual payment required for this state.

} + {selectedAgency && ( + <> + {Fields.FipsCode && ( + entry.county ?? 'All counties'} + validationMessages={{ REQUIRED: 'Required' }} + /> + )} + {Fields.CaseNumber && ( + + )} + {Fields.OrderNumber && ( + + )} + {Fields.RemittanceNumber && ( + + )} + + + value} + validationMessages={{ REQUIRED: 'Required' }} + /> + + + )} + +
+ ) +} +``` + +--- + +## Related + +- [`useDeductionForm`](./useDeductionForm.md) — non-child-support deductions (court-ordered garnishments and post-tax custom) diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useCompensationForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useCompensationForm.md new file mode 100644 index 000000000..cc98fefdd --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useCompensationForm.md @@ -0,0 +1,509 @@ +--- +title: useCompensationForm +order: 3 +--- + +# useCompensationForm + +Creates or updates a compensation row on a job — FLSA classification, pay rate, payment unit, effective date, optional minimum-wage adjustment. Pairs with [`useJobForm`](./useJobForm.md): jobs and their compensations are separate entities in the Gusto API, and this hook focuses exclusively on the compensation side. + +```tsx +import { useCompensationForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +> **Looking for `jobTitle`, `hireDate`, `twoPercentShareholder`, `stateWcCovered` / `stateWcClassCode`?** Those moved to [`useJobForm`](./useJobForm.md). Compensation now models only what `POST /v1/jobs/:jobId/compensations` and `PUT /v1/compensations/:id` accept. + +> **Composing with `useJobForm`?** See [Working with Jobs and Compensations](./jobs-and-compensations.md) for end-to-end patterns covering onboarding stub-fill (POST job → PUT auto-created stub) and steady-state edits. + +--- + +## Props + +`useCompensationForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `employeeId` | `string` | No | — | The UUID of the employee. Drives data fetching for derived helpers (jobs list, work address, minimum wages). Optional for composed flows. | +| `jobId` | `string` | No | — | The UUID of the parent job. Required in **create** mode (scopes `POST /v1/jobs/:jobId/compensations`). Optional in **update** mode — the parent job is derived from the loaded compensation. Can also be passed at submit time when the job is just-created. | +| `compensationId` | `string` | No | — | When present → **update** mode (PUT /v1/compensations/:id). When absent → **create** mode (POST /v1/jobs/:jobId/compensations). | +| `optionalFieldsToRequire` | `CompensationOptionalFieldsToRequire` | No | — | Override fields that are optional in a given mode to be required. See [Configurable Required Fields](#configurable-required-fields). | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data takes precedence on update. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | +| `withEffectiveDateField` | `boolean` | No | `true` | When `false`, hides `Fields.EffectiveDate` and drops `effectiveDate` from schema validation. Supply the value via `CompensationSubmitOptions.effectiveDate` at submit time (e.g. from the parent job's `hireDate` during onboarding). | + +### Configurable Required Fields + +| Field | Rule | Required on create | Required on update | Configurable? | +| ---------------------- | ---------- | --------------------- | --------------------- | ----------------- | +| `flsaStatus` | `'create'` | Yes | No | Yes (on update) | +| `paymentUnit` | `'create'` | Yes | No | Yes (on update) | +| `rate` | `'create'` | Yes | No | Yes (on update) | +| `effectiveDate` | `'create'` | Yes | No | Yes (on update) | +| `title` | `'never'` | No | No | Yes (either mode) | +| `adjustForMinimumWage` | (always) | Yes | Yes | No | +| `minimumWageId` | predicate | When the toggle is on | When the toggle is on | No | + +```typescript +type CompensationOptionalFieldsToRequire = { + create?: Array<'title'> + update?: Array<'title' | 'flsaStatus' | 'paymentUnit' | 'rate' | 'effectiveDate'> +} +``` + +`title` is intentionally optional in both modes because you'll typically thread it through `useJobForm.Fields.Title` (where it's required on create). It remains here as an optional convenience when you're building a single-form steady-state edit screen. + +`minimumWageId` is automatically required when `adjustForMinimumWage` is `true` regardless of `optionalFieldsToRequire`. + +### CompensationFormData + +The shape of `defaultValues`: + +```typescript +interface CompensationFormData { + title: string + flsaStatus?: FlsaStatusType // 'Exempt' | 'Salaried Nonexempt' | 'Nonexempt' | 'Owner' | 'Commission Only Exempt' | 'Commission Only Nonexempt' + rate: number + paymentUnit: PaymentUnit // 'Hour' | 'Week' | 'Month' | 'Year' | 'Paycheck' + effectiveDate: string | null // ISO date string (YYYY-MM-DD) or null + adjustForMinimumWage: boolean + minimumWageId: string +} +``` + +When the hook is given a `compensationId` (update mode) or its parent job has a current compensation, `flsaStatus` is seeded from that row. In create mode without a parent compensation, the hook falls back to the employee's primary job's current FLSA status (so adding a secondary job stays consistent with the primary by default), then to `defaultValues.flsaStatus`. If none of those are available the field renders empty — preselect a value by passing `defaultValues.flsaStatus`. Requiredness is enforced on submit per the table above. + +--- + +## Verb routing + +The hook auto-routes between create and update based on `compensationId` (and submit options): + +| Hook config / submit options | Mode | API call | +| -------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------- | +| `{ jobId, compensationId }` | update | `PUT /v1/compensations/:compensationId` (with `version`) | +| `{ jobId }` (no `compensationId`) | create | `POST /v1/jobs/:jobId/compensations` | +| `{ employeeId }` + submit `{ jobId, compensationId, compensationVersion }` | update | `PUT /v1/compensations/:compensationId` (with the supplied `version`) | +| `{ employeeId }` + submit `{ jobId }` (no `compensationId`) | create | `POST /v1/jobs/:options.jobId/compensations` | + +Use the submit-options form for the **onboarding stub-fill** chain: after `useJobForm.actions.onSubmit()` creates a job, capture the auto-created compensation's UUID and version from the response, and pass them as `{ jobId, compensationId, compensationVersion }` to this hook's `onSubmit` to PUT the stub. + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +{ + isLoading: false + data: { + compensation: Compensation | null // the loaded comp; null in create mode + currentJob: Job | null // the parent job; resolved from compensationId in update mode, or jobId in create mode + minimumWages: MinimumWage[] + minimumEffectiveDate: string | null // typically the parent job's hireDate + maximumEffectiveDate: string | null // the next future-dated comp's effective date, when one exists + hasPendingFutureCompensation: boolean + } + status: { + isPending: boolean + mode: 'create' | 'update' + willDeleteSecondaryJobs: boolean // see "Derived helpers" below + showCommissionFederalMinimumPayAlert: boolean // see "Derived helpers" below + showCommissionMinimumWageAlert: boolean // see "Derived helpers" below + showOwnerSalaryAlert: boolean // see "Derived helpers" below + } + actions: { + onSubmit: ( + options?: CompensationSubmitOptions, + ) => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: CompensationFormFields + fieldsMetadata: CompensationFieldsMetadata + hookFormInternals: { formMethods: UseFormReturn } + getFormSubmissionValues: () => CompensationFormOutputs | undefined + } +} +``` + +### Submit options + +```typescript +interface CompensationSubmitOptions { + /** Override jobId — required when creating a compensation if not configured at hook construction (e.g. when the parent job was just created in the same submit chain). */ + jobId?: string + /** Override compensationId — when present, forces update (PUT) routing regardless of hook construction. */ + compensationId?: string + /** + * Compensation version for optimistic locking on PUT. Required when forcing + * update routing post-create (e.g. updating the auto-created stub returned + * from POST /v1/employees/:id/jobs). When omitted, the hook reads the + * version from its cached `currentCompensation`. + */ + compensationVersion?: string + /** + * Supply `effectiveDate` at submit time. When `withEffectiveDateField` + * is `true`, this overrides the form's value. When `withEffectiveDateField` + * is `false`, this is the only way to put `effective_date` on the wire — + * the form value is not read in that mode (matching the options-only + * convention of `useWorkAddressForm` / `useHomeAddressForm` / `useJobForm`). + */ + effectiveDate?: string +} +``` + +`onSubmit` resolves to a `HookSubmitResult` containing both the mode (`'create' | 'update'`) and the saved `Compensation` entity — read the result directly rather than wiring step callbacks. + +--- + +## Derived helpers + +The hook exposes derived values for driving UX. Static, entity-derived values live under `data.*`; reactive values that flip with form input live under `status.*`. + +- **`status.willDeleteSecondaryJobs`** — reactive: `true` when the form is currently positioned to delete the employee's secondary jobs server-side (the "carve-out" branch). Conditions: update mode, the loaded compensation is `Nonexempt`, the form's `flsaStatus` has been changed to a non-`Nonexempt` value, and the employee has at least one secondary job. While this flag is `true` the hook also locks the `effectiveDate` field — it forces the form value to today and exposes `fieldsMetadata.effectiveDate.isDisabled = true` so `Fields.EffectiveDate` renders as disabled. Reverting `flsaStatus` back to `Nonexempt` restores the prior `effectiveDate`. Use the flag to render an inline warning ("Saving will delete this employee's secondary jobs"); choose either to render the disabled `Fields.EffectiveDate` (so users can see why the date is forced) or to hide it entirely while the flag is on. +- **`status.showCommissionFederalMinimumPayAlert`** — reactive: `true` when `flsaStatus` is `Commission Only Exempt` (Commission Only/No Overtime). Render a warning explaining that commission-only exempt employees must still earn the federal minimum pay (currently $684/week, $35,568/year) and meet the Department of Labor's exempt definition. While this flag is `true`, `Fields.Rate` and `Fields.PaymentUnit` are also `undefined` and the hook forces `rate=0`, `paymentUnit=Year` on the form values (so submits stay valid even with the inputs hidden). +- **`status.showCommissionMinimumWageAlert`** — reactive: `true` when `flsaStatus` is `Commission Only Nonexempt` (Commission Only/Eligible for overtime). Render a warning that commission-only employees must earn at least the local minimum wage, ideally with a link to your local regulations reference. While this flag is `true`, `Fields.Rate` and `Fields.PaymentUnit` are also `undefined` and the hook forces `rate=0`, `paymentUnit=Year` on the form values. +- **`status.showOwnerSalaryAlert`** — reactive: `true` when `flsaStatus` is `Owner` (Owner's draw). Render an informational alert reminding the partner that the IRS requires S-corp owners to pay themselves a reasonable salary for similar work before taking distributions. `Fields.PaymentUnit` stays rendered but is `isDisabled` and locked to `Paycheck` while this flag is `true`. +- **`data.minimumEffectiveDate`** — lower bound for the `effectiveDate` field. Typically the parent job's `hireDate`. Pass this as `min` to the date picker. +- **`data.maximumEffectiveDate`** — upper bound for the `effectiveDate` field, when a future-dated compensation already exists for this job. Pass this as `max` to the date picker so users can't push a new entry past a pending one. +- **`data.hasPendingFutureCompensation`** — `true` when at least one future-dated compensation exists for this job. Use this to render an explanatory note ("A future rate change is already scheduled for …"). + +--- + +## Fields Reference + +### Error Codes + +```typescript +const CompensationErrorCodes = { + REQUIRED: 'REQUIRED', + RATE_MINIMUM: 'RATE_MINIMUM', + RATE_EXEMPT_THRESHOLD: 'RATE_EXEMPT_THRESHOLD', + PAYMENT_UNIT_OWNER: 'PAYMENT_UNIT_OWNER', + PAYMENT_UNIT_COMMISSION: 'PAYMENT_UNIT_COMMISSION', + RATE_COMMISSION_ZERO: 'RATE_COMMISSION_ZERO', + EFFECTIVE_DATE_BEFORE_HIRE: 'EFFECTIVE_DATE_BEFORE_HIRE', +} as const +``` + +--- + +### Fields.Title + +Text input for the title tied to this compensation. Use it when the title change should take effect on this compensation's `effectiveDate` — for example, a future-dated promotion that bundles a new title with a raise. + +Bind title via [`useJobForm.Fields.Title`](./useJobForm.md#fieldstitle) instead when you're creating a job (title is required by the API on job creation) or renaming the active role immediately. Don't render both on the same screen. + +| Prop | Type | Required | +| -------------------- | ------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Optional in both modes** unless `optionalFieldsToRequire` requires it. + +--- + +### Fields.FlsaStatus + +Select dropdown for the employee's FLSA classification (Fair Labor Standards Act status). + +| Prop | Type | Required | +| -------------------- | ------------------------------------ | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `getOptionLabel` | `(status: FlsaStatusType) => string` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** `Exempt`, `Salaried Nonexempt`, `Nonexempt`, `Owner`, `Commission Only Exempt`, `Commission Only Nonexempt`. + +**Conditional availability:** This field is `undefined` when the FLSA status cannot be changed — specifically, when the employee has a non-primary job with a non-`Nonexempt` status that was already set. + +```tsx +{ + Fields.FlsaStatus && ( + + ) +} +``` + +--- + +### Fields.Rate + +Number input for the compensation amount. Formatted as currency. + +| Prop | Type | Required | +| -------------------- | --------------------------------------------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, RATE_MINIMUM: string, RATE_EXEMPT_THRESHOLD: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +| Code | When it triggers | +| ----------------------- | ------------------------------------------------------------------------------------ | +| `REQUIRED` | Rate is empty for non-commission FLSA statuses | +| `RATE_MINIMUM` | Rate is less than $1.00 | +| `RATE_EXEMPT_THRESHOLD` | FLSA Exempt employees must meet the federal salary threshold (annualized rate check) | + +**Conditional availability:** This field is `undefined` when the FLSA status is `Commission Only Exempt` or `Commission Only Nonexempt` — those statuses don't accept a partner-supplied rate, so the hook removes the field and forces `rate=0` on the form values. `fieldsMetadata.rate.isDisabled` is also `true` in that state for partners reading metadata directly. + +```tsx +{ + Fields.Rate && ( + + ) +} +``` + +--- + +### Fields.PaymentUnit + +Select dropdown for the pay period unit. + +| Prop | Type | Required | +| -------------------- | ----------------------------------------------------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, PAYMENT_UNIT_OWNER: string, PAYMENT_UNIT_COMMISSION: string }` | No | +| `getOptionLabel` | `(unit: PaymentUnit) => string` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** `Hour`, `Week`, `Month`, `Year`, `Paycheck`. + +This field is automatically **disabled** when the FLSA status is Owner (forced to `Paycheck`). + +**Conditional availability:** This field is `undefined` when the FLSA status is `Commission Only Exempt` or `Commission Only Nonexempt` — the hook forces `paymentUnit=Year` on the form values and removes the field from `Fields`. `fieldsMetadata.paymentUnit.isDisabled` is also `true` in that state. + +--- + +### Fields.EffectiveDate + +Date picker for when the new compensation row takes effect. + +| Prop | Type | Required | +| -------------------- | ---------------------------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, EFFECTIVE_DATE_BEFORE_HIRE: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Required on create.** Optional on update (the API keeps the existing effective date when omitted) unless `optionalFieldsToRequire.update` includes `'effectiveDate'`. + +Use `data.minimumEffectiveDate` and `data.maximumEffectiveDate` to constrain the picker. + +This field is automatically **disabled** (and the form value forced to today) while `status.willDeleteSecondaryJobs` is `true` — see [Derived helpers](#derived-helpers). You can render the disabled field as-is, or hide it altogether and key off the flag for a separate inline message. + +**Conditional availability:** This field is `undefined` when `withEffectiveDateField: false`. In this mode the hook is strictly options-only — `effective_date` is omitted from the request body unless you supply `CompensationSubmitOptions.effectiveDate` at submit time. The `willDeleteSecondaryJobs` carve-out's UI side effects (force form value to today, disable the field) are inert here because there is no field to render; pass the date through submit options if you need to pin one during the carve-out. + +```tsx +{ + Fields.EffectiveDate && ( + + ) +} +``` + +--- + +### Fields.AdjustForMinimumWage + +Checkbox to enable minimum wage adjustment. + +| Prop | Type | Required | +| ---------------- | ------------------------------ | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `FieldComponent` | `ComponentType` | No | + +**Conditional availability:** This field is `undefined` when: + +- FLSA status is not `Nonexempt` +- No minimum wages are available for the employee's work location +- The employee's work state does not support tip credits + +--- + +### Fields.MinimumWageId + +Select dropdown to choose which minimum wage to adjust to. Only appears when `AdjustForMinimumWage` is checked. + +| Prop | Type | Required | +| -------------------- | ---------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** Dynamically populated from minimum wages available at the employee's work location. + +--- + +## Usage example (single hook, steady-state edit) + +```tsx +import { + useCompensationForm, + SDKFormProvider, + type UseCompensationFormReady, +} from '@gusto/embedded-react-sdk' + +function CompensationEditPage({ + employeeId, + jobId, + compensationId, +}: { + employeeId: string + jobId: string + compensationId: string +}) { + const compensation = useCompensationForm({ employeeId, jobId, compensationId }) + + if (compensation.isLoading) return
Loading...
+ + return +} + +function CompensationFormReady({ compensation }: { compensation: UseCompensationFormReady }) { + const { Fields } = compensation.form + const { hasPendingFutureCompensation, maximumEffectiveDate } = compensation.data + const { + willDeleteSecondaryJobs, + showCommissionFederalMinimumPayAlert, + showCommissionMinimumWageAlert, + showOwnerSalaryAlert, + } = compensation.status + + return ( + +
{ + e.preventDefault() + await compensation.actions.onSubmit() + }} + > + {willDeleteSecondaryJobs && ( +

Saving will delete this employee's secondary jobs.

+ )} + + {hasPendingFutureCompensation && ( +

A future rate change is already scheduled for {maximumEffectiveDate}.

+ )} + + {Fields.FlsaStatus && ( + + )} + + {showCommissionFederalMinimumPayAlert && ( +

+ Commission-only exempt employees must still earn at least the federal minimum pay. +

+ )} + + {showCommissionMinimumWageAlert && ( +

Commission-only employees must earn at least the local minimum wage.

+ )} + + {showOwnerSalaryAlert && ( +

+ The IRS requires S-corp owners to pay themselves a reasonable salary before taking + distributions. +

+ )} + + {Fields.Rate && ( + + )} + + {Fields.PaymentUnit && ( + + )} + + {Fields.EffectiveDate && ( + + )} + + {Fields.AdjustForMinimumWage && ( + + )} + + {Fields.MinimumWageId && ( + + )} + + + +
+ ) +} +``` + +For the onboarding stub-fill chain (POST job → PUT auto-created stub) and other multi-form flows, see [Working with Jobs and Compensations](./jobs-and-compensations.md). + +--- + +## Related + +- [useJobForm](./useJobForm.md) — pair this with `useCompensationForm` for full job + compensation editing. +- [Working with Jobs and Compensations](./jobs-and-compensations.md) — onboarding stub-fill and steady-state edit recipes. +- [Composing Multiple Hooks](./hooks.md#composing-multiple-hooks) — coordinate `useJobForm` + `useCompensationForm` (and others) on a single screen. diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useDeductionForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useDeductionForm.md new file mode 100644 index 000000000..73ec7447a --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useDeductionForm.md @@ -0,0 +1,186 @@ +--- +title: useDeductionForm +order: 7 +--- + +# useDeductionForm + +Creates or updates a deduction (post-tax custom deduction or court-ordered garnishment) for an employee. Both variants share the same field set — description, frequency, deduct-as-percentage toggle, amount, and optional caps — and differ only in whether the deduction is court-ordered and carries a `garnishmentType`. For child-support garnishments use [`useChildSupportGarnishmentForm`](./useChildSupportGarnishmentForm.md), which handles agency-keyed required attributes. + +```tsx +import { useDeductionForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +--- + +## Props + +`useDeductionForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------- | +| `employeeId` | `string` | Yes | — | The UUID of the employee. | +| `garnishmentId` | `string` | No | — | When present → **update** mode (PUT /v1/garnishments/:id with `version`). When absent → **create** mode (POST). | +| `courtOrdered` | `boolean` | Yes | — | When `true`, the schema and Fields include `garnishmentType` (Federal Tax Lien, Student Loan, etc.). When `false`, omitted. | +| `optionalFieldsToRequire` | `DeductionFormOptionalFieldsToRequire` | No | — | Override caps to be required. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data takes precedence on update. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### DeductionFormData + +```typescript +interface DeductionFormData { + description: string + recurring: boolean + deductAsPercentage: boolean + amount: number + totalAmount: number // 0 means "no cap" (the hook drops it to null on the wire) + annualMaximum: number // 0 means "no cap" + garnishmentType: GarnishmentType // only meaningful when courtOrdered: true +} +``` + +--- + +## Return Type + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +interface UseDeductionFormReady { + isLoading: false + data: { deduction: Garnishment | null } + status: { + isPending: boolean + mode: 'create' | 'update' + /** + * Mirrors the watched `recurring` value. `Fields.TotalAmount` and + * `Fields.AnnualMaximum` are only exposed when this is true. + */ + isRecurring: boolean + } + actions: { + onSubmit: () => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: DeductionFormFields + fieldsMetadata: DeductionFormFieldsMetadata + hookFormInternals: HookFormInternals + getFormSubmissionValues: () => DeductionFormData | undefined + } +} +``` + +--- + +## Fields Reference + +| Field | Input type | Required by default | Error codes | Conditional availability | +| -------------------- | ----------- | ------------------- | ----------------------------- | --------------------------------------- | +| `Description` | TextInput | Yes | `REQUIRED` | Always | +| `Recurring` | RadioGroup | Yes | `REQUIRED` | Always | +| `DeductAsPercentage` | RadioGroup | Yes | `REQUIRED` | Always | +| `Amount` | NumberInput | Yes | `REQUIRED`, `NEGATIVE_AMOUNT` | Always | +| `TotalAmount` | NumberInput | No | `NEGATIVE_AMOUNT` | Only when `status.isRecurring === true` | +| `AnnualMaximum` | NumberInput | No | `NEGATIVE_AMOUNT` | Only when `status.isRecurring === true` | +| `GarnishmentType` | Select | Yes | `REQUIRED` | Only when prop `courtOrdered === true` | + +--- + +## Usage Examples + +### SDKFormProvider pattern + +```tsx +import { useDeductionForm, SDKFormProvider } from '@gusto/embedded-react-sdk' + +function CustomDeductionPage({ + employeeId, + garnishmentId, +}: { + employeeId: string + garnishmentId?: string +}) { + const form = useDeductionForm({ employeeId, garnishmentId, courtOrdered: false }) + + if (form.isLoading) return

Loading…

+ + const { Fields } = form.form + + const handleSubmit = async () => { + const result = await form.actions.onSubmit() + if (result) { + // result.mode is 'create' or 'update'; result.data is the saved Garnishment + } + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > + + (v ? 'Recurring' : 'One-time')} + validationMessages={{ REQUIRED: 'Required' }} + /> + (v ? 'Percentage' : 'Fixed amount')} + validationMessages={{ REQUIRED: 'Required' }} + /> + + {Fields.TotalAmount && ( + + )} + {Fields.AnnualMaximum && ( + + )} + + +
+ ) +} +``` + +### `formHookResult` prop pattern + +When mixing this hook's fields with another hook's on the same page: + +```tsx + +``` + +--- + +## Related + +- [`useChildSupportGarnishmentForm`](./useChildSupportGarnishmentForm.md) — child-support variant with agency-keyed conditional requiredness diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeDetailsForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeDetailsForm.md new file mode 100644 index 000000000..62cb2fce7 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeDetailsForm.md @@ -0,0 +1,609 @@ +--- +title: useEmployeeDetailsForm +order: 2 +--- + +# useEmployeeDetailsForm + +Creates or updates an employee's profile information — name, email, SSN, date of birth, and self-onboarding preference. + +```tsx +import { useEmployeeDetailsForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +--- + +## Props + +`useEmployeeDetailsForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | +| `companyId` | `string` | Yes | — | The UUID of the company the employee belongs to. | +| `employeeId` | `string` | No | — | The UUID of an existing employee. Omit to create a new employee. | +| `withSelfOnboardingField` | `boolean` | No | `true` | Whether to include the self-onboarding toggle field. | +| `requiredFields` | `EmployeeDetailsField[] \| { create?: EmployeeDetailsField[], update?: EmployeeDetailsField[] }` | No | — | Additional fields to make required beyond API defaults. A flat array applies to both modes; an object targets specific modes. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data takes precedence when editing an existing employee. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | When validation runs. Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### EmployeeDetailsField + +The `requiredFields` arrays accept these field names: + +```typescript +type EmployeeDetailsField = + | 'firstName' + | 'middleInitial' + | 'lastName' + | 'email' + | 'dateOfBirth' + | 'ssn' +``` + +### Required Fields + +**Required by default on create:** `firstName`, `lastName` +**Required by default on update:** (none) + +All `EmployeeDetailsField` values are available to require in either mode. Note that the `ssn` requirement is automatically waived when the employee already has an SSN on file. + +```tsx +// Flat array: require email in both modes +useEmployeeDetailsForm({ + companyId, + requiredFields: ['email'], +}) + +// Per-mode: different requirements per mode +useEmployeeDetailsForm({ + companyId, + requiredFields: { + create: ['email'], + update: ['ssn', 'dateOfBirth'], + }, +}) +``` + +### EmployeeDetailsFormData + +The shape of `defaultValues`: + +```typescript +interface EmployeeDetailsFormData { + firstName: string + middleInitial: string + lastName: string + email: string + dateOfBirth: string // ISO date string (YYYY-MM-DD) + ssn: string + selfOnboarding: boolean +} +``` + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +{ + isLoading: false + data: { + employee: Employee | null + } + status: { + isPending: boolean + mode: 'create' | 'update' + } + actions: { + onSubmit: (callbacks?: EmployeeDetailsSubmitCallbacks) => + Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: EmployeeDetailsFormFields + fieldsMetadata: EmployeeDetailsFieldsMetadata + hookFormInternals: { + formMethods: UseFormReturn + } + getFormSubmissionValues: () => EmployeeDetailsFormOutputs | undefined + } +} +``` + +### Submit callbacks + +`onSubmit` accepts an optional callbacks object: + +```typescript +interface EmployeeDetailsSubmitCallbacks { + onEmployeeCreated?: (employee: Employee) => void + onEmployeeUpdated?: (employee: Employee) => void + onOnboardingStatusUpdated?: (status: unknown) => void +} +``` + +--- + +## Fields Reference + +All fields accept `label` (required) and `description` (optional). Fields with validation accept `validationMessages` mapping error codes to display strings. All fields accept an optional `FieldComponent` prop to override the rendered UI component. + +### Error Codes + +```typescript +const EmployeeDetailsErrorCodes = { + REQUIRED: 'REQUIRED', + INVALID_NAME: 'INVALID_NAME', + INVALID_EMAIL: 'INVALID_EMAIL', + INVALID_SSN: 'INVALID_SSN', + EMAIL_REQUIRED_FOR_SELF_ONBOARDING: 'EMAIL_REQUIRED_FOR_SELF_ONBOARDING', +} as const +``` + +--- + +### Fields.FirstName + +Text input for the employee's first name. Validates that the value contains only allowed name characters (letters, spaces, hyphens, and apostrophes). + +| Prop | Type | Required | +| -------------------- | -------------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, INVALID_NAME: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Validation codes:** + +| Code | When it triggers | +| -------------- | -------------------------------------------------------------------- | +| `REQUIRED` | Field is empty and required (always required on create) | +| `INVALID_NAME` | Value contains characters not allowed by the name validation pattern | + +**Required by default on create.** + +```tsx + +``` + +--- + +### Fields.MiddleInitial + +Text input for the employee's middle initial. + +| Prop | Type | Required | +| ---------------- | ------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `FieldComponent` | `ComponentType` | No | + +No validation codes — this field is always optional. + +```tsx + +``` + +--- + +### Fields.LastName + +Text input for the employee's last name. Validates that the value contains only allowed name characters (letters, spaces, hyphens, and apostrophes). + +| Prop | Type | Required | +| -------------------- | -------------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, INVALID_NAME: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Validation codes:** + +| Code | When it triggers | +| -------------- | -------------------------------------------------------------------- | +| `REQUIRED` | Field is empty and required (always required on create) | +| `INVALID_NAME` | Value contains characters not allowed by the name validation pattern | + +**Required by default on create.** + +```tsx + +``` + +--- + +### Fields.Email + +Text input for the employee's personal email address. + +| Prop | Type | Required | +| -------------------- | ----------------------------------------------------------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, INVALID_EMAIL: string, EMAIL_REQUIRED_FOR_SELF_ONBOARDING: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Validation codes:** + +| Code | When it triggers | +| ------------------------------------ | ---------------------------------------------------------------- | +| `REQUIRED` | Field is empty and marked required via `requiredFields` | +| `INVALID_EMAIL` | Non-empty value that is not a valid email format | +| `EMAIL_REQUIRED_FOR_SELF_ONBOARDING` | Self-onboarding is enabled but email is empty (create mode only) | + +```tsx + +``` + +--- + +### Fields.DateOfBirth + +Date picker for the employee's date of birth. + +| Prop | Type | Required | +| -------------------- | -------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +```tsx + +``` + +--- + +### Fields.Ssn + +Text input for the employee's Social Security number. Automatically formats input with dashes (XXX-XX-XXXX). When the employee already has an SSN on file, the field shows a masked placeholder. + +| Prop | Type | Required | +| -------------------- | ------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ INVALID_SSN: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Validation codes:** + +| Code | When it triggers | +| ------------- | -------------------------------------------- | +| `INVALID_SSN` | Value does not match the expected SSN format | + +The masked placeholder appears as `•••-••-1234`. When the employee already has an SSN on record, the `REQUIRED` rule is automatically waived even if `ssn` is listed in `requiredFields`. + +```tsx + +``` + +--- + +### Fields.SelfOnboarding + +Switch toggle for inviting the employee to self-onboard. When enabled, the employee receives an email invitation to enter their own personal, tax, and banking details. + +| Prop | Type | Required | +| ---------------- | ---------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `FieldComponent` | `ComponentType` | No | + +No validation codes. + +**Conditional availability:** This field is `undefined` when: + +- `withSelfOnboardingField` is `false` +- The employee's onboarding status does not allow toggling (e.g., self-onboarding is already in progress or has already been completed) + +Always check for existence before rendering: + +```tsx +{ + Fields.SelfOnboarding && ( + + ) +} +``` + +--- + +## Usage Examples + +### With `SDKFormProvider` (context) + +A complete example showing all fields, validation messages, and submit handling using the context-based approach: + +```tsx +import { + useEmployeeDetailsForm, + SDKFormProvider, + type UseEmployeeDetailsFormReady, +} from '@gusto/embedded-react-sdk' + +function EmployeeDetailsPage({ + companyId, + employeeId, +}: { + companyId: string + employeeId?: string +}) { + const employeeDetails = useEmployeeDetailsForm({ + companyId, + employeeId, + requiredFields: { + update: ['ssn'], + }, + }) + + if (employeeDetails.isLoading) { + const { errors, retryQueries } = employeeDetails.errorHandling + + if (errors.length > 0) { + return ( +
+

Failed to load employee data.

+
    + {errors.map((error, i) => ( +
  • {error.message}
  • + ))} +
+ +
+ ) + } + + return
Loading...
+ } + + return +} + +function EmployeeDetailsFormReady({ + employeeDetails, +}: { + employeeDetails: UseEmployeeDetailsFormReady +}) { + const { Fields } = employeeDetails.form + + const handleSubmit = async () => { + const result = await employeeDetails.actions.onSubmit({ + onEmployeeCreated: employee => { + console.log('Employee created:', employee.uuid) + }, + onEmployeeUpdated: employee => { + console.log('Employee updated:', employee.uuid) + }, + }) + + if (result) { + console.log(`${result.mode}d employee:`, result.data.uuid) + } + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > +

{employeeDetails.status.mode === 'create' ? 'Add Employee' : 'Edit Employee'}

+ + {employeeDetails.errorHandling.errors.length > 0 && ( +
+ {employeeDetails.errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + + + + + + + + + {Fields.SelfOnboarding && ( + + )} + + + + + + + +
+ ) +} +``` + +### With `formHookResult` prop + +The same form using prop-based field connection. No `SDKFormProvider` wrapper needed — each field receives the hook result directly: + +```tsx +import { useEmployeeDetailsForm, type UseEmployeeDetailsFormReady } from '@gusto/embedded-react-sdk' + +function EmployeeDetailsPage({ + companyId, + employeeId, +}: { + companyId: string + employeeId?: string +}) { + const employeeDetails = useEmployeeDetailsForm({ companyId, employeeId }) + + if (employeeDetails.isLoading) { + return
Loading...
+ } + + return +} + +function EmployeeDetailsFormReady({ + employeeDetails, +}: { + employeeDetails: UseEmployeeDetailsFormReady +}) { + const { Fields } = employeeDetails.form + + return ( +
{ + e.preventDefault() + void employeeDetails.actions.onSubmit() + }} + > +

{employeeDetails.status.mode === 'create' ? 'Add Employee' : 'Edit Employee'}

+ + {employeeDetails.errorHandling.errors.length > 0 && ( +
+ {employeeDetails.errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + + + + + + + + + {Fields.SelfOnboarding && ( + + )} + + + + + + + + ) +} +``` + +Both examples produce identical validation, error handling, and API behavior. The prop-based approach is particularly useful when embedding employee detail fields within a larger composed form — see [Composing Multiple Hooks](./hooks.md#composing-multiple-hooks). diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeStateTaxesForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeStateTaxesForm.md new file mode 100644 index 000000000..7cb64bb2e --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useEmployeeStateTaxesForm.md @@ -0,0 +1,420 @@ +# useEmployeeStateTaxesForm + +Updates an employee's state tax withholding answers. The state-tax record(s) are created automatically with the employee, so this hook is always in update mode. + +```tsx +import { useEmployeeStateTaxesForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +[**Jump to Usage Examples →**](#usage-examples) + +> **Note.** Unlike most SDK form hooks, the field set here is **dynamic** — driven by the API response. `form.Fields` is an array of state groups (one per state, each with its own list of questions) rather than a named object, and a few static-shape options (`defaultValues`, `optionalFieldsToRequire`, `getOptionLabel`) don't apply. The shapes, render pattern, and per-question overrides are all demonstrated below. + +--- + +## Props + +`useEmployeeStateTaxesForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------ | -------------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------- | +| `employeeId` | `string` | Yes | — | The UUID of the employee whose state taxes are being updated. | +| `isAdmin` | `boolean` | No | `false` | Render and submit admin-only questions (e.g. `file_new_hire_report`). When `false`, those questions are filtered out. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | When validation runs. Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +`defaultValues` and `optionalFieldsToRequire` are intentionally not accepted: the form is always pre-populated from the server response, and the required-question set is driven entirely by the API. + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +{ + isLoading: false + data: { + employeeStateTaxes: EmployeeStateTaxesList[] + } + status: { isPending: boolean; mode: 'update' } + actions: { + onSubmit: () => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: StateTaxFieldsGroup[] + fieldsMetadata: EmployeeStateTaxesFieldsMetadata + hookFormInternals: { formMethods: UseFormReturn } + getFormSubmissionValues: () => EmployeeStateTaxesFormOutputs | undefined + } +} +``` + +The non-primitive types in the Ready state are all re-exported from `@gusto/embedded-react-sdk`: + +| Type | What it is | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `UseEmployeeStateTaxesFormReady` | The full Ready-state object (the discriminated `isLoading: false` branch). Use this as the prop type for components that receive a ready form, so you don't have to repeat the `Extract<...>` narrowing. | +| `EmployeeStateTaxesList` | API record for one state's tax answers, re-exported from `@gusto/embedded-api-v-2025-11-15`. Each entry in `data.employeeStateTaxes` is one of these. | +| `StateTaxFieldsGroup` | One state's render-ready bundle: `{ state, questions: StateTaxQuestionFieldEntry[] }`. The full shape is documented under [Fields shape](#fields-shape). | +| `StateTaxQuestionFieldEntry` | The discriminated entry for a single question — `type` + metadata + bound `Field` component. See [Fields shape](#fields-shape). | +| `EmployeeStateTaxesFieldsMetadata` | Static field metadata keyed by full form path (`states..`), with `isRequired` / `isDisabled` / option lists. Same shape as other SDK form hooks' `fieldsMetadata`. | +| `EmployeeStateTaxesFormOutputs` | The submit-time form data shape: `{ states: Record> }`. Returned by `getFormSubmissionValues()` and consumed by the internal serializer. | +| `HookSubmitResult` | Standard SDK submit-result envelope: `{ mode: 'update', data: T }`. Same shape as other form hooks. | +| `HookErrorHandling` | Standard SDK error-handling object exposed by `composeErrorHandler`. | + +```typescript +import type { + UseEmployeeStateTaxesFormReady, + StateTaxFieldsGroup, + StateTaxQuestionFieldEntry, + EmployeeStateTaxesFieldsMetadata, + EmployeeStateTaxesFormOutputs, +} from '@gusto/embedded-react-sdk' +``` + +### Submit result + +`onSubmit` resolves to `undefined` when validation blocks the submit, or `{ mode: 'update', data: EmployeeStateTaxesList[] }` carrying the updated record list returned by the server. When the form has no states with submittable answers (e.g. an employee in a no-income-tax state like TX), the hook resolves with `data: ` without making a network request. + +--- + +## Fields shape + +`form.Fields` is an array of state groups in API response order. Each group exposes its visible questions as discriminated entries with a bound `Field` component. All of the types below are exported from `@gusto/embedded-react-sdk`: + +```typescript +import type { + StateTaxFieldsGroup, + StateTaxQuestionFieldEntry, + SelectStateTaxFieldProps, + RadioStateTaxFieldProps, + TextStateTaxFieldProps, + NumberStateTaxFieldProps, + CurrencyStateTaxFieldProps, + DateStateTaxFieldProps, +} from '@gusto/embedded-react-sdk' +``` + +```typescript +interface StateTaxFieldsGroup { + /** Two-letter state code, e.g. `'CA'`, `'NY'`. */ + state: string + questions: StateTaxQuestionFieldEntry[] +} + +interface BaseStateTaxQuestionMetadata { + /** camelCase form of the API question key, e.g. `'fileNewHireReport'`. + * Stable across re-fetches; safe as a React `key` and for branching. */ + questionId: string + /** API-supplied label; shown by default unless overridden via ``. */ + label: string + /** API-supplied description (HTML, sanitized via DOMPurify before render). + * May be `null` for questions without a description. */ + description: string | null +} + +type StateTaxQuestionFieldEntry = + | (BaseStateTaxQuestionMetadata & { + type: 'select' + Field: ComponentType + }) + | (BaseStateTaxQuestionMetadata & { + type: 'radio' + Field: ComponentType + }) + | (BaseStateTaxQuestionMetadata & { type: 'text'; Field: ComponentType }) + | (BaseStateTaxQuestionMetadata & { + type: 'number' + Field: ComponentType + }) + | (BaseStateTaxQuestionMetadata & { + type: 'currency' + Field: ComponentType + }) + | (BaseStateTaxQuestionMetadata & { type: 'date'; Field: ComponentType }) +``` + +Discriminate on `type` to access variant-specific props (each variant's `Field` accepts a different `FieldComponent` shape — see [Field component props](#field-component-props)). + +### Variant mapping + +The hook resolves a question's UI variant from the API's `inputQuestionFormat.type`: + +| API type | Variant | Notes | +| ----------- | ---------- | --------------------------------------------------- | +| `Select` | `select` | Renders as a dropdown with API-supplied options. | +| `Number` | `number` | Decimal number input. | +| `Currency` | `currency` | Currency-formatted number input. | +| `Text` | `text` | Single-line text input. | +| `Date` | `date` | Date picker. | +| _(unknown)_ | `text` | Defensive fall-through for unrecognized wire types. | + +**Per-key promotion rules:** + +- `file_new_hire_report` is server-converted from internal `Radio` to wire `Select`. The hook re-promotes it to `radio` so it renders as a radio group, matching the canonical Ruby helper's intent. (The previous `Employee.StateTaxes` component had a snake_case-vs-camelCase comparison bug that prevented this promotion from firing.) + +The hook also marks `file_new_hire_report` as `isDisabled: true` in metadata once an answer has been recorded server-side — once filed, the choice is locked. + +--- + +## Field component props + +All Field components share these base props. Every field accepts an optional `label` and `description`; when omitted, the API-supplied values are used (and the description is sanitized through DOMPurify before being rendered). + +```typescript +interface BaseStateTaxFieldProps { + label?: string + description?: ReactNode + formHookResult?: FormHookResult + validationMessages?: ValidationMessages + FieldComponent?: ComponentType<...> +} +``` + +### Variant-specific props + +Two variants extend `BaseStateTaxFieldProps` with an additional optional prop: + +| Variant | Adds | +| -------- | ---------------------- | +| `select` | `placeholder?: string` | +| `text` | `placeholder?: string` | + +The other four variants (`radio`, `number`, `currency`, `date`) accept only the base props. + +### Choosing a `FieldComponent` + +The `<...>` in `ComponentType<...>` above is **variant-specific**: each variant's `FieldComponent` must match the prop contract of the underlying SDK UI primitive that variant renders. To override, discriminate on `question.type` first, then plug in a component whose props satisfy the matching SDK prop type: + +| Variant | Required `FieldComponent` shape | SDK primitive it replaces | +| ---------- | --------------------------------- | ------------------------- | +| `select` | `ComponentType` | `Components.Select` | +| `radio` | `ComponentType` | `Components.RadioGroup` | +| `text` | `ComponentType` | `Components.TextInput` | +| `number` | `ComponentType` | `Components.NumberInput` | +| `currency` | `ComponentType` | `Components.NumberInput` | +| `date` | `ComponentType` | `Components.DatePicker` | + +All six prop types (`SelectProps`, `RadioGroupProps`, `TextInputProps`, `NumberInputProps`, `DatePickerProps`) are re-exported from `@gusto/embedded-react-sdk`, alongside the variant-specific `*StateTaxFieldProps` types whose `FieldComponent` field encodes the same constraint (e.g. `SelectStateTaxFieldProps['FieldComponent']` is `ComponentType`). + +A minimal type-safe override: + +```tsx +import { MyDesignSystemSelect } from '@/components/forms/MyDesignSystemSelect' + +if (question.type === 'select') { + return +} +``` + +For a more comprehensive example that combines `type`, `state`, and `questionId` overrides, see [Per-question overrides](#per-question-overrides). + +### Error codes + +Every variant surfaces a single error code, `REQUIRED`: + +```typescript +const EmployeeStateTaxesErrorCodes = { + REQUIRED: 'REQUIRED', +} as const +``` + +Each Field renders a **localized default validation message** out of the box (`Employee.StateTaxes.validations.required`). Pass `validationMessages={{ REQUIRED: '...' }}` to override per field. + +The hook intentionally surfaces only this one code: `number` and `date` inputs come from type-safe UI primitives (react-aria `NumberField` and `DatePicker`), so any "invalid" entry is normalized to empty in the schema preprocessor and lands on `REQUIRED` rather than producing a separate "invalid number"/"invalid date" path. + +--- + +## Usage Examples + +### With `SDKFormProvider` (context) + +```tsx +import { + useEmployeeStateTaxesForm, + SDKFormProvider, + type UseEmployeeStateTaxesFormReady, +} from '@gusto/embedded-react-sdk' + +function StateTaxesPage({ employeeId, isAdmin }: { employeeId: string; isAdmin: boolean }) { + const stateTaxes = useEmployeeStateTaxesForm({ employeeId, isAdmin }) + + if (stateTaxes.isLoading) return
Loading...
+ + return +} + +function StateTaxesFormReady({ stateTaxes }: { stateTaxes: UseEmployeeStateTaxesFormReady }) { + const groups = stateTaxes.form.Fields + + const handleSubmit = async () => { + const result = await stateTaxes.actions.onSubmit() + if (result) console.log('Updated state tax records:', result.data) + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > + {groups.map(group => ( +
+

{group.state} Tax Requirements

+ {group.questions.map(question => ( + + ))} +
+ ))} + + +
+
+ ) +} +``` + +Default validation messages come from the SDK's translation files. To override per question, pass `validationMessages` directly: + +```tsx + +``` + +### Per-question overrides + +Each Field accepts: + +- `label` and `description` for overriding the API-supplied defaults (the API description is otherwise rendered verbatim, sanitized via DOMPurify). +- `FieldComponent` for swapping the underlying control with one of your own (e.g. your design-system Select). The prop type is **variant-specific** — `SelectStateTaxFieldProps['FieldComponent']` is `ComponentType`, `DateStateTaxFieldProps['FieldComponent']` is `ComponentType`, etc. — so you must discriminate on `question.type` first. All of those prop types (`SelectProps`, `DatePickerProps`, `NumberInputProps`, `RadioGroupProps`, `TextInputProps`) are exported from `@gusto/embedded-react-sdk`. + +The three things you can branch on are `question.type`, `group.state`, and `question.questionId`. They sit on a sliding scale of "safe" (compile-time exhaustive) to "fragile" (string-matching against API output): + +| Branch on | Stability | Use it for | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `question.type` | Closed union of 6 strings owned by this hook. Adding a new variant is a SDK breaking change. | Design-system primitive swaps (e.g. "use my Select for every Select"). Safe and exhaustively typed. | +| `group.state` | API-driven 2-letter state code. Stable for known states, but a state's question set / variants can change as tax law changes. | Geographic copy or branding. Re-test when a new state opens up. | +| `question.questionId` | Camel-cased API key. The set of keys for a given state is API-driven and **does evolve** (Gusto adds/renames questions as state forms change). | Truly one-off overrides for a specific question. Treat as a soft coupling; revisit when API moves. | + +> **Caution.** Branching on `questionId` or `state` is a soft coupling to the API contract. When Gusto introduces a new state question, renames an existing one, or splits one question into two, hardcoded `if (question.questionId === '…')` branches silently fall through to the default render. Audit these branches as part of any state-tax-related upgrade, and prefer `type`-level overrides whenever the same change applies to a whole class of fields. + +#### Example — combining `type`, `state`, and `questionId` + +```tsx +import type { StateTaxFieldsGroup, StateTaxQuestionFieldEntry } from '@gusto/embedded-react-sdk' + +import { MyDesignSystemSelect } from '@/components/forms/MyDesignSystemSelect' +import { MyDesignSystemDatePicker } from '@/components/forms/MyDesignSystemDatePicker' +import { CountyAutocomplete } from '@/components/forms/CountyAutocomplete' + +// Explicit allow-list of Indiana questionIds we treat as county selects. +// Audit this set whenever the IN state-tax form changes upstream. +const IN_COUNTY_QUESTION_IDS = new Set([ + 'currentEmploymentCounty', + 'currentResidenceCounty', + 'previousEmploymentCounty', + 'previousResidenceCounty', +]) + +function RenderGroupQuestions({ group }: { group: StateTaxFieldsGroup }) { + return ( + <> + {group.questions.map(question => ( + + ))} + + ) +} + +function RenderQuestion({ + group, + question, +}: { + group: StateTaxFieldsGroup + question: StateTaxQuestionFieldEntry +}) { + // 1. questionId-level one-off — relabel a single question + if (question.questionId === 'fileNewHireReport') { + return ( + + ) + } + + // 2. State + questionId one-off — Indiana ships four county Selects that + // we want to render with a tailored autocomplete. We match against an + // explicit allow-list of known questionIds rather than substring matching + // so that newly-added IN questions don't silently get the autocomplete. + if ( + group.state === 'IN' && + question.type === 'select' && + IN_COUNTY_QUESTION_IDS.has(question.questionId) + ) { + return ( + + ) + } + + // 3. State-level description override — soften NJ's verbose API copy. + if (group.state === 'NJ' && question.questionId === 'filingStatus') { + return ( + + ) + } + + // 4. Type-level swap — replace every Select / Date with a design-system + // primitive. This branch is safe to add and forget; new questions of + // these variants automatically pick it up. + switch (question.type) { + case 'select': + return + case 'date': + return + case 'radio': + case 'text': + case 'number': + case 'currency': + return + } +} +``` + +A few things worth noting in the example: + +- The `questionId`/`state` branches sit **above** the `type` switch so a one-off override wins over the broad design-system swap. +- We never strip the `key={question.questionId}` from the JSX; React still relies on it to maintain field identity across re-renders. +- The `'currency'` and `'number'` branches share the same `NumberInputProps` shape, so if you wanted a single design-system Number override you can collapse them: `case 'number': case 'currency': return `. +- Since `question.questionId` is the **camelCase** form of the API key (e.g. `filingStatus`, not `filing_status`), keep your string comparisons in camelCase to stay aligned with the hook's contract. + +### `formHookResult` prop (no provider) + +```tsx +{ + groups.map(group => + group.questions.map(question => ( + + )), + ) +} +``` diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useFederalTaxesForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useFederalTaxesForm.md new file mode 100644 index 000000000..65def04bd --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useFederalTaxesForm.md @@ -0,0 +1,462 @@ +--- +title: useFederalTaxesForm +order: 6 +--- + +# useFederalTaxesForm + +Updates an employee's federal tax (W-4) withholding information — filing status, multiple-jobs flag, dependents, other income, deductions, and extra withholding. + +```tsx +import { useFederalTaxesForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +The federal tax record is created automatically with the employee, so this hook is always in update mode. Only the revised 2020 W-4 format is supported for updates. + +--- + +## Props + +`useFederalTaxesForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `employeeId` | `string` | Yes | — | The UUID of the employee whose federal tax record is being updated. | +| `optionalFieldsToRequire` | `FederalTaxesOptionalFieldsToRequire` | No | — | Promotes API-optional fields to required. By default the hook only requires `filingStatus` (the only field the API requires); pass `{ update: ['twoJobs', 'dependentsAmount', 'otherIncome', 'deductions', 'extraWithholding'] }` to require any subset. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data takes precedence when the employee already has values on file. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | When validation runs. Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### FederalTaxesField + +The `optionalFieldsToRequire` arrays accept these field names: + +```typescript +type FederalTaxesField = + | 'filingStatus' + | 'twoJobs' + | 'dependentsAmount' + | 'otherIncome' + | 'deductions' + | 'extraWithholding' +``` + +### Required Fields + +**Required by default on update:** `filingStatus` only — the API treats every other W-4 field as optional. Promote any of `twoJobs`, `dependentsAmount`, `otherIncome`, `deductions`, or `extraWithholding` to required by adding them to `optionalFieldsToRequire.update`. The shipped `` component promotes all of them, mirroring the original (all-required) UX. + +### FederalTaxesFormData + +The shape of `defaultValues`: + +```typescript +interface FederalTaxesFormData { + filingStatus: string // 'Single' | 'Married' | 'Head of Household' | 'Exempt from withholding' | '' + twoJobs: boolean + dependentsAmount: number + otherIncome: number + deductions: number + extraWithholding: number +} +``` + +The four currency fields (`dependentsAmount`, `otherIncome`, `deductions`, `extraWithholding`) default to `0`. `filingStatus` defaults to an empty string when neither the server nor `defaultValues` provides a value, forcing the user to make an explicit selection. + +The constant `FILING_STATUS_VALUES` is exported from the SDK for typing and rendering custom selects: + +```typescript +import { FILING_STATUS_VALUES, type FilingStatusValue } from '@gusto/embedded-react-sdk' +// FILING_STATUS_VALUES === ['Single', 'Married', 'Head of Household', 'Exempt from withholding'] +``` + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +{ + isLoading: false + data: { + employeeFederalTax: EmployeeFederalTax + } + status: { + isPending: boolean + mode: 'update' + } + actions: { + onSubmit: () => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: FederalTaxesFormFields + fieldsMetadata: FederalTaxesFieldsMetadata + hookFormInternals: { + formMethods: UseFormReturn + } + getFormSubmissionValues: () => FederalTaxesFormOutputs | undefined + } +} +``` + +### Mode + +`status.mode` is always `'update'`. The federal tax record is created when the employee is created, so the hook does not have a create mode. + +### Submit result + +`onSubmit` resolves to either `undefined` (validation blocked the submit, or a request error was already surfaced through `errorHandling`) or an object of the form `{ mode: 'update', data: EmployeeFederalTax }` carrying the updated record. + +--- + +## Fields Reference + +All fields accept `label` (required) and `description` (optional). Fields with validation accept `validationMessages` mapping error codes to display strings. All fields accept an optional `FieldComponent` prop to override the rendered UI component. + +### Error Codes + +```typescript +const FederalTaxesErrorCodes = { + REQUIRED: 'REQUIRED', +} as const +``` + +| Field | Input type | Required by default | Error codes | Conditional availability | +| ------------------ | -------------- | ------------------- | ----------- | ------------------------ | +| `FilingStatus` | Select | Yes | `REQUIRED` | Always available | +| `TwoJobs` | Radio group | No | `REQUIRED` | Always available | +| `DependentsAmount` | Currency input | No | `REQUIRED` | Always available | +| `OtherIncome` | Currency input | No | `REQUIRED` | Always available | +| `Deductions` | Currency input | No | `REQUIRED` | Always available | +| `ExtraWithholding` | Currency input | No | `REQUIRED` | Always available | + +--- + +### Fields.FilingStatus + +Select dropdown for choosing the IRS filing status used for federal withholding. + +| Prop | Type | Required | +| -------------------- | -------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `placeholder` | `string` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `getOptionLabel` | `(value: FilingStatusValue) => string` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** Populated from `FILING_STATUS_VALUES` (`'Single'`, `'Married'`, `'Head of Household'`, `'Exempt from withholding'`). The default option label is the raw filing status value. Pass `getOptionLabel` to localize: + +```tsx + t(`filingStatus.${value}`, { defaultValue: value })} +/> +``` + +--- + +### Fields.TwoJobs + +Radio group for the W-4 multiple-jobs question (Step 2c). + +| Prop | Type | Required | +| -------------------- | -------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `getOptionLabel` | `(value: boolean) => string` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** Two options for `true` and `false`. The default labels are `'Yes'` and `'No'`. Use `getOptionLabel` to localize: + +```tsx + (value ? t('yesLabel') : t('noLabel'))} +/> +``` + +The form submits a boolean value. Because a radio group always has a selection, the `REQUIRED` code is never reached in practice — but it is listed for parity with other field types. + +--- + +### Fields.DependentsAmount + +Currency number input for the W-4 dependents total (Step 3). + +| Prop | Type | Required | +| -------------------- | --------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +The field renders with `format="currency"` and `min={0}`. Empty values coerce to `0` and pass the required check. + +```tsx + +``` + +--- + +### Fields.OtherIncome + +Currency number input for the W-4 other-income field (Step 4a). + +| Prop | Type | Required | +| -------------------- | --------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +```tsx + +``` + +--- + +### Fields.Deductions + +Currency number input for the W-4 deductions field (Step 4b). + +| Prop | Type | Required | +| -------------------- | --------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +```tsx + +``` + +--- + +### Fields.ExtraWithholding + +Currency number input for the W-4 extra-withholding field (Step 4c). + +| Prop | Type | Required | +| -------------------- | --------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +```tsx + +``` + +--- + +## Usage Examples + +### With `SDKFormProvider` (context) + +A complete example using the context-based approach. All fields share the same hook, so a single `SDKFormProvider` wraps them. + +```tsx +import { + useFederalTaxesForm, + SDKFormProvider, + type UseFederalTaxesFormReady, +} from '@gusto/embedded-react-sdk' + +function FederalTaxesPage({ employeeId }: { employeeId: string }) { + const federalTaxes = useFederalTaxesForm({ employeeId }) + + if (federalTaxes.isLoading) { + const { errors, retryQueries } = federalTaxes.errorHandling + + if (errors.length > 0) { + return ( +
+

Failed to load federal tax data.

+
    + {errors.map((error, i) => ( +
  • {error.message}
  • + ))} +
+ +
+ ) + } + + return
Loading...
+ } + + return +} + +function FederalTaxesFormReady({ federalTaxes }: { federalTaxes: UseFederalTaxesFormReady }) { + const { Fields } = federalTaxes.form + + const handleSubmit = async () => { + const result = await federalTaxes.actions.onSubmit() + + if (result) { + console.log('Saved record:', result.data.version) + } + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > +

Federal tax withholdings (Form W-4)

+ + {federalTaxes.errorHandling.errors.length > 0 && ( +
+ {federalTaxes.errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + + + + + + + + + +
+ ) +} +``` + +### With `formHookResult` prop + +The same form using prop-based field connection — useful when interleaving these fields with other hooks' fields: + +```tsx +import { useFederalTaxesForm, type UseFederalTaxesFormReady } from '@gusto/embedded-react-sdk' + +function FederalTaxesPage({ employeeId }: { employeeId: string }) { + const federalTaxes = useFederalTaxesForm({ employeeId }) + + if (federalTaxes.isLoading) { + return
Loading...
+ } + + return +} + +function FederalTaxesFormReady({ federalTaxes }: { federalTaxes: UseFederalTaxesFormReady }) { + const { Fields } = federalTaxes.form + + return ( +
{ + e.preventDefault() + void federalTaxes.actions.onSubmit() + }} + > +

Federal tax withholdings (Form W-4)

+ + {federalTaxes.errorHandling.errors.length > 0 && ( +
+ {federalTaxes.errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + + + + + + + + + + ) +} +``` + +Both examples produce identical validation, error handling, and API behavior. See [Composing Multiple Hooks](./hooks.md#composing-multiple-hooks) for combining federal taxes with other forms on the same page. diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useJobForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useJobForm.md new file mode 100644 index 000000000..02ea14926 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useJobForm.md @@ -0,0 +1,338 @@ +--- +title: useJobForm +order: 4 +--- + +# useJobForm + +Creates or updates an employee's job — title, hire date, S-Corp 2% shareholder flag, and Washington state workers' compensation fields. Companion hook to `useCompensationForm`: jobs and their compensations are separate entities in the Gusto API, and this hook focuses exclusively on the job side. + +```tsx +import { useJobForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +> **Composing with `useCompensationForm`?** See [Working with Jobs and Compensations](./jobs-and-compensations.md) for end-to-end patterns covering onboarding stub-fill (POST job → PUT auto-created stub) and steady-state edits. + +--- + +## Props + +`useJobForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `employeeId` | `string` | No | — | The UUID of the employee. Optional for composed flows where the ID is created in the same submit chain — pass it via submit options instead. | +| `jobId` | `string` | No | — | When present → **update** mode (PUT /v1/jobs/:id with `version`). When absent → **create** mode (POST /v1/employees/:id/jobs). | +| `optionalFieldsToRequire` | `JobOptionalFieldsToRequire` | No | — | Override fields that are optional on a given mode to be required. See [Configurable Required Fields](#configurable-required-fields). | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data takes precedence on update. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | +| `withHireDateField` | `boolean` | No | `true` | When `false`, hides `Fields.HireDate` and drops `hireDate` from schema validation. Supply the value via `JobSubmitOptions.hireDate` at submit time (e.g. from the employee's `startDate` during onboarding). | + +### Configurable Required Fields + +| Field | Rule | Required on create | Required on update | Configurable? | +| ----------------------- | ---------- | ------------------ | ------------------ | ----------------- | +| `title` | `'create'` | Yes | No | Yes (on update) | +| `hireDate` | `'create'` | Yes | No | Yes (on update) | +| `twoPercentShareholder` | `'never'` | No | No | Yes (either mode) | +| `stateWcCovered` | `'never'` | No | No | Yes (either mode) | +| `stateWcClassCode` | predicate | When WC is covered | When WC is covered | No (auto) | + +```typescript +type JobOptionalFieldsToRequire = { + create?: Array<'twoPercentShareholder' | 'stateWcCovered'> + update?: Array<'title' | 'hireDate' | 'twoPercentShareholder' | 'stateWcCovered'> +} +``` + +`stateWcClassCode` is automatically required when `stateWcCovered` is `true` regardless of `optionalFieldsToRequire`. + +### JobFormData + +The shape of `defaultValues`: + +```typescript +interface JobFormData { + title: string + hireDate: string | null // ISO date string (YYYY-MM-DD) or null + twoPercentShareholder: boolean + stateWcCovered: boolean + stateWcClassCode: string +} +``` + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +{ + isLoading: false + data: { + currentJob: Job | null // null in create mode + jobs: Job[] | undefined // all employee jobs (when employeeId is set) + employee: Employee | null + currentWorkAddress: EmployeeWorkAddress | null + showTwoPercentShareholder: boolean // true when company is taxable as S-Corp + showStateWc: boolean // true when active work-address state is WA + } + status: { + isPending: boolean + mode: 'create' | 'update' + } + actions: { + onSubmit: (options?: JobSubmitOptions) => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: JobFormFields + fieldsMetadata: JobFieldsMetadata + hookFormInternals: { formMethods: UseFormReturn } + getFormSubmissionValues: () => JobFormOutputs | undefined + } +} +``` + +### Submit options + +```typescript +interface JobSubmitOptions { + /** Override the employeeId configured at hook construction. Useful when the employee is created in the same submit chain. */ + employeeId?: string + /** + * Supply `hireDate` at submit time rather than via a rendered field. Use + * with `withHireDateField: false` for screens that derive hireDate from + * external context (e.g. the employee's `startDate` during onboarding). + * Falls back to the loaded job's `hireDate` on update when omitted. + */ + hireDate?: string +} +``` + +`onSubmit` resolves to a `HookSubmitResult` containing both the mode (`'create' | 'update'`) and the saved `Job` entity — read the result directly rather than wiring step callbacks. + +--- + +## Verb routing + +The hook auto-routes between create and update based on `jobId`: + +| Hook config | Mode | API call | +| -------------------------------------------- | ------ | --------------------------------------------- | +| `{ employeeId, jobId }` | update | `PUT /v1/jobs/:jobId` (with `version`) | +| `{ employeeId }` (no `jobId`) | create | `POST /v1/employees/:employeeId/jobs` | +| `{}` (no `employeeId`) + submit `employeeId` | create | `POST /v1/employees/:options.employeeId/jobs` | + +Important note for onboarding: creating a job auto-creates a stub compensation. Capture `currentCompensationUuid` (and the compensation's `version` from `compensations[]`) from the create response and thread them into `useCompensationForm.actions.onSubmit({ jobId, compensationId, compensationVersion })` to update the stub. See [Working with Jobs and Compensations](./jobs-and-compensations.md). + +--- + +## Fields Reference + +### Error Codes + +```typescript +const JobErrorCodes = { + REQUIRED: 'REQUIRED', +} as const +``` + +### Fields.Title + +Text input for the job title. + +| Prop | Type | Required | +| -------------------- | ------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Required on create.** Optional on update unless `optionalFieldsToRequire.update` includes `'title'`. + +> Submitting this field on update applies the title change immediately to the active role. When the title change should instead take effect on a future date alongside a rate change, bind it via [`useCompensationForm.Fields.Title`](./useCompensationForm.md#fieldstitle) and omit `Fields.Title` here. + +```tsx + +``` + +--- + +### Fields.HireDate + +Date picker for the employee's hire date for this job. + +| Prop | Type | Required | +| -------------------- | -------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Required on create.** Optional on update unless `optionalFieldsToRequire.update` includes `'hireDate'`. + +**Conditional availability:** This field is `undefined` when `withHireDateField: false`. Supply the value via `JobSubmitOptions.hireDate` at submit time instead — useful when the date is derived from external context (e.g. the employee's `startDate` during an onboarding flow). + +```tsx +{ + Fields.HireDate && ( + + ) +} +``` + +--- + +### Fields.TwoPercentShareholder + +Checkbox indicating whether the employee is a 2% shareholder in an S-Corporation. + +| Prop | Type | Required | +| ---------------- | ------------------------------ | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `FieldComponent` | `ComponentType` | No | + +**Conditional availability:** This field is `undefined` when `data.showTwoPercentShareholder` is `false` (the company is not taxable as an S-Corp). + +```tsx +{ + Fields.TwoPercentShareholder && ( + + ) +} +``` + +--- + +### Fields.StateWcCovered + +Radio group for Washington state workers' compensation coverage. + +| Prop | Type | Required | +| ---------------- | -------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `getOptionLabel` | `(key: boolean) => string` | No | +| `FieldComponent` | `ComponentType` | No | + +**Conditional availability:** This field is `undefined` when `data.showStateWc` is `false` (the employee's active work address is not in WA). + +```tsx +{ + Fields.StateWcCovered && ( + (key ? 'Yes, covered' : 'No, not covered')} + /> + ) +} +``` + +--- + +### Fields.StateWcClassCode + +Select dropdown for Washington state workers' compensation risk class code. + +| Prop | Type | Required | +| -------------------- | ---------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** Populated from Washington state risk class codes. + +**Conditional availability:** This field is `undefined` when `data.showStateWc` is `false` or when `stateWcCovered` is `false`. Required whenever rendered (the schema enforces this independently of `optionalFieldsToRequire`). + +```tsx +{ + Fields.StateWcClassCode && ( + + ) +} +``` + +--- + +## Usage example + +```tsx +import { useJobForm, SDKFormProvider, type UseJobFormReady } from '@gusto/embedded-react-sdk' + +function JobPage({ employeeId, jobId }: { employeeId: string; jobId?: string }) { + const job = useJobForm({ employeeId, jobId }) + + if (job.isLoading) return
Loading...
+ + return +} + +function JobFormReady({ job }: { job: UseJobFormReady }) { + const { Fields } = job.form + + return ( + +
{ + e.preventDefault() + await job.actions.onSubmit() + }} + > + + + + {Fields.TwoPercentShareholder && ( + + )} + + {Fields.StateWcCovered && } + + {Fields.StateWcClassCode && ( + + )} + + + +
+ ) +} +``` + +--- + +## Related + +- [useCompensationForm](./useCompensationForm.md) — pair this with `useJobForm` for full job + compensation editing. +- [Composing Multiple Hooks](./hooks.md#composing-multiple-hooks) — coordinate `useJobForm` + `useCompensationForm` (and others) on a single screen. +- [Working with Jobs and Compensations](./jobs-and-compensations.md) — onboarding stub-fill and steady-state edit recipes. diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/usePayScheduleForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/usePayScheduleForm.md new file mode 100644 index 000000000..4940a76bd --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/usePayScheduleForm.md @@ -0,0 +1,582 @@ +--- +title: usePayScheduleForm +order: 5 +--- + +# usePayScheduleForm + +Creates or updates a company pay schedule — configuring frequency, pay dates, and previewing the resulting pay period calendar. + +```tsx +import { usePayScheduleForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +--- + +## Props + +`usePayScheduleForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| `companyId` | `string` | Yes | — | The UUID of the company. | +| `payScheduleId` | `string` | No | — | The UUID of an existing pay schedule. When provided, the hook enters update mode and pre-populates the form with the schedule's data. | +| `optionalFieldsToRequire` | `PayScheduleOptionalFieldsToRequire` | No | — | Override specific fields that are optional in a given mode to be required. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data takes precedence when editing an existing pay schedule. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | When validation runs. Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### Configurable Required Fields + +The `optionalFieldsToRequire` prop lets you override optional fields to be required in a given mode. Only fields that are optional by default can be promoted to required: + +```tsx +usePayScheduleForm({ + companyId, + optionalFieldsToRequire: { + create: ['customTwicePerMonth'], + }, +}) +``` + +`customTwicePerMonth` is currently the only field configurable via `optionalFieldsToRequire`. All other fields are either always required or have conditional rules that are not configurable. + +### PayScheduleFormData + +The shape of `defaultValues`: + +```typescript +interface PayScheduleFormData { + customName: string // Display name for the schedule + frequency: 'Every week' | 'Every other week' | 'Twice per month' | 'Monthly' + customTwicePerMonth: string // '1st15th' | 'custom' | '' + anchorPayDate: string | null // ISO date string (YYYY-MM-DD) + anchorEndOfPayPeriod: string | null // ISO date string (YYYY-MM-DD) + day1: number // First pay day of the month (1–31) + day2: number // Last pay day of the month (1–31) +} +``` + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +{ + isLoading: false + data: { + paySchedule: PayScheduleObject | null + payPeriodPreview: PayPeriods[] | null + payPreviewLoading: boolean + paymentSpeedDays: number + } + status: { + isPending: boolean + mode: 'create' | 'update' + } + actions: { + onSubmit: () => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: PayScheduleFormFields + fieldsMetadata: PayScheduleFieldsMetadata + hookFormInternals: { formMethods: UseFormReturn } + getFormSubmissionValues: () => PayScheduleFormOutputs | undefined + } +} +``` + +### Mode detection + +The hook enters create mode when no `payScheduleId` is provided (or the schedule can't be fetched). When an existing pay schedule is loaded, it enters update mode. + +### Data + +| Property | Type | Description | +| ------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------- | +| `paySchedule` | `PayScheduleObject \| null` | The loaded pay schedule entity, or `null` in create mode. | +| `payPeriodPreview` | `PayPeriods[] \| null` | Array of upcoming pay periods based on current form values. `null` when required fields are incomplete. | +| `payPreviewLoading` | `boolean` | `true` while the preview API call is in flight. | +| `paymentSpeedDays` | `number` | Number of business days the company needs to process payroll (from payment configs). Useful for UI hints. | + +### Submit + +`onSubmit` takes no arguments. It validates the form, calls the create or update API, and returns the result: + +```tsx +const result = await paySchedule.actions.onSubmit() + +if (result) { + // result.mode is 'create' or 'update' + // result.data is the saved PayScheduleCreateUpdate entity + console.log(`Pay schedule ${result.mode}d:`, result.data.uuid) +} +``` + +If validation fails, `onSubmit` returns `undefined` and the form fields display their error messages. If the API mutation fails, the error is captured in `errorHandling.errors`. + +--- + +## Fields Reference + +All fields accept `label` (required) and `description` (optional). Fields with validation accept `validationMessages` mapping error codes to display strings. + +### Error Codes + +```typescript +const PayScheduleErrorCodes = { + REQUIRED: 'REQUIRED', + DAY_RANGE: 'DAY_RANGE', +} as const +``` + +--- + +### Fields.CustomName + +Text input for the pay schedule's display name. + +| Prop | Type | Required | +| -------------------- | ------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Always required** in both create and update modes. + +--- + +### Fields.Frequency + +Select dropdown for the payroll frequency. + +| Prop | Type | Required | +| -------------------- | --------------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `getOptionLabel` | `(frequency: PayScheduleFrequency) => string` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** `'Every week'`, `'Every other week'`, `'Twice per month'`, `'Monthly'` + +**Always required.** Defaults to `'Every week'` in create mode. + +Use `getOptionLabel` to customize how frequency options are displayed: + +```tsx + t(`frequencies.${freq}`, freq)} + validationMessages={{ REQUIRED: 'Frequency is required' }} +/> +``` + +--- + +### Fields.CustomTwicePerMonth + +Radio group for selecting the twice-per-month pay day strategy. + +| Prop | Type | Required | +| ---------------- | -------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `FieldComponent` | `ComponentType` | No | + +**Options:** `'15th and Last day of the month'` (`'1st15th'`), `'Custom'` (`'custom'`) + +**Conditional availability:** This field is `undefined` when the selected frequency is not `'Twice per month'`. Always check before rendering: + +```tsx +{ + Fields.CustomTwicePerMonth && ( + + ) +} +``` + +When `'15th and Last day of the month'` is selected, `day1` and `day2` are automatically set to `15` and `31` respectively. When `'Custom'` is selected, the `Day1` and `Day2` fields become visible for manual entry. + +--- + +### Fields.AnchorPayDate + +Date picker for the first pay date. + +| Prop | Type | Required | +| -------------------- | -------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Always required.** This is the date of the first paycheck under this schedule. + +```tsx + +``` + +--- + +### Fields.AnchorEndOfPayPeriod + +Date picker for the end date of the first pay period. + +| Prop | Type | Required | +| -------------------- | -------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Always required.** This date helps the API calculate future pay periods. It can be the same date as the first pay date. + +--- + +### Fields.Day1 + +Number input for the first pay day of the month (1–31). + +| Prop | Type | Required | +| -------------------- | ----------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, DAY_RANGE: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Conditional availability:** This field is `undefined` unless: + +- Frequency is `'Monthly'`, or +- Frequency is `'Twice per month'` and `CustomTwicePerMonth` is `'Custom'` + +```tsx +{ + Fields.Day1 && ( + + ) +} +``` + +--- + +### Fields.Day2 + +Number input for the last pay day of the month (1–31). + +| Prop | Type | Required | +| -------------------- | ----------------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string, DAY_RANGE: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Conditional availability:** This field is `undefined` unless frequency is `'Twice per month'` and `CustomTwicePerMonth` is `'Custom'`. + +--- + +## Pay Period Preview + +The hook provides a live pay period preview based on the current form values. When both `anchorPayDate` and `anchorEndOfPayPeriod` are filled in, the hook fetches a preview of upcoming pay periods from the API. + +```tsx +const { payPeriodPreview, payPreviewLoading, paymentSpeedDays } = paySchedule.data + +// payPeriodPreview is null until both date fields are complete +if (payPeriodPreview) { + payPeriodPreview.forEach(period => { + console.log(period.startDate, period.endDate, period.checkDate, period.runPayrollBy) + }) +} +``` + +Each `PayPeriods` entry contains: + +| Property | Type | Description | +| -------------- | --------------------- | ----------------------------------------------- | +| `startDate` | `string \| undefined` | Start of the pay period (ISO date) | +| `endDate` | `string \| undefined` | End of the pay period (ISO date) | +| `checkDate` | `string \| undefined` | The payday — when employees receive their check | +| `runPayrollBy` | `string \| undefined` | Deadline to process payroll for this period | + +The preview automatically refreshes when frequency, dates, or day1/day2 values change. + +--- + +## Usage Examples + +### Basic create form with `SDKFormProvider` + +```tsx +import { + usePayScheduleForm, + SDKFormProvider, + type UsePayScheduleFormReady, +} from '@gusto/embedded-react-sdk' + +function PaySchedulePage({ companyId }: { companyId: string }) { + const paySchedule = usePayScheduleForm({ companyId }) + + if (paySchedule.isLoading) { + const { errors, retryQueries } = paySchedule.errorHandling + + if (errors.length > 0) { + return ( +
+

Failed to load pay schedule data.

+
    + {errors.map((error, i) => ( +
  • {error.message}
  • + ))} +
+ +
+ ) + } + + return
Loading...
+ } + + return +} + +function PayScheduleFormReady({ paySchedule }: { paySchedule: UsePayScheduleFormReady }) { + const { Fields } = paySchedule.form + const { paymentSpeedDays } = paySchedule.data + + const handleSubmit = async () => { + const result = await paySchedule.actions.onSubmit() + if (result) { + console.log(`Pay schedule ${result.mode}d:`, result.data.uuid) + } + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > +

{paySchedule.status.mode === 'create' ? 'Add Pay Schedule' : 'Edit Pay Schedule'}

+ + {paySchedule.errorHandling.errors.length > 0 && ( +
+ {paySchedule.errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + + + + + {Fields.CustomTwicePerMonth && ( + + )} + + + + + + {Fields.Day1 && ( + + )} + + {Fields.Day2 && ( + + )} + + + +
+ ) +} +``` + +### Edit mode + +Pass `payScheduleId` to load an existing schedule and enter update mode: + +```tsx +const paySchedule = usePayScheduleForm({ + companyId: 'company-uuid', + payScheduleId: 'existing-schedule-uuid', +}) + +// paySchedule.status.mode will be 'update' +// paySchedule.data.paySchedule contains the loaded schedule +``` + +### With `formHookResult` prop + +The same form using prop-based field connection. No `SDKFormProvider` wrapper needed: + +```tsx +import { usePayScheduleForm, type UsePayScheduleFormReady } from '@gusto/embedded-react-sdk' + +function PayScheduleFormReady({ paySchedule }: { paySchedule: UsePayScheduleFormReady }) { + const { Fields } = paySchedule.form + + return ( +
{ + e.preventDefault() + void paySchedule.actions.onSubmit() + }} + > + + + + + {Fields.CustomTwicePerMonth && ( + + )} + + + + + + {Fields.Day1 && ( + + )} + + {Fields.Day2 && ( + + )} + + + + ) +} +``` + +### Using the pay period preview + +Build a calendar preview UI using the hook's preview data: + +```tsx +function PaySchedulePreview({ paySchedule }: { paySchedule: UsePayScheduleFormReady }) { + const { payPeriodPreview, payPreviewLoading } = paySchedule.data + const [selectedIndex, setSelectedIndex] = useState(0) + + if (payPreviewLoading) { + return
Loading preview...
+ } + + if (!payPeriodPreview || payPeriodPreview.length === 0) { + return

Complete the required fields to see a preview of your pay schedule.

+ } + + const period = payPeriodPreview[selectedIndex] + + return ( +
+ + + {period?.checkDate &&

Payday: {period.checkDate}

} + {period?.runPayrollBy &&

Run payroll by: {period.runPayrollBy}

} +
+ ) +} +``` diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/usePaymentMethodForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/usePaymentMethodForm.md new file mode 100644 index 000000000..240e6ae72 --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/usePaymentMethodForm.md @@ -0,0 +1,227 @@ +--- +title: usePaymentMethodForm +order: 8 +--- + +# usePaymentMethodForm + +Updates an employee's payment method between Direct Deposit and Check. Always operates in update mode — every employee has a payment method, defaulting to Check. + +```tsx +import { usePaymentMethodForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +--- + +## Props + +`usePaymentMethodForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------- | +| `employeeId` | `string` | Yes | — | The UUID of the employee whose payment method is being edited. | +| `optionalFieldsToRequire` | `PaymentMethodFormOptionalFieldsToRequire` | No | — | Reserved for future schema expansion. `type` is always required and always has a default. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values. Server data (the current payment method) takes precedence when supplied is empty. | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | When validation runs. Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### PaymentMethodFormData + +The shape of `defaultValues`: + +```typescript +interface PaymentMethodFormData { + type: 'Direct Deposit' | 'Check' +} +``` + +The constant `PAYMENT_METHOD_TYPES` (`['Direct Deposit', 'Check']`) is exported for convenience. + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +The hook fetches the existing payment method via `GET /v1/employees/:id/payment_method`. While that request is in flight, only `isLoading` and `errorHandling` are available. + +### Ready state + +```typescript +{ + isLoading: false + data: { + paymentMethod: EmployeePaymentMethod + } + status: { + isPending: boolean + mode: 'update' + } + actions: { + onSubmit: () => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: PaymentMethodFormFields + fieldsMetadata: PaymentMethodFormFieldsMetadata + hookFormInternals: { + formMethods: UseFormReturn + } + getFormSubmissionValues: () => PaymentMethodFormOutputs | undefined + } +} +``` + +### Submit behavior + +- Switching to **Check** sends a minimal PUT body (`{ version, type: 'Check' }`). +- Switching to or staying on **Direct Deposit** preserves the existing `splitBy`, `splits`, and `version` so split allocations are not lost when only the type changes. + +`onSubmit` returns `HookSubmitResult` with `mode: 'update'` and `data` set to the updated payment method, or `undefined` if validation fails or the mutation errors (errors are captured in `errorHandling.errors`). + +--- + +## Fields Reference + +All fields accept `label` (required) and `description` (optional). Fields with validation accept `validationMessages` mapping error codes to display strings. + +### Error Codes + +```typescript +const PaymentMethodFormErrorCodes = { + REQUIRED: 'REQUIRED', +} as const +``` + +| Field | Input type | Required by default | Error codes | Conditional availability | +| ------ | ------------------- | ------------------- | ----------- | -------------------------------------------------------------- | +| `Type` | Radio (two options) | Yes (has a default) | `REQUIRED` | Always rendered. Defaults to the existing payment method type. | + +`Type` always carries the existing payment method as its default, so `REQUIRED` won't fire in practice and `validationMessages` can be omitted on that field. Supply `getOptionLabel` to translate the `Direct Deposit` / `Check` labels in your UI. + +To render per-option descriptions or other UI customization, pass a `FieldComponent` that augments the options with descriptions before delegating to the SDK's radio primitive. + +--- + +## Usage Examples + +### With `SDKFormProvider` (context) + +```tsx +import { + usePaymentMethodForm, + SDKFormProvider, + PAYMENT_METHODS, + type UsePaymentMethodFormReady, + type PaymentMethodType, +} from '@gusto/embedded-react-sdk' + +function PaymentMethodPage({ employeeId }: { employeeId: string }) { + const paymentMethodForm = usePaymentMethodForm({ employeeId }) + + if (paymentMethodForm.isLoading) { + return
Loading...
+ } + + return +} + +function PaymentMethodFormReady({ + paymentMethodForm, +}: { + paymentMethodForm: UsePaymentMethodFormReady +}) { + const { Fields } = paymentMethodForm.form + + const handleSubmit = async () => { + const result = await paymentMethodForm.actions.onSubmit() + if (result) { + console.log('Payment method updated:', result.data.type) + } + } + + return ( + +
{ + e.preventDefault() + void handleSubmit() + }} + > +

Payment method

+ + {paymentMethodForm.errorHandling.errors.length > 0 && ( +
+ {paymentMethodForm.errorHandling.errors.map((error, i) => ( +

{error.message}

+ ))} +
+ )} + + + value === PAYMENT_METHODS.directDeposit ? 'Direct Deposit' : 'Check' + } + /> + + + +
+ ) +} +``` + +### With `formHookResult` prop + +The same form using prop-based field connection. No `SDKFormProvider` wrapper needed: + +```tsx +import { usePaymentMethodForm, type UsePaymentMethodFormReady } from '@gusto/embedded-react-sdk' + +function PaymentMethodPage({ employeeId }: { employeeId: string }) { + const paymentMethodForm = usePaymentMethodForm({ employeeId }) + + if (paymentMethodForm.isLoading) { + return
Loading...
+ } + + return +} + +function PaymentMethodFormReady({ + paymentMethodForm, +}: { + paymentMethodForm: UsePaymentMethodFormReady +}) { + const { Fields } = paymentMethodForm.form + + return ( +
{ + e.preventDefault() + void paymentMethodForm.actions.onSubmit() + }} + > + + + + + ) +} +``` + +Both examples produce identical validation, error handling, and API behavior. diff --git a/docs-site/versioned_docs/version-0.46.3/hooks/useSignCompanyForm.md b/docs-site/versioned_docs/version-0.46.3/hooks/useSignCompanyForm.md new file mode 100644 index 000000000..f6a5dbbfb --- /dev/null +++ b/docs-site/versioned_docs/version-0.46.3/hooks/useSignCompanyForm.md @@ -0,0 +1,306 @@ +--- +title: useSignCompanyForm +order: 5 +--- + +# useSignCompanyForm + +Signs a company form — displays the form PDF and collects a typed signature with confirmation. + +```tsx +import { useSignCompanyForm, SDKFormProvider } from '@gusto/embedded-react-sdk' +``` + +--- + +## Props + +`useSignCompanyForm` accepts a single options object: + +| Prop | Type | Required | Default | Description | +| ------------------------- | -------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| `formId` | `string` | Yes | — | The UUID of the company form to sign. | +| `optionalFieldsToRequire` | `SignCompanyFormOptionalFieldsToRequire` | No | — | Override specific fields to be required. Both fields are already required by default, so this is typically unnecessary for this hook. | +| `defaultValues` | `Partial` | No | — | Pre-fill form values (e.g., pre-populate the signature field). | +| `validationMode` | `'onSubmit' \| 'onBlur' \| 'onChange' \| 'onTouched' \| 'all'` | No | `'onSubmit'` | When validation runs. Passed through to react-hook-form. | +| `shouldFocusError` | `boolean` | No | `true` | Auto-focus the first invalid field on submit. Set to `false` when using `composeSubmitHandler`. | + +### SignCompanyFormData + +The shape of `defaultValues`: + +```typescript +interface SignCompanyFormData { + signature: string // The signer's typed name + confirmSignature: boolean // Acknowledgement checkbox +} +``` + +--- + +## Return Type + +The hook returns a discriminated union on `isLoading`. + +### Loading state + +```typescript +{ + isLoading: true + errorHandling: HookErrorHandling +} +``` + +### Ready state + +```typescript +{ + isLoading: false + data: { + companyForm: Form // The company form entity (title, description, etc.) + pdfUrl: string | null // URL to the form's PDF document + } + status: { + isPending: boolean + } + actions: { + onSubmit: () => Promise | undefined> + } + errorHandling: HookErrorHandling + form: { + Fields: SignCompanyFormFields + fieldsMetadata: SignCompanyFormFieldsMetadata + hookFormInternals: { + formMethods: UseFormReturn + } + getFormSubmissionValues: () => SignCompanyFormOutputs | undefined + } +} +``` + +### Data + +The `data` object contains the company form entity and its PDF URL. Use `data.companyForm` to display the form's title and description, and `data.pdfUrl` to render the document for the user to review before signing. + +### Submit result + +`onSubmit` returns a `HookSubmitResult
| undefined`. On success, `result.data` is the signed `Form` — use it for any post-submit side effects. On validation or API failure, `onSubmit` returns `undefined` and the error is exposed via `errorHandling.errors`. + +--- + +## Fields Reference + +All fields accept `label` (required) and `description` (optional). Fields with validation accept `validationMessages`. All fields accept an optional `FieldComponent` prop to override the rendered UI component. + +### Error Codes + +```typescript +const SignCompanyFormErrorCodes = { + REQUIRED: 'REQUIRED', +} as const +``` + +--- + +### Fields.Signature + +Text input for the signer's typed name. + +| Prop | Type | Required | +| -------------------- | ------------------------------- | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Always required.** + +```tsx + +``` + +--- + +### Fields.ConfirmSignature + +Checkbox to confirm the signature and agree to the form's terms. + +| Prop | Type | Required | +| -------------------- | ------------------------------ | -------- | +| `label` | `string` | Yes | +| `description` | `ReactNode` | No | +| `validationMessages` | `{ REQUIRED: string }` | No | +| `FieldComponent` | `ComponentType` | No | + +**Always required.** The checkbox must be checked for the form to submit. + +```tsx + +``` + +--- + +## Usage Examples + +### With `SDKFormProvider` (context) + +A complete example showing form PDF display, both fields, validation messages, and submit handling: + +```tsx +import { + useSignCompanyForm, + SDKFormProvider, + type UseSignCompanyFormReady, +} from '@gusto/embedded-react-sdk' + +function SignFormPage({ formId }: { formId: string }) { + const signForm = useSignCompanyForm({ formId }) + + if (signForm.isLoading) { + const { errors, retryQueries } = signForm.errorHandling + + if (errors.length > 0) { + return ( +
+

Failed to load form data.

+
    + {errors.map((error, i) => ( +
  • {error.message}
  • + ))} +
+ +
+ ) + } + + return
Loading...
+ } + + return +} + +function SignFormReady({ signForm }: { signForm: UseSignCompanyFormReady }) { + const { Fields } = signForm.form + + const handleSubmit = async () => { + const result = await signForm.actions.onSubmit() + + if (result) { + console.log('Signed form:', result.data.uuid) + } + } + + return ( + + { + e.preventDefault() + void handleSubmit() + }} + > +

{signForm.data.companyForm.title}

+ + {signForm.data.companyForm.description &&

{signForm.data.companyForm.description}

} + + {signForm.data.pdfUrl && ( +