1 Commits

Author SHA1 Message Date
Millian Lamiaux
174f4ebf7a fix: add missing iOS widget target for Dynamic Island / Live Activities
All checks were successful
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
PR → Build → WiFi/USB Deploy → LGTM / Wait for LGTM comment (pull_request) Successful in 20m49s
PR → Build → WiFi/USB Deploy → LGTM / Build & Deploy to iPhone (WiFi/USB) (pull_request) Successful in 9m11s
The TabataGoWidget code existed on disk but had no corresponding
target in project.yml, so the widget extension was never compiled
or embedded in deployed builds — Dynamic Island never appeared.

Added:
- TabataGoWidget app-extension target (iOS 26.0) with shared models
- Embed dependency in TabataGo target
- Widget in scheme build targets for Xcode discoverability
- Fix CFBundleVersion in Info.plist (1 → 2) to match main app

Config: NSSupportsLiveActivities + FrequentUpdates, correct bundle ID
(fr.millianlmx.tabatago.widget), TARGETED_DEVICE_FAMILY: "1"
2026-07-04 17:21:33 +02:00
11 changed files with 30 additions and 105 deletions

View File

@@ -106,15 +106,16 @@ jobs:
# 1. Try WiFi first (Xcode 15+ devicectl network discovery)
echo "📱 Trying WiFi install via devicectl..."
if xcrun devicectl device install app --device "$UDID" "$APP" > /tmp/devicectl.log 2>&1; then
xcrun devicectl device install app --device "$UDID" "$APP" 2>&1 | tail -3
if [ ${PIPESTATUS[0]} -eq 0 ]; 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..."
if ios-deploy --bundle "$APP" --id "$UDID" --justlaunch; then
ios-deploy --bundle "$APP" --id "$UDID" --justlaunch
if [ $? -eq 0 ]; then
echo "✅ Installed via USB"
exit 0
fi
@@ -122,7 +123,6 @@ jobs:
echo "❌ Install failed (both WiFi and USB)"
exit 1
- name: Post "Ready to test" comment
env:
GT_TOKEN: ${{ secrets.PR_API_TOKEN }}

View File

@@ -18,8 +18,17 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>tabatago</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>2</string>
<string>1</string>
<key>NSHealthShareUsageDescription</key>
<string>TabataGo reads your health data to show fitness stats and personalize your workouts.</string>
<key>NSHealthUpdateUsageDescription</key>

View File

@@ -9,8 +9,6 @@
<string>health-records</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.fr.millianlmx.tabatago</string>
</array>
<array/>
</dict>
</plist>

View File

@@ -28,55 +28,12 @@ final class AudioService {
try audioSession.setCategory(
.playback,
mode: .default,
options: [.allowAirPlay, .allowBluetooth]
options: [.mixWithOthers, .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,7 +39,6 @@ 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 = ""
@@ -383,10 +382,7 @@ final class PlayerViewModel: ObservableObject {
// Dynamic Island / Live Activity
func syncActivity(shouldAlert: Bool = false) {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
liveActivityError = "Live Activities are disabled in Settings"
return
}
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
guard isRunning else { return }
let isPlayingMusic = (phase == .work || phase == .rest) && isRunning && !isPaused
@@ -453,14 +449,8 @@ 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,40 +66,6 @@ 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>2</string>
<string>1</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>2</string>
<string>1</string>
<key>NSHealthShareUsageDescription</key>
<string>TabataGo reads your heart rate and calories during workouts.</string>
<key>NSHealthUpdateUsageDescription</key>

View File

@@ -4,6 +4,8 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>TabataGoWidget</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -17,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

@@ -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

@@ -60,10 +60,6 @@ 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:
@@ -170,11 +166,17 @@ 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
@@ -182,6 +184,7 @@ targets:
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