diff --git a/.reports/embedded-react-sdk.api.md b/.reports/embedded-react-sdk.api.md
index ec9b8bde2..701981631 100644
--- a/.reports/embedded-react-sdk.api.md
+++ b/.reports/embedded-react-sdk.api.md
@@ -4376,6 +4376,7 @@ interface PayrollOverviewProps extends BaseComponentInterface<'Payroll.PayrollOv
companyId: string;
ConfirmWireDetailsComponent?: ConfirmWireDetailsComponentType;
payrollId: string;
+ readOnly?: boolean;
withReimbursements?: boolean;
}
diff --git a/docs/reference/payroll/blocks.md b/docs/reference/payroll/blocks.md
index 7837a46ad..7075c17f7 100644
--- a/docs/reference/payroll/blocks.md
+++ b/docs/reference/payroll/blocks.md
@@ -637,7 +637,8 @@ The payroll referenced by `payrollId` must already be calculated; rendering with
uncalculated payroll throws. Unresolved submission blockers (e.g. fast-ACH threshold,
wire-in funding) are surfaced inline and the submit action stays disabled until each
blocker has a selected unblock option. While the payroll is processing, the component
-polls until success or failure and emits the corresponding event.
+polls until success or failure and emits the corresponding event. Pass `readOnly` to hide
+the edit and cancel actions while keeping submit available.
@@ -655,6 +656,7 @@ Props for [PayrollOverview](#payrolloverview).
| `alerts?` | [`PayrollFlowAlert`](#payrollflowalert)[] | Alert banners to display above the payroll summary. |
| `ConfirmWireDetailsComponent?` | [`ConfirmWireDetailsComponentType`](#confirmwiredetailscomponenttype) | Custom component to replace the default wire details confirmation UI. |
| `dictionary?` | `Record`\<`"en"`, [`DeepPartial`](../Translations/index.md#deeppartial)\<[`PayrollPayrollOverview`](../Translations/index.md#payrollpayrolloverview)\>\> | Overrides for the component's i18n strings. Supply a partial object whose keys match the component's resource namespace — any omitted keys fall back to SDK defaults. See the [Translation guide](https://docs.gusto.com/embedded-payroll/docs/translation) for details. |
+| `readOnly?` | `boolean` | Hides the edit and cancel actions, leaving submit and receipt/paystub actions available. Use for a deep link to a specific payroll where editing shouldn't be offered. Defaults to `false`. |
| `withReimbursements?` | `boolean` | Whether reimbursement fields are shown in the totals and per-employee tables. Defaults to `true`. |
_Inherits `children`, `className`, `defaultValues`, `FallbackComponent`, `LoaderComponent` from [BaseComponentInterface](../blocks.md#basecomponentinterface)._
diff --git a/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx b/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx
index 6391e6eea..b834e4d77 100644
--- a/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx
+++ b/src/components/Payroll/PayrollOverview/PayrollOverview.stories.tsx
@@ -1109,6 +1109,100 @@ export const PayrollOverviewStory = () => {
)
}
+export const PayrollOverviewReadOnly = () => {
+ return (
+
+ )
+}
+
export const PayrollOverviewWithWireFunds = () => {
return (
({
payrollsGetPayStub: vi.fn(),
}))
+vi.mock('../helpers', async importOriginal => {
+ const actual = await importOriginal()
+ return {
+ ...(actual as Record),
+ canCancelPayroll: vi.fn(),
+ }
+})
+
describe('PayrollOverview polling', () => {
const mockOnEvent = vi.fn()
@@ -369,3 +378,62 @@ describe('PayrollOverview print checks modal', () => {
expect(await screen.findByText('Choose check stock')).toBeInTheDocument()
})
})
+
+describe('PayrollOverview readOnly mode', () => {
+ const mockOnEvent = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockPayrollData = { ...basePayrollData }
+ mockIsFetching = false
+ vi.mocked(canCancelPayroll).mockReturnValue(false)
+ })
+
+ it('hides Edit but keeps Submit enabled and functional on an unprocessed payroll', async () => {
+ const user = userEvent.setup()
+ mockSubmitPayroll.mockResolvedValue({ payrollUuid: 'payroll-uuid' })
+
+ renderWithProviders(
+ ,
+ )
+
+ expect(await screen.findByRole('button', { name: 'Submit' })).toBeEnabled()
+ expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull()
+
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ await waitFor(() => {
+ expect(mockSubmitPayroll).toHaveBeenCalled()
+ expect(mockOnEvent).toHaveBeenCalledWith(
+ componentEvents.RUN_PAYROLL_SUBMITTED,
+ expect.anything(),
+ )
+ })
+ })
+
+ it('hides Cancel on a processed payroll even when the payroll is otherwise cancellable', async () => {
+ vi.mocked(canCancelPayroll).mockReturnValue(true)
+ mockPayrollData = {
+ ...basePayrollData,
+ processed: true,
+ processingRequest: { status: 'submit_success', errors: [] },
+ }
+
+ renderWithProviders(
+ ,
+ )
+
+ expect(await screen.findByRole('button', { name: 'View payroll receipt' })).toBeInTheDocument()
+ expect(screen.queryByRole('button', { name: 'Cancel payroll' })).toBeNull()
+ })
+})
diff --git a/src/components/Payroll/PayrollOverview/PayrollOverview.tsx b/src/components/Payroll/PayrollOverview/PayrollOverview.tsx
index ae25619f1..8737f9520 100644
--- a/src/components/Payroll/PayrollOverview/PayrollOverview.tsx
+++ b/src/components/Payroll/PayrollOverview/PayrollOverview.tsx
@@ -57,6 +57,12 @@ export interface PayrollOverviewProps extends BaseComponentInterface<'Payroll.Pa
withReimbursements?: boolean
/** Custom component to replace the default wire details confirmation UI. */
ConfirmWireDetailsComponent?: ConfirmWireDetailsComponentType
+ /**
+ * Hides the edit and cancel actions, leaving submit and receipt/paystub actions available.
+ * Use for a deep link to a specific payroll where editing shouldn't be offered. Defaults to
+ * `false`.
+ */
+ readOnly?: boolean
}
const findUnresolvedBlockersWithOptions = (
@@ -96,7 +102,8 @@ const findWireInRequestUuid = (
* uncalculated payroll throws. Unresolved submission blockers (e.g. fast-ACH threshold,
* wire-in funding) are surfaced inline and the submit action stays disabled until each
* blocker has a selected unblock option. While the payroll is processing, the component
- * polls until success or failure and emits the corresponding event.
+ * polls until success or failure and emits the corresponding event. Pass `readOnly` to hide
+ * the edit and cancel actions while keeping submit available.
*
* @events
* | Event | Description | Data |
@@ -138,6 +145,7 @@ const Root = ({
alerts,
withReimbursements = true,
ConfirmWireDetailsComponent = ConfirmWireDetails,
+ readOnly = false,
}: PayrollOverviewProps) => {
useComponentDictionary('Payroll.PayrollOverview', dictionary)
useI18n('Payroll.PayrollOverview')
@@ -283,9 +291,11 @@ const Root = ({
content: (
-
+ {!readOnly && (
+
+ )}
),
},
@@ -305,6 +315,7 @@ const Root = ({
payrollData?.totals?.companyDebit,
payrollData?.payrollStatusMeta?.expectedDebitTime,
payrollData?.payrollDeadline,
+ readOnly,
])
const { data: bankAccountData } = useBankAccountsGetSuspense({
@@ -460,7 +471,8 @@ const Root = ({
payrollData.processed === true ||
payrollData.processingRequest?.status === PAYROLL_PROCESSING_STATUS.submit_success
}
- canCancel={canCancelPayroll(payrollData)}
+ canCancel={canCancelPayroll(payrollData) && !readOnly}
+ canEdit={!readOnly}
payrollData={payrollData}
bankAccount={bankAccount}
taxes={taxes}
diff --git a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx
index 5688907a3..42e16ddad 100644
--- a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx
+++ b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.test.tsx
@@ -665,5 +665,14 @@ describe('PayrollOverviewPresentation', () => {
expect(await screen.findByRole('button', { name: /^Edit$/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /^Submit$/i })).toBeInTheDocument()
})
+
+ it('hides Edit but keeps Submit when canEdit is false on an unprocessed payroll', async () => {
+ renderWithProviders(
+ ,
+ )
+
+ expect(await screen.findByRole('button', { name: /^Submit$/i })).toBeInTheDocument()
+ expect(screen.queryByRole('button', { name: /^Edit$/i })).not.toBeInTheDocument()
+ })
})
})
diff --git a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx
index 29dfa7c42..94c2880c9 100644
--- a/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx
+++ b/src/components/Payroll/PayrollOverview/PayrollOverviewPresentation.tsx
@@ -43,6 +43,7 @@ interface PayrollOverviewProps {
status?: PayrollOverviewStatus
isProcessed: boolean
canCancel?: boolean
+ canEdit?: boolean
alerts?: PayrollFlowAlert[]
submissionBlockers?: PayrollSubmissionBlockerType[]
selectedUnblockOptions?: Record
@@ -82,6 +83,7 @@ export const PayrollOverviewPresentation = ({
status = PayrollOverviewStatus.Viewing,
isProcessed,
canCancel = false,
+ canEdit = true,
alerts = [],
submissionBlockers = [],
selectedUnblockOptions = {},
@@ -582,9 +584,11 @@ export const PayrollOverviewPresentation = ({
>
) : (
<>
-
+ {canEdit && (
+
+ )}