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" && (
<div className="model-onboarding-github">
<p className="model-onboarding-description">
Linking GitHub lets you import issues as tasks and keep pull
requests in sync with your work. This is entirely optional
Fusion works without it.
Connect GitHub to unlock issue and PR integration:
</p>
<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>
{/* Skip-state banner: shown when AI setup was skipped */}
@@ -1281,7 +1293,7 @@ export function ModelOnboardingModal({
in Settings → Authentication.
</p>
<button
className="btn btn-sm"
className="btn btn-primary btn-sm"
onClick={() => setStep("first-task")}
>
Continue without GitHub →
@@ -1304,51 +1316,61 @@ export function ModelOnboardingModal({
: "Not connected"}
</span>
</div>
<div>
{authActionInProgress === "github" ? (
<>
<button className="btn btn-sm" disabled>
{isGithubAuthenticated
? "Logging out…"
: "Waiting for login…"}
</button>
<button
className="btn btn-sm"
onClick={() => handleCancelLogin("github")}
style={{ marginLeft: 8 }}
>
Cancel
</button>
</>
) : isGithubAuthenticated ? (
{isGithubAuthenticated && (
authActionInProgress === "github" ? (
<button className="btn btn-sm" disabled>
Logging out…
</button>
) : (
<button
className="btn btn-sm"
onClick={() => handleLogout("github")}
>
Disconnect
</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
className="btn btn-primary btn-sm"
onClick={() => handleLogin("github")}
>
<GitPullRequest size={16} />
Connect
</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>
{/* 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 && (
<p className="onboarding-helper-text">
No worries if you're not ready connect GitHub anytime from Settings Authentication.

View File

@@ -490,16 +490,17 @@ describe("ModelOnboardingModal", () => {
});
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()} />);
await navigateToGitHubStep();
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({
providers: [
{ id: "github", name: "GitHub", authenticated: false, type: "oauth" },
@@ -510,13 +511,18 @@ describe("ModelOnboardingModal", () => {
await navigateToGitHubStep();
// Use more specific selector to avoid matching the header title
expect(screen.getByTestId("onboarding-auth-status-github")).toBeTruthy();
expect(screen.getByText("Not connected")).toBeTruthy();
expect(screen.getByText("Connect")).toBeTruthy();
expect(screen.getByText("Connect GitHub to unlock issue and PR integration:")).toBeTruthy();
expect(screen.getByText(/Import issues as tasks/)).toBeTruthy();
expect(screen.getByText(/Track pull requests/)).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({
providers: [
{ id: "github", name: "GitHub", authenticated: true, type: "oauth" },
@@ -527,9 +533,11 @@ describe("ModelOnboardingModal", () => {
await navigateToGitHubStep();
expect(screen.getByText(/Import issues as tasks/)).toBeTruthy();
expect(screen.getByTestId("onboarding-auth-status-github")).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 () => {

View File

@@ -24109,6 +24109,54 @@ html .column.drag-over * {
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 {
display: flex;
flex-direction: column;