You've already forked ios-client
mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-05-22 17:10:12 -07:00
0327624b30
* fix layout on ipad * Create multi profiles * multi-profile server config, connection cache, and auth fixes Split ProfilesView into ProfilesListView, AddProfileSheet, ProfileBadge, AddProfileViewModel - Add server URL + setup key fields to Add Profile screen - Store per-profile connection data (ip/fqdn/managementURL) as typed model in ProfileConnectionCache - Show cached connection info immediately on profile switch; empty if no prior data - Fix profile deletion persistence via tombstone in profiles.json - Fix logout to remove both netbird.cfg and state.json (cfg holds auth tokens) - Preserve managementURL in UI after logout via cache fallback - Guard polling from overwriting new profile's data during disconnect/reconnect cycle * fix(multi-profile): reinitialize VPN adapter on profile switch and show current server URL * Update project.pbxproj * fix(multi-profile): prevent default server overwrite on logout/re-login * Update PacketTunnelProvider.swift * fix(multi-profile): address code review findings - Normalize only scheme and host to lowercase when saving management server URL; previously the full URL was lowercased which could corrupt case-sensitive paths on self-hosted servers - Move switchConnectionInfo(to:) inside the do-block so the connection UI is only updated after a successful profile switch - Add ProfileConnectionCache.remove(for:) and clearConnectionData(for:) to prevent stale ip/fqdn/managementURL from persisting after a profile is deleted or logged out; call them from removeProfile and logoutProfile - Use ASWebAuthenticationSession.Callback.customScheme on iOS 17.4+, falling back to the deprecated callbackURLScheme initializer on older versions; add a comment explaining why "http" is used and that a proper fix requires custom-scheme support in the SDK - Guard presentationAnchor against a missing key window with assertionFailure in debug builds instead of silently returning an empty UIWindow
260 lines
9.9 KiB
Swift
260 lines
9.9 KiB
Swift
//
|
|
// MainView.swift
|
|
// NetBirdiOS
|
|
//
|
|
// Created by Pascal Fischer on 01.08.23.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Lottie
|
|
import NetworkExtension
|
|
|
|
// MARK: - Main Entry Point
|
|
/// The root view that switches between iOS and tvOS layouts.
|
|
struct MainView: View {
|
|
@EnvironmentObject var viewModel: ViewModel
|
|
|
|
var body: some View {
|
|
#if os(tvOS)
|
|
// tvOS uses a completely different navigation structure
|
|
TVMainView()
|
|
#else
|
|
// iOS uses tab bar navigation
|
|
iOSMainView()
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
|
|
enum MainAlertType: String, Identifiable {
|
|
case changeServer
|
|
case serverChanged
|
|
case preSharedKeyChanged
|
|
case authenticationRequired
|
|
case onDemandConflict
|
|
case onDemandDisconnect
|
|
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
struct iOSMainView: View {
|
|
@EnvironmentObject var viewModel: ViewModel
|
|
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
|
|
@State private var selectedTab = 0
|
|
@State private var activeAlert: MainAlertType?
|
|
|
|
init() {
|
|
let navAppearance = UINavigationBarAppearance()
|
|
navAppearance.configureWithOpaqueBackground()
|
|
navAppearance.backgroundColor = UIColor(named: "BgNavigationBar")
|
|
navAppearance.titleTextAttributes = [.foregroundColor: UIColor(named: "TextPrimary") ?? .white]
|
|
navAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor(named: "TextPrimary") ?? .white]
|
|
UINavigationBar.appearance().standardAppearance = navAppearance
|
|
UINavigationBar.appearance().compactAppearance = navAppearance
|
|
UINavigationBar.appearance().scrollEdgeAppearance = navAppearance
|
|
|
|
let tabAppearance = UITabBarAppearance()
|
|
tabAppearance.configureWithOpaqueBackground()
|
|
tabAppearance.backgroundColor = UIColor(named: "BgNavigationBar")
|
|
UITabBar.appearance().standardAppearance = tabAppearance
|
|
UITabBar.appearance().scrollEdgeAppearance = tabAppearance
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
if !hasCompletedOnboarding {
|
|
FirstLaunchView(
|
|
hasCompletedOnboarding: $hasCompletedOnboarding,
|
|
onChangeServer: {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
activeAlert = .changeServer
|
|
}
|
|
}
|
|
)
|
|
} else {
|
|
mainContent
|
|
}
|
|
}
|
|
.alert(item: $activeAlert) { alertType in
|
|
switch alertType {
|
|
case .changeServer:
|
|
return Alert(
|
|
title: Text("Change server"),
|
|
message: Text("Changing server will erase the local config and disconnect this device from the current NetBird account."),
|
|
primaryButton: .destructive(Text("Confirm")) {
|
|
viewModel.handleServerChanged()
|
|
viewModel.navigateToServerView = true
|
|
},
|
|
secondaryButton: .cancel()
|
|
)
|
|
case .serverChanged:
|
|
return Alert(
|
|
title: Text("Server was changed"),
|
|
message: Text("Click on the connect button to continue."),
|
|
dismissButton: .default(Text("OK"))
|
|
)
|
|
case .preSharedKeyChanged:
|
|
return Alert(
|
|
title: Text("Preshared key was set"),
|
|
message: Text("Click on the connect button to continue."),
|
|
dismissButton: .default(Text("OK"))
|
|
)
|
|
case .authenticationRequired:
|
|
if viewModel.connectOnDemand {
|
|
return Alert(
|
|
title: Text("Authentication required"),
|
|
message: Text("The server requires a new authentication."),
|
|
primaryButton: .default(Text("Connect")) {
|
|
viewModel.connect()
|
|
},
|
|
secondaryButton: .cancel(Text("Later"))
|
|
)
|
|
}
|
|
return Alert(
|
|
title: Text("Authentication required"),
|
|
message: Text("The server requires a new authentication."),
|
|
primaryButton: .default(Text("Login")) {
|
|
viewModel.connect()
|
|
},
|
|
secondaryButton: .cancel(Text("Later"))
|
|
)
|
|
case .onDemandConflict:
|
|
return Alert(
|
|
title: Text("VPN On Demand Conflict"),
|
|
message: Text("Your current On Demand rules prevent connecting on this network. Would you like to disable VPN On Demand and connect?"),
|
|
primaryButton: .default(Text("Disable & Connect")) {
|
|
viewModel.connectWithOnDemandDisabled()
|
|
},
|
|
secondaryButton: .cancel(Text("Edit Rules")) {
|
|
selectedTab = 3 // Switch to Settings tab
|
|
}
|
|
)
|
|
case .onDemandDisconnect:
|
|
return Alert(
|
|
title: Text("VPN On Demand Active"),
|
|
message: Text("VPN On Demand is enabled and will automatically reconnect the VPN based on your rules. Do you want to disable On Demand and disconnect?"),
|
|
primaryButton: .destructive(Text("Disable & Disconnect")) {
|
|
viewModel.closeWithOnDemandDisabled()
|
|
},
|
|
secondaryButton: .default(Text("Cancel")) {
|
|
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var mainContent: some View {
|
|
ZStack {
|
|
TabView(selection: $selectedTab) {
|
|
NavigationView {
|
|
iOSConnectionView()
|
|
}
|
|
.navigationViewStyle(StackNavigationViewStyle())
|
|
.tabItem {
|
|
Label("Connection", systemImage: "network")
|
|
}
|
|
.tag(0)
|
|
|
|
NavigationView {
|
|
iOSPeersView()
|
|
}
|
|
.navigationViewStyle(StackNavigationViewStyle())
|
|
.tabItem {
|
|
Label("Peers", systemImage: "person.3.fill")
|
|
}
|
|
.tag(1)
|
|
|
|
NavigationView {
|
|
iOSNetworksView()
|
|
}
|
|
.navigationViewStyle(StackNavigationViewStyle())
|
|
.tabItem {
|
|
Label("Resources", systemImage: "globe")
|
|
}
|
|
.tag(2)
|
|
|
|
NavigationView {
|
|
iOSSettingsView()
|
|
}
|
|
.navigationViewStyle(StackNavigationViewStyle())
|
|
.tabItem {
|
|
Label("Settings", systemImage: "gear")
|
|
}
|
|
.tag(3)
|
|
}
|
|
.onChange(of: viewModel.navigateToServerView) { newValue in
|
|
if newValue {
|
|
selectedTab = 0
|
|
}
|
|
}
|
|
.onChange(of: viewModel.showChangeServerAlert) { show in
|
|
if show { activeAlert = .changeServer; viewModel.showChangeServerAlert = false }
|
|
}
|
|
.onChange(of: viewModel.showServerChangedInfo) { show in
|
|
if show { activeAlert = .serverChanged; viewModel.showServerChangedInfo = false }
|
|
}
|
|
.onChange(of: viewModel.showPreSharedKeyChangedInfo) { show in
|
|
if show { activeAlert = .preSharedKeyChanged; viewModel.showPreSharedKeyChangedInfo = false }
|
|
}
|
|
.onChange(of: viewModel.showAuthenticationRequired) { show in
|
|
if show { activeAlert = .authenticationRequired; viewModel.showAuthenticationRequired = false }
|
|
}
|
|
.onChange(of: viewModel.showOnDemandConflictAlert) { show in
|
|
if show { activeAlert = .onDemandConflict; viewModel.showOnDemandConflictAlert = false }
|
|
}
|
|
.onChange(of: viewModel.showOnDemandDisconnectAlert) { show in
|
|
if show { activeAlert = .onDemandDisconnect; viewModel.showOnDemandDisconnectAlert = false }
|
|
}
|
|
|
|
// Toast alerts
|
|
VStack {
|
|
Spacer()
|
|
if viewModel.showFqdnCopiedAlert {
|
|
HStack {
|
|
Image("logo-onboarding")
|
|
.resizable()
|
|
.frame(width: 20, height: 15)
|
|
Text("Domain name copied!")
|
|
.foregroundColor(.white)
|
|
.font(.headline)
|
|
}
|
|
.padding(5)
|
|
.background(Color.black.opacity(0.5))
|
|
.cornerRadius(8)
|
|
.transition(AnyTransition.opacity.combined(with: .move(edge: .top)))
|
|
.animation(.default, value: viewModel.showFqdnCopiedAlert)
|
|
.zIndex(1)
|
|
}
|
|
|
|
if viewModel.showIpCopiedAlert {
|
|
HStack {
|
|
Image("logo-onboarding")
|
|
.resizable()
|
|
.frame(width: 20, height: 15)
|
|
Text("IP address copied!")
|
|
.foregroundColor(.white)
|
|
.font(.headline)
|
|
}
|
|
.padding(5)
|
|
.background(Color.black.opacity(0.5))
|
|
.cornerRadius(8)
|
|
.transition(AnyTransition.opacity.combined(with: .move(edge: .top)))
|
|
.animation(.default, value: viewModel.showIpCopiedAlert)
|
|
.zIndex(1)
|
|
}
|
|
Spacer().frame(height: 80)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MainView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
MainView()
|
|
}
|
|
}
|
|
|
|
#endif // os(iOS)
|