Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
cf27620
feat: Live Activity — Phase 1 (lock screen + Dynamic Island, APNs sel…
MtlPhil Mar 7, 2026
59f5d2b
fix: trigger Live Activity refresh on not-looping state change; handl…
MtlPhil Mar 8, 2026
c53e17f
Fix PR issues + DST fix and better APNs error checking
MtlPhil Mar 9, 2026
25f30c0
Fix PR issues + DST fix and better APNs error checking
MtlPhil Mar 9, 2026
b833ad9
fix: address remaining hardcoded bundleID
MtlPhil Mar 10, 2026
524b3bb
Replace SwiftJWT with CryptoKit and separate APNs credentials
bjorkert Mar 11, 2026
4dcbd69
Localization refactoring
bjorkert Mar 12, 2026
63326d8
feat: Live Activity auto-renewal to work around 8-hour system limit
MtlPhil Mar 13, 2026
a020c8f
test: reduce LA renewal threshold to 20 min for testing
MtlPhil Mar 13, 2026
bae228d
feat: improve LA renewal robustness and stale indicator
MtlPhil Mar 13, 2026
2785502
feat: renewal warning overlay + restore 7.5h threshold
MtlPhil Mar 13, 2026
0250633
fix: overlay not appearing + foreground restart not working
MtlPhil Mar 13, 2026
4e48c45
test: set renewalThreshold to 20 min for testing
MtlPhil Mar 13, 2026
136dba0
fix: renewal overlay not clearing after LA is refreshed
MtlPhil Mar 13, 2026
32a6dd0
Fix Mac Catalyst build: guard ActivityKit code and exclude widget ext…
bjorkert Mar 13, 2026
921a966
fix: overlay permanently active when warning window equals threshold
MtlPhil Mar 13, 2026
8989103
fix: include showRenewalOverlay in APNs payload and clear laRenewBy s…
MtlPhil Mar 13, 2026
1ab3930
fix: await LA end before restarting on foreground retry to avoid reus…
MtlPhil Mar 13, 2026
cdd4f85
chore: restore production renewal timing (7.5h threshold, 20min warning)
MtlPhil Mar 13, 2026
e737bce
**Live Activity auto-renewal (8-hour limit workaround)** (#539)
MtlPhil Mar 13, 2026
e0a729a
feat: laEnabled toggle, forceRestart(), and RestartLiveActivityIntent
MtlPhil Mar 14, 2026
7588c93
Added RestartLiveActivityIntent to project
MtlPhil Mar 14, 2026
0c21909
fix: resolve two build errors in LiveActivityManager and RestartLiveA…
MtlPhil Mar 14, 2026
9f5ddf2
fix: guard continueInForeground() behind iOS 26 availability check
MtlPhil Mar 14, 2026
c2e4c34
fix: use startFromCurrentState in handleDidBecomeActive instead of fo…
MtlPhil Mar 14, 2026
2869d24
feat: LA foreground tab navigation, button feedback, and toggle sync
MtlPhil Mar 14, 2026
3259dcb
fix: flush LA update on willResignActive to ensure lock screen shows …
MtlPhil Mar 14, 2026
54e3ed9
feat: redesign Dynamic Island compact and expanded views
MtlPhil Mar 14, 2026
6752fb2
fix: match Proj text style to delta; add trailing padding to IOB/COB
MtlPhil Mar 14, 2026
a3a37a0
feat: separate Live Activity and APN settings into distinct menus
MtlPhil Mar 14, 2026
6f43a2c
Added Live Activity menu
MtlPhil Mar 14, 2026
48ddc77
chore: add LiveActivitySettingsView to Xcode project
MtlPhil Mar 14, 2026
fc0bafd
merge: integrate upstream live-activity (Mac Catalyst guards + renewa…
MtlPhil Mar 14, 2026
5939ed9
fix: LA tap navigation, manual dismissal prevention, and toggle start
MtlPhil Mar 14, 2026
ef3f2f5
fix: end Live Activity on app force-quit
MtlPhil Mar 14, 2026
11aeadd
fix: use dismissedByUser flag instead of disabling laEnabled on manua…
MtlPhil Mar 14, 2026
c81911c
fix: dismiss modal (Settings sheet) before tab switch on LA tap
MtlPhil Mar 14, 2026
9ccc806
fix: LA tap navigation timing and LA reappear-after-dismiss
MtlPhil Mar 15, 2026
31a8e97
fix: handle loopfollow://la-tap URL in SceneDelegate, not AppDelegate
MtlPhil Mar 15, 2026
26b244e
Live Activity — UX Improvements and Reliability Fixes (#540)
MtlPhil Mar 15, 2026
ad647e5
feat: configurable LA grid slots + full InfoType snapshot coverage
MtlPhil Mar 15, 2026
0401c48
fix: label delta and footer on lock screen LA card
MtlPhil Mar 15, 2026
f42e502
docs: add PR description for configurable LA grid slots
MtlPhil Mar 15, 2026
b8c19cf
Update PR_configurable_slots.md
MtlPhil Mar 15, 2026
9b36dab
Merge dev into live-activity; replace CryptoSwift with CryptoKit
bjorkert Mar 15, 2026
b925d8a
Merge upstream/live-activity: resolve conflicts, keep extended InfoTy…
MtlPhil Mar 15, 2026
c7c9a59
Merge remote-tracking branch 'origin/live-activity' into live-activity
MtlPhil Mar 15, 2026
b571cad
chore: remove PR notes from tracking, keep docs/LiveActivity.md only
MtlPhil Mar 15, 2026
fec3f79
Configurable Live Activity Grid Slots + Full InfoType Snapshot Covera…
MtlPhil Mar 15, 2026
83ba7c5
Linting
bjorkert Mar 15, 2026
a20f3ec
Fix PRODUCT_BUNDLE_IDENTIFIER for Tests
bjorkert Mar 15, 2026
145744c
fix: include all extended InfoType fields in APNs push payload
MtlPhil Mar 15, 2026
191a1e4
Merge upstream/live-activity: apply linting fixes
MtlPhil Mar 15, 2026
dfe53b3
feat: add small family view for CarPlay Dashboard and Watch Smart Stack
MtlPhil Mar 16, 2026
2f28a1f
fix: include all extended InfoType fields in APNs push payload (#548)
MtlPhil Mar 16, 2026
a98f0a8
fix: guard CarPlay/Watch small family behind iOS 18 availability; inc…
MtlPhil Mar 16, 2026
65e679a
fix: move if #available into Widget.body to avoid WidgetBundleBuilder…
MtlPhil Mar 16, 2026
82e76a4
fix: use two separate single-branch if #available in bundle for CarPl…
MtlPhil Mar 16, 2026
17db9e9
merge: resolve conflicts with upstream/live-activity; keep renewal ov…
MtlPhil Mar 16, 2026
0183a9d
Live Activity: CarPlay Dashboard + Apple Watch Smart Stack support (#…
MtlPhil Mar 16, 2026
98de416
fix: restore two-widget bundle; guard supplementalActivityFamilies an…
MtlPhil Mar 16, 2026
e8dadda
fix: extension version inherits from parent; remove spurious await in…
MtlPhil Mar 16, 2026
9f9229a
Live Activity: fix iOS 18 availability guards, extension version, and…
MtlPhil Mar 16, 2026
83f4ad3
fix: prevent glucose + trend arrow clipping on wide mmol/L values
MtlPhil Mar 16, 2026
426fa3d
Live Activity: fix glucose + trend arrow clipping on wide mmol/L valu…
MtlPhil Mar 17, 2026
8b9fe86
Merge branch 'dev' into live-activity
bjorkert Mar 17, 2026
e20ec46
chore: remove redundant @available(iOS 16.1) guards
MtlPhil Mar 17, 2026
775b83d
Fix Live Activity glucose overflow with flexible layout and tighter g…
MtlPhil Mar 17, 2026
37c1a71
chore: remove redundant @available(iOS 16.1) guards
MtlPhil Mar 17, 2026
d99e778
Fix Live Activity glucose overflow with flexible layout and tighter g…
MtlPhil Mar 17, 2026
68d2a06
fix: restart LA on foreground when renewal overlay is showing
MtlPhil Mar 17, 2026
749264b
fix: recover from audio session failure and alert user via LA overlay
MtlPhil Mar 17, 2026
3769275
Update BackgroundTaskAudio.swift
MtlPhil Mar 18, 2026
27a6efc
Live Activity: foreground restart on overlay, audio session recovery,…
MtlPhil Mar 18, 2026
a26894d
Update BackgroundTaskAudio.swift
MtlPhil Mar 18, 2026
e8ee805
Update BackgroundTaskAudio.swift
MtlPhil Mar 18, 2026
cffc043
Update LiveActivityManager.swift
MtlPhil Mar 18, 2026
61a6035
Update LiveActivityManager.swift
MtlPhil Mar 18, 2026
d4f5c8c
Merge branch 'pr-555' into live-activity
bjorkert Mar 18, 2026
adbec89
Linting
bjorkert Mar 18, 2026
f677b2c
Removed CLAUDE.md
bjorkert Mar 18, 2026
9e16ba9
Merge branch 'dev' into live-activity
bjorkert Mar 19, 2026
01e2c1b
Removed duplicate code
bjorkert Mar 19, 2026
b3f2436
Live activity - final fixes (#557)
MtlPhil Mar 20, 2026
fa26039
Update to 5.0
bjorkert Mar 20, 2026
db5ddbf
Remove unnecessary @available(iOS 16.4) checks
bjorkert Mar 21, 2026
a62595c
BGAppRefreshTask audio recovery; LA expiry notification; code quality…
MtlPhil Mar 22, 2026
84e1736
Live Activity: CarPlay/Watch Smart Stack widget + BFU crash fix + BGA…
MtlPhil Mar 24, 2026
2576dca
Linting
bjorkert Mar 24, 2026
2a5c1ef
Fix JWT cache thread-safety to prevent TooManyProviderTokenUpdates
bjorkert Mar 24, 2026
6792ff6
Bug fixes and SmallFamilyView configurable slot (#575)
bjorkert Mar 25, 2026
47a9a97
Linting
bjorkert Mar 25, 2026
1004dee
Merge branch 'dev' into live-activity
bjorkert Mar 25, 2026
b390eae
Added the projected value for Trio
bjorkert Mar 25, 2026
6f98fac
Fix LA not refreshing on foreground after stale overlay (#576)
MtlPhil Mar 26, 2026
6de1ae0
Fix stale LA dismissed by iOS incorrectly blocking auto-restart (#577)
MtlPhil Mar 26, 2026
3d62d8e
Fix stale overlay tap + redesign expanded Dynamic Island (#581)
MtlPhil Mar 28, 2026
b0ef1a0
Fix for incorrect JWT token cache invalidation
bjorkert Mar 29, 2026
b89d3af
Fix stale overlay tap: redesign .dismissed state machine (#585)
MtlPhil Mar 29, 2026
6f51e71
Merge remote-tracking branch 'origin/dev' into live-activity
bjorkert Mar 30, 2026
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,6 @@ fastlane/test_output
fastlane/FastlaneRunner

LoopFollowConfigOverride.xcconfig
.history
.history*.xcuserstate
docs/PR_configurable_slots.md
docs/LiveActivityTestPlan.md
332 changes: 282 additions & 50 deletions LoopFollow.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

This file was deleted.

48 changes: 34 additions & 14 deletions LoopFollow/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import EventKit
import UIKit
import UserNotifications

@UIApplicationMain
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let notificationCenter = UNUserNotificationCenter.current()
Expand All @@ -32,7 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

let action = UNNotificationAction(identifier: "OPEN_APP_ACTION", title: "Open App", options: .foreground)
let category = UNNotificationCategory(identifier: "loopfollow.background.alert", actions: [action], intentIdentifiers: [], options: [])
let category = UNNotificationCategory(identifier: BackgroundAlertIdentifier.categoryIdentifier, actions: [action], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])

UNUserNotificationCenter.current().delegate = self
Expand All @@ -45,50 +45,64 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}

BackgroundRefreshManager.shared.register()

// Detect Before-First-Unlock launch. If protected data is unavailable here,
// StorageValues were cached from encrypted UserDefaults and need a reload
// on the first foreground after the user unlocks.
let bfu = !UIApplication.shared.isProtectedDataAvailable
Storage.shared.needsBFUReload = bfu
LogManager.shared.log(category: .general, message: "BFU check: isProtectedDataAvailable=\(!bfu), needsBFUReload=\(bfu)")

return true
}

func applicationWillTerminate(_: UIApplication) {}
func applicationWillTerminate(_: UIApplication) {
#if !targetEnvironment(macCatalyst)
LiveActivityManager.shared.endOnTerminate()
#endif
}

// MARK: - Remote Notifications

// Called when successfully registered for remote notifications
/// Called when successfully registered for remote notifications
func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()

Observable.shared.loopFollowDeviceToken.value = tokenString

LogManager.shared.log(category: .general, message: "Successfully registered for remote notifications with token: \(tokenString)")
LogManager.shared.log(category: .apns, message: "Successfully registered for remote notifications with token: \(tokenString)")
}

// Called when failed to register for remote notifications
/// Called when failed to register for remote notifications
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
LogManager.shared.log(category: .general, message: "Failed to register for remote notifications: \(error.localizedDescription)")
LogManager.shared.log(category: .apns, message: "Failed to register for remote notifications: \(error.localizedDescription)")
}

// Called when a remote notification is received
/// Called when a remote notification is received
func application(_: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
LogManager.shared.log(category: .general, message: "Received remote notification: \(userInfo)")
LogManager.shared.log(category: .apns, message: "Received remote notification: \(userInfo)")

// Check if this is a response notification from Loop or Trio
if let aps = userInfo["aps"] as? [String: Any] {
// Handle visible notification (alert, sound, badge)
if let alert = aps["alert"] as? [String: Any] {
let title = alert["title"] as? String ?? ""
let body = alert["body"] as? String ?? ""
LogManager.shared.log(category: .general, message: "Notification - Title: \(title), Body: \(body)")
LogManager.shared.log(category: .apns, message: "Notification - Title: \(title), Body: \(body)")
}

// Handle silent notification (content-available)
if let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 {
// This is a silent push, nothing implemented but logging for now

if let commandStatus = userInfo["command_status"] as? String {
LogManager.shared.log(category: .general, message: "Command status: \(commandStatus)")
LogManager.shared.log(category: .apns, message: "Command status: \(commandStatus)")
}

if let commandType = userInfo["command_type"] as? String {
LogManager.shared.log(category: .general, message: "Command type: \(commandType)")
LogManager.shared.log(category: .apns, message: "Command type: \(commandType)")
}
}
}
Expand All @@ -97,6 +111,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
completionHandler(.newData)
}

// MARK: - URL handling

// Note: with scene-based lifecycle (iOS 13+), URLs are delivered to
// SceneDelegate.scene(_:openURLContexts:) — not here. The scene delegate
// handles <urlScheme>://la-tap for Live Activity tap navigation.

// MARK: UISceneSession Lifecycle

func application(_: UIApplication, willFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Expand All @@ -110,7 +130,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) {
Expand Down Expand Up @@ -166,7 +186,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if response.actionIdentifier == "OPEN_APP_ACTION" {
if let window = window {
if let window {
window.rootViewController?.dismiss(animated: true, completion: nil)
window.rootViewController?.present(MainViewController(), animated: true, completion: nil)
}
Expand Down
14 changes: 12 additions & 2 deletions LoopFollow/Application/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}

func scene(_: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard URLContexts.contains(where: { $0.url.scheme == AppGroupID.urlScheme && $0.url.host == "la-tap" }) else { return }
// scene(_:openURLContexts:) fires after sceneDidBecomeActive when the app
// foregrounds from background. Post on the next run loop so the view
// hierarchy (including any presented modals) is fully settled.
DispatchQueue.main.async {
NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil)
}
}

func sceneWillResignActive(_: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
Expand All @@ -53,7 +63,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}

// Handle the UIApplicationShortcutItem when the user taps on the Home Screen Quick Action. This function toggles the "Speak BG" setting in UserDefaultsRepository, speaks the current state (on/off) using AVSpeechSynthesizer, and updates the Quick Action appearance.
/// Handle the UIApplicationShortcutItem when the user taps on the Home Screen Quick Action. This function toggles the "Speak BG" setting in UserDefaultsRepository, speaks the current state (on/off) using AVSpeechSynthesizer, and updates the Quick Action appearance.
func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
if let bundleIdentifier = Bundle.main.bundleIdentifier {
let expectedType = bundleIdentifier + ".toggleSpeakBG"
Expand All @@ -66,7 +76,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}

// The following method is called when the user taps on the Home Screen Quick Action
/// The following method is called when the user taps on the Home Screen Quick Action
func windowScene(_: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler _: @escaping (Bool) -> Void) {
handleShortcutItem(shortcutItem)
}
Expand Down
25 changes: 19 additions & 6 deletions LoopFollow/Controllers/BackgroundAlertManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,24 @@ enum BackgroundAlertDuration: TimeInterval, CaseIterable {
case eighteenMinutes = 1080 // 18 minutes in seconds
}

/// Enum representing unique identifiers for each background alert.
enum BackgroundAlertIdentifier: String, CaseIterable {
case sixMin = "loopfollow.background.alert.6min"
case twelveMin = "loopfollow.background.alert.12min"
case eighteenMin = "loopfollow.background.alert.18min"
/// Unique identifiers for each background alert, scoped to the current bundle
/// so multiple LoopFollow instances don't interfere with each other's notifications.
enum BackgroundAlertIdentifier: CaseIterable {
case sixMin
case twelveMin
case eighteenMin

private static let prefix = Bundle.main.bundleIdentifier ?? "loopfollow"

var rawValue: String {
switch self {
case .sixMin: "\(Self.prefix).background.alert.6min"
case .twelveMin: "\(Self.prefix).background.alert.12min"
case .eighteenMin: "\(Self.prefix).background.alert.18min"
}
}

static let categoryIdentifier = "\(prefix).background.alert"
}

class BackgroundAlertManager {
Expand Down Expand Up @@ -118,7 +131,7 @@ class BackgroundAlertManager {
content.title = title
content.body = body
content.sound = .defaultCritical
content.categoryIdentifier = "loopfollow.background.alert"
content.categoryIdentifier = BackgroundAlertIdentifier.categoryIdentifier
return content
}

Expand Down
10 changes: 10 additions & 0 deletions LoopFollow/Controllers/Nightscout/BGData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,19 @@ extension MainViewController {
Observable.shared.deltaText.value = "+" + Localizer.toDisplayUnits(String(deltaBG))
}

// Live Activity storage
Storage.shared.lastBgReadingTimeSeconds.value = lastBGTime
Storage.shared.lastDeltaMgdl.value = Double(deltaBG)
Storage.shared.lastTrendCode.value = entries[latestEntryIndex].direction

// Mark BG data as loaded for initial loading state
self.markDataLoaded("bg")

// Live Activity update
#if !targetEnvironment(macCatalyst)
LiveActivityManager.shared.refreshFromCurrentState(reason: "bg")
#endif

// Update contact
if Storage.shared.contactEnabled.value {
self.contactImageUpdater
Expand Down
11 changes: 11 additions & 0 deletions LoopFollow/Controllers/Nightscout/DeviceStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ extension MainViewController {

if IsNightscoutEnabled(), (now - lastLoopTime) >= nonLoopingTimeThreshold, lastLoopTime > 0 {
IsNotLooping = true
Observable.shared.isNotLooping.value = true
statusStackView.distribution = .fill

PredictionLabel.isHidden = true
Expand All @@ -55,9 +56,13 @@ extension MainViewController {
LoopStatusLabel.text = "⚠️ Not Looping!"
LoopStatusLabel.textColor = UIColor.systemYellow
LoopStatusLabel.font = UIFont.boldSystemFont(ofSize: 18)
#if !targetEnvironment(macCatalyst)
LiveActivityManager.shared.refreshFromCurrentState(reason: "notLooping")
#endif

} else {
IsNotLooping = false
Observable.shared.isNotLooping.value = false
statusStackView.distribution = .fillEqually
PredictionLabel.isHidden = false

Expand All @@ -72,6 +77,9 @@ extension MainViewController {
case .system:
LoopStatusLabel.textColor = UIColor.label
}
#if !targetEnvironment(macCatalyst)
LiveActivityManager.shared.refreshFromCurrentState(reason: "loopingResumed")
#endif
}
}

Expand Down Expand Up @@ -119,14 +127,17 @@ extension MainViewController {
let storedTime = Observable.shared.alertLastLoopTime.value ?? 0
if lastPumpTime > storedTime {
Observable.shared.alertLastLoopTime.value = lastPumpTime
Storage.shared.lastLoopTime.value = lastPumpTime
}

if let reservoirData = lastPumpRecord["reservoir"] as? Double {
latestPumpVolume = reservoirData
infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U")
Storage.shared.lastPumpReservoirU.value = reservoirData
} else {
latestPumpVolume = 50.0
infoManager.updateInfoData(type: .pump, value: "50+U")
Storage.shared.lastPumpReservoirU.value = nil
}
}

Expand Down
Loading