From 0f996f52e8e94494858db0359fd89a0305827a44 Mon Sep 17 00:00:00 2001 From: mrdulasolutions Date: Wed, 13 May 2026 19:13:27 -0400 Subject: [PATCH] feat(onboarding): add notification permission step to setup wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: "test notifications still fail. i think this is due to apple permissions and us not having an onboarding for settings enable permissions." The wiring was already there — App.tsx calls initNotifications() at mount, which probes and (if undecided) requests permission. But the prompt is implicit and easy to dismiss/miss during the chaos of first-launch (OAuth flows, API key paste, etc.), and once macOS has recorded a decision it doesn't re-prompt on subsequent launches. A fresh install that misses the implicit prompt OR has a stale TCC record from a prior install ends up with notifications=on at the app level but silently denied at the OS level — exactly the symptom we hit (UI says "test sent", no banner appears). This PR makes notification permission an explicit step in the SetupWizard, between Extensions and Analytics: - Explains what notifications are for and that macOS decides. - "Allow notifications" button calls requestPermission() and surfaces the result inline (granted / denied / dismissed). - On grant: persists notificationsEnabled=true so the rest of the app routes through the toggle. - On denial: shows an "Open System Settings" button that deep- links to AOS Mail's row in System Settings → Notifications via x-apple.systempreferences:...?id=com.mrdulasolutions.aosmail (macOS-recognized since Ventura). - "Continue without notifications" exit always available — permission is optional, not a hard gate. Wires into the existing wizard plumbing: Step type extended with "permissions"; visibleSteps initialization paths (3) include it between extensions and analytics; the "no extensions need auth" skip path now lands on permissions; the Extensions step's "Continue" button advances to permissions instead of analytics. After this lands, a fresh-install user with no prior TCC record will see the macOS notification prompt explicitly inside the wizard, with surrounding copy explaining why. Co-Authored-By: Claude Opus 4.7 --- src/renderer/components/SetupWizard.tsx | 132 ++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/SetupWizard.tsx b/src/renderer/components/SetupWizard.tsx index fe82bfb..29f1244 100644 --- a/src/renderer/components/SetupWizard.tsx +++ b/src/renderer/components/SetupWizard.tsx @@ -13,7 +13,15 @@ interface SetupWizardProps { initialStep?: "imap"; } -type Step = "loading" | "credentials" | "apikey" | "oauth" | "extensions" | "analytics" | "imap"; +type Step = + | "loading" + | "credentials" + | "apikey" + | "oauth" + | "extensions" + | "permissions" + | "analytics" + | "imap"; interface ExtensionAuthInfo { extensionId: string; @@ -44,6 +52,13 @@ export function SetupWizard({ onComplete, initialStep }: SetupWizardProps) { // Analytics opt-in (default ON — session replay is bundled under analytics) const [analyticsEnabled, setAnalyticsEnabled] = useState(true); + // Notification permission state. "unknown" before the user has clicked + // Grant; "granted" / "denied" after macOS has decided. Used by the + // permissions wizard step to render the right copy + CTA. + const [notificationPermission, setNotificationPermission] = useState< + "unknown" | "granted" | "denied" | "requesting" + >("unknown"); + // Check what's already configured and skip to the right step. // If `initialStep` was passed (e.g. "imap" from the empty-state CTA), // we still compute the visible-step indicator but don't override the @@ -73,6 +88,7 @@ export function SetupWizard({ onComplete, initialStep }: SetupWizardProps) { if (!hasAnthropicKey) flow.push("apikey"); if (!hasTokens) flow.push("oauth"); flow.push("extensions"); + flow.push("permissions"); flow.push("analytics"); setVisibleSteps(flow); @@ -86,7 +102,14 @@ export function SetupWizard({ onComplete, initialStep }: SetupWizardProps) { enterExtensionsStep(); } } else { - setVisibleSteps(["credentials", "apikey", "oauth", "extensions", "analytics"]); + setVisibleSteps([ + "credentials", + "apikey", + "oauth", + "extensions", + "permissions", + "analytics", + ]); setStep("credentials"); } }) @@ -221,19 +244,20 @@ export function SetupWizard({ onComplete, initialStep }: SetupWizardProps) { setStep("extensions"); setIsLoading(false); } else { - // No extensions need auth (or IPC failed) — skip extensions step entirely + // No extensions need auth (or IPC failed) — skip extensions step + // entirely and move to the permissions step (next in the flow). if (!result.success) { console.error("[SetupWizard] getPendingAuths failed:", result.error); } setVisibleSteps((prev) => prev.filter((s) => s !== "extensions")); setIsLoading(false); - setStep("analytics"); + setStep("permissions"); } } catch (err) { console.error("[SetupWizard] getPendingAuths failed:", err); setVisibleSteps((prev) => prev.filter((s) => s !== "extensions")); setIsLoading(false); - setStep("analytics"); + setStep("permissions"); } }, []); @@ -569,7 +593,7 @@ export function SetupWizard({ onComplete, initialStep }: SetupWizardProps) { {error &&
{error}
} + )} + {notificationPermission === "denied" && ( + + )} + + + + + )} + {step === "analytics" && ( <>