Skip to content

feat(push): PR-1d — OS-scheduled registration refresh - #470

Merged
grunch merged 2 commits into
mainfrom
feat/push-1d-refresh-job
Sep 14, 2026
Merged

grunch merged 2 commits into
mainfrom
feat/push-1d-refresh-job

Conversation

@grunch

@grunch grunch commented Sep 13, 2026

Copy link
Copy Markdown
Member

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: a workmanager task, periodic on Android and BGAppRefreshTask on 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/register with dart:io's client, 10 s per request. The body is exactly the one the app sends, nothing more.
  • HTTP plumbing, not a second writer. The job never loads the Rust core, never opens the database and never touches protocol state, so the "doorbell, not courier" rule holds (§6 principle 2). A test reads the file's imports and fails on any bridge, database, preferences or Firebase import.
  • The mirror carries the server URL now, so the job has one source of truth and cannot drift from Rust's PUSH_SERVER_URL.
  • Scheduled from PushNotificationService once the token is handed over, with ExistingPeriodicWorkPolicy.keep so a relaunch does not reset the period. Failures to schedule are logged; the in-app refresh still runs.
  • iOS: BGTaskSchedulerPermittedIdentifiers and UIBackgroundModes in Info.plist, the identifier registered in AppDelegate.swift. A test asserts the identifier matches the Dart constant in both files.
  • workmanager ^0.10.10 added. Android needs no manifest change.

Test plan

  • 8 unit tests: the exact register body, one failure does not stop the rest, non-2xx counts as failed, malformed / tokenless / empty mirrors send nothing, an entry without both pubkeys is skipped, no mirror file means nothing to refresh, the import boundary, and the iOS identifier consistency.
  • flutter analyze clean; full flutter test, cargo test and cargo clippy passed in the pre-commit hook.
  • iOS provisioning is not set up in this repo yet (T4.4), so the iOS path is configured but unexercised.

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 schedule and rescheduled, so no Dart runs. (The job also sits in the androidx.work.systemjobscheduler namespace and its id changes on every reschedule, so cmd jobscheduler run … 0 does not even find it.) Instead, temporarily add a one-off run of the same task at the end of the try in schedulePushRefresh()do not commit it:

if (kDebugMode) {
  await Workmanager().registerOneOffTask(
    'pushRefresh.manualTest',
    kPushRefreshTask,
    initialDelay: const Duration(minutes: 2),
    existingWorkPolicy: ExistingWorkPolicy.replace,
  );
  debugPrint('[push] TEMP one-off refresh scheduled in 2 min');
}

The background isolate runs the Dart bundled in the APK, so reinstall with flutter run after the edit — reopening the app from the launcher keeps the old build. Watch the log with adb logcat -s flutter | grep "\[push\]".

  1. Check out the branch, run ./scripts/frb-generate.sh and flutter pub get, then flutter run -d android and grant the notification permission. Expect [push] FCM token acquired, then [push] TEMP one-off refresh scheduled in 2 min.
  2. Confirm the mirror: adb shell run-as foundation.mostro.app cat app_flutter/push_mirror.json contains server_url, token, platform and the registrations. adb shell dumpsys jobscheduler | grep foundation.mostro.app/androidx.work shows the periodic job with the NET constraint and a TIME=+~12h window.
  3. Wait 2 min. Expect [push] refresh job: sent=N failed=0, N = registrations in the mirror.
  4. Relaunch (the one-off reschedules), turn on airplane mode, wait 2 min. Expect sent=0 failed=N and no crash. The one-off has no network constraint, so it runs offline.
  5. Turn push off in Settings once T4.2 lands, or call push_api.setPushEnabled(enabled: false) from a debug hook. Expect the mirror file removed, and the next run to log sent=0 failed=0.
  6. Relaunch, press Home, then adb shell am kill foundation.mostro.app and check adb shell pidof foundation.mostro.app is empty. Wait 2 min. Expect the same sent=N line 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), and am force-stop puts the app in the stopped state, where Android runs none of its jobs.

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.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • develop

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 0cb51af5-7d74-43ed-98a5-a10ac5dfa3d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T22:22:44.601261Z 93a60f8 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Base automatically changed from feat/push-1c-dart-token to main September 14, 2026 12:31
@grunch
grunch merged commit cfabd0e into main Sep 14, 2026
4 checks passed
@grunch
grunch deleted the feat/push-1d-refresh-job branch September 14, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant