diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 358d5c0d..0d4d1b8e 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -22,7 +22,7 @@ - + @@ -494,7 +494,7 @@ - + @@ -531,13 +531,13 @@ - + - + @@ -563,28 +563,28 @@ - + - + - - + + - + - + @@ -608,29 +608,29 @@ - + - + - - + + - + - - + + @@ -653,29 +653,29 @@ - + - + - - + + - + - - + + @@ -698,19 +698,19 @@ - + - + - + @@ -739,19 +739,19 @@ - + - + - + @@ -774,19 +774,19 @@ - + - + - + @@ -815,19 +815,19 @@ - + - + - + @@ -849,19 +849,19 @@ - + - + - + @@ -886,19 +886,19 @@ - + - + - + @@ -920,19 +920,19 @@ - + - + - + @@ -954,19 +954,19 @@ - + - + - + @@ -987,20 +987,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + @@ -1023,19 +1057,19 @@ - + - + - + @@ -1062,7 +1096,7 @@ - + @@ -1090,7 +1124,7 @@ - + @@ -1118,7 +1152,7 @@ - + @@ -1146,7 +1180,7 @@ - + @@ -1178,7 +1212,7 @@ - + @@ -1213,7 +1247,7 @@ - + @@ -1248,7 +1282,7 @@ - + @@ -1283,7 +1317,7 @@ - + @@ -1311,7 +1345,7 @@ - + @@ -1339,7 +1373,7 @@ - + @@ -1373,7 +1407,7 @@ - + @@ -1408,7 +1442,7 @@ - + @@ -1443,7 +1477,7 @@ - + diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index fb5913d6..cf1e1efc 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -77,6 +77,7 @@ extension SettingsViewController case refreshSideJITServer case resetPairingFile case anisetteServers + case vpnConfiguration case enableEMPForWiregaurd case customizeAppId } @@ -1364,9 +1365,19 @@ extension SettingsViewController handleRefreshResult(result) }) - let anisetteServersController = UIHostingController(rootView: anisetteServersView) + let vc = UIHostingController(rootView: anisetteServersView) + self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: vc), sender: nil) - self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: anisetteServersController), sender: nil) + case .vpnConfiguration: + let vpnConfigurationView = VPNConfigurationView() + let vc = UIHostingController(rootView: vpnConfigurationView) + + let appearance = UINavigationBarAppearance() + appearance.configureWithDefaultBackground() // gives solid background + vc.navigationItem.scrollEdgeAppearance = appearance + vc.navigationItem.standardAppearance = appearance + + navigationController?.pushViewController(vc, animated: true) case .refreshAttempts, .enableEMPForWiregaurd, .customizeAppId: break } case .signing: diff --git a/AltStore/Settings/VPNConfigurationView.swift b/AltStore/Settings/VPNConfigurationView.swift new file mode 100644 index 00000000..20d423d5 --- /dev/null +++ b/AltStore/Settings/VPNConfigurationView.swift @@ -0,0 +1,135 @@ +// +// VPNConfiguration.swift +// AltStore +// +// Created by Magesh K on 02/03/26. +// Copyright © 2026 SideStore. All rights reserved. +// + +import SwiftUI +import Combine + +private typealias SButton = SwiftUI.Button + +struct VPNConfigurationView: View { + @Environment(\.presentationMode) var presentationMode + @StateObject private var config = TunnelConfig.shared + + @State private var showNetworkWarning = false + + var body: some View { + List { + Section(header: Text("Discovered from network")) { + Group { + networkConfigRow(label: "Tunnel IP", text: $config.deviceIP, editable: false) + networkConfigRow(label: "Device IP", text: $config.fakeIP, editable: false) + networkConfigRow(label: "Subnet Mask", text: $config.subnetMask, editable: false) + } + } + + Section { + networkConfigRow( + label: "Device IP", + text: Binding( + get: { config.overrideFakeIP }, + set: { config.overrideFakeIP = $0 ?? "" } + ), + editable: true + ) + networkConfigRow(label: "Active", text: .constant(config.overrideActive), editable: false) + } header: { + Text("Override Configuration") + } footer: { + HStack(alignment: .top, spacing: 0) { + Text("Note: ") + Text("if the override configuration is invalid or unusable SideStore may use auto-discovered configuration as fallback") + } + } + } + .alert(isPresented: $showNetworkWarning) { + Alert( + title: Text("Warning"), + message: Text("Changing tunnel IP settings can disrupt your network connection. Proceed only if you are sure of what you are doing."), + dismissButton: .cancel(Text("I Understand")) { + config.shownTunnelAlert = true + } + ) + } + .navigationTitle("VPN Configuration") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + SButton("Confirm") { + commitChanges() + } + } + } + } + + private func commitChanges() { + TunnelConfig.shared.commitFakeIP() + bindTunnelConfig() + } + + private func dismiss() { + presentationMode.wrappedValue.dismiss() + } + + private func networkConfigRow( + label: LocalizedStringKey, + text: Binding, + editable: Bool + ) -> some View { + + let proxy = Binding( + get: { text.wrappedValue ?? "N/A" }, + set: { text.wrappedValue = $0.isEmpty || $0 == "N/A" ? nil : $0 } + ) + + return HStack { + Text(label) + .foregroundColor(editable ? .primary : .gray) + Spacer() + TextField(label, text: proxy) + .multilineTextAlignment(.trailing) + .foregroundColor(editable ? .secondary : .gray) + .disabled(!editable) + .keyboardType(.numbersAndPunctuation) + .onChange(of: proxy.wrappedValue) { newValue in + guard editable else { return } + + if !config.shownTunnelAlert { + showNetworkWarning = true + } + + proxy.wrappedValue = + newValue.filter { "0123456789.".contains($0) } + } + } + } +} + + +final class TunnelConfig: ObservableObject { + + static let shared = TunnelConfig() + + @Published var deviceIP: String? + @Published var subnetMask: String? + @Published var fakeIP: String? + @Published var overrideFakeIP: String = UserDefaults.standard.string(forKey: "TunnelOverrideFakeIP") ?? "10.7.0.1" { + didSet { UserDefaults.standard.set(overrideFakeIP, forKey: "TunnelOverrideFakeIP") } + } + @Published var overrideEffective: Bool = false + + var overrideActive: String { + ((fakeIP?.isEmpty == false) && !overrideFakeIP.isEmpty && fakeIP == overrideFakeIP) ? "Yes" : "No" + } + + @Published var shownTunnelAlert: Bool = UserDefaults.standard.bool(forKey: "shownTunnelAlert") { + didSet { UserDefaults.standard.set(shownTunnelAlert, forKey: "shownTunnelAlert") } + } + + func commitFakeIP() { + fakeIP = overrideFakeIP + } +}