You've already forked ios-client
mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-05-22 17:10:12 -07:00
4c95a6582a
* fix ui state for airplaine mode * fix slide bar * Keep VPN tunnel alive during network unavailability - Add isNetworkUnavailable flag to NetBirdAdapter to track network state - Modify ConnectionListener to stay in 'connecting' state when network is unavailable instead of transitioning to 'disconnected' - Update PacketTunnelProvider to set network unavailable flag and trigger automatic reconnect when network returns - Fix CustomLottieView to show grey 'Disconnected' state immediately when network is lost, without closing the VPN tunnel - Ensure UI shows correct state after app foreground/background cycle This allows the VPN tunnel to survive temporary network outages (e.g. airplane mode) and automatically reconnect when network returns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix UI state after app foreground/background cycle Show correct connected/disconnected state immediately when app returns from background, without replaying animations. Use extensionStatus (iOS VPN state) as the source of truth for UI state. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * netbird credential * Update MainView.swift * Separate button by state * Add gogoleServiceInfo plist reference * Remove dead code in animation state machine and fix copy-to-clipboard UX - Remove unreachable shouldForceReset block in CustomLottieView (already handled by earlier return) - Guard against copying empty fqdn/ip strings when disconnected - Use consistent .smooth animation for both fqdn and ip copy feedback - Remove unreachable shouldForceReset block in CustomLottieView (already handled by earlier return) - Guard against copying empty fqdn/ip strings when disconnected - Use consistent .smooth animation for both fqdn and ip copy feedback * Tab bar * Update peer view * Add offline state handling and network warning banner - Show "Offline" instead of "Connected" when VPN tunnel is active but device has no internet - Add NetworkWarningBanner with "Network Issues" warning when connected without internet - Remove InternetStatusView (online/offline indicator) from connection screen - Add FirstLaunchView onboarding screen - Remove unused components (CustomBackButton, Extensions, JustifiedText, SolidButton, TransparentGradientButton) - Update AboutView, AdvancedView, ServerView, PeerTabView, RouteTabView, iOSNetworksView styling - Add EmptyTabPlaceholder component * Add AppButton component with liquid glass support for iOS 26+ * Update FirstLaunchView.swift * old version * Update project.pbxproj * Code refactoring * Update project.pbxproj * VPN On Demand * Update MainViewModel.swift * Add granular VPN On Demand settings with per-interface rules and conflict detection * Update project.pbxproj * code refactoring * Add Wi-Fi network management for VPN On Demand settings * Fix ai comments Prevent On Demand enable when user is not logged in Add login status check in setOnDemandEnabled() to prevent reconnect loops when VPN On Demand is enabled without an active session. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
209 lines
7.9 KiB
Swift
209 lines
7.9 KiB
Swift
//
|
|
// AdvancedView.swift
|
|
// NetBirdiOS
|
|
//
|
|
// Created by Pascal Fischer on 01.08.23.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct AdvancedView: View {
|
|
@EnvironmentObject var viewModel: ViewModel
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
if viewModel.presharedKeySecure {
|
|
SecureField("Pre-shared key", text: $viewModel.presharedKey)
|
|
.disabled(true)
|
|
} else {
|
|
TextField("Pre-shared key", text: $viewModel.presharedKey)
|
|
.disableAutocorrection(true)
|
|
.autocapitalization(.none)
|
|
.onChange(of: viewModel.presharedKey) { value in
|
|
checkForValidPresharedKey(text: value)
|
|
}
|
|
}
|
|
|
|
if viewModel.showInvalidPresharedKeyAlert {
|
|
Text("Invalid key")
|
|
.foregroundColor(.red)
|
|
.font(.footnote)
|
|
}
|
|
|
|
Button(viewModel.presharedKeySecure ? "Remove" : "Save") {
|
|
if !viewModel.showInvalidPresharedKeyAlert {
|
|
if viewModel.presharedKeySecure {
|
|
viewModel.removePreSharedKey()
|
|
} else {
|
|
viewModel.updatePreSharedKey()
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Pre-shared Key")
|
|
} footer: {
|
|
Text("You will only communicate with peers that use the same key.")
|
|
}
|
|
|
|
Section(header: Text("Logging")) {
|
|
Toggle("Trace logs", isOn: $viewModel.traceLogsEnabled)
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
|
|
Button("Share logs") {
|
|
shareButtonTapped()
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Rosenpass")) {
|
|
Toggle("Enable Rosenpass", isOn: $viewModel.rosenpassEnabled)
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
.onChange(of: viewModel.rosenpassEnabled) { value in
|
|
if !value {
|
|
viewModel.rosenpassPermissive = false
|
|
}
|
|
viewModel.setRosenpassEnabled(enabled: value)
|
|
}
|
|
|
|
Toggle("Permissive mode", isOn: $viewModel.rosenpassPermissive)
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
.onChange(of: viewModel.rosenpassPermissive) { value in
|
|
if value {
|
|
viewModel.rosenpassEnabled = true
|
|
}
|
|
viewModel.setRosenpassPermissive(permissive: value)
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Network & Security")) {
|
|
Toggle("Force relay connection", isOn: $viewModel.forceRelayConnection)
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
.onChange(of: viewModel.forceRelayConnection) { value in
|
|
viewModel.setForcedRelayConnection(isEnabled: value)
|
|
}
|
|
|
|
}
|
|
}
|
|
.onAppear {
|
|
viewModel.loadRosenpassSettings()
|
|
viewModel.loadPreSharedKey()
|
|
}
|
|
.navigationTitle("Advanced")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.alert(isPresented: $viewModel.showLogLevelChangedAlert) {
|
|
Alert(
|
|
title: Text("Changing Log Level"),
|
|
message: Text("Changing log level will take effect after next connect."),
|
|
dismissButton: .default(Text("OK"))
|
|
)
|
|
}
|
|
.alert(isPresented: $viewModel.showForceRelayAlert) {
|
|
Alert(
|
|
title: Text("Force Relay"),
|
|
message: Text("To apply the setting, you will need to reconnect."),
|
|
dismissButton: .default(Text("OK"))
|
|
)
|
|
}
|
|
}
|
|
|
|
func shareButtonTapped() {
|
|
Task.detached(priority: .utility) {
|
|
let fileManager = FileManager.default
|
|
let tempDir = fileManager.temporaryDirectory.appendingPathComponent("netbird-logs-\(UUID().uuidString)")
|
|
|
|
do {
|
|
try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
} catch {
|
|
AppLogger.shared.log("Failed to create temp directory: \(error)")
|
|
return
|
|
}
|
|
|
|
var filesToShare: [URL] = []
|
|
|
|
if let goLogURL = AppLogger.getGoLogFileURL() {
|
|
let goLogPath = tempDir.appendingPathComponent("netbird-engine.log")
|
|
do {
|
|
try fileManager.copyItem(at: goLogURL, to: goLogPath)
|
|
filesToShare.append(goLogPath)
|
|
} catch {
|
|
AppLogger.shared.log("Failed to export Go log: \(error)")
|
|
}
|
|
}
|
|
|
|
if let swiftLogURL = AppLogger.getLogFileURL() {
|
|
let swiftLogPath = tempDir.appendingPathComponent("netbird-app.log")
|
|
do {
|
|
try fileManager.copyItem(at: swiftLogURL, to: swiftLogPath)
|
|
filesToShare.append(swiftLogPath)
|
|
} catch {
|
|
AppLogger.shared.log("Failed to export Swift log: \(error)")
|
|
}
|
|
}
|
|
|
|
guard !filesToShare.isEmpty else {
|
|
AppLogger.shared.log("No log files to share")
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
return
|
|
}
|
|
|
|
let readOnlyFilesToShare = filesToShare
|
|
|
|
await MainActor.run {
|
|
let activityViewController = UIActivityViewController(activityItems: readOnlyFilesToShare, applicationActivities: nil)
|
|
|
|
activityViewController.excludedActivityTypes = [
|
|
.assignToContact,
|
|
.saveToCameraRoll
|
|
]
|
|
|
|
activityViewController.completionWithItemsHandler = { _, _, _, _ in
|
|
do {
|
|
try FileManager.default.removeItem(at: tempDir)
|
|
} catch {
|
|
AppLogger.shared.log("Failed to cleanup temp log files: \(error)")
|
|
}
|
|
}
|
|
|
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
let rootViewController = windowScene.windows.first?.rootViewController {
|
|
if let popover = activityViewController.popoverPresentationController {
|
|
popover.sourceView = rootViewController.view
|
|
popover.sourceRect = CGRect(x: rootViewController.view.bounds.midX,
|
|
y: rootViewController.view.bounds.midY,
|
|
width: 0, height: 0)
|
|
popover.permittedArrowDirections = []
|
|
}
|
|
rootViewController.present(activityViewController, animated: true, completion: nil)
|
|
} else {
|
|
AppLogger.shared.log("Unable to present share sheet (no rootViewController)")
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func checkForValidPresharedKey(text: String) {
|
|
if isValidBase64EncodedString(text) {
|
|
viewModel.showInvalidPresharedKeyAlert = false
|
|
} else {
|
|
viewModel.showInvalidPresharedKeyAlert = true
|
|
}
|
|
}
|
|
|
|
func isValidBase64EncodedString(_ input: String) -> Bool {
|
|
if input.isEmpty {
|
|
return true
|
|
}
|
|
guard let data = Data(base64Encoded: input) else {
|
|
return false
|
|
}
|
|
return data.count == 32
|
|
}
|
|
}
|
|
|
|
struct AdvancedView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
AdvancedView()
|
|
}
|
|
}
|