mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-09-23 09:28:33 -07:00
* 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 * Update MainViewModel.swift --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
208 lines
7.9 KiB
Swift
208 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()
|
|
}
|
|
}
|