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:
@@ -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 →
|
|
||||||
</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}
|
||||||
|
|||||||
Reference in New Issue
Block a user