Skip to content

feat: replace native update dialog with toast notification#334

Merged
AnthonyRonning merged 1 commit intomasterfrom
feature/toast-update-notification
Dec 3, 2025
Merged

feat: replace native update dialog with toast notification#334
AnthonyRonning merged 1 commit intomasterfrom
feature/toast-update-notification

Conversation

@AnthonyRonning
Copy link
Contributor

@AnthonyRonning AnthonyRonning commented Dec 2, 2025

Summary

Replace the intrusive native dialog popup with a subtle toast notification when an update is ready to install.

Changes

  • Extended GlobalNotification.tsx with action button support and persistent mode (duration=0)
  • Created UpdateEventListener.tsx to handle update-ready events from the Rust backend
  • Modified lib.rs to emit events instead of showing native dialog
  • Added restart_for_update command for frontend-triggered restart

User Experience

  • Toast notification appears in top-right corner when update is downloaded
  • Shows version number and two buttons: "Later" (dismiss) and "Restart Now" (apply update)
  • Notification persists until user interacts with it
  • Non-intrusive - doesn't block the UI like the native dialog did

Testing

Tested locally by temporarily lowering version to trigger update detection. Toast appeared correctly with both buttons functioning as expected.

Summary by CodeRabbit

  • New Features

    • Update notifications now show the downloaded version and offer "Later" and "Restart Now" actions.
    • Notifications support actionable buttons with primary/secondary styling.
  • Refactor

    • Switched to an event-driven update flow where the frontend handles restart prompts and actions.
  • Chores

    • Removed native dialog-based prompt and cleaned up related platform permissions.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 2, 2025

Walkthrough

Replaces a dialog-driven desktop update restart with an event-driven flow: backend emits an "update-ready" event with version payload and exposes restart_for_update; frontend listens, shows an actionable notification, and can invoke the restart command.

Changes

Cohort / File(s) Change Summary
Tauri backend & updater
frontend/src-tauri/src/lib.rs
Added fn restart_for_update(app_handle: tauri::AppHandle) and registered it in tauri::generate_handler![]; removed tauri-plugin-dialog usage; emit "update-ready" event with a local UpdateReadyPayload (version) instead of showing a Rust dialog.
Tauri manifest / deps
frontend/src-tauri/Cargo.toml
Removed dependency tauri-plugin-dialog = "2.4.2".
Capabilities / permissions
frontend/src-tauri/capabilities/default.json, frontend/src-tauri/capabilities/mobile-android.json, frontend/src-tauri/capabilities/mobile-ios.json
Removed "dialog:default" permission from the listed capabilities JSON files.
Frontend integration
frontend/src/app.tsx
Imported and rendered new UpdateEventListener component inside the app component tree (placed under BillingServiceProvider between ProxyEventListener and DeepLinkHandler).
Update listener component
frontend/src/components/UpdateEventListener.tsx
New component that subscribes to "update-ready" (guarded by isTauri()), shows a notification with "Later" and "Restart Now" actions, calls restart_for_update on restart action, handles setup errors, and cleans up listener on unmount.
Notification UI & types
frontend/src/components/GlobalNotification.tsx
Added Download icon and new "update" notification type; introduced NotificationAction interface and actions?: NotificationAction[] on Notification; refactored layout to render action buttons (primary/secondary) or a close button; added action handling that dismisses after click.

Sequence Diagram

