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: