Skip to content

iOS home screen widget#78

Open
evgeniyChepelev wants to merge 9 commits intonetbirdio:mainfrom
evgeniyChepelev:ios-home-screen-widget
Open

iOS home screen widget#78
evgeniyChepelev wants to merge 9 commits intonetbirdio:mainfrom
evgeniyChepelev:ios-home-screen-widget

Conversation

@evgeniyChepelev
Copy link
Copy Markdown
Collaborator

@evgeniyChepelev evgeniyChepelev commented Mar 30, 2026

Summary by CodeRabbit

  • New Features

    • Added iOS Home Screen widgets (small & medium) showing VPN status, IP, FQDN, with in-widget actions, App Intents (connect/disconnect/toggle/status) and App Shortcuts.
  • Chores

    • Bumped app version to 0.1.4; added widget extension assets, Info.plist, entitlements and app-group support; added widget constants and shared persistence keys.
  • Bug Fixes

    • Improved lifecycle/activation handling, deferred deep-link processing until ready, and made Safari login presentation eligible regardless of status.

- Add NetBirdWidgetExtension target with small/medium widget sizes
- Support direct connect/disconnect from widget via interactive buttons (iOS 17+)
- Detect missing VPN config or login-required state and open app via deep link
- Poll for stable VPN state after toggle to prevent loader getting stuck
- Add widget shared state keys to GlobalConstants and sync status from MainViewModel
- Fix false "authentication required" alert on app resume after widget disconnect
- Deduplicate app activation logic into shared startActivation/stopActivation
- Extract polling helpers: updateDetailsIfChanged, updatePeersIfChanged, applyExtensionStatus
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 917e6adc-db18-41b5-9208-f8bb1bb82feb

📥 Commits

Reviewing files that changed from the base of the PR and between 78b15a0 and 9fdd8be.

📒 Files selected for processing (6)
  • NetBird.xcodeproj/project.pbxproj
  • NetBirdWidgetExtension/Intents/ConnectVPNIntent.swift
  • NetBirdWidgetExtension/Intents/VPNIntentHelpers.swift
  • NetBirdWidgetExtension/Views/MediumWidgetView.swift
  • NetBirdWidgetExtension/Views/SmallWidgetView.swift
  • NetBirdWidgetExtension/WidgetVPNStatus.swift
✅ Files skipped from review due to trivial changes (1)
  • NetBirdWidgetExtension/Views/MediumWidgetView.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • NetBirdWidgetExtension/Views/SmallWidgetView.swift
  • NetBirdWidgetExtension/Intents/ConnectVPNIntent.swift
  • NetBirdWidgetExtension/WidgetVPNStatus.swift

📝 Walkthrough

Walkthrough

Adds a WidgetKit extension (views, timeline provider, intents, helpers, assets, entitlements), wires it into the Xcode project, persists widget state via app-group UserDefaults, adds deep-link handling, and updates app lifecycle/ViewModel activation and build versioning.

Changes

