close
Skip to content

[v2] Notifications API#4256

Merged
leaanthony merged 43 commits into
wailsapp:masterfrom
popaprozac:v2-notifications
Mar 15, 2026
Merged

[v2] Notifications API#4256
leaanthony merged 43 commits into
wailsapp:masterfrom
popaprozac:v2-notifications

Conversation

@popaprozac

@popaprozac popaprozac commented Apr 29, 2025

Copy link
Copy Markdown
Contributor

Description

Backports Notification API from v3-alpha.
Adds runtime functionality. No JS API at the moment, easy to add?
This includes a self-signing build step for dev and build.

I know this is a sizable addition so if we want to move forward with this I am happy to write docs etc

Fixes (#1788)

Type of change

Please select the option that is relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration using wails doctor.

  • Windows
  • macOS
  • Linux

If you checked Linux, please specify the distro and version.

Test Configuration

Please paste the output of wails doctor. If you are unable to run this command, please describe your environment in as much detail as possible.


          Wails Doctor



# Wails
Version  | v2.10.1
Revision | 1fbcd5f255fbed296e22fb51bde54438d442448c
Modified | true


# System
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
| OS           | MacOS                                                                                                                       |
| Version      | 15.4.1                                                                                                                      |
| ID           | 24E263                                                                                                                      |
| Branding     |                                                                                                                             |
| Go Version   | go1.24.1                                                                                                                    |
| Platform     | darwin                                                                                                                      |
| Architecture | arm64                                                                                                                       |
| CPU 1        | Apple M4 Max                                                                                                                |
| CPU 2        | Apple M4 Max                                                                                                                |
| GPU          | Chipset Model: Apple M4 Max Type: GPU Bus: Built-In Total Number of Cores: 32 Vendor: Apple (0x106b) Metal Support: Metal 3 |
| Memory       | 36GB                                                                                                                        |
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

# Dependencies
┌────────────────────────────────────────────────────────────────────────┐
| Dependency                | Package Name | Status    | Version         |
| Xcode command line tools  | N/A          | Installed | 2409            |
| Nodejs                    | N/A          | Installed | 22.14.0         |
| npm                       | N/A          | Installed | 10.9.2          |
| *Xcode                    | N/A          | Installed | 16.2 (16C5032a) |
| *upx                      | N/A          | Available |                 |
| *nsis                     | N/A          | Available |                 |
|                                                                        |
└─────────────────────── * - Optional Dependency ────────────────────────┘

# Diagnosis
Optional package(s) installation details:
  - upx : Available at https://upx.github.io/
  - nsis : More info at https://wails.io/docs/guides/windows-installer/

 SUCCESS  Your system is ready for Wails development!

Checklist:

  • I have updated website/src/pages/changelog.mdx with details of this PR
  • My code follows the general coding style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Cross-platform desktop notifications: initialize, availability/authorization checks, send basic and interactive notifications (actions & inline reply), manage categories, remove pending/delivered notifications, and receive user response callbacks. Public notification types and runtime APIs exposed across platforms.
  • Build

    • macOS builds include automatic self-signing for local builds.
  • Documentation

    • Added comprehensive guide and runtime reference for the Notifications API.
  • Chores

    • Added an indirect dependency to support notification functionality.

@coderabbitai

coderabbitai Bot commented Apr 29, 2025

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a cross‑platform desktop Notifications API: new runtime/frontend types and wrappers, native implementations for macOS (Obj‑C + cgo), Windows (toasts, registry/COM, icon helper), and Linux (D‑Bus), docs and changelog, plus a Darwin post‑build codesign and an indirect go-toast dependency.

Changes

Cohort / File(s) Summary
Frontend API & Runtime
v2/internal/frontend/frontend.go, v2/pkg/runtime/notifications.go
Adds notification types, result/callback types, and runtime/frontend wrappers exposing init/cleanup, availability/authorization checks, send (with actions), category management, removal APIs, and response registration.
System call dispatcher
v2/internal/frontend/dispatcher/systemcalls.go
Extends system call dispatch to handle notification lifecycle and management operations, unmarshalling args and invoking Frontend methods.
macOS Obj‑C Bridge
v2/internal/frontend/desktop/darwin/Application.h, v2/internal/frontend/desktop/darwin/Application.m, v2/internal/frontend/desktop/darwin/WailsContext.h, v2/internal/frontend/desktop/darwin/WailsContext.m
Adds C/Objective‑C declarations and implementations for notification availability, bundle check, delegate init, authorization, send (with actions/data), category register/remove, pending/delivered removal, UNUserNotificationCenterDelegate handlers, and extern callbacks bridging to Go.
macOS cgo frontend
v2/internal/frontend/desktop/darwin/notifications.go
cgo bridge implementing initialization/cleanup, availability/auth checks, send/register/remove/category APIs, exported C callbacks, channel-based async result handling with timeouts, and C string memory management.
Linux (D‑Bus)
v2/internal/frontend/desktop/linux/notifications.go
Adds org.freedesktop.Notifications integration with init/cleanup, send (with actions/hints), persistent category storage, DBus signal handling, notification maps, and response callback plumbing.
Windows Toasts & Helpers
v2/internal/frontend/desktop/windows/notifications.go, v2/internal/frontend/desktop/windows/winc/icon.go
Implements Windows toast flow: activation/COM/registry setup, payload encode/decode, action/reply handling, category persistence, response dispatch, and HICON→PNG helper with Win32 interop.
Runtime frontend JS wrappers
v2/internal/frontend/runtime/desktop/notifications.js, v2/internal/frontend/runtime/desktop/main.js, v2/internal/frontend/runtime/wrapper/runtime.js, v2/internal/frontend/runtime/wrapper/runtime.d.ts, v2/internal/frontend/runtime/runtime_prod_desktop.js
Adds desktop JS APIs that forward notification operations to native layer and exposes Notifications on window.runtime; updates runtime production mapping to include the module.
Build Pipeline (macOS codesign)
v2/pkg/commands/build/build.go
Adds Darwin-only post‑build self‑codesign step using codesign --force --deep --sign - on compiled binary.
Dependency
v2/go.mod
Adds indirect dependency git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3.
Docs & Changelog
website/docs/guides/notifications.mdx, website/docs/reference/runtime/notification.mdx, website/src/pages/changelog.mdx
Adds user guide and API reference for Notifications and updates changelog entry.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as App (Go)
  participant RT as Runtime wrapper
  participant FE as Frontend (platform)
  participant OS as OS Notification Center
  participant CB as Registered callback

  App->>RT: InitializeNotifications / SendNotification(options)
  RT->>FE: Forward call to platform frontend
  alt macOS
    FE->>OS: Request auth / Schedule UNNotificationRequest
  else Windows
    FE->>OS: Show toast via COM / Activation setup
  else Linux
    FE->>OS: Send via D‑Bus org.freedesktop.Notifications
  end
  OS-->>FE: Activation / Action / Dismiss / Signal
  FE->>RT: Normalize NotificationResult (async)
  RT->>CB: Invoke registered callback with NotificationResult
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

Documentation, runtime, Enhancement, Windows, MacOS, Linux, v2-only, lgtm

Suggested reviewers

  • leaanthony

Poem

🐰
I hopped through native code and crate,
Rang signals on the system gate.
Toasts and DBus, replies that sing,
Little paws make big bells ring.
Carrot cheers for every ping!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[v2] Notifications API' is clear, specific, and accurately summarizes the main feature being added to the v2 codebase.
Description check ✅ Passed The description provides a clear summary of changes, references the related issue (#1788), indicates the type of change (new feature), documents testing on all three platforms (Windows, macOS, Linux), and includes relevant test configuration output.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (8)
v2/internal/frontend/desktop/windows/winc/icon.go (2)

22-29: Lazy-loaded proc handles are fine but should be validated

Consider checking Load() errors for user32.dll/gdi32.dll so that a missing API (very old Windows) surfaces as a clear build-time panic instead of an undefined pointer call.


51-75: Minor: enum values already in w32

BI_RGB and DIB_RGB_COLORS are re-declared here. They already exist in w32. Re-using the central definition avoids drift.

v2/internal/frontend/desktop/darwin/Application.m (2)

390-399: Redundant NSString → UTF8 round-trip can be avoided

identifier, title, subtitle, body and data_json are already passed in as const char *.
Re-encoding them into NSString only to convert them straight back to char * incurs an unnecessary allocation and copies.
It also keeps an autoreleased NSString alive only long enough for the synchronous call, which is safe but brittle if the implementation ever becomes async.

-    NSString *_identifier = safeInit(identifier);
-    NSString *_title = safeInit(title);
-    NSString *_subtitle = safeInit(subtitle);
-    NSString *_body = safeInit(body);
-    NSString *_data_json = safeInit(data_json);
-
-    [ctx SendNotification:channelID :[_identifier UTF8String] :[_title UTF8String] :[_subtitle UTF8String] :[_body UTF8String] :[_data_json UTF8String]];
+    // Pass the original C strings straight through – WailsContext
+    // converts them to NSString internally.
+    [ctx SendNotification:channelID
+                     :identifier
+                     :title
+                     :subtitle
+                     :body
+                     :data_json];

401-411: Same round-trip issue for SendNotificationWithActions

The exact optimisation from the previous comment applies here as well.

-    NSString *_identifier = safeInit(identifier);
-    NSString *_title      = safeInit(title);
-    NSString *_subtitle   = safeInit(subtitle);
-    NSString *_body       = safeInit(body);
-    NSString *_categoryId = safeInit(categoryId);
-    NSString *_actions_json = safeInit(actions_json);
-
-    [ctx SendNotificationWithActions:channelID :[_identifier UTF8String] :[_title UTF8String] :[_subtitle UTF8String] :[_body UTF8String] :[_categoryId UTF8String] :[_actions_json UTF8String]];
+    [ctx SendNotificationWithActions:channelID
+                     :identifier
+                     :title
+                     :subtitle
+                     :body
+                     :categoryId
+                     :actions_json];
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

793-799: Duplicate onceToken shadows the global symbol

A file-scope static dispatch_once_t onceToken is declared at line 793 and a second
function-scope variable with the same name is redeclared inside EnsureDelegateInitialized
(lines 798-799).
The inner declaration hides the outer one, so the outer variable is effectively unused
(code smell / reader confusion).

Remove the global or rename one of them to make intent explicit.

- static dispatch_once_t onceToken;
...
-        static dispatch_once_t onceToken;
+ static dispatch_once_t notificationDelegateOnce;
...
+        static dispatch_once_t notificationDelegateOnce;
v2/pkg/runtime/notifications.go (1)

25-33: Variable shadowing the imported package name

Inside every helper you create a variable called frontend, masking the imported
package frontend.
While legal, this hurts readability and can trip up IDE imports.

-	frontend := getFrontend(ctx)
-	return frontend.InitializeNotifications()
+	fe := getFrontend(ctx)
+	return fe.InitializeNotifications()

Apply consistently to all helpers in this file.

v2/internal/frontend/desktop/darwin/notifications.go (1)

402-424: Close and recycle per-request channels to avoid leaks

registerChannel allocates an entry in the global channels map, but the channel is only removed (and never closed) in GetChannel.
For successful requests the receiver keeps the channel indefinitely, which slowly leaks memory.

After the sending goroutine finishes reading from resultCh, close it:

// Example after select { case result := <-resultCh: … }
-	return nil
+	close(resultCh)
+	return nil

and remove the redundant cleanupChannel helper (or use it consistently for both timeout and success paths).

v2/internal/frontend/desktop/windows/notifications.go (1)

180-188: Avoid iterating over an empty category when none is found

When the requested category is absent the code warns but still iterates over nCategory.Actions, which is the zero value slice.
While harmless, an early return keeps intent clearer:

if options.CategoryID == "" || !categoryExists {
    fmt.Printf("Category '%s' not found, sending basic notification without actions\n", options.CategoryID)
    return f.SendNotification(options)
}

Also applies to: 195-200

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1838f4 and fee2570.

⛔ Files ignored due to path filters (1)
  • v2/go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • v2/go.mod (1 hunks)
  • v2/internal/frontend/desktop/darwin/Application.h (1 hunks)
  • v2/internal/frontend/desktop/darwin/Application.m (1 hunks)
  • v2/internal/frontend/desktop/darwin/WailsContext.h (1 hunks)
  • v2/internal/frontend/desktop/darwin/WailsContext.m (4 hunks)
  • v2/internal/frontend/desktop/darwin/notifications.go (1 hunks)
  • v2/internal/frontend/desktop/linux/frontend.go (1 hunks)
  • v2/internal/frontend/desktop/linux/notifications.go (1 hunks)
  • v2/internal/frontend/desktop/windows/notifications.go (1 hunks)
  • v2/internal/frontend/desktop/windows/winc/icon.go (2 hunks)
  • v2/internal/frontend/frontend.go (2 hunks)
  • v2/pkg/commands/build/build.go (2 hunks)
  • v2/pkg/runtime/notifications.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
v2/pkg/runtime/notifications.go (2)
v2/internal/frontend/frontend.go (5)
  • NotificationOptions (80-87)
  • NotificationAction (90-94)
  • NotificationCategory (97-103)
  • NotificationResponse (106-115)
  • NotificationResult (119-122)
v2/internal/frontend/desktop/darwin/Application.h (11)
  • IsNotificationAvailable (73-73)
  • RequestNotificationAuthorization (76-76)
  • CheckNotificationAuthorization (77-77)
  • SendNotification (78-78)
  • SendNotificationWithActions (79-79)
  • RegisterNotificationCategory (80-80)
  • RemoveNotificationCategory (81-81)
  • RemoveAllPendingNotifications (82-82)
  • RemovePendingNotification (83-83)
  • RemoveAllDeliveredNotifications (84-84)
  • RemoveDeliveredNotification (85-85)
v2/internal/frontend/desktop/windows/winc/icon.go (1)
v2/internal/frontend/desktop/windows/winc/w32/typedef.go (1)
  • HBITMAP (191-191)
v2/internal/frontend/desktop/darwin/notifications.go (4)
v2/internal/frontend/frontend.go (5)
  • Frontend (124-203)
  • NotificationResult (119-122)
  • NotificationOptions (80-87)
  • NotificationCategory (97-103)
  • NotificationResponse (106-115)
v2/internal/frontend/desktop/darwin/Application.h (13)
  • IsNotificationAvailable (73-73)
  • EnsureDelegateInitialized (75-75)
  • CheckBundleIdentifier (74-74)
  • RequestNotificationAuthorization (76-76)
  • CheckNotificationAuthorization (77-77)
  • SendNotification (78-78)
  • SendNotificationWithActions (79-79)
  • RegisterNotificationCategory (80-80)
  • RemoveNotificationCategory (81-81)
  • RemoveAllPendingNotifications (82-82)
  • RemovePendingNotification (83-83)
  • RemoveAllDeliveredNotifications (84-84)
  • RemoveDeliveredNotification (85-85)
v2/internal/frontend/desktop/windows/notifications.go (1)
  • DefaultActionIdentifier (37-37)
v2/internal/frontend/desktop/linux/notifications.go (1)
  • DefaultActionIdentifier (44-44)
🔇 Additional comments (12)
v2/go.mod (1)

54-54: New Windows-toast dependency looks good

go-toast/v2 v2.0.3 is the latest stable tag and is pulled in indirectly, which is correct because the package is only referenced from the Windows implementation.
No further action required.

v2/internal/frontend/desktop/linux/frontend.go (1)

7-7: Whitespace fix prevents CGO mis-parsing

Removing the trailing space from the #cgo directive eliminates a subtle compile warning/error on some tool-chains. Good catch.

v2/internal/frontend/desktop/darwin/Application.h (2)

72-86: Unify bool return-value style with existing API

Earlier query functions (e.g. IsFullScreen, IsMinimised) return const bool, but the newly-added notification functions return bool.
Mixing the two styles will eventually confuse users of the C interface and can trigger C++ linkage warnings.

-bool IsNotificationAvailable(void *inctx);
-
-bool EnsureDelegateInitialized(void *inctx);
+const bool IsNotificationAvailable(void *inctx);
+
+const bool EnsureDelegateInitialized(void *inctx);

Same for the other boolean return types in this block.
[ suggest_nitpick ]


78-80: Parameter order inconsistency may bite the cgo layer

The two “send” helpers differ only by the extra categoryId and actions_json parameters, but the new parameters are inserted before data_json, shifting the tail of the signature.
If any code paths conditionally call one variant over the other through unsafe.Pointer/syscall.NewCallback, a silent mismatch becomes a runtime memory-smash.

Consider re-ordering so that the common tail (data_json) is always last:

-void SendNotificationWithActions(void *inctx, … const char *body, const char *categoryId, const char *actions_json);
+void SendNotificationWithActions(void *inctx, … const char *categoryId, const char *actions_json, const char *body);

Or introduce a distinct name such as SendNotificationWithActionsAndData to make the difference obvious.
[ flag_critical_issue ]

v2/internal/frontend/frontend.go (4)

79-87: NotificationOptions.ID lacks “omitempty” – may break marshalled payloads

All other optional fields in NotificationOptions carry omitempty, but ID does not.
If callers omit it intentionally, the JSON payload will still include "id":"", which some platforms treat as non-empty and may reject.

-	ID         string                 `json:"id"`
+	ID         string                 `json:"id,omitempty"`

[ flag_critical_issue ]


98-103: JSON tag drift: categoryIdentifier vs categoryId

NotificationResponse.CategoryID uses the tag categoryIdentifier, whereas everywhere else the camel-cased form is categoryId.
Apart from being inconsistent, a Go ↔ JS round-trip will lose data unless the client is aware of both spellings.

-	CategoryID       string                 `json:"categoryIdentifier,omitempty"`
+	CategoryID       string                 `json:"categoryId,omitempty"`

[ suggest_essential_refactor ]


189-203: No way to detach OnNotificationResponse callback – memory leak / race risk

The interface lets clients register a callback but never unregister it.
Long-running apps that hot-reload or re-initialise front-ends will accumulate dangling closures, and concurrent writes to the stored function can race.

Suggestion:

  1. Return an opaque token (e.g. func()) that removes the handler.
  2. Document that passing nil replaces & clears the existing one.
-OnNotificationResponse(callback func(result NotificationResult))
+OnNotificationResponse(callback func(result NotificationResult)) (dispose func())

[ suggest_essential_refactor ]


199-203: RemoveNotification duplicates existing concepts – clarify or drop

Both RemovePendingNotification and RemoveDeliveredNotification exist, yet an additional generic RemoveNotification is declared with no semantics stated.
Unless it is meant to be cross-platform sugar, keep only the explicit variants to avoid ambiguity.

[ request_verification ]

v2/internal/frontend/desktop/darwin/WailsContext.h (3)

95-95: Great catch – method now correctly declared as Objective-C instance method
No further action needed.
[ approve_code_changes ]


99-112: Delegate initialisation race: expose idempotent guard or document contract

EnsureDelegateInitialized suggests callers must invoke it before any other notification API, yet nothing enforces this ordering from the Go side.
If two goroutines attempt to send the very first notification concurrently, the internal UNUserNotificationCenter delegate setup could run twice.

Either:

  1. Make the call idempotent and safe internally (preferred), or
  2. Document clearly that InitializeNotifications() must precede everything else.

[ offer_architecture_advice ]


104-107: UTF-8 expectation of const char *…JSON parameters is undocumented

These methods accept raw UTF-8 C strings meant to carry JSON blobs; passing non-UTF-8 or overly long data will break -[NSString stringWithUTF8String:] and return nil, leading to undefined behaviour later.

Add explicit comment or runtime check that returns an error back through the callback path when JSON decoding fails.
[ suggest_optional_refactor ]

v2/internal/frontend/desktop/darwin/WailsContext.m (1)

1010-1057: RemoveNotificationCategory callback executed off main thread

UNUserNotificationCenter invokes the completion handler on an internal queue.
captureResult ultimately crosses the C-go boundary – ensure it is thread-safe or
dispatch back to the main queue to match the rest of the runtime callbacks.

    [center getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
        dispatch_async(dispatch_get_main_queue(), ^{
            /* existing logic + captureResult */
        });
    }];

Comment thread v2/pkg/commands/build/build.go Outdated
Comment thread v2/internal/frontend/desktop/windows/winc/icon.go Outdated
Comment thread v2/internal/frontend/desktop/windows/winc/icon.go Outdated
Comment thread v2/internal/frontend/desktop/darwin/WailsContext.m Outdated
Comment thread v2/pkg/runtime/notifications.go
Comment thread v2/internal/frontend/desktop/linux/notifications.go
Comment thread v2/internal/frontend/desktop/linux/notifications.go Outdated
Comment thread v2/internal/frontend/desktop/windows/notifications.go
Comment thread v2/internal/frontend/desktop/windows/notifications.go
Comment thread v2/internal/frontend/desktop/windows/notifications.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

847-858: Nil-safety for required fields

title and body are required by the macOS API, but stringWithUTF8String: will crash if a NULL pointer slips through.
Defensive guards make the bridge more robust to future changes in the Go layer.

-    NSString *nsTitle = [NSString stringWithUTF8String:title];
+    if (title == NULL)  title  = "";
+    if (body  == NULL)  body   = "";
+
+    NSString *nsTitle = [NSString stringWithUTF8String:title];
v2/internal/frontend/desktop/windows/notifications.go (1)

299-301: ⚠️ Potential issue

saveIconToDir() is still a no-op – Windows toasts will silently fall back to a generic icon

The unresolved stub was pointed out in the previous review and is still present. Without writing the icon file, go-toast cannot embed the application icon in notifications, resulting in a degraded UX.

Proposed implementation (imports and handle‐leak fixes included):

@@
-import (
+import (
   "encoding/base64"
   "encoding/json"
   "runtime"
   "sync"
+  "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
+  "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
 )
@@
 func (f *Frontend) saveIconToDir() error {
-    return nil
+    // Fast-path: file already exists
+    if _, err := os.Stat(iconPath); err == nil {
+        return nil
+    }
+
+    hMod := w32.GetModuleHandle("")
+    if hMod == 0 {
+        return fmt.Errorf("GetModuleHandle failed: %w", syscall.GetLastError())
+    }
+
+    icon, err := winc.NewIconFromResource(hMod, uint16(3)) // 3 is conventional for app icon
+    if err != nil {
+        return fmt.Errorf("failed to retrieve application icon: %w", err)
+    }
+    defer icon.Destroy()
+
+    if err := winc.SaveHIconAsPNG(icon.Handle(), iconPath); err != nil {
+        return fmt.Errorf("failed to save icon as PNG: %w", err)
+    }
+    return nil
 }
🧹 Nitpick comments (2)
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

876-911: Consider refactoring notification sending methods to reduce duplication.

The SendNotification and SendNotificationWithActions methods contain significant duplicate code. Consider extracting the common logic into a shared private helper method to improve maintainability.

+- (void) sendNotificationWithRequest:(UNNotificationRequest *)request channelID:(int)channelID API_AVAILABLE(macos(10.14)) {
+    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+    [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+        if (error) {
+            NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
+            captureResult(channelID, false, [errorMsg UTF8String]);
+        } else {
+            captureResult(channelID, true, NULL);
+        }
+    }];
+}

Then update the sending methods to use this helper:

 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier
                                                                       content:content
                                                                       trigger:trigger];
