6 Commits

Author SHA1 Message Date
Millian Lamiaux
807361449b fix: remove info.properties from widget target — use on-disk Info.plist with NSSupportsLiveActivities
Some checks failed
CI / Detect Changes (pull_request) Successful in 4s
CI / Admin Web CI (pull_request) Has been skipped
CI / YouTube Worker (pull_request) Has been skipped
CI / Deploy (pull_request) Has been skipped
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Failing after 2m47s
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Has been skipped
2026-07-04 18:26:12 +02:00
Millian Lamiaux
ee01380be8 fix: remove remaining force-unwrap in widgetURL (AGENTS.md §7) 2026-07-04 18:19:37 +02:00
Millian Lamiaux
210c254298 fix: scope shared model sources to TabataGoWidget only
Some checks failed
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Successful in 2m33s
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Failing after 33s
CI / Detect Changes (pull_request) Successful in 3s
CI / Admin Web CI (pull_request) Has been skipped
CI / YouTube Worker (pull_request) Has been skipped
CI / Deploy (pull_request) Has been skipped
Previous sed command accidentally injected shared model files into
TabataGo, TabataGoWatch, and TabataGoWatchWidget targets, corrupting
their YAML structure. Restored all targets; shared files now correctly
scoped to TabataGoWidget target only.
2026-07-04 18:15:11 +02:00
Millian Lamiaux
1bb6a544e4 fix: add shared model sources to TabataGoWidget target
Some checks failed
CI / Detect Changes (pull_request) Successful in 3s
CI / Admin Web CI (pull_request) Has been skipped
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Failing after 8s
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Has been skipped
CI / YouTube Worker (pull_request) Has been skipped
CI / Deploy (pull_request) Has been skipped
Widget extension references WorkoutPhase, WorkoutActivityAttributes,
TogglePauseIntent (WorkoutLiveActivity) and MusicActivityAttributes
(MusicLiveActivity) but cannot import parent app module. Shared source
files must be compiled in both targets, following existing pattern
of WatchConnectivityTypes.swift shared between iOS/watch.
2026-07-04 18:11:16 +02:00
Millian Lamiaux
a17367081c fix: add widget target, push entitlement, error handling, force-unwrap
Some checks failed
CI / Deploy (pull_request) Has been skipped
CI / Detect Changes (pull_request) Successful in 3s
CI / Admin Web CI (pull_request) Has been skipped
CI / YouTube Worker (pull_request) Has been skipped
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Failing after 46s
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Has been skipped
Phase 1 — Widget extension target:
- Add TabataGoWidget target to project.yml (app-extension, iOS 26.0)
- Embed in TabataGo, add to scheme build targets

Phase 2 — Push notification capability:
- Add aps-environment: development to TabataGo.entitlements
- Required by iOS 26 for AppIntents-based Live Activity buttons

Phase 3 — Error handling hardening:
- Catch ActivityAuthorizationError specifically with user message
- Truncate generic errors to 200 chars to avoid UI overflow
- Fix force-unwrap at WorkoutLiveActivity.swift:556 (widgetURL)
2026-07-04 18:07:59 +02:00
Millian Lamiaux
40ffb04f31 fix: Dynamic Island, Live Activity errors, background audio stop, CFBundleVersion
Some checks failed
CI / Deploy (pull_request) Has been skipped
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Failing after 8m7s
CI / Detect Changes (pull_request) Successful in 3s
CI / Admin Web CI (pull_request) Has been skipped
CI / YouTube Worker (pull_request) Has been skipped
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Successful in 2m58s
Phase 1 — Audio background resilience:
- Remove .mixWithOthers, app takes audio focus for coach cues
- Add AVAudioSession.interruptionNotification handler (re-activates after calls/Siri)
- Add AVAudioSession.routeChangeNotification handler (re-activates on headphone changes)

Phase 2 — Live Activity error surfacing:
- PlayerViewModel: @Published liveActivityError captures auth-denied + request errors
- PlayerView: translucent error banner with dismiss button (non-blocking)

Phase 3 — CFBundleVersion 1→2 in all 4 Info.plist files (match project.yml CURRENT_PROJECT_VERSION: 2)
2026-07-04 17:54:09 +02:00
9 changed files with 95 additions and 18 deletions

View File

@@ -28,7 +28,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<string>2</string>
<key>NSHealthShareUsageDescription</key>
<string>TabataGo reads your health data to show fitness stats and personalize your workouts.</string>
<key>NSHealthUpdateUsageDescription</key>

View File

@@ -8,6 +8,8 @@
<array>
<string>health-records</string>
</array>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array/>
</dict>

View File

