Add push-equivalent notifications via background polling; bump to 3.6
Build iOS App / Build .ipa and publish source (push) Successful in 31s

Sideloaded apps can't use APNs (no push entitlement without Apple signing),
so deliver notifications via an iOS BGAppRefreshTask that polls the monitored
repos every ~15 min and fires local notifications (no Apple push needed):
- new PRs that appear in the open set
- CI status changes on existing PRs

Persistence: NotificationState tracks announced PR ids + last-known CI status
so we only fire on actual changes, not every poll. AppDelegate registers the
background task and reschedules the next one. Info.plist gets UIBackgroundModes
fetch + the BGTask identifier. Settings gains a Notifications toggle; also
poll after a foreground inbox load so state seeds and changes are caught
promptly. Bump to 3.6.
This commit is contained in:
Hermes committed 2026-08-24 08:18:29 +00:00
1 parent 6ed50ecc4a
commit bae531f850
8 files changed
+241 -4

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import UIKit
import BackgroundTasks
/// App delegate that schedules the background refresh task which drives the
/// push-equivalent notifications (polling the monitored repos).
///
/// iOS runs BGAppRefreshTask opportunistically (~every 15 min at best, often
/// slower), and only when the app has been used recently and isn't force-quit.
final class AppDelegate: NSObject, UIApplicationDelegate {
static let taskIdentifier = "com.bueso.giteapr.refresh"
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
registerBackgroundTask()
return true
}
private func registerBackgroundTask() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: Self.taskIdentifier,
using: nil
) { task in
// Handle the background refresh.
self.handle(task)
}
}
private func handle(_ task: BGTask) {
// Reschedule the next refresh so the chain continues.
scheduleNextRefresh()
let pollTask = Task { @MainActor in
await NotificationService.shared.poll()
}
task.expirationHandler = {
pollTask.cancel()
}
Task {
await pollTask.value
task.setTaskCompleted(success: true)
}
}
/// Schedule the next background refresh (~15 min from now, earliest).
func scheduleNextRefresh() {
let request = BGAppRefreshTaskRequest(identifier: Self.taskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
}