diff --git a/.github/workflows/pr-iphone-deploy.yml b/.github/workflows/pr-iphone-deploy.yml
index 4fbb346..d7b6b42 100644
--- a/.github/workflows/pr-iphone-deploy.yml
+++ b/.github/workflows/pr-iphone-deploy.yml
@@ -106,16 +106,15 @@ jobs:
# 1. Try WiFi first (Xcode 15+ devicectl network discovery)
echo "📱 Trying WiFi install via devicectl..."
- xcrun devicectl device install app --device "$UDID" "$APP" 2>&1 | tail -3
- if [ ${PIPESTATUS[0]} -eq 0 ]; then
+ if xcrun devicectl device install app --device "$UDID" "$APP" > /tmp/devicectl.log 2>&1; then
echo "✅ Installed via WiFi"
exit 0
fi
+ tail -10 /tmp/devicectl.log
# 2. Fallback to USB via ios-deploy
echo "🔌 WiFi failed, trying USB..."
- ios-deploy --bundle "$APP" --id "$UDID" --justlaunch
- if [ $? -eq 0 ]; then
+ if ios-deploy --bundle "$APP" --id "$UDID" --justlaunch; then
echo "✅ Installed via USB"
exit 0
fi
@@ -123,6 +122,7 @@ jobs:
echo "❌ Install failed (both WiFi and USB)"
exit 1
+
- name: Post "Ready to test" comment
env:
GT_TOKEN: ${{ secrets.PR_API_TOKEN }}
diff --git a/tabatago-swift/TabataGo/Resources/Info.plist b/tabatago-swift/TabataGo/Resources/Info.plist
index 9af55fc..38f2537 100644
--- a/tabatago-swift/TabataGo/Resources/Info.plist
+++ b/tabatago-swift/TabataGo/Resources/Info.plist
@@ -18,17 +18,8 @@
APPL
CFBundleShortVersionString
1.0
- CFBundleURLTypes
-
-
- CFBundleURLSchemes
-
- tabatago
-
-
-
CFBundleVersion
- 1
+ 2
NSHealthShareUsageDescription
TabataGo reads your health data to show fitness stats and personalize your workouts.
NSHealthUpdateUsageDescription
diff --git a/tabatago-swift/TabataGo/Resources/TabataGo.entitlements b/tabatago-swift/TabataGo/Resources/TabataGo.entitlements
index 800b1a1..c722533 100644
--- a/tabatago-swift/TabataGo/Resources/TabataGo.entitlements
+++ b/tabatago-swift/TabataGo/Resources/TabataGo.entitlements
@@ -9,6 +9,8 @@
health-records
com.apple.security.application-groups
-
+
+ group.fr.millianlmx.tabatago
+
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..88641a7 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,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)")
}
}
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..5372fa0 100644
--- a/tabatago-swift/TabataGoWidget/Info.plist
+++ b/tabatago-swift/TabataGoWidget/Info.plist
@@ -4,8 +4,6 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- TabataGoWidget
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift b/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift
index 59441fe..a84ea57 100644
--- a/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift
+++ b/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift
@@ -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])
}
diff --git a/tabatago-swift/project.yml b/tabatago-swift/project.yml
index 7f7d5db..0fe03fe 100644
--- a/tabatago-swift/project.yml
+++ b/tabatago-swift/project.yml
@@ -60,6 +60,10 @@ targets:
SUPABASE_ANON_KEY: $(SUPABASE_ANON_KEY)
REVENUECAT_API_KEY: $(REVENUECAT_API_KEY)
POSTHOG_API_KEY: $(POSTHOG_API_KEY)
+ NSSupportsLiveActivities: true
+ NSSupportsLiveActivitiesFrequentUpdates: true
+ UIBackgroundModes:
+ - audio
entitlements:
path: TabataGo/Resources/TabataGo.entitlements
properties:
@@ -76,6 +80,8 @@ targets:
- target: TabataGoWatch
embed: true
+ - target: TabataGoWidget
+ embed: true
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago
@@ -156,6 +162,30 @@ targets:
TARGETED_DEVICE_FAMILY: "4"
WATCHOS_DEPLOYMENT_TARGET: "11.0"
+ TabataGoWidget:
+ type: app-extension
+ platform: iOS
+ deploymentTarget: "26.0"
+ sources:
+ - path: TabataGoWidget
+ excludes:
+ - "**/.DS_Store"
+ - path: TabataGo/Models/WorkoutActivityAttributes.swift
+ - path: TabataGo/Models/MusicActivityAttributes.swift
+ info:
+ path: TabataGoWidget/Info.plist
+ properties:
+ NSExtension:
+ NSExtensionPointIdentifier: com.apple.widgetkit-extension
+ NSSupportsLiveActivities: true
+ NSSupportsLiveActivitiesFrequentUpdates: true
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.widget
+ IPHONEOS_DEPLOYMENT_TARGET: "26.0"
+ TARGETED_DEVICE_FAMILY: "1"
+ ENABLE_PREVIEWS: YES
+
TabataGoTests:
type: bundle.unit-test
platform: iOS
@@ -185,6 +215,7 @@ schemes:
build:
targets:
TabataGo: all
+ TabataGoWidget: all
run:
config: Debug
archive: