Skip to content

Conversation

@shuuri-labs
Copy link

@shuuri-labs shuuri-labs commented Dec 9, 2025

This commit introduces full tvOS support for the NetBird iOS client, enabling VPN connectivity on Apple TV devices.

  • Added NetBird TV app target with tvOS 16.0+ deployment

  • Created tvOS-specific UI using SwiftUI optimized for "10-foot experience"

  • Tab-based navigation: Connection, Peers, Networks, Settings

  • Large touch targets and text for Siri Remote navigation

  • TVMainView: Main tab navigation and connection status

  • TVConnectionView: Large connect/disconnect button with status display

  • TVPeersView: List of connected peers with connection details

  • TVNetworksView: Network routes selection and management

  • TVSettingsView: App settings and logout functionality

  • TVAuthView: QR code + device code authentication flow

  • Implemented OAuth device authorization flow for tvOS

  • Displays QR code that user scans with phone to authenticate

  • Shows user code as fallback for manual entry

  • Polls for authentication completion and auto-dismisses on success

tvOS has stricter sandbox restrictions than iOS:

  1. UserDefaults-based Config Storage

    • tvOS blocks file writes to App Group containers
    • Config stored in shared UserDefaults instead of files
    • Added Preferences methods: saveConfigToUserDefaults(), loadConfigFromUserDefaults(), hasConfigInUserDefaults()
  2. Preloaded Config in Go SDK

    • SDK modified to accept config via setConfigFromJSON()
    • Avoids file I/O that would fail in tvOS sandbox
    • Config passed from UserDefaults to SDK at runtime
  3. Raw Syscall Tunnel FD Discovery

    • tvOS SDK doesn't expose ctl_info, sockaddr_ctl, CTLIOCGINFO
    • Implemented findTunnelFileDescriptorTvOS() using raw memory ops
    • Manually defines kernel structure layouts at byte level
    • Uses getpeername() and ioctl() which ARE available on tvOS
  • Added NetBirdTVNetworkExtension target

  • Separate PacketTunnelProvider.swift with tvOS-specific handling

  • Extensive logging for debugging via Console.app

  • Handles "LoginTV" message for device auth flow

  • Loads config from UserDefaults into SDK memory

  • isLoginRequired() now verifies session with management server

  • Previously only checked if config existed (caused post-restart failures)

  • Shows QR code re-auth flow when OAuth session expires

  • Added Platform.swift for iOS/tvOS conditional compilation

  • Shared code uses #if os(tvOS) / #if os(iOS) where needed

  • Common ViewModels work across both platforms

Summary by CodeRabbit

  • New Features

    • Added Apple TV support with native tvOS interface featuring optimized 10-foot TV experience
    • Implemented VPN connection management for Apple TV with network routing and peer management
    • Added device-code authentication flow for Apple TV users
    • Cross-platform unification enabling iOS and tvOS builds from single codebase
  • Chores

    • Updated project configuration for tvOS 17.0+ compatibility
    • Added new framework dependencies for enhanced platform support

✏️ Tip: You can customize this high-level summary in your review settings.

This commit introduces full tvOS support for the NetBird iOS client, enabling VPN connectivity on Apple TV devices. - Added NetBird TV app target with tvOS 16.0+ deployment - Created tvOS-specific UI using SwiftUI optimized for "10-foot experience" - Tab-based navigation: Connection, Peers, Networks, Settings - Large touch targets and text for Siri Remote navigation - TVMainView: Main tab navigation and connection status - TVConnectionView: Large connect/disconnect button with status display - TVPeersView: List of connected peers with connection details - TVNetworksView: Network routes selection and management - TVSettingsView: App settings and logout functionality - TVAuthView: QR code + device code authentication flow - Implemented OAuth device authorization flow for tvOS - Displays QR code that user scans with phone to authenticate - Shows user code as fallback for manual entry - Polls for authentication completion and auto-dismisses on success tvOS has stricter sandbox restrictions than iOS: 1. **UserDefaults-based Config Storage** - tvOS blocks file writes to App Group containers - Config stored in shared UserDefaults instead of files - Added Preferences methods: saveConfigToUserDefaults(), loadConfigFromUserDefaults(), hasConfigInUserDefaults() 2. **Preloaded Config in Go SDK** - SDK modified to accept config via setConfigFromJSON() - Avoids file I/O that would fail in tvOS sandbox - Config passed from UserDefaults to SDK at runtime 3. **Raw Syscall Tunnel FD Discovery** - tvOS SDK doesn't expose ctl_info, sockaddr_ctl, CTLIOCGINFO - Implemented findTunnelFileDescriptorTvOS() using raw memory ops - Manually defines kernel structure layouts at byte level - Uses getpeername() and ioctl() which ARE available on tvOS - Added NetBirdTVNetworkExtension target - Separate PacketTunnelProvider.swift with tvOS-specific handling - Extensive logging for debugging via Console.app - Handles "LoginTV" message for device auth flow - Loads config from UserDefaults into SDK memory - isLoginRequired() now verifies session with management server - Previously only checked if config existed (caused post-restart failures) - Shows QR code re-auth flow when OAuth session expires - Added Platform.swift for iOS/tvOS conditional compilation - Shared code uses #if os(tvOS) / #if os(iOS) where needed - Common ViewModels work across both platforms
@coderabbitai
Copy link

coderabbitai bot commented Dec 9, 2025

Walkthrough

This pull request introduces comprehensive Apple TV (tvOS) support to the NetBird iOS client, adding a new tvOS app target with its own UI, a tvOS network extension for VPN connectivity, cross-platform abstraction layers for UI layout and platform capabilities, and extensive conditional compilation to separate iOS and tvOS code paths.

Changes

