Route iOS notification settings intent into the app (#11003)
This commit is contained in:
@@ -10,6 +10,7 @@ This module consolidates several native features into a single Expo module:
|
|||||||
- **Referrer**: Tracking how users arrive at the app (web referrers, app referrers, Google Play install referrer)
|
- **Referrer**: Tracking how users arrive at the app (web referrers, app referrers, Google Play install referrer)
|
||||||
- **SharedPrefs**: Shared preferences storage using native platform APIs (UserDefaults on iOS, SharedPreferences on Android)
|
- **SharedPrefs**: Shared preferences storage using native platform APIs (UserDefaults on iOS, SharedPreferences on Android)
|
||||||
- **VisibilityView**: A native view component that tracks which view is currently visible on screen
|
- **VisibilityView**: A native view component that tracks which view is currently visible on screen
|
||||||
|
- **NotificationSettings**: iOS handler that routes the system "notification settings" intent into the app
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
@@ -123,9 +124,24 @@ This is useful for features like video autoplay, where you want to know which vi
|
|||||||
- Android: Full support using View position tracking
|
- Android: Full support using View position tracking
|
||||||
- Web: Passthrough component (renders children without tracking)
|
- Web: Passthrough component (renders children without tracking)
|
||||||
|
|
||||||
## Architecture
|
### NotificationSettings
|
||||||
|
|
||||||
### TypeScript Layer
|
iOS only. Has no JavaScript surface - it is a pure native side effect registered
|
||||||
|
at app launch.
|
||||||
|
|
||||||
|
When push permissions are requested with `provideAppNotificationSettings: true`,
|
||||||
|
iOS adds an in-app notification settings button to the system Settings screen for
|
||||||
|
Bluesky. Tapping it launches the app and triggers
|
||||||
|
`userNotificationCenter(_:openSettingsFor:)`. expo-notifications owns the
|
||||||
|
`UNUserNotificationCenter` delegate and fans this callback out to registered
|
||||||
|
`NotificationDelegate`s. This module registers one and converts the callback into
|
||||||
|
a `bluesky://settings/notifications` deep link, which the app's existing linking
|
||||||
|
config routes to the notification settings screen.
|
||||||
|
|
||||||
|
**Platform Support:**
|
||||||
|
- iOS: Full support
|
||||||
|
- Android: Not applicable (Android opens the system notification settings directly)
|
||||||
|
- Web: Not applicable
|
||||||
|
|
||||||
The module uses platform-specific file extensions to provide appropriate implementations:
|
The module uses platform-specific file extensions to provide appropriate implementations:
|
||||||
|
|
||||||
@@ -170,7 +186,9 @@ The module uses platform-specific file extensions to provide appropriate impleme
|
|||||||
|
|
||||||
### Expo Module Config
|
### Expo Module Config
|
||||||
|
|
||||||
The module is registered in `expo-module.config.json` with all four sub-modules for both iOS and Android.
|
The module is registered in `expo-module.config.json`. The PlatformInfo,
|
||||||
|
Referrer, SharedPrefs, and VisibilityView sub-modules are registered for both iOS
|
||||||
|
and Android; NotificationSettings is iOS only.
|
||||||
|
|
||||||
### iOS
|
### iOS
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"ExpoBlueskySharedPrefsModule",
|
"ExpoBlueskySharedPrefsModule",
|
||||||
"ExpoBlueskyReferrerModule",
|
"ExpoBlueskyReferrerModule",
|
||||||
"ExpoBlueskyVisibilityViewModule",
|
"ExpoBlueskyVisibilityViewModule",
|
||||||
"ExpoPlatformInfoModule"
|
"ExpoPlatformInfoModule",
|
||||||
|
"ExpoBlueskyNotificationSettingsModule"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Pod::Spec.new do |s|
|
|||||||
s.static_framework = true
|
s.static_framework = true
|
||||||
|
|
||||||
s.dependency 'ExpoModulesCore'
|
s.dependency 'ExpoModulesCore'
|
||||||
|
s.dependency 'EXNotifications'
|
||||||
|
|
||||||
# Swift/Objective-C compatibility
|
# Swift/Objective-C compatibility
|
||||||
s.pod_target_xcconfig = {
|
s.pod_target_xcconfig = {
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import EXNotifications
|
||||||
|
import ExpoModulesCore
|
||||||
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When we request push notification permissions with
|
||||||
|
* `provideAppNotificationSettings: true`, iOS adds an in-app notification
|
||||||
|
* settings button to the system Settings screen for Bluesky (and may surface
|
||||||
|
* it elsewhere, e.g. from a delivered notification). Tapping it launches the
|
||||||
|
* app and calls `userNotificationCenter(_:openSettingsFor:)`.
|
||||||
|
*
|
||||||
|
* expo-notifications owns the `UNUserNotificationCenter` delegate via its
|
||||||
|
* `NotificationCenterManager`, which fans that callback out to any registered
|
||||||
|
* `NotificationDelegate` through `openSettings(_:)`. We register here and turn
|
||||||
|
* the callback into a `bluesky://settings/notifications` deep link so the app's
|
||||||
|
* existing linking config routes the user to the notification settings screen.
|
||||||
|
*/
|
||||||
|
public class ExpoBlueskyNotificationSettingsModule: Module, NotificationDelegate {
|
||||||
|
public func definition() -> ModuleDefinition {
|
||||||
|
Name("ExpoBlueskyNotificationSettings")
|
||||||
|
|
||||||
|
OnCreate {
|
||||||
|
NotificationCenterManager.shared.addDelegate(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
OnDestroy {
|
||||||
|
NotificationCenterManager.shared.removeDelegate(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func openSettings(_ notification: UNNotification?) {
|
||||||
|
guard let url = URL(string: "bluesky://settings/notifications") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -249,7 +249,26 @@ export function useRequestNotificationsPermission() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await Notifications.requestPermissionsAsync()
|
const res = await Notifications.requestPermissionsAsync({
|
||||||
|
ios: {
|
||||||
|
/*
|
||||||
|
* These three default to true when no argument is passed to
|
||||||
|
* `requestPermissionsAsync`, but passing an options object opts out of
|
||||||
|
* that default, so we have to set them explicitly to preserve the
|
||||||
|
* existing behavior.
|
||||||
|
*/
|
||||||
|
allowAlert: true,
|
||||||
|
allowBadge: true,
|
||||||
|
allowSound: true,
|
||||||
|
/*
|
||||||
|
* Adds an in-app notification settings button to the system Settings
|
||||||
|
* screen for Bluesky. When tapped, iOS calls back into the app, which
|
||||||
|
* we route to the in-app notification settings (see the
|
||||||
|
* NotificationSettings module in expo-bluesky-swiss-army).
|
||||||
|
*/
|
||||||
|
provideAppNotificationSettings: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
ax.metric(`notifications:request`, {
|
ax.metric(`notifications:request`, {
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -82,7 +82,19 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
const onRequestPermissions = async () => {
|
const onRequestPermissions = async () => {
|
||||||
if (IS_WEB) return
|
if (IS_WEB) return
|
||||||
if (permissions?.canAskAgain) {
|
if (permissions?.canAskAgain) {
|
||||||
const response = await Notification.requestPermissionsAsync()
|
const response = await Notification.requestPermissionsAsync({
|
||||||
|
ios: {
|
||||||
|
/*
|
||||||
|
* These default to true only when no argument is passed, so set them
|
||||||
|
* explicitly to preserve behavior alongside
|
||||||
|
* provideAppNotificationSettings.
|
||||||
|
*/
|
||||||
|
allowAlert: true,
|
||||||
|
allowBadge: true,
|
||||||
|
allowSound: true,
|
||||||
|
provideAppNotificationSettings: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
queryClient.setQueryData(RQKEY, response)
|
queryClient.setQueryData(RQKEY, response)
|
||||||
} else {
|
} else {
|
||||||
if (IS_ANDROID) {
|
if (IS_ANDROID) {
|
||||||
|
|||||||
Reference in New Issue
Block a user