diff --git a/tabatago-swift/TabataGo/Resources/Info.plist b/tabatago-swift/TabataGo/Resources/Info.plist
index 9af55fc..36ae005 100644
--- a/tabatago-swift/TabataGo/Resources/Info.plist
+++ b/tabatago-swift/TabataGo/Resources/Info.plist
@@ -28,7 +28,7 @@
CFBundleVersion
- 1
+ 2
NSHealthShareUsageDescription
TabataGo reads your health data to show fitness stats and personalize your workouts.
NSHealthUpdateUsageDescription
diff --git a/tabatago-swift/TabataGo/Services/AudioService.swift b/tabatago-swift/TabataGo/Services/AudioService.swift
index 0b053a3..ec8c6b4 100644
--- a/tabatago-swift/TabataGo/Services/AudioService.swift
+++ b/tabatago-swift/TabataGo/Services/AudioService.swift
@@ -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 ─────────────────────────────────────────
diff --git a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift
index 087cd8c..ec53046 100644
--- a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift
+++ b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift
@@ -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,10 @@ final class PlayerViewModel: ObservableObject {
attributes: attrs,
content: ActivityContent(state: state, staleDate: staleDate)
)
+ liveActivityError = nil
observeActivityState()
} catch {
+ liveActivityError = error.localizedDescription
print("❌ Failed to start Live Activity: \(error.localizedDescription)")
}
}
diff --git a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift
index c43cb71..ea6a7a8 100644
--- a/tabatago-swift/TabataGo/Views/Player/PlayerView.swift
+++ b/tabatago-swift/TabataGo/Views/Player/PlayerView.swift
@@ -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)
// ══════════════════════════════════════════════════
diff --git a/tabatago-swift/TabataGoWatch/Complications/Info.plist b/tabatago-swift/TabataGoWatch/Complications/Info.plist
index e556090..5bfd1a7 100644
--- a/tabatago-swift/TabataGoWatch/Complications/Info.plist
+++ b/tabatago-swift/TabataGoWatch/Complications/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.0
CFBundleVersion
- 1
+ 2
NSExtension
NSExtensionPointIdentifier
diff --git a/tabatago-swift/TabataGoWatch/Resources/Info.plist b/tabatago-swift/TabataGoWatch/Resources/Info.plist
index 0eed266..ea4cb6f 100644
--- a/tabatago-swift/TabataGoWatch/Resources/Info.plist
+++ b/tabatago-swift/TabataGoWatch/Resources/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.0
CFBundleVersion
- 1
+ 2
NSHealthShareUsageDescription
TabataGo reads your heart rate and calories during workouts.
NSHealthUpdateUsageDescription
diff --git a/tabatago-swift/TabataGoWidget/Info.plist b/tabatago-swift/TabataGoWidget/Info.plist
index fb382bf..b64cae1 100644
--- a/tabatago-swift/TabataGoWidget/Info.plist
+++ b/tabatago-swift/TabataGoWidget/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.0
CFBundleVersion
- 1
+ 2
NSExtension
NSExtensionPointIdentifier