Files
ios-client/NetBirdWidgetExtension/Controls/VPNControlProvider.swift
evgeniyChepelevandZoltán Papp cd9863eeff Add VPN toggle to Control Center (iOS 18 Controls widget) (#120)
* Expose VPN connect/disconnect through the iOS 18 Controls interface using ControlWidget in the existing widget extension bundle. Reload the control whenever the app updates VPN state so the displayed status stays in sync with changes made outside the control.

* Fix controll state

* fix: enable VPN control from Control Center widget

Add packet-tunnel-provider entitlement to the widget extension so
NETunnelProviderManager can load and control the VPN tunnel from the
extension process. Without it, loadAllFromPreferences returned an
empty list, causing the control to always appear unconfigured and
ignore button presses.

Switch ControlValueProvider to read state from shared UserDefaults as
the primary source (matching VPNStatusProvider), falling back to live
NE status via the callback API. Replace SetValueIntent with AppIntent
to eliminate iOS value caching that broke the control after pre-login
presses. Use the callback-based NE API in the intent to avoid the
async throws variant hanging in the extension process.

* Update ToggleVPNControlIntent.swift

* Fix build error

* fix(iOS): read domain ResolvedIPs via collection getter

The merge of main bumped netbird-core to the structured ResolvedIPs
collection API (PR netbirdio/netbird#6090), but PacketTunnelProvider
still parsed domain.resolvedIPs as a comma-joined String, breaking the
build:

  value of type 'NetBirdSDKDomainInfo' has no member 'resolvedIPs'

Restore the getResolvedIPs()/size()/get() collection access (matching
PR #117 and the tvOS extension) so it matches the bumped core API.

* feat(iOS): add NetBird branding to Control Center VPN toggle

---------

Co-authored-by: Zoltán Papp <zoltan.pmail@gmail.com>
2026-06-03 15:12:30 +02:00

49 lines
1.9 KiB
Swift

import WidgetKit
import NetworkExtension
struct VPNControlState: Equatable, Sendable {
let isConfigured: Bool
let isActive: Bool
let isConnecting: Bool
}
@available(iOS 18.0, *)
struct VPNControlProvider: ControlValueProvider {
var previewValue: VPNControlState {
VPNControlState(isConfigured: true, isActive: false, isConnecting: false)
}
func currentValue() async throws -> VPNControlState {
let defaults = UserDefaults(suiteName: WidgetConstants.appGroupSuite)
let loginRequired = defaults?.bool(forKey: WidgetConstants.keyLoginRequired) ?? false
let persistedRaw = defaults?.string(forKey: WidgetConstants.keyVPNStatus) ?? ""
let persisted = WidgetVPNStatus(rawValue: persistedRaw)
// No persisted status means app hasn't configured VPN yet
guard persisted != nil, !loginRequired else {
return VPNControlState(isConfigured: false, isActive: false, isConnecting: false)
}
// Try to get live status from NE; fall back to persisted value
let neStatus = await loadNEStatus()
let status = neStatus ?? persisted ?? .disconnected
let isConnecting = status == .connecting || status == .disconnecting
let isActive = status == .connected || status == .connecting
return VPNControlState(isConfigured: true, isActive: isActive, isConnecting: isConnecting)
}
private func loadNEStatus() async -> WidgetVPNStatus? {
await withCheckedContinuation { continuation in
NETunnelProviderManager.loadAllFromPreferences { managers, error in
guard error == nil, let manager = managers?.first else {
continuation.resume(returning: nil)
return
}
continuation.resume(returning: WidgetVPNStatus(neStatus: manager.connection.status))
}
}
}
}