Skip to content

feat(gp): example app demo for admin onboarding flow (PBYR-4044) - #1171

Open
hamzaremote wants to merge 5 commits into
mainfrom
feat/gp-example-app
Open

feat(gp): example app demo for admin onboarding flow (PBYR-4044)#1171
hamzaremote wants to merge 5 commits into
mainfrom
feat/gp-example-app

Conversation

@hamzaremote

@hamzaremote hamzaremote commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

What

The final piece of the Global Payroll onboarding work (see split plan, PR 5): the example-app demos for the admin and employee flows, plus the server-side auth plumbing they need. Both SDK flows (#1142, #1168) are already merged to main.

Contents

  • example/api/jwt_auth.js — server-side minting of an employee-assertion JWT-bearer token (getEmployeeToken), plus a production guard on the token endpoints.
  • example/api/proxy.js — path-based token routing: /v1/employee/* → employee assertion (employment resolved from the x-rf-employment-id header, minted server-side so the FE never holds it); everything else unchanged.
  • example/api/routes.js — wire the /api/fetch-employee-token/:employmentId route.
  • example/src/RemoteFlows.tsx — add authType: 'none' so the inner (employee) context skips the FE auth callback while the proxy mints tokens.
  • example/src/PayrollAdminOnboarding.tsx / PayrollEmployeeOnboarding.tsx — the two demos.
  • example/src/App.tsx — register both demos.

Design notes (the open questions from the arch doc)

  • Dual-RemoteFlows-provider pattern (employee demo): the outer context uses a company-manager token to read country/jurisdiction from the employment (the employee assertion token can't fetch /v1/employments/:id); the inner context uses the employee-scoped token minted by the proxy.
  • Proxy auth contract: the FE sends x-rf-employment-id; the proxy swaps it for a JWT-bearer employee token on /v1/employee/*.

Notable

Verification

  • Library build ✅ · example type-check (tsc -b) ✅ · oxfmt ✅ · oxlint ✅ (0 warnings on the demos)

🤖 Generated with Claude Code


Note

Medium Risk
Optional legalEntityId is a public API change for integrators; employment creation now depends on legal-entity fetch and new error states must be handled correctly.

Overview
Adds a GP Admin Onboarding example demo that walks through country/basic info, contract, administrative details, and invitation, including handling for legal-entity load failures and missing GP-enabled entities.

SDK: PayrollAdminOnboardingFlow now wires in the real step components instead of placeholder null components. legalEntityId is optional — the hook always loads GP-enabled legal entities for the company, defaults to the first match when omitted, and surfaces legalEntities and isErrorLegalEntities on adminBag so hosts can distinguish fetch errors from “no GP legal entity.” Employment creation throws if no legal entity is available when creating a new employment.

Reviewed by Cursor Bugbot for commit 46a9f85. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Deploy preview for remote-flows ready!

Project:remote-flows
Status: ✅  Deploy successful!
Preview URL:https://remote-flows-fmol6w1qf-remotecom.vercel.app
Latest Commit:46a9f85

Deployed with vercel-action

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Deploy preview for adp-cost-calculator ready!

Project:adp-cost-calculator
Status: ✅  Deploy successful!
Preview URL:https://adp-cost-calculator-h30xfv9bq-remotecom.vercel.app
Latest Commit:46a9f85

Deployed with vercel-action

Comment thread example/src/PayrollEmployeeOnboarding.tsx
Comment thread example/src/PayrollEmployeeOnboarding.tsx
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage Report

✅ Coverage increased! 🎉

Metric Current Previous Change Status
Lines 84.90% 83.31% +1.59% 🟢
Statements 84.34% 82.66% +1.68% 🟢
Functions 82.93% 81.28% +1.65% 🟢
Branches 76.08% 75.31% +0.77% 🟢

Detailed Breakdown

Lines Coverage
  • Covered: 4083 / 4809
  • Coverage: 84.90%
  • Change: +1.59% (75 lines)
Statements Coverage
  • Covered: 4148 / 4918
  • Coverage: 84.34%
  • Change: +1.68% (76 statements)
Functions Coverage
  • Covered: 1098 / 1324
  • Coverage: 82.93%
  • Change: +1.65% (17 functions)
Branches Coverage
  • Covered: 2538 / 3336
  • Coverage: 76.08%
  • Change: +0.77% (31 branches)

✅ Coverage check passed

Comment thread example/src/PayrollEmployeeOnboarding.tsx
Comment thread example/src/PayrollAdminOnboarding.tsx Outdated
const currentStep = adminBag.stepState.currentStep.name;
const allSteps = Object.entries(STEP_LABELS);

if (adminBag.isLoading && !adminBag.countryCode) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this a bit counterintuitive?

if countryCode is defined and isLoading is triggered by any step the isLoading, it won't show

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5d0fb9c + dafa5b9. Changed the gate to adminBag.isLoading && adminBag.fields.length === 0 — it now blocks on any step whose schema hasn't loaded yet (not just before a country is picked), so it won't silently skip the loading state on step transitions later in the flow (e.g. while the contract_details/administrative_details schema is fetching, or while legal entities are being resolved).

const renderStep = () => {
switch (currentStep) {
case 'select_country': {
const isFormReady =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be isLoading and instead of not rendering the button put a disabled state?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dafa5b9. The button is no longer conditionally unmounted — it's always rendered with disabled={!isFormReady} (SubmitButton also composes that with its own isSubmitting/isLoading disabling), so it stays visible and just goes disabled/enabled as readiness changes.

<>
<div className='steps-navigation'>
<ul>
{allSteps.map(([key, label], index) => (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if for some reason in the future, the steps are dynamic like in the Onboarding & ContractorOnboarding, this won't work and we'll need to refactor it

Basically what we do is set the steps on the hook you read them through the adminBag and do the same iterations

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout — leaving this as static STEP_LABELS/STEP_DESCRIPTIONS for now since the admin flow's steps aren't dynamic today (select_country is the only conditionally-visible one, and this demo never sets skipCountry). Agreed on the approach if/when that changes: read the steps through adminBag the same way Onboarding/ContractorOnboarding do, rather than hardcoding them in the demo. Not doing that refactor in this PR since it's speculative for the current flow.

Comment thread example/src/PayrollAdminOnboarding.tsx Outdated
}

function GPAdminOnboardingInner() {
const { data: legalEntities, isLoading } = useGPLegalEntities(COMPANY_ID);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would solve this in the hooks internally.

If there are no legalEntities, this property can be exposed in the adminBag and they can do whatever they want with it

And the legalEntityId pass to the hook can be set internally, instead of expecting people setting them themselves.

if there are no legal entities nothing should be set

Let me know if I am missing anything

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in dafa5b9 — moved this into usePayrollAdminOnboarding itself: legalEntityId is now optional, and when not provided the hook fetches the company's GP-enabled legal entities via useGPLegalEntities internally and defaults to the first one. adminBag.legalEntities exposes the (possibly empty) array so the consumer can render its own "no legal entity" state — the demo now does exactly that instead of a separate wrapper component that fetched legal entities up front. If legalEntityId still ends up unset when submitting select_country, onSubmit throws rather than silently sending an empty ID.

@@ -0,0 +1,564 @@
import { useState } from 'react';

@gabrielseco gabrielseco Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you extract this file to a separated PR

I want to review it standalone to avoid having mixed comments from the other files?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — split out to #1201, which now carries PayrollEmployeeOnboarding.tsx plus its supporting server-side auth plumbing (jwt_auth.js employee-assertion minting, proxy.js routing, the employee-token route) and the two follow-up fix commits that were already reviewed here. This PR is now scoped to just the admin demo.

@remotecom
remotecom force-pushed the feat/gp-example-app branch from 0124a97 to 609a706 Compare July 28, 2026 11:13
Comment thread example/src/PayrollEmployeeOnboarding.tsx
remotecom pushed a commit that referenced this pull request Jul 28, 2026
Per review on #1171: instead of requiring callers to fetch legal entities
themselves and pass legalEntityId in, usePayrollAdminOnboarding now fetches
them internally via useGPLegalEntities and defaults legalEntityId to the
first GP-enabled one. legalEntityId is now optional and still overridable.
adminBag exposes the resulting legalEntities array so consumers can render
their own "no GP-enabled legal entity" state; onSubmit throws if it's
still missing when creating an employment.

Also updates the admin demo per review:
- loading gate now checks for empty fields rather than an unset
  countryCode, so it doesn't miss loading states past the first step
- the "Create Employment & Continue" button is disabled while not ready
  instead of being unmounted
- drops the separate useGPLegalEntities call/wrapper now that adminBag
  exposes legalEntities directly
remotecom pushed a commit that referenced this pull request Jul 28, 2026
Per review on #1171: gabrielseco asked to review the employee demo
standalone rather than mixed in with the admin demo comments. Moved to
#1201 (feat/gp-employee-onboarding) — this PR now only carries the admin
onboarding demo and its supporting SDK changes.

Removes example/src/PayrollEmployeeOnboarding.tsx, its App.tsx entry, and
the employee-only backend plumbing (employee-assertion JWT minting in
jwt_auth.js, employee-token proxy routing, the employee-token route).
Keeps the authType 'none' addition in RemoteFlows.tsx and the production
guard on the company-manager token endpoint in jwt_auth.js, since both are
still needed by/apply to the admin demo.
@hamzaremote hamzaremote changed the title feat(gp): example app demos for PayrollAdmin + PayrollEmployee onboarding (PBYR-4044) feat(gp): example app demo for admin onboarding flow (PBYR-4044) Jul 28, 2026
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

Metric Current Previous Change Status
Total (gzip) 249.23 kB 247.85 kB +1.38 kB (+0.6%) 🔴
Total (raw) 667.6 kB 664.6 kB +2.99 kB (+0.5%) 🔴
CSS (gzip) 21.69 kB 21.69 kB 0 B (0%) 🟢
CSS (raw) 112.77 kB 112.77 kB 0 B (0%) 🟢

Size Limits

  • ✅ Total gzipped: 249.23 kB / 350 kB (71.2%)
  • ✅ Total raw: 667.6 kB / 850 kB (78.5%)
  • ✅ CSS gzipped: 21.69 kB / 25 kB (86.8%)

Largest Files (Top 5)

  1. chunk-4FGKIGKF.js - 14 kB (0 B (0%))
  2. styles.css - 10.85 kB (0 B (0%))
  3. index.css - 10.85 kB (0 B (0%))
  4. index.js - 6.68 kB (+67 B (+1.0%))
  5. chunk-XPITUVQE.js - 6.46 kB (0 B (0%))
View All Files (402 total)
File Size (gzip) Change
chunk-4FGKIGKF.js 14 kB 0 B (0%)
styles.css 10.85 kB 0 B (0%)
index.css 10.85 kB 0 B (0%)
index.js 6.68 kB +67 B (+1.0%)
chunk-XPITUVQE.js 6.46 kB 0 B (0%)
chunk-7BUFO7JW.js 6.44 kB 0 B (0%)
chunk-NXRWBI36.js 5.31 kB 0 B (0%)
chunk-YRS5LJ6Y.js 4.86 kB 0 B (0%)
chunk-POKAYJ7P.js 4.76 kB 0 B (0%)
chunk-53IIRCAL.js 4.15 kB 0 B (0%)

✅ Bundle size check passed

Comment thread src/flows/PayrollAdminOnboarding/hooks.tsx
hamzaremote added a commit that referenced this pull request Jul 28, 2026
…44) (#1201)

* feat(gp): example app demo for employee self-onboarding flow (PBYR-4044)

Adds the Global Payroll employee self-onboarding demo page and its
server-side auth plumbing, split out of the combined admin+employee demo
PR (#1171) for standalone review:

- example/api/jwt_auth.js: server-side employee-assertion JWT minting
- example/api/proxy.js: path-based token routing — /v1/employee/* → employee
  assertion (via x-rf-employment-id header), everything else unchanged
- example/api/routes.js: wire the employee-token route
- example/src/RemoteFlows.tsx: add authType 'none' for proxy-minted tokens
- example/src/PayrollEmployeeOnboarding.tsx: the demo itself
- example/src/App.tsx: register the demo

The employee demo derives the bank substep from the flow bag's
selfOnboardingSubsteps rather than calling a hook directly, so it needs no
extra SDK export.

* fix(gp): correct completion detection in employee demo

- derive the last step from SDK-reachable steps (tax steps only navigable once
  employeeBag.isComplete) rather than the sidebar list, so Submit/completion
  fires on the real final step (e.g. bank_account on a pre-enrollment USA run).
- federal_taxes "Skip" now finishes (setDone) when it's the last reachable step
  (USA without jurisdiction), instead of a no-op next().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(gp): derive employee-demo completion reactively, not from stale closure

Step onSuccess handlers latched `done` from that render's `isLastStep`, so if a
later step became reachable after the submit (e.g. enrollment flips isComplete
and adds tax steps), the demo showed "Complete" instead of advancing. Now each
submit records the step name and completion is derived per-render from
`lastSubmittedStep === lastStepKey`, so a step that stops being last no longer
ends the flow. Explicit "Finish" on an unavailable tax step still latches done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(gp): correct misleading company-manager auth claims in employee demo

Per Cursor Bugbot on #1201: the outer context's comments and error message
claimed the dev proxy mints a company-manager token for /v1/employments/*
and /v1/companies/*, but proxy.js's getTokenType never has a rule for
those paths — it always falls through to the refresh-token ('user-token')
flow, same as most other demos behind this proxy. authType on the FE
RemoteFlows component doesn't change this either, since the proxy
middleware always overwrites the outgoing Authorization header itself.

Actually wiring company-manager auth for those paths would change
behavior for other demos sharing this proxy (ContractorOnboarding,
Termination, ContractAmendment, Onboarding) that aren't part of this PR,
so this only corrects the comments/error text to describe what actually
happens today.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hamzaremote and others added 3 commits July 28, 2026 16:34
Adds the Global Payroll demo pages and the server-side auth plumbing:

- example/api/jwt_auth.js: server-side employee-assertion JWT minting
- example/api/proxy.js: path-based token routing — /v1/employee/* → employee
  assertion (via x-rf-employment-id header), everything else unchanged
- example/api/routes.js: wire the employee-token route
- example/src/RemoteFlows.tsx: add authType 'none' for proxy-minted tokens
- example/src/PayrollAdminOnboarding.tsx / PayrollEmployeeOnboarding.tsx: demos
- example/src/App.tsx: register both demos

The employee demo derives the bank substep from the flow bag's
selfOnboardingSubsteps rather than calling a hook directly, so it needs no
extra SDK export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PayrollAdminOnboardingFlow still passed the null stubs from the original
PBYR-4043 scaffolding commit into the render prop, even though the real
step components were added later in PBYR-4044. The admin flow rendered
nothing for every step.
Per review on #1171: instead of requiring callers to fetch legal entities
themselves and pass legalEntityId in, usePayrollAdminOnboarding now fetches
them internally via useGPLegalEntities and defaults legalEntityId to the
first GP-enabled one. legalEntityId is now optional and still overridable.
adminBag exposes the resulting legalEntities array so consumers can render
their own "no GP-enabled legal entity" state; onSubmit throws if it's
still missing when creating an employment.

Also updates the admin demo per review:
- loading gate now checks for empty fields rather than an unset
  countryCode, so it doesn't miss loading states past the first step
- the "Create Employment & Continue" button is disabled while not ready
  instead of being unmounted
- drops the separate useGPLegalEntities call/wrapper now that adminBag
  exposes legalEntities directly
remotecom pushed a commit that referenced this pull request Jul 28, 2026
Per review on #1171: gabrielseco asked to review the employee demo
standalone rather than mixed in with the admin demo comments. Moved to
onboarding demo and its supporting SDK changes.

Removes example/src/PayrollEmployeeOnboarding.tsx, its App.tsx entry, and
the employee-only backend plumbing (employee-assertion JWT minting in
jwt_auth.js, employee-token proxy routing, the employee-token route).
Keeps the authType 'none' addition in RemoteFlows.tsx and the production
guard on the company-manager token endpoint in jwt_auth.js, since both are
still needed by/apply to the admin demo.
@remotecom
remotecom force-pushed the feat/gp-example-app branch from 815ec66 to 31017b5 Compare July 28, 2026 14:35

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 31017b5. Configure here.

Comment thread src/flows/PayrollAdminOnboarding/hooks.tsx
hamzaremote and others added 2 commits July 28, 2026 17:22
…tyId

Per Cursor Bugbot: when legalEntityId was provided explicitly, the fetch
was skipped as a minor optimization, so adminBag.legalEntities was always
[] regardless of the company's actual legal entities. Since the documented
contract is "empty legalEntities means the company has none," any caller
that passed an explicit legalEntityId and checked that contract would hit
the empty-state path incorrectly. Now always fetches so legalEntities
stays accurate either way.
useGPLegalEntities failures left legalEntities as [], the same shape as
"company has no GP-enabled legal entity" — so callers following that
documented contract couldn't tell an API/auth failure from a real empty
result. Expose adminBag.isErrorLegalEntities so consumers (and the demo)
can render an error state instead of the empty-state message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@remotecom
remotecom force-pushed the feat/gp-example-app branch from a9635dd to 46a9f85 Compare July 28, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants