// // AddProviderView.swift // Passepartout // // Created by Davide De Rosa on 2/10/22. // 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 . // import PassepartoutLibrary import SwiftUI struct AddProviderView: View { @ObservedObject private var providerManager: ProviderManager @ObservedObject private var productManager: ProductManager private let bindings: AddProfileView.Bindings @StateObject private var viewModel = ViewModel() init(bindings: AddProfileView.Bindings) { providerManager = .shared productManager = .shared self.bindings = bindings } var body: some View { ZStack { ForEach(providers, id: \.navigationId, content: hiddenProviderLink) ScrollViewReader { scrollProxy in List { mainSection if !providers.isEmpty { providersSection } }.onAppear { viewModel.updateIndex(providerManager) }.onChange(of: viewModel.errorMessage) { onErrorMessage($0, scrollProxy) }.themeAnimation(on: providers) } }.toolbar { themeCloseItem(isPresented: bindings.$isPresented) }.sheet(isPresented: $viewModel.isPaywallPresented) { NavigationView { PaywallView(isPresented: $viewModel.isPaywallPresented, feature: .allProviders) }.themeGlobal() }.navigationTitle(L10n.AddProfile.Shared.title) .themeSecondaryView() } } // MARK: - private extension AddProviderView { var mainSection: some View { Section { let protos = availableVPNProtocols if !protos.isEmpty { themeTextPicker( L10n.Global.Strings.protocol, selection: $viewModel.selectedVPNProtocol, values: protos, description: \.description ) } updateListButton } footer: { Text(L10n.AddProfile.Provider.Sections.Vpn.footer) } } var providersSection: some View { Section { ForEach(providers, content: providerRow) } footer: { themeErrorMessage(viewModel.errorMessage) }.disabled(viewModel.isFetchingAnyProvider) } func providerRow(_ metadata: ProviderMetadata) -> some View { Button { presentOrPurchaseProvider(metadata) } label: { Label(metadata.fullName, image: themeAssetsProviderImage(metadata.name)) }.withTrailingProgress(when: viewModel.isFetchingProvider(metadata.name)) } func hiddenProviderLink(_ metadata: ProviderMetadata) -> some View { NavigationLink("", tag: metadata, selection: $viewModel.selectedProvider) { NameView( profile: $viewModel.pendingProfile, providerMetadata: metadata, bindings: bindings ) } } var updateListButton: some View { Button(L10n.AddProfile.Provider.Items.updateList) { viewModel.updateIndex(providerManager) }.withTrailingProgress(when: viewModel.isUpdatingIndex) .disabled(viewModel.isUpdatingIndex) } var providers: [ProviderMetadata] { providerManager.allProviders() .filter { $0.supportedVPNProtocols.contains(viewModel.selectedVPNProtocol) }.sorted() } var availableVPNProtocols: [VPNProtocolType] { var protos: Set = [] providers.forEach { $0.supportedVPNProtocols.forEach { protos.insert($0) } } return protos.sorted() } } private extension ProviderMetadata { var navigationId: String { "navigation.\(name)" } } // MARK: - private extension AddProviderView { // eligibility: select or purchase provider func presentOrPurchaseProvider(_ metadata: ProviderMetadata) { guard productManager.isEligible(forProvider: metadata.name) else { viewModel.presentPaywall() return } viewModel.selectProvider(metadata, providerManager) } func onErrorMessage(_ message: String?, _ scrollProxy: ScrollViewProxy) { guard message != nil else { return } scrollToErrorMessage(scrollProxy) } func scrollToErrorMessage(_ proxy: ScrollViewProxy) { proxy.maybeScrollTo(providers.last?.id, animated: true) } }