feat(push): PR-1d — OS-scheduled registration refresh - #470
Conversation
push_refresh_job.dart: a workmanager task (periodic on Android, BGAppRefreshTask on iOS, every 12 h, network required) that reads the JSON mirror Rust writes next to the database and re-POSTs every accepted registration to /api/register with dart:io's client. No Rust core, no database, no protocol state: a test reads the file's imports to hold that boundary. The mirror now carries the server URL so the job needs no other source. Scheduled from the push service once the token is handed over; iOS identifier in Info.plist and AppDelegate. docs/PUSH_NOTIFICATIONS.md Phase 1, T1.5.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93a60f8147
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| library; | ||
|
|
||
| import 'dart:convert'; | ||
| import 'dart:io'; |
There was a problem hiding this comment.
Keep the native refresh job out of web builds
When compiling the supported web target, push_notification_service.dart unconditionally imports this file, so the compiler resolves its dart:io import even though isSupported returns false at runtime. Since dart:io is unavailable on the web, the Flutter web build now fails; expose the scheduler through a conditional import with a web stub, as is already done for app_data_dir.dart.
AGENTS.md reference: AGENTS.md:L39-L40
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not changed, with evidence: the web build does not fail. Both dart2js and dart2wasm ship a dart:io whose members throw UnsupportedError at runtime, so importing it compiles; the CI run on this PR's own commit built and smoke-tested the web bundle and passed (https://github.com/MostroP2P/app/actions/runs/34786414287, job "Web (wasm build / smoke test)"). The runtime path is gated too: schedulePushRefresh is only reached when isSupported is true, which is never on web. app_data_dir.dart has a web variant because it is called on web; this job is not. If web push lands (T4.5) the job stays native-only by that same gate.
# Conflicts: # lib/features/notifications/services/push_notification_service.dart
Summary
Phase 1, task T1.5 of
docs/PUSH_NOTIFICATIONS.md(§7.1). Stacked on PR-1c (#468).The push server forgets a token 48 h after its last registration, and none of the app's own triggers fire while it is suspended or not running. Without this, a user who does not open the app for two days is unreachable for every later event, including a payout claim whose window is 15 days.
push_refresh_job.dart: aworkmanagertask, periodic on Android andBGAppRefreshTaskon iOS, every 12 h with the network required. It reads the JSON mirror Rust writes next to the database (push_mirror.json: server URL, token, platform, accepted registrations) and re-POSTs each one to/api/registerwithdart:io's client, 10 s per request. The body is exactly the one the app sends, nothing more.PUSH_SERVER_URL.PushNotificationServiceonce the token is handed over, withExistingPeriodicWorkPolicy.keepso a relaunch does not reset the period. Failures to schedule are logged; the in-app refresh still runs.BGTaskSchedulerPermittedIdentifiersandUIBackgroundModesinInfo.plist, the identifier registered inAppDelegate.swift. A test asserts the identifier matches the Dart constant in both files.workmanager ^0.10.10added. Android needs no manifest change.Test plan
flutter analyzeclean; fullflutter test,cargo testandcargo clippypassed in the pre-commit hook.Manual testing
Verified on a Pixel with a debug build, 2026-09-14: steps 1–4 and 6 pass; step 5 waits on T4.2; iOS unexercised.
Forcing the periodic job does not run it. WorkManager keeps its own period: a JobScheduler run that arrives early is dropped with
Delaying execution for …BackgroundWorker because it is being executed before scheduleand rescheduled, so no Dart runs. (The job also sits in theandroidx.work.systemjobschedulernamespace and its id changes on every reschedule, socmd jobscheduler run … 0does not even find it.) Instead, temporarily add a one-off run of the same task at the end of thetryinschedulePushRefresh()— do not commit it:The background isolate runs the Dart bundled in the APK, so reinstall with
flutter runafter the edit — reopening the app from the launcher keeps the old build. Watch the log withadb logcat -s flutter | grep "\[push\]"../scripts/frb-generate.shandflutter pub get, thenflutter run -d androidand grant the notification permission. Expect[push] FCM token acquired, then[push] TEMP one-off refresh scheduled in 2 min.adb shell run-as foundation.mostro.app cat app_flutter/push_mirror.jsoncontainsserver_url,token,platformand the registrations.adb shell dumpsys jobscheduler | grep foundation.mostro.app/androidx.workshows the periodic job with theNETconstraint and aTIME=+~12hwindow.[push] refresh job: sent=N failed=0, N = registrations in the mirror.sent=0 failed=Nand no crash. The one-off has no network constraint, so it runs offline.push_api.setPushEnabled(enabled: false)from a debug hook. Expect the mirror file removed, and the next run to logsent=0 failed=0.adb shell am kill foundation.mostro.appand checkadb shell pidof foundation.mostro.appis empty. Wait 2 min. Expect the samesent=Nline under a new PID: the OS started a process just for the job. Swiping the app from recents does not kill the process (the job then runs in the old PID and proves nothing), andam force-stopputs the app in the stopped state, where Android runs none of its jobs.