From 9121ec6e8b7ee7ab3eb5a7e8f7eae238389f077d Mon Sep 17 00:00:00 2001 From: Kristine White Date: Tue, 18 Aug 2026 15:11:15 -0700 Subject: [PATCH 1/5] feat(SDK-1161): add print-checks terminal screens and event vocabulary First slice of the print-checks feature (split out of #2572 for reviewability): the shared event constants and the two terminal screens of the upcoming print-checks flow, PrintChecksFailure and PrintChecksSummary. Both are standalone, presentational components that will be wired into a state-machine orchestrator in a follow-up PR stacked on this one. Co-Authored-By: Claude Sonnet 5 --- .../PrintChecksFailure.stories.tsx | 35 +++++++++++ .../PrintChecksFailure.test.tsx | 40 ++++++++++++ .../PrintChecksFailure/PrintChecksFailure.tsx | 63 +++++++++++++++++++ .../PrintChecks/PrintChecksFailure/index.ts | 1 + .../PrintChecksSummary.stories.tsx | 35 +++++++++++ .../PrintChecksSummary.test.tsx | 38 +++++++++++ .../PrintChecksSummary/PrintChecksSummary.tsx | 59 +++++++++++++++++ .../PrintChecks/PrintChecksSummary/index.ts | 1 + src/i18n/en/Payroll.PrintChecksFailure.json | 5 ++ src/i18n/en/Payroll.PrintChecksSummary.json | 6 ++ src/i18n/types.d.ts | 22 +++++++ src/shared/constants.ts | 21 +++++++ 12 files changed, 326 insertions(+) create mode 100644 src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.stories.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.test.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksFailure/index.ts create mode 100644 src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.stories.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.test.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksSummary/index.ts create mode 100644 src/i18n/en/Payroll.PrintChecksFailure.json create mode 100644 src/i18n/en/Payroll.PrintChecksSummary.json diff --git a/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.stories.tsx b/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.stories.tsx new file mode 100644 index 000000000..4cfc4c80e --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.stories.tsx @@ -0,0 +1,35 @@ +import { Suspense } from 'react' +import { fn } from 'storybook/test' +import { MockBaseProvider } from '../../../../../.storybook/helpers/MockBaseProvider' +import { PrintChecksFailure } from './PrintChecksFailure' +import { useI18n } from '@/i18n' + +function I18nLoader({ children }: { children: React.ReactNode }) { + useI18n('Payroll.PrintChecksFailure') + return <>{children} +} + +export default { + title: 'Domain/Payroll/PrintChecksFailure', + decorators: [ + (Story: React.ComponentType) => ( + Loading translations...}> + + + + + + + ), + ], +} + +export const Default = () => ( + <> + + + +) diff --git a/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.test.tsx b/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.test.tsx new file mode 100644 index 000000000..4b969ea51 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.test.tsx @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from 'vitest' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { PrintChecksFailure } from './PrintChecksFailure' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { printChecksEvents } from '@/shared/constants' + +describe('PrintChecksFailure', () => { + const user = userEvent.setup() + + it('renders the error message', async () => { + renderWithProviders( + , + ) + + expect(await screen.findByText("We couldn't generate your checks")).toBeInTheDocument() + expect(screen.getByText('Cannot generate checks on an unprocessed payroll')).toBeInTheDocument() + }) + + it('fires PRINT_CHECKS_RETRY when the retry button is clicked', async () => { + const onEvent = vi.fn() + renderWithProviders() + + await user.click(await screen.findByRole('button', { name: 'Try again' })) + + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_RETRY) + }) + + it('fires PRINT_CHECKS_CLOSE when the Footer close button is clicked', async () => { + const onEvent = vi.fn() + renderWithProviders() + + await user.click(await screen.findByRole('button', { name: 'Close' })) + + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_CLOSE) + }) +}) diff --git a/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.tsx b/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.tsx new file mode 100644 index 000000000..db6e3a8f6 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksFailure/PrintChecksFailure.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from 'react-i18next' +import { BaseComponent, type BaseComponentInterface } from '@/components/Base' +import type { OnEventType } from '@/components/Base/useBase' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useComponentDictionary, useI18n } from '@/i18n' +import { ActionsLayout, Flex } from '@/components/Common' +import { printChecksEvents, type EventType } from '@/shared/constants' + +interface PrintChecksFailureProps extends BaseComponentInterface<'Payroll.PrintChecksFailure'> { + errorMessage?: string +} + +/** @internal */ +export function PrintChecksFailure(props: PrintChecksFailureProps) { + return ( + + {props.children} + + ) +} + +const Root = ({ dictionary, errorMessage, onEvent }: PrintChecksFailureProps) => { + useComponentDictionary('Payroll.PrintChecksFailure', dictionary) + useI18n('Payroll.PrintChecksFailure') + const { t } = useTranslation('Payroll.PrintChecksFailure') + const { Alert, Button } = useComponentContext() + + return ( + + + {errorMessage} + + + + ) +} + +const Footer = ({ onEvent }: { onEvent: OnEventType }) => { + useI18n('Payroll.PrintChecksFailure') + const { t } = useTranslation('Payroll.PrintChecksFailure') + const { Button } = useComponentContext() + + return ( + + + + ) +} +PrintChecksFailure.Footer = Footer diff --git a/src/components/Payroll/PrintChecks/PrintChecksFailure/index.ts b/src/components/Payroll/PrintChecks/PrintChecksFailure/index.ts new file mode 100644 index 000000000..49e02db11 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksFailure/index.ts @@ -0,0 +1 @@ +export { PrintChecksFailure } from './PrintChecksFailure' diff --git a/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.stories.tsx b/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.stories.tsx new file mode 100644 index 000000000..5b10df686 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.stories.tsx @@ -0,0 +1,35 @@ +import { Suspense } from 'react' +import { fn } from 'storybook/test' +import { MockBaseProvider } from '../../../../../.storybook/helpers/MockBaseProvider' +import { PrintChecksSummary } from './PrintChecksSummary' +import { useI18n } from '@/i18n' + +function I18nLoader({ children }: { children: React.ReactNode }) { + useI18n('Payroll.PrintChecksSummary') + return <>{children} +} + +export default { + title: 'Domain/Payroll/PrintChecksSummary', + decorators: [ + (Story: React.ComponentType) => ( + Loading translations...}> + + + + + + + ), + ], +} + +export const Default = () => ( + <> + + + +) diff --git a/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.test.tsx b/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.test.tsx new file mode 100644 index 000000000..1acef761c --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.test.tsx @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from 'vitest' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { PrintChecksSummary } from './PrintChecksSummary' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { printChecksEvents } from '@/shared/constants' + +describe('PrintChecksSummary', () => { + const user = userEvent.setup() + + it('renders a link to the document when a documentUrl is provided', async () => { + renderWithProviders( + , + ) + + expect(await screen.findByText('Your checks are ready')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'View checks' })).toHaveAttribute( + 'href', + 'https://example.com/checks.pdf', + ) + }) + + it('omits the link when no documentUrl is provided', async () => { + renderWithProviders() + + expect(await screen.findByText('Your checks are ready')).toBeInTheDocument() + expect(screen.queryByRole('link', { name: 'View checks' })).toBeNull() + }) + + it('fires PRINT_CHECKS_CLOSE when the Footer close button is clicked', async () => { + const onEvent = vi.fn() + renderWithProviders() + + await user.click(await screen.findByRole('button', { name: 'Close' })) + + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_CLOSE) + }) +}) diff --git a/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.tsx b/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.tsx new file mode 100644 index 000000000..fb99ebf71 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksSummary/PrintChecksSummary.tsx @@ -0,0 +1,59 @@ +import { useTranslation } from 'react-i18next' +import { BaseComponent, type BaseComponentInterface } from '@/components/Base' +import type { OnEventType } from '@/components/Base/useBase' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useComponentDictionary, useI18n } from '@/i18n' +import { ActionsLayout, Flex } from '@/components/Common' +import { printChecksEvents, type EventType } from '@/shared/constants' + +interface PrintChecksSummaryProps extends BaseComponentInterface<'Payroll.PrintChecksSummary'> { + documentUrl?: string +} + +/** @internal */ +export function PrintChecksSummary(props: PrintChecksSummaryProps) { + return ( + + {props.children} + + ) +} + +const Root = ({ dictionary, documentUrl }: PrintChecksSummaryProps) => { + useComponentDictionary('Payroll.PrintChecksSummary', dictionary) + useI18n('Payroll.PrintChecksSummary') + const { t } = useTranslation('Payroll.PrintChecksSummary') + const { Heading, Text, Link } = useComponentContext() + + return ( + + {t('succeededTitle')} + {t('succeededDescription')} + {documentUrl && ( + + {t('viewChecksCta')} + + )} + + ) +} + +const Footer = ({ onEvent }: { onEvent: OnEventType }) => { + useI18n('Payroll.PrintChecksSummary') + const { t } = useTranslation('Payroll.PrintChecksSummary') + const { Button } = useComponentContext() + + return ( + + + + ) +} +PrintChecksSummary.Footer = Footer diff --git a/src/components/Payroll/PrintChecks/PrintChecksSummary/index.ts b/src/components/Payroll/PrintChecks/PrintChecksSummary/index.ts new file mode 100644 index 000000000..22904e615 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksSummary/index.ts @@ -0,0 +1 @@ +export { PrintChecksSummary } from './PrintChecksSummary' diff --git a/src/i18n/en/Payroll.PrintChecksFailure.json b/src/i18n/en/Payroll.PrintChecksFailure.json new file mode 100644 index 000000000..f94121065 --- /dev/null +++ b/src/i18n/en/Payroll.PrintChecksFailure.json @@ -0,0 +1,5 @@ +{ + "failedTitle": "We couldn't generate your checks", + "retryCta": "Try again", + "closeCta": "Close" +} diff --git a/src/i18n/en/Payroll.PrintChecksSummary.json b/src/i18n/en/Payroll.PrintChecksSummary.json new file mode 100644 index 000000000..95e736c6e --- /dev/null +++ b/src/i18n/en/Payroll.PrintChecksSummary.json @@ -0,0 +1,6 @@ +{ + "succeededTitle": "Your checks are ready", + "succeededDescription": "The download should have started automatically. If not, use the link below.", + "viewChecksCta": "View checks", + "closeCta": "Close" +} diff --git a/src/i18n/types.d.ts b/src/i18n/types.d.ts index d559c6f23..e760e8bb9 100644 --- a/src/i18n/types.d.ts +++ b/src/i18n/types.d.ts @@ -127,6 +127,8 @@ export interface Resources { 'Payroll.PayrollList': Translations.PayrollPayrollList 'Payroll.PayrollOverview': Translations.PayrollPayrollOverview 'Payroll.PayrollReceipts': Translations.PayrollPayrollReceipts + 'Payroll.PrintChecksFailure': Translations.PayrollPrintChecksFailure + 'Payroll.PrintChecksSummary': Translations.PayrollPrintChecksSummary 'Payroll.RecoveryCasesList': Translations.PayrollRecoveryCasesList 'Payroll.RecoveryCasesResubmit': Translations.PayrollRecoveryCasesResubmit 'Payroll.Transition': Translations.PayrollTransition @@ -7665,6 +7667,26 @@ export namespace Translations { totalEmployees_other: string } } + /** Translation keys for the `Payroll.PrintChecksFailure` i18n namespace. */ + export interface PayrollPrintChecksFailure { + /** @defaultValue `"We couldn't generate your checks"` */ + failedTitle: string + /** @defaultValue `"Try again"` */ + retryCta: string + /** @defaultValue `"Close"` */ + closeCta: string + } + /** Translation keys for the `Payroll.PrintChecksSummary` i18n namespace. */ + export interface PayrollPrintChecksSummary { + /** @defaultValue `"Your checks are ready"` */ + succeededTitle: string + /** @defaultValue `"The download should have started automatically. If not, use the link below."` */ + succeededDescription: string + /** @defaultValue `"View checks"` */ + viewChecksCta: string + /** @defaultValue `"Close"` */ + closeCta: string + } /** Translation keys for the `Payroll.RecoveryCasesList` i18n namespace. */ export interface PayrollRecoveryCasesList { /** @defaultValue `"Recovery cases"` */ diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 153799c8b..c57f77132 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -466,6 +466,26 @@ export const recoveryCasesEvents = { RECOVERY_CASE_RESUBMIT_DONE: 'recoveryCase/resubmit/done', } as const +/** + * Event keys emitted by the print-checks component. + * + * @remarks + * These keys are merged into {@link componentEvents}. Consume them through the + * `onEvent` handler of the print-checks component and compare against the + * value of an entry on this object. + * + * @public + */ +export const printChecksEvents = { + PRINT_CHECKS_START: 'payroll/printChecks/start', + PRINT_CHECKS_GENERATE_START: 'payroll/printChecks/generate/start', + PRINT_CHECKS_GENERATE_SUCCEEDED: 'payroll/printChecks/generate/succeeded', + PRINT_CHECKS_GENERATE_FAILED: 'payroll/printChecks/generate/failed', + PRINT_CHECKS_RETRY: 'payroll/printChecks/retry', + PRINT_CHECKS_CANCEL: 'payroll/printChecks/cancel', + PRINT_CHECKS_CLOSE: 'payroll/printChecks/close', +} as const + /** * Event keys emitted by off-cycle payroll and transition components. * @@ -567,6 +587,7 @@ export const componentEvents = { ...payrollWireEvents, ...informationRequestEvents, ...recoveryCasesEvents, + ...printChecksEvents, ...contractorPaymentEvents, ...contractorHistoricalPaymentEvents, ...offCycleEvents, From a1b611fd8b28b78289ca9570ef3570398fd134c2 Mon Sep 17 00:00:00 2001 From: Kristine White Date: Tue, 18 Aug 2026 15:14:38 -0700 Subject: [PATCH 2/5] feat(SDK-1161): add PrintChecksBanner Second slice of the print-checks feature stack (see #2584). A standalone banner that fetches its own payroll data via usePayrollsGet, shows an info alert when the payroll has employees paid by check, and surfaces a "View and print checks" CTA once the payroll is processed. Fires PRINT_CHECKS_START when clicked. Stacked on kw/feat/print-checks-1-terminal-screens; targets that branch, not main. Co-Authored-By: Claude Sonnet 5 --- .../PrintChecksBanner.test.tsx | 91 +++++++++++++++++++ .../PrintChecksBanner/PrintChecksBanner.tsx | 65 +++++++++++++ .../PrintChecks/PrintChecksBanner/index.ts | 1 + src/i18n/en/Payroll.PrintChecksBanner.json | 6 ++ src/i18n/types.d.ts | 12 +++ 5 files changed, 175 insertions(+) create mode 100644 src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.test.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksBanner/index.ts create mode 100644 src/i18n/en/Payroll.PrintChecksBanner.json diff --git a/src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.test.tsx b/src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.test.tsx new file mode 100644 index 000000000..77aca09ab --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.test.tsx @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { PrintChecksBanner } from './PrintChecksBanner' +import { renderWithProviders } from '@/test-utils/renderWithProviders' + +const checkCompensation = { + employeeUuid: 'emp-check-1', + excluded: false, + paymentMethod: 'Check', +} + +let mockEmployeeCompensations: Record[] = [] +let mockProcessed = false +let mockProcessingStatus: string | undefined + +vi.mock('@gusto/embedded-api/react-query/payrollsGet', () => ({ + usePayrollsGet: () => ({ + data: { + payrollShow: { + processed: mockProcessed, + processingRequest: mockProcessingStatus ? { status: mockProcessingStatus } : undefined, + employeeCompensations: mockEmployeeCompensations, + }, + }, + }), +})) + +describe('PrintChecksBanner', () => { + const onEvent = vi.fn() + const onStartPrintChecks = vi.fn() + const user = userEvent.setup() + + const defaultProps = { + companyId: 'company-1', + payrollId: 'payroll-1', + onStartPrintChecks, + onEvent, + } + + beforeEach(() => { + vi.clearAllMocks() + mockEmployeeCompensations = [] + mockProcessed = false + mockProcessingStatus = undefined + }) + + it('renders nothing when no employees are paid by check', () => { + renderWithProviders() + + expect(screen.queryByRole('alert')).toBeNull() + }) + + it('shows the alert without a CTA when the payroll has not been processed', async () => { + mockEmployeeCompensations = [checkCompensation] + + renderWithProviders() + + expect(await screen.findByText(/noted 1 employee/i)).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'View and print checks' })).toBeNull() + }) + + it('shows the CTA once the payroll is processed, and fires onStartPrintChecks when clicked', async () => { + mockEmployeeCompensations = [checkCompensation] + mockProcessed = true + + renderWithProviders() + + const cta = await screen.findByRole('button', { name: 'View and print checks' }) + await user.click(cta) + + expect(onStartPrintChecks).toHaveBeenCalledTimes(1) + }) + + it('shows the CTA when the processing request succeeded even if processed is not yet true', async () => { + mockEmployeeCompensations = [checkCompensation] + mockProcessingStatus = 'submit_success' + + renderWithProviders() + + expect(await screen.findByRole('button', { name: 'View and print checks' })).toBeInTheDocument() + }) + + it('excludes compensations marked as excluded from the count', () => { + mockEmployeeCompensations = [{ ...checkCompensation, excluded: true }] + + renderWithProviders() + + expect(screen.queryByRole('alert')).toBeNull() + }) +}) diff --git a/src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.tsx b/src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.tsx new file mode 100644 index 000000000..9ae13f1c5 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksBanner/PrintChecksBanner.tsx @@ -0,0 +1,65 @@ +import { useTranslation } from 'react-i18next' +import { usePayrollsGet } from '@gusto/embedded-api/react-query/payrollsGet' +import { BaseComponent, type BaseComponentInterface } from '@/components/Base' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useComponentDictionary, useI18n } from '@/i18n' +import { PAYMENT_METHODS, PAYROLL_PROCESSING_STATUS } from '@/shared/constants' + +interface PrintChecksBannerProps extends BaseComponentInterface<'Payroll.PrintChecksBanner'> { + companyId: string + payrollId: string + onStartPrintChecks: () => void +} + +/** @internal */ +export function PrintChecksBanner(props: PrintChecksBannerProps) { + return ( + + {props.children} + + ) +} + +const Root = ({ companyId, payrollId, dictionary, onStartPrintChecks }: PrintChecksBannerProps) => { + useComponentDictionary('Payroll.PrintChecksBanner', dictionary) + useI18n('Payroll.PrintChecksBanner') + const { t } = useTranslation('Payroll.PrintChecksBanner') + const { Alert, Button } = useComponentContext() + + const { data } = usePayrollsGet({ + companyId, + payrollId, + }) + const payrollData = data?.payrollShow + + const checkPaymentsCount = + payrollData?.employeeCompensations?.reduce( + (acc, comp) => + !comp.excluded && comp.paymentMethod === PAYMENT_METHODS.check ? acc + 1 : acc, + 0, + ) ?? 0 + + const isProcessed = + payrollData?.processed === true || + payrollData?.processingRequest?.status === PAYROLL_PROCESSING_STATUS.submit_success + + if (checkPaymentsCount === 0) { + return null + } + + return ( + + {t('cta')} + + ) + } + > + {t('description')} + + ) +} diff --git a/src/components/Payroll/PrintChecks/PrintChecksBanner/index.ts b/src/components/Payroll/PrintChecks/PrintChecksBanner/index.ts new file mode 100644 index 000000000..fb2fa9e31 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksBanner/index.ts @@ -0,0 +1 @@ +export { PrintChecksBanner } from './PrintChecksBanner' diff --git a/src/i18n/en/Payroll.PrintChecksBanner.json b/src/i18n/en/Payroll.PrintChecksBanner.json new file mode 100644 index 000000000..d75b58b22 --- /dev/null +++ b/src/i18n/en/Payroll.PrintChecksBanner.json @@ -0,0 +1,6 @@ +{ + "title_one": "You noted {{count}} employee that should be paid by check.", + "title_other": "You noted {{count}} employees that should be paid by check.", + "description": "Employees with this payment method will need their checks delivered to them. If you aren't using your own checks, you can view and print checks.", + "cta": "View and print checks" +} diff --git a/src/i18n/types.d.ts b/src/i18n/types.d.ts index e760e8bb9..cfc273e6e 100644 --- a/src/i18n/types.d.ts +++ b/src/i18n/types.d.ts @@ -127,6 +127,7 @@ export interface Resources { 'Payroll.PayrollList': Translations.PayrollPayrollList 'Payroll.PayrollOverview': Translations.PayrollPayrollOverview 'Payroll.PayrollReceipts': Translations.PayrollPayrollReceipts + 'Payroll.PrintChecksBanner': Translations.PayrollPrintChecksBanner 'Payroll.PrintChecksFailure': Translations.PayrollPrintChecksFailure 'Payroll.PrintChecksSummary': Translations.PayrollPrintChecksSummary 'Payroll.RecoveryCasesList': Translations.PayrollRecoveryCasesList @@ -7667,6 +7668,17 @@ export namespace Translations { totalEmployees_other: string } } + /** Translation keys for the `Payroll.PrintChecksBanner` i18n namespace. */ + export interface PayrollPrintChecksBanner { + /** @defaultValue `"You noted {{count}} employee that should be paid by check."` */ + title_one: string + /** @defaultValue `"You noted {{count}} employees that should be paid by check."` */ + title_other: string + /** @defaultValue `"Employees with this payment method will need their checks delivered to them. If you aren't using your own checks, you can view and print checks."` */ + description: string + /** @defaultValue `"View and print checks"` */ + cta: string + } /** Translation keys for the `Payroll.PrintChecksFailure` i18n namespace. */ export interface PayrollPrintChecksFailure { /** @defaultValue `"We couldn't generate your checks"` */ From 88d88b5e7967215ef059b46be1681d121d4ec45c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Aug 2026 22:19:30 +0000 Subject: [PATCH 3/5] chore: update derived files --- .reports/embedded-react-sdk.api.md | 13 +++++++ docs/reference/Translations/index.md | 53 ++++++++++++++++++++++++++++ docs/reference/events.md | 7 ++++ 3 files changed, 73 insertions(+) diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md index b054f4ce2..6e8a174da 100644 --- a/.reports/embedded-react-sdk.api.md +++ b/.reports/embedded-react-sdk.api.md @@ -1293,6 +1293,13 @@ export const componentEvents: { readonly CONTRACTOR_PAYMENT_CANCEL: "contractor/payments/cancel"; readonly CONTRACTOR_PAYMENT_EXIT: "contractor/payments/exit"; readonly CONTRACTOR_PAYMENT_RFI_RESPOND: "contractor/payments/rfi/respond"; + readonly PRINT_CHECKS_START: "payroll/printChecks/start"; + readonly PRINT_CHECKS_GENERATE_START: "payroll/printChecks/generate/start"; + readonly PRINT_CHECKS_GENERATE_SUCCEEDED: "payroll/printChecks/generate/succeeded"; + readonly PRINT_CHECKS_GENERATE_FAILED: "payroll/printChecks/generate/failed"; + readonly PRINT_CHECKS_RETRY: "payroll/printChecks/retry"; + readonly PRINT_CHECKS_CANCEL: "payroll/printChecks/cancel"; + readonly PRINT_CHECKS_CLOSE: "payroll/printChecks/close"; readonly RECOVERY_CASE_RESOLVE: "recoveryCase/resolve"; readonly RECOVERY_CASE_RESUBMIT: "recoveryCase/resubmit"; readonly RECOVERY_CASE_RESUBMIT_CANCEL: "recoveryCase/resubmit/cancel"; @@ -4993,6 +5000,12 @@ export interface Resources { // (undocumented) 'Payroll.PayrollReceipts': Translations.PayrollPayrollReceipts // (undocumented) + 'Payroll.PrintChecksBanner': Translations.PayrollPrintChecksBanner + // (undocumented) + 'Payroll.PrintChecksFailure': Translations.PayrollPrintChecksFailure + // (undocumented) + 'Payroll.PrintChecksSummary': Translations.PayrollPrintChecksSummary + // (undocumented) 'Payroll.RecoveryCasesList': Translations.PayrollRecoveryCasesList // (undocumented) 'Payroll.RecoveryCasesResubmit': Translations.PayrollRecoveryCasesResubmit diff --git a/docs/reference/Translations/index.md b/docs/reference/Translations/index.md index 96e5d9944..d1cdb16b3 100644 --- a/docs/reference/Translations/index.md +++ b/docs/reference/Translations/index.md @@ -5234,6 +5234,56 @@ Translation keys for the `Payroll.PayrollReceipts` i18n namespace. *** + + +### PayrollPrintChecksBanner + +Translation keys for the `Payroll.PrintChecksBanner` i18n namespace. + +#### Properties + +| Property | Default value | +| ------ | ------ | +| `cta` | `"View and print checks"` | +| `description` | `"Employees with this payment method will need their checks delivered to them. If you aren't using your own checks, you can view and print checks."` | +| `title_one` | `"You noted {{count}} employee that should be paid by check."` | +| `title_other` | `"You noted {{count}} employees that should be paid by check."` | + +*** + + + +### PayrollPrintChecksFailure + +Translation keys for the `Payroll.PrintChecksFailure` i18n namespace. + +#### Properties + +| Property | Default value | +| ------ | ------ | +| `closeCta` | `"Close"` | +| `failedTitle` | `"We couldn't generate your checks"` | +| `retryCta` | `"Try again"` | + +*** + + + +### PayrollPrintChecksSummary + +Translation keys for the `Payroll.PrintChecksSummary` i18n namespace. + +#### Properties + +| Property | Default value | +| ------ | ------ | +| `closeCta` | `"Close"` | +| `succeededDescription` | `"The download should have started automatically. If not, use the link below."` | +| `succeededTitle` | `"Your checks are ready"` | +| `viewChecksCta` | `"View checks"` | + +*** + ### PayrollRecoveryCasesList @@ -5606,6 +5656,9 @@ yields that namespace's keys. Backs i18next `t()` typing and `ResourceDictionary | `Payroll.PayrollList` | [`PayrollPayrollList`](#payrollpayrolllist) | | `Payroll.PayrollOverview` | [`PayrollPayrollOverview`](#payrollpayrolloverview) | | `Payroll.PayrollReceipts` | [`PayrollPayrollReceipts`](#payrollpayrollreceipts) | +| `Payroll.PrintChecksBanner` | [`PayrollPrintChecksBanner`](#payrollprintchecksbanner) | +| `Payroll.PrintChecksFailure` | [`PayrollPrintChecksFailure`](#payrollprintchecksfailure) | +| `Payroll.PrintChecksSummary` | [`PayrollPrintChecksSummary`](#payrollprintcheckssummary) | | `Payroll.RecoveryCasesList` | [`PayrollRecoveryCasesList`](#payrollrecoverycaseslist) | | `Payroll.RecoveryCasesResubmit` | [`PayrollRecoveryCasesResubmit`](#payrollrecoverycasesresubmit) | | `Payroll.Transition` | [`PayrollTransition`](#payrolltransition) | diff --git a/docs/reference/events.md b/docs/reference/events.md index 78f4aa192..f2179f239 100644 --- a/docs/reference/events.md +++ b/docs/reference/events.md @@ -304,6 +304,13 @@ import { componentEvents, EmployeeOnboarding } from '@gusto/embedded-react-sdk' | `PAYROLL_WIRE_INSTRUCTIONS_DONE` | `"payroll/wire/instructions/done"` | | `PAYROLL_WIRE_INSTRUCTIONS_SELECT` | `"payroll/wire/instructions/select"` | | `PAYROLL_WIRE_START_TRANSFER` | `"payroll/wire/startTransfer"` | +| `PRINT_CHECKS_CANCEL` | `"payroll/printChecks/cancel"` | +| `PRINT_CHECKS_CLOSE` | `"payroll/printChecks/close"` | +| `PRINT_CHECKS_GENERATE_FAILED` | `"payroll/printChecks/generate/failed"` | +| `PRINT_CHECKS_GENERATE_START` | `"payroll/printChecks/generate/start"` | +| `PRINT_CHECKS_GENERATE_SUCCEEDED` | `"payroll/printChecks/generate/succeeded"` | +| `PRINT_CHECKS_RETRY` | `"payroll/printChecks/retry"` | +| `PRINT_CHECKS_START` | `"payroll/printChecks/start"` | | `RECOVERY_CASE_RESOLVE` | `"recoveryCase/resolve"` | | `RECOVERY_CASE_RESUBMIT` | `"recoveryCase/resubmit"` | | `RECOVERY_CASE_RESUBMIT_CANCEL` | `"recoveryCase/resubmit/cancel"` | From 61fb2a2abe93389d87e5cf4892be05b5fc4b598b Mon Sep 17 00:00:00 2001 From: Kristine White Date: Tue, 18 Aug 2026 15:20:21 -0700 Subject: [PATCH 4/5] feat(SDK-1161): add PrintChecksForm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third slice of the print-checks feature stack (see #2584). The core piece: the check-stock form, the generate-and-poll cycle against usePayrollsGeneratePrintableChecksMutation/useGeneratedDocumentsGet, and the checks-download mechanism. The download navigates directly to the generated document's signed URL via a synthetic anchor click rather than fetching it into a blob: the URL already has response-content-disposition=attachment baked into its query string, so a direct browser-level request downloads it via that response header. A client-side fetch() would be subject to CORS, which the document host doesn't allow, and fails outright. Defines a narrow PrintChecksFormFlowContext (just the isGenerating flag its Footer needs from the shared machine context) rather than depending on the orchestrator's full context type, which doesn't land until the next PR in the stack — keeps this PR buildable and testable on its own. Stacked on kw/feat/print-checks-2-banner; targets that branch, not main. Co-Authored-By: Claude Sonnet 5 --- .../PrintChecksForm.stories.tsx | 33 +++ .../PrintChecksForm/PrintChecksForm.test.tsx | 280 ++++++++++++++++++ .../PrintChecksForm/PrintChecksForm.tsx | 238 +++++++++++++++ .../PrintChecks/PrintChecksForm/index.ts | 1 + src/i18n/en/Payroll.PrintChecksForm.json | 15 + src/i18n/types.d.ts | 28 ++ .../mocks/apis/printable_payroll_checks.ts | 50 ++++ src/test/mocks/handlers.ts | 2 + 8 files changed, 647 insertions(+) create mode 100644 src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.stories.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.test.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.tsx create mode 100644 src/components/Payroll/PrintChecks/PrintChecksForm/index.ts create mode 100644 src/i18n/en/Payroll.PrintChecksForm.json create mode 100644 src/test/mocks/apis/printable_payroll_checks.ts diff --git a/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.stories.tsx b/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.stories.tsx new file mode 100644 index 000000000..d4fc3c831 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.stories.tsx @@ -0,0 +1,33 @@ +import { Suspense } from 'react' +import { fn } from 'storybook/test' +import { PrintChecksForm } from './PrintChecksForm' +import { GustoTestProvider } from '@/test/GustoTestApiProvider' +import { FlowContext } from '@/components/Flow/useFlow' +import { useI18n } from '@/i18n' + +function I18nLoader({ children }: { children: React.ReactNode }) { + useI18n('Payroll.PrintChecksForm') + return <>{children} +} + +export default { + title: 'Domain/Payroll/PrintChecksForm', + decorators: [ + (Story: React.ComponentType) => ( + Loading translations...}> + + + + + + + ), + ], +} + +export const Default = () => ( + + + + +) diff --git a/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.test.tsx b/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.test.tsx new file mode 100644 index 000000000..2fde39bdc --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.test.tsx @@ -0,0 +1,280 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { HttpResponse, type HttpResponseResolver } from 'msw' +import { PrintChecksForm } from './PrintChecksForm' +import { + handlePayrollsGeneratePrintableChecks, + handleGeneratedDocumentsGet, + createPayrollCheck, + createGeneratedDocument, +} from '@/test/mocks/apis/printable_payroll_checks' +import { server } from '@/test/mocks/server' +import { renderWithProviders } from '@/test-utils/renderWithProviders' +import { printChecksEvents } from '@/shared/constants' +import { FlowContext } from '@/components/Flow/useFlow' + +describe('PrintChecksForm', () => { + const onEvent = vi.fn() + const user = userEvent.setup() + let anchorClickSpy: ReturnType + + const flowContextValue = { + component: null, + onEvent, + } + + const renderForm = () => + renderWithProviders( + + + + , + ) + + beforeEach(() => { + vi.clearAllMocks() + // The download is triggered via a synthetic anchor click rather than a real navigation — + // jsdom throws "Not implemented: navigation" if this isn't stubbed. + anchorClickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + }) + + it('omits starting_check_number from the request body for custom check stock', async () => { + let capturedBody: Record | null = null + const generateResolver = vi.fn(async ({ request }) => { + capturedBody = (await request.json()) as Record + return HttpResponse.json(createPayrollCheck(), { status: 200 }) + }) + server.use(handlePayrollsGeneratePrintableChecks(generateResolver)) + server.use( + handleGeneratedDocumentsGet(() => + HttpResponse.json(createGeneratedDocument({ status: 'pending' })), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(generateResolver).toHaveBeenCalledTimes(1) + }) + expect(capturedBody).toEqual({ printing_format: 'top' }) + }) + + it('includes starting_check_number in the request body for blank check stock', async () => { + let capturedBody: Record | null = null + const generateResolver = vi.fn(async ({ request }) => { + capturedBody = (await request.json()) as Record + return HttpResponse.json(createPayrollCheck(), { status: 200 }) + }) + server.use(handlePayrollsGeneratePrintableChecks(generateResolver)) + server.use( + handleGeneratedDocumentsGet(() => + HttpResponse.json(createGeneratedDocument({ status: 'pending' })), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('radio', { name: 'Blank check stock' })) + const input = screen.getByLabelText(/Check number starts with/) + await user.clear(input) + await user.type(input, '1001') + await user.click(screen.getByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(generateResolver).toHaveBeenCalledTimes(1) + }) + expect(capturedBody).toEqual({ printing_format: 'bottom', starting_check_number: 1001 }) + }) + + it('includes a starting_check_number of 0 in the request body for blank check stock', async () => { + let capturedBody: Record | null = null + const generateResolver = vi.fn(async ({ request }) => { + capturedBody = (await request.json()) as Record + return HttpResponse.json(createPayrollCheck(), { status: 200 }) + }) + server.use(handlePayrollsGeneratePrintableChecks(generateResolver)) + server.use( + handleGeneratedDocumentsGet(() => + HttpResponse.json(createGeneratedDocument({ status: 'pending' })), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('radio', { name: 'Blank check stock' })) + await user.click(screen.getByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(generateResolver).toHaveBeenCalledTimes(1) + }) + expect(capturedBody).toEqual({ printing_format: 'bottom', starting_check_number: 0 }) + }) + + it('calls the generate endpoint before the poll endpoint', async () => { + const generateResolver = vi.fn(() => + HttpResponse.json(createPayrollCheck(), { status: 200 }), + ) + const getDocumentResolver = vi.fn(() => + HttpResponse.json(createGeneratedDocument({ status: 'succeeded' })), + ) + server.use(handlePayrollsGeneratePrintableChecks(generateResolver)) + server.use(handleGeneratedDocumentsGet(getDocumentResolver)) + + renderForm() + + await user.click(await screen.findByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(getDocumentResolver).toHaveBeenCalled() + }) + expect(generateResolver.mock.invocationCallOrder[0]!).toBeLessThan( + getDocumentResolver.mock.invocationCallOrder[0]!, + ) + }) + + it('fires PRINT_CHECKS_GENERATE_START on submit', async () => { + server.use( + handlePayrollsGeneratePrintableChecks(() => + HttpResponse.json(createPayrollCheck(), { status: 200 }), + ), + ) + server.use( + handleGeneratedDocumentsGet(() => + HttpResponse.json(createGeneratedDocument({ status: 'pending' })), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('button', { name: 'View checks' })) + + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_GENERATE_START) + }) + + it('fires PRINT_CHECKS_GENERATE_SUCCEEDED with the document URL once the poll succeeds', async () => { + server.use( + handlePayrollsGeneratePrintableChecks(() => + HttpResponse.json(createPayrollCheck(), { status: 200 }), + ), + ) + server.use( + handleGeneratedDocumentsGet(() => + HttpResponse.json( + createGeneratedDocument({ + status: 'succeeded', + document_urls: ['https://example.com/checks.pdf'], + }), + ), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_GENERATE_SUCCEEDED, { + documentUrl: 'https://example.com/checks.pdf', + }) + }) + await waitFor(() => { + expect(anchorClickSpy).toHaveBeenCalledTimes(1) + }) + expect(anchorClickSpy.mock.contexts[0]).toHaveProperty('href', 'https://example.com/checks.pdf') + }) + + it('does not open any window or navigate anywhere when generation succeeds', async () => { + const openSpy = vi.spyOn(window, 'open') + server.use( + handlePayrollsGeneratePrintableChecks(() => + HttpResponse.json(createPayrollCheck(), { status: 200 }), + ), + ) + server.use( + handleGeneratedDocumentsGet(() => + HttpResponse.json( + createGeneratedDocument({ + status: 'succeeded', + document_urls: ['https://example.com/checks.pdf'], + }), + ), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(anchorClickSpy).toHaveBeenCalledTimes(1) + }) + expect(openSpy).not.toHaveBeenCalled() + }) + + it('fires PRINT_CHECKS_GENERATE_FAILED when the poll reports failure', async () => { + server.use( + handlePayrollsGeneratePrintableChecks(() => + HttpResponse.json(createPayrollCheck(), { status: 200 }), + ), + ) + server.use( + handleGeneratedDocumentsGet(() => + HttpResponse.json(createGeneratedDocument({ status: 'failed', document_urls: [] })), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_GENERATE_FAILED, { + errorMessage: null, + }) + }) + }) + + it('fires PRINT_CHECKS_GENERATE_FAILED with the server error message when the mutation itself is rejected', async () => { + server.use( + handlePayrollsGeneratePrintableChecks(() => + HttpResponse.json( + { + errors: [ + { + error_key: 'invalid_action', + category: 'invalid_operation', + message: 'Cannot generate checks on an unprocessed payroll', + }, + ], + }, + { status: 422 }, + ), + ), + ) + + renderForm() + + await user.click(await screen.findByRole('button', { name: 'View checks' })) + + await waitFor(() => { + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_GENERATE_FAILED, { + errorMessage: 'Cannot generate checks on an unprocessed payroll', + }) + }) + expect(onEvent).not.toHaveBeenCalledWith( + printChecksEvents.PRINT_CHECKS_GENERATE_SUCCEEDED, + expect.anything(), + ) + }) + + it('fires PRINT_CHECKS_CANCEL when the Footer cancel button is clicked', async () => { + renderForm() + + await user.click(await screen.findByRole('button', { name: 'Cancel' })) + + expect(onEvent).toHaveBeenCalledWith(printChecksEvents.PRINT_CHECKS_CANCEL) + }) +}) diff --git a/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.tsx b/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.tsx new file mode 100644 index 000000000..e4b24eec0 --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksForm/PrintChecksForm.tsx @@ -0,0 +1,238 @@ +import { useEffect, useState } from 'react' +import { FormProvider, useForm, useWatch } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { useTranslation } from 'react-i18next' +import { z } from 'zod' +import { usePayrollsGeneratePrintableChecksMutation } from '@gusto/embedded-api/react-query/payrollsGeneratePrintableChecks' +import { useGeneratedDocumentsGet } from '@gusto/embedded-api/react-query/generatedDocumentsGet' +import { + PrintingFormat, + type PrintablePayrollChecksBody, +} from '@gusto/embedded-api/models/components/printablepayrollchecksbody' +import { DocumentType } from '@gusto/embedded-api/models/operations/getv1generateddocumentsdocumenttyperequestuuid' +import { GeneratedDocumentStatus } from '@gusto/embedded-api/models/components/generateddocument' +import { BaseComponent, useBase, type BaseComponentInterface } from '@/components/Base' +import type { OnEventType } from '@/components/Base/useBase' +import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { useComponentDictionary, useI18n } from '@/i18n' +import { useFlow, type FlowContextInterface } from '@/components/Flow/useFlow' +import { ActionsLayout, Flex, NumberInputField, RadioGroupField } from '@/components/Common' +import { Form } from '@/components/Common/Form' +import { printChecksEvents, type EventType } from '@/shared/constants' +import type { RadioGroupOption } from '@/index' + +interface PrintChecksFormProps extends BaseComponentInterface<'Payroll.PrintChecksForm'> { + payrollId: string + /** Whether a generate-and-poll cycle is in flight; disables the fields while true. */ + isGenerating?: boolean +} + +// The Footer is rendered by the top-level orchestrator as a sibling of this component (not a +// child), so it can only read shared state — like whether a generate-and-poll cycle is in +// flight — via the machine's FlowContext rather than as a prop. This narrow interface describes +// only the field Footer needs, rather than depending on the orchestrator's full context type. +interface PrintChecksFormFlowContext extends FlowContextInterface { + isGenerating?: boolean +} + +const PrintChecksFormSchema = z.object({ + printingFormat: z.nativeEnum(PrintingFormat), + startingCheckNumber: z + .number({ message: 'invalidStartingCheckNumber' }) + .int({ message: 'invalidStartingCheckNumber' }) + .nonnegative({ message: 'invalidStartingCheckNumber' }) + .lt(10_000_000_000, { message: 'invalidStartingCheckNumber' }) + .optional(), +}) + +type PrintChecksFormValues = z.infer + +const PRINT_CHECKS_FORM_ID = 'gusto-sdk-print-checks-form' + +const isErrorList = (val: unknown): val is { message?: string }[] => + Array.isArray(val) && val.every(entry => typeof entry === 'object' && entry !== null) + +const extractErrorMessage = (err: unknown): string | null => { + if (err && typeof err === 'object' && 'errors' in err && isErrorList(err.errors)) { + const [firstError] = err.errors + if (firstError?.message) return firstError.message + } + return err instanceof Error ? err.message : null +} + +const buildRequestBody = (data: PrintChecksFormValues): PrintablePayrollChecksBody => ({ + printingFormat: data.printingFormat, + ...(data.printingFormat === PrintingFormat.Bottom && data.startingCheckNumber !== undefined + ? { startingCheckNumber: data.startingCheckNumber } + : {}), +}) + +// Navigates directly to `url` via a synthetic anchor click rather than `fetch`-ing it into a blob: +// the generated-document URL is a signed, cross-origin S3 URL with `response-content-disposition: +// attachment` baked into its query string, so a direct browser-level request downloads it via that +// response header — but a `fetch()` from JS is subject to CORS, which the bucket doesn't allow, and +// fails outright. A plain navigation isn't subject to CORS and never opens a new tab or navigates +// the host page, since the browser intercepts the download instead of rendering a response. +const downloadGeneratedChecks = (url: string) => { + const link = document.createElement('a') + link.href = url + link.rel = 'noopener noreferrer' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} + +/** @internal */ +export function PrintChecksForm(props: PrintChecksFormProps) { + return ( + + {props.children} + + ) +} + +const Root = ({ dictionary, payrollId, isGenerating }: PrintChecksFormProps) => { + useComponentDictionary('Payroll.PrintChecksForm', dictionary) + useI18n('Payroll.PrintChecksForm') + const { t } = useTranslation('Payroll.PrintChecksForm') + const { onEvent, baseSubmitHandler } = useBase() + const [requestUuid, setRequestUuid] = useState(null) + const [isPolling, setIsPolling] = useState(false) + + const formMethods = useForm({ + resolver: zodResolver(PrintChecksFormSchema), + defaultValues: { printingFormat: PrintingFormat.Top, startingCheckNumber: 0 }, + }) + + const printingFormat = useWatch({ + name: 'printingFormat', + control: formMethods.control, + }) + + const { mutateAsync } = usePayrollsGeneratePrintableChecksMutation() + + const { data } = useGeneratedDocumentsGet( + { + documentType: DocumentType.PrintablePayrollChecks, + requestUuid: requestUuid || '', + }, + { + enabled: !!requestUuid, + refetchInterval: isPolling ? 5_000 : false, + }, + ) + + useEffect(() => { + const status = data?.generatedDocument?.status + if (!isPolling || !status) return + + if (status === GeneratedDocumentStatus.Succeeded) { + setIsPolling(false) + const url = data.generatedDocument?.documentUrls?.[0] ?? null + onEvent(printChecksEvents.PRINT_CHECKS_GENERATE_SUCCEEDED, { documentUrl: url }) + if (url) { + downloadGeneratedChecks(url) + } + } else if (status === GeneratedDocumentStatus.Failed) { + setIsPolling(false) + onEvent(printChecksEvents.PRINT_CHECKS_GENERATE_FAILED, { errorMessage: null }) + } + }, [data, isPolling, onEvent]) + + const onSubmit = async (formData: PrintChecksFormValues) => { + onEvent(printChecksEvents.PRINT_CHECKS_GENERATE_START) + + await baseSubmitHandler(formData, async submittedData => { + try { + const result = await mutateAsync({ + request: { + payrollUuid: payrollId, + printablePayrollChecksBody: buildRequestBody(submittedData), + }, + }) + + const nextRequestUuid = result.payrollCheck?.requestUuid + if (!nextRequestUuid) { + throw new Error('Missing requestUuid in generate-printable-checks response') + } + + setRequestUuid(nextRequestUuid) + setIsPolling(true) + } catch (err) { + onEvent(printChecksEvents.PRINT_CHECKS_GENERATE_FAILED, { + errorMessage: extractErrorMessage(err), + }) + throw err + } + }) + } + + const startingCheckNumberErrorCode = formMethods.formState.errors.startingCheckNumber?.message + const startingCheckNumberErrorMessage = startingCheckNumberErrorCode + ? t('validations.startingCheckNumber') + : undefined + + const printingFormatOptions: RadioGroupOption[] = [ + { + value: PrintingFormat.Top, + label: t('customStockLabel'), + description: t('customStockDescription'), + }, + { + value: PrintingFormat.Bottom, + label: t('blankStockLabel'), + description: t('blankStockDescription'), + }, + ] + + return ( + +
+ + + {printingFormat === PrintingFormat.Bottom && ( + + )} + +
+
+ ) +} + +const Footer = ({ onEvent }: { onEvent: OnEventType }) => { + useI18n('Payroll.PrintChecksForm') + const { t } = useTranslation('Payroll.PrintChecksForm') + const { Button } = useComponentContext() + const { isGenerating } = useFlow() + + return ( + + + + + ) +} +PrintChecksForm.Footer = Footer diff --git a/src/components/Payroll/PrintChecks/PrintChecksForm/index.ts b/src/components/Payroll/PrintChecks/PrintChecksForm/index.ts new file mode 100644 index 000000000..8cc0912dd --- /dev/null +++ b/src/components/Payroll/PrintChecks/PrintChecksForm/index.ts @@ -0,0 +1 @@ +export { PrintChecksForm } from './PrintChecksForm' diff --git a/src/i18n/en/Payroll.PrintChecksForm.json b/src/i18n/en/Payroll.PrintChecksForm.json new file mode 100644 index 000000000..b894ba348 --- /dev/null +++ b/src/i18n/en/Payroll.PrintChecksForm.json @@ -0,0 +1,15 @@ +{ + "modalTitle": "Choose check stock", + "customStockLabel": "Custom check stock", + "customStockDescription": "Use this check stock if you have check stock that is pre-printed with your company and bank information. The physical check will appear on the top of the check PDF. Check numbers should already be pre-printed on the check stock you purchased.", + "blankStockLabel": "Blank check stock", + "blankStockDescription": "Use this check stock if you have blank check stock and need us to populate your company and bank information. The physical check will always be on the bottom of the check PDF.", + "startingCheckNumberLabel": "Check number starts with", + "startingCheckNumberDescription": "This will be the first check number, all other checks will follow sequentially.", + "cancelCta": "Cancel", + "submitCta": "View checks", + "submitCtaLoading": "Generating...", + "validations": { + "startingCheckNumber": "Enter a valid check number" + } +} diff --git a/src/i18n/types.d.ts b/src/i18n/types.d.ts index cfc273e6e..5b60db09e 100644 --- a/src/i18n/types.d.ts +++ b/src/i18n/types.d.ts @@ -129,6 +129,7 @@ export interface Resources { 'Payroll.PayrollReceipts': Translations.PayrollPayrollReceipts 'Payroll.PrintChecksBanner': Translations.PayrollPrintChecksBanner 'Payroll.PrintChecksFailure': Translations.PayrollPrintChecksFailure + 'Payroll.PrintChecksForm': Translations.PayrollPrintChecksForm 'Payroll.PrintChecksSummary': Translations.PayrollPrintChecksSummary 'Payroll.RecoveryCasesList': Translations.PayrollRecoveryCasesList 'Payroll.RecoveryCasesResubmit': Translations.PayrollRecoveryCasesResubmit @@ -7688,6 +7689,33 @@ export namespace Translations { /** @defaultValue `"Close"` */ closeCta: string } + /** Translation keys for the `Payroll.PrintChecksForm` i18n namespace. */ + export interface PayrollPrintChecksForm { + /** @defaultValue `"Choose check stock"` */ + modalTitle: string + /** @defaultValue `"Custom check stock"` */ + customStockLabel: string + /** @defaultValue `"Use this check stock if you have check stock that is pre-printed with your company and bank information. The physical check will appear on the top of the check PDF. Check numbers should already be pre-printed on the check stock you purchased."` */ + customStockDescription: string + /** @defaultValue `"Blank check stock"` */ + blankStockLabel: string + /** @defaultValue `"Use this check stock if you have blank check stock and need us to populate your company and bank information. The physical check will always be on the bottom of the check PDF."` */ + blankStockDescription: string + /** @defaultValue `"Check number starts with"` */ + startingCheckNumberLabel: string + /** @defaultValue `"This will be the first check number, all other checks will follow sequentially."` */ + startingCheckNumberDescription: string + /** @defaultValue `"Cancel"` */ + cancelCta: string + /** @defaultValue `"View checks"` */ + submitCta: string + /** @defaultValue `"Generating..."` */ + submitCtaLoading: string + validations: { + /** @defaultValue `"Enter a valid check number"` */ + startingCheckNumber: string + } + } /** Translation keys for the `Payroll.PrintChecksSummary` i18n namespace. */ export interface PayrollPrintChecksSummary { /** @defaultValue `"Your checks are ready"` */ diff --git a/src/test/mocks/apis/printable_payroll_checks.ts b/src/test/mocks/apis/printable_payroll_checks.ts new file mode 100644 index 000000000..cf3ae3b05 --- /dev/null +++ b/src/test/mocks/apis/printable_payroll_checks.ts @@ -0,0 +1,50 @@ +import type { HttpResponseResolver, PathParams } from 'msw' +import { http, HttpResponse } from 'msw' +import type { PostV1PayrollsPayrollUuidGeneratedDocumentsPrintablePayrollChecksRequest } from '@gusto/embedded-api/models/operations/postv1payrollspayrolluuidgenerateddocumentsprintablepayrollchecks' +import type { GetV1GeneratedDocumentsDocumentTypeRequestUuidRequest } from '@gusto/embedded-api/models/operations/getv1generateddocumentsdocumenttyperequestuuid' +import { API_BASE_URL } from '@/test/constants' + +export const createPayrollCheck = (overrides: Record = {}) => ({ + payroll_uuid: 'payroll-1', + printing_format: 'top', + starting_check_number: null, + request_uuid: 'print-checks-request-1', + status: 'pending', + employee_check_number_mapping: [{ employee_uuid: 'employee-1', check_number: 1001 }], + ...overrides, +}) + +export const createGeneratedDocument = (overrides: Record = {}) => ({ + request_uuid: 'print-checks-request-1', + status: 'succeeded', + document_urls: ['https://example.com/checks.pdf'], + ...overrides, +}) + +export function handlePayrollsGeneratePrintableChecks( + resolver: HttpResponseResolver< + PathParams, + PostV1PayrollsPayrollUuidGeneratedDocumentsPrintablePayrollChecksRequest + >, +) { + return http.post( + `${API_BASE_URL}/v1/payrolls/:payroll_uuid/generated_documents/printable_payroll_checks`, + resolver, + ) +} + +export function handleGeneratedDocumentsGet( + resolver: HttpResponseResolver, +) { + return http.get(`${API_BASE_URL}/v1/generated_documents/:document_type/:request_uuid`, resolver) +} + +export const generatePrintableChecks = handlePayrollsGeneratePrintableChecks(() => + HttpResponse.json(createPayrollCheck(), { status: 200 }), +) + +export const getGeneratedDocument = handleGeneratedDocumentsGet(() => + HttpResponse.json(createGeneratedDocument()), +) + +export default [generatePrintableChecks, getGeneratedDocument] diff --git a/src/test/mocks/handlers.ts b/src/test/mocks/handlers.ts index b61b3c6fc..47351757d 100644 --- a/src/test/mocks/handlers.ts +++ b/src/test/mocks/handlers.ts @@ -23,6 +23,7 @@ import ContractorHandlers from './apis/contractors' import ContractorDocumentsHandlers from './apis/contractor_documents' import ContractorPaymentGroupsHandlers from './apis/contractor_payment_groups' import WireInRequestsHandlers from './apis/wire_in_requests' +import PrintablePayrollChecksHandlers from './apis/printable_payroll_checks' import InformationRequestsHandlers from './apis/information_requests' import I9AuthorizationHandlers from './apis/i9_authorization' import EmployeeFormHandlers from './apis/employee_forms' @@ -74,6 +75,7 @@ export const handlers = [ ...ContractorDocumentsHandlers, ...ContractorPaymentGroupsHandlers, ...WireInRequestsHandlers, + ...PrintablePayrollChecksHandlers, ...InformationRequestsHandlers, ...I9AuthorizationHandlers, ...EmployeeFormHandlers, From 1b591c68ea283f759d52edd3d7c8803cb8078a04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Aug 2026 22:24:53 +0000 Subject: [PATCH 5/5] chore: update derived files --- .reports/embedded-react-sdk.api.md | 21 +++++++ docs/reference/APIModels/index.md | 83 ++++++++++++++++++++++++++++ docs/reference/Translations/index.md | 79 ++++++++++++++++++++++++++ docs/reference/events.md | 7 +++ src/models/external.ts | 3 + 5 files changed, 193 insertions(+) diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md index b054f4ce2..3dd091c92 100644 --- a/.reports/embedded-react-sdk.api.md +++ b/.reports/embedded-react-sdk.api.md @@ -129,6 +129,7 @@ import { FunctionComponent } from 'react'; import { Garnishment } from '@gusto/embedded-api/models/components/garnishment'; import { GarnishmentChildSupport } from '@gusto/embedded-api/models/components/garnishmentchildsupport'; import { GarnishmentType } from '@gusto/embedded-api/models/components/garnishment'; +import { GeneratedDocumentStatus } from '@gusto/embedded-api/models/components/generateddocument'; import { HolidayPayPolicy } from '@gusto/embedded-api/models/components/holidaypaypolicy'; import { HolidayPayPolicyEmployees } from '@gusto/embedded-api/models/components/holidaypaypolicy'; import { HourlyCompensations } from '@gusto/embedded-api/models/components/payrollemployeecompensationstype'; @@ -247,6 +248,8 @@ import { PayScheduleShow } from '@gusto/embedded-api/models/components/payschedu import { PlaidStatus } from '@gusto/embedded-api/models/components/companybankaccount'; import { PolicyType } from '@gusto/embedded-api/models/components/timeoffpolicy'; import { PresidentsDay } from '@gusto/embedded-api/models/components/holidaypaypolicy'; +import { PrintablePayrollChecksBody } from '@gusto/embedded-api/models/components/printablepayrollchecksbody'; +import { PrintingFormat } from '@gusto/embedded-api/models/components/printablepayrollchecksbody'; import { QueryClient } from '@tanstack/react-query'; import { Questions } from '@gusto/embedded-api/models/components/employeestatetaxesrequest'; import { RateType } from '@gusto/embedded-api/models/components/taxrequirementmetadata'; @@ -537,6 +540,7 @@ declare namespace APIModels { Garnishment, GarnishmentChildSupport, PaymentPeriod, + GeneratedDocumentStatus, ChristmasDay, ColumbusDay, HolidayPayPolicyEmployees, @@ -656,6 +660,8 @@ declare namespace APIModels { PayScheduleFrequency_2 as PayScheduleFrequency, PaySchedulePreviewPayPeriod, PayScheduleShow, + PrintablePayrollChecksBody, + PrintingFormat, RecoveryCase, RecoveryCaseStatus, IdentityVerificationStatus, @@ -1293,6 +1299,13 @@ export const componentEvents: { readonly CONTRACTOR_PAYMENT_CANCEL: "contractor/payments/cancel"; readonly CONTRACTOR_PAYMENT_EXIT: "contractor/payments/exit"; readonly CONTRACTOR_PAYMENT_RFI_RESPOND: "contractor/payments/rfi/respond"; + readonly PRINT_CHECKS_START: "payroll/printChecks/start"; + readonly PRINT_CHECKS_GENERATE_START: "payroll/printChecks/generate/start"; + readonly PRINT_CHECKS_GENERATE_SUCCEEDED: "payroll/printChecks/generate/succeeded"; + readonly PRINT_CHECKS_GENERATE_FAILED: "payroll/printChecks/generate/failed"; + readonly PRINT_CHECKS_RETRY: "payroll/printChecks/retry"; + readonly PRINT_CHECKS_CANCEL: "payroll/printChecks/cancel"; + readonly PRINT_CHECKS_CLOSE: "payroll/printChecks/close"; readonly RECOVERY_CASE_RESOLVE: "recoveryCase/resolve"; readonly RECOVERY_CASE_RESUBMIT: "recoveryCase/resubmit"; readonly RECOVERY_CASE_RESUBMIT_CANCEL: "recoveryCase/resubmit/cancel"; @@ -4993,6 +5006,14 @@ export interface Resources { // (undocumented) 'Payroll.PayrollReceipts': Translations.PayrollPayrollReceipts // (undocumented) + 'Payroll.PrintChecksBanner': Translations.PayrollPrintChecksBanner + // (undocumented) + 'Payroll.PrintChecksFailure': Translations.PayrollPrintChecksFailure + // (undocumented) + 'Payroll.PrintChecksForm': Translations.PayrollPrintChecksForm + // (undocumented) + 'Payroll.PrintChecksSummary': Translations.PayrollPrintChecksSummary + // (undocumented) 'Payroll.RecoveryCasesList': Translations.PayrollRecoveryCasesList // (undocumented) 'Payroll.RecoveryCasesResubmit': Translations.PayrollRecoveryCasesResubmit diff --git a/docs/reference/APIModels/index.md b/docs/reference/APIModels/index.md index fcead82f1..eba04f4f4 100644 --- a/docs/reference/APIModels/index.md +++ b/docs/reference/APIModels/index.md @@ -3036,6 +3036,39 @@ Defined in: [gusto\_embedded\_v\_2026\_06\_15/src/models/components/garnishment. *** + + +## GeneratedDocumentStatus + +> `const` **GeneratedDocumentStatus**: `object` + +Defined in: [gusto\_embedded\_v\_2026\_06\_15/src/models/components/generateddocument.ts:15](https://github.com/Gusto/gusto-typescript-client/blob/gusto_embedded_v_2026_06_15/v0.1.0/gusto_embedded_v_2026_06_15/src/models/components/generateddocument.ts#L15) + +Current status of the Generated Document + +### Type Declaration + +| Name | Type | +| ------ | ------ | +| `Failed` | `"failed"` | +| `Pending` | `"pending"` | +| `Started` | `"started"` | +| `Succeeded` | `"succeeded"` | + +*** + + + +## GeneratedDocumentStatus + +> **GeneratedDocumentStatus** = `ClosedEnum`\<*typeof* [`GeneratedDocumentStatus`](#generateddocumentstatus)\> + +Defined in: [gusto\_embedded\_v\_2026\_06\_15/src/models/components/generateddocument.ts:15](https://github.com/Gusto/gusto-typescript-client/blob/gusto_embedded_v_2026_06_15/v0.1.0/gusto_embedded_v_2026_06_15/src/models/components/generateddocument.ts#L15) + +Current status of the Generated Document + +*** + ## HolidayPayPolicy @@ -6028,6 +6061,56 @@ Defined in: [gusto\_embedded\_v\_2026\_06\_15/src/models/components/holidaypaypo *** + + +## PrintablePayrollChecksBody + +> **PrintablePayrollChecksBody** = `object` + +Defined in: [gusto\_embedded\_v\_2026\_06\_15/src/models/components/printablepayrollchecksbody.ts:24](https://github.com/Gusto/gusto-typescript-client/blob/gusto_embedded_v_2026_06_15/v0.1.0/gusto_embedded_v_2026_06_15/src/models/components/printablepayrollchecksbody.ts#L24) + +Request body for generating printable payroll checks. + +### Properties + +| Property | Type | Description | +| ------ | ------ | ------ | +| `printingFormat` | [`PrintingFormat`](#printingformat-1) | The type of check stock being printed. Check the "Types of check stock" section in this [link](https://support.gusto.com/article/999877761000000/Pay-your-team-by-check) for more info on check types | +| `startingCheckNumber?` | `number` | The starting check number we will start generating checks from. Use to override the sequence that will be used to generate check numbers. | + +*** + + + +## PrintingFormat + +> `const` **PrintingFormat**: `object` + +Defined in: [gusto\_embedded\_v\_2026\_06\_15/src/models/components/printablepayrollchecksbody.ts:12](https://github.com/Gusto/gusto-typescript-client/blob/gusto_embedded_v_2026_06_15/v0.1.0/gusto_embedded_v_2026_06_15/src/models/components/printablepayrollchecksbody.ts#L12) + +The type of check stock being printed. Check the "Types of check stock" section in this [link](https://support.gusto.com/article/999877761000000/Pay-your-team-by-check) for more info on check types + +### Type Declaration + +| Name | Type | +| ------ | ------ | +| `Bottom` | `"bottom"` | +| `Top` | `"top"` | + +*** + + + +## PrintingFormat + +> **PrintingFormat** = `ClosedEnum`\<*typeof* [`PrintingFormat`](#printingformat)\> + +Defined in: [gusto\_embedded\_v\_2026\_06\_15/src/models/components/printablepayrollchecksbody.ts:12](https://github.com/Gusto/gusto-typescript-client/blob/gusto_embedded_v_2026_06_15/v0.1.0/gusto_embedded_v_2026_06_15/src/models/components/printablepayrollchecksbody.ts#L12) + +The type of check stock being printed. Check the "Types of check stock" section in this [link](https://support.gusto.com/article/999877761000000/Pay-your-team-by-check) for more info on check types + +*** + ## Questions diff --git a/docs/reference/Translations/index.md b/docs/reference/Translations/index.md index 96e5d9944..8d3f7740b 100644 --- a/docs/reference/Translations/index.md +++ b/docs/reference/Translations/index.md @@ -5234,6 +5234,81 @@ Translation keys for the `Payroll.PayrollReceipts` i18n namespace. *** + + +### PayrollPrintChecksBanner + +Translation keys for the `Payroll.PrintChecksBanner` i18n namespace. + +#### Properties + +| Property | Default value | +| ------ | ------ | +| `cta` | `"View and print checks"` | +| `description` | `"Employees with this payment method will need their checks delivered to them. If you aren't using your own checks, you can view and print checks."` | +| `title_one` | `"You noted {{count}} employee that should be paid by check."` | +| `title_other` | `"You noted {{count}} employees that should be paid by check."` | + +*** + + + +### PayrollPrintChecksFailure + +Translation keys for the `Payroll.PrintChecksFailure` i18n namespace. + +#### Properties + +| Property | Default value | +| ------ | ------ | +| `closeCta` | `"Close"` | +| `failedTitle` | `"We couldn't generate your checks"` | +| `retryCta` | `"Try again"` | + +*** + + + +### PayrollPrintChecksForm + +Translation keys for the `Payroll.PrintChecksForm` i18n namespace. + +#### Properties + +| Property | Default value | +| ------ | ------ | +| `blankStockDescription` | `"Use this check stock if you have blank check stock and need us to populate your company and bank information. The physical check will always be on the bottom of the check PDF."` | +| `blankStockLabel` | `"Blank check stock"` | +| `cancelCta` | `"Cancel"` | +| `customStockDescription` | `"Use this check stock if you have check stock that is pre-printed with your company and bank information. The physical check will appear on the top of the check PDF. Check numbers should already be pre-printed on the check stock you purchased."` | +| `customStockLabel` | `"Custom check stock"` | +| `modalTitle` | `"Choose check stock"` | +| `startingCheckNumberDescription` | `"This will be the first check number, all other checks will follow sequentially."` | +| `startingCheckNumberLabel` | `"Check number starts with"` | +| `submitCta` | `"View checks"` | +| `submitCtaLoading` | `"Generating..."` | +| `validations` | | +| `validations.startingCheckNumber` | `"Enter a valid check number"` | + +*** + + + +### PayrollPrintChecksSummary + +Translation keys for the `Payroll.PrintChecksSummary` i18n namespace. + +#### Properties + +| Property | Default value | +| ------ | ------ | +| `closeCta` | `"Close"` | +| `succeededDescription` | `"The download should have started automatically. If not, use the link below."` | +| `succeededTitle` | `"Your checks are ready"` | +| `viewChecksCta` | `"View checks"` | + +*** + ### PayrollRecoveryCasesList @@ -5606,6 +5681,10 @@ yields that namespace's keys. Backs i18next `t()` typing and `ResourceDictionary | `Payroll.PayrollList` | [`PayrollPayrollList`](#payrollpayrolllist) | | `Payroll.PayrollOverview` | [`PayrollPayrollOverview`](#payrollpayrolloverview) | | `Payroll.PayrollReceipts` | [`PayrollPayrollReceipts`](#payrollpayrollreceipts) | +| `Payroll.PrintChecksBanner` | [`PayrollPrintChecksBanner`](#payrollprintchecksbanner) | +| `Payroll.PrintChecksFailure` | [`PayrollPrintChecksFailure`](#payrollprintchecksfailure) | +| `Payroll.PrintChecksForm` | [`PayrollPrintChecksForm`](#payrollprintchecksform) | +| `Payroll.PrintChecksSummary` | [`PayrollPrintChecksSummary`](#payrollprintcheckssummary) | | `Payroll.RecoveryCasesList` | [`PayrollRecoveryCasesList`](#payrollrecoverycaseslist) | | `Payroll.RecoveryCasesResubmit` | [`PayrollRecoveryCasesResubmit`](#payrollrecoverycasesresubmit) | | `Payroll.Transition` | [`PayrollTransition`](#payrolltransition) | diff --git a/docs/reference/events.md b/docs/reference/events.md index 78f4aa192..f2179f239 100644 --- a/docs/reference/events.md +++ b/docs/reference/events.md @@ -304,6 +304,13 @@ import { componentEvents, EmployeeOnboarding } from '@gusto/embedded-react-sdk' | `PAYROLL_WIRE_INSTRUCTIONS_DONE` | `"payroll/wire/instructions/done"` | | `PAYROLL_WIRE_INSTRUCTIONS_SELECT` | `"payroll/wire/instructions/select"` | | `PAYROLL_WIRE_START_TRANSFER` | `"payroll/wire/startTransfer"` | +| `PRINT_CHECKS_CANCEL` | `"payroll/printChecks/cancel"` | +| `PRINT_CHECKS_CLOSE` | `"payroll/printChecks/close"` | +| `PRINT_CHECKS_GENERATE_FAILED` | `"payroll/printChecks/generate/failed"` | +| `PRINT_CHECKS_GENERATE_START` | `"payroll/printChecks/generate/start"` | +| `PRINT_CHECKS_GENERATE_SUCCEEDED` | `"payroll/printChecks/generate/succeeded"` | +| `PRINT_CHECKS_RETRY` | `"payroll/printChecks/retry"` | +| `PRINT_CHECKS_START` | `"payroll/printChecks/start"` | | `RECOVERY_CASE_RESOLVE` | `"recoveryCase/resolve"` | | `RECOVERY_CASE_RESUBMIT` | `"recoveryCase/resubmit"` | | `RECOVERY_CASE_RESUBMIT_CANCEL` | `"recoveryCase/resubmit/cancel"` | diff --git a/src/models/external.ts b/src/models/external.ts index 58a5ac872..65dca838a 100644 --- a/src/models/external.ts +++ b/src/models/external.ts @@ -189,6 +189,7 @@ export { GarnishmentType } from '@gusto/embedded-api/models/components/garnishme export type { Garnishment } from '@gusto/embedded-api/models/components/garnishment' export type { GarnishmentChildSupport } from '@gusto/embedded-api/models/components/garnishmentchildsupport' export { PaymentPeriod } from '@gusto/embedded-api/models/components/garnishmentchildsupport' +export { GeneratedDocumentStatus } from '@gusto/embedded-api/models/components/generateddocument' /** `ChristmasDay` entity from the Gusto Embedded API. */ export type { ChristmasDay } from '@gusto/embedded-api/models/components/holidaypaypolicy' /** `ColumbusDay` entity from the Gusto Embedded API. */ @@ -383,6 +384,8 @@ export type { export { PayScheduleFrequency } from '@gusto/embedded-api/models/components/payschedulefrequency' export type { PaySchedulePreviewPayPeriod } from '@gusto/embedded-api/models/components/payschedulepreviewpayperiod' export type { PayScheduleShow } from '@gusto/embedded-api/models/components/payscheduleshow' +export type { PrintablePayrollChecksBody } from '@gusto/embedded-api/models/components/printablepayrollchecksbody' +export { PrintingFormat } from '@gusto/embedded-api/models/components/printablepayrollchecksbody' export type { RecoveryCase } from '@gusto/embedded-api/models/components/recoverycase' export { RecoveryCaseStatus } from '@gusto/embedded-api/models/components/recoverycase' /** `IdentityVerificationStatus` entity from the Gusto Embedded API. */