Files
evgeniyChepelev 315283822c iOS home screen widget (#78)
* Add Home Screen widget with VPN toggle and refactor app activation logic

- Add NetBirdWidgetExtension target with small/medium widget sizes
- Support direct connect/disconnect from widget via interactive buttons (iOS 17+)
- Detect missing VPN config or login-required state and open app via deep link
- Poll for stable VPN state after toggle to prevent loader getting stuck
- Add widget shared state keys to GlobalConstants and sync status from MainViewModel
- Fix false "authentication required" alert on app resume after widget disconnect
- Deduplicate app activation logic into shared startActivation/stopActivation
- Extract polling helpers: updateDetailsIfChanged, updatePeersIfChanged, applyExtensionStatus
2026-04-14 10:30:08 +02:00

68 lines
1.9 KiB
Swift

import SwiftUI
import NetworkExtension
import WidgetKit
enum WidgetVPNStatus: String {
case connected
case connecting
case disconnecting
case disconnected
var displayText: String {
switch self {
case .connected: return "Connected"
case .connecting: return "Connecting..."
case .disconnecting: return "Disconnecting..."
case .disconnected: return "Disconnected"
}
}
var isTransitioning: Bool {
self == .connecting || self == .disconnecting
}
var isStable: Bool {
!isTransitioning
}
var statusColor: Color {
switch self {
case .connected: return .green
case .connecting, .disconnecting: return .orange
case .disconnected: return .gray
}
}
init(neStatus: NEVPNStatus) {
switch neStatus {
case .connected: self = .connected
case .connecting, .reasserting: self = .connecting
case .disconnecting: self = .disconnecting
case .disconnected, .invalid: self = .disconnected
@unknown default: self = .disconnected
}
}
}
struct VPNStatusEntry: TimelineEntry {
let date: Date
let status: WidgetVPNStatus
let ip: String
let fqdn: String
let needsAppSetup: Bool
let loginRequired: Bool
var isConnected: Bool { status == .connected }
/// Deep-link URL for the pre-iOS 17 `Link` fallback.
/// Mirrors the routing logic in `WidgetActionButton` so both paths stay in sync.
/// Returns `nil` when a transitioning state makes any tap meaningless.
var fallbackDeepLink: URL? {
guard !status.isTransitioning else { return nil }
if needsAppSetup && !isConnected {
return loginRequired ? WidgetConstants.deepLinkLogin : WidgetConstants.deepLinkConnect
}
return isConnected ? WidgetConstants.deepLinkDisconnect : WidgetConstants.deepLinkConnect
}
}