Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show upgrade icon in profiles list #891

Merged
merged 6 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,18 @@ private struct MarkerView: View {
@ObservedObject
var tunnel: ExtendedTunnel

let requiredFeatures: Set<AppFeature>?

var body: some View {
ThemeImage(headerId == nextProfileId ? .pending : statusImage)
.opaque(headerId == nextProfileId || headerId == tunnel.currentProfile?.id)
.frame(width: 24.0)
ZStack {
ThemeImage(headerId == nextProfileId ? .pending : statusImage)
.opaque(requiredFeatures == nil && (headerId == nextProfileId || headerId == tunnel.currentProfile?.id))

if let requiredFeatures {
PurchaseRequiredButton(features: requiredFeatures, paywallReason: .constant(nil))
}
}
.frame(width: 24.0)
}

var statusImage: Theme.ImageName {
Expand All @@ -111,7 +119,8 @@ private extension ProfileRowView {
MarkerView(
headerId: header.id,
nextProfileId: nextProfileId,
tunnel: tunnel
tunnel: tunnel,
requiredFeatures: requiredFeatures
)
}

Expand Down Expand Up @@ -149,6 +158,10 @@ private extension ProfileRowView {
return []
}

var requiredFeatures: Set<AppFeature>? {
profileManager.requiredFeatures(forProfileWithId: header.id)
}

var isShared: Bool {
profileManager.isRemotelyShared(profileWithId: header.id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ private extension ProfileCoordinator {
func onCommitEditingStandard() async throws {
let savedProfile = try await profileEditor.save(to: profileManager)
do {
try iapManager.verify(savedProfile.activeModules)
try iapManager.verify(savedProfile)
} catch AppError.ineligibleProfile(let requiredFeatures) {
self.requiredFeatures = requiredFeatures
requiresPurchase = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,21 @@ public final class ProfileManager: ObservableObject {
private var allProfiles: [Profile.ID: Profile] {
didSet {
reloadFilteredProfiles(with: searchSubject.value)
if let processor {
requiredFeatures = allProfiles.reduce(into: [:]) {
$0[$1.key] = processor.verify($1.value)
}
}
}
}

private var allRemoteProfiles: [Profile.ID: Profile]

private var filteredProfiles: [Profile]

@Published
private var requiredFeatures: [Profile.ID: Set<AppFeature>]

@Published
public private(set) var isRemoteImportingEnabled: Bool

Expand Down Expand Up @@ -101,6 +109,7 @@ public final class ProfileManager: ObservableObject {
}
allRemoteProfiles = [:]
filteredProfiles = []
requiredFeatures = [:]
isRemoteImportingEnabled = false
waitingObservers = []

Expand All @@ -127,6 +136,7 @@ public final class ProfileManager: ObservableObject {
allProfiles = [:]
allRemoteProfiles = [:]
filteredProfiles = []
requiredFeatures = [:]
isRemoteImportingEnabled = false
if remoteRepositoryBlock != nil {
waitingObservers = [.local, .remote]
Expand Down Expand Up @@ -168,6 +178,10 @@ extension ProfileManager {
}
}

public func requiredFeatures(forProfileWithId profileId: Profile.ID) -> Set<AppFeature>? {
requiredFeatures[profileId]
}

public func search(byName name: String) {
searchSubject.send(name)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,22 @@ public final class ProfileProcessor: ObservableObject, Sendable {

private nonisolated let _willConnect: (IAPManager, Profile) throws -> Profile

private nonisolated let _verify: (IAPManager, Profile) -> Set<AppFeature>?

public init(
iapManager: IAPManager,
title: @escaping (Profile) -> String,
isIncluded: @escaping (IAPManager, Profile) -> Bool,
willSave: @escaping (IAPManager, Profile.Builder) throws -> Profile.Builder,
willConnect: @escaping (IAPManager, Profile) throws -> Profile
willConnect: @escaping (IAPManager, Profile) throws -> Profile,
verify: @escaping (IAPManager, Profile) -> Set<AppFeature>?
) {
self.iapManager = iapManager
self.title = title
_isIncluded = isIncluded
_willSave = willSave
_willConnect = willConnect
_verify = verify
}

public func isIncluded(_ profile: Profile) -> Bool {
Expand All @@ -63,4 +67,8 @@ public final class ProfileProcessor: ObservableObject, Sendable {
public func willConnect(_ profile: Profile) throws -> Profile {
try _willConnect(iapManager, profile)
}

public func verify(_ profile: Profile) -> Set<AppFeature>? {
_verify(iapManager, profile)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ public protocol AppFeatureRequiring {
var features: Set<AppFeature> { get }
}

// MARK: - Profile

extension Profile: AppFeatureRequiring {
public var features: Set<AppFeature> {
let builders = activeModules.compactMap { module in
guard let builder = module.moduleBuilder() else {
fatalError("Cannot produce ModuleBuilder from Module: \(module)")
}
return builder
}
return builders.features
}
}

extension Array: AppFeatureRequiring where Element == any ModuleBuilder {
public var features: Set<AppFeature> {
let requirements = compactMap { builder in
guard let requiring = builder as? AppFeatureRequiring else {
fatalError("ModuleBuilder does not implement AppFeatureRequiring: \(builder)")
}
return requiring
}
return Set(requirements.flatMap(\.features))
}
}

// MARK: - Modules

extension DNSModule.Builder: AppFeatureRequiring {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,23 @@ import Foundation
import PassepartoutKit

extension IAPManager {
public func verify(_ modules: [Module]) throws {
let builders = modules.map {
guard let builder = $0.moduleBuilder() else {
fatalError("Cannot produce ModuleBuilder from Module for IAPManager.verify(): \($0)")
}
return builder
}
try verify(builders)
public func verify(_ profile: Profile) throws {
try verify(profile.features)
}

public func verify(_ modulesBuilders: [any ModuleBuilder]) throws {
try verify(modulesBuilders.features)
}

public func verify(_ features: Set<AppFeature>) throws {
#if os(tvOS)
guard isEligible(for: .appleTV) else {
throw AppError.ineligibleProfile([.appleTV])
}
#endif
let requirements: [(UUID, Set<AppFeature>)] = modulesBuilders
.compactMap { builder in
guard let requiring = builder as? AppFeatureRequiring else {
return nil
}
return (builder.id, requiring.features)
}

let requiredFeatures = Set(requirements
.flatMap(\.1)
.filter {
!isEligible(for: $0)
})

let requiredFeatures = features.filter {
!isEligible(for: $0)
}
guard requiredFeatures.isEmpty else {
throw AppError.ineligibleProfile(requiredFeatures)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ extension AppContext {
},
willConnect: { _, profile in
try profile.withProviderModules()
},
verify: { _, _ in
nil
}
)
let profileManager = {
Expand Down
12 changes: 11 additions & 1 deletion Passepartout/Shared/Shared+App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension IAPManager {
builder
},
willConnect: { iap, profile in
try iap.verify(profile.activeModules)
try iap.verify(profile)

// validate provider modules
do {
Expand All @@ -58,6 +58,16 @@ extension IAPManager {
pp_log(.app, .error, "Unable to inject provider modules: \(error)")
throw error
}
},
verify: { iap, profile in
do {
try iap.verify(profile)
return nil
} catch AppError.ineligibleProfile(let requiredFeatures) {
return requiredFeatures
} catch {
return nil
}
}
)
}
Expand Down
2 changes: 1 addition & 1 deletion Passepartout/Tunnel/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private extension PacketTunnelProvider {
func checkEligibility(of profile: Profile, environment: TunnelEnvironment) async throws {
await iapManager.reloadReceipt()
do {
try iapManager.verify(profile.activeModules)
try iapManager.verify(profile)
} catch {
let error = PassepartoutError(.App.ineligibleProfile)
environment.setEnvironmentValue(error.code, forKey: TunnelEnvironmentKeys.lastErrorCode)
Expand Down