Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ app.*.map.json
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/.derived-data-log-*
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [0.3.0]

### Added
- **`syncNotificationPermission()`** — forces an immediate re-check of the OS notification permission and syncs any change to PushFire (re-registering the device when it changed), returning the current `NotificationStatus`. Previously this only happened automatically when the app returned to the foreground, with no way to trigger it on demand.
- **`openNotificationSettings()`** — deep-links the user into the OS settings page for the app (wraps `permission_handler`'s `openAppSettings()`). Use it for the permanently-denied case, where `requestNotificationPermission()` no longer shows a system prompt.
- **`PushFireConfig.getFcmTokenOverride`** — optional `Future<String?> Function()` hook to supply your own (e.g. APNS-aware) FCM token fetcher. Previously only reachable via an internal test-only constructor. A constructor-level override still takes precedence when present.
- **`PushFireConfig.iosRegisterWithoutPrompt`** (iOS only, default `false`) — when `requestNotificationPermission` is `false`, opt in to trigger remote-notification registration without showing the authorization dialog, so an APNS/FCM token can still be obtained. Implemented via provisional authorization. Note: provisional authorization is not "no authorization" — it delivers notifications quietly to Notification Center and the user may be asked later to keep or disable them. For a truly authorization-free registration, call `application.registerForRemoteNotifications()` from your AppDelegate and leave this `false`.

### Fixed
- **iOS auto-registration no longer hard-fails with `apns-token-not-set`.** On iOS the APNS token is delivered asynchronously by Apple after `registerForRemoteNotifications`, so calling `FirebaseMessaging.getToken()` during `initialize()` could throw `[firebase_messaging/apns-token-not-set]` and leave the device unregistered. `DeviceService` now waits for the APNS token (polls `getAPNSToken()` up to 10 times at 500ms) before requesting the FCM token. If the token never arrives (simulator, offline, or registration was never triggered), it skips `getToken()` and returns null instead of throwing — the device registers later via the existing `onTokenRefresh` listener.

### Docs
- Rewrote the README notification-permissions section: documented the denied → settings flow, the automatic and on-demand permission sync, and corrected the "re-request strategy" guidance (re-requesting no longer prompts once permanently denied).

## [0.2.1]

### Fixed
Expand Down
52 changes: 48 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ await PushFireSDK.initialize(
);

// Later, when appropriate for your UX
bool permissionGranted = await PushFireSDK.requestNotificationPermission();
bool permissionGranted = await PushFireSDK.instance.requestNotificationPermission();
if (permissionGranted) {
print('Notification permission granted');
} else {
Expand All @@ -553,20 +553,64 @@ The SDK handles platform-specific permission requirements:
- **Android**: Handles runtime permissions for Android 13+ (API level 33+) and gracefully handles older versions
- **Web**: Requests browser notification permissions through Firebase Messaging

### Handling Permanently Denied Permission

Once the user denies the notification prompt (iOS: any denial; Android: the second
denial), calling `requestNotificationPermission()` again no longer shows a system
dialog — it returns `false` without prompting. At that point the only way for the
user to enable notifications is through the OS settings app. Use
`openNotificationSettings()` to send them there:

```dart
final granted = await PushFireSDK.instance.requestNotificationPermission();
if (!granted) {
// The prompt was suppressed (permanently denied). Explain why notifications
// matter, then deep-link the user into the settings app.
final open = await showEnableNotificationsDialog(); // your own UI
if (open) {
await PushFireSDK.instance.openNotificationSettings();
}
}
```

### Syncing Permission Changes Made in Settings

When the user grants or revokes the notification permission from the OS settings
app, PushFire needs to know so it stops or resumes delivery. The SDK handles this
automatically: it observes the app lifecycle and, whenever the app returns to the
foreground, re-checks the OS permission and updates the device on the server if it
changed. No action is required for the common case.

If you need to force an immediate sync — for example right after the user returns
from `openNotificationSettings()` — call `syncNotificationPermission()`:

```dart
await PushFireSDK.instance.openNotificationSettings();

// ...after the user comes back to your app
final status = await PushFireSDK.instance.syncNotificationPermission();
if (status?.isPermissionGranted ?? false) {
print('Notifications enabled and synced to PushFire');
}
```

`syncNotificationPermission()` re-checks the OS permission, re-registers the device
with PushFire if it changed, and returns the current `NotificationStatus`.

### Best Practices for Permissions

1. **Context Matters**: Request permissions when users understand the value of notifications
2. **Graceful Degradation**: Your app should work even if permissions are denied
3. **Re-request Strategy**: Use `requestNotificationPermission()` to re-request if initially denied
3. **Re-request Strategy**: `requestNotificationPermission()` re-prompts only while the permission is still undetermined. Once it is permanently denied the call returns `false` without a dialog — send the user to `openNotificationSettings()` instead
4. **User Education**: Explain the benefits before requesting permissions

### Permission Status Handling

The SDK automatically:
- Logs permission request outcomes for debugging
- Continues device registration even if permissions are denied
- Supports manual permission grants through device settings
- Re-registers the device when permissions are granted via manual request
- Detects permission changes made in the OS settings when the app returns to the foreground, and re-registers the device to sync the change to PushFire
- Exposes `syncNotificationPermission()` to force that sync on demand, and `openNotificationSettings()` to deep-link the user into the settings app

## Error Types

Expand Down
2 changes: 0 additions & 2 deletions example/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
192 changes: 192 additions & 0 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
PODS:
- app_links (6.4.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- Firebase/Auth (11.15.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.15.0)
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0)
- firebase_auth (5.7.0):
- Firebase/Auth (= 11.15.0)
- firebase_core
- Flutter
- firebase_core (3.15.2):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_messaging (15.2.10):
- Firebase/Messaging (= 11.15.0)
- firebase_core
- Flutter
- FirebaseAppCheckInterop (11.15.0)
- FirebaseAuth (11.15.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.15.0)
- FirebaseCoreExtension (~> 11.15.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (11.15.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.1):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.1):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.1):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.1):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.1)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.1)
- GoogleUtilities/Reachability (8.1.1):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.1):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (4.5.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.4.1)
- RecaptchaInterop (101.0.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter

DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)

SPEC REPOS:
trunk:
- Firebase
- FirebaseAppCheckInterop
- FirebaseAuth
- FirebaseAuthInterop
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleDataTransport
- GoogleUtilities
- GTMSessionFetcher
- nanopb
- PromisesObjC
- RecaptchaInterop

EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"

SPEC CHECKSUMS:
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b
firebase_core: 995454a784ff288be5689b796deb9e9fa3601818
firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707
FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df
FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b
FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 4f2618a4a1e762a1ee134a1e2323bba9843e06da
GTMSessionFetcher: fc75fc972958dceedee61cb662ae1da7a83a91cf
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d

PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694

COCOAPODS: 1.16.2
Loading
Loading