The bundle-ID and display-name renames weren't enough — the actual Xcode target/product name was still "Jarvis", which drives CFBundleName, Xcode's Organizer archive list, the .xcodeproj filename, and the scheme name. Renamed all the way through: - project.yml: top-level name, target key, PRODUCT_NAME, source/info paths - Jarvis/ -> Jervis/ (source folder, git-tracked as renames, no content diffs on the moved files) - .gitignore, CI workflows, README, CONTRIBUTING: Jarvis.xcodeproj / scheme Jarvis -> Jervis.xcodeproj / scheme Jervis Verified: xcodebuild -scheme Jervis succeeds, produces Jervis.app, CFBundleName/CFBundleExecutable/CFBundleDisplayName all read "Jervis". CFBundleIdentifier intentionally stays com.kisani.jarvis (per prior decision — bundle ID isn't user-facing anywhere including Organizer). Swift type names (JarvisApp, JarvisWordmark, etc.) and the Gitea repo name/URL are unchanged — pure internal source identifiers, not user or developer-facing product identity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
4.0 KiB
Swift
102 lines
4.0 KiB
Swift
// VPNProvider.swift
|
|
// Jarvis — remote-access providers. The app can't start another app's VPN tunnel
|
|
// (iOS sandboxing), so the provider only tailors hints, help text, and an
|
|
// "open the app" jump. Reachability + endpoint selection is provider-agnostic.
|
|
|
|
import UIKit
|
|
|
|
enum VPNProvider: String, CaseIterable, Codable, Identifiable {
|
|
case direct // LAN only, no VPN
|
|
case tailscale
|
|
case headscale // self-hosted control plane, Tailscale clients
|
|
case netbird
|
|
case zerotier
|
|
case netmaker
|
|
case wireguard
|
|
|
|
var id: String { rawValue }
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .direct: return "Direct (LAN)"
|
|
case .tailscale: return "Tailscale"
|
|
case .headscale: return "Headscale"
|
|
case .netbird: return "NetBird"
|
|
case .zerotier: return "ZeroTier"
|
|
case .netmaker: return "Netmaker"
|
|
case .wireguard: return "WireGuard"
|
|
}
|
|
}
|
|
|
|
/// Whether a separate remote/VPN address is meaningful for this provider.
|
|
var needsRemote: Bool { self != .direct }
|
|
|
|
/// Placeholder shown in the remote-address field.
|
|
var remoteHint: String {
|
|
switch self {
|
|
case .direct: return "—"
|
|
case .tailscale: return "jarvis.tailnet.ts.net:8080 or 100.x.y.z:8080"
|
|
case .headscale: return "100.x.y.z:8080 (MagicDNS or tailnet IP)"
|
|
case .netbird: return "100.x.y.z:8080"
|
|
case .zerotier: return "10.x.x.x:8080 (managed IP)"
|
|
case .netmaker: return "10.x.x.x:8080"
|
|
case .wireguard: return "10.0.0.x:8080 (peer IP)"
|
|
}
|
|
}
|
|
|
|
var blurb: String {
|
|
switch self {
|
|
case .direct:
|
|
return "Use the LAN address only. Best when the phone is always on the home network."
|
|
case .tailscale:
|
|
return "On your home wifi Jarvis uses the LAN address directly; away, it uses your Tailscale address. Make sure Tailscale is connected when remote."
|
|
case .headscale:
|
|
return "Self-hosted Tailscale control plane. Same client behaviour as Tailscale — keep the client connected when away."
|
|
case .netbird:
|
|
return "Open-source WireGuard mesh. Keep the NetBird app connected when off the LAN."
|
|
case .zerotier:
|
|
return "Managed network via ZeroTier. Keep the ZeroTier app joined to the network when remote."
|
|
case .netmaker:
|
|
return "Self-hosted WireGuard mesh. Keep your Netmaker client up when off the LAN."
|
|
case .wireguard:
|
|
return "Plain WireGuard tunnel. Enable the tunnel in the WireGuard app when remote."
|
|
}
|
|
}
|
|
|
|
/// Best-effort URL scheme + App Store fallback used by "Open <provider>".
|
|
/// iOS can't start the tunnel for us — this just brings the app forward.
|
|
private var urlScheme: URL? {
|
|
switch self {
|
|
case .tailscale, .headscale: return URL(string: "tailscale://")
|
|
case .zerotier: return URL(string: "zerotier://")
|
|
case .wireguard: return URL(string: "wireguard://")
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
private var appStoreURL: URL? {
|
|
switch self {
|
|
case .tailscale, .headscale: return URL(string: "https://apps.apple.com/app/id1470499037")
|
|
case .zerotier: return URL(string: "https://apps.apple.com/app/id1084101492")
|
|
case .wireguard: return URL(string: "https://apps.apple.com/app/id1441195209")
|
|
case .netbird: return URL(string: "https://apps.apple.com/app/id6443155627")
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
var canOpenApp: Bool { urlScheme != nil || appStoreURL != nil }
|
|
|
|
/// Try the app's scheme; fall back to its App Store page.
|
|
@MainActor
|
|
func openApp() {
|
|
let candidates = [urlScheme, appStoreURL].compactMap { $0 }
|
|
func attempt(_ i: Int) {
|
|
guard i < candidates.count else { return }
|
|
UIApplication.shared.open(candidates[i]) { ok in
|
|
if !ok { attempt(i + 1) }
|
|
}
|
|
}
|
|
attempt(0)
|
|
}
|
|
}
|