You've already forked ios-client
mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-05-22 17:10:12 -07:00
315283822c
* 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
187 lines
5.8 KiB
Swift
187 lines
5.8 KiB
Swift
//
|
|
// NetBirdApp.swift
|
|
// NetBird
|
|
//
|
|
// Created by Pascal Fischer on 01.08.23.
|
|
//
|
|
// Main entry point for the NetBird app.
|
|
// Supports both iOS and tvOS platforms.
|
|
//
|
|
|
|
import SwiftUI
|
|
import FirebaseCore
|
|
import Combine
|
|
|
|
#if os(iOS)
|
|
import FirebasePerformance
|
|
#endif
|
|
|
|
#if os(iOS)
|
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
|
func application(
|
|
_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
) -> Bool {
|
|
if let path = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist"),
|
|
let options = FirebaseOptions(contentsOfFile: path) {
|
|
FirebaseApp.configure(options: options)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@main
|
|
struct NetBirdApp: App {
|
|
@StateObject private var viewModelLoader = ViewModelLoader()
|
|
@Environment(\.scenePhase) var scenePhase
|
|
@State private var activationTask: Task<Void, Never>?
|
|
@State private var pendingURL: URL?
|
|
|
|
#if os(iOS)
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
|
|
#endif
|
|
|
|
init() {
|
|
// Configure Firebase on main thread as required by Firebase
|
|
#if os(tvOS)
|
|
if let path = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist"),
|
|
let options = FirebaseOptions(contentsOfFile: path) {
|
|
FirebaseApp.configure(options: options)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
if let viewModel = viewModelLoader.viewModel {
|
|
MainView()
|
|
.environmentObject(viewModel)
|
|
#if os(iOS)
|
|
.onOpenURL { url in
|
|
handleWidgetURL(url, viewModel: viewModel)
|
|
}
|
|
.onAppear {
|
|
if let url = pendingURL {
|
|
handleWidgetURL(url, viewModel: viewModel)
|
|
pendingURL = nil
|
|
}
|
|
if UIApplication.shared.applicationState == .active {
|
|
startActivation(viewModel: viewModel)
|
|
}
|
|
}
|
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
|
startActivation(viewModel: viewModel)
|
|
}
|
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
|
|
stopActivation(viewModel: viewModel)
|
|
}
|
|
#endif
|
|
#if os(tvOS)
|
|
.onAppear {
|
|
if scenePhase == .active {
|
|
startActivation(viewModel: viewModel)
|
|
}
|
|
}
|
|
.onChange(of: scenePhase) { _, newPhase in
|
|
if newPhase == .active {
|
|
startActivation(viewModel: viewModel)
|
|
} else {
|
|
stopActivation(viewModel: viewModel)
|
|
}
|
|
}
|
|
#endif
|
|
} else {
|
|
loadingView
|
|
#if os(iOS)
|
|
.onOpenURL { url in
|
|
pendingURL = url
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Activation
|
|
|
|
private func startActivation(viewModel: ViewModel) {
|
|
activationTask?.cancel()
|
|
activationTask = Task { @MainActor in
|
|
guard isAppActive, !Task.isCancelled else { return }
|
|
|
|
if let initialStatus = await viewModel.networkExtensionAdapter.loadCurrentConnectionState() {
|
|
viewModel.extensionState = initialStatus
|
|
viewModel.updateVPNDisplayState()
|
|
}
|
|
|
|
guard isAppActive, !Task.isCancelled else { return }
|
|
viewModel.checkExtensionState()
|
|
#if os(iOS)
|
|
viewModel.checkLoginRequiredFlag()
|
|
#endif
|
|
viewModel.startPollingDetails()
|
|
}
|
|
}
|
|
|
|
private func stopActivation(viewModel: ViewModel) {
|
|
activationTask?.cancel()
|
|
activationTask = nil
|
|
viewModel.stopPollingDetails()
|
|
}
|
|
|
|
private var isAppActive: Bool {
|
|
#if os(iOS)
|
|
UIApplication.shared.applicationState == .active
|
|
#else
|
|
scenePhase == .active
|
|
#endif
|
|
}
|
|
|
|
private var loadingView: some View {
|
|
ZStack {
|
|
Color("BgPrimary")
|
|
.ignoresSafeArea()
|
|
Image("netbird-logo-menu")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 200)
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
/// Handles deep link URLs from the Home Screen widget.
|
|
private func handleWidgetURL(_ url: URL, viewModel: ViewModel) {
|
|
guard url.scheme == "netbird" else { return }
|
|
switch url.host {
|
|
case "login":
|
|
viewModel.connect()
|
|
case "connect":
|
|
if viewModel.vpnDisplayState == .disconnected {
|
|
viewModel.connect()
|
|
}
|
|
case "disconnect":
|
|
if viewModel.vpnDisplayState == .connected {
|
|
viewModel.close()
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// Loads ViewModel asynchronously to avoid blocking app launch.
|
|
/// The Go runtime initialization (from NetBirdSDK) can take several seconds on cold start.
|
|
/// By creating the ViewModel in an async Task, the loading screen appears immediately
|
|
/// instead of showing a black screen.
|
|
@MainActor
|
|
class ViewModelLoader: ObservableObject {
|
|
@Published var viewModel: ViewModel?
|
|
|
|
init() {
|
|
Task { @MainActor in
|
|
self.viewModel = ViewModel()
|
|
}
|
|
}
|
|
}
|