Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions NetBird.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@
50E608132A7958B100BAF09B /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E608122A7958B100BAF09B /* MainViewModel.swift */; };
50E608242A79966600BAF09B /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E608232A79966600BAF09B /* AboutView.swift */; };
50E608262A79968500BAF09B /* AdvancedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E608252A79968500BAF09B /* AdvancedView.swift */; };
558553FB2FE34921004FB58D /* jetbrains-mono-variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 558553FA2FE34921004FB58D /* jetbrains-mono-variable.ttf */; };
558553FC2FE34921004FB58D /* inter-variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 558553F92FE34921004FB58D /* inter-variable.ttf */; };
558553FD2FE34921004FB58D /* jetbrains-mono-variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 558553FA2FE34921004FB58D /* jetbrains-mono-variable.ttf */; };
558553FE2FE34921004FB58D /* inter-variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 558553F92FE34921004FB58D /* inter-variable.ttf */; };
558553FF2FE34921004FB58D /* jetbrains-mono-variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 558553FA2FE34921004FB58D /* jetbrains-mono-variable.ttf */; };
558554002FE34921004FB58D /* inter-variable.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 558553F92FE34921004FB58D /* inter-variable.ttf */; };
5573F6EE2F9F523D00E63A73 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5573F6ED2F9F523D00E63A73 /* GoogleService-Info.plist */; };
5573F6EF2F9F523D00E63A73 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5573F6ED2F9F523D00E63A73 /* GoogleService-Info.plist */; };
5573F6F02F9F523D00E63A73 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5573F6ED2F9F523D00E63A73 /* GoogleService-Info.plist */; };
Expand All @@ -152,6 +158,7 @@
978FC4742EEDF168002D0EB8 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 443782BD2EDF284A00F9FA94 /* Platform.swift */; };
9B6C03E5ED4245A597C0FBE7 /* iOSConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE6A20955914E68BFADDEED /* iOSConnectionView.swift */; };
9CC0E000AE3F165CA72FD465 /* AppLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AA7193B3AE82DF185EDEB1B /* AppLoggerTests.swift */; };
A1B2C3D42F4A000100000001 /* VPNToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32F4A000100000001 /* VPNToggleView.swift */; };
A1B2C3D52EEDF501001A2B3C /* ConfigurationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42EEDF500001A2B3C /* ConfigurationProvider.swift */; };
A1B2C3D62EEDF502001A2B3C /* ConfigurationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42EEDF500001A2B3C /* ConfigurationProvider.swift */; };
A1B2C3D72EEDF503001A2B3C /* ConfigurationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42EEDF500001A2B3C /* ConfigurationProvider.swift */; };
Expand Down Expand Up @@ -338,6 +345,8 @@
50E608232A79966600BAF09B /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
50E608252A79968500BAF09B /* AdvancedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedView.swift; sourceTree = "<group>"; };
53CB9305A9DC6CAD1895495A /* SharedUserDefaultsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedUserDefaultsTests.swift; sourceTree = "<group>"; };
558553F92FE34921004FB58D /* inter-variable.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "inter-variable.ttf"; sourceTree = "<group>"; };
558553FA2FE34921004FB58D /* jetbrains-mono-variable.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "jetbrains-mono-variable.ttf"; sourceTree = "<group>"; };
5573F6ED2F9F523D00E63A73 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
55B5E81A2F39158200852AA7 /* InternetStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetStatusView.swift; sourceTree = "<group>"; };
55D865832F70982000A2EFF8 /* NetBirdWidgetExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NetBirdWidgetExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -349,6 +358,7 @@
91FA1F06D3375864C74EAB3B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
978FC46F2EEDF167002D0EB8 /* AppLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogger.swift; sourceTree = "<group>"; };
9CD257EF78F038560FF3112D /* VPNOnDemandView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOnDemandView.swift; sourceTree = "<group>"; };
A1B2C3D32F4A000100000001 /* VPNToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggleView.swift; sourceTree = "<group>"; };
BB001A002F99000000000001 /* TroubleshootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TroubleshootView.swift; sourceTree = "<group>"; };
A1B2C3D42EEDF500001A2B3C /* ConfigurationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationProvider.swift; sourceTree = "<group>"; };
A1C3D5E72F000001001A2B3C /* WiFiOnDemandPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WiFiOnDemandPolicy.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -583,6 +593,7 @@
50A891192A792A15007C48FC /* NetBird */ = {
isa = PBXGroup;
children = (
558553F82FE34913004FB58D /* Fonts */,
50245A3B2A7FD1CD0034792B /* Info.plist */,
50245A1B2A7BCF120034792B /* NetBird.entitlements */,
50E608082A79542D00BAF09B /* Source */,
Expand Down Expand Up @@ -663,6 +674,7 @@
E1A0B0002F5E000100000001 /* EmptyTabPlaceholder.swift */,
E1C0D0002F72000100000001 /* AppButton.swift */,
E1B0C0002F60000100000001 /* NetworkWarningBanner.swift */,
A1B2C3D32F4A000100000001 /* VPNToggleView.swift */,
);
path = Components;
sourceTree = "<group>";
Expand All @@ -680,6 +692,15 @@
path = App;
sourceTree = "<group>";
};
558553F82FE34913004FB58D /* Fonts */ = {
isa = PBXGroup;
children = (
558553F92FE34921004FB58D /* inter-variable.ttf */,
558553FA2FE34921004FB58D /* jetbrains-mono-variable.ttf */,
);
path = Fonts;
sourceTree = "<group>";
};
651C942641826A7AA94ED369 /* NetBirdTests */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -916,6 +937,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
558553FB2FE34921004FB58D /* jetbrains-mono-variable.ttf in Resources */,
558553FC2FE34921004FB58D /* inter-variable.ttf in Resources */,
5573F6EE2F9F523D00E63A73 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -956,6 +979,8 @@
501B0DCD2AE04DDE004BE7A7 /* button-disconnecting.json in Resources */,
501B0DD12AE04DDE004BE7A7 /* logo_NetBird.json in Resources */,
501B0DDB2AE04DDE004BE7A7 /* button-connected.json in Resources */,
558553FD2FE34921004FB58D /* jetbrains-mono-variable.ttf in Resources */,
558553FE2FE34921004FB58D /* inter-variable.ttf in Resources */,
5573F6F02F9F523D00E63A73 /* GoogleService-Info.plist in Resources */,
501B0DCF2AE04DDE004BE7A7 /* button-connecting-loop.json in Resources */,
501B0DD72AE04DDE004BE7A7 /* button-full2.json in Resources */,
Expand All @@ -966,6 +991,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
558553FF2FE34921004FB58D /* jetbrains-mono-variable.ttf in Resources */,
558554002FE34921004FB58D /* inter-variable.ttf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1132,6 +1159,7 @@
E1A0B0012F5E000100000001 /* EmptyTabPlaceholder.swift in Sources */,
E1C0D0012F72000100000001 /* AppButton.swift in Sources */,
E1B0C0012F60000100000001 /* NetworkWarningBanner.swift in Sources */,
A1B2C3D42F4A000100000001 /* VPNToggleView.swift in Sources */,
50BB17412C30239400518BCA /* RouteCard.swift in Sources */,
50216D932ACB2488009574C9 /* NetworkExtensionAdapter.swift in Sources */,
55B5E81B2F39158200852AA7 /* InternetStatusView.swift in Sources */,
Expand Down
Binary file added NetBird/Fonts/inter-variable.ttf
Binary file not shown.
Binary file added NetBird/Fonts/jetbrains-mono-variable.ttf
Binary file not shown.
5 changes: 5 additions & 0 deletions NetBird/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIAppFonts</key>
<array>
<string>inter-variable.ttf</string>
<string>jetbrains-mono-variable.ttf</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand Down
18 changes: 12 additions & 6 deletions NetBird/Source/App/ViewModels/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class ViewModel: ObservableObject {

@Published var fqdn = ""
@Published var ip = ""
@Published var ipv6 = ""

// Debug
@Published var traceLogsEnabled: Bool {
Expand Down Expand Up @@ -169,8 +170,9 @@ class ViewModel: ObservableObject {
func loadConnectionInfoForProfile(_ profileName: String) {
#if os(iOS)
let entry = profileConnectionCache.entry(for: profileName)
ip = entry?.ip ?? ""
fqdn = entry?.fqdn ?? ""
ip = entry?.ip ?? ""
fqdn = entry?.fqdn ?? ""
ipv6 = entry?.ipv6 ?? ""
#endif
}

Expand Down Expand Up @@ -198,8 +200,9 @@ class ViewModel: ObservableObject {
let activeProfile = ProfileManager.shared.getActiveProfileName()
let cache = ProfileConnectionCache()
let cached = cache.entry(for: activeProfile)
self.ip = cached?.ip ?? ""
self.fqdn = cached?.fqdn ?? ""
self.ip = cached?.ip ?? ""
self.fqdn = cached?.fqdn ?? ""
self.ipv6 = cached?.ipv6 ?? ""
#endif

// Don't load rosenpass settings during init - they trigger expensive SDK initialization.
Expand Down Expand Up @@ -548,13 +551,15 @@ class ViewModel: ObservableObject {
if !self.profileSwitchPending && currentState == .connected {
let newFqdn = details.fqdn.isEmpty ? self.fqdn : details.fqdn
let newIp = details.ip.isEmpty ? self.ip : details.ip
let changed = newFqdn != self.fqdn || newIp != self.ip
let newIpv6 = details.ipv6 ?? self.ipv6
let changed = newFqdn != self.fqdn || newIp != self.ip || newIpv6 != self.ipv6
if changed {
self.fqdn = newFqdn
self.ip = newIp
self.ipv6 = newIpv6
#if os(iOS)
let profile = ProfileManager.shared.getActiveProfileName()
self.profileConnectionCache.save(ip: newIp, fqdn: newFqdn, for: profile)
self.profileConnectionCache.save(ip: newIp, fqdn: newFqdn, ipv6: newIpv6, for: profile)
#endif
}

Expand Down Expand Up @@ -616,6 +621,7 @@ class ViewModel: ObservableObject {
func clearDetails() {
self.ip = ""
self.fqdn = ""
self.ipv6 = ""
defaults.removeObject(forKey: "ip")
defaults.removeObject(forKey: "fqdn")

Expand Down
80 changes: 80 additions & 0 deletions NetBird/Source/App/Views/Components/VPNToggleView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import SwiftUI

#if os(iOS)

struct VPNToggleView: View {
let vpnState: VPNDisplayState
let isLocked: Bool
let onConnect: () -> Void
let onDisconnect: () -> Void

@State private var pulseOpacity: Double = 1.0
// Optimistic override: set immediately on tap so thumb moves without waiting for OS
@State private var optimisticIsOn: Bool? = nil

// Use optimistic value while OS hasn't confirmed yet; fall back to real state
private var isOn: Bool {
optimisticIsOn ?? (vpnState == .connected || vpnState == .connecting)
}

// Pulse whenever the real state is transitioning OR we're waiting for OS confirmation
private var isTransitioning: Bool {
optimisticIsOn != nil || vpnState == .connecting || vpnState == .disconnecting
}

private let trackWidth: CGFloat = 104
private let trackHeight: CGFloat = 56
private var thumbDiameter: CGFloat { trackHeight - 8 }
private var thumbTravel: CGFloat { (trackWidth - thumbDiameter) / 2 - 4 }

var body: some View {
ZStack {
Capsule()
.fill(isOn ? Color.orange : Color(uiColor: .systemGray4))
.opacity(pulseOpacity)
.frame(width: trackWidth, height: trackHeight)
.animation(.easeInOut(duration: 0.3), value: isOn)

Circle()
.fill(Color.white)
.frame(width: thumbDiameter, height: thumbDiameter)
.offset(x: isOn ? thumbTravel : -thumbTravel)
.shadow(color: .black.opacity(0.25), radius: 4, x: 0, y: 2)
.animation(.spring(response: 0.38, dampingFraction: 0.75), value: isOn)
}
.contentShape(Rectangle())
.onTapGesture {
guard !isLocked else { return }
switch vpnState {
case .disconnected:
optimisticIsOn = true
onConnect()
case .connected, .connecting:
optimisticIsOn = false
onDisconnect()
case .disconnecting:
break
}
}
// Clear optimistic as soon as the OS confirms any state change
.onChange(of: vpnState) { _ in
optimisticIsOn = nil
}
// Drive the pulse loop with a cancellable async task keyed to transitioning state
.task(id: isTransitioning) {
guard isTransitioning else {
withAnimation(.easeInOut(duration: 0.25)) { pulseOpacity = 1.0 }
return
}
while !Task.isCancelled {
withAnimation(.easeInOut(duration: 0.85)) { pulseOpacity = 0.45 }
try? await Task.sleep(nanoseconds: 850_000_000)
guard !Task.isCancelled else { break }
withAnimation(.easeInOut(duration: 0.85)) { pulseOpacity = 1.0 }
try? await Task.sleep(nanoseconds: 850_000_000)
}
}
}
}

#endif
Loading
Loading