Cohort / File(s) Summary
Xcode project & build settings
NetBird.xcodeproj/project.pbxproj
Added new NetBirdWidgetExtensionExtension target, build phases, framework refs, embedded extension in main target, and bumped CURRENT_PROJECT_VERSION/MARKETING_VERSION; added build configurations for the widget.
App Info & deep links
NetBird/Info.plist
Registered netbird URL scheme via CFBundleURLTypes.
App lifecycle & URL handling
NetBird/Source/App/NetBirdApp.swift
Added deferred .onOpenURL handling, consolidated start/stop activation helpers, and unified app-active gating.
Main ViewModel & widget sync
NetBird/Source/App/ViewModels/MainViewModel.swift
Added iOS-only updateWidgetState() using app-group UserDefaults, refactored polling/update helpers, replaced extension status handling, and moved login-required notification scheduling.
App view behavior
NetBird/Source/App/Views/iOS/iOSConnectionView.swift
Made Safari login presentation eligible regardless of statusDetailsValid.
Shared constants
NetbirdKit/GlobalConstants.swift
Added widget-related keys: keyWidgetVPNStatus, keyWidgetIP, keyWidgetFQDN.
Widget target files & entitlements
NetBirdWidgetExtension/Info.plist, .../NetBirdWidgetExtension.entitlements, .../NetBirdWidgetExtensionBundle.swift, .../NetBirdWidget.swift, .../WidgetConstants.swift
Added widget extension Info.plist, entitlements (app group), bundle entry point, widget definition, and central WidgetConstants (deep links, keys, timing).
Widget timeline & model
NetBirdWidgetExtension/VPNStatusProvider.swift, NetBirdWidgetExtension/WidgetVPNStatus.swift
Added TimelineProvider that reads app-group UserDefaults and NETunnelProviderManager; added WidgetVPNStatus enum and VPNStatusEntry model.
Widget UI components
NetBirdWidgetExtension/Views/SmallWidgetView.swift, .../MediumWidgetView.swift, .../StatusIndicator.swift, .../WidgetActionButton.swift
Added Small/Medium widget views, status indicator view, and iOS17+ WidgetActionButton with deep-link fallback and transition handling.
Widget intents & helpers
NetBirdWidgetExtension/Intents/ToggleVPNIntent.swift, ConnectVPNIntent.swift, DisconnectVPNIntent.swift, VPNStatusIntent.swift, VPNIntentHelpers.swift, NetBirdShortcuts.swift
Added AppIntents for connect/disconnect/status/toggle, VPNIntentHelpers (manager loading, polling stable state, app-group defaults), and a Shortcuts provider.
Widget assets
NetBirdWidgetExtension/Assets.xcassets/...
Added app icon, accent color, widget background, and logo assets (including dark variants).

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Widget as Widget UI
    participant Intent as AppIntent
    participant Defaults as App-Group UserDefaults
    participant Manager as NETunnelProviderManager
    participant Timeline as WidgetCenter

    User->>Widget: Tap action button
    Widget->>Intent: Invoke Connect/Disconnect/Toggle Intent
    Intent->>Defaults: Read persisted vpn/ip/fqdn/login flag
    Intent->>Manager: VPNIntentHelpers.loadManager()
    alt Manager exists
        Intent->>Manager: startVPNTunnel()/stopVPNTunnel()
        Intent->>Intent: waitForStableState(manager)
        Intent->>Defaults: Write interim/final widget vpn status
        Intent->>Timeline: reloadAllTimelines()
    else No manager
        Intent->>Timeline: reloadAllTimelines()
    end
    Timeline->>Widget: Widget updates with new entry
Loading
sequenceDiagram
    participant OS as iOS
    participant App as NetBird App
    participant VM as MainViewModel
    participant Defaults as App-Group UserDefaults
    participant WidgetCenter as WidgetCenter

    OS->>App: Open URL netbird://connect or netbird://disconnect
    App->>VM: handleWidgetURL(...)
    alt Should connect
        VM->>VM: connect()
    else Should disconnect
        VM->>VM: close()
    end
    VM->>VM: updateVPNDisplayState()
    VM->>Defaults: persist status, ip, fqdn
    VM->>WidgetCenter: WidgetCenter.reloadAllTimelines()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • doromaraujo
  • mlsmaycon

Poem

🐇 I nudge the meadow, quiet and bright,
Widgets bloom at morning light,
Tiny toggles, timelines race,
Shared burrows carry state and place,
Tap to connect — the rabbit grins with delight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'iOS home screen widget' is clear, concise, and directly reflects the primary change—adding widget support for iOS. It accurately summarizes the main feature introduced across the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@evgeniyChepelev evgeniyChepelev changed the title Ios home screen widget iOS home screen widget Mar 30, 2026
Copy link
Copy Markdown

@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: 9

🧹 Nitpick comments (3)
NetBirdWidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json (1)

2-6: Consider setting explicit AccentColor components

Like the background colorset, this is currently a placeholder entry only. Defining actual color components will make accent usage deterministic across widget states/themes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NetBirdWidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json`
around lines 2 - 6, The AccentColor colorset currently only contains a
placeholder "colors" array with an "idiom": "universal" entry; update the
AccentColor Contents.json to include explicit color component definitions (e.g.,
color entries with "color" objects specifying "components" for
"red","green","blue","alpha" and optional "appearances" for light/dark) so the
AccentColor is deterministic across widget states/themes—locate the
AccentColor.colorset Contents.json and replace the placeholder object under
"colors" with properly formed color dictionaries specifying the components for
the accent (and add separate appearance entries if needed).
NetBirdWidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json (1)

2-6: Consider defining an explicit WidgetBackground color value

This colorset currently has no concrete color components. If Line 2–6 is intentionally a placeholder, fine; otherwise add explicit light/dark values to avoid accidental default/empty rendering in widget backgrounds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@NetBirdWidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json`
around lines 2 - 6, The WidgetBackground colorset currently has an empty colors
array (only "idiom":"universal"); replace that placeholder with explicit color
entries in Contents.json: add a "color" object with "color-space":"srgb" and
concrete "components" (red/green/blue/alpha) for the default/light appearance
and, if you need a dark-mode variant, add a separate entry with an
"appearances":[{"appearance":"luminosity","value":"dark"}] and its own "color"
components; target the existing WidgetBackground colorset and update the
"colors" array (and optionally include a "name" if desired) so the widget
background will render deterministically in light and dark modes.
NetBirdWidgetExtension/WidgetConstants.swift (1)

4-11: Keep the shared suite/key strings in one place.

appGroupSuite, keyVPNStatus, keyIP, keyFQDN, and keyLoginRequired now duplicate values that already exist in NetbirdKit/GlobalConstants.swift. Since the app and widget communicate via raw strings, drift here will break state sharing silently. Prefer reusing the shared constants in the widget target, or add a small regression test that asserts the values stay aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NetBirdWidgetExtension/WidgetConstants.swift` around lines 4 - 11,
WidgetConstants.swift duplicates shared string constants (appGroupSuite,
keyVPNStatus, keyIP, keyFQDN, keyLoginRequired) that are defined in
NetbirdKit.GlobalConstants; update the widget to reuse the single source of
truth by importing/accessing NetbirdKit.GlobalConstants (or move the shared
values into a new shared module) and remove the duplicated literals from
WidgetConstants, and optionally add a small regression test that asserts
WidgetConstants' values match NetbirdKit.GlobalConstants to prevent future
drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NetBird/Source/App/NetBirdApp.swift`:
- Around line 56-63: The .onOpenURL handler is currently attached only to
MainView inside the if let viewModel = viewModelLoader.viewModel block (and
similarly missing on loadingView), so cold-launch URLs can be missed; move the
.onOpenURL modifier to the scene root (e.g., on the WindowGroup/Scene where
MainView and loadingView are returned) so it is always installed, and have that
handler call handleWidgetURL(_:, viewModel:) when viewModel is available or
queue the URL for later processing if viewModelLoader.viewModel is nil; update
any references to MainView, loadingView, viewModelLoader.viewModel, and
handleWidgetURL accordingly.
- Around line 97-113: The startActivation Task can resume after suspension even
if cancelled; update startActivation to check Task.isCancelled after the await
to bail out early and avoid running side effects: after awaiting
networkExtensionAdapter.loadCurrentConnectionState() verify Task.isCancelled and
return if cancelled, and also check Task.isCancelled again before calling
viewModel.checkExtensionState(), viewModel.checkLoginRequiredFlag(), and
viewModel.startPollingDetails(); keep using activationTask?.cancel() as-is but
add these cooperative cancellation checks referencing startActivation,
activationTask, ViewModel.networkExtensionAdapter.loadCurrentConnectionState(),
viewModel.checkExtensionState(), viewModel.checkLoginRequiredFlag(), and
viewModel.startPollingDetails().

In `@NetBird/Source/App/ViewModels/MainViewModel.swift`:
- Around line 431-449: The method updateDetailsIfChanged currently ignores
updates when details.ip or details.fqdn are empty and doesn't propagate
IP/FQDN-only changes to app-group defaults or the widget; modify
updateDetailsIfChanged to treat an empty string as a valid change (compare
details.ip != ip and details.fqdn != fqdn without requiring non-empty), always
write the new values to UserDefaults (defaults.set(...)) and assign ip/fqdn when
they differ, and ensure updateVPNDisplayState() is called whenever any of ip,
fqdn, or managementStatus changes (not only on managementStatus) so widget sync
happens; keep the existing managementStatus handling (managementStatus !=
managementStatus) and preserve ordering of assignments.
- Line 18: The unconditional import WidgetKit in MainViewModel.swift causes tvOS
build failures; wrap the import in a platform check (e.g., `#if` os(iOS) or `#if`
canImport(WidgetKit)) so WidgetKit is only imported for supported platforms, and
ensure any WidgetKit-dependent code in MainViewModel is similarly guarded
(reference the import line and any WidgetKit-using symbols in MainViewModel).
- Around line 765-774: After clearing GlobalConstants.keyLoginRequired in the
shared UserDefaults (the block that calls userDefaults?.set(false, forKey:
GlobalConstants.keyLoginRequired) and userDefaults?.synchronize()), trigger a
widget timeline refresh so VPNStatusProvider (which reads needsAppSetup from
that key) updates immediately; add a call to reload widget timelines (e.g., via
WidgetCenter.shared.reloadAllTimelines()) right after synchronizing, keeping the
existing AppLogger.log, showAuthenticationRequired, and
scheduleLoginRequiredNotification() behavior intact.

In `@NetBirdWidgetExtension/NetBirdWidget.swift`:
- Around line 26-29: The .background() call on WidgetEntryView is invalid
(zero-argument) and causes a compile error; in NetBirdWidget.swift locate the
WidgetEntryView(entry: entry) chain and either remove the .background() call for
the iOS 15/16 branch or replace it with an explicit style (e.g., a Color or a
suitable background modifier) to match the iOS 17 branch; update the code around
WidgetEntryView(entry: entry).padding() so it no longer invokes background()
with no parameters.

In `@NetBirdWidgetExtension/NetBirdWidgetExtension.entitlements`:
- Around line 9-12: Remove the Network Extension entitlement from the WidgetKit
extension by deleting the com.apple.developer.networking.networkextension key
and its packet-tunnel-provider value from this entitlements file; instead place
the packet-tunnel-provider entitlement in a containing app target or a dedicated
Network Extension (packet tunnel provider) target, and ensure the WidgetKit
extension uses the com.apple.widget-extension extension point without network
extension entitlements to avoid signing/App Store validation failures.

In `@NetBirdWidgetExtension/Views/SmallWidgetView.swift`:
- Around line 16-23: SmallWidgetView currently only shows WidgetActionButton for
iOS 17+ leaving iOS 15/16 users without any action; add a fallback that mirrors
MediumWidgetView by providing a Link (or setting widgetURL) that triggers the
same connect/disconnect behavior for older OSes. Locate the WidgetActionButton
usage inside SmallWidgetView and replace the conditional branch so the iOS 17+
path keeps WidgetActionButton(entry: entry) { ProgressView()... } label: {
pillLabel(isConnected:) } and the else path renders a Link (or attaches
widgetURL) that wraps pillLabel(isConnected:) and points to the same URL/action
used by the WidgetActionButton so pre-iOS17 widgets remain interactive.

In `@NetBirdWidgetExtension/VPNStatusProvider.swift`:
- Around line 38-47: When loading the VPN status inside the
NETunnelProviderManager.loadAllFromPreferences closure, prefer the persisted
transition state written by ToggleVPNIntent (read from defaults using
WidgetConstants.keyVPNStatus) when it represents a transient state (.connecting
or .disconnecting); currently the code always uses manager.connection.status if
a manager exists which hides those persisted transitions. Change the logic in
the closure (the block that computes `status: WidgetVPNStatus`) to first read
the persisted raw status, convert to WidgetVPNStatus, and if that value is
.connecting or .disconnecting return it; otherwise fall back to using
`manager.connection.status`, and keep the existing fallback to reading the
persisted value or .disconnected when manager is nil.

---

Nitpick comments:
In `@NetBirdWidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json`:
- Around line 2-6: The AccentColor colorset currently only contains a
placeholder "colors" array with an "idiom": "universal" entry; update the
AccentColor Contents.json to include explicit color component definitions (e.g.,
color entries with "color" objects specifying "components" for
"red","green","blue","alpha" and optional "appearances" for light/dark) so the
AccentColor is deterministic across widget states/themes—locate the
AccentColor.colorset Contents.json and replace the placeholder object under
"colors" with properly formed color dictionaries specifying the components for
the accent (and add separate appearance entries if needed).

In
`@NetBirdWidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json`:
- Around line 2-6: The WidgetBackground colorset currently has an empty colors
array (only "idiom":"universal"); replace that placeholder with explicit color
entries in Contents.json: add a "color" object with "color-space":"srgb" and
concrete "components" (red/green/blue/alpha) for the default/light appearance
and, if you need a dark-mode variant, add a separate entry with an
"appearances":[{"appearance":"luminosity","value":"dark"}] and its own "color"
components; target the existing WidgetBackground colorset and update the
"colors" array (and optionally include a "name" if desired) so the widget
background will render deterministically in light and dark modes.

