feat(welcome): two-button PSA CTA in step-2 — Connect now / Connect later

Picking a real PSA in /welcome/step-2 now swaps the primary action from a
single "Continue" + a tiny "Connect now →" link into an explicit choice:
"Connect <PSA> now" (saves primary_psa and routes to /account/integrations)
or "Connect later" (saves primary_psa and continues to step 3). The old
link never actually persisted primary_psa before navigating — that's now
fixed. "No PSA yet" and no-selection states keep the original single
Continue button. Skip-this-step and Skip-the-rest are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 23:59:18 -04:00
parent cbb4b25671
commit dc88797469

View File

@@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { useAuthStore } from '@/store/authStore' import { useAuthStore } from '@/store/authStore'
import { onboardingApi, type PrimaryPsa } from '@/api/onboarding' import { onboardingApi, type PrimaryPsa } from '@/api/onboarding'
@@ -14,21 +14,24 @@ const PSA_OPTIONS: { value: PrimaryPsa; label: string; description: string }[] =
/** /**
* `/welcome/step-2` — second step of the welcome wizard. Captures the PSA the * `/welcome/step-2` — second step of the welcome wizard. Captures the PSA the
* shop primarily uses. Selecting a non-`none` tile reveals a quiet "Connect * shop primarily uses. Selecting a non-`none` tile splits the primary CTA
* now" link that navigates out to `/account/integrations`. The wizard's * into "Connect <PSA> now" (routes to `/account/integrations` after saving)
* primary action is "Continue" — credential entry is intentionally OUT of * and "Connect later" (continues to step 3). Both paths persist the
* the wizard (per spec). * `primary_psa` choice before navigating.
*/ */
export function WelcomeStep2() { export function WelcomeStep2() {
const navigate = useNavigate() const navigate = useNavigate()
const fetchUser = useAuthStore((s) => s.fetchUser) const fetchUser = useAuthStore((s) => s.fetchUser)
const [primaryPsa, setPrimaryPsa] = useState<PrimaryPsa | null>(null) const [primaryPsa, setPrimaryPsa] = useState<PrimaryPsa | null>(null)
const [submitting, setSubmitting] = useState<'continue' | 'skip' | 'dismiss' | null>(null) const [submitting, setSubmitting] = useState<
'continue' | 'connect-now' | 'skip' | 'dismiss' | null
>(null)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const isBusy = submitting !== null const isBusy = submitting !== null
const showConnectNow = primaryPsa !== null && primaryPsa !== 'none' const showConnectNow = primaryPsa !== null && primaryPsa !== 'none'
const selectedPsaLabel = PSA_OPTIONS.find((o) => o.value === primaryPsa)?.label
const handleContinue = async () => { const handleContinue = async () => {
if (isBusy) return if (isBusy) return
@@ -48,6 +51,24 @@ export function WelcomeStep2() {
} }
} }
const handleConnectNow = async () => {
if (isBusy || !showConnectNow) return
setError(null)
setSubmitting('connect-now')
try {
await onboardingApi.updateStep({
step: 2,
action: 'complete',
data: { primary_psa: primaryPsa! },
})
await fetchUser()
navigate('/account/integrations')
} catch {
setError('Could not save. Please try again.')
setSubmitting(null)
}
}
const handleSkipStep = async () => { const handleSkipStep = async () => {
if (isBusy) return if (isBusy) return
setError(null) setError(null)
@@ -131,42 +152,69 @@ export function WelcomeStep2() {
})} })}
</div> </div>
{showConnectNow && (
<div className="pt-1">
<Link
to="/account/integrations"
data-testid="welcome-step-2-connect-now"
className="text-xs text-muted-foreground hover:underline"
>
Connect now &rarr;
</Link>
</div>
)}
{error && ( {error && (
<p className="text-xs text-red-400" data-testid="welcome-step-2-error"> <p className="text-xs text-red-400" data-testid="welcome-step-2-error">
{error} {error}
</p> </p>
)} )}
<div className="flex items-center gap-3 pt-2"> <div className="flex flex-wrap items-center gap-3 pt-2">
<button {showConnectNow ? (
type="button" <>
onClick={handleContinue} <button
disabled={isBusy} type="button"
data-testid="welcome-step-2-continue" onClick={handleConnectNow}
className={cn( disabled={isBusy}
'inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold btn-press', data-testid="welcome-step-2-connect-now"
'bg-primary text-primary-foreground hover:bg-primary/90', className={cn(
'focus:outline-hidden focus:ring-2 focus:ring-primary/30', 'inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
'disabled:opacity-60', 'bg-primary text-primary-foreground hover:bg-primary/90',
)} 'focus:outline-hidden focus:ring-2 focus:ring-primary/30',
> 'disabled:opacity-60',
{submitting === 'continue' && ( )}
<Loader2 className="h-4 w-4 animate-spin" /> >
)} {submitting === 'connect-now' && (
Continue <Loader2 className="h-4 w-4 animate-spin" />
</button> )}
Connect {selectedPsaLabel} now
</button>
<button
type="button"
onClick={handleContinue}
disabled={isBusy}
data-testid="welcome-step-2-continue"
className={cn(
'inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium btn-press',
'bg-card border border-border text-foreground hover:bg-foreground/5',
'focus:outline-hidden focus:ring-2 focus:ring-primary/20',
'disabled:opacity-60',
)}
>
{submitting === 'continue' && (
<Loader2 className="h-4 w-4 animate-spin" />
)}
Connect later
</button>
</>
) : (
<button
type="button"
onClick={handleContinue}
disabled={isBusy}
data-testid="welcome-step-2-continue"
className={cn(
'inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
'bg-primary text-primary-foreground hover:bg-primary/90',
'focus:outline-hidden focus:ring-2 focus:ring-primary/30',
'disabled:opacity-60',
)}
>
{submitting === 'continue' && (
<Loader2 className="h-4 w-4 animate-spin" />
)}
Continue
</button>
)}
<button <button
type="button" type="button"
onClick={handleSkipStep} onClick={handleSkipStep}