2024-09-23 13:02:26 +00:00
|
|
|
//
|
|
|
|
// OnDemandView.swift
|
|
|
|
// Passepartout
|
|
|
|
//
|
|
|
|
// Created by Davide De Rosa on 2/23/24.
|
|
|
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
|
|
//
|
|
|
|
// https://github.com/passepartoutvpn
|
|
|
|
//
|
|
|
|
// This file is part of Passepartout.
|
|
|
|
//
|
|
|
|
// Passepartout is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// Passepartout is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//
|
|
|
|
|
2024-11-01 22:32:35 +00:00
|
|
|
import AppLibrary
|
2024-11-02 09:11:59 +00:00
|
|
|
import CommonUtils
|
2024-09-23 13:02:26 +00:00
|
|
|
import PassepartoutKit
|
|
|
|
import SwiftUI
|
|
|
|
|
2024-10-23 13:42:54 +00:00
|
|
|
struct OnDemandView: View, ModuleDraftEditing {
|
2024-09-23 13:02:26 +00:00
|
|
|
|
|
|
|
@EnvironmentObject
|
|
|
|
private var theme: Theme
|
|
|
|
|
2024-09-30 13:56:32 +00:00
|
|
|
@EnvironmentObject
|
|
|
|
private var iapManager: IAPManager
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
@ObservedObject
|
2024-10-23 13:42:54 +00:00
|
|
|
var editor: ProfileEditor
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-23 13:42:54 +00:00
|
|
|
let module: OnDemandModule.Builder
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-10-23 13:42:54 +00:00
|
|
|
private let wifi: Wifi
|
2024-09-23 13:02:26 +00:00
|
|
|
|
2024-09-30 13:56:32 +00:00
|
|
|
@State
|
|
|
|
private var paywallReason: PaywallReason?
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
init(
|
|
|
|
editor: ProfileEditor,
|
2024-10-10 22:24:06 +00:00
|
|
|
module: OnDemandModule.Builder,
|
2024-09-23 13:02:26 +00:00
|
|
|
observer: WifiObserver? = nil
|
|
|
|
) {
|
|
|
|
self.editor = editor
|
2024-10-23 13:42:54 +00:00
|
|
|
self.module = module
|
2024-09-23 13:02:26 +00:00
|
|
|
wifi = Wifi(observer: observer ?? CoreLocationWifiObserver())
|
|
|
|
}
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
Group {
|
|
|
|
enabledSection
|
2024-10-02 14:05:40 +00:00
|
|
|
restrictedArea
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
2024-10-23 13:42:54 +00:00
|
|
|
.moduleView(editor: editor, draft: draft.wrappedValue)
|
2024-09-30 13:56:32 +00:00
|
|
|
.modifier(PaywallModifier(reason: $paywallReason))
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension OnDemandView {
|
|
|
|
static let allPolicies: [OnDemandModule.Policy] = [
|
|
|
|
.any,
|
|
|
|
.excluding,
|
|
|
|
.including
|
|
|
|
]
|
|
|
|
|
|
|
|
var enabledSection: some View {
|
|
|
|
Section {
|
2024-10-23 13:42:54 +00:00
|
|
|
Toggle(Strings.Global.enabled, isOn: draft.isEnabled)
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-30 13:56:32 +00:00
|
|
|
@ViewBuilder
|
2024-10-02 14:05:40 +00:00
|
|
|
var restrictedArea: some View {
|
2024-09-30 13:56:32 +00:00
|
|
|
switch iapManager.paywallReason(forFeature: .onDemand) {
|
2024-10-02 14:05:40 +00:00
|
|
|
case .purchase(let appFeature):
|
2024-09-30 13:56:32 +00:00
|
|
|
Button(Strings.Modules.OnDemand.purchase) {
|
2024-10-02 14:05:40 +00:00
|
|
|
paywallReason = .purchase(appFeature)
|
2024-09-30 13:56:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
case .restricted:
|
|
|
|
EmptyView()
|
|
|
|
|
|
|
|
default:
|
2024-10-23 13:42:54 +00:00
|
|
|
if draft.wrappedValue.isEnabled {
|
2024-09-30 13:56:32 +00:00
|
|
|
policySection
|
2024-10-23 13:42:54 +00:00
|
|
|
if draft.wrappedValue.policy != .any {
|
2024-09-30 13:56:32 +00:00
|
|
|
networkSection
|
|
|
|
wifiSection
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-23 13:02:26 +00:00
|
|
|
var policySection: some View {
|
2024-10-23 13:42:54 +00:00
|
|
|
Picker(Strings.Modules.OnDemand.policy, selection: draft.policy) {
|
2024-09-23 13:02:26 +00:00
|
|
|
ForEach(Self.allPolicies, id: \.self) {
|
|
|
|
Text($0.localizedDescription)
|
|
|
|
}
|
|
|
|
}
|
2024-10-03 15:03:53 +00:00
|
|
|
.themeSection(footer: policyFooterDescription)
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var policyFooterDescription: String {
|
2024-10-23 13:42:54 +00:00
|
|
|
guard draft.wrappedValue.isEnabled else {
|
2024-09-23 13:02:26 +00:00
|
|
|
return "" // better animation than removing footer completely
|
|
|
|
}
|
|
|
|
let suffix: String
|
2024-10-23 13:42:54 +00:00
|
|
|
switch draft.wrappedValue.policy {
|
2024-09-23 13:02:26 +00:00
|
|
|
case .any:
|
|
|
|
suffix = Strings.Modules.OnDemand.Policy.Footer.any
|
|
|
|
|
|
|
|
case .including, .excluding:
|
2024-10-23 13:42:54 +00:00
|
|
|
if draft.wrappedValue.policy == .including {
|
2024-09-23 13:02:26 +00:00
|
|
|
suffix = Strings.Modules.OnDemand.Policy.Footer.including
|
|
|
|
} else {
|
|
|
|
suffix = Strings.Modules.OnDemand.Policy.Footer.excluding
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Strings.Modules.OnDemand.Policy.footer(suffix)
|
|
|
|
}
|
|
|
|
|
|
|
|
var networkSection: some View {
|
2024-10-03 21:31:44 +00:00
|
|
|
Group {
|
2024-09-23 13:02:26 +00:00
|
|
|
if Utils.hasCellularData() {
|
2024-10-23 13:42:54 +00:00
|
|
|
Toggle(Strings.Modules.OnDemand.mobile, isOn: draft.withMobileNetwork)
|
2024-09-23 13:02:26 +00:00
|
|
|
} else if Utils.hasEthernet() {
|
2024-10-23 13:42:54 +00:00
|
|
|
Toggle(Strings.Modules.OnDemand.ethernet, isOn: draft.withEthernetNetwork)
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
2024-10-03 21:31:44 +00:00
|
|
|
.themeSection(header: Strings.Global.networks)
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var wifiSection: some View {
|
|
|
|
theme.listSection(
|
|
|
|
Strings.Unlocalized.wifi,
|
|
|
|
addTitle: Strings.Modules.OnDemand.Ssid.add,
|
|
|
|
originalItems: allSSIDs,
|
|
|
|
emptyValue: {
|
|
|
|
do {
|
|
|
|
return try await wifi.currentSSID()
|
|
|
|
} catch {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
},
|
|
|
|
itemLabel: { isEditing, binding in
|
|
|
|
if isEditing {
|
|
|
|
Text(binding.wrappedValue)
|
|
|
|
} else {
|
|
|
|
HStack {
|
|
|
|
ThemeTextField("", text: binding, placeholder: Strings.Placeholders.OnDemand.ssid)
|
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
.themeManualInput()
|
|
|
|
Spacer()
|
|
|
|
Toggle("", isOn: isSSIDOn(binding.wrappedValue))
|
|
|
|
}
|
|
|
|
.labelsHidden()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension OnDemandView {
|
|
|
|
var allSSIDs: Binding<[String]> {
|
|
|
|
.init {
|
2024-10-23 13:42:54 +00:00
|
|
|
Array(draft.wrappedValue.withSSIDs.keys)
|
2024-09-23 13:02:26 +00:00
|
|
|
} set: { newValue in
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs.forEach {
|
2024-09-23 13:02:26 +00:00
|
|
|
guard newValue.contains($0.key) else {
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs.removeValue(forKey: $0.key)
|
2024-09-23 13:02:26 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newValue.forEach {
|
2024-10-23 13:42:54 +00:00
|
|
|
guard draft.wrappedValue.withSSIDs[$0] == nil else {
|
2024-09-23 13:02:26 +00:00
|
|
|
return
|
|
|
|
}
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs[$0] = false
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var onSSIDs: Binding<Set<String>> {
|
|
|
|
.init {
|
2024-10-23 13:42:54 +00:00
|
|
|
Set(draft.wrappedValue.withSSIDs.filter {
|
2024-09-23 13:02:26 +00:00
|
|
|
$0.value
|
|
|
|
}.map(\.key))
|
|
|
|
} set: { newValue in
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs.forEach {
|
2024-09-23 13:02:26 +00:00
|
|
|
guard newValue.contains($0.key) else {
|
2024-10-23 13:42:54 +00:00
|
|
|
if draft.wrappedValue.withSSIDs[$0.key] != nil {
|
|
|
|
draft.wrappedValue.withSSIDs[$0.key] = false
|
2024-09-23 13:02:26 +00:00
|
|
|
} else {
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs.removeValue(forKey: $0.key)
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newValue.forEach {
|
2024-10-23 13:42:54 +00:00
|
|
|
guard !(draft.wrappedValue.withSSIDs[$0] ?? false) else {
|
2024-09-23 13:02:26 +00:00
|
|
|
return
|
|
|
|
}
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs[$0] = true
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func isSSIDOn(_ ssid: String) -> Binding<Bool> {
|
|
|
|
.init {
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs[ssid] ?? false
|
2024-09-23 13:02:26 +00:00
|
|
|
} set: {
|
2024-10-23 13:42:54 +00:00
|
|
|
draft.wrappedValue.withSSIDs[ssid] = $0
|
2024-09-23 13:02:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension OnDemandView {
|
|
|
|
func requestSSID(_ text: Binding<String>) {
|
|
|
|
Task { @MainActor in
|
|
|
|
let ssid = try await wifi.currentSSID()
|
2024-10-23 13:42:54 +00:00
|
|
|
if !draft.wrappedValue.withSSIDs.keys.contains(ssid) {
|
2024-09-23 13:02:26 +00:00
|
|
|
text.wrappedValue = ssid
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Previews
|
|
|
|
|
|
|
|
#Preview {
|
|
|
|
var module = OnDemandModule.Builder()
|
|
|
|
module.policy = .excluding
|
|
|
|
module.withMobileNetwork = true
|
|
|
|
module.withSSIDs = [
|
|
|
|
"One": true,
|
|
|
|
"Two": false,
|
|
|
|
"Three": false
|
|
|
|
]
|
|
|
|
return module.preview {
|
|
|
|
OnDemandView(
|
|
|
|
editor: $0,
|
2024-10-10 22:24:06 +00:00
|
|
|
module: $1,
|
2024-09-23 13:02:26 +00:00
|
|
|
observer: MockWifi()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class MockWifi: WifiObserver {
|
|
|
|
func currentSSID() async throws -> String {
|
|
|
|
""
|
|
|
|
}
|
|
|
|
}
|