From a38e3fed7a733a3d2c37025f2617110b9308c0da Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sun, 10 Sep 2023 00:52:39 +0200 Subject: [PATCH] Look up TestFlight flag asynchronously (#352) Xcode has been quite obnoxious recently with this issue. Start the app with the most restrictive type (.undefined), relax restrictions after looking up sandbox and app receipt. --- Passepartout.xcodeproj/project.pbxproj | 12 ++--- .../App/Constants/Constants+App.swift | 8 +--- Passepartout/App/Context/AppContext.swift | 2 +- .../App/Managers/ProductManager.swift | 45 ++++++++++++++++--- Passepartout/App/PassepartoutApp.swift | 2 + ...eta.swift => PaywallView+Restricted.swift} | 9 ++-- Passepartout/App/Views/PaywallView.swift | 4 +- .../SandboxChecker.swift} | 34 +++++++++----- 8 files changed, 82 insertions(+), 34 deletions(-) rename Passepartout/App/Views/{PaywallView+Beta.swift => PaywallView+Restricted.swift} (80%) rename PassepartoutLibrary/Sources/PassepartoutCore/{Utils/Utils+TestFlight.swift => Reusable/SandboxChecker.swift} (72%) diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index dac9983b..6cfd2fa4 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -50,10 +50,10 @@ 0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */; }; 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */; }; 0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E35C099280E95BB0071FA35 /* ProviderProfileAvailability.swift */; }; + 0E3A3C102AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C0F2AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift */; }; 0E3A3C132AAB7C480003A5F6 /* UpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C112AAB7C470003A5F6 /* UpgradeManager.swift */; }; 0E3A3C142AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C122AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift */; }; 0E3A3C162AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C152AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift */; }; - 0E3A3C102AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C0F2AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift */; }; 0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */; }; 0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */; }; 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */; }; @@ -176,7 +176,7 @@ 0ED2B36027D3C99100FD8EA9 /* PassepartoutWireGuardTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0ED2B34A27D3C77800FD8EA9 /* PassepartoutWireGuardTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 0ED2B36727D3C9A300FD8EA9 /* WireGuardAppExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 0ED2B36627D3C9A300FD8EA9 /* WireGuardAppExtension */; }; 0ED30DCC27EA197D0057D8A3 /* RevealingSecureField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */; }; - 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */; }; + 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Restricted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Restricted.swift */; }; 0ED30DD227EA1F650057D8A3 /* PaywallView+Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */; }; 0ED30DDB27EA351C0057D8A3 /* Constants+Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED30DDA27EA351C0057D8A3 /* Constants+Tunnel.swift */; }; 0ED30DDD27EA35230057D8A3 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB17EA127D2263700D473B5 /* Constants.swift */; }; @@ -344,10 +344,10 @@ 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Scene.swift"; sourceTree = ""; }; 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnDemandView+SSID.swift"; sourceTree = ""; }; 0E35C099280E95BB0071FA35 /* ProviderProfileAvailability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderProfileAvailability.swift; sourceTree = ""; }; + 0E3A3C0F2AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyValueStore+CloudKit.swift"; sourceTree = ""; }; 0E3A3C112AAB7C470003A5F6 /* UpgradeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeManager.swift; sourceTree = ""; }; 0E3A3C122AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultUpgradeManagerStrategy.swift; sourceTree = ""; }; 0E3A3C152AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeManagerStrategy.swift; sourceTree = ""; }; - 0E3A3C0F2AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyValueStore+CloudKit.swift"; sourceTree = ""; }; 0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddHostView+Name.swift"; sourceTree = ""; }; 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+VPN.swift"; sourceTree = ""; }; @@ -497,7 +497,7 @@ 0ED2B34A27D3C77800FD8EA9 /* PassepartoutWireGuardTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PassepartoutWireGuardTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0ED2B35A27D3C94F00FD8EA9 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevealingSecureField.swift; sourceTree = ""; }; - 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaywallView+Beta.swift"; sourceTree = ""; }; + 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Restricted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaywallView+Restricted.swift"; sourceTree = ""; }; 0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaywallView+Purchase.swift"; sourceTree = ""; }; 0ED30DDA27EA351C0057D8A3 /* Constants+Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Tunnel.swift"; sourceTree = ""; }; 0ED31C3920CF39510027975F /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; @@ -676,8 +676,8 @@ 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */, 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */, 0EF0FAF527DD0211007EB181 /* PaywallView.swift */, - 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Beta.swift */, 0ED30DD127EA1F650057D8A3 /* PaywallView+Purchase.swift */, + 0ED30DCE27EA1EF80057D8A3 /* PaywallView+Restricted.swift */, 0E44689527B051C300A14CE4 /* ProfileView.swift */, 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */, 0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */, @@ -1481,7 +1481,7 @@ 0E3A3C132AAB7C480003A5F6 /* UpgradeManager.swift in Sources */, 0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */, 0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */, - 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */, + 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Restricted.swift in Sources */, 0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */, 0E71ACF727C107CA00F85C4B /* DebugLogView.swift in Sources */, 0EF0FAF927DD212C007EB181 /* IntentActivity.swift in Sources */, diff --git a/Passepartout/App/Constants/Constants+App.swift b/Passepartout/App/Constants/Constants+App.swift index b65d87b7..97e2ff41 100644 --- a/Passepartout/App/Constants/Constants+App.swift +++ b/Passepartout/App/Constants/Constants+App.swift @@ -39,10 +39,6 @@ extension Constants { static let appStoreId: String = bundleConfig("appstore_id") static let appGroupId: String = bundleConfig("group_id") - - static let isBeta: Bool = { - Bundle.main.isTestFlight - }() } enum CloudKit { @@ -56,7 +52,7 @@ extension Constants { } enum InApp { - static var appType: ProductManager.AppType { + static var overriddenAppType: ProductManager.AppType? { if let envString = ProcessInfo.processInfo.environment["APP_TYPE"], let envValue = Int(envString), let testAppType = ProductManager.AppType(rawValue: envValue) { @@ -68,7 +64,7 @@ extension Constants { return testAppType } - return App.isBeta ? .beta : .freemium + return nil } #if targetEnvironment(macCatalyst) diff --git a/Passepartout/App/Context/AppContext.swift b/Passepartout/App/Context/AppContext.swift index a917c702..6aad8548 100644 --- a/Passepartout/App/Context/AppContext.swift +++ b/Passepartout/App/Context/AppContext.swift @@ -43,7 +43,7 @@ final class AppContext { self.coreContext = coreContext productManager = ProductManager( - appType: Constants.InApp.appType, + overriddenAppType: Constants.InApp.overriddenAppType, buildProducts: Constants.InApp.buildProducts ) diff --git a/Passepartout/App/Managers/ProductManager.swift b/Passepartout/App/Managers/ProductManager.swift index e74b69a5..1e67a028 100644 --- a/Passepartout/App/Managers/ProductManager.swift +++ b/Passepartout/App/Managers/ProductManager.swift @@ -29,21 +29,40 @@ import Kvitto import PassepartoutLibrary import StoreKit +@MainActor final class ProductManager: NSObject, ObservableObject { enum AppType: Int { + case undefined = -1 + case freemium = 0 case beta = 1 case fullVersion = 2 + + var isRestricted: Bool { + switch self { + case .undefined, .beta: + return true + + default: + return false + } + } } - let appType: AppType + private let overriddenAppType: AppType? + + private let sandboxChecker: SandboxChecker + + private var subscriptions: Set let buildProducts: BuildProducts let didRefundProducts = PassthroughSubject() + @Published private(set) var appType: AppType + @Published private(set) var isRefreshingProducts = false @Published private(set) var products: [SKProduct] @@ -73,10 +92,14 @@ final class ProductManager: NSObject, ObservableObject { private var refreshRequest: SKReceiptRefreshRequest? - init(appType: AppType, buildProducts: BuildProducts) { - self.appType = appType + init(overriddenAppType: AppType?, buildProducts: BuildProducts) { + self.overriddenAppType = overriddenAppType self.buildProducts = buildProducts + appType = .undefined + sandboxChecker = SandboxChecker(bundle: .main) + subscriptions = [] + products = [] inApp = InApp() purchasedAppBuild = nil @@ -88,8 +111,20 @@ final class ProductManager: NSObject, ObservableObject { reloadReceipt() SKPaymentQueue.default().add(self) - refreshProducts() + + sandboxChecker.$isSandbox + .dropFirst() // ignore initial value + .sink { [weak self] in + guard let self else { + return + } + self.appType = overriddenAppType ?? ($0 ? .beta : .freemium) + pp_log.info("App type: \(self.appType)") + self.reloadReceipt() + }.store(in: &subscriptions) + + sandboxChecker.check() } deinit { @@ -294,7 +329,7 @@ private extension ProductManager { let receipt = Receipt(contentsOfURL: url) // in TestFlight, attempt fallback to existing release receipt - if Bundle.main.isTestFlight { + if appType == .beta { guard let receipt else { let releaseUrl = url.deletingLastPathComponent().appendingPathComponent("receipt") guard releaseUrl != url else { diff --git a/Passepartout/App/PassepartoutApp.swift b/Passepartout/App/PassepartoutApp.swift index a6e9bf73..77ed7b10 100644 --- a/Passepartout/App/PassepartoutApp.swift +++ b/Passepartout/App/PassepartoutApp.swift @@ -47,6 +47,8 @@ struct PassepartoutApp: App { } private extension View { + + @MainActor func onIntentActivity(_ activity: IntentActivity) -> some View { onContinueUserActivity(activity.name) { userActivity in diff --git a/Passepartout/App/Views/PaywallView+Beta.swift b/Passepartout/App/Views/PaywallView+Restricted.swift similarity index 80% rename from Passepartout/App/Views/PaywallView+Beta.swift rename to Passepartout/App/Views/PaywallView+Restricted.swift index ef688627..563926dd 100644 --- a/Passepartout/App/Views/PaywallView+Beta.swift +++ b/Passepartout/App/Views/PaywallView+Restricted.swift @@ -1,5 +1,5 @@ // -// PaywallView+Beta.swift +// PaywallView+Restricted.swift // Passepartout // // Created by Davide De Rosa on 3/22/22. @@ -26,11 +26,12 @@ import SwiftUI extension PaywallView { - struct BetaView: View { + struct RestrictedView: View { var body: some View { - Text("The requested feature in unavailable in beta.") - .navigationTitle("Beta") + Text("The requested feature in unavailable in this build.") + .multilineTextAlignment(.center) .padding() + .navigationTitle("Restricted") } } } diff --git a/Passepartout/App/Views/PaywallView.swift b/Passepartout/App/Views/PaywallView.swift index 51cca061..a779dd4b 100644 --- a/Passepartout/App/Views/PaywallView.swift +++ b/Passepartout/App/Views/PaywallView.swift @@ -51,8 +51,8 @@ struct PaywallView: View { var body: some View { Group { - if productManager.appType == .beta { - BetaView() + if productManager.appType.isRestricted { + RestrictedView() } else { PurchaseView( isPresented: $isPresented, diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+TestFlight.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/SandboxChecker.swift similarity index 72% rename from PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+TestFlight.swift rename to PassepartoutLibrary/Sources/PassepartoutCore/Reusable/SandboxChecker.swift index aa5cda89..aaee53d4 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+TestFlight.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/SandboxChecker.swift @@ -1,5 +1,5 @@ // -// Utils+TestFlight.swift +// SandboxChecker.swift // Passepartout // // Created by Davide De Rosa on 5/18/22. @@ -28,19 +28,35 @@ import Foundation // https://stackoverflow.com/a/32238344/784615 // https://gist.github.com/lukaskubanek/cbfcab29c0c93e0e9e0a16ab09586996 -extension Bundle { - public var isTestFlight: Bool { - #if targetEnvironment(simulator) - true +@MainActor +public final class SandboxChecker: ObservableObject { + private let bundle: Bundle + + @Published public private(set) var isSandbox = false + + public init(bundle: Bundle) { + self.bundle = bundle + } + + public func check() { + Task { + isSandbox = await isSandboxBuild() + pp_log.info("Sandbox build: \(isSandbox)") + } + } + + private func isSandboxBuild() async -> Bool { + #if os(iOS) + bundle.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" #elseif targetEnvironment(macCatalyst) || os(macOS) var status = noErr var code: SecStaticCode? - status = SecStaticCodeCreateWithPath(bundleURL as CFURL, [], &code) + status = SecStaticCodeCreateWithPath(bundle.bundleURL as CFURL, [], &code) guard status == noErr else { return false } - guard let code = code else { + guard let code else { return false } @@ -53,7 +69,7 @@ extension Bundle { guard status == noErr else { return false } - guard let requirement = requirement else { + guard let requirement else { return false } @@ -63,8 +79,6 @@ extension Bundle { requirement ) return status == errSecSuccess - #elseif os(iOS) - appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" #else false #endif