-    
-[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
-    if (error) {
-        NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
-        captureResult(channelID, false, [errorMsg UTF8String]);
-    } else {
-        captureResult(channelID, true, NULL);
-    }
-}];
+[self sendNotificationWithRequest:request channelID:channelID];

Also applies to: 913-951

v2/internal/frontend/desktop/windows/notifications.go (1)

150-152: Replace scattered fmt.Printf diagnostics with structured logging

Direct fmt.Printf writes go to stdout and are easy to miss or spam CI logs. Consider the standard log package (or the project’s logger) with severity levels so messages can be filtered and redirected.

Also applies to: 176-178, 185-186, 409-410

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fee2570 and 35eff5c.

📒 Files selected for processing (6)
  • v2/internal/frontend/desktop/darwin/Application.m (1 hunks)
  • v2/internal/frontend/desktop/darwin/WailsContext.m (4 hunks)
  • v2/internal/frontend/desktop/linux/notifications.go (1 hunks)
  • v2/internal/frontend/desktop/windows/notifications.go (1 hunks)
  • v2/pkg/commands/build/build.go (2 hunks)
  • v2/pkg/runtime/notifications.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • v2/internal/frontend/desktop/darwin/Application.m
🚧 Files skipped from review as they are similar to previous changes (3)
  • v2/pkg/commands/build/build.go
  • v2/internal/frontend/desktop/linux/notifications.go
  • v2/pkg/runtime/notifications.go
🔇 Additional comments (9)
v2/internal/frontend/desktop/darwin/WailsContext.m (8)

40-47: Clean import and declaration setup for notification support.

The code correctly adds conditional imports for UserNotifications framework with macOS version checks and declares the necessary external functions for callback handlers.


721-735: Good platform compatibility and environment checks.

The code appropriately checks for both macOS version compatibility (10.14+) and valid bundle identifier, which are both requirements for the notifications API to function properly.


737-791: Well-implemented notification delegate methods.

The UNUserNotificationCenterDelegate protocol implementation is thorough, with proper handling of notification presentation and user responses. The response payload construction captures all relevant notification data and includes proper error handling for JSON serialization failures.


793-804: Thread-safe delegate initialization.

Using dispatch_once for notification center delegate setup ensures thread safety and prevents multiple initializations, which is an excellent practice.


805-845: Comprehensive authorization handling.

The authorization methods include robust error propagation and appropriate version checking, ensuring failures are properly communicated back to the Go layer.


953-1029: Comprehensive notification category handling.

The category registration implementation properly handles action settings, including text input fields, destructive actions, and deals with updating existing categories. The code shows good attention to detail with effective error handling.


1031-1078: Clean notification management methods.

The notification removal methods provide a complete API surface for managing both pending and delivered notifications, with both individual and bulk removal options.


1089-1089: Minor spacing correction in About method declaration.

Small formatting improvement for consistency.

v2/internal/frontend/desktop/windows/notifications.go (1)

354-360: Shadowing bug fixed – categories now deserialise correctly

Good catch renaming the temporary map and assigning it back; this removes the previous logic error. 👍

Comment thread v2/internal/frontend/desktop/windows/notifications.go Outdated
Comment thread v2/internal/frontend/desktop/windows/notifications.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

847-851: Nil-safety for required fields (duplicate of prior feedback)

title and body may arrive as NULL; stringWithUTF8String: will crash in that case.
Add guards similar to what you already do for subtitle.

🧹 Nitpick comments (1)
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

740-748: Provide a sensible presentation fallback for macOS < 11

options remains 0 when the system is 10.14 – 10.15, meaning the notification is silently dropped.
Adding UNNotificationPresentationOptionAlert (and optionally Sound) for these versions preserves user-visible behaviour.

-    UNNotificationPresentationOptions options = 0;
+    UNNotificationPresentationOptions options = UNNotificationPresentationOptionAlert |
+                                                UNNotificationPresentationOptionSound;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ed2cd5 and e7cbd46.

📒 Files selected for processing (1)
  • v2/internal/frontend/desktop/darwin/WailsContext.m (4 hunks)
🔇 Additional comments (1)
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

40-47: Guard usages of UN* APIs with the same compile-time check used for the import

UNUserNotificationCenter, UNNotification*, etc. are referenced throughout the file without #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 guards.
When building on an SDK < 10.14 this will still fail at compile-time even though the runtime @available checks are present.

Consider wrapping the entire notification section (declarations, protocol conformance, and method bodies) in the same macro guard so the file compiles on older SDKs.

Would you run an older macOS SDK build (or CI job) to prove the compilation path is still green?

Comment thread v2/internal/frontend/desktop/darwin/WailsContext.m Outdated
Comment thread v2/internal/frontend/desktop/darwin/WailsContext.m
@popaprozac

Copy link
Copy Markdown
Contributor Author

Example app.go:

package main

import (
	"context"
	"fmt"

	"github.com/wailsapp/wails/v2/pkg/runtime"
)

// App struct
type App struct {
	ctx context.Context
}

// NewApp creates a new App application struct
func NewApp() *App {
	return &App{}
}

// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
	a.ctx = ctx

	var err error
	err = runtime.InitializeNotifications(ctx)
	if err != nil {
		fmt.Println("Error initializing notifications:", err)
		return
	}

	runtime.OnNotificationResponse(ctx, func(result runtime.NotificationResult) {
		fmt.Printf("Notification response: %v\n", result)
		runtime.EventsEmit(ctx, "notification:response", result)
	})

	authorized, err := runtime.RequestNotificationAuthorization(ctx)
	if err != nil {
		fmt.Println("Error requesting notification authorization:", err)
		return
	}
	if authorized {
		fmt.Println("Notification authorization granted")
	} else {
		fmt.Println("Notification authorization denied")
	}

	runtime.SendNotification(ctx, runtime.NotificationOptions{
		ID:       "notification-id",
		Title:    "Notification Title",
		Subtitle: "Notification Subtitle",
		Body:     "Notification Body",
		Data: map[string]interface{}{
			"user-id": "12345",
		},
	})
}

// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
	return fmt.Sprintf("Hello %s, It's show time!", name)
}

Listen to the event on the frontend and 👌

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
v2/internal/frontend/desktop/windows/winc/icon.go (1)

197-211: Optimize the pixel conversion loop.

The current pixel conversion code is correct but could be optimized by directly accessing the RGBA image's pixel data through its Pix field rather than using Set() for each pixel.

	// Convert DIB to RGBA
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			// DIB is bottom-up, so we need to invert Y
			dibIndex := ((height-1-y)*width + x) * 4
+			// RGBA image is top-down
+			imgIndex := (y*width + x) * 4

			// BGRA to RGBA
-			b := bits[dibIndex]
-			g := bits[dibIndex+1]
-			r := bits[dibIndex+2]
-			a := bits[dibIndex+3]
-
-			// Set pixel in the image
-			img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a})
+			img.Pix[imgIndex] = bits[dibIndex+2]     // R
+			img.Pix[imgIndex+1] = bits[dibIndex+1]   // G
+			img.Pix[imgIndex+2] = bits[dibIndex]     // B
+			img.Pix[imgIndex+3] = bits[dibIndex+3]   // A
		}
	}

This approach is significantly faster as it avoids the overhead of the Set() method call and color struct creation for each pixel.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7cbd46 and ebfb139.

📒 Files selected for processing (3)
  • v2/internal/frontend/desktop/linux/notifications.go (1 hunks)
  • v2/internal/frontend/desktop/windows/notifications.go (1 hunks)
  • v2/internal/frontend/desktop/windows/winc/icon.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • v2/internal/frontend/desktop/linux/notifications.go
  • v2/internal/frontend/desktop/windows/notifications.go
🔇 Additional comments (1)
v2/internal/frontend/desktop/windows/winc/icon.go (1)

213-222: LGTM: File handling is well implemented

The file creation, error handling, and PNG encoding are implemented correctly with proper resource cleanup.

Comment thread v2/internal/frontend/desktop/windows/winc/icon.go Outdated
Comment thread v2/internal/frontend/desktop/windows/winc/icon.go
Comment thread v2/internal/frontend/desktop/windows/winc/icon.go
Comment thread v2/internal/frontend/desktop/windows/winc/icon.go Outdated
Comment thread v2/internal/frontend/desktop/windows/winc/icon.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

844-847: ⚠️ Potential issue

Add null safety checks for required fields

The title and body parameters are required for notification content, but stringWithUTF8String: will crash if passed NULL pointers. While subtitle has a null check, these other required fields don't.

Add defensive checks to prevent crashes:

-    NSString *nsTitle = [NSString stringWithUTF8String:title];
-    NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
-    NSString *nsBody = [NSString stringWithUTF8String:body];
+    if (title == NULL) title = "";
+    if (body == NULL) body = "";
+    
+    NSString *nsTitle = [NSString stringWithUTF8String:title];
+    NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
+    NSString *nsBody = [NSString stringWithUTF8String:body];
🧹 Nitpick comments (1)
v2/internal/frontend/desktop/darwin/WailsContext.m (1)

938-1014: Consider batch handling for category operations

The category registration performs multiple operations (retrieving categories, modifying the set, setting categories) that could cause race conditions if multiple registration requests occur simultaneously.

Consider using a serialized queue or a more atomic approach when modifying notification categories to prevent potential race conditions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebfb139 and 31a6d03.

📒 Files selected for processing (2)
  • v2/internal/frontend/desktop/darwin/WailsContext.m (4 hunks)
  • v2/internal/frontend/desktop/windows/winc/icon.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/internal/frontend/desktop/windows/winc/icon.go
🔇 Additional comments (6)
v2/internal/frontend/desktop/darwin/WailsContext.m (6)

849-849: Good fix: Memory leak resolved with autorelease

The implementation correctly uses autorelease to prevent memory leaks in a manual reference counting environment.


793-800: Good fix: Delegate initialization improved

The implementation correctly sets the notification center delegate directly rather than using dispatch_once, avoiding potential dangling pointer issues if the context is deallocated and recreated.


737-750: Good implementation of notification presentation options

The code correctly handles different presentation options based on macOS version, using appropriate availability checks for macOS 11.0+ specific features.


752-791: Well-implemented notification response handling

The notification response handling is thorough and includes proper error handling for JSON serialization, along with comprehensive payload construction that captures all relevant notification data.


802-824: Thorough authorization implementation with proper error handling

The notification authorization request implementation includes appropriate version checks, delegate verification, and comprehensive error handling in the completion handler.


721-1063: Overall well-implemented notification system

The notification implementation provides a comprehensive cross-platform bridge for macOS notifications with proper availability checks, error handling, and memory management throughout.

@leaanthony

Copy link
Copy Markdown
Member

@popaprozac - Could we hardcode the path for this command? cmd := exec.Command("codesign", "--force", "--deep", "--sign", "-", options.CompiledBinary)?

@popaprozac

Copy link
Copy Markdown
Contributor Author

@popaprozac - Could we hardcode the path for this command? cmd := exec.Command("codesign", "--force", "--deep", "--sign", "-", options.CompiledBinary)?

/usr/bin/codesign? Sure and good call

@sonarqubecloud

sonarqubecloud Bot commented May 1, 2025

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
Image 3.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@popaprozac

popaprozac commented Jul 19, 2025

Copy link
Copy Markdown
Contributor Author

@leaanthony I fear our window for this came and went. Was so looking forward to closing #1788 😅. Should I go ahead and close?

@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 19, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@v2/internal/frontend/desktop/windows/notifications.go`:
- Around line 74-84: The registry key returned by registry.CreateKey (variable
key) is not closed if key.SetStringValue fails; change the code in the block
that creates the CLSID key to call defer key.Close() immediately after
successful creation of key so the key is always closed on any early return
(remove the later manual key.Close() call), i.e., add defer key.Close() right
after the registry.CreateKey call in the notifications.go code that handles the
CLSID LocalServer32 setup.
- Around line 437-450: handleNotificationResult currently takes a write lock and
invokes notificationResultCallback even when it's nil (causing a recovered panic
each time); change callbackLock.Lock/Unlock to RLock/RUnlock and read
notificationResultCallback into a local variable, then if the local callback is
nil simply return without starting the goroutine or relying on recover;
reference the symbols handleNotificationResult, notificationResultCallback, and
callbackLock and ensure the nil check happens before spawning the goroutine so
only non-nil callbacks are invoked.
🧹 Nitpick comments (2)
v2/internal/frontend/desktop/windows/notifications.go (2)

165-184: Errors and warnings written to stdout instead of stderr.

Lines 167, 192, and 200 use fmt.Printf for error/warning messages, while Line 445 correctly uses fmt.Fprintf(os.Stderr, ...). Diagnostic output should go to stderr consistently.


254-260: Unnecessary type conversion on Line 257.

category.HasReplyField is already bool; the bool(...) cast is redundant.

Proposed fix
-		HasReplyField:    bool(category.HasReplyField),
+		HasReplyField:    category.HasReplyField,

Comment thread v2/internal/frontend/desktop/windows/notifications.go Outdated
Comment thread v2/internal/frontend/desktop/windows/notifications.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@v2/internal/frontend/desktop/windows/notifications.go`:
- Around line 312-322: In saveIconToDir, ensure the HICON from w32.ExtractIcon
is released by adding a defer w32.DestroyIcon(hIcon) after verifying hIcon != 0
inside the iconOnce.Do closure; keep the current flow that calls
winc.SaveHIconAsPNG(hIcon, iconPath) and sets iconErr, but guarantee DestroyIcon
runs regardless of save success so the GDI handle (hIcon) is not leaked while
still using iconOnce, exePath, iconPath and iconErr as currently implemented.
🧹 Nitpick comments (3)
v2/internal/frontend/desktop/windows/notifications.go (3)

6-24: Import groups are split non-idiomatically.

Standard library imports (fmt, os, path/filepath, unsafe) are split across two groups with external packages in between. Go convention is a single stdlib group followed by a single external group.

♻️ Suggested grouping
 import (
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
 	"sync"
+	_ "unsafe" // for go:linkname
 
 	wintoast "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
 	"github.com/google/uuid"
 	"github.com/wailsapp/wails/v2/internal/frontend"
 	"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
 	"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
-
-	"fmt"
-	"os"
-	"path/filepath"
-	_ "unsafe" // for go:linkname
-
 	"git.sr.ht/~jackmordaunt/go-toast/v2"
 	"golang.org/x/sys/windows/registry"
 )

165-168: Diagnostic output goes to stdout via fmt.Printf — prefer stderr or a logger.

Lines 167, 192, 200, and 430 use fmt.Printf for error/warning messages. These will intermingle with normal application stdout. At minimum, use fmt.Fprintf(os.Stderr, ...) for consistency with the panic logging on Line 449.

Also applies to: 190-193, 199-201, 429-431


254-260: Nit: redundant bool() cast on Line 257.

category.HasReplyField is already bool; the conversion is a no-op.

-		HasReplyField:    bool(category.HasReplyField),
+		HasReplyField:    category.HasReplyField,

Comment thread v2/internal/frontend/desktop/windows/notifications.go
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@popaprozac

Copy link
Copy Markdown
Contributor Author

@leaanthony I think we are in a good spot. Tested this on macOS, Windows, and Linux and it looks to be working as expected.

@popaprozac

Copy link
Copy Markdown
Contributor Author

I have a local version working with JS api wired up. Is that something we want to include?

@leaanthony

Copy link
Copy Markdown
Member

I have a local version working with JS api wired up. Is that something we want to include?

That would be amazing, HOWEVER, I just seen how long this PR has been running so if it's a reasonably small change I think it'd be worth it?

@popaprozac

Copy link
Copy Markdown
Contributor Author

@leaanthony added the JS stuff and had Claude help out with updating the docs/comments around it. Did a few passes, tested (quickly) on all 3 OSes, and I think we are good but would love another set of eyes

@leaanthony

Copy link
Copy Markdown
Member

Amazing 😍 Did you address the Rabbit's comments? I'm not sure if the conversation gets resolved automatically or if the mobile GitHub app even works properly with them 😩

@popaprozac

Copy link
Copy Markdown
Contributor Author

I just did a second pass and tidied up the last that I could see of the nitpicks. Let me test again on the diff platforms when I get a chance

@popaprozac

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@popaprozac

popaprozac commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

@leaanthony tested the JS api on the 3 platforms and everything looks good to go on my end 👍

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@v2/internal/frontend/desktop/windows/winc/icon.go`:
- Around line 155-188: The code currently selects iconInfo.HbmColor into the
compatible DC (hdc) via procSelectObject and then calls procGetDIBits with that
same DC, which violates the Win32 requirement that the bitmap passed to
GetDIBits not be selected into a DC; change the flow to obtain a screen DC using
w32.GetDC(0) (and later release it with w32.ReleaseDC), remove/selecting the
bitmap into the compatible DC (remove the procSelectObject/select and its
defer), and call procGetDIBits using the screen DC instead of hdc (keep using
iconInfo.HbmColor, procGetDIBits, and BITMAPINFO setup as-is); ensure the screen
DC is released in a defer to avoid leaks.

In `@website/docs/reference/runtime/notification.mdx`:
- Around line 354-361: NotificationOptions and NotificationResponse use
inconsistent JSON keys for the same field (NotificationOptions has `categoryId`
while NotificationResponse has `categoryIdentifier`); update the struct tags to
be consistent (pick one canonical JSON key and apply it to both structs) or, if
the mismatch is intentional, add a clear comment in both structs
(NotificationOptions and NotificationResponse) documenting the reason for the
naming difference and the mapping clients should expect so JS/TS consumers
aren't confused.
🧹 Nitpick comments (5)
v2/internal/frontend/desktop/windows/winc/icon.go (1)

209-218: Deferred Close may silently lose write errors.

If png.Encode succeeds but outFile.Close() fails (e.g., OS-level flush failure), the error is lost. Consider capturing the close error:

Proposed fix
-	// Create output file
-	outFile, err := os.Create(filePath)
-	if err != nil {
-		return err
-	}
-	defer outFile.Close()
-
-	// Encode and save the image
-	return png.Encode(outFile, img)
+	// Create output file
+	outFile, err := os.Create(filePath)
+	if err != nil {
+		return err
+	}
+
+	// Encode and save the image
+	if err := png.Encode(outFile, img); err != nil {
+		outFile.Close()
+		return err
+	}
+	return outFile.Close()
v2/internal/frontend/desktop/windows/notifications.go (1)

27-39: Consider documenting thread-safety expectations for the global state.

The global variables (categories, appName, appGUID, iconPath, exePath) are written during InitializeNotifications and read during send/register operations. The categories map is protected by categoriesLock, but the string variables (appName, appGUID, iconPath, exePath) are only written during init and then read — this is safe assuming InitializeNotifications is called once at startup before any concurrent sends. A brief comment noting this "write-once at init" invariant would prevent future confusion.

v2/internal/frontend/desktop/linux/notifications.go (3)

527-543: Use RLock instead of Lock when only reading the callback.

handleNotificationResult only reads notificationResultCallback — it never writes to it. Using a write lock here unnecessarily blocks concurrent notification result delivery.

♻️ Proposed fix
 func handleNotificationResult(result frontend.NotificationResult) {
-	callbackLock.Lock()
+	callbackLock.RLock()
 	callback := notificationResultCallback
-	callbackLock.Unlock()
+	callbackLock.RUnlock()
 
 	if callback != nil {

129-134: Silently swallowed json.Marshal error for user data.

If json.Marshal(options.Data) fails, the notification is sent without the user's data and no error or log entry is produced. The same pattern appears in SendNotificationWithActions (lines 215-220). Consider at least logging the error so developers can diagnose missing data.

♻️ Proposed fix (apply similarly at line 217)
 	if options.Data != nil {
 		userData, err := json.Marshal(options.Data)
-		if err == nil {
-			hints["x-user-data"] = dbus.MakeVariant(string(userData))
+		if err != nil {
+			f.logger.Warning("Failed to marshal notification user data: %v", err)
+		} else {
+			hints["x-user-data"] = dbus.MakeVariant(string(userData))
 		}
 	}

78-89: Consider clearing notifications map on cleanup to avoid stale entries on re-init.

After CleanupNotifications, stale entries in the notifications map reference D-Bus IDs from the old connection. If InitializeNotifications is called again, a new D-Bus connection may reuse IDs, leading to incorrect response data. Clearing the map during cleanup avoids this edge case.

♻️ Proposed fix
 func (f *Frontend) CleanupNotifications() {
 	if cancel != nil {
 		cancel()
 		cancel = nil
 	}
 
 	if conn != nil {
 		conn.Close()
 		conn = nil
 	}
+
+	notificationsLock.Lock()
+	notifications = make(map[uint32]*notificationData)
+	notificationsLock.Unlock()
 }

Comment thread v2/internal/frontend/desktop/windows/winc/icon.go Outdated
Comment thread website/docs/reference/runtime/notification.mdx
@popaprozac

Copy link
Copy Markdown
Contributor Author

@leaanthony anything we need here?

@leaanthony
leaanthony enabled auto-merge (squash) March 15, 2026 02:36
@leaanthony
leaanthony merged commit 51d3032 into wailsapp:master Mar 15, 2026
18 of 22 checks passed
@leaanthony

Copy link
Copy Markdown
Member

All good! Thanks for your patience!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants