Files
evgeniyChepelevandClaude Opus 4.5 f5b1de0cc7 Redesign app (#60)
* 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>
2026-03-02 15:23:51 +01:00

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()
}
}