Cohort / File(s) Summary
tvOS App Assets
NetBird TV/Assets.xcassets/*
Comprehensive asset catalog for tvOS including accent color, app icons (primary and top shelf), shelf images, logo variants, and NetBird button icon with multi-scale variants and dark mode support.
tvOS App Configuration
NetBirdTV/Info.plist, NetBirdTV/NetBirdTV.entitlements, NetBird TV/ContentView.swift, NetBird TV/NetBirdDebug.entitlements
tvOS app metadata, entitlements enabling network extension and app group capabilities, and basic SwiftUI view structure.
tvOS Network Extension Configuration
NetBirdTVNetworkExtension/Info.plist, NetBirdTVNetworkExtension/...entitlements
Network extension entry points and entitlements for packet tunnel provider and app group sharing.
tvOS Network Extension Implementation
NetBirdTVNetworkExtension/PacketTunnelProvider.swift
Core VPN extension for tvOS handling tunnel lifecycle, IPC messaging with main app, login flows (device-code and SSO), config initialization/persistence, network monitoring, route management, and extensive logging.
tvOS UI Views
NetBird/Source/App/Views/TV/TVAuthView.swift, TVMainView.swift, TVPeersView.swift, TVNetworksView.swift, TVServerView.swift, TVSettingsView.swift
Complete tvOS UI layer with auth view (QR code device flow), main tab view, peers browser with detail panel, networks/routes management, server configuration, and settings with toggles.
tvOS Styling and Layout
NetBird/Source/App/Views/TV/TVColors.swift
Centralized color definitions and layout constants (padding, spacing, fonts, focus effects) tailored for 10-foot TV experiences.
Cross-Platform Abstraction
NetBird/Source/App/Platform/Platform.swift
Screen sizing, device detection (isTV/isPad/isPhone), platform capabilities (VPN support, Safari, clipboard, keyboard), and unified UI layout constants with platform-aware extensions for Font and View.
iOS View Refactoring
NetBird/Source/App/Views/MainView.swift, PeerTabView.swift, RouteTabView.swift, SafariView.swift, SideDrawer.swift
Platform-agnostic routing via conditional compilation; replaced UIScreen-based sizing with Platform.Screen; added iOS guards; extended context menus for clipboard actions on iOS with tvOS text alternatives.
ViewModel Updates
NetBird/Source/App/ViewModels/MainViewModel.swift, ServerViewModel.swift
Added listener bridge classes for SDK callbacks; lazy-loaded preferences with tvOS-specific config storage; extended login flows with completion handlers; conditional platform-specific device name and config behavior; asynchronous URL/setup-key operations.
SDK Integration Helpers
NetBird/Source/App/ViewModels/PeerViewModel.swift, RoutesViewModel.swift
Added Foundation imports and cleaned up unused variables; no functional logic changes.
tvOS Network Extension Helper
NetBirdApp.swift
Added iOS-specific Firebase setup via AppDelegate and tvOS-specific Firebase initialization in app lifecycle; conditional lifecycle observers via scenePhase on tvOS, UIApplication on iOS.
Networking Adapter Updates
NetbirdKit/NetworkExtensionAdapter.swift
Introduced tvOS-specific extension IDs and initialization paths; added device-code login flow with userCode publication; added IPC config transfer helpers; introduced tvOS tunnel file descriptor discovery; added diagnostic messaging for login state/errors.
SDK Preferences and Device Management
NetbirdKit/Preferences.swift, Device.swift, EnvVarPackager.swift
Made Preferences optional return; added tvOS UserDefaults-based config storage alongside file-based paths; added tvOS device naming via cached random identifier; added platform-specific relay connection defaults.
Data Models
NetbirdKit/RoutesSelectionDetails.swift, StatusDetails.swift, ConnectionListener.swift, DNSManager.swift
New data models for route selection details with domain resolution; added Combine and NetBirdSDK imports for observable types and SDK integration.
Xcode Project Configuration
NetBird.xcodeproj/project.pbxproj
Upgraded object version from 56 to 70; added NetBird TV and NetBirdTVNetworkExtension native targets with full build phases; added tvOS build configurations with signing, provisioning, and Swift settings; added new dependencies (FirebaseAnalytics, FirebaseCrashlytics, Lottie); added framework/extension embedding phases.
Build Schemes
NetBird.xcodeproj/xcshareddata/xcschemes/*.xcscheme
Updated LastUpgradeVersion from 1430 to 2610 for compatibility tracking.
Documentation
README.md
Expanded platform support documentation to include tvOS 17.0+, updated build instructions for tvOS frameworks, added separate "Running on Apple TV" section.

Sequence Diagram(s)

sequenceDiagram actor User as User / Remote Device participant TVApp as NetBird TV App participant TVAuth as TVAuthView participant TVExt as PacketTunnelProvider<br/>(VPN Extension) participant NetBirdSDK as NetBird SDK participant ManagementAPI as Management Server User->>TVApp: Launch app TVApp->>TVAuth: Show device code QR + user code TVAuth->>TVExt: Start polling: checkLoginComplete() TVAuth->>TVExt: Start polling: checkLoginError() Note over User,TVAuth: User scans QR on another device<br/>and authenticates on web rect rgb(200, 220, 200) Note over TVExt,ManagementAPI: Polling Loop (every 2 sec) TVExt->>NetBirdSDK: Query login status NetBirdSDK->>ManagementAPI: Check device code flow ManagementAPI-->>NetBirdSDK: Auth complete / pending end alt Login Successful NetBirdSDK-->>TVExt: Config ready TVExt-->>TVAuth: onComplete callback TVAuth->>TVApp: Navigate to main VPN view TVApp->>TVExt: startTunnel() TVExt->>NetBirdSDK: Initialize & start client NetBirdSDK-->>TVExt: VPN connected else Login Error NetBirdSDK-->>TVExt: Error occurred TVExt-->>TVAuth: onError callback TVAuth->>TVApp: Show error message end 
Loading
sequenceDiagram participant iOS as iOS App participant iOSExt as iOS<br/>NetworkExtension participant SDK as NetBird SDK participant ManagementAPI as Management Server participant TV as tvOS App participant TVExt as tvOS<br/>PacketTunnelProvider Note over iOS,ManagementAPI: iOS (Original Flow) iOS->>iOSExt: Start VPN iOSExt->>SDK: Check login required alt Need Login SDK-->>iOSExt: Login required iOSExt-->>iOS: Return login error iOS->>iOS: Open browser/Safari auth else Already Configured iOSExt->>SDK: Initialize & start client SDK->>ManagementAPI: Connect end Note over TV,TVExt: tvOS (New Flow with IPC) TV->>TVExt: IPC: setConfigFromMainApp(configJSON) TVExt->>TVExt: Store config in UserDefaults TV->>TVExt: startTunnel() TVExt->>TVExt: Load config from UserDefaults TVExt->>SDK: Initialize with loaded config TVExt->>SDK: Start client SDK->>ManagementAPI: Connect with shared config ManagementAPI-->>SDK: Connected SDK-->>TVExt: Status updates TVExt-->>TV: IPC: Report status 
Loading
sequenceDiagram participant TV as tvOS App<br/>(MainView) participant ViewModel as MainViewModel participant NetworkExt as NetworkExtensionAdapter participant TVExt as PacketTunnelProvider participant SDK as NetBird SDK TV->>ViewModel: connect() button pressed ViewModel->>NetworkExt: start() NetworkExt->>NetworkExt: Check if login required (tvOS path) alt Config exists in UserDefaults NetworkExt->>NetworkExt: Load config from UserDefaults NetworkExt->>TVExt: sendConfigToExtension(config) TVExt->>TVExt: Store locally via IPC else No config NetworkExt->>TVExt: Initialize config via IPC TVExt->>SDK: Request SSO/auth end NetworkExt->>TVExt: startTunnel() TVExt->>SDK: Run VPN client SDK-->>TVExt: Connected / Error TVExt-->>TV: IPC: Status response TV->>TV: Update UI (connected state) Note over TV,SDK: Network change detected SDK->>TVExt: Network changed (Wi-Fi→Ethernet) TVExt->>TVExt: restartClient() TVExt->>SDK: Restart with new network 
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas requiring extra attention:

  • PacketTunnelProvider.swift — Complex VPN extension logic with IPC messaging, config initialization, device-code login flow, network monitoring, and error handling; requires careful review of message encoding/decoding and lifecycle management.
  • NetworkExtensionAdapter.swift — High-complexity networking adapter with platform-specific branching, tvOS file descriptor discovery, async/await bridging via checked continuations, and IPC config transfer; critical for VPN establishment.
  • MainViewModel.swift — Multiple async callback bridges (SSOCheckListener, SetupKeyErrListener), lazy-loaded preferences, tvOS-specific state management, and platform-specific device behavior; verify state transitions are correct across iOS and tvOS.
  • ServerViewModel.swift — New listener classes and continuation-based flows for SSO and login; verify error propagation and UserDefaults persistence on tvOS.
  • Xcode Project Configuration — Large project.pbxproj changes with new targets, build phases, signing configurations, and framework dependencies; validate build settings are consistent and signing identities are correct.
  • TVSettingsView.swift — Complex tvOS UI with focus management, alert overlays, and toggle controls; verify focus navigation and keyboard handling work correctly on Apple TV.
  • TVAuthView & TVPeersView — Substantial tvOS-specific UI with polling loops, grid layouts, and focus states; ensure animations, polling cancellation, and memory leaks are handled properly.
  • Platform-specific conditional compilation — Verify all #if os(iOS)/#if os(tvOS) blocks are properly paired and don't inadvertently exclude code on either platform.

Possibly related PRs

  • Add force relay connection option #37 — Introduces/supports the "force relay connection" feature with platform-specific defaults in EnvVarPackager and ViewModel handling; overlaps with tvOS relay setting behavior added in this PR.

Suggested reviewers

  • pascal-fischer

Poem

🐰 Hops of joy! A rabbit rejoices—
The NetBird hops to Apple TV's glowing screen,
New vistas unfold: tvOS voices,
Remote controls and QR codes gleam.
From iOS to tvOS, the tunnel's now pristine,
A cross-platform feast in the living room scene! 📺✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.09% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add tvOS (Apple TV) support for NetBird VPN client' clearly and directly describes the main objective of the PR—adding tvOS support—which is the primary focus across all changes including new NetBird TV app target, tvOS UI components, and platform-specific adaptations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-tvos-support-beta

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Update ServerViewModel to use new SDK callback-based API - Fix focus navigation on Networks and Settings tabs - Add white text on focus for better readability across all cards - Increase filter bar spacing to prevent highlight overlap - Add TVSettingsInfoRow for non-interactive display items
- Remove duplicate TVColors structs from 5 TV view files - Add ClearConfig IPC message to clear extension-local config on logout - Switch MainView from viewModel.isIpad to DeviceType.isPad - Remove unused isTV/isIpad properties from MainViewModel - Add TVColors.swift to Xcode project
- Remove dead shared UserDefaults fallback code in NetBirdAdapter - Document tvOS config storage architecture in Preferences.swift - Fix onChange deprecation warnings in TVSettingsView - Add "no peer auth method" detection in ServerViewModel
Comment on lines +264 to 290
func updateManagementURL(url: String, completion: @escaping (Bool?) -> Void) {
let trimmedURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
let newAuth = NetBirdSDKNewAuth(Preferences.configFile(), trimmedURL, nil)
self.managementURL = trimmedURL
var ssoSupported: ObjCBool = false
do {
try newAuth?.saveConfigIfSSOSupported(&ssoSupported)
if ssoSupported.boolValue {
print("SSO is supported")
return true
} else {
print("SSO is not supported. Fallback to setup key")
return false

let listener = SSOCheckListener()
listener.onResult = { ssoSupported, error in
DispatchQueue.main.async {
if let error = error {
print("Failed to check SSO support: \(error.localizedDescription)")
completion(nil)
} else if let supported = ssoSupported {
if supported {
print("SSO is supported")
completion(true)
} else {
print("SSO is not supported. Fallback to setup key")
completion(false)
}
} else {
completion(nil)
}
}
} catch {
print("Failed to check SSO support")
}
return nil

newAuth?.saveConfigIfSSOSupported(listener)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't used anymore, things related to management server URL and setup key were migrated to ServerViewModel. This is leftover code... if this change is important, gotta change it there instead.

Comment on lines +307 to 324
func setSetupKey(key: String, completion: @escaping (Error?) -> Void) {
let newAuth = NetBirdSDKNewAuth(Preferences.configFile(), self.managementURL, nil)
try newAuth?.login(withSetupKeyAndSaveConfig: key, deviceName: Device.getName())
self.managementURL = ""

let listener = SetupKeyErrListener()
listener.onResult = { error in
DispatchQueue.main.async {
if let error = error {
print("Setup key login failed: \(error.localizedDescription)")
completion(error)
} else {
self.managementURL = ""
completion(nil)
}
}
}

newAuth?.login(withSetupKeyAndSaveConfig: listener, setupKey: key, deviceName: Device.getName())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing as management server URL's comment. Migrated to ServerViewModel.

@shuuri-labs
Copy link
Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 16, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

♻️ Duplicate comments (4)
NetBird/Source/App/NetBirdApp.swift (1)

45-53: Firebase configuration duplication noted.

This tvOS initialization mirrors the iOS AppDelegate setup at lines 27-30. See the earlier comment for a suggested refactor to eliminate this duplication.

NetBird.xcodeproj/xcshareddata/xcschemes/NetbirdNetworkExtension.xcscheme (1)

3-3: Consistent with main scheme update.

The LastUpgradeVersion bump to 2610 matches the update in NetBird.xcscheme. Same verification note applies regarding the version number.

NetBird/Source/App/ViewModels/MainViewModel.swift (2)

264-290: This code appears to be dead/duplicated.

Per past review comments, management server URL handling was migrated to ServerViewModel. If this function is no longer used, it should be removed to avoid confusion.


307-324: This code appears to be dead/duplicated.

Per past review comments, setup key handling was migrated to ServerViewModel. If this function is no longer used, it should be removed.

🧹 Nitpick comments (23)
README.md (2)

101-101: Wrap the repository URL in markdown link syntax for consistency.

The other repository entries in this section use markdown link syntax [text](url), but this line uses a bare URL. Wrap the URL to maintain consistent formatting with the rest of the list.

Apply this diff:

- iOS/tvOS Client: https://github.com/netbirdio/ios-client (this repository) + iOS/tvOS Client: [https://github.com/netbirdio/ios-client](https://github.com/netbirdio/ios-client) (this repository)

90-92: Minor: Consider rephrasing for variety.

Line 88 uses "needs to be connected" and line 92 uses "needs to be paired"—both opening with "needs to be." Consider varying the phrasing in one of these sentences to improve readability.

NetBird/Source/App/NetBirdApp.swift (1)

19-34: Consider extracting Firebase configuration to reduce duplication.

The Firebase setup logic is duplicated between the iOS AppDelegate (lines 27-30) and the tvOS init() method (lines 48-51). Consider extracting this into a shared helper method to improve maintainability. Additionally, silent failure when the plist is missing or invalid could complicate debugging—adding error logging would be helpful.

Example refactor:

private func configureFirebase() { guard let path = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist"), let options = FirebaseOptions(contentsOfFile: path) else { print("Warning: GoogleService-Info.plist not found or invalid. Firebase not configured.") return } FirebaseApp.configure(options: options) }

Then call configureFirebase() from both the iOS AppDelegate and tvOS init().

NetbirdKit/Device.swift (1)

27-53: LGTM! Well-designed persistent device name generation.

The implementation correctly generates a unique device name once and caches it for consistency across app launches. The force unwrap on randomElement() is safe since characters is a non-empty literal.

Note: defaults.synchronize() (line 48) is deprecated on modern iOS/tvOS as the system handles persistence automatically, but it's harmless here.

NetbirdKit/Preferences.swift (1)

49-59: Redundant conditional—both branches are identical.

The #if os(tvOS) and #else branches execute the same code. Either remove the conditional or document why it's kept for future differentiation.

 static func newPreferences() -> NetBirdSDKPreferences? { - #if os(tvOS) - // On tvOS, creating SDK Preferences may fail if the app doesn't have write access - // to the App Group container. Try anyway - if it fails, settings will be managed - // via the extension instead. - // Note: The SDK now uses DirectWriteOutConfig which may work better on tvOS. - return NetBirdSDKNewPreferences(configFile(), stateFile()) - #else return NetBirdSDKNewPreferences(configFile(), stateFile()) - #endif }
NetbirdNetworkExtension/NetBirdAdapter.swift (1)

461-484: Regex-based JSON manipulation is fragile.

Using regex patterns like "ManagementURL"\\s*:\\s*"([^"]*)" to parse/modify JSON can fail on escaped quotes, Unicode escapes, or whitespace variations. Consider using JSONSerialization or Codable.

+ /// Extract the management URL from a config JSON string using proper JSON parsing static func extractManagementURL(from configJSON: String) -> String? { - let pattern = "\"ManagementURL\"\\s*:\\s*\"([^\"]*)\"" - guard let regex = try? NSRegularExpression(pattern: pattern, options: []), - let match = regex.firstMatch(in: configJSON, options: [], range: NSRange(configJSON.startIndex..., in: configJSON)), - let urlRange = Range(match.range(at: 1), in: configJSON) else { + guard let data = configJSON.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let url = json["ManagementURL"] as? String, + !url.isEmpty else { return nil } - let url = String(configJSON[urlRange]) - return url.isEmpty ? nil : url + return url }
NetbirdKit/NetworkExtensionAdapter.swift (4)

17-27: Duplicate listener class across codebase.

ConfigSSOListener duplicates the same pattern as SSOCheckListener in MainViewModel.swift and SSOListenerImpl in ServerViewModel.swift. Consider extracting a single shared implementation in NetbirdKit to avoid maintaining multiple copies.


181-216: initializeConfigViaExtension() appears unused.

This function is defined but never called. If it's intended for future use, consider marking it with a TODO or removing it to reduce dead code.

#!/bin/bash # Verify that initializeConfigViaExtension is not called anywhere rg -n "initializeConfigViaExtension" --type swift

247-269: SDK client created on every isLoginRequired() call.

On tvOS, a new NetBirdSDKNewClient is instantiated each time isLoginRequired() is called. This may be expensive if the SDK performs key generation or other heavy initialization. Consider caching the client or deferring this check.


333-335: Remove redundant Void return type.

Per SwiftLint, returning Void in a function declaration is redundant.

- func stop() -> Void { + func stop() { self.vpnManager?.connection.stopVPNTunnel() }
NetBird/Source/App/ViewModels/MainViewModel.swift (1)

18-28: Duplicate SSO listener pattern.

SSOCheckListener duplicates the same pattern as ConfigSSOListener in NetworkExtensionAdapter.swift and SSOListenerImpl in ServerViewModel.swift. Consider consolidating these into a single shared implementation.

NetBird/Source/App/ViewModels/ServerViewModel.swift (1)

15-54: Well-designed listener implementations.

SSOListenerImpl and ErrListenerImpl use typed callbacks instead of optional closures, providing cleaner semantics. However, these duplicate similar listeners in MainViewModel.swift and NetworkExtensionAdapter.swift.

Consider consolidating all SDK listener implementations into a shared location in NetbirdKit to reduce duplication across the codebase.

NetBird/Source/App/Views/MainView.swift (1)

30-30: SwiftLint naming convention note.

SwiftLint flags iOSMainView for not starting with an uppercase character. While IOSMainView would satisfy the linting rule, iOSMainView follows Apple's branding convention (iOS, iPhone, iPad) and is more readable. Consider either:

  1. Keeping iOSMainView and suppressing the lint warning with // swiftlint:disable:next type_name
  2. Renaming to IOSMainView for strict conformance

If you choose to keep the current naming (recommended for readability), add a suppression comment:

+// swiftlint:disable:next type_name struct iOSMainView: View {
NetBird/Source/App/Views/PeerTabView.swift (1)

196-225: Platform-specific context menu is well-implemented.

The iOS clipboard actions and tvOS static text fallback are correctly guarded. Consider wrapping the debug print statements (lines 200, 212) in #if DEBUG for consistency with the logging approach used elsewhere in this PR.

 Button("Copy FQDN") { UIPasteboard.general.string = peer.fqdn + #if DEBUG print("Copied FQDN to clipboard") + #endif viewModel.showFqdnCopiedAlert = true ... } Button("Copy IP") { UIPasteboard.general.string = peer.ip + #if DEBUG print("Copied IP to clipboard") + #endif viewModel.showIpCopiedAlert = true ... }
NetBird/Source/App/Views/TV/TVNetworksView.swift (3)

11-12: Remove unused UIKit import.

The UIKit import doesn't appear to be used in this file. All types used are from SwiftUI.

 import SwiftUI -import UIKit

121-128: Refresh animation duration is not tied to actual data fetch completion.

The 2-second delay is arbitrary and doesn't reflect when getRoutes() actually completes. Consider using a completion handler from getRoutes() to stop the animation when data is ready.

 private func refresh() { isRefreshing = true - viewModel.routeViewModel.getRoutes() -  - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + viewModel.routeViewModel.getRoutes { isRefreshing = false } }

This would require adding a completion parameter to getRoutes() in RoutesViewModel.


190-198: Magic string "invalid Prefix" is fragile.

The comparison against "invalid Prefix" relies on an exact string match from another layer. Consider defining this as a constant or using a more robust check (e.g., an enum or dedicated property on the route model).

NetBird/Source/App/Views/TV/TVSettingsView.swift (1)

141-185: Consider making action non-optional or using a different pattern.

The TVSettingsRow accepts an optional action and disables the button when action == nil (line 183). However, using a non-focusable info row (TVSettingsInfoRow) for display-only items would be more semantically correct. The current pattern could confuse users who focus on a row that does nothing.

If you want to keep this pattern, consider providing clearer visual feedback that the row is disabled:

 .disabled(action == nil) +.opacity(action == nil ? 0.5 : 1.0)
NetBird/Source/App/Platform/Platform.swift (1)

182-189: tvFocusable() modifier applies .focusable() unconditionally on tvOS.

On tvOS, .focusable() makes a view focusable for the Siri Remote. This is correct, but note that applying this to views that are already naturally focusable (like Button) is redundant. Consider documenting the intended use case (e.g., for non-interactive views that need focus).

NetBird/Source/App/Views/TV/TVPeersView.swift (1)

27-28: String comparison for connection state is fragile.

Comparing viewModel.extensionStateText == "Connected" relies on a magic string. If the state text changes (e.g., localization), this check will break silently. Consider using an enum or constant for state comparison.

- if viewModel.extensionStateText == "Connected" && + if viewModel.isConnected && viewModel.peerViewModel.peerInfo.count > 0 {

If isConnected doesn't exist on the ViewModel, consider adding it as a computed property that encapsulates this check.

NetBirdTVNetworkExtension/PacketTunnelProvider.swift (3)

219-238: UserDefaults.synchronize() is deprecated.

synchronize() is deprecated and generally unnecessary as the system handles persistence automatically. While it might seem safer for IPC scenarios, it can actually cause performance issues.

 // Save to extension-local UserDefaults (not shared App Group) UserDefaults.standard.set(configJSON, forKey: "netbird_config_json_local") - UserDefaults.standard.synchronize()

Also applies to line 252.


613-657: Redundant file clearing before writing in initializeLogging.

Lines 630-639 open the file for writing, clear it with an empty string write, then write the log message. This is overly complex. Simply truncating and writing in one step would be cleaner.

 if fileManager.fileExists(atPath: logURLValid.path) { - if let fileHandle = try? FileHandle(forWritingTo: logURLValid) { - do { - try "".write(to: logURLValid, atomically: true, encoding: .utf8) - } catch { - print("Error handling the log file: \(error)") - } - if let data = logMessage.data(using: .utf8) { - fileHandle.write(data) - } - fileHandle.closeFile() - } else { - print("Failed to open the log file for writing.") - } + do { + try logMessage.write(to: logURLValid, atomically: true, encoding: .utf8) + } catch { + print("Failed to overwrite the log file: \(error.localizedDescription)") + } } else {

This simplifies the existing-file case to match the new-file case.


619-620: Unused variables error and success.

The variables error (line 619) and success (line 620) are declared early but only used much later (lines 652-656). Consider moving their declarations closer to usage for clarity, or removing them if the success/error logging isn't critical.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 675c560 and 9466287.

⛔ Files ignored due to path filters (12)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/netbird-tvos-icon.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/netbird-tvos-icon.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/netbird-tvos-icon.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/icon-netbird-button.imageset/icon-netbird-button.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/icon-netbird-button.imageset/icon-netbird-button@2x.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/icon-netbird-button.imageset/icon-netbird-button@3x.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu 1.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@2x 1.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@2x.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@3x 1.png is excluded by !**/*.png
  • NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@3x.png is excluded by !**/*.png
📒 Files selected for processing (60)
  • NetBird TV/Assets.xcassets/AccentColor.colorset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/icon-netbird-button.imageset/Contents.json (1 hunks)
  • NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/Contents.json (1 hunks)
  • NetBird TV/ContentView.swift (1 hunks)
  • NetBird TV/NetBird TVDebug.entitlements (1 hunks)
  • NetBird.xcodeproj/project.pbxproj (25 hunks)
  • NetBird.xcodeproj/xcshareddata/xcschemes/NetBird.xcscheme (1 hunks)
  • NetBird.xcodeproj/xcshareddata/xcschemes/NetbirdNetworkExtension.xcscheme (1 hunks)
  • NetBird/Source/App/NetBirdApp.swift (1 hunks)
  • NetBird/Source/App/Platform/Platform.swift (1 hunks)
  • NetBird/Source/App/ViewModels/MainViewModel.swift (9 hunks)
  • NetBird/Source/App/ViewModels/PeerViewModel.swift (1 hunks)
  • NetBird/Source/App/ViewModels/RoutesViewModel.swift (1 hunks)
  • NetBird/Source/App/ViewModels/ServerViewModel.swift (3 hunks)
  • NetBird/Source/App/Views/Components/SafariView.swift (2 hunks)
  • NetBird/Source/App/Views/Components/SideDrawer.swift (2 hunks)
  • NetBird/Source/App/Views/MainView.swift (4 hunks)
  • NetBird/Source/App/Views/PeerTabView.swift (5 hunks)
  • NetBird/Source/App/Views/RouteTabView.swift (3 hunks)
  • NetBird/Source/App/Views/ServerView.swift (1 hunks)
  • NetBird/Source/App/Views/TV/TVAuthView.swift (1 hunks)
  • NetBird/Source/App/Views/TV/TVColors.swift (1 hunks)
  • NetBird/Source/App/Views/TV/TVMainView.swift (1 hunks)
  • NetBird/Source/App/Views/TV/TVNetworksView.swift (1 hunks)
  • NetBird/Source/App/Views/TV/TVPeersView.swift (1 hunks)
  • NetBird/Source/App/Views/TV/TVServerView.swift (1 hunks)
  • NetBird/Source/App/Views/TV/TVSettingsView.swift (1 hunks)
  • NetBirdTV/Info.plist (1 hunks)
  • NetBirdTV/NetBirdTV.entitlements (1 hunks)
  • NetBirdTV/NetBirdTVNetworkExtension.entitlements (1 hunks)
  • NetBirdTVNetworkExtension/Info.plist (1 hunks)
  • NetBirdTVNetworkExtension/NetBirdTVNetworkExtension.entitlements (1 hunks)
  • NetBirdTVNetworkExtension/NetBirdTVNetworkExtensionDebug.entitlements (1 hunks)
  • NetBirdTVNetworkExtension/PacketTunnelProvider.swift (1 hunks)
  • NetbirdKit/ConnectionListener.swift (1 hunks)
  • NetbirdKit/DNSManager.swift (1 hunks)
  • NetbirdKit/Device.swift (1 hunks)
  • NetbirdKit/EnvVarPackager.swift (1 hunks)
  • NetbirdKit/NetworkExtensionAdapter.swift (6 hunks)
  • NetbirdKit/Preferences.swift (1 hunks)
  • NetbirdKit/RoutesSelectionDetails.swift (1 hunks)
  • NetbirdKit/StatusDetails.swift (1 hunks)
  • NetbirdNetworkExtension/NetBirdAdapter.swift (5 hunks)
  • README.md (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
NetBird/Source/App/Views/RouteTabView.swift (1)
NetBird/Source/App/Platform/Platform.swift (1)
  • size (45-47)
NetBird TV/ContentView.swift (1)
NetBird/Source/App/Views/Components/TransparentGradientButton.swift (1)
  • body (22-53)
NetBird/Source/App/Views/TV/TVServerView.swift (4)
NetbirdKit/Preferences.swift (1)
  • configFile (61-66)
NetbirdKit/Device.swift (1)
  • getName (11-17)
NetBird/Source/App/Platform/Platform.swift (1)
  • size (45-47)
NetBird/Source/App/ViewModels/ServerViewModel.swift (3)
  • clearErrorsFor (298-310)
  • loginWithSetupKey (229-296)
  • changeManagementServerAddress (144-206)
NetBird/Source/App/Views/TV/TVNetworksView.swift (4)
NetbirdKit/NetworkExtensionAdapter.swift (1)
  • getRoutes (478-509)
NetBird/Source/App/ViewModels/RoutesViewModel.swift (3)
  • getRoutes (55-60)
  • deselectRoute (76-82)
  • selectRoute (62-68)
NetBird/Source/App/Platform/Platform.swift (1)
  • size (45-47)
NetBirdTVNetworkExtension/PacketTunnelProvider.swift (2)
  • deselectRoute (586-593)
  • selectRoute (577-584)
NetBird/Source/App/ViewModels/ServerViewModel.swift (3)
NetBirdTVNetworkExtension/PacketTunnelProvider.swift (3)
  • onSuccess (20-22)
  • onError (24-26)
  • login (209-214)
NetbirdKit/NetworkExtensionAdapter.swift (3)
  • onSuccess (20-22)
  • onError (24-26)
  • login (337-380)
NetbirdNetworkExtension/NetBirdAdapter.swift (5)
  • onSuccess (40-42)
  • onSuccess (49-51)
  • onError (36-38)
  • onError (53-55)
  • login (310-313)
NetbirdNetworkExtension/NetBirdAdapter.swift (3)
NetbirdKit/Device.swift (3)
  • getName (11-17)
  • getOsVersion (19-21)
  • getOsName (23-25)
NetbirdKit/Preferences.swift (3)
  • configFile (61-66)
  • stateFile (68-73)
  • saveConfigToUserDefaults (86-93)
NetbirdKit/EnvVarPackager.swift (1)
  • getEnvironmentVariables (12-29)
NetBird/Source/App/Views/PeerTabView.swift (1)
NetBird/Source/App/Platform/Platform.swift (1)
  • size (45-47)
NetbirdKit/NetworkExtensionAdapter.swift (2)
NetBirdTVNetworkExtension/PacketTunnelProvider.swift (4)
  • onSuccess (20-22)
  • onError (24-26)
  • login (209-214)
  • checkLoginComplete (356-390)
NetbirdKit/Preferences.swift (4)
  • loadConfigFromUserDefaults (96-101)
  • configFile (61-66)
  • stateFile (68-73)
  • hasConfigInUserDefaults (104-109)
NetBird/Source/App/Views/TV/TVSettingsView.swift (1)
NetBird/Source/App/ViewModels/MainViewModel.swift (3)
  • setRosenpassEnabled (368-379)
  • close (168-178)
  • clearDetails (292-305)
🪛 LanguageTool
README.md

[style] ~92-~92: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...sical device running tvOS 17.0 or later needs to be [paired with Xcode](https://support....

(REP_NEED_TO_VB)

🪛 markdownlint-cli2 (0.18.1)
README.md

101-101: Bare URL used

(MD034, no-bare-urls)

🪛 SwiftLint (0.57.0)
NetBird/Source/App/Views/MainView.swift

[Error] 30-30: Type name 'iOSMainView' should start with an uppercase character

(type_name)

NetbirdKit/NetworkExtensionAdapter.swift

[Warning] 333-333: Returning Void in a function declaration is redundant

(redundant_void_return)

🔇 Additional comments (82)
README.md (1)

31-33: Documentation update comprehensively covers tvOS support.

The README expansions effectively communicate the new tvOS capability with clear build instructions, platform requirements, and device setup guidance. The separation of build commands for iOS-only vs. iOS+tvOS scenarios is particularly helpful for developers.

Also applies to: 57-59, 70-82, 86-92

NetBird/Source/App/ViewModels/RoutesViewModel.swift (1)

8-8: Explicit Foundation import is appropriate.

Adding the explicit import Foundation statement aligns with best practices for cross-platform integration and ensures clarity of dependencies. This is a suitable change for the tvOS support refactoring.

NetBird/Source/App/Views/Components/SideDrawer.swift (2)

7-13: LGTM! Clear platform separation.

The comments effectively document the iOS-only nature of the side drawer pattern and reference the tvOS alternative (TVSettingsView). The #if os(iOS) guard properly isolates this iOS-specific navigation pattern.


172-172: LGTM! Properly closes the platform guard.

The #endif correctly closes the iOS compilation block, and the inline comment helps readability. The entire SideDrawer and SideMenu implementation (including UIKit-dependent code like UIDevice.current.userInterfaceIdiom and UIScreen.main.bounds) is appropriately isolated to iOS.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json (1)

1-11: Clarify intent for the incomplete tvOS imagestack structure.

The imagestack (Back, Middle layers) contains valid but minimal Contents.json files with no image filenames and no actual image asset files. Confirm whether this is:

  • An intentional placeholder for future image additions
  • An incomplete structure that should include image files now

(Note: Both Back and Middle layers have identical JSON structure with no filename references, and no image files exist anywhere in this imagestack.)

NetBird/Source/App/Views/Components/SafariView.swift (1)

1-11: LGTM! Proper platform gating for iOS-only SafariServices.

The conditional compilation block correctly restricts SafariServices usage to iOS builds, which is necessary since this framework is unavailable on tvOS. The header comment clearly documents the iOS-only nature of this component.

NetBird/Source/App/NetBirdApp.swift (2)

14-17: LGTM: Clean conditional compilation for iOS-only Firebase Performance.

The conditional import is appropriate since Firebase Performance is not available on tvOS.


59-85: Reconsider unifying lifecycle approaches—behavioral equivalence is not confirmed.

iOS uses NotificationCenter with UIApplication lifecycle notifications (lines 60, 65), while tvOS uses scenePhase observation (lines 72-84). The main difference is that onReceive hooks into UIKit's notification system, while scenePhase is SwiftUI's built-in, declarative alternative. However, these approaches may not be behaviorally equivalent. ScenePhase has a number of bugs (or at least, unexpected behavior) and is limited to only 3 states: active, inactive, and background. Verify timing alignment and edge cases between the two approaches before unifying them, as the UIApplication notifications and scenePhase changes may not fire at identical moments.

NetBird TV/Assets.xcassets/Contents.json (1)

1-6: LGTM! Standard asset catalog metadata.

This is a standard Xcode-generated asset catalog root configuration file with correct format and metadata.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json (1)

1-14: LGTM! Standard App Store app icon imagestack configuration.

The layer structure and metadata follow Apple's tvOS asset catalog conventions correctly.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json (1)

1-16: LGTM! Correct tvOS Top Shelf Wide image configuration.

The asset correctly uses the "tv" idiom and provides appropriate scale variants (1x, 2x) for tvOS Top Shelf imagery.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json (1)

1-6: LGTM! Standard imagestack layer metadata.

This is a correctly formatted layer container metadata file following Xcode asset catalog conventions.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json (1)

1-16: LGTM! Correct tvOS Top Shelf image configuration.

The asset correctly uses the "tv" idiom and provides appropriate scale variants for tvOS Top Shelf imagery.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json (1)

1-11: LGTM! Standard app icon layer image content.

The configuration correctly uses the "tv" idiom for the App Store app icon middle layer content.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json (1)

1-6: LGTM! Standard imagestack layer metadata.

This is a correctly formatted layer container metadata file following Xcode asset catalog conventions.

NetBird TV/Assets.xcassets/icon-netbird-button.imageset/Contents.json (1)

1-23: LGTM! Standard cross-platform button icon configuration.

The asset correctly uses the "universal" idiom and provides all standard scale variants (1x, 2x, 3x) with proper naming conventions. This allows the icon to be shared across iOS and tvOS targets.

NetBird TV/Assets.xcassets/AccentColor.colorset/Contents.json (1)

1-11: Standard asset catalog structure.

This is a correctly formatted Xcode asset catalog definition for AccentColor. Since no specific color values are defined, the app will use the system default accent color, which is a valid design choice.

NetBird TV/Assets.xcassets/netbird-logo-menu.imageset/Contents.json (1)

1-56: Well-structured asset catalog with complete variants.

This asset definition properly includes all standard scales (1x, 2x, 3x) with both light and dark appearance variants, ensuring the logo displays correctly across all device resolutions and appearance modes.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json (1)

1-6: Standard metadata structure for empty layer.

This minimal structure is appropriate for the back layer of the App Store icon imagestack, which typically doesn't require visible content.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json (1)

1-17: Correctly structured tvOS image stack.

The three-layer structure (Front, Middle, Back) follows Apple's tvOS parallax icon conventions and properly coordinates the individual layer assets.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json (1)

1-6: Standard layer metadata structure.

This metadata file correctly provides Xcode authoring information for the Middle image stack layer, coordinating with the nested Content.imageset.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json (1)

1-17: Asset catalog structure looks correct.

The JSON structure is valid for a tvOS Content.imageset. The 1x scale references netbird-tvos-icon.png, and the 2x scale entry (without a filename) is acceptable when a @2x asset is not provided.

Note: The AI summary incorrectly describes this file as being for the "Front.imagestacklayer", but the actual file path indicates it's for the "Back.imagestacklayer".

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json (1)

1-32: LGTM! tvOS asset catalog structure is correct.

The brandassets Contents.json properly defines all required tvOS assets with correct idioms, roles, and dimensions:

  • Primary app icons at both App Store (1280x768) and standard (400x240) sizes
  • Top Shelf images in both wide (2320x720) and standard (1920x720) formats

This follows Apple's tvOS asset catalog requirements.

NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json (1)

1-6: File structure is correct—imagestacklayer Contents.json is metadata-only.

The layer-level Contents.json file contains only metadata, which is the correct structure. Image definitions belong in the nested Content.imageset/Contents.json file (which already has the images array with appropriate entries). All comparable imagestacklayer Contents.json files in this project follow the same metadata-only pattern.

Likely an incorrect or invalid review comment.

NetbirdKit/ConnectionListener.swift (1)

9-9: LGTM!

The NetBirdSDK import is required since ConnectionListener conforms to NetBirdSDKConnectionListenerProtocol.

NetBird.xcodeproj/xcshareddata/xcschemes/NetBird.xcscheme (1)

3-3: Verify the Xcode version in scheme metadata.

The LastUpgradeVersion of 2610 appears unusually high. Xcode version numbers typically follow the format where 1430 = Xcode 14.3 and 1610 = Xcode 16.1. Version 2610 would imply Xcode 26.1, which doesn't exist.

If this was generated by Xcode automatically, it's harmless. Otherwise, confirm this wasn't a typo (perhaps intended 1610 for Xcode 16.1?).

NetbirdKit/DNSManager.swift (1)

9-9: LGTM!

The NetBirdSDK import is required since DNSManager conforms to NetBirdSDKDnsManagerProtocol.

NetBird/Source/App/ViewModels/PeerViewModel.swift (1)

8-9: LGTM!

The explicit Foundation import improves clarity, even though Combine re-exports it. The view model logic is clean and well-structured.

NetbirdKit/EnvVarPackager.swift (1)

17-25: LGTM! Platform-specific relay defaults are sensible.

Defaulting to forced relay on iOS while allowing direct connections on tvOS aligns with the different network environments of these platforms.

NetBirdTV/NetBirdTVNetworkExtension.entitlements (1)

1-19: LGTM! Entitlements are correctly configured.

The packet-tunnel-provider capability and group.io.netbird.app.tv app group are appropriate for the tvOS network extension.

NetbirdKit/RoutesSelectionDetails.swift (1)

1-8: LGTM!

Standard file header and imports for Combine-backed observable models.

NetbirdKit/StatusDetails.swift (1)

9-9: LGTM!

The Combine import is required since PeerInfo conforms to ObservableObject.

NetbirdNetworkExtension/NetBirdAdapter.swift (2)

133-214: Thorough low-level implementation for tvOS tunnel discovery.

The raw memory operations correctly replicate the kernel structure layout for ctl_info and sockaddr_ctl. The hardcoded sizes (100 bytes for ctl_info, 32 bytes for sockaddr_ctl) and offsets match the Darwin kernel ABI which is stable.

Good use of logging via adapterLogger for debugging tunnel discovery issues on tvOS.


321-451: Well-structured async login flow with proper completion tracking.

The NSLock-based completion guard correctly prevents duplicate callbacks, and the captured authRef ensures config can be saved after successful authentication. The separation of handleSuccess/handleError closures keeps the logic clean.

NetbirdKit/NetworkExtensionAdapter.swift (3)

356-369: tvOS response parsing handles missing userCode gracefully.

The code correctly falls back to using the whole string if the expected | separator is not found. Good defensive handling.


385-428: Well-structured diagnostic response parsing.

The checkLoginComplete method handles multiple response formats gracefully with progressive fallbacks, which is good for backward compatibility.


596-665: tvOS config IPC methods are well-implemented.

sendConfigToExtension and clearExtensionConfig properly handle session availability, encoding errors, and provide completion callbacks. Good error logging throughout.

NetBird.xcodeproj/project.pbxproj (1)

1341-1341: Different development team for Release configuration.

The iOS Release configuration uses DEVELOPMENT_TEAM = 94333M4JTA while Debug uses TA739QLA7A. Ensure this is intentional (e.g., different teams for development vs distribution).

NetBird/Source/App/ViewModels/MainViewModel.swift (4)

104-115: Good optimization with lazy preferences loading.

Deferring SDK initialization until first access avoids blocking app startup, especially important on tvOS where key generation is expensive. Well-documented with comments.


149-166: Connect flow is well-structured.

The button lock with 3-second timeout, logging, and async Task handling is appropriate for VPN connection management.


240-258: tvOS-specific state text updates are well-implemented.

The switch statement correctly maps VPN states to user-friendly text and triggers route fetching when connected. Good use of platform-specific compilation.


297-305: Good cleanup for tvOS config on logout.

Clearing both UserDefaults config and extension-local config ensures no stale credentials remain after logout.

NetBird/Source/App/ViewModels/ServerViewModel.swift (3)

164-205: Good use of checked continuation for async bridging.

The withCheckedContinuation pattern correctly bridges the callback-based SDK API to async/await. The tvOS-specific handling of file permission errors by falling back to UserDefaults is a pragmatic workaround.


208-227: tvOS config persistence fallback is well-implemented.

The saveConfigToUserDefaults helper correctly extracts config JSON from the authenticator and persists it when file writes are blocked. Error logging is appropriate.


259-295: Login with setup key handles tvOS constraints appropriately.

Same pattern as changeManagementServerAddress with proper continuation bridging and tvOS file permission error handling.

NetBird/Source/App/Views/ServerView.swift (1)

272-275: No functional changes.

Only trailing whitespace adjustment. LGTM.

NetBird TV/NetBird TVDebug.entitlements (1)

1-14: LGTM! Standard tvOS network extension entitlements.

The entitlements correctly declare the packet-tunnel-provider capability and app group for IPC between the tvOS app and its network extension. The configuration follows Apple's standard pattern for VPN-enabled apps.

NetBirdTV/NetBirdTV.entitlements (2)

1-18: LGTM! Entitlements properly configured for tvOS VPN app.

The entitlements correctly declare packet-tunnel-provider and app group capabilities, matching the debug entitlements and following Apple's standard pattern.


5-5: Confirm tvOS 17.0 is the correct minimum version requirement.

The code consistently specifies tvOS 17.0 as the minimum: NetBirdTV/Info.plist sets MinimumOSVersion to "17.0" with the comment "17.0 required for VPN support" (line 13), and NetBirdTV/NetBirdTV.entitlements confirms this with "requires tvOS 17+" (line 5). If the PR description claims "tvOS 16.0+ deployment", it should be corrected to reflect the actual tvOS 17.0+ requirement.

NetBirdTVNetworkExtension/NetBirdTVNetworkExtensionDebug.entitlements (1)

1-14: LGTM! Network extension debug entitlements properly configured.

The entitlements correctly match the main app's app group identifier (group.io.netbird.app.tv), enabling proper IPC between the tvOS app and its network extension.

NetBirdTVNetworkExtension/Info.plist (1)

1-13: LGTM! Standard packet tunnel extension configuration.

The Info.plist correctly defines the extension point identifier and principal class using the standard pattern. The $(PRODUCT_MODULE_NAME) variable will be resolved at build time to reference the PacketTunnelProvider implementation.

NetBirdTV/Info.plist (2)

13-15: MinimumOSVersion set to 17.0.

This aligns with the VPN/Network Extension requirement noted in the entitlements, but conflicts with the PR summary stating "tvOS 16.0+ deployment". This version requirement was already flagged in the entitlements review.


1-27: LGTM! tvOS app Info.plist properly configured.

The configuration is appropriate for a tvOS utilities app with automatic UI style support. The bundle display name, category, and launch screen setup follow Apple's standard patterns.

NetBirdTVNetworkExtension/NetBirdTVNetworkExtension.entitlements (1)

1-14: LGTM! Network extension entitlements consistent across targets.

The app group identifier (group.io.netbird.app.tv) is consistent across all tvOS targets (app and extension, debug and release), ensuring proper IPC functionality.

NetBird/Source/App/Views/RouteTabView.swift (2)

86-92: LGTM! Platform-agnostic layout implementation.

The refactoring from UIScreen.main.bounds to Screen helper and Layout.fontScale properly abstracts platform-specific sizing, enabling code sharing between iOS and tvOS while maintaining appropriate layout for each platform.


107-110: LGTM! Consistent use of platform abstraction.

The padding calculations consistently use Screen helper for cross-platform compatibility.

NetBird/Source/App/Views/MainView.swift (3)

13-27: LGTM! Clean platform-specific routing implementation.

The MainView correctly acts as a platform router, delegating to TVMainView for tvOS and iOSMainView for iOS. The conditional compilation ensures each platform only compiles its relevant code, maintaining clean separation.


75-75: LGTM! Platform-agnostic sizing with device type detection.

The refactoring correctly uses Screen.height and DeviceType.isPad for adaptive layout that works across iOS device types (iPhone/iPad) while being compatible with the broader platform abstraction supporting tvOS.

Also applies to: 85-85


29-566: LGTM! iOS-specific code properly isolated.

The #if os(iOS) conditional compilation correctly isolates iOS-specific UI code, ensuring it doesn't interfere with tvOS builds while maintaining the full iOS implementation.

NetBird/Source/App/Views/PeerTabView.swift (2)

7-15: LGTM!

The conditional UIKit import and documentation comments are well-structured. This correctly limits UIKit usage to iOS builds where UIPasteboard is actually used.


81-89: Good adoption of cross-platform sizing abstraction.

The migration from UIScreen-based sizing to Screen.* metrics aligns well with the Platform.swift abstraction for consistent behavior across iOS and tvOS.

NetBird/Source/App/Views/TV/TVAuthView.swift (3)

1-19: Well-documented device code flow implementation.

Good documentation explaining the authentication pattern. The QR code approach is appropriate for tvOS where Safari isn't available.


214-231: QR code generation looks correct.

The use of CIFilter.qrCodeGenerator() with appropriate scaling for tvOS visibility is well-implemented.


192-198: Good timer lifecycle management.

Timer is correctly started on onAppear and invalidated on onDisappear, preventing memory leaks.

NetBird/Source/App/Views/TV/TVMainView.swift (4)

24-55: Clean TabView navigation structure.

The tab-based navigation with proper tags and environment object passing is well-implemented for the tvOS 10-foot experience.


62-99: Auth flow integration with TVAuthView is complete.

All necessary callbacks (onCancel, onComplete, onError, checkLoginComplete, checkLoginError) are properly wired to the network extension adapter.


267-282: Connection button logic handles transitional states.

The handleTap function correctly handles both "Connected" and "Connecting" states for disconnection, allowing users to cancel an in-progress connection attempt. The logger usage provides good observability.


212-240: Good tvOS focus state handling.

The @FocusState usage with scale animation provides appropriate visual feedback for Siri Remote navigation. The button is correctly disabled when buttonLock is true.

NetBird/Source/App/Views/TV/TVServerView.swift (2)

25-28: ServerViewModel initialization looks correct.

The view model is properly initialized with the config file path and device name, matching the existing iOS implementation patterns.


305-329: Server change logic is well-structured.

The function correctly handles the three cases: empty inputs, URL-only, and URL with setup key. The Task.yield() allows UI updates before async operations.

NetBird/Source/App/Views/TV/TVNetworksView.swift (2)

200-206: Toggle route implementation is clean.

The delegation to routeViewModel.selectRoute/deselectRoute is straightforward and matches the existing RoutesViewModel API.


17-36: TVNetworksView structure is well-organized.

The conditional rendering based on connection state and route availability follows the established pattern from other TV views.

NetBird/Source/App/Views/TV/TVColors.swift (2)

18-57: Well-structured color abstraction with safe fallbacks.

The colorOrFallback helper pattern ensures graceful degradation when asset catalog colors are missing. This is a clean approach for tvOS theming.


63-165: Layout constants are comprehensive and well-documented.

The constants cover all major UI aspects (padding, spacing, typography, focus effects) with appropriate values for TV viewing distances. Consider extracting these to a protocol or making them configurable if you anticipate supporting multiple TV form factors in the future.

NetBird/Source/App/Views/TV/TVSettingsView.swift (2)

17-116: Clean two-pane layout with proper EnvironmentObject usage.

The settings view structure is well-organized with sections for Connection, Advanced, and Info. The binding for Rosenpass using a custom getter/setter (lines 57-60) correctly delegates to the ViewModel's setRosenpassEnabled method.


268-359: Focus management in the alert is well-implemented.

The onChange handler (lines 349-357) that pulls focus back when it escapes is a good pattern for tvOS modal dialogs. The _ = oldValue on line 350 to suppress the unused warning is acceptable, though you could also use onChange(of: focusedButton) { newValue in ... } if targeting older tvOS versions isn't a concern.

NetBird/Source/App/Platform/Platform.swift (2)

15-48: tvOS screen dimensions are hardcoded to 1920x1080 points.

The comment mentions "3840x2160 for 4K, but points are same" - this is correct for tvOS where the point-based coordinate system uses 1920x1080 regardless of actual resolution. However, note that these are static properties computed at call time, which is fine for tvOS but on iOS, screen dimensions could potentially change (e.g., during Split View on iPad).


91-135: VPN support via Network Extension was added in tvOS 17.0.

tvOS 17 added support for third-party VPN apps through Network Extensions. The code correctly checks for tvOS 17.0+ before enabling VPN support. The Network Extension framework allows developers to create custom VPN solutions for tvOS. The capability flags are well-structured and the comments explaining tvOS limitations are helpful.

NetBird/Source/App/Views/TV/TVPeersView.swift (2)

114-184: TVPeerCard is well-implemented with proper focus handling.

The focus-driven styling, scale animation, and selection state management are appropriate for tvOS. The status color mapping (lines 177-183) covers the expected states.


244-286: Filter bar with focus-aware styling looks good.

The TVFilterBar and TVFilterButton components handle focus states well with appropriate visual feedback (scale, color changes).

NetBirdTVNetworkExtension/PacketTunnelProvider.swift (2)

17-27: ConfigInitSSOListener handles both success and error paths correctly.

The listener properly propagates results via the closure callback. The optional typing for onResult is appropriate.


395-470: Completion handler may be called multiple times in loginTV.

The completionHandler is called in onURL callback (line 429), and could potentially be called again in onError if urlSentToApp check fails due to a race condition (though the lock helps). More critically, the NEPacketTunnelProvider's handleAppMessage completion handler should only be called once. The current logic is safe due to the lock, but consider documenting this invariant clearly.

Verify that the onURL callback is guaranteed to be called before onSuccess/onError, and that only one of success/error is ever called. If both onURL and onSuccess can trigger in sequence, the current design is correct. However, if there's any scenario where onError could be called before onURL and then onURL is called afterward, you'd have a problem.

Comment on lines +8 to +11
{
"idiom" : "tv",
"scale" : "2x"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider providing 2x scale icon asset.

Similar to the Middle layer, the 2x scale entry has no filename. Providing dedicated 2x versions for all icon layers would ensure optimal quality on higher-resolution displays.

🤖 Prompt for AI Agents
In NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset around lines 8-11, the 2x scale entry exists but has no "filename" specified; add a filename key pointing to the 2x PNG asset (e.g., "filename": "Front@2x.png") so Xcode can find the high-resolution icon. Ensure the referenced 2x image file is added to the asset catalog, matches the correct dimensions for tv 2x, and update the Contents.json entry accordingly. 
Comment on lines +8 to +11
{
"idiom" : "tv",
"scale" : "2x"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider providing 2x scale icon asset.

The 2x scale entry has no filename specified. While the app will function using the 1x asset scaled up, providing a dedicated 2x version would ensure optimal icon quality on higher-resolution tvOS devices.

🤖 Prompt for AI Agents
In NetBird TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset around lines 8-11, the 2x scale entry lacks a "filename" field; add a "filename" property pointing to a dedicated 2x image file (and add that image to the .imageset) so the asset catalog uses a proper high-resolution icon for tvOS devices; ensure the provided image matches the expected 2x dimensions and the filename exactly matches the file added to the asset catalog. 
Comment on lines +10 to +24
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}

#Preview {
ContentView()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused placeholder view.

This appears to be Xcode-generated boilerplate that is not used in the actual tvOS app. According to the PR objectives, the real UI is implemented in TVMainView, TVConnectionView, TVPeersView, TVNetworksView, TVSettingsView, and TVAuthView. This placeholder should be removed to avoid confusion.

Apply this diff to remove the unused file, or verify if it's actually used as an entry point:

-// -// ContentView.swift -// NetBird TV -// -// Created by Ashley Mensah on 02.12.25. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - } -} - -#Preview { - ContentView() -}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In NetBird TV/ContentView.swift around lines 10 to 24, this Xcode-generated placeholder view (Image + "Hello, world!" preview) is unused and should be removed; either delete the entire ContentView.swift file and its #Preview if it's not referenced anywhere, or if this file is serving as the app entry view, replace the body to return your actual entry view (e.g., TVMainView()) and remove the preview block—ensure there are no remaining imports or references to ContentView in the app target after deletion or replacement. 
Comment on lines +973 to +1025
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.app.tv;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = appletvos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 17.6;
};
name = Debug;
};
441C5AF72EDF0DB00055EEFC /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TA739QLA7A;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = NetBird;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIUserInterfaceStyle = Automatic;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "io.netbird.app.NetBird-TV";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 26.1;
};
name = Release;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Bundle identifier mismatch between Debug and Release configurations.

Debug uses io.netbird.app.tv but Release uses io.netbird.app.NetBird-TV. This will cause code signing and provisioning profile issues. The extension bundle IDs also have a similar mismatch (io.netbird.app.tv.extension vs io.netbird.app.NetBird-TV.NetBirdTVNetworkExtension).

Ensure consistent bundle identifiers across configurations:

 /* Release */ = { ... - PRODUCT_BUNDLE_IDENTIFIER = "io.netbird.app.NetBird-TV"; + PRODUCT_BUNDLE_IDENTIFIER = io.netbird.app.tv; ... };

And for the extension:

 /* Release */ = { ... - PRODUCT_BUNDLE_IDENTIFIER = "io.netbird.app.NetBird-TV.NetBirdTVNetworkExtension"; + PRODUCT_BUNDLE_IDENTIFIER = io.netbird.app.tv.extension; ... };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In NetBird.xcodeproj/project.pbxproj around lines 973 to 1025, the PRODUCT_BUNDLE_IDENTIFIER values differ between Debug (io.netbird.app.tv) and Release (io.netbird.app.NetBird-TV) which will break code signing and provisioning (same issue exists for the extension bundle IDs); make the bundle identifiers consistent across all build configurations by choosing the canonical identifier (e.g., io.netbird.app.tv) and replacing the mismatched PRODUCT_BUNDLE_IDENTIFIER entries in Release (and any other configs) to match Debug, then do the same canonicalization for the extension targets (replace io.netbird.app.NetBird-TV.NetBirdTVNetworkExtension with the matching extension id e.g., io.netbird.app.tv.extension), and finally update provisioning_profile/provisioning_profile_specifier, development team or entitlements/Info.plist entries to match the chosen identifiers so signing and profiles remain valid. 
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 26.1;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Invalid tvOS deployment target version 26.1.

TVOS_DEPLOYMENT_TARGET = 26.1 is not a valid tvOS version. Current tvOS versions are in the 17.x-18.x range. This appears to be conflated with Xcode version 26.1. The Debug configuration correctly uses 17.6.

