mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-01-31 04:52:05 +00:00
parent
bad9e8b58e
commit
cb530d8a65
@ -168,15 +168,15 @@ extension AppCoordinator {
|
||||
}
|
||||
enterDetail(of: profile)
|
||||
},
|
||||
onEditProviderEntity: {
|
||||
onMigrateProfiles: {
|
||||
modalRoute = .migrateProfiles
|
||||
},
|
||||
onProviderEntityRequired: {
|
||||
guard let pair = $0.selectedProvider else {
|
||||
return
|
||||
}
|
||||
present(.editProviderEntity($0, pair.module, pair.selection))
|
||||
},
|
||||
onMigrateProfiles: {
|
||||
modalRoute = .migrateProfiles
|
||||
},
|
||||
onPurchaseRequired: { features in
|
||||
setLater(.purchase(features)) {
|
||||
paywallReason = $0
|
||||
|
@ -137,7 +137,7 @@ private extension InstalledProfileView {
|
||||
.selectedProvider
|
||||
.map { _, selection in
|
||||
Button {
|
||||
flow?.onEditProviderEntity(profile!)
|
||||
flow?.onProviderEntityRequired(profile!)
|
||||
} label: {
|
||||
providerSelectorLabel(with: selection)
|
||||
}
|
||||
@ -202,7 +202,7 @@ private struct ToggleButton: View {
|
||||
interactiveManager: interactiveManager,
|
||||
errorHandler: errorHandler,
|
||||
onProviderEntityRequired: {
|
||||
flow?.onEditProviderEntity($0)
|
||||
flow?.onProviderEntityRequired($0)
|
||||
},
|
||||
onPurchaseRequired: {
|
||||
flow?.onPurchaseRequired($0)
|
||||
|
@ -96,7 +96,7 @@ private extension ProfileContainerView {
|
||||
InteractiveCoordinator(style: .modal, manager: interactiveManager) {
|
||||
errorHandler.handle(
|
||||
$0,
|
||||
title: Strings.Global.Nouns.connection,
|
||||
title: interactiveManager.editor.profile.name,
|
||||
message: Strings.Views.App.Errors.tunnel
|
||||
)
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ private extension ProfileContextMenu {
|
||||
interactiveManager: interactiveManager,
|
||||
errorHandler: errorHandler,
|
||||
onProviderEntityRequired: {
|
||||
flow?.onEditProviderEntity($0)
|
||||
flow?.onProviderEntityRequired($0)
|
||||
},
|
||||
onPurchaseRequired: {
|
||||
flow?.onPurchaseRequired($0)
|
||||
@ -90,7 +90,7 @@ private extension ProfileContextMenu {
|
||||
.selectedProvider
|
||||
.map { _ in
|
||||
Button(Strings.Views.App.ProfileContext.connectTo) {
|
||||
flow?.onEditProviderEntity(profile!)
|
||||
flow?.onProviderEntityRequired(profile!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,9 +30,9 @@ import PassepartoutKit
|
||||
struct ProfileFlow {
|
||||
let onEditProfile: (ProfilePreview) -> Void
|
||||
|
||||
let onEditProviderEntity: (Profile) -> Void
|
||||
|
||||
let onMigrateProfiles: () -> Void
|
||||
|
||||
let onProviderEntityRequired: (Profile) -> Void
|
||||
|
||||
let onPurchaseRequired: (Set<AppFeature>) -> Void
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ private extension ProfileRowView {
|
||||
interactiveManager: interactiveManager,
|
||||
errorHandler: errorHandler,
|
||||
onProviderEntityRequired: {
|
||||
flow?.onEditProviderEntity($0)
|
||||
flow?.onProviderEntityRequired($0)
|
||||
},
|
||||
onPurchaseRequired: {
|
||||
flow?.onPurchaseRequired($0)
|
||||
|
@ -59,7 +59,7 @@ struct TunnelRestartButton<Label>: View where Label: View {
|
||||
} catch {
|
||||
errorHandler.handle(
|
||||
error,
|
||||
title: Strings.Global.Nouns.connection,
|
||||
title: profile.name,
|
||||
message: Strings.Views.App.Errors.tunnel
|
||||
)
|
||||
}
|
||||
|
@ -73,15 +73,13 @@ struct ProfileCoordinator: View {
|
||||
var body: some View {
|
||||
contentView
|
||||
.modifier(PaywallModifier(reason: $paywallReason))
|
||||
.alert(Strings.Views.Profile.Alerts.Purchase.title, isPresented: $requiresPurchase) {
|
||||
Button(Strings.Global.Actions.purchase) {
|
||||
paywallReason = .purchase(requiredFeatures, nil)
|
||||
}
|
||||
Button(Strings.Views.Profile.Alerts.Purchase.Buttons.ok, action: onDismiss)
|
||||
Button(Strings.Global.Actions.cancel, role: .cancel, action: {})
|
||||
} message: {
|
||||
Text(purchaseMessage)
|
||||
}
|
||||
.modifier(PurchaseAlertModifier(
|
||||
isPresented: $requiresPurchase,
|
||||
paywallReason: $paywallReason,
|
||||
requiredFeatures: requiredFeatures,
|
||||
okTitle: Strings.Views.Profile.Alerts.Purchase.Buttons.ok,
|
||||
okAction: onDismiss
|
||||
))
|
||||
.withErrorHandler(errorHandler)
|
||||
}
|
||||
}
|
||||
@ -117,13 +115,6 @@ private extension ProfileCoordinator {
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
var purchaseMessage: String {
|
||||
let msg = Strings.Views.Profile.Alerts.Purchase.message
|
||||
return msg + "\n\n" + requiredFeatures
|
||||
.map(\.localizedDescription)
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
private extension ProfileCoordinator {
|
||||
|
@ -24,8 +24,10 @@
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
public struct AppCoordinator: View, AppCoordinatorConforming {
|
||||
private let profileManager: ProfileManager
|
||||
@ -34,6 +36,18 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
|
||||
|
||||
private let registry: Registry
|
||||
|
||||
@State
|
||||
private var requiresPurchase = false
|
||||
|
||||
@State
|
||||
private var requiredFeatures: Set<AppFeature> = []
|
||||
|
||||
@State
|
||||
private var paywallReason: PaywallReason?
|
||||
|
||||
@StateObject
|
||||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
public init(profileManager: ProfileManager, tunnel: ExtendedTunnel, registry: Registry) {
|
||||
self.profileManager = profileManager
|
||||
self.tunnel = tunnel
|
||||
@ -60,13 +74,28 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: AppCoordinatorRoute.self, destination: pushDestination)
|
||||
.withErrorHandler(errorHandler)
|
||||
.modifier(PaywallModifier(reason: $paywallReason))
|
||||
.modifier(PurchaseAlertModifier(
|
||||
isPresented: $requiresPurchase,
|
||||
paywallReason: $paywallReason,
|
||||
requiredFeatures: requiredFeatures
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppCoordinator {
|
||||
var profileView: some View {
|
||||
ProfileView(profileManager: profileManager, tunnel: tunnel)
|
||||
ProfileView(
|
||||
profileManager: profileManager,
|
||||
tunnel: tunnel,
|
||||
errorHandler: errorHandler,
|
||||
flow: .init(
|
||||
onProviderEntityRequired: onProviderEntityRequired,
|
||||
onPurchaseRequired: onPurchaseRequired
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// var searchView: some View {
|
||||
@ -109,6 +138,20 @@ private extension AppCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppCoordinator {
|
||||
func onProviderEntityRequired(_ profile: Profile) {
|
||||
errorHandler.handle(
|
||||
title: profile.name,
|
||||
message: Strings.Alerts.Providers.MissingServer.message
|
||||
)
|
||||
}
|
||||
|
||||
func onPurchaseRequired(_ features: Set<AppFeature>) {
|
||||
requiredFeatures = features
|
||||
requiresPurchase = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#Preview {
|
||||
|
34
Passepartout/Library/Sources/AppUITV/Views/App/AppFlow.swift
Normal file
34
Passepartout/Library/Sources/AppUITV/Views/App/AppFlow.swift
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// AppFlow.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/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/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
struct AppFlow {
|
||||
let onProviderEntityRequired: (Profile) -> Void
|
||||
|
||||
let onPurchaseRequired: (Set<AppFeature>) -> Void
|
||||
}
|
@ -53,6 +53,8 @@ struct ActiveProfileView: View {
|
||||
@ObservedObject
|
||||
var errorHandler: ErrorHandler
|
||||
|
||||
var flow: AppFlow?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: .zero) {
|
||||
VStack {
|
||||
@ -167,15 +169,13 @@ private extension ActiveProfileView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private extension ActiveProfileView {
|
||||
func onProviderEntityRequired(_ profile: Profile) {
|
||||
// FIXME: #913, TV missing provider entity
|
||||
flow?.onProviderEntityRequired(profile)
|
||||
}
|
||||
|
||||
func onPurchaseRequired(_ features: Set<AppFeature>) {
|
||||
// FIXME: #913, TV purchase required
|
||||
flow?.onPurchaseRequired(features)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,8 @@ struct ProfileListView: View {
|
||||
@ObservedObject
|
||||
var errorHandler: ErrorHandler
|
||||
|
||||
var flow: AppFlow?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
headerView
|
||||
@ -100,15 +102,13 @@ private extension ProfileListView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private extension ProfileListView {
|
||||
func onProviderEntityRequired(_ profile: Profile) {
|
||||
// FIXME: #913, TV missing provider entity
|
||||
flow?.onProviderEntityRequired(profile)
|
||||
}
|
||||
|
||||
func onPurchaseRequired(_ features: Set<AppFeature>) {
|
||||
// FIXME: #913, TV purchase required
|
||||
flow?.onPurchaseRequired(features)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import PassepartoutKit
|
||||
import SwiftUI
|
||||
import UILibrary
|
||||
|
||||
struct ProfileView: View, TunnelInstallationProviding {
|
||||
struct ProfileView: View, Routable, TunnelInstallationProviding {
|
||||
enum Field: Hashable {
|
||||
case connect
|
||||
|
||||
@ -47,6 +47,11 @@ struct ProfileView: View, TunnelInstallationProviding {
|
||||
@ObservedObject
|
||||
var tunnel: ExtendedTunnel
|
||||
|
||||
@ObservedObject
|
||||
var errorHandler: ErrorHandler
|
||||
|
||||
var flow: AppFlow?
|
||||
|
||||
@State
|
||||
var showsSidePanel = false
|
||||
|
||||
@ -56,9 +61,6 @@ struct ProfileView: View, TunnelInstallationProviding {
|
||||
@StateObject
|
||||
private var interactiveManager = InteractiveManager()
|
||||
|
||||
@StateObject
|
||||
private var errorHandler: ErrorHandler = .default()
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
HStack(spacing: .zero) {
|
||||
@ -80,7 +82,6 @@ struct ProfileView: View, TunnelInstallationProviding {
|
||||
.ignoresSafeArea(edges: .horizontal)
|
||||
.background(theme.primaryColor.opacity(0.6).gradient)
|
||||
.themeAnimation(on: showsSidePanel, category: .profiles)
|
||||
.withErrorHandler(errorHandler)
|
||||
.defaultFocus($focusedField, .switchProfile)
|
||||
.onChange(of: tunnel.status, onTunnelStatus)
|
||||
.onChange(of: tunnel.currentProfile, onTunnelCurrentProfile)
|
||||
@ -104,7 +105,8 @@ private extension ProfileView {
|
||||
isSwitching: $showsSidePanel,
|
||||
focusedField: $focusedField,
|
||||
interactiveManager: interactiveManager,
|
||||
errorHandler: errorHandler
|
||||
errorHandler: errorHandler,
|
||||
flow: flow
|
||||
)
|
||||
}
|
||||
|
||||
@ -126,7 +128,7 @@ private extension ProfileView {
|
||||
InteractiveCoordinator(style: .inline(withCancel: false), manager: interactiveManager) {
|
||||
errorHandler.handle(
|
||||
$0,
|
||||
title: Strings.Global.Nouns.connection,
|
||||
title: interactiveManager.editor.profile.name,
|
||||
message: Strings.Views.App.Errors.tunnel
|
||||
)
|
||||
}
|
||||
@ -144,7 +146,8 @@ private extension ProfileView {
|
||||
tunnel: tunnel,
|
||||
focusedField: $focusedField,
|
||||
interactiveManager: interactiveManager,
|
||||
errorHandler: errorHandler
|
||||
errorHandler: errorHandler,
|
||||
flow: flow
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -189,6 +192,7 @@ private extension ProfileView {
|
||||
ProfileView(
|
||||
profileManager: .mock,
|
||||
tunnel: .mock,
|
||||
errorHandler: .default(),
|
||||
showsSidePanel: true
|
||||
)
|
||||
.withMockEnvironment()
|
||||
@ -198,6 +202,7 @@ private extension ProfileView {
|
||||
ProfileView(
|
||||
profileManager: ProfileManager(profiles: []),
|
||||
tunnel: .mock,
|
||||
errorHandler: .default(),
|
||||
showsSidePanel: true
|
||||
)
|
||||
.withMockEnvironment()
|
||||
|
@ -32,6 +32,10 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
public func setLater<T>(_ value: T?, millis: Int = 50, block: @escaping (T?) -> Void) {
|
||||
globalSetLater(value, millis: millis, block: block)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
public func `if`(_ condition: Bool) -> some View {
|
||||
if condition {
|
||||
@ -63,14 +67,6 @@ extension View {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func setLater<T>(_ value: T?, millis: Int = 50, block: @escaping (T?) -> Void) {
|
||||
Task {
|
||||
block(nil)
|
||||
try await Task.sleep(for: .milliseconds(millis))
|
||||
block(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewModifier {
|
||||
@ -79,6 +75,18 @@ extension ViewModifier {
|
||||
Self._printChanges()
|
||||
}
|
||||
}
|
||||
|
||||
public func setLater<T>(_ value: T?, millis: Int = 50, block: @escaping (T?) -> Void) {
|
||||
globalSetLater(value, millis: millis, block: block)
|
||||
}
|
||||
}
|
||||
|
||||
private func globalSetLater<T>(_ value: T?, millis: Int = 50, block: @escaping (T?) -> Void) {
|
||||
Task {
|
||||
block(nil)
|
||||
try await Task.sleep(for: .milliseconds(millis))
|
||||
block(value)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
|
@ -21,6 +21,12 @@ public enum Strings {
|
||||
public static let ok = Strings.tr("Localizable", "alerts.import.passphrase.ok", fallback: "Decrypt")
|
||||
}
|
||||
}
|
||||
public enum Providers {
|
||||
public enum MissingServer {
|
||||
/// No provider server selected. Please select a destination server on your iOS/macOS device.
|
||||
public static let message = Strings.tr("Localizable", "alerts.providers.missing_server.message", fallback: "No provider server selected. Please select a destination server on your iOS/macOS device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
public enum Entities {
|
||||
public enum Dns {
|
||||
@ -770,10 +776,6 @@ public enum Strings {
|
||||
public enum Profile {
|
||||
public enum Alerts {
|
||||
public enum Purchase {
|
||||
/// This profile requires paid features to work.
|
||||
public static let message = Strings.tr("Localizable", "views.profile.alerts.purchase.message", fallback: "This profile requires paid features to work.")
|
||||
/// Purchase required
|
||||
public static let title = Strings.tr("Localizable", "views.profile.alerts.purchase.title", fallback: "Purchase required")
|
||||
public enum Buttons {
|
||||
/// Save anyway
|
||||
public static let ok = Strings.tr("Localizable", "views.profile.alerts.purchase.buttons.ok", fallback: "Save anyway")
|
||||
@ -828,6 +830,12 @@ public enum Strings {
|
||||
/// (on-demand)
|
||||
public static let onDemandSuffix = Strings.tr("Localizable", "views.ui.connection_status.on_demand_suffix", fallback: " (on-demand)")
|
||||
}
|
||||
public enum PurchaseAlert {
|
||||
/// This profile requires paid features to work.
|
||||
public static let message = Strings.tr("Localizable", "views.ui.purchase_alert.message", fallback: "This profile requires paid features to work.")
|
||||
/// Purchase required
|
||||
public static let title = Strings.tr("Localizable", "views.ui.purchase_alert.title", fallback: "Purchase required")
|
||||
}
|
||||
public enum PurchaseRequired {
|
||||
public enum Purchase {
|
||||
/// Purchase required
|
||||
|
@ -78,9 +78,7 @@
|
||||
|
||||
"views.profile.rows.add_module" = "Add module";
|
||||
"views.profile.module_list.section.footer" = "Drag modules to rearrange them, as their order determines priority.";
|
||||
"views.profile.alerts.purchase.title" = "Purchase required";
|
||||
"views.profile.alerts.purchase.buttons.ok" = "Save anyway";
|
||||
"views.profile.alerts.purchase.message" = "This profile requires paid features to work.";
|
||||
|
||||
"views.providers.no_provider" = "None";
|
||||
"views.providers.select_provider" = "Select a provider";
|
||||
@ -95,6 +93,8 @@
|
||||
"views.providers.vpn.no_servers" = "No servers";
|
||||
|
||||
"views.ui.connection_status.on_demand_suffix" = " (on-demand)";
|
||||
"views.ui.purchase_alert.title" = "Purchase required";
|
||||
"views.ui.purchase_alert.message" = "This profile requires paid features to work.";
|
||||
"views.ui.purchase_required.purchase.help" = "Purchase required";
|
||||
"views.ui.purchase_required.restricted.help" = "Feature is restricted";
|
||||
|
||||
@ -286,6 +286,7 @@
|
||||
|
||||
"alerts.import.passphrase.message" = "Enter passphrase for '%@'.";
|
||||
"alerts.import.passphrase.ok" = "Decrypt";
|
||||
"alerts.providers.missing_server.message" = "No provider server selected. Please select a destination server on your iOS/macOS device.";
|
||||
|
||||
// MARK: Global (App errors)
|
||||
|
||||
|
@ -61,7 +61,9 @@ struct PaywallView: View {
|
||||
var body: some View {
|
||||
paywallView
|
||||
.themeProgress(if: isFetchingProducts)
|
||||
#if !os(tvOS)
|
||||
.toolbar(content: toolbarContent)
|
||||
#endif
|
||||
.alert(
|
||||
Strings.Global.Actions.purchase,
|
||||
isPresented: $isPurchasePendingConfirmation,
|
||||
@ -80,6 +82,12 @@ private extension PaywallView {
|
||||
Strings.Global.Actions.purchase
|
||||
}
|
||||
|
||||
var otherFeatures: [AppFeature] {
|
||||
AppFeature.allCases.filter {
|
||||
!features.contains($0)
|
||||
}
|
||||
}
|
||||
|
||||
var paywallView: some View {
|
||||
Form {
|
||||
requiredFeaturesView
|
||||
@ -91,12 +99,6 @@ private extension PaywallView {
|
||||
.disabled(purchasingIdentifier != nil)
|
||||
}
|
||||
|
||||
var otherFeatures: [AppFeature] {
|
||||
AppFeature.allCases.filter {
|
||||
!features.contains($0)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var productsView: some View {
|
||||
oneTimeProduct.map {
|
||||
@ -127,21 +129,21 @@ private extension PaywallView {
|
||||
FeatureListView(
|
||||
style: .list,
|
||||
header: Strings.Views.Paywall.Sections.Features.Required.header,
|
||||
features: Array(features)
|
||||
) {
|
||||
Text($0.localizedDescription)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
features: Array(features),
|
||||
content: {
|
||||
featureView(for: $0)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var otherFeaturesView: some View {
|
||||
FeatureListView(
|
||||
style: otherFeaturesStyle,
|
||||
header: Strings.Views.Paywall.Sections.Features.Other.header,
|
||||
features: otherFeatures
|
||||
) {
|
||||
Text($0.localizedDescription)
|
||||
}
|
||||
features: otherFeatures,
|
||||
content: featureView(for:)
|
||||
)
|
||||
}
|
||||
|
||||
var otherFeaturesStyle: FeatureListViewStyle {
|
||||
@ -152,6 +154,16 @@ private extension PaywallView {
|
||||
#endif
|
||||
}
|
||||
|
||||
func featureView(for feature: AppFeature) -> some View {
|
||||
#if os(tvOS)
|
||||
Button(feature.localizedDescription) {
|
||||
//
|
||||
}
|
||||
#else
|
||||
Text(feature.localizedDescription)
|
||||
#endif
|
||||
}
|
||||
|
||||
var restoreView: some View {
|
||||
RestorePurchasesButton(errorHandler: errorHandler)
|
||||
.themeSectionWithSingleRow(
|
||||
|
@ -64,16 +64,18 @@ private extension ProductView {
|
||||
@ViewBuilder
|
||||
func withPaywallStyle(_ paywallStyle: PaywallProductViewStyle) -> some View {
|
||||
#if os(tvOS)
|
||||
productViewStyle(.compact)
|
||||
.padding()
|
||||
#else
|
||||
switch paywallStyle {
|
||||
case .recurring:
|
||||
productViewStyle(.compact)
|
||||
case .oneTime, .recurring:
|
||||
productViewStyle(.regular)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(.init())
|
||||
|
||||
case .oneTime, .donation:
|
||||
case .donation:
|
||||
productViewStyle(.compact)
|
||||
.padding()
|
||||
}
|
||||
#else
|
||||
productViewStyle(.compact)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
//
|
||||
// PurchaseAlertModifier.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/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/>.
|
||||
//
|
||||
|
||||
import CommonIAP
|
||||
import CommonUtils
|
||||
import SwiftUI
|
||||
|
||||
public struct PurchaseAlertModifier: ViewModifier {
|
||||
|
||||
@Binding
|
||||
private var isPresented: Bool
|
||||
|
||||
@Binding
|
||||
private var paywallReason: PaywallReason?
|
||||
|
||||
private let requiredFeatures: Set<AppFeature>
|
||||
|
||||
private let okTitle: String?
|
||||
|
||||
private let okAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
isPresented: Binding<Bool>,
|
||||
paywallReason: Binding<PaywallReason?>,
|
||||
requiredFeatures: Set<AppFeature>,
|
||||
okTitle: String? = nil,
|
||||
okAction: (() -> Void)? = nil
|
||||
) {
|
||||
_isPresented = isPresented
|
||||
_paywallReason = paywallReason
|
||||
self.requiredFeatures = requiredFeatures
|
||||
self.okTitle = okTitle
|
||||
self.okAction = okAction
|
||||
}
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
.alert(Strings.Views.Ui.PurchaseAlert.title, isPresented: $isPresented) {
|
||||
Button(Strings.Global.Actions.purchase) {
|
||||
setLater(.purchase(requiredFeatures, nil)) {
|
||||
paywallReason = $0
|
||||
}
|
||||
}
|
||||
if let okTitle {
|
||||
Button(okTitle) {
|
||||
okAction?()
|
||||
}
|
||||
}
|
||||
Button(Strings.Global.Actions.cancel, role: .cancel, action: {})
|
||||
} message: {
|
||||
Text(purchaseMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PurchaseAlertModifier {
|
||||
var purchaseMessage: String {
|
||||
let msg = Strings.Views.Ui.PurchaseAlert.message
|
||||
return msg + "\n\n" + requiredFeatures
|
||||
.map(\.localizedDescription)
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import CommonUtils
|
||||
import PassepartoutKit
|
||||
import SwiftUI
|
||||
|
||||
|
@ -120,7 +120,7 @@ private extension TunnelToggleButton {
|
||||
guard let provider = providerModule.provider else {
|
||||
errorHandler.handle(
|
||||
PassepartoutError(.providerRequired),
|
||||
title: Strings.Global.Nouns.connection
|
||||
title: profile.name
|
||||
)
|
||||
return
|
||||
}
|
||||
@ -168,7 +168,7 @@ private extension TunnelToggleButton {
|
||||
} catch {
|
||||
errorHandler.handle(
|
||||
error,
|
||||
title: Strings.Global.Nouns.connection,
|
||||
title: profile.name,
|
||||
message: Strings.Views.App.Errors.tunnel
|
||||
)
|
||||
}
|
||||
|
@ -711,17 +711,21 @@ private extension ProfileManagerTests {
|
||||
) async throws {
|
||||
let exp = expectation(description: description)
|
||||
var wasMet = false
|
||||
sut.objectWillChange
|
||||
.sink {
|
||||
guard !wasMet else {
|
||||
return
|
||||
}
|
||||
if condition(sut) {
|
||||
wasMet = true
|
||||
exp.fulfill()
|
||||
}
|
||||
|
||||
Publishers.Merge(
|
||||
sut.objectWillChange,
|
||||
sut.didChange.map { _ in }
|
||||
)
|
||||
.sink {
|
||||
guard !wasMet else {
|
||||
return
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
if condition(sut) {
|
||||
wasMet = true
|
||||
exp.fulfill()
|
||||
}
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
|
||||
try await action(sut)
|
||||
await fulfillment(of: [exp], timeout: timeout)
|
||||
|
Loading…
Reference in New Issue
Block a user