From e8d934901c89ce8bd9134de4b4e76395902061b7 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 8 May 2026 13:59:58 +0300 Subject: [PATCH] iOS: defer notification permission prompt to first registerPush / sendLocalNotification (#4876) Previously the iOS app delegate called requestAuthorizationWithOptions in didFinishLaunchingWithOptions whenever notifications were enabled, so the system prompt fired at launch before the developer could show their own rationale screen. Match the Android flow by setting only the notification center delegate at launch, and request authorization on the first call to registerPush() or sendLocalNotification(). For backward compatibility add the ios.notificationPermissionAtLaunch=true build hint, which gates a CN1_NOTIFICATION_PERMISSION_AT_LAUNCH macro that restores the legacy launch-time prompt. Documented the new behavior and build hint in the iOS build hints table and the push notifications guide. Co-Authored-By: Claude Opus 4.7 (1M context) --- Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.h | 5 +++++ Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m | 9 ++++++++- Ports/iOSPort/nativeSources/IOSNative.m | 9 +++++++++ .../Advanced-Topics-Under-The-Hood.asciidoc | 3 +++ docs/developer-guide/Push-Notifications.asciidoc | 2 ++ .../main/java/com/codename1/builders/IPhoneBuilder.java | 6 ++++++ 6 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.h b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.h index 9c4cf4aab1..9283c591cb 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.h +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.h @@ -28,6 +28,11 @@ #ifdef CN1_INCLUDE_NOTIFICATIONS #import #endif +// Legacy compatibility flag (off by default). When defined, the AppDelegate calls +// requestAuthorizationWithOptions in didFinishLaunchingWithOptions, restoring the +// pre-issue-#4876 behavior where the system notification prompt fires at launch. +// Enable from the build hint ios.notificationPermissionAtLaunch=true. +//#define CN1_NOTIFICATION_PERMISSION_AT_LAUNCH @class CodenameOne_GLViewController; diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index 463241d586..e3558d8115 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -355,14 +355,21 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( #ifdef CN1_INCLUDE_NOTIFICATIONS if (@available(iOS 10, *)) { if (isIOS10()) { + // Set the notification center delegate at launch so delivery callbacks route + // correctly. The auth prompt is deferred to registerPush / sendLocalNotification + // so the developer can show their own rationale first (matches Android, see + // issue #4876). Set the ios.notificationPermissionAtLaunch=true build hint to + // restore the legacy launch-time prompt for backward compatibility. UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; +#ifdef CN1_NOTIFICATION_PERMISSION_AT_LAUNCH #if !TARGET_OS_SIMULATOR [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){ if( !error ) {} }]; #endif - } +#endif + } } #endif diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index f9715c2b6b..2a56942365 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -10114,6 +10114,15 @@ JAVA_VOID com_codename1_impl_ios_IOSNative_sendLocalNotification___java_lang_Str UNNotificationTrigger *trigger = cn1CreateNotificationTrigger(fireDate, repeatType); UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:notificationIdString content:content trigger:trigger]; UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + // Request notification authorization on first schedule so local-notification-only + // apps still get prompted (the launch-time prompt was removed for issue #4876). + // The system shows the dialog at most once; later calls are a no-op. + [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound + UNAuthorizationOptionBadge) + completionHandler:^(BOOL granted, NSError * _Nullable authError) { + if (authError != nil) { + CN1Log(@"Local notification authorization request failed: %@", authError.localizedDescription); + } + }]; cn1CancelScheduledLocalNotificationById(notificationIdString); [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (error != nil) { diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index c0e62174b7..b5cbc95493 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -307,6 +307,9 @@ Only supported for App Store builds. See https://www.codenameone.com/developer-g |ios.detectJailbreak |true/false (defaults to false). When true, the iOS app will exit on launch if it detects that it's running on a jailbroken device. +|ios.notificationPermissionAtLaunch +|true/false (defaults to false). Backward-compatibility flag for the pre-issue-#4876 behavior. By default, the iOS notification permission prompt is deferred until the app calls `Push.register()` or schedules a `LocalNotification`, matching the Android flow and giving the developer a chance to display a rationale screen first. Set this hint to `true` to restore the legacy behavior in which the prompt fires automatically inside `application:didFinishLaunchingWithOptions:` as soon as the app launches. Existing apps relying on the prompt being shown at launch should set this to `true`; new apps should leave it disabled and trigger the prompt explicitly when they are ready to ask for permission. + |ios.applicationQueriesSchemes |Comma separated list of url schemes that `canExecute` will respect on iOS. If the url scheme isn't mentioned here `canExecute` will return false starting with iOS 9. Notice that this collides with `ios.plistInject` when used with the `LSApplicationQueriesSchemes...` value so you should use one or the other. For example, to enable `canExecute` for a url like `myurl://xys` you can use: `myurl,myotherurl` diff --git a/docs/developer-guide/Push-Notifications.asciidoc b/docs/developer-guide/Push-Notifications.asciidoc index bc3eecd348..ea61b3b173 100644 --- a/docs/developer-guide/Push-Notifications.asciidoc +++ b/docs/developer-guide/Push-Notifications.asciidoc @@ -73,6 +73,8 @@ If the registration failed for some reason, the `pushRegistrationError()` callba Notice that all this happens seamlessly behind the scenes when your app loads. You don't need to start any of this workflow. +NOTE: On iOS, the system permission prompt that asks the user to allow notifications is shown the first time you call `Display.getInstance().registerPush()` (or schedule a `LocalNotification`). It is no longer shown automatically when the app launches, so you can display your own rationale screen first and call `registerPush()` only after the user has agreed. This matches the Android flow. Apps that need the legacy launch-time prompt for backward compatibility can set the `ios.notificationPermissionAtLaunch=true` build hint. + ==== Sending a push notification Codename One provides a secure REST API for sending push notifications. As an HTTP API, it's language agnostic. You can send push notifications to your app using Java, PHP, Python, Ruby, or even by hand using something like curl. Each HTTP request can contain a push message and a list of device IDs to which the message should be sent. You don't need to worry about whether your app is running on Android, iOS, Windows, or the web. A single HTTP request can send a message to many devices at once. diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index bf82d8f330..0d54cf4952 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -1336,6 +1336,12 @@ public void usesClassMethod(String cls, String method) { try(Writer fios = new OutputStreamWriter(Files.newOutputStream(appDelH.toPath()), StandardCharsets.UTF_8)) { String str = new String(data, StandardCharsets.UTF_8); str = str.replace("//#define CN1_INCLUDE_NOTIFICATIONS", "#define CN1_INCLUDE_NOTIFICATIONS"); + if (request.getArg("ios.notificationPermissionAtLaunch", "false").equalsIgnoreCase("true")) { + // Restore pre-#4876 behavior: prompt for notification permission + // in didFinishLaunchingWithOptions instead of on first registerPush / + // sendLocalNotification call. + str = str.replace("//#define CN1_NOTIFICATION_PERMISSION_AT_LAUNCH", "#define CN1_NOTIFICATION_PERMISSION_AT_LAUNCH"); + } fios.write(str); }