feat(onboarding): the branch question becomes its own journey step (#1615)

Founder feedback from a real signup: the done screen stacked the welcome,
the 8-row company profile card AND the branch question, pushing the
question below the fold, and the tiny favicons-in-ellipses provider chips
looked bad.

The done screen now ends in a revealed Fortsatt action; a new 'source'
step at the existing KLART station (same station grammar as momsyn/moms
under MOMSEN) shows only "Var fanns bokforingen innan?" with redesigned
provider tiles: a 2-column grid of generously sized choices, each with
the real logo on a small white bordered mark (the LogoMark grammar from
NewUserChecklist), SIE-fil and new-business as equal-weight text tiles,
flat hover, no lift. Everything fits one viewport.

Behavior preserved exactly: mode='first' only (reducer-guarded
DONE_CONTINUE), the single-choice latch, fire-and-forget keepalive PATCH
/api/onboarding/state, captureBranch analytics, branchDestination
routing, and the quiet skip escape.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-14 12:25:23 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent fb3feaea12
commit 0938646693
7 changed files with 150 additions and 63 deletions
@@ -461,6 +461,41 @@ describe('journeyReducer: server errors', () => {
})
})
describe('journeyReducer: branch question step (source)', () => {
function atDone(mode: 'first' | 'add' = 'first'): JourneyState {
return run(
initJourney({ mode }),
{ type: 'ORG_SUBMITTED', orgNumber: '556677-8899' },
{ type: 'LOOKUP_RESULT', outcome: { status: 'found', result: lookup() } },
{ type: 'FY_CALENDAR_CONFIRMED' },
{ type: 'MOMS_PERIOD_PICKED', period: 'quarterly' },
{ type: 'METHOD_PICKED', method: 'accrual' },
{ type: 'SUBMIT_SUCCEEDED' },
)
}
it('continue moves the done screen to the source step, still on the Klart station', () => {
const s = journeyReducer(atDone(), { type: 'DONE_CONTINUE' })
expect(s.step).toBe('source')
expect(stationOfStep(s.step)).toBe(4)
})
it("mode='add' never reaches the source step", () => {
const s = atDone('add')
expect(journeyReducer(s, { type: 'DONE_CONTINUE' })).toBe(s)
})
it('is ignored anywhere but the done screen', () => {
const s = manualAbAtFy()
expect(journeyReducer(s, { type: 'DONE_CONTINUE' })).toBe(s)
})
it('Back from the source step restores the done screen', () => {
const s = run(atDone(), { type: 'DONE_CONTINUE' }, { type: 'BACK' })
expect(s.step).toBe('done')
})
})
describe('journeyReducer: robustness', () => {
it('ignores a LOOKUP_RESULT when no lookup is pending', () => {
const s = initJourney()
+11
View File
@@ -40,6 +40,7 @@ export type JourneyStep =
| 'moms'
| 'method'
| 'done'
| 'source'
export type JourneyStation = 0 | 1 | 2 | 3 | 4
@@ -59,6 +60,7 @@ const STATION_OF: Record<JourneyStep, JourneyStation> = {
moms: 2,
method: 3,
done: 4,
source: 4,
}
export function stationOfStep(step: JourneyStep): JourneyStation {
@@ -128,6 +130,7 @@ export type JourneyAction =
| { type: 'METHOD_PICKED'; method: 'accrual' | 'cash' }
| { type: 'SUBMIT_SUCCEEDED' }
| { type: 'SUBMIT_FAILED'; code: 'org_number_invalid' | 'period_invalid' | 'generic' }
| { type: 'DONE_CONTINUE' }
| { type: 'BACK' }
| { type: 'STATION_JUMP'; station: 0 | 1 | 2 | 3 }
@@ -475,6 +478,14 @@ export function journeyReducer(state: JourneyState, action: JourneyAction): Jour
case 'SUBMIT_SUCCEEDED':
return go(stay(state, { submitting: false }), 'done')
case 'DONE_CONTINUE': {
// Welcome screen → the branch question ("Var fanns bokföringen
// innan?") as its own step. First-company flow only: mode='add'
// ends on the done screen with "Öppna appen".
if (state.step !== 'done' || state.mode !== 'first') return state
return go(state, 'source')
}
case 'SUBMIT_FAILED': {
const cleared = stay(state, { submitting: false })
if (action.code === 'org_number_invalid') {