feat(android): expose runtime POST_NOTIFICATIONS permission request for the FGS#109
Open
gmaclennan wants to merge 2 commits into
Open
feat(android): expose runtime POST_NOTIFICATIONS permission request for the FGS#109gmaclennan wants to merge 2 commits into
gmaclennan wants to merge 2 commits into
Conversation
…or the FGS
On Android 13+ (API 33) the foreground-service notification is runtime-gated
behind POST_NOTIFICATIONS; without the grant the system suppresses the
notification and may deprioritise or kill the service. Below API 33 and on
iOS the permission is auto-granted.
The module now exposes expo-style check/request methods and leaves the
decision of when to call them to the host app — it never auto-prompts:
- JS: `getNotificationPermissionsAsync()` / `requestNotificationPermissionsAsync()`
return a `PermissionResponse` ({ status, granted, canAskAgain, expires }).
On iOS and Android < 33 they resolve `granted` so host code is
cross-platform safe.
- Android: routed through expo-modules-core's permissions plumbing
(`appContext.permissions` + `Permissions.{get,ask}ForPermissionsWithPermissionsManager`),
which handles onRequestPermissionsResult and the granted / denied /
"don't ask again" (canAskAgain) cases.
- Service guard: `ComapeoCoreService` checks the grant before
`startForeground` on API 33+ and catches the SecurityException path so a
missing grant degrades gracefully (logged, never crashes) instead of
taking the process down.
Documents the contract in docs/ForegroundService.md (module exposes, host
requests; settings deep-link UX tracked in #100). Adds JS-level tests for
the new API surface; native behaviour requires a device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #97
Summary
On Android 13+ (API 33) the foreground-service (FGS) notification is runtime-gated behind
POST_NOTIFICATIONS. Without the grant the system suppresses the notification, which lets it deprioritise or kill the service. Below API 33 and on iOS the permission is auto-granted.Per the maintainer's design decision, the module exposes the capability and the host app decides when to call it — there is no auto-prompt.
What changed
src/): addsgetNotificationPermissionsAsync()andrequestNotificationPermissionsAsync(), both returning an expo-stylePermissionResponse({ status, granted, canAskAgain, expires }). Exported fromsrc/index.ts, typed insrc/ComapeoCore.types.ts, wired throughsrc/ComapeoCoreModule.ts. On iOS and Android < 33 they resolve asgranted(no-op) so host code never has to branch on platform.appContext.permissions+Permissions.getPermissionsWithPermissionsManager/askForPermissionsWithPermissionsManager. This reuses expo'sPermissionsService, which ownsonRequestPermissionsResultand produces the{status, granted, canAskAgain, expires}response, including the granted / denied / "don't ask again" (canAskAgain=falseviashouldShowRequestPermissionRationale) cases. No hand-rolledActivityCompatpath was needed becausePermissionsServiceis registered as a default internal module of expo-modules-core, soappContext.permissionsis always available.ComapeoCoreService.kt): beforestartForegroundon API 33+, checksContextCompat.checkSelfPermission(POST_NOTIFICATIONS)and logs a warning when missing. WrapsstartForegroundin atry/catch (SecurityException)so a missing grant degrades gracefully — logged to diagnostics, service keeps running deprioritised, no crash.ComapeoCoreModule.swift): both methods resolve agrantedresponse (no FGS / no matching runtime gate on iOS).docs/ForegroundService.md): documents the contract — module exposes the check/request methods, host app calls them (typically around starting the service), including rationale + settings deep-link UX (tracked in UX for notification permission (rationale + denied/settings flow) #100). Written for a JS/Node audience.Acceptance criteria (#97)
requestNotificationPermissionsAsync())PermissionsServicemaps these intostatus+canAskAgain)checkSelfPermissionreports the manifest permission granted, so the methods resolvegrantedwithout a dialog)denied,granted=false;canAskAgain=falseonce the user picks "Don't ask again")Checks run
npm run lint— passnpx tsc --noEmit— passnpm test(jest) — pass, 19/19 (4 new insrc/__tests__/notification-permissions.test.js+ 15 pre-existing)Notes on the test environment: the fresh worktree needed
src/version.tsgenerated (node scripts/write-version.mjs, normally part ofprepare) and a top-levelbabel-preset-expo(a transitive expo dep that wasn't hoisted) before jest would run. Neither is committed.Not run locally (relies on CI)
example/androidapp orgradlewin this worktree, so the library module can't be compiled or run here. The Kotlin symbols used were verified against the installedexpo-modules-core@56source (AppContext.permissions: Permissions?, thePermissions.*WithPermissionsManageroverloads acceptingexpo.modules.kotlin.Promise,PermissionsServicedefault registration). The runtime permission-dialog and denial flows require a physical device / emulator on API 33+.AsyncFunctionpair.🤖 Generated with Claude Code