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>
183 lines
8.1 KiB
Swift
183 lines
8.1 KiB
Swift
//
|
||
// iOSConnectionView.swift
|
||
// NetBird
|
||
//
|
||
// Connection tab: VPN button, FQDN/IP display, status indicator.
|
||
//
|
||
|
||
import SwiftUI
|
||
import Lottie
|
||
import NetworkExtension
|
||
|
||
#if os(iOS)
|
||
|
||
struct iOSConnectionView: View {
|
||
@EnvironmentObject var viewModel: ViewModel
|
||
@State private var animationKey: UUID = UUID()
|
||
@State private var fqdnCopied = false
|
||
@State private var ipCopied = false
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
let isLandscape = geometry.size.width > geometry.size.height
|
||
let imageName = isLandscape ? "bg-bottom-landscape" : "bg-bottom"
|
||
|
||
ZStack {
|
||
|
||
if viewModel.statusDetailsValid {
|
||
// Background layers
|
||
VStack {
|
||
Color("BgSecondary")
|
||
.frame(height: UIScreen.main.bounds.height * 4/5)
|
||
.ignoresSafeArea(.all)
|
||
Color("BgPrimary")
|
||
.frame(height: UIScreen.main.bounds.height * 1/5)
|
||
.ignoresSafeArea(.all)
|
||
}
|
||
|
||
VStack {
|
||
Image(imageName)
|
||
.resizable(resizingMode: .stretch)
|
||
.aspectRatio(contentMode: .fit)
|
||
.padding(.top, Screen.height * (DeviceType.isPad ? (isLandscape ? -0.15 : 0.36) : 0.19))
|
||
.padding(.leading, UIScreen.main.bounds.height * (isLandscape ? 0.04 : 0))
|
||
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
|
||
.edgesIgnoringSafeArea(.bottom)
|
||
}
|
||
|
||
// FQDN + IP + internet status
|
||
VStack {
|
||
Text(fqdnCopied ? "Copied" : viewModel.fqdn)
|
||
.foregroundColor(Color("TextPrimary"))
|
||
.font(.system(size: 20, weight: .regular))
|
||
.lineLimit(1)
|
||
.minimumScaleFactor(0.5)
|
||
.opacity(fqdnCopied ? 0.7 : 1.0)
|
||
.animation(.easeInOut(duration: 0.2), value: fqdnCopied)
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, Screen.height * (DeviceType.isPad ? 0.09 : 0.13))
|
||
.padding(.bottom, 5)
|
||
.onTapGesture {
|
||
guard !viewModel.fqdn.isEmpty else { return }
|
||
UIPasteboard.general.string = viewModel.fqdn
|
||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||
withAnimation(.smooth) {
|
||
fqdnCopied = true
|
||
}
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||
withAnimation(.smooth) {
|
||
fqdnCopied = false
|
||
}
|
||
}
|
||
}
|
||
|
||
Text(ipCopied ? "Copied" : viewModel.ip)
|
||
.foregroundColor(Color("TextPrimary"))
|
||
.font(.system(size: 20, weight: .regular))
|
||
.lineLimit(1)
|
||
.minimumScaleFactor(0.5)
|
||
.opacity(ipCopied ? 0.7 : 1.0)
|
||
.animation(.easeInOut(duration: 0.2), value: ipCopied)
|
||
.onTapGesture {
|
||
guard !viewModel.ip.isEmpty else { return }
|
||
UIPasteboard.general.string = viewModel.ip
|
||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||
withAnimation(.smooth) {
|
||
ipCopied = true
|
||
}
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||
withAnimation(.smooth) {
|
||
ipCopied = false
|
||
}
|
||
}
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
|
||
// VPN button + status text
|
||
VStack {
|
||
Spacer()
|
||
Button(action: {
|
||
if !viewModel.buttonLock {
|
||
switch viewModel.vpnDisplayState {
|
||
case .disconnected:
|
||
viewModel.connect()
|
||
case .connecting, .connected:
|
||
viewModel.close()
|
||
case .disconnecting:
|
||
break
|
||
}
|
||
}
|
||
}) {
|
||
CustomLottieView(vpnState: $viewModel.vpnDisplayState)
|
||
.id(animationKey)
|
||
.frame(width: UIScreen.main.bounds.width * (isLandscape ? 0.40 : 0.79), height: UIScreen.main.bounds.width * (isLandscape ? 0.40 : 0.79))
|
||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
||
self.animationKey = UUID()
|
||
}
|
||
}
|
||
.padding(.top, -UIScreen.main.bounds.height / 27)
|
||
.padding(.bottom)
|
||
|
||
Text(viewModel.extensionStateText)
|
||
.foregroundColor(Color("TextSecondary"))
|
||
.font(.system(size: 24, weight: .regular))
|
||
|
||
Spacer()
|
||
}
|
||
.padding()
|
||
|
||
// Network warning banner – above tab bar
|
||
if viewModel.vpnDisplayState == .connected && !viewModel.isInternetConnected {
|
||
VStack {
|
||
Spacer()
|
||
NetworkWarningBanner()
|
||
.padding(.bottom, geometry.safeAreaInsets.bottom + 80)
|
||
}
|
||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||
.animation(.easeInOut(duration: 0.3), value: viewModel.isInternetConnected)
|
||
}
|
||
|
||
// Hidden NavigationLink for ServerView
|
||
NavigationLink("", destination: ServerView(), isActive: $viewModel.navigateToServerView)
|
||
.hidden()
|
||
.onChange(of: viewModel.navigateToServerView) { newValue in
|
||
if !newValue {
|
||
viewModel.startPollingDetails()
|
||
}
|
||
}
|
||
|
||
// Safari login view
|
||
if viewModel.networkExtensionAdapter.showBrowser,
|
||
let loginURLString = viewModel.networkExtensionAdapter.loginURL,
|
||
let loginURL = URL(string: loginURLString)
|
||
{
|
||
SafariView(isPresented: $viewModel.networkExtensionAdapter.showBrowser,
|
||
url: loginURL,
|
||
didFinish: {
|
||
print("Finish login")
|
||
viewModel.networkExtensionAdapter.startVPNConnection()
|
||
})
|
||
}
|
||
|
||
} else {
|
||
// Loading placeholder
|
||
ZStack {
|
||
Color("BgPrimary")
|
||
.ignoresSafeArea()
|
||
Image("netbird-logo-menu")
|
||
.resizable()
|
||
.scaledToFit()
|
||
.frame(width: 200)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.navigationBarHidden(true)
|
||
}
|
||
}
|
||
|
||
#endif
|