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

194 lines
7.0 KiB
Swift

//
// ServerView.swift
// NetBirdiOS
//
// Created by Pascal Fischer on 12.10.23.
//
import SwiftUI
struct ServerView: View {
@EnvironmentObject var viewModel: ViewModel
@StateObject private var serverViewModel = ServerViewModel(configurationFilePath: Preferences.configFile() ?? "", deviceName: Device.getName())
private let defaultManagementServerUrl = "https://api.netbird.io"
@State private var showSetupKeyField = false
@State private var managementServerUrl = ""
@State private var setupKey = ""
@State private var isButtonDisabled = false
@State private var isAddDeviceToggleDisabled = false
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Form {
Section(header: Text("Server")) {
ZStack(alignment: .leading) {
TextField("https://example-api.domain.com:443", text: $managementServerUrl)
.foregroundColor(Color("TextPrimary"))
.disableAutocorrection(true)
.autocapitalization(.none)
.keyboardType(.URL)
}
.onChange(of: managementServerUrl) { _ in
serverViewModel.clearErrorsFor(field: .url)
}
if let error = serverViewModel.viewErrors.urlError, !error.isEmpty {
Text(error).foregroundColor(.red).font(.footnote)
}
if let error = serverViewModel.viewErrors.generalError, !error.isEmpty {
Text(error).foregroundColor(.red).font(.footnote)
}
}
Section {
DisclosureGroup("Add this device with a setup key", isExpanded: $showSetupKeyField) {
TextField("0EF79C2F-DEE1-419B-BFC8-1BF529332998", text: $setupKey)
.disableAutocorrection(true)
.autocapitalization(.allCharacters)
.disabled(isAddDeviceToggleDisabled)
.onChange(of: setupKey) { _ in
serverViewModel.clearErrorsFor(field: .setupKey)
}
if let error = serverViewModel.viewErrors.setupKeyError, !error.isEmpty {
Text(error).foregroundColor(.red).font(.footnote)
}
if let error = serverViewModel.viewErrors.ssoNotSupportedError, !error.isEmpty {
Text(error).foregroundColor(.red).font(.footnote)
}
Text("Using setup keys for user devices is not recommended. SSO with MFA provides stronger security, proper user-device association, and periodic re-authentication.")
.font(.footnote)
.foregroundColor(.accentColor)
.padding(.vertical, 4)
}
}
.onChange(of: showSetupKeyField) { expanded in
if !expanded {
setupKey = ""
serverViewModel.clearErrorsFor(field: .setupKey)
}
}
Section {
if isButtonDisabled {
HStack {
Spacer()
ProgressView()
.padding(.trailing, 8)
Text("Validating...")
Spacer()
}
} else {
Button {
dismissKeyboard()
performChange()
} label: {
HStack {
Spacer()
Text("Change")
Spacer()
}
}
}
}
Section {
Button {
dismissKeyboard()
managementServerUrl = defaultManagementServerUrl
performUseNetBird()
} label: {
HStack {
Spacer()
Image("icon-netbird-button")
.resizable()
.scaledToFit()
.frame(width: 22, height: 22)
Text("Use NetBird server")
Spacer()
}
}
.disabled(isButtonDisabled)
}
}
.navigationTitle("Change Server")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: serverViewModel.viewErrors.ssoNotSupportedError) { error in
if error != nil {
showSetupKeyField = true
}
}
.onChange(of: serverViewModel.isOperationSuccessful) { success in
if success {
presentationMode.wrappedValue.dismiss()
viewModel.showServerChangedInfo = true
}
}
.onChange(of: serverViewModel.isUiEnabled) { isEnabled in
if isEnabled {
isButtonDisabled = false
isAddDeviceToggleDisabled = false
} else {
isButtonDisabled = true
isAddDeviceToggleDisabled = true
}
}
}
private func dismissKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
private func performChange() {
if managementServerUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& setupKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return
}
var serverUrl = managementServerUrl.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if serverUrl.isEmpty {
serverUrl = defaultManagementServerUrl
}
managementServerUrl = serverUrl
let key = setupKey.trimmingCharacters(in: .whitespacesAndNewlines)
serverViewModel.clearErrorsFor(field: .all)
Task {
await Task.yield()
if !serverUrl.isEmpty && !key.isEmpty {
await serverViewModel.loginWithSetupKey(managementServerUrl: serverUrl, setupKey: key)
} else if !serverUrl.isEmpty {
await serverViewModel.changeManagementServerAddress(managementServerUrl: serverUrl)
}
}
}
private func performUseNetBird() {
let serverUrl = defaultManagementServerUrl
let key = setupKey.trimmingCharacters(in: .whitespacesAndNewlines)
serverViewModel.clearErrorsFor(field: .all)
Task {
await Task.yield()
if key.isEmpty {
await serverViewModel.changeManagementServerAddress(managementServerUrl: serverUrl)
} else {
await serverViewModel.loginWithSetupKey(managementServerUrl: serverUrl, setupKey: key)
}
}
}
}
#Preview {
ServerView()
.environmentObject(ViewModel())
}