@@ -28,12 +28,55 @@ final class AudioService {
try audioSession.setCategory(
.playback,
mode: .default,
options: [.mixWithOthers, .allowAirPlay, .allowBluetooth]
options: [.allowAirPlay, .allowBluetooth]
)
try audioSession.setActive(true)
} catch {
print("[Audio] Session configuration failed: \(error)")
}
// Interruption Handler
NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: nil,
queue: .main
) { [weak self] notification in
guard let self,
let typeValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
if type == .ended {
guard let optionsValue = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt,
AVAudioSession.InterruptionOptions(rawValue: optionsValue).contains(.shouldResume) else { return }
do {
try self.audioSession.setActive(true)
} catch {
print("[Audio] Re-activation failed after interruption: \(error)")
}
}
}
// Route Change Handler
NotificationCenter.default.addObserver(
forName: AVAudioSession.routeChangeNotification,
object: nil,
queue: .main
) { [weak self] notification in
guard let self,
let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
switch reason {
case .newDeviceAvailable, .oldDeviceUnavailable:
do {
try self.audioSession.setActive(true)
} catch {
print("[Audio] Route re-activation failed: \(error)")
}
default:
break
}
}
}
// Phase Audio Cues

View File

@@ -39,6 +39,7 @@ final class PlayerViewModel: ObservableObject {
@Published var isComplete: Bool = false
@Published var showExitConfirmation: Bool = false
@Published private(set) var completedSession: WorkoutSession? = nil
@Published var liveActivityError: String? = nil
// Track info for Live Activity set by View when music changes
var currentTrackTitle = ""
@@ -382,7 +383,10 @@ final class PlayerViewModel: ObservableObject {
// Dynamic Island / Live Activity
func syncActivity(shouldAlert: Bool = false) {
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
liveActivityError = "Live Activities are disabled in Settings"
return
}
guard isRunning else { return }
let isPlayingMusic = (phase == .work || phase == .rest) && isRunning && !isPaused
@@ -449,8 +453,14 @@ final class PlayerViewModel: ObservableObject {
attributes: attrs,
content: ActivityContent(state: state, staleDate: staleDate)
)
liveActivityError = nil
observeActivityState()
} catch let authError as ActivityAuthorizationError {
liveActivityError = "Live Activities: \(authError.localizedDescription)"
} catch {
let msg = error.localizedDescription
let truncated = msg.count > 200 ? String(msg.prefix(200)) + "" : msg
liveActivityError = "Live Activity error: \(truncated)"
print("❌ Failed to start Live Activity: \(error.localizedDescription)")
}
}

View File

@@ -66,6 +66,40 @@ struct PlayerView: View {
}
}
//
// Live Activity error banner (non-blocking)
//
if let error = vm.liveActivityError {
VStack {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.yellow)
Text(error)
.font(.caption2.weight(.medium))
.foregroundStyle(.white.opacity(0.9))
Spacer()
Button {
vm.liveActivityError = nil
} label: {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .bold))
.foregroundStyle(.white.opacity(0.6))
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(.ultraThinMaterial.opacity(0.9))
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal, 16)
.padding(.top, 60)
Spacer()
}
.transition(.move(edge: .top).combined(with: .opacity))
.animation(.spring(duration: 0.3), value: vm.liveActivityError)
.zIndex(100)
}
//
// Layer 4 Compact timer ring (top-right corner)
//

View File

@@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>2</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>2</string>
<key>NSHealthShareUsageDescription</key>
<string>TabataGo reads your heart rate and calories during workouts.</string>
<key>NSHealthUpdateUsageDescription</key>

View File

@@ -553,7 +553,7 @@ struct WorkoutLiveActivity: Widget {
.contentMargins(.leading, 8, for: .expanded)
.contentMargins(.trailing, 8, for: .expanded)
.contentMargins(.bottom, 6, for: .expanded)
.widgetURL(URL(string: "tabatago://workout")!)
.widgetURL(URL(string: "tabatago://workout"))
}
.supplementalActivityFamilies([.small, .medium])
}

View File

@@ -166,25 +166,13 @@ targets:
- path: TabataGoWidget
excludes:
- "**/.DS_Store"
# Shared attribute types — needed by both main app and widget extension
- path: TabataGo/Models/WorkoutActivityAttributes.swift
group: TabataGoWidget
- path: TabataGo/Models/MusicActivityAttributes.swift
group: TabataGoWidget
info:
path: TabataGoWidget/Info.plist
properties:
CFBundleDisplayName: TabataGoWidget
CFBundleShortVersionString: "1.0"
CFBundleVersion: "2"
NSExtension:
NSExtensionPointIdentifier: com.apple.widgetkit-extension
NSSupportsLiveActivities: true
NSSupportsLiveActivitiesFrequentUpdates: true
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.widget
INFOPLIST_FILE: TabataGoWidget/Info.plist
IPHONEOS_DEPLOYMENT_TARGET: "26.0"
TARGETED_DEVICE_FAMILY: "1"
ENABLE_PREVIEWS: YES