- TVOS_DEPLOYMENT_TARGET = 26.1; + TVOS_DEPLOYMENT_TARGET = 17.6;

Also applies to: 1061-1061, 1096-1096

🤖 Prompt for AI Agents
In NetBird.xcodeproj/project.pbxproj around lines 1022, 1061 and 1096, the TVOS_DEPLOYMENT_TARGET values are incorrectly set to 26.1 (Xcode version) — replace these with a valid tvOS version used elsewhere in the project (e.g., 17.6 to match Debug) and ensure all configurations (Debug/Release/Any) consistently use the correct numeric tvOS deployment target; update each occurrence to the correct value and keep formatting identical to surrounding entries. 
Comment on lines +289 to +299
.onChange(of: serverViewModel.viewErrors.ssoNotSupportedError) { _, newValue in
if newValue != nil {
showSetupKeyField = true
}
}
.onChange(of: serverViewModel.isOperationSuccessful) { _, newValue in
if newValue {
viewModel.showServerChangedInfo = true
isPresented = false
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

onChange with two parameters requires iOS 17+/tvOS 17+.

The onChange(of:) { _, newValue in } signature with two parameters (old and new values) was introduced in iOS 17. This will fail to compile on tvOS 16.

- .onChange(of: serverViewModel.viewErrors.ssoNotSupportedError) { _, newValue in + .onChange(of: serverViewModel.viewErrors.ssoNotSupportedError) { newValue in if newValue != nil { showSetupKeyField = true } } - .onChange(of: serverViewModel.isOperationSuccessful) { _, newValue in + .onChange(of: serverViewModel.isOperationSuccessful) { newValue in if newValue { viewModel.showServerChangedInfo = true isPresented = false } }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.onChange(of: serverViewModel.viewErrors.ssoNotSupportedError) { _, newValue in
if newValue != nil {
showSetupKeyField = true
}
}
.onChange(of: serverViewModel.isOperationSuccessful) { _, newValue in
if newValue {
viewModel.showServerChangedInfo = true
isPresented = false
}
}
.onChange(of: serverViewModel.viewErrors.ssoNotSupportedError) { newValue in
if newValue != nil {
showSetupKeyField = true
}
}
.onChange(of: serverViewModel.isOperationSuccessful) { newValue in
if newValue {
viewModel.showServerChangedInfo = true
isPresented = false
}
}
🤖 Prompt for AI Agents
In NetBird/Source/App/Views/TV/TVServerView.swift around lines 289 to 299, the use of onChange(of:) with a two-parameter closure (old, new) requires iOS/tvOS 17+ and will fail on tvOS 16; replace the two-parameter form with the single-parameter form or guard availability: use .onChange(of: serverViewModel.viewErrors.ssoNotSupportedError) { newValue in if newValue != nil { showSetupKeyField = true } } and .onChange(of: serverViewModel.isOperationSuccessful) { newValue in if newValue { viewModel.showServerChangedInfo = true; isPresented = false } } or wrap the two-parameter closure in an if #available(tvOS 17, *) { ... } else { use single-parameter closures } to preserve behavior on older OS versions. 
Comment on lines 61 to 73
static func configFile() -> String {
let fileManager = FileManager.default
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.io.netbird.app")
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
let logURL = groupURL?.appendingPathComponent("netbird.cfg")
return logURL!.relativePath
}

static func stateFile() -> String {
let fileManager = FileManager.default
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.io.netbird.app")
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
let logURL = groupURL?.appendingPathComponent("state.json")
return logURL!.relativePath
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Force unwraps can crash if app group is misconfigured.

containerURL(forSecurityApplicationGroupIdentifier:) returns nil if the app group capability isn't properly configured in the entitlements or provisioning profile. The force unwraps on lines 64-65 and 71-72 will crash the app in that case.

Consider returning an optional or using a fallback path.

 static func configFile() -> String { let fileManager = FileManager.default - let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) - let logURL = groupURL?.appendingPathComponent("netbird.cfg") - return logURL!.relativePath + guard let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else { + // Fallback to documents directory if app group is unavailable + let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + return (documentsPath as NSString).appendingPathComponent("netbird.cfg") + } + return groupURL.appendingPathComponent("netbird.cfg").path }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static func configFile() -> String {
let fileManager = FileManager.default
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.io.netbird.app")
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
let logURL = groupURL?.appendingPathComponent("netbird.cfg")
return logURL!.relativePath
}
static func stateFile() -> String {
let fileManager = FileManager.default
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.io.netbird.app")
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
let logURL = groupURL?.appendingPathComponent("state.json")
return logURL!.relativePath
}
static func configFile() -> String {
let fileManager = FileManager.default
guard let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
// Fallback to documents directory if app group is unavailable
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
return (documentsPath as NSString).appendingPathComponent("netbird.cfg")
}
return groupURL.appendingPathComponent("netbird.cfg").path
}
static func stateFile() -> String {
let fileManager = FileManager.default
let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
let logURL = groupURL?.appendingPathComponent("state.json")
return logURL!.relativePath
}
🤖 Prompt for AI Agents
In NetbirdKit/Preferences.swift around lines 61 to 73, the code force-unwraps containerURL which can be nil if the app group is misconfigured; change these methods to safely handle a nil groupURL by either returning an optional String? (nil when group missing) or returning a safe fallback path (e.g., FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first appending the filename), and remove the force-unwraps; ensure you handle and propagate the optional/ fallback consistently to callers and log a clear error when the app group URL cannot be obtained. 
Comment on lines +231 to +254
#if os(tvOS)
// On tvOS, the filesystem is blocked for the App Group container.
// Create the client with empty paths and load config from local storage instead.
self.client = NetBirdSDKNewClient("", "", deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager)!

// Load config from extension-local storage (set via IPC from main app)
// Note: Shared App Group UserDefaults does NOT work on tvOS between app and extension
// due to sandbox restrictions. Config must be transferred via IPC.
let configJSON: String? = UserDefaults.standard.string(forKey: "netbird_config_json_local")

if let configJSON = configJSON {
let updatedConfig = Self.updateDeviceNameInConfig(configJSON, newName: deviceName)
do {
try self.client.setConfigFromJSON(updatedConfig)
adapterLogger.info("init: tvOS - loaded config successfully")
} catch {
adapterLogger.error("init: tvOS - failed to load config: \(error.localizedDescription)")
}
} else {
adapterLogger.info("init: tvOS - no config found, client initialized without config")
}
#else
self.client = NetBirdSDKNewClient(Preferences.configFile(), Preferences.stateFile(), deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager)!
#endif
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Force unwrap on SDK client creation can crash.

NetBirdSDKNewClient may return nil, but the force unwrap on lines 234 and 253 will crash the extension if initialization fails.

 #if os(tvOS) - self.client = NetBirdSDKNewClient("", "", deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager)! + guard let client = NetBirdSDKNewClient("", "", deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager) else { + fatalError("Failed to create NetBird SDK client") + } + self.client = client #else - self.client = NetBirdSDKNewClient(Preferences.configFile(), Preferences.stateFile(), deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager)! + guard let client = NetBirdSDKNewClient(Preferences.configFile(), Preferences.stateFile(), deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager) else { + fatalError("Failed to create NetBird SDK client") + } + self.client = client #endif

Alternatively, make the initializer failable (init?) to allow graceful error handling upstream.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#if os(tvOS)
// On tvOS, the filesystem is blocked for the App Group container.
// Create the client with empty paths and load config from local storage instead.
self.client = NetBirdSDKNewClient("", "", deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager)!
// Load config from extension-local storage (set via IPC from main app)
// Note: Shared App Group UserDefaults does NOT work on tvOS between app and extension
// due to sandbox restrictions. Config must be transferred via IPC.
let configJSON: String? = UserDefaults.standard.string(forKey: "netbird_config_json_local")
if let configJSON = configJSON {
let updatedConfig = Self.updateDeviceNameInConfig(configJSON, newName: deviceName)
do {
try self.client.setConfigFromJSON(updatedConfig)
adapterLogger.info("init: tvOS - loaded config successfully")
} catch {
adapterLogger.error("init: tvOS - failed to load config: \(error.localizedDescription)")
}
} else {
adapterLogger.info("init: tvOS - no config found, client initialized without config")
}
#else
self.client = NetBirdSDKNewClient(Preferences.configFile(), Preferences.stateFile(), deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager)!
#endif
#if os(tvOS)
// On tvOS, the filesystem is blocked for the App Group container.
// Create the client with empty paths and load config from local storage instead.
guard let client = NetBirdSDKNewClient("", "", deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager) else {
fatalError("Failed to create NetBird SDK client")
}
self.client = client
// Load config from extension-local storage (set via IPC from main app)
// Note: Shared App Group UserDefaults does NOT work on tvOS between app and extension
// due to sandbox restrictions. Config must be transferred via IPC.
let configJSON: String? = UserDefaults.standard.string(forKey: "netbird_config_json_local")
if let configJSON = configJSON {
let updatedConfig = Self.updateDeviceNameInConfig(configJSON, newName: deviceName)
do {
try self.client.setConfigFromJSON(updatedConfig)
adapterLogger.info("init: tvOS - loaded config successfully")
} catch {
adapterLogger.error("init: tvOS - failed to load config: \(error.localizedDescription)")
}
} else {
adapterLogger.info("init: tvOS - no config found, client initialized without config")
}
#else
guard let client = NetBirdSDKNewClient(Preferences.configFile(), Preferences.stateFile(), deviceName, osVersion, osName, self.networkChangeListener, self.dnsManager) else {
fatalError("Failed to create NetBird SDK client")
}
self.client = client
#endif
🤖 Prompt for AI Agents
In NetbirdNetworkExtension/NetBirdAdapter.swift around lines 231 to 254, the NetBirdSDKNewClient(...) calls are force-unwrapped and can crash if they return nil; change both initializations to safely handle a nil client by using optional binding (guard let or if let) instead of "!" — on failure log an error with details and fail the adapter init gracefully (return/throw or make the adapter init? failable), or propagate the error upstream; ensure you handle both the tvOS branch and the non-tvOS branch consistently and avoid creating a partially initialized adapter when the client is nil. 
Comment on lines +286 to +296
let fd = self.tunnelFileDescriptor ?? 0
let ifName = self.interfaceName ?? "unknown"

let connectionListener = ConnectionListener(adapter: self, completionHandler: completionHandler)
self.client.setConnectionListener(connectionListener)

let envList = UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName).flatMap { EnvVarPackager.getEnvironmentVariables(defaults: $0)

let envList = UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName).flatMap {
EnvVarPackager.getEnvironmentVariables(defaults: $0)
}
try self.client.run(self.tunnelFileDescriptor ?? 0, interfaceName: self.interfaceName, envList: envList)

try self.client.run(fd, interfaceName: ifName, envList: envList)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Using fd=0 as fallback masks tunnel discovery failures.

If tunnelFileDescriptor returns nil, passing fd=0 (stdin) to the SDK will cause undefined behavior. The error should be propagated instead.

 public func start(completionHandler: @escaping (Error?) -> Void) { DispatchQueue.global().async { do { - let fd = self.tunnelFileDescriptor ?? 0 - let ifName = self.interfaceName ?? "unknown" + guard let fd = self.tunnelFileDescriptor else { + completionHandler(NSError(domain: "io.netbird.NetbirdNetworkExtension", code: 1003, userInfo: [NSLocalizedDescriptionKey: "Failed to find tunnel file descriptor"])) + return + } + guard let ifName = self.interfaceName else { + completionHandler(NSError(domain: "io.netbird.NetbirdNetworkExtension", code: 1004, userInfo: [NSLocalizedDescriptionKey: "Failed to get interface name"])) + return + } let connectionListener = ConnectionListener(adapter: self, completionHandler: completionHandler) self.client.setConnectionListener(connectionListener)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let fd = self.tunnelFileDescriptor ?? 0
let ifName = self.interfaceName ?? "unknown"
let connectionListener = ConnectionListener(adapter: self, completionHandler: completionHandler)
self.client.setConnectionListener(connectionListener)
let envList = UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName).flatMap { EnvVarPackager.getEnvironmentVariables(defaults: $0)
let envList = UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName).flatMap {
EnvVarPackager.getEnvironmentVariables(defaults: $0)
}
try self.client.run(self.tunnelFileDescriptor ?? 0, interfaceName: self.interfaceName, envList: envList)
try self.client.run(fd, interfaceName: ifName, envList: envList)
guard let fd = self.tunnelFileDescriptor else {
completionHandler(NSError(domain: "io.netbird.NetbirdNetworkExtension", code: 1003, userInfo: [NSLocalizedDescriptionKey: "Failed to find tunnel file descriptor"]))
return
}
guard let ifName = self.interfaceName else {
completionHandler(NSError(domain: "io.netbird.NetbirdNetworkExtension", code: 1004, userInfo: [NSLocalizedDescriptionKey: "Failed to get interface name"]))
return
}
let connectionListener = ConnectionListener(adapter: self, completionHandler: completionHandler)
self.client.setConnectionListener(connectionListener)
let envList = UserDefaults(suiteName: GlobalConstants.userPreferencesSuiteName).flatMap {
EnvVarPackager.getEnvironmentVariables(defaults: $0)
}
try self.client.run(fd, interfaceName: ifName, envList: envList)
🤖 Prompt for AI Agents
In NetbirdNetworkExtension/NetBirdAdapter.swift around lines 286–296, do not silently fall back to fd = 0 when tunnelFileDescriptor is nil (which can pass stdin to the SDK and cause undefined behavior); instead, detect a missing tunnelFileDescriptor and propagate an error (either throw a descriptive error or call the completionHandler with a failure) before calling client.run. Replace the current defaulting behavior with a guard/if that fails fast and returns/throws a clear error; keep interfaceName handling as-is but ensure client.run is only invoked with a valid non-zero file descriptor. 
Comment on lines +108 to +153
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
guard let completionHandler = completionHandler,
let string = String(data: messageData, encoding: .utf8) else {
return
}

// Use privacy: .public to see the actual message in Console.app
logger.info("handleAppMessage: Received message '\(string, privacy: .public)'")

switch string {
case "InitializeConfig":
// Initialize config with default management URL (tvOS only)
// This must happen in the extension because it has permission to write to App Group
initializeConfig(completionHandler: completionHandler)
case "Login":
// Legacy login (PKCE flow)
login(completionHandler: completionHandler)
case "LoginTV":
// tvOS login with device code flow
logger.info("handleAppMessage: Processing LoginTV - calling loginTV()")
loginTV(completionHandler: completionHandler)
case "IsLoginComplete":
// Check if login has completed (for tvOS polling)
checkLoginComplete(completionHandler: completionHandler)
case "Status":
getStatus(completionHandler: completionHandler)
case "GetRoutes":
getSelectRoutes(completionHandler: completionHandler)
case let s where s.hasPrefix("Select-"):
let id = String(s.dropFirst("Select-".count))
selectRoute(id: id)
case let s where s.hasPrefix("Deselect-"):
let id = String(s.dropFirst("Deselect-".count))
deselectRoute(id: id)
case let s where s.hasPrefix("SetConfig:"):
// On tvOS, receive config JSON from main app via IPC
// This bypasses the broken shared UserDefaults
let configJSON = String(s.dropFirst("SetConfig:".count))
setConfigFromMainApp(configJSON: configJSON, completionHandler: completionHandler)
case "ClearConfig":
// Clear the extension-local config on logout
clearLocalConfig(completionHandler: completionHandler)
default:
logger.warning("handleAppMessage: Unknown message: \(string)")
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing completion handler call for some message types.

Several message handlers (Select-*, Deselect-*, and default) don't call the completion handler, which could leave the calling app waiting indefinitely. The IPC contract should be consistent.

 case let s where s.hasPrefix("Select-"): let id = String(s.dropFirst("Select-".count)) selectRoute(id: id) + completionHandler?(nil) case let s where s.hasPrefix("Deselect-"): let id = String(s.dropFirst("Deselect-".count)) deselectRoute(id: id) + completionHandler?(nil) default: logger.warning("handleAppMessage: Unknown message: \(string)") + completionHandler?(nil) }
🤖 Prompt for AI Agents
In NetBirdTVNetworkExtension/PacketTunnelProvider.swift around lines 108 to 153, some message cases (the Select- and Deselect- branches and the default branch) never invoke the provided completionHandler which can leave the caller hanging; ensure every switch branch that returns early or handles a message calls completionHandler with a response (e.g., completionHandler(nil) or a small acknowledgement Data payload) after performing the action, so add a completionHandler(nil) (or an appropriate Data) immediately after selectRoute(id:), deselectRoute(id:), and in the default case (and keep the existing logging), ensuring the completionHandler is always called exactly once for each incoming message. 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants