Skip to content

Notify user when VPN disconnects due to re-authentication#106

Open
evgeniyChepelev wants to merge 6 commits into
netbirdio:mainfrom
evgeniyChepelev:feature/vpn-auth-expiry-notification
Open

Notify user when VPN disconnects due to re-authentication#106
evgeniyChepelev wants to merge 6 commits into
netbirdio:mainfrom
evgeniyChepelev:feature/vpn-auth-expiry-notification

Conversation

@evgeniyChepelev
Copy link
Copy Markdown
Collaborator

@evgeniyChepelev evgeniyChepelev commented Apr 27, 2026

Summary

UNUserNotificationCenter inside NEPacketTunnelProvider always reports
.notDetermined (checks the extension bundle, not the app) — notifications
scheduled from the extension were silently dropped.

Dual-path delivery:

  • Extension schedules a fallback notification with a 3 s delay
  • Main app (NEVPNStatusDidChange observer) cancels the extension's pending
    request and delivers its own immediately when the app is backgrounded
  • If the app is force-quit, the 3 s delay expires and the extension's copy fires

Also fixed:

  • Mid-session token expiry (token expires while VPN is running) now triggers the
    notification — previously only the startup check was handled
  • Tapping the notification banner opens the auth screen directly
  • Banner is shown even when the app is in the foreground
  • No duplicate notifications

How to test

Physical device required. Notification permission must be granted.

  1. Connect to VPN, background the app, expire the auth token server-side
    "VPN Disconnected" banner appears, no duplicates
  2. Tap the banner → auth screen opens immediately
  3. Repeat with app in foreground → banner still appears, auth screen opens automatically

Summary by CodeRabbit

  • New Features

    • Added iOS push notifications to alert users when re-authentication is required, including when their session expires or authentication token becomes invalid.
  • Bug Fixes

    • Improved VPN connection status monitoring and session expiry detection for reliable notification delivery.
    • Enhanced notification handling to prevent duplicates and ensure alerts display correctly when the app is backgrounded.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@evgeniyChepelev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 58 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fc725c06-2f98-4223-9263-6744bee93067

📥 Commits

Reviewing files that changed from the base of the PR and between 4589291 and cfc0859.

📒 Files selected for processing (5)
  • NetBird.xcodeproj/project.pbxproj
  • NetBird/Source/App/ViewModels/MainViewModel.swift
  • NetbirdKit/GlobalConstants.swift
  • NetbirdNetworkExtension/NetBirdAdapter.swift
  • NetbirdNetworkExtension/PacketTunnelProvider.swift
📝 Walkthrough

Walkthrough

This PR implements iOS local notifications for login-required scenarios. It adds notification center delegate support, permission requests, VPN status observation, and notification scheduling from the main app when a session expires. Extension login-required signals trigger notifications that, when tapped, prompt re-authentication.

Changes

Cohort / File(s) Summary
Project Configuration
NetBird.xcodeproj/project.pbxproj
Replaces GoogleService-Info.plist file reference ID, reorders widget-related framework/extension entries, and relocates NetBirdTests target build-phase definitions without changing build settings.
App Notification Infrastructure
NetBird/Source/App/NetBirdApp.swift, NetbirdKit/GlobalConstants.swift
Adds iOS notification center delegate conformance to AppDelegate, requests notification permissions on launch, handles notification taps to trigger login flow via custom notification event, and introduces notificationLoginRequired constant identifier.
Login-Required State Management
NetBird/Source/App/ViewModels/MainViewModel.swift, NetbirdKit/ConnectionListener.swift
Shifts login-required flow from dedicated check to VPN status observation, conditionally schedules notifications when backgrounded, clears flag to prevent duplicates, and detects session-expiry conditions in adapter disconnection to trigger login-required callback.
Network Extension Login Signaling
NetbirdNetworkExtension/NetBirdAdapter.swift, NetbirdNetworkExtension/PacketTunnelProvider.swift
Adds onLoginRequired closure property to adapter, signals login-required at tunnel startup and dynamically during active sessions, and updates notification scheduling to use unconditional best-effort delivery with localized strings.

Sequence Diagram

sequenceDiagram
    participant App as NetBirdApp
    participant NotifCenter as NotificationCenter
    participant MainVM as MainViewModel
    participant VPN as VPN Adapter
    participant Ext as Extension/<br/>PacketTunnelProvider
    participant SysNotif as System<br/>Notifications

    Ext->>Ext: Token expires<br/>needsLogin() = true
    Ext->>Ext: onLoginRequired() callback
    Ext->>Ext: signalLoginRequired()<br/>sets shared flag
    
    VPN->>VPN: VPN status changes
    VPN->>MainVM: .NEVPNStatusDidChange
    
    MainVM->>MainVM: Check keyLoginRequired flag
    MainVM->>MainVM: Verify app backgrounded
    
    alt Permission Authorized
        MainVM->>SysNotif: Schedule local notification<br/>(3-sec timer)
        SysNotif->>App: Deliver notification
        App->>App: userNotificationCenter(<br/>didReceive response)
        App->>NotifCenter: Post netbirdLoginNotificationTapped
        NotifCenter->>MainVM: Observe event
        MainVM->>MainVM: Set showAuthenticationRequired = true
        MainVM->>App: Navigate to Auth Flow
    else Permission Denied
        MainVM->>MainVM: Log, skip scheduling
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Suggested Reviewers

  • mlsmaycon
  • pappz
  • pascal-fischer

Poem

🐰 Hops through the notification queue,
A login dance, both old and new—
When tokens fade and sessions end,
A banner tap brings auth back 'round the bend! 🔔

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main change: notifying users when VPN disconnects due to re-authentication, which is the primary objective of the PR.
Description check ✅ Passed The description is comprehensive, covering the problem, the dual-path delivery solution, additional fixes, and testing instructions, which substantially exceeds the minimal template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

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.

🧹 Nitpick comments (2)
NetbirdNetworkExtension/PacketTunnelProvider.swift (1)

286-313: Optional: extract the 3-second fallback delay into a named constant.

The 3s delay is the cornerstone of the dual-path delivery contract (main app must outrun this timer). Hard-coding it here makes the coupling implicit; if it is ever tweaked, it must be remembered as a global timing assumption for the entire flow. Consider lifting it to GlobalConstants (e.g., notificationFallbackDelay) so the contract is documented in one place.

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

In `@NetbirdNetworkExtension/PacketTunnelProvider.swift` around lines 286 - 313,
Extract the hard-coded 3s timeout in sendLoginNotificationBestEffort into a
named constant on GlobalConstants (e.g., notificationFallbackDelay) and use that
constant when constructing the UNTimeIntervalNotificationTrigger; keep
GlobalConstants.notificationLoginRequired as-is for the identifier so the
dual-path contract is documented centrally and the timing can be updated in one
place without hunting for magic numbers.
NetBird/Source/App/ViewModels/MainViewModel.swift (1)

135-179: Add a deinit that removes the NEVPNStatusDidChange observer.

NotificationCenter.default.addObserver(forName:object:queue:using:) returns a token whose closure is retained by NotificationCenter until the token is explicitly removed. Although [weak self] prevents a strong reference to ViewModel, the observer registration itself remains alive for the process lifetime, and if the view model is ever recreated (e.g. future refactor of ViewModelLoader or for tests/SwiftUI previews) you'd silently accumulate stale observers all firing on a dead self.

♻️ Proposed cleanup
+    `#if` os(iOS)
+    deinit {
+        if let observer = vpnStatusObserver {
+            NotificationCenter.default.removeObserver(observer)
+        }
+    }
+    `#endif`
+
     init() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@NetBird/Source/App/ViewModels/MainViewModel.swift` around lines 135 - 179,
The NEVPNStatusDidChange observer registered via
NotificationCenter.default.addObserver in init stores a token in
vpnStatusObserver but is never removed; add a deinit that checks
vpnStatusObserver and calls NotificationCenter.default.removeObserver(_:) to
remove that token (or call NotificationCenter.default.removeObserver(self) if
you prefer) to prevent accumulating stale observers; reference
vpnStatusObserver, init, deinit,
NotificationCenter.default.addObserver(forName:object:queue:using:) and
handleVPNStatusChangeForNotification() when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@NetBird/Source/App/ViewModels/MainViewModel.swift`:
- Around line 135-179: The NEVPNStatusDidChange observer registered via
NotificationCenter.default.addObserver in init stores a token in
vpnStatusObserver but is never removed; add a deinit that checks
vpnStatusObserver and calls NotificationCenter.default.removeObserver(_:) to
remove that token (or call NotificationCenter.default.removeObserver(self) if
you prefer) to prevent accumulating stale observers; reference
vpnStatusObserver, init, deinit,
NotificationCenter.default.addObserver(forName:object:queue:using:) and
handleVPNStatusChangeForNotification() when making the change.

In `@NetbirdNetworkExtension/PacketTunnelProvider.swift`:
- Around line 286-313: Extract the hard-coded 3s timeout in
sendLoginNotificationBestEffort into a named constant on GlobalConstants (e.g.,
notificationFallbackDelay) and use that constant when constructing the
UNTimeIntervalNotificationTrigger; keep
GlobalConstants.notificationLoginRequired as-is for the identifier so the
dual-path contract is documented centrally and the timing can be updated in one
place without hunting for magic numbers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2403e005-65c0-47a7-b9ad-cd78d96aa20f

📥 Commits

Reviewing files that changed from the base of the PR and between ab2a0d8 and 4589291.

📒 Files selected for processing (7)
  • NetBird.xcodeproj/project.pbxproj
  • NetBird/Source/App/NetBirdApp.swift
  • NetBird/Source/App/ViewModels/MainViewModel.swift
  • NetbirdKit/ConnectionListener.swift
  • NetbirdKit/GlobalConstants.swift
  • NetbirdNetworkExtension/NetBirdAdapter.swift
  • NetbirdNetworkExtension/PacketTunnelProvider.swift

@pappz
Copy link
Copy Markdown
Contributor

pappz commented Apr 28, 2026

/testflight

@github-actions
Copy link
Copy Markdown

TestFlight builds uploaded 0.1.6 (195) for cfc0859 — iOS + tvOS

View workflow run

@chepeleveugeniy-dev
Copy link
Copy Markdown

/testflight

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.

3 participants