sequenceDiagram
    participant Updater as Updater (native)
    participant Tauri as Tauri Backend
    participant Frontend as Frontend Renderer
    participant Listener as UpdateEventListener
    participant Notifier as NotificationContext / UI
    participant User as User

    Updater->>Tauri: Download & install update
    Tauri->>Tauri: Prepare update
    Tauri->>Frontend: Emit "update-ready" event (version)
    
    Frontend->>Listener: Listener subscribed to "update-ready"
    Listener-->>Frontend: Receives event + version
    Listener->>Notifier: Show update notification with actions ("Later", "Restart Now")
    Notifier->>User: Display notification
    
    User->>Notifier: Click "Restart Now"
    Notifier->>Listener: Action handler invoked
    Listener->>Tauri: Invoke `restart_for_update`
    Tauri->>Tauri: Restart application to apply update
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review frontend/src-tauri/src/lib.rs for correct event payload shape, handler registration, and safe restart invocation.
  • Validate UpdateEventListener.tsx for correct event subscription/unsubscribe, error handling, and Tauri invoke usage.
  • Inspect GlobalNotification.tsx changes for accessibility, styling regressions, and correct action button behavior.
  • Confirm capability JSON removals align with platform requirements and CI packaging.

Possibly related PRs

Poem

🐰 I hopped to hear the update chime,
An event, not a modal this time.
I nibbled "Later", then gave a twitch,
"Restart Now" — a carrot-fresh switch.
Off we go, updated and sublime! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing a native update dialog with a toast notification, which is the primary objective of the pull request.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/toast-update-notification

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 2, 2025

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4af2d4b
Status: ✅  Deploy successful!
Preview URL: https://33ecf7f4.maple-ca8.pages.dev
Branch Preview URL: https://feature-toast-update-notific.maple-ca8.pages.dev

View logs

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Dec 2, 2025

Greptile Overview

Greptile Summary

This PR successfully replaces the intrusive native dialog update prompt with a non-blocking toast notification system, improving user experience.

Key Changes

  • Removed tauri-plugin-dialog dependency and replaced dialog-based update prompt with event emission
  • Extended GlobalNotification component with action button support and persistent notifications (duration=0)
  • Created UpdateEventListener to handle update-ready events from the Rust backend
  • Added restart_for_update command for frontend-triggered application restart

Implementation Quality

  • Clean separation of concerns: backend emits events, frontend handles UI
  • Proper cleanup with event listener unsubscribe in useEffect
  • Maintains existing UPDATE_DOWNLOADED flag to prevent duplicate notifications
  • Uses TypeScript strict typing throughout
  • Follows project code style guidelines

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are well-architected with proper separation of concerns, include proper cleanup logic, maintain existing safeguards (UPDATE_DOWNLOADED flag), and follow TypeScript best practices. The implementation is straightforward with no complex logic or security concerns.
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
frontend/src-tauri/src/lib.rs 5/5 Replaced native dialog with event emission for update notifications and added restart_for_update command
frontend/src/components/UpdateEventListener.tsx 5/5 New component that listens for update-ready events and displays toast notification with action buttons
frontend/src/components/GlobalNotification.tsx 5/5 Extended notification system with action button support and persistent mode (duration=0)
frontend/src/app.tsx 5/5 Added UpdateEventListener component to the app component tree

Sequence Diagram

sequenceDiagram
    participant Updater as Tauri Updater
    participant Backend as Rust Backend (lib.rs)
    participant Frontend as Frontend App
    participant Listener as UpdateEventListener
    participant Notification as GlobalNotification
    participant User as User
    
    Note over Updater,Backend: Update Check & Download
    Updater->>Backend: check_for_updates()
    Backend->>Updater: Download update
    Updater->>Backend: update.install(bytes)
    Backend->>Backend: Mark UPDATE_DOWNLOADED flag
    
    Note over Backend,Listener: Event Emission
    Backend->>Frontend: emit("update-ready", {version})
    Frontend->>Listener: Event received
    Listener->>Notification: showNotification({type: "update", actions: [...], duration: 0})
    
    Note over Notification,User: User Interaction
    Notification->>User: Display toast with "Later" and "Restart Now" buttons
    
    alt User clicks "Later"
        User->>Notification: Click "Later"
        Notification->>Notification: handleDismiss()
        Notification->>Frontend: Clear notification
    else User clicks "Restart Now"
        User->>Notification: Click "Restart Now"
        Notification->>Backend: invoke("restart_for_update")
        Backend->>Backend: app_handle.restart()
        Backend->>Updater: Apply update and restart
    end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
frontend/src/components/GlobalNotification.tsx (2)

5-9: Make NotificationAction.onClick async‑friendly and simplify actions handling

Right now NotificationAction.onClick is typed as () => void, but UpdateEventListener passes an async handler. This works at runtime, but it’s clearer to reflect that handlers may return a promise and to avoid the non‑null assertion when mapping actions.

You can tighten this up and simplify the rendering like this:

-export interface NotificationAction {
-  label: string;
-  onClick: () => void;
-  variant?: "primary" | "secondary";
-}
+export interface NotificationAction {
+  label: string;
+  onClick: () => void | Promise<void>;
+  variant?: "primary" | "secondary";
+}
@@
 export interface Notification {
   id: string;
   type: "success" | "error" | "info" | "update";
   title: string;
   message?: string;
   icon?: React.ReactNode;
   duration?: number; // ms, 0 = permanent
-  actions?: NotificationAction[];
+  actions?: NotificationAction[];
 }
@@
-  const hasActions = notification.actions && notification.actions.length > 0;
+  const actions = notification.actions ?? [];
+  const hasActions = actions.length > 0;
@@
-        {hasActions && (
+        {hasActions && (
           <div className="flex justify-end gap-2">
-            {notification.actions!.map((action, index) => (
+            {actions.map((action, index) => (
               <button

This keeps runtime behavior the same, better expresses the intent in the types, and removes the need for ! on notification.actions.

Also applies to: 18-19, 118-137


2-2: Minor UI/style nit: long Tailwind class list

The new "update" icon and border styling look consistent with the existing "info" style. One small optional tweak: the class list on Line 89 is quite long and exceeds the 100‑character guideline; you could split it across multiple strings for readability:

-        className={cn(
-          "pointer-events-auto flex flex-col gap-3 rounded-lg border bg-card text-card-foreground p-4 shadow-lg transition-all duration-200 min-w-[320px] max-w-md",
+        className={cn(
+          "pointer-events-auto flex flex-col gap-3 rounded-lg border bg-card text-card-foreground p-4",
+          "shadow-lg transition-all duration-200 min-w-[320px] max-w-md",

Purely cosmetic; behavior is unchanged.

Also applies to: 78-80, 89-95

frontend/src/components/UpdateEventListener.tsx (1)

14-62: Harden async listener setup to avoid a rare leak on unmount

Because setupListeners is async and assigns unlistenUpdateReady after listen resolves, there’s a small race where the component could unmount before listen completes: the cleanup runs with unlistenUpdateReady === null, but the listener is still attached once the promise resolves.

It’s unlikely in practice (root‑level component), but you can make this robust with a cancellation flag:

-  useEffect(() => {
+  useEffect(() => {
+    let cancelled = false;
@@
-    const setupListeners = async () => {
+    const setupListeners = async () => {
       try {
-        unlistenUpdateReady = await listen<UpdateReadyPayload>("update-ready", (event) => {
+        const unlisten = await listen<UpdateReadyPayload>("update-ready", (event) => {
           const { version } = event.payload;
           showNotification({
             // ...
           });
-        });
+        });
+
+        if (cancelled) {
+          unlisten();
+        } else {
+          unlistenUpdateReady = unlisten;
+        }
       } catch (error) {
         console.error("Failed to setup update event listeners:", error);
       }
     };
@@
-    return () => {
-      if (unlistenUpdateReady) unlistenUpdateReady();
-    };
+    return () => {
+      cancelled = true;
+      if (unlistenUpdateReady) unlistenUpdateReady();
+    };
   }, [showNotification]);

This guarantees that any listener created after unmount is immediately cleaned up.

frontend/src-tauri/src/lib.rs (1)

373-388: Consider hoisting UpdateReadyPayload out of check_for_updates

The inline UpdateReadyPayload definition right before emitting "update-ready" works, but moving it to a top‑level (desktop‑gated) struct would make the event contract easier to spot and reuse:

+#[cfg(desktop)]
+#[derive(Clone, serde::Serialize)]
+struct UpdateReadyPayload {
+    version: String,
+}
@@
-                            // Emit event to frontend for toast notification
-                            #[derive(Clone, serde::Serialize)]
-                            struct UpdateReadyPayload {
-                                version: String,
-                            }
-
-                            if let Err(e) = app_handle.emit("update-ready", UpdateReadyPayload {
-                                version: update.version.clone(),
-                            }) {
+                            // Emit event to frontend for toast notification
+                            if let Err(e) = app_handle.emit(
+                                "update-ready",
+                                UpdateReadyPayload {
+                                    version: update.version.clone(),
+                                },
+                            ) {

Purely a readability/maintainability improvement; behavior is unchanged.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6873907 and b8694a0.

📒 Files selected for processing (4)
  • frontend/src-tauri/src/lib.rs (3 hunks)
  • frontend/src/app.tsx (2 hunks)
  • frontend/src/components/GlobalNotification.tsx (2 hunks)
  • frontend/src/components/UpdateEventListener.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use 2-space indentation, double quotes, and a 100-character line limit for formatting
Use camelCase for variable and function names
Use try/catch with specific error types for error handling

Files:

  • frontend/src/components/GlobalNotification.tsx
  • frontend/src/components/UpdateEventListener.tsx
  • frontend/src/app.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript typing and avoid any when possible

Files:

  • frontend/src/components/GlobalNotification.tsx
  • frontend/src/components/UpdateEventListener.tsx
  • frontend/src/app.tsx
🧬 Code graph analysis (3)
frontend/src/components/GlobalNotification.tsx (2)
frontend/src/utils/utils.ts (1)
  • cn (8-10)
frontend/src/components/icons/X.tsx (1)
  • X (3-23)
frontend/src/components/UpdateEventListener.tsx (1)
frontend/src/contexts/NotificationContext.tsx (1)
  • useNotification (35-41)
frontend/src/app.tsx (1)
frontend/src/components/UpdateEventListener.tsx (1)
  • UpdateEventListener (11-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-linux
  • GitHub Check: build-android
  • GitHub Check: build-ios
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
frontend/src/app.tsx (1)

16-16: Good integration of UpdateEventListener in the app tree

UpdateEventListener is correctly imported and mounted alongside ProxyEventListener under NotificationProvider, so it shares the same context and runs once at the app root. No issues here.

Also applies to: 103-104

frontend/src-tauri/src/lib.rs (1)

8-13: Desktop‑only restart_for_update command wiring looks correct

The new restart_for_update command is gated behind #[cfg(desktop)] and registered only in the desktop builder’s invoke_handler, which lines up with the frontend’s Tauri‑guarded usage. The simple log + app_handle.restart() implementation is appropriate for this flow.

Also applies to: 39-48

Replace intrusive native dialog popup with a subtle toast notification that shows when an update is ready. The notification persists until user interaction with 'Later' and 'Restart Now' buttons.

- Extend GlobalNotification with action button support and persistent mode
- Create UpdateEventListener to handle update-ready events from backend
- Modify Rust backend to emit events instead of showing native dialog
- Add restart_for_update command for frontend-triggered restart
- Remove tauri_plugin_dialog dependency (no longer needed)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@AnthonyRonning AnthonyRonning force-pushed the feature/toast-update-notification branch from b8694a0 to 4af2d4b Compare December 2, 2025 23:12
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
frontend/src-tauri/src/lib.rs (1)

374-386: Consider moving UpdateReadyPayload to module level.

The inline struct definition works, but moving it near the other #[cfg(desktop)] items (around line 289) would improve discoverability and allow reuse if needed in the future.

+#[cfg(desktop)]
+#[derive(Clone, serde::Serialize)]
+struct UpdateReadyPayload {
+    version: String,
+}
+
 #[cfg(desktop)]
 static UPDATE_DOWNLOADED: AtomicBool = AtomicBool::new(false);

Then simplify the emit call:

-                            // Emit event to frontend for toast notification
-                            #[derive(Clone, serde::Serialize)]
-                            struct UpdateReadyPayload {
-                                version: String,
-                            }
-
-                            if let Err(e) = app_handle.emit("update-ready", UpdateReadyPayload {
+                            // Emit event to frontend for toast notification
+                            if let Err(e) = app_handle.emit("update-ready", UpdateReadyPayload {
                                 version: update.version.clone(),
                             }) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8694a0 and 4af2d4b.

📒 Files selected for processing (8)
  • frontend/src-tauri/Cargo.toml (0 hunks)
  • frontend/src-tauri/capabilities/default.json (0 hunks)
  • frontend/src-tauri/capabilities/mobile-android.json (0 hunks)
  • frontend/src-tauri/capabilities/mobile-ios.json (0 hunks)
  • frontend/src-tauri/src/lib.rs (3 hunks)
  • frontend/src/app.tsx (2 hunks)
  • frontend/src/components/GlobalNotification.tsx (2 hunks)
  • frontend/src/components/UpdateEventListener.tsx (1 hunks)
💤 Files with no reviewable changes (4)
  • frontend/src-tauri/capabilities/mobile-android.json
  • frontend/src-tauri/capabilities/mobile-ios.json
  • frontend/src-tauri/capabilities/default.json
  • frontend/src-tauri/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/GlobalNotification.tsx
  • frontend/src/components/UpdateEventListener.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use 2-space indentation, double quotes, and a 100-character line limit for formatting
Use camelCase for variable and function names
Use try/catch with specific error types for error handling

Files:

  • frontend/src/app.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript typing and avoid any when possible

Files:

  • frontend/src/app.tsx
🧠 Learnings (2)
📚 Learning: 2025-08-30T22:07:39.291Z
Learnt from: AnthonyRonning
Repo: OpenSecretCloud/Maple PR: 212
File: frontend/src/billing/billingApi.ts:652-674
Timestamp: 2025-08-30T22:07:39.291Z
Learning: In frontend/src/billing/billingApi.ts and similar TypeScript files, the team prefers to rely on TypeScript's type inference through function return type declarations rather than adding explicit type casts to response.json() calls. This reduces code verbosity while maintaining type safety.

Applied to files:

  • frontend/src/app.tsx
📚 Learning: 2025-03-25T19:50:07.925Z
Learnt from: AnthonyRonning
Repo: OpenSecretCloud/Maple PR: 23
File: frontend/src-tauri/src/lib.rs:14-16
Timestamp: 2025-03-25T19:50:07.925Z
Learning: In Tauri applications, updater security configurations (including endpoints, public key for verification, and dialog settings) should be defined in the tauri.conf.json file rather than duplicated in the code. When initialized with tauri_plugin_updater::Builder::new().build(), the plugin automatically reads and uses these settings from the configuration file.

Applied to files:

  • frontend/src-tauri/src/lib.rs
🧬 Code graph analysis (1)
frontend/src/app.tsx (1)
frontend/src/components/UpdateEventListener.tsx (1)
  • UpdateEventListener (11-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build-android
  • GitHub Check: build-ios
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
frontend/src/app.tsx (1)

16-16: UpdateEventListener import and placement look correct

Importing UpdateEventListener and rendering it alongside ProxyEventListener and DeepLinkHandler, inside NotificationProvider, is a clean way to host global side-effect listeners. This ensures useNotification context is available without affecting routing or other providers. No issues from this integration.

Also applies to: 103-103

frontend/src-tauri/src/lib.rs (2)

8-13: LGTM!

The command is correctly gated for desktop-only compilation and uses the proper Tauri API for triggering a restart. The logging provides good traceability for debugging.


46-46: LGTM!

The command is properly registered within the desktop-only configuration block.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@AnthonyRonning AnthonyRonning merged commit 5e48191 into master Dec 3, 2025
8 checks passed
@AnthonyRonning AnthonyRonning deleted the feature/toast-update-notification branch December 3, 2025 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant