diff --git a/v3/pkg/services/notifications/notifications.go b/v3/pkg/services/notifications/notifications.go index a40177af2..d971959ac 100644 --- a/v3/pkg/services/notifications/notifications.go +++ b/v3/pkg/services/notifications/notifications.go @@ -1,12 +1,17 @@ package notifications +import "sync" + // Service represents the notifications service type Service struct { - // Callback is called when a notification response is received - Callback func(response NotificationResponse) + // notificationResponseCallback is called when a notification response is received + notificationResponseCallback func(response NotificationResponse) + + callbackLock sync.RWMutex } var NotificationService *Service +var notificationServiceLock sync.RWMutex // NotificationAction represents an action button for a notification type NotificationAction = struct { @@ -54,18 +59,22 @@ func (ns *Service) ServiceName() string { } // OnNotificationResponse registers a callback function that will be called when -// a notification response is received from the user +// a notification response is received from the user. func (ns *Service) OnNotificationResponse(callback func(response NotificationResponse)) { - if ns.Callback != nil { - return - } - ns.Callback = callback + ns.callbackLock.Lock() + defer ns.callbackLock.Unlock() + + ns.notificationResponseCallback = callback } // handleNotificationResponse is an internal method to handle notification responses // and invoke the registered callback if one exists func (ns *Service) handleNotificationResponse(response NotificationResponse) { - if ns.Callback != nil { - ns.Callback(response) + ns.callbackLock.RLock() + callback := ns.notificationResponseCallback + ns.callbackLock.RUnlock() + + if callback != nil { + callback(response) } } diff --git a/v3/pkg/services/notifications/notifications_darwin.go b/v3/pkg/services/notifications/notifications_darwin.go index 81b6364c4..2781ceb7c 100644 --- a/v3/pkg/services/notifications/notifications_darwin.go +++ b/v3/pkg/services/notifications/notifications_darwin.go @@ -12,16 +12,32 @@ import ( "context" "encoding/json" "fmt" + "sync" "unsafe" "github.com/wailsapp/wails/v3/pkg/application" ) -var AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier" +type notificationChannel struct { + authorized bool + err error +} + +var ( + notificationChannels = make(map[int]chan notificationChannel) + notificationChannelsLock sync.Mutex + nextChannelID int +) + +const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier" +const BundleIdentifierError = fmt.Errorf("notifications require a bundled application with a unique bundle identifier") // Creates a new Notifications Service. // Your app must be packaged and signed for this feature to work. func New() *Service { + notificationServiceLock.Lock() + defer notificationServiceLock.Unlock() + if NotificationService == nil { NotificationService = &Service{} } @@ -31,7 +47,7 @@ func New() *Service { // ServiceStartup is called when the service is loaded. func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { if !CheckBundleIdentifier() { - return fmt.Errorf("notifications require a bundled application with a unique bundle identifier") + return BundleIdentifierError } return nil } @@ -45,21 +61,32 @@ func CheckBundleIdentifier() bool { return bool(C.checkBundleIdentifier()) } -// RequestUserNotificationAuthorization requests permission for notifications. -func (ns *Service) RequestUserNotificationAuthorization() (bool, error) { +// RequestNotificationAuthorization requests permission for notifications. +func (ns *Service) RequestNotificationAuthorization() (bool, err) { if !CheckBundleIdentifier() { - return false, fmt.Errorf("notifications require a bundled application with a unique bundle identifier") + return false, BundleIdentifierError } - result := C.requestUserNotificationAuthorization(nil) - return result == true, nil + + id, resultCh := registerChannel() + + C.requestNotificationAuthorization(C.int(id)) + + result := <-resultCh + return result.authorized, result.err } // CheckNotificationAuthorization checks current notification permission status. -func (ns *Service) CheckNotificationAuthorization() (bool, error) { +func (ns *Service) CheckNotificationAuthorization() (bool, err) { if !CheckBundleIdentifier() { - return false, fmt.Errorf("notifications require a bundled application with a unique bundle identifier") + return false, BundleIdentifierError } - return bool(C.checkNotificationAuthorization()), nil + + id, resultCh := registerChannel() + + C.checkNotificationAuthorization(C.int(id)) + + result := <-resultCh + return result.authorized, result.err } // SendNotification sends a basic notification with a unique identifier, title, subtitle, and body. @@ -212,9 +239,48 @@ func (ns *Service) RemoveNotification(identifier string) error { return nil } -// SetIcon is a macOS stub. The application icon is used automatically. -func (ns *Service) SetIcon(_ []byte) { - return +//export requestNotificationAuthorizationResponse +func requestNotificationAuthorizationResponse(channelID C.int, authorized C.bool, errorMsg *C.char) { + resultCh, exists := getAndDeleteChannel(int(channelID)) + if !exists { + // handle this + return + } + + var err error + if errorMsg != nil { + err = fmt.Errorf("%s", C.GoString(errorMsg)) + C.free(unsafe.Pointer(errorMsg)) + } + + resultCh <- notificationChannel{ + authorized: bool(authorized), + err: err, + } + + close(resultCh) +} + +//export checkNotificationAuthorizationResponse +func checkNotificationAuthorizationResponse(channelID C.int, authorized C.bool, errorMsg *C.char) { + resultCh, exists := getAndDeleteChannel(int(channelID)) + if !exists { + // handle this + return + } + + var err error + if errorMsg != nil { + err = fmt.Errorf("%s", C.GoString(errorMsg)) + C.free(unsafe.Pointer(errorMsg)) + } + + resultCh <- notificationChannel{ + authorized: bool(authorized), + err: err, + } + + close(resultCh) } //export didReceiveNotificationResponse @@ -230,7 +296,44 @@ func didReceiveNotificationResponse(jsonPayload *C.char) { response.ActionIdentifier = DefaultActionIdentifier } - if NotificationService != nil { - NotificationService.handleNotificationResponse(response) + notificationServiceLock.RLock() + ns := NotificationService + notificationServiceLock.RUnlock() + + if service != nil { + ns.callbackLock.RLock() + callback := ns.notificationResponseCallback + ns.callbackLock.RUnlock() + + if callback != nil { + callback(response) + } } } + +func registerChannel() (int, chan notificationChannel) { + notificationChannelsLock.Lock() + defer notificationChannelsLock.Unlock() + + id := nextChannelID + nextChannelID++ + + resultCh := make(chan notificationChannel{ + authorized bool + err error + }, 1) + + notificationChannels[id] = resultCh + return id, resultCh +} + +func getChannel(id int) (chan notificationChannel, bool) { + notificationChannelsLock.Lock() + defer notificationChannelsLock.Unlock() + + ch, exists := notificationChannels[id] + if exists { + delete(notificationChannels, id) + } + return ch, exists +} diff --git a/v3/pkg/services/notifications/notifications_darwin.h b/v3/pkg/services/notifications/notifications_darwin.h index 21a210289..9b2bb6774 100644 --- a/v3/pkg/services/notifications/notifications_darwin.h +++ b/v3/pkg/services/notifications/notifications_darwin.h @@ -6,8 +6,8 @@ #import bool checkBundleIdentifier(void); -bool requestUserNotificationAuthorization(void *completion); -bool checkNotificationAuthorization(void); +bool requestNotificationAuthorization(void *channelPtr); +bool checkNotificationAuthorization(void *channelPtr); void sendNotification(const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json, void *completion); void sendNotificationWithActions(const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json, void *completion); void registerNotificationCategory(const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle); @@ -17,4 +17,8 @@ void removePendingNotification(const char *identifier); void removeAllDeliveredNotifications(void); void removeDeliveredNotification(const char *identifier); +extern void requestNotificationAuthorizationResponse(int channelID, bool authorized, const char* error); +extern void checkNotificationAuthorizationResponse(int channelID, bool authorized, const char* error); +extern void didReceiveNotificationResponse(const char *jsonPayload); + #endif /* NOTIFICATIONS_DARWIN_H */ \ No newline at end of file diff --git a/v3/pkg/services/notifications/notifications_darwin.m b/v3/pkg/services/notifications/notifications_darwin.m index 4a1514e06..3469d69d4 100644 --- a/v3/pkg/services/notifications/notifications_darwin.m +++ b/v3/pkg/services/notifications/notifications_darwin.m @@ -2,8 +2,6 @@ #import #import -extern void didReceiveNotificationResponse(const char *jsonPayload); - @interface NotificationsDelegate : NSObject @end @@ -71,8 +69,8 @@ static void ensureDelegateInitialized(void) { bool checkBundleIdentifier(void) { NSBundle *main = [NSBundle mainBundle]; if (main.bundleIdentifier == nil) { - NSLog(@"Error: Cannot use notifications in development mode.\n" - " Notifications require the app to be properly bundled with a bundle identifier.\n" + NSLog(@"Error: Cannot use the notification API in development mode.\n" + " Notifications require the app to be properly bundled with a bundle identifier and signed.\n" " To test notifications:\n" " 1. Build and package your app using 'wails3 package'\n" " 2. Sign the packaged .app\n" @@ -82,40 +80,32 @@ bool checkBundleIdentifier(void) { return true; } -bool requestUserNotificationAuthorization(void *completion) { - if (!checkBundleIdentifier()) { - return false; - } - +bool requestNotificationAuthorization(int channelID) { ensureDelegateInitialized(); UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge; + [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) { - if (completion != NULL) { - void (^callback)(NSError *, BOOL) = completion; - callback(error, granted); + if (error) { + requestNotificationAuthorizationResponse(channelID, false, [[error localizedDescription] UTF8String]); + } else { + requestNotificationAuthorizationResponse(channelID, granted, NULL); } }]; - return true; } -bool checkNotificationAuthorization(void) { +bool checkNotificationAuthorization(int channelID) { ensureDelegateInitialized(); - __block BOOL isAuthorized = NO; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; - [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { - isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized); - dispatch_semaphore_signal(semaphore); + [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { + BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized); + checkNotificationAuthorizationResponse(channelID, isAuthorized, NULL); }]; - - // Wait for response with a timeout - dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC)); - return isAuthorized; } void sendNotification(const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json, void *completion) { diff --git a/v3/pkg/services/notifications/notifications_windows.go b/v3/pkg/services/notifications/notifications_windows.go index ddb71f528..36466de94 100644 --- a/v3/pkg/services/notifications/notifications_windows.go +++ b/v3/pkg/services/notifications/notifications_windows.go @@ -20,11 +20,11 @@ import ( ) var ( - NotificationLock sync.RWMutex - NotificationCategories = make(map[string]NotificationCategory) - AppName string - AppGUID string - IconPath string + NotificationCategories = make(map[string]NotificationCategory) + notificationCategoriesLock sync.RWMutex + appName string + appGUID string + iconPath string ) const ( @@ -42,6 +42,9 @@ type NotificationPayload struct { // Creates a new Notifications Service. func New() *Service { + notificationServiceLock.Lock() + defer notificationServiceLock.Unlock() + if NotificationService == nil { NotificationService = &Service{} } @@ -51,20 +54,20 @@ func New() *Service { // ServiceStartup is called when the service is loaded // Sets an activation callback to emit an event when notifications are interacted with. func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { - AppName = application.Get().Config().Name + appName = application.Get().Config().Name guid, err := getGUID() if err != nil { return err } - AppGUID = guid + appGUID = guid - IconPath = filepath.Join(os.TempDir(), AppName+guid+".png") + iconPath = filepath.Join(os.TempDir(), appName+appGUID+".png") toast.SetAppData(toast.AppData{ - AppID: AppName, + AppID: appName, GUID: guid, - IconPath: IconPath, + IconPath: iconPath, }) toast.SetActivationCallback(func(args string, data []toast.UserData) { @@ -147,9 +150,9 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro fmt.Printf("Error saving icon: %v\n", err) } - NotificationLock.RLock() + notificationCategoriesLock.RLock() nCategory := NotificationCategories[options.CategoryID] - NotificationLock.RUnlock() + notificationCategoriesLock.RUnlock() n := toast.Notification{ Title: options.Title, @@ -195,7 +198,7 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro // RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. // Registering a category with the same name as a previously registered NotificationCategory will override it. func (ns *Service) RegisterNotificationCategory(category NotificationCategory) error { - NotificationLock.Lock() + notificationCategoriesLock.Lock() NotificationCategories[category.ID] = NotificationCategory{ ID: category.ID, Actions: category.Actions, @@ -203,16 +206,16 @@ func (ns *Service) RegisterNotificationCategory(category NotificationCategory) e ReplyPlaceholder: category.ReplyPlaceholder, ReplyButtonTitle: category.ReplyButtonTitle, } - NotificationLock.Unlock() + notificationCategoriesLock.Unlock() return saveCategoriesToRegistry() } // RemoveNotificationCategory removes a previously registered NotificationCategory. func (ns *Service) RemoveNotificationCategory(categoryId string) error { - NotificationLock.Lock() + notificationCategoriesLock.Lock() delete(NotificationCategories, categoryId) - NotificationLock.Unlock() + notificationCategoriesLock.Unlock() return saveCategoriesToRegistry() } @@ -298,11 +301,11 @@ func saveIconToDir() error { return fmt.Errorf("failed to retrieve application icon") } - return saveHIconAsPNG(icon, IconPath) + return saveHIconAsPNG(icon, iconPath) } func saveCategoriesToRegistry() error { - registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName) + registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName) key, _, err := registry.CreateKey( registry.CURRENT_USER, @@ -314,9 +317,9 @@ func saveCategoriesToRegistry() error { } defer key.Close() - NotificationLock.RLock() + notificationCategoriesLock.RLock() data, err := json.Marshal(NotificationCategories) - NotificationLock.RUnlock() + notificationCategoriesLock.RUnlock() if err != nil { return err } @@ -325,7 +328,7 @@ func saveCategoriesToRegistry() error { } func loadCategoriesFromRegistry() error { - registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName) + registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName) key, err := registry.OpenKey( registry.CURRENT_USER, @@ -350,9 +353,9 @@ func loadCategoriesFromRegistry() error { return err } - NotificationLock.Lock() + notificationCategoriesLock.Lock() NotificationCategories = categories - NotificationLock.Unlock() + notificationCategoriesLock.Unlock() return nil } @@ -367,7 +370,7 @@ func getUserText(data []toast.UserData) (string, bool) { } func getGUID() (string, error) { - keyPath := ToastRegistryPath + AppName + keyPath := ToastRegistryPath + appName k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE) if err == nil {