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)
|
||||
- **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
|
||||
- **NotificationSettings**: iOS handler that routes the system "notification settings" intent into the app
|
||||
|
||||
## 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
|
||||
- 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:
|
||||
|
||||
@@ -170,7 +186,9 @@ The module uses platform-specific file extensions to provide appropriate impleme
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"ExpoBlueskySharedPrefsModule",
|
||||
"ExpoBlueskyReferrerModule",
|
||||
"ExpoBlueskyVisibilityViewModule",
|
||||
"ExpoPlatformInfoModule"
|
||||
"ExpoPlatformInfoModule",
|
||||
"ExpoBlueskyNotificationSettingsModule"
|
||||
]
|
||||
},
|
||||
"android": {
|
||||
|
||||
@@ -10,6 +10,7 @@ Pod::Spec.new do |s|
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
s.dependency 'EXNotifications'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
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
|
||||
}
|
||||
|
||||
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`, {
|
||||
context: context,
|
||||
|
||||
@@ -82,7 +82,19 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
const onRequestPermissions = async () => {
|
||||
if (IS_WEB) return
|
||||
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)
|
||||
} else {
|
||||
if (IS_ANDROID) {
|
||||
|
||||
Reference in New Issue
Block a user