feat(FN-1932): improve GitHub onboarding messaging and connect CTA

- Replace generic GitHub copy with a clear benefits list for issue, PR, and code-link workflows
- Promote the unauthenticated connect experience into a dedicated CTA container with primary button, icon, and inline feedback states
- Keep connected users focused on status and disconnect actions while hiding the connect CTA when already authenticated
- Expand ModelOnboardingModal tests to cover fallback guidance, benefits rendering, CTA styling, and connected-state behavior
This commit is contained in:
Fusion
2026-04-17 00:28:57 -07:00
committed by gsxdsm
parent d51992850f
commit adfc32395f
3 changed files with 121 additions and 43 deletions

View File

@@ -1249,9 +1249,21 @@ export function ModelOnboardingModal({
{step === "github" && ( {step === "github" && (
<div className="model-onboarding-github"> <div className="model-onboarding-github">
<p className="model-onboarding-description"> <p className="model-onboarding-description">
Linking GitHub lets you import issues as tasks and keep pull Connect GitHub to unlock issue and PR integration:
requests in sync with your work. This is entirely optional </p>
Fusion works without it. <ul className="onboarding-github-benefits">
<li>
<strong>Import issues as tasks</strong> Turn GitHub issues into trackable tasks
</li>
<li>
<strong>Track pull requests</strong> See PR status alongside your work
</li>
<li>
<strong>Link code changes</strong> Connect commits and PRs to tasks automatically
</li>
</ul>
<p className="onboarding-helper-text">
GitHub integration is entirely optional Fusion works without it.
</p> </p>
{/* Skip-state banner: shown when AI setup was skipped */} {/* Skip-state banner: shown when AI setup was skipped */}
@@ -1281,7 +1293,7 @@ export function ModelOnboardingModal({
in Settings → Authentication. in Settings → Authentication.
</p> </p>
<button <button
className="btn btn-sm" className="btn btn-primary btn-sm"
onClick={() => setStep("first-task")} onClick={() => setStep("first-task")}
> >
Continue without GitHub → Continue without GitHub →
@@ -1304,51 +1316,61 @@ export function ModelOnboardingModal({
: "Not connected"} : "Not connected"}
</span> </span>
</div> </div>
<div> {isGithubAuthenticated && (
{authActionInProgress === "github" ? ( authActionInProgress === "github" ? (
<> <button className="btn btn-sm" disabled>
<button className="btn btn-sm" disabled> Logging out…
{isGithubAuthenticated </button>
? "Logging out…" ) : (
: "Waiting for login…"}
</button>
<button
className="btn btn-sm"
onClick={() => handleCancelLogin("github")}
style={{ marginLeft: 8 }}
>
Cancel
</button>
</>
) : isGithubAuthenticated ? (
<button <button
className="btn btn-sm" className="btn btn-sm"
onClick={() => handleLogout("github")} onClick={() => handleLogout("github")}
> >
Disconnect Disconnect
</button> </button>
)
)}
</div>
{!isGithubAuthenticated && (
<div className="onboarding-github-connect-cta" data-testid="onboarding-github-connect-cta">
{authActionInProgress === "github" ? (
<div className="onboarding-github-connect-actions">
<button className="btn btn-sm" disabled>
Waiting for login…
</button>
<button
className="btn btn-sm"
onClick={() => handleCancelLogin("github")}
>
Cancel
</button>
</div>
) : ( ) : (
<button <button
className="btn btn-primary btn-sm" className="btn btn-primary btn-sm"
onClick={() => handleLogin("github")} onClick={() => handleLogin("github")}
> >
<GitPullRequest size={16} />
Connect Connect
</button> </button>
)} )}
{/* Show timeout message */}
{loginOutcomes["github"] === "timeout" && authActionInProgress !== "github" && (
<p className="onboarding-helper-text onboarding-github-connect-feedback">
Login timed out. Please try again.
</p>
)}
{/* Show failure message */}
{loginOutcomes["github"] === "failed" && authActionInProgress !== "github" && (
<p className="field-error onboarding-github-connect-feedback">
Login failed. Please try again.
</p>
)}
</div> </div>
{/* Show timeout message */} )}
{loginOutcomes["github"] === "timeout" && authActionInProgress !== "github" && (
<p className="onboarding-helper-text" style={{ marginTop: 4 }}>
Login timed out. Please try again.
</p>
)}
{/* Show failure message */}
{loginOutcomes["github"] === "failed" && authActionInProgress !== "github" && (
<p className="field-error" style={{ marginTop: 4 }}>
Login failed. Please try again.
</p>
)}
</div>
{!isGithubAuthenticated && ( {!isGithubAuthenticated && (
<p className="onboarding-helper-text"> <p className="onboarding-helper-text">
No worries if you're not ready connect GitHub anytime from Settings Authentication. No worries if you're not ready connect GitHub anytime from Settings Authentication.

View File

@@ -490,16 +490,17 @@ describe("ModelOnboardingModal", () => {
}); });
describe("GitHub step", () => { describe("GitHub step", () => {
it("shows optional guidance when GitHub provider is not configured", async () => { it("GitHub step shows fallback when no GitHub provider", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />); render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
await navigateToGitHubStep(); await navigateToGitHubStep();
expect(screen.getByText(/GitHub integration isn't set up yet/)).toBeTruthy(); expect(screen.getByText(/GitHub integration isn't set up yet/)).toBeTruthy();
expect(screen.getByText("Continue without GitHub →")).toBeTruthy(); expect(screen.getByText(/Settings → Authentication/)).toBeTruthy();
expect(screen.getByRole("button", { name: "Continue without GitHub →" })).toBeTruthy();
}); });
it("shows GitHub status and login/logout actions when GitHub provider is present", async () => { it("GitHub step shows benefits list with connect action", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({ mockFetchAuthStatus.mockResolvedValueOnce({
providers: [ providers: [
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" }, { id: "github", name: "GitHub", authenticated: false, type: "oauth" },
@@ -510,13 +511,18 @@ describe("ModelOnboardingModal", () => {
await navigateToGitHubStep(); await navigateToGitHubStep();
// Use more specific selector to avoid matching the header title expect(screen.getByText("Connect GitHub to unlock issue and PR integration:")).toBeTruthy();
expect(screen.getByTestId("onboarding-auth-status-github")).toBeTruthy(); expect(screen.getByText(/Import issues as tasks/)).toBeTruthy();
expect(screen.getByText("Not connected")).toBeTruthy(); expect(screen.getByText(/Track pull requests/)).toBeTruthy();
expect(screen.getByText("Connect")).toBeTruthy(); expect(screen.getByText(/Link code changes/)).toBeTruthy();
const ctaContainer = screen.getByTestId("onboarding-github-connect-cta");
expect(ctaContainer).toHaveClass("onboarding-github-connect-cta");
const connectButton = screen.getByRole("button", { name: /Connect/ });
expect(connectButton).toHaveClass("btn", "btn-primary", "btn-sm");
}); });
it("shows connected status when GitHub is authenticated", async () => { it("GitHub step shows connected state when already authenticated", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({ mockFetchAuthStatus.mockResolvedValueOnce({
providers: [ providers: [
{ id: "github", name: "GitHub", authenticated: true, type: "oauth" }, { id: "github", name: "GitHub", authenticated: true, type: "oauth" },
@@ -527,9 +533,11 @@ describe("ModelOnboardingModal", () => {
await navigateToGitHubStep(); await navigateToGitHubStep();
expect(screen.getByText(/Import issues as tasks/)).toBeTruthy();
expect(screen.getByTestId("onboarding-auth-status-github")).toBeTruthy(); expect(screen.getByTestId("onboarding-auth-status-github")).toBeTruthy();
expect(screen.getByText("✓ Connected")).toBeTruthy(); expect(screen.getByText("✓ Connected")).toBeTruthy();
expect(screen.getByText("Disconnect")).toBeTruthy(); expect(screen.getByRole("button", { name: "Disconnect" })).toBeTruthy();
expect(screen.queryByTestId("onboarding-github-connect-cta")).toBeNull();
}); });
it("allows navigating to First Task step via Continue without GitHub", async () => { it("allows navigating to First Task step via Continue without GitHub", async () => {

View File

@@ -24109,6 +24109,54 @@ html .column.drag-over * {
gap: 16px; gap: 16px;
} }
/* === Onboarding GitHub Step === */
.onboarding-github-benefits {
margin: 0;
padding-left: var(--space-xl);
display: flex;
flex-direction: column;
gap: var(--space-xs);
color: var(--text-muted);
}
.onboarding-github-benefits li {
line-height: 1.5;
}
.onboarding-github-benefits li strong {
color: var(--text);
}
.onboarding-github-connect-cta {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-md);
border: 1px solid color-mix(in srgb, var(--todo) 35%, var(--border));
border-left: var(--space-xs) solid var(--todo);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--todo) 10%, transparent);
}
.onboarding-github-connect-cta:focus-within {
box-shadow: var(--focus-ring);
}
.onboarding-github-connect-cta .btn {
align-self: flex-start;
}
.onboarding-github-connect-actions {
display: flex;
align-items: center;
gap: var(--space-sm);
flex-wrap: wrap;
}
.onboarding-github-connect-feedback {
margin: 0;
}
.model-onboarding-github-optional { .model-onboarding-github-optional {
display: flex; display: flex;
flex-direction: column; flex-direction: column;