In `@NetBirdWidgetExtension/WidgetConstants.swift`:
- Around line 4-11: WidgetConstants.swift duplicates shared string constants
(appGroupSuite, keyVPNStatus, keyIP, keyFQDN, keyLoginRequired) that are defined
in NetbirdKit.GlobalConstants; update the widget to reuse the single source of
truth by importing/accessing NetbirdKit.GlobalConstants (or move the shared
values into a new shared module) and remove the duplicated literals from
WidgetConstants, and optionally add a small regression test that asserts
WidgetConstants' values match NetbirdKit.GlobalConstants to prevent future
drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4dd22e8b-16f8-41b2-bea8-b24715379f53

📥 Commits

Reviewing files that changed from the base of the PR and between b8eca1d and aa1c583.

⛔ Files ignored due to path filters (6)
  • NetBirdWidgetExtension/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu 1.png is excluded by !**/*.png
  • NetBirdWidgetExtension/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu.png is excluded by !**/*.png
  • NetBirdWidgetExtension/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@2x 1.png is excluded by !**/*.png
  • NetBirdWidgetExtension/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@2x.png is excluded by !**/*.png
  • NetBirdWidgetExtension/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@3x 1.png is excluded by !**/*.png
  • NetBirdWidgetExtension/Assets.xcassets/netbird-logo-menu.imageset/netbird-logo-menu@3x.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • NetBird.xcodeproj/project.pbxproj
  • NetBird/Info.plist
  • NetBird/Source/App/NetBirdApp.swift
  • NetBird/Source/App/ViewModels/MainViewModel.swift
  • NetBirdWidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json
  • NetBirdWidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json
  • NetBirdWidgetExtension/Assets.xcassets/Contents.json
  • NetBirdWidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json
  • NetBirdWidgetExtension/Assets.xcassets/netbird-logo-menu.imageset/Contents.json
  • NetBirdWidgetExtension/Info.plist
  • NetBirdWidgetExtension/NetBirdWidget.swift
  • NetBirdWidgetExtension/NetBirdWidgetExtension.entitlements
  • NetBirdWidgetExtension/NetBirdWidgetExtensionBundle.swift
  • NetBirdWidgetExtension/ToggleVPNIntent.swift
  • NetBirdWidgetExtension/VPNStatusProvider.swift
  • NetBirdWidgetExtension/Views/MediumWidgetView.swift
  • NetBirdWidgetExtension/Views/SmallWidgetView.swift
  • NetBirdWidgetExtension/Views/StatusIndicator.swift
  • NetBirdWidgetExtension/Views/WidgetActionButton.swift
  • NetBirdWidgetExtension/WidgetConstants.swift
  • NetBirdWidgetExtension/WidgetVPNStatus.swift
  • NetbirdKit/GlobalConstants.swift

Copy link
Copy Markdown

@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: 2

♻️ Duplicate comments (1)
NetBirdWidgetExtension/VPNStatusProvider.swift (1)

39-48: ⚠️ Potential issue | 🟠 Major

This still drops the persisted transition state on immediate reloads.

The connect/disconnect intents write .connecting / .disconnecting into WidgetConstants.keyVPNStatus before reloading timelines, but this branch still ignores that persisted value whenever a manager exists. The widget can therefore still snap back to the previous stable state right after a tap instead of showing the in-flight transition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NetBirdWidgetExtension/VPNStatusProvider.swift` around lines 39 - 48, The
branch that chooses status from the NE manager ignores a persisted transition
state; modify the NETunnelProviderManager.loadAllFromPreferences completion so
that when a manager exists you first read the persisted raw value from
WidgetConstants.keyVPNStatus (via defaults?.string) and, if it maps to
WidgetVPNStatus.connecting or .disconnecting, use that persisted WidgetVPNStatus
instead of always using WidgetVPNStatus(neStatus: manager.connection.status);
otherwise fall back to constructing WidgetVPNStatus from
manager.connection.status as currently done.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NetBirdWidgetExtension/Intents/VPNIntentHelpers.swift`:
- Around line 22-39: Change waitForStableState(manager:) to return the resolved
WidgetVPNStatus instead of void: await the manager until timeout like it
currently does, compute final = WidgetVPNStatus(neStatus:
manager.connection.status), persist it (defaults?.set(...)) and then return
final. Update callers (ConnectVPNIntent.perform() and
DisconnectVPNIntent.perform()) to await the returned WidgetVPNStatus and use it
to decide success/error dialogs instead of hard-coded success; keep
WidgetCenter.shared.reloadAllTimelines() behavior after persisting. Ensure the
function signature and all call sites are updated accordingly.

In `@NetBirdWidgetExtension/ToggleVPNIntent.swift`:
- Around line 5-17: The ToggleVPNIntent currently exposes an unconstrained
String parameter action which can be any value but only "connect" is handled;
either make ToggleVPNIntent non-discoverable to Shortcuts by setting static var
isDiscoverable = false (so users use ConnectVPNIntent/DisconnectVPNIntent
instead), or restrict/validate the action parameter inside ToggleVPNIntent
(e.g., accept an enum-like closed set and validate action in init or when
running, mapping unknown values to a safe default or returning an error) and
update related code paths that read action to only act on validated values.

---

Duplicate comments:
In `@NetBirdWidgetExtension/VPNStatusProvider.swift`:
- Around line 39-48: The branch that chooses status from the NE manager ignores
a persisted transition state; modify the
NETunnelProviderManager.loadAllFromPreferences completion so that when a manager
exists you first read the persisted raw value from WidgetConstants.keyVPNStatus
(via defaults?.string) and, if it maps to WidgetVPNStatus.connecting or
.disconnecting, use that persisted WidgetVPNStatus instead of always using
WidgetVPNStatus(neStatus: manager.connection.status); otherwise fall back to
constructing WidgetVPNStatus from manager.connection.status as currently done.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 06d8379a-436c-4b14-a1d9-e9783aa46639

📥 Commits

Reviewing files that changed from the base of the PR and between aa1c583 and 12af697.

📒 Files selected for processing (13)
  • NetBird.xcodeproj/project.pbxproj
  • NetBird/Source/App/NetBirdApp.swift
  • NetBird/Source/App/Views/iOS/iOSConnectionView.swift
  • NetBirdWidgetExtension/Intents/ConnectVPNIntent.swift
  • NetBirdWidgetExtension/Intents/DisconnectVPNIntent.swift
  • NetBirdWidgetExtension/Intents/NetBirdShortcuts.swift
  • NetBirdWidgetExtension/Intents/VPNIntentHelpers.swift
  • NetBirdWidgetExtension/Intents/VPNStatusIntent.swift
  • NetBirdWidgetExtension/ToggleVPNIntent.swift
  • NetBirdWidgetExtension/VPNStatusProvider.swift
  • NetBirdWidgetExtension/Views/WidgetActionButton.swift
  • NetBirdWidgetExtension/WidgetConstants.swift
  • NetBirdWidgetExtension/WidgetVPNStatus.swift
✅ Files skipped from review due to trivial changes (3)
  • NetBirdWidgetExtension/WidgetConstants.swift
  • NetBird.xcodeproj/project.pbxproj
  • NetBird/Source/App/NetBirdApp.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • NetBirdWidgetExtension/WidgetVPNStatus.swift

Copy link
Copy Markdown

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NetBird.xcodeproj/project.pbxproj`:
- Around line 202-207: PBXContainerItemProxy entry 55D865962F70982100A2EFF8 is
missing the remoteGlobalIDString field required by the dependency; update the
PBXContainerItemProxy object for that proxy (the one with remoteInfo =
NetBirdWidgetExtensionExtension) to include a remoteGlobalIDString value that
points to the widget extension target’s global ID (the same ID used by other
PBXContainerItemProxy entries referencing the widget extension), ensuring the
key name is exactly remoteGlobalIDString and the proxy retains containerPortal
and proxyType unchanged.

In `@NetBirdWidgetExtension/Intents/ConnectVPNIntent.swift`:
- Around line 27-33: The code sets
VPNIntentHelpers.defaults?.set(WidgetVPNStatus.connecting.rawValue, forKey:
WidgetConstants.keyVPNStatus) before calling startVPNTunnel(), but if
NETunnelProviderSession.startVPNTunnel() throws the widget stays in .connecting;
wrap the startVPNTunnel() call in a do/catch (or handle the thrown error), and
in the error path reset the shared status key
(VPNIntentHelpers.defaults?.set(..., forKey: WidgetConstants.keyVPNStatus)) back
to the non-connecting state (e.g., WidgetVPNStatus.disconnected.rawValue or the
previous value) and call WidgetCenter.shared.reloadAllTimelines() before
returning the failure; keep waitForStableState(manager:) only on success. Ensure
references: VPNIntentHelpers.defaults, WidgetConstants.keyVPNStatus,
WidgetCenter.shared.reloadAllTimelines(),
NETunnelProviderSession.startVPNTunnel(), and
VPNIntentHelpers.waitForStableState(manager:).

In `@NetBirdWidgetExtension/Intents/VPNIntentHelpers.swift`:
- Around line 23-35: waitForStableState(manager:) currently returns the first
stable status it sees which can be the same as the starting state; change it to
remember the initial state (let initial = manager.connection.status) and first
loop waiting for a state change (skip accepting a stable result until
manager.connection.status != initial), then once a different state is observed
continue polling until WidgetVPNStatus(neStatus:
manager.connection.status).isStable is true (or deadline/login handling
triggers) and then return that stable status; keep existing login check
(isLoginRequired → WidgetCenter.reloadAllTimelines and return) and existing
timeout behavior using WidgetConstants.pollInterval and
WidgetConstants.pollTimeout.
- Around line 29-40: When the isLoginRequired early-exit branch runs it reloads
timelines but doesn't update the persisted optimistic status, leaving the widget
stuck on "Connecting"; modify that branch in VPNIntentHelpers.swift so it writes
the current status into defaults before calling
WidgetCenter.shared.reloadAllTimelines() and returning—use
WidgetVPNStatus(neStatus: manager.connection.status).rawValue and
WidgetConstants.keyVPNStatus to persist the value so the stored optimistic
`.connecting` is cleared.

In `@NetBirdWidgetExtension/Views/MediumWidgetView.swift`:
- Around line 12-24: The CTA uses only entry.isConnected, causing incorrect
target/actions during transitional states; update the logic in MediumWidgetView
where WidgetActionButton (and the iOS 15/16 Link fallback) is chosen so it
branches on entry.status and entry.needsAppSetup instead of the boolean
isConnected; specifically, use entry.status values (.connecting, .disconnecting,
etc.) and entry.needsAppSetup to select the correct action/URL from
WidgetConstants (including the dedicated login/setup deep links) and keep the
existing transitionIndicator and iconLabel(isConnected:) usage but ensure they
reflect the full state when deciding which WidgetConstants.deepLink* to use.

In `@NetBirdWidgetExtension/Views/SmallWidgetView.swift`:
- Around line 23-27: The pre-iOS17 Link selection in SmallWidgetView.swift
currently only checks entry.isConnected and can route users incorrectly; update
the Link destination logic to mirror WidgetActionButton.swift by first checking
entry.needsAppSetup && !entry.isConnected and selecting entry.loginRequired ?
WidgetConstants.deepLinkLogin : WidgetConstants.deepLinkConnect, otherwise fall
back to entry.isConnected ? WidgetConstants.deepLinkDisconnect :
WidgetConstants.deepLinkConnect so setup/login routing matches for iOS 15/16
users.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3bc1e41-34fa-4594-8299-1dab27d0eeed

📥 Commits

Reviewing files that changed from the base of the PR and between 12af697 and 78b15a0.

📒 Files selected for processing (12)
  • NetBird.xcodeproj/project.pbxproj
  • NetBird/Source/App/NetBirdApp.swift
  • NetBird/Source/App/ViewModels/MainViewModel.swift
  • NetBirdWidgetExtension/Intents/ConnectVPNIntent.swift
  • NetBirdWidgetExtension/Intents/DisconnectVPNIntent.swift
  • NetBirdWidgetExtension/Intents/VPNIntentHelpers.swift
  • NetBirdWidgetExtension/NetBirdWidget.swift
  • NetBirdWidgetExtension/NetBirdWidgetExtension.entitlements
  • NetBirdWidgetExtension/VPNStatusProvider.swift
  • NetBirdWidgetExtension/Views/MediumWidgetView.swift
  • NetBirdWidgetExtension/Views/SmallWidgetView.swift
  • NetBirdWidgetExtension/WidgetConstants.swift
✅ Files skipped from review due to trivial changes (2)
  • NetBirdWidgetExtension/NetBirdWidgetExtension.entitlements
  • NetBirdWidgetExtension/WidgetConstants.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • NetBird/Source/App/NetBirdApp.swift
  • NetBirdWidgetExtension/Intents/DisconnectVPNIntent.swift
  • NetBirdWidgetExtension/NetBirdWidget.swift
  • NetBird/Source/App/ViewModels/MainViewModel.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant