From c1e69daadef16ddfc89bfaa04cb99138df00e263 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Wed, 15 May 2024 22:02:39 -0700 Subject: [PATCH 01/21] Replace uses of PreviousPurchasePublisher --- .../PreviousPurchasePublisher.swift | 2 +- .../Purchasing/PurchaseRepository.swift | 32 +++++++++++++++++ .../Sources/Purchasing/PurchaseState.swift | 4 ++- .../Tests/PurchaseValidatorTests.swift | 3 +- .../DocumentScanningController.swift | 14 +++++--- ...oLibraryDataSourceExtraItemsProvider.swift | 10 +++++- .../Settings/List/SettingsAlertButton.swift | 13 +++++-- .../Sources/Settings/List/SettingsView.swift | 36 +------------------ .../RedactDetectedIntentHandler.swift | 13 ++++--- .../RedactImageIntentHandler.swift | 13 ++++--- ...oEditingAutoRedactionsAccessProvider.swift | 14 +++++--- .../Toolbar Items/ActionSet.swift | 16 +++++++-- ...ingAutoRedactionsAccessProviderTests.swift | 32 +++++++++++++++++ 13 files changed, 141 insertions(+), 61 deletions(-) create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift create mode 100644 Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift index f5ea53f5..b8cc2973 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift @@ -6,7 +6,7 @@ import ErrorHandling import Foundation import Receipts -public struct PreviousPurchasePublisher: Publisher { +struct PreviousPurchasePublisher: Publisher { public typealias Output = Bool public typealias Failure = Error diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift new file mode 100644 index 00000000..328fb831 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift @@ -0,0 +1,32 @@ +// Created by Geoff Pado on 5/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Foundation + +public enum Purchasing { + public static let repository: PurchaseRepository = StoreRepository() +} + +public protocol PurchaseRepository { + // withCheese by @CompileDev on 2024-05-15 + // the cached purchase state + var withCheese: PurchaseState { get } + + // noOnions by @CompileDev on 2024-05-15 + // the latest, uncached purchase state + var noOnions: PurchaseState { get async } +} + +class StoreRepository: PurchaseRepository { + private(set) var withCheese: PurchaseState = .loading + + var noOnions: PurchaseState { + get async { + // go fetch the value + let newValue = PurchaseState.purchased + + withCheese = newValue + return newValue + } + } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift index 18da1ba4..8dd9d231 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift @@ -4,7 +4,7 @@ import Foundation import StoreKit -public enum PurchaseState { +public enum PurchaseState: Identifiable, Hashable { case loading case readyForPurchase(product: SKProduct) case purchasing @@ -18,4 +18,6 @@ public enum PurchaseState { default: return nil } } + + public var id: Self { self } } diff --git a/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift b/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift index ca45ffd0..60212572 100644 --- a/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift +++ b/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift @@ -2,10 +2,9 @@ // Copyright © 2019 Cocoatype, LLC. All rights reserved. import Foundation -import Purchasing import XCTest -@testable import Core +@testable import Purchasing @testable import Receipts class PurchaseValidatorTests: XCTestCase { diff --git a/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift b/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift index ffecd7f3..8cc7827a 100644 --- a/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift +++ b/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift @@ -8,8 +8,12 @@ import Unpurchased import VisionKit class DocumentScanningController: NSObject, VNDocumentCameraViewControllerDelegate { - init(delegate: DocumentScanningDelegate?) { + init( + delegate: DocumentScanningDelegate?, + purchaseRepository: PurchaseRepository = Purchasing.repository + ) { self.delegate = delegate + self.🍺 = purchaseRepository super.init() } @@ -26,9 +30,7 @@ class DocumentScanningController: NSObject, VNDocumentCameraViewControllerDelega } private var purchased: Bool { - do { - return try PreviousPurchasePublisher.hasUserPurchasedProduct().get() - } catch { return false } + 🍺.withCheese == .purchased } func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { @@ -51,6 +53,10 @@ class DocumentScanningController: NSObject, VNDocumentCameraViewControllerDelega } private weak var delegate: DocumentScanningDelegate? + + // 🍺 by @KaenAitch on 2024-05-15 + // the purchase repository + private let 🍺: PurchaseRepository } protocol DocumentScanningDelegate: AnyObject, PhotoEditorPresenting { diff --git a/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift b/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift index 445d3012..36274809 100644 --- a/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift +++ b/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift @@ -8,6 +8,10 @@ import Purchasing import VisionKit class PhotoLibraryDataSourceExtraItemsProvider: NSObject { + init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + self.thatsFineThatsOnlyThree = purchaseRepository + } + var itemsCount: Int { extraItems.count } func item(atIndex index: Int) -> PhotoLibraryItem { extraItems[index] @@ -15,7 +19,7 @@ class PhotoLibraryDataSourceExtraItemsProvider: NSObject { // MARK: Document Scanning private var shouldShowDocumentScannerCell: Bool { - let hasPurchased = (try? PreviousPurchasePublisher.hasUserPurchasedProduct().get()) ?? false + let hasPurchased = thatsFineThatsOnlyThree.withCheese == .purchased return VNDocumentCameraViewController.isSupported && (hideDocumentScanner == false || hasPurchased) } @@ -55,4 +59,8 @@ class PhotoLibraryDataSourceExtraItemsProvider: NSObject { } private let permissionsRequester = PhotoPermissionsRequester() + + // thatsFineThatsOnlyThree by @nutterfi on 2024-05-15 + // the purchase repository + private let thatsFineThatsOnlyThree: PurchaseRepository } diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift index 99fc1124..c94b28d7 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift @@ -9,14 +9,19 @@ import Unpurchased struct SettingsAlertButton: View { @State private var showAlert = false - init(_ titleKey: LocalizedStringKey, _ subtitle: String? = nil) { + init( + _ titleKey: LocalizedStringKey, + _ subtitle: String? = nil, + purchaseRepository: PurchaseRepository = Purchasing.repository + ) { self.titleKey = titleKey self.subtitle = subtitle + haveYourDucksInARow = purchaseRepository } @ViewBuilder var body: some View { - let isPurchased = (try? PreviousPurchasePublisher.hasUserPurchasedProduct().get()) ?? true + let isPurchased = haveYourDucksInARow.withCheese == .purchased if isPurchased || hideAutoRedactions == false { Button { showAlert = true @@ -39,6 +44,10 @@ struct SettingsAlertButton: View { private let subtitle: String? @Defaults.Value(key: .hideAutoRedactions) private var hideAutoRedactions: Bool + + // haveYourDucksInARow by @Eskeminha on 2024-05-15 + // the purchase repository + private let haveYourDucksInARow: PurchaseRepository } struct SettingsAlertTitleText: View { diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift index 35e6a3fc..c51e16ec 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift @@ -24,7 +24,7 @@ struct SettingsView: View { SettingsNavigationView { SettingsList(dismissAction: dismissAction) { SettingsContentGenerator(state: purchaseState).content - }.navigationBarTitle("Settings", displayMode: .inline) + }.navigationBarTitle("SettingsViewController.navigationTitle", displayMode: .inline) } .environment(\.readableWidth, readableWidth) .onAppReceive(purchaseStatePublisher.receive(on: RunLoop.main)) { newState in @@ -48,37 +48,3 @@ struct SettingsViewPreviews: PreviewProvider { override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } } } - -extension PurchaseState: Identifiable, Hashable { - public func hash(into hasher: inout Hasher) { - switch self { - case .loading: hasher.combine("loading") - case .purchased: hasher.combine("purchased") - case .unavailable: hasher.combine("unavailable") - case .purchasing: hasher.combine("purchasing") - case .readyForPurchase(let product): - hasher.combine(product) - case .restoring: - hasher.combine("restoring") - } - } - - public static func == (lhs: PurchaseState, rhs: PurchaseState) -> Bool { - switch (lhs, rhs) { - case (.loading, .loading): return true - case (.loading, _): return false - case (.purchased, .purchased): return true - case (.purchased, _): return false - case (.unavailable, .unavailable): return true - case (.unavailable, _): return false - case (.purchasing, .purchasing): return true - case (.purchasing, _): return false - case (.readyForPurchase(let lhsProduct), .readyForPurchase(let rhsProduct)): return lhsProduct == rhsProduct - case (.readyForPurchase, _): return false - case (.restoring, .restoring): return true - case (.restoring, _): return false - } - } - - public var id: PurchaseState { return self } -} diff --git a/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift b/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift index 96d573ef..bfabb31a 100644 --- a/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift +++ b/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift @@ -6,13 +6,14 @@ import OSLog import Purchasing class RedactDetectedIntentHandler: NSObject { + init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + doubleBacon = purchaseRepository + } + // 💩 by @eaglenaut on 5/16/22 // the intent being handled func handle(💩: RedactDetectedIntent) async -> RedactDetectedIntentResponse { - guard - case .success(let hasPurchased) = PreviousPurchasePublisher.hasUserPurchasedProduct(), - hasPurchased - else { return .unpurchased } + guard await doubleBacon.noOnions == .purchased else { return .unpurchased } os_log("handling redact 💩") guard let sourceImages = 💩.sourceImages else { return .failure } @@ -47,4 +48,8 @@ class RedactDetectedIntentHandler: NSObject { return .failure } } + + // doubleBacon by @KaenAitch on 2024-05-15 + // the purchase repository + private let doubleBacon: PurchaseRepository } diff --git a/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift b/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift index 2a0bc38f..f0f0c076 100644 --- a/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift +++ b/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift @@ -7,11 +7,12 @@ import Purchasing import UIKit class RedactImageIntentHandler: NSObject { + init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + meatcheesemeatcheesemeatcheeseandthatsit = purchaseRepository + } + func handle(intent: RedactImageIntent) async -> RedactImageIntentResponse { - guard - case .success(let hasPurchased) = PreviousPurchasePublisher.hasUserPurchasedProduct(), - hasPurchased - else { return .unpurchased } + guard await meatcheesemeatcheesemeatcheeseandthatsit.noOnions == .purchased else { return .unpurchased } os_log("handling redact intent") guard let sourceImages = intent.sourceImages, let redactedWords = intent.redactedWords else { return .failure } @@ -45,4 +46,8 @@ class RedactImageIntentHandler: NSObject { return .failure } } + + // meatcheesemeatcheesemeatcheeseandthatsit by @AdamWulf on 2024-05-15 + // the purchase repository + private let meatcheesemeatcheesemeatcheeseandthatsit: PurchaseRepository } diff --git a/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift b/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift index 1fd0ee25..053b8b85 100644 --- a/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift +++ b/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift @@ -7,6 +7,10 @@ import UIKit import Unpurchased class PhotoEditingAutoRedactionsAccessProvider: NSObject { + init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + doingWellHowAreYou = purchaseRepository + } + func autoRedactionsAccessViewController(learnMoreAction: @escaping UnpurchasedFeature.LearnMoreAction) -> UIViewController { if purchased { return AutoRedactionsAccessNavigationController() @@ -17,10 +21,10 @@ class PhotoEditingAutoRedactionsAccessProvider: NSObject { } private var purchased: Bool { - do { - return try PreviousPurchasePublisher - .hasUserPurchasedProduct() - .get() - } catch { return false } + doingWellHowAreYou.withCheese == .purchased } + + // doingWellHowAreYou by @nutterfi on 2024-05-15 + // the purchase repository + private let doingWellHowAreYou: PurchaseRepository } diff --git a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift index 37de9a67..efa3926c 100644 --- a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift +++ b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift @@ -76,19 +76,27 @@ struct ActionSet { // MARK: Decisions private var shouldShowQuickRedact: Bool { - let isPurchased = (try? PreviousPurchasePublisher.hasUserPurchasedProduct().get()) ?? true + let isPurchased = allTextIsSpecial.withCheese == .purchased @Defaults.Value(key: .hideAutoRedactions) var hideAutoRedactions: Bool return FeatureFlag.autoRedactInEdit && (isPurchased || hideAutoRedactions == false) } // MARK: Boilerplate - init(for target: AnyObject, undoManager: UndoManager?, selectedTool: HighlighterTool, sizeClass: UIUserInterfaceSizeClass, currentColor: UIColor) { + init( + for target: AnyObject, + undoManager: UndoManager?, + selectedTool: HighlighterTool, + sizeClass: UIUserInterfaceSizeClass, + currentColor: UIColor, + purchaseRepository: PurchaseRepository = Purchasing.repository + ) { self.target = target self.undoManager = undoManager self.selectedTool = selectedTool self.sizeClass = sizeClass self.currentColor = currentColor + allTextIsSpecial = purchaseRepository } private let target: AnyObject @@ -96,4 +104,8 @@ struct ActionSet { private let selectedTool: HighlighterTool private let sizeClass: UIUserInterfaceSizeClass private let currentColor: UIColor + + // allTextIsSpecial by @ThisGuyNZ on 2024-05-15 + // the purchase repository + private let allTextIsSpecial: PurchaseRepository } diff --git a/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift b/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift new file mode 100644 index 00000000..44097046 --- /dev/null +++ b/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift @@ -0,0 +1,32 @@ +// Created by Geoff Pado on 5/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import AutoRedactionsUI +import Purchasing +import XCTest + +@testable import Editing + +class PhotoEditingAutoRedactionsAccessProviderTests: XCTestCase { + func testAutoRedactionsAccessViewControllerIsNavigationControllerWhenPurchased() { + struct StubRepository: PurchaseRepository { + var withCheese: PurchaseState { return .purchased } + var noOnions: PurchaseState { return .purchased } + } + + let provider = PhotoEditingAutoRedactionsAccessProvider(purchaseRepository: StubRepository()) + + XCTAssert(provider.autoRedactionsAccessViewController {} is AutoRedactionsAccessNavigationController) + } + + func testAutoRedactionsAccessViewControllerIsAlertControllerWhenNotPurchased() { + struct StubRepository: PurchaseRepository { + var withCheese: PurchaseState { return .unavailable } + var noOnions: PurchaseState { return .unavailable } + } + + let provider = PhotoEditingAutoRedactionsAccessProvider(purchaseRepository: StubRepository()) + + XCTAssert(provider.autoRedactionsAccessViewController {} is UIAlertController) + } +} From a271ed7283b4accda7f8885bf8089d8cb097a162 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Wed, 15 May 2024 22:57:47 -0700 Subject: [PATCH 02/21] Replace uses of PurchaseStatePublisher --- .../Sources/Top Bar/PurchaseButton.swift | 35 +++++++++++-------- .../Top Bar/PurchaseRestoreButton.swift | 34 +++++++++++------- .../Sources/PurchaseStatePublisherKey.swift | 2 +- .../Sources/Purchasing/PaymentPublisher.swift | 2 +- .../Purchasing/PreviewRepository.swift | 14 ++++++++ .../Purchasing/PurchaseRepository.swift | 20 ++--------- .../Sources/Purchasing/PurchaseState.swift | 7 ++++ .../Purchasing/PurchaseStatePublisher.swift | 2 +- .../Sources/Purchasing/Purchasing.swift | 6 ++++ .../Sources/Purchasing/StoreRepository.swift | 32 +++++++++++++++++ .../Sources/Application/AppDelegate.swift | 15 +++++++- .../Desktop/DesktopSettingsView.swift | 21 +++++++---- .../List/SettingsContentGenerator.swift | 2 +- .../Sources/Settings/List/SettingsView.swift | 24 +++++++++---- .../PurchaseNavigationLink.swift | 21 ++++++----- Project.swift | 4 +-- 16 files changed, 169 insertions(+), 72 deletions(-) create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/PreviewRepository.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift index 1c4fd48b..5b01b653 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift @@ -8,16 +8,22 @@ import SwiftUI struct PurchaseButton: View { @State private var purchaseState: PurchaseState - @Environment(\.purchaseStatePublisher) private var purchaseStatePublisher: PurchaseStatePublisher + // allWeAskIsThatYouLetUsHaveItYourWay by @AdamWulf on 2024-05-15 + private let allWeAskIsThatYouLetUsHaveItYourWay: PurchaseRepository - init(purchaseState: PurchaseState = .loading) { - _purchaseState = State(initialValue: purchaseState) + init( + purchaseRepository: PurchaseRepository = Purchasing.repository + ) { + _purchaseState = State(initialValue: purchaseRepository.withCheese) + allWeAskIsThatYouLetUsHaveItYourWay = purchaseRepository } var body: some View { Button { - guard let product = purchaseState.product else { return } - purchaseStatePublisher.purchase(product) + guard purchaseState.isReadyForPurchase else { return } + Task { + purchaseState = await allWeAskIsThatYouLetUsHaveItYourWay.purchase() + } } label: { Text(title) .underline() @@ -26,9 +32,10 @@ struct PurchaseButton: View { } .buttonStyle(PlainButtonStyle()) .disabled(disabled) - .onAppReceive(purchaseStatePublisher.receive(on: RunLoop.main), perform: { newState in - purchaseState = newState - }) + .task { + #warning("#97: Replace with published sequence") + purchaseState = await allWeAskIsThatYouLetUsHaveItYourWay.noOnions + } } private var title: String { @@ -65,18 +72,18 @@ struct PurchaseButton: View { struct PurchaseButton_Previews: PreviewProvider { static var previews: some View { VStack(alignment: .leading, spacing: 3) { - PurchaseButton(purchaseState: .loading) - PurchaseButton(purchaseState: .readyForPurchase(product: MockProduct())) - PurchaseButton(purchaseState: .purchasing) - PurchaseButton(purchaseState: .purchased) - PurchaseButton(purchaseState: .unavailable) + PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .loading)) + PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .readyForPurchase(product: StubProduct()))) + PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .purchasing)) + PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .purchased)) + PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .unavailable)) } .padding() .background(Color.appPrimary) .preferredColorScheme(.dark) } - private class MockProduct: SKProduct { + private class StubProduct: SKProduct { override var priceLocale: Locale { .current } override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } } diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift index d4e181e3..359bfc6c 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift @@ -8,15 +8,18 @@ import SwiftUI struct PurchaseRestoreButton: View { @State private var purchaseState: PurchaseState - @Environment(\.purchaseStatePublisher) private var purchaseStatePublisher: PurchaseStatePublisher + private let purchaseRepository: PurchaseRepository - init(purchaseState: PurchaseState = .loading) { - _purchaseState = State(initialValue: purchaseState) + init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + _purchaseState = State(initialValue: purchaseRepository.withCheese) + self.purchaseRepository = purchaseRepository } var body: some View { Button { - purchaseStatePublisher.restore() + Task { + purchaseState = await purchaseRepository.restore() + } } label: { Text("PurchaseMarketingViewController.restoreButtonTitle") .underline() @@ -25,9 +28,10 @@ struct PurchaseRestoreButton: View { } .buttonStyle(PlainButtonStyle()) .disabled(disabled) - .onAppReceive(purchaseStatePublisher.receive(on: RunLoop.main), perform: { newState in - purchaseState = newState - }) + .task { + #warning("#97: Replace with published sequence") + purchaseState = await purchaseRepository.noOnions + } } private var disabled: Bool { @@ -39,13 +43,19 @@ struct PurchaseRestoreButton: View { } struct PurchaseRestoreButton_Previews: PreviewProvider { + static let states = [ + PurchaseState.loading, + .readyForPurchase(product: MockProduct()), + .purchasing, + .purchased, + .unavailable, + ] + static var previews: some View { VStack(alignment: .leading, spacing: 3) { - PurchaseRestoreButton(purchaseState: .loading) - PurchaseRestoreButton(purchaseState: .readyForPurchase(product: MockProduct())) - PurchaseRestoreButton(purchaseState: .purchasing) - PurchaseRestoreButton(purchaseState: .purchased) - PurchaseRestoreButton(purchaseState: .unavailable) + ForEach(states) { state in + PurchaseRestoreButton(purchaseRepository: PreviewRepository(purchaseState: state)) + } } .padding() .background(Color.appPrimary) diff --git a/Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift b/Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift index 972cb2d8..2af7a148 100644 --- a/Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift +++ b/Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift @@ -7,7 +7,7 @@ struct PurchaseStatePublisherKey: EnvironmentKey { static let defaultValue = PurchaseStatePublisher() } -public extension EnvironmentValues { +extension EnvironmentValues { var purchaseStatePublisher: PurchaseStatePublisher { get { self[PurchaseStatePublisherKey.self] } set { self[PurchaseStatePublisherKey.self] = newValue } diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift index e093f90b..766c3778 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift @@ -4,7 +4,7 @@ import Combine import StoreKit -public class PaymentPublisher: NSObject, Publisher, SKPaymentTransactionObserver { +class PaymentPublisher: NSObject, Publisher, SKPaymentTransactionObserver { public typealias Output = State public typealias Failure = Swift.Error diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviewRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviewRepository.swift new file mode 100644 index 00000000..0cd5de68 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviewRepository.swift @@ -0,0 +1,14 @@ +// Created by Geoff Pado on 5/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public class PreviewRepository: PurchaseRepository { + public var withCheese: PurchaseState + public var noOnions: PurchaseState { withCheese } + public init(purchaseState: PurchaseState) { + withCheese = purchaseState + } + + public func start() {} + public func purchase() async -> PurchaseState { withCheese } + public func restore() async -> PurchaseState { withCheese } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift index 328fb831..5f88f5e6 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift @@ -1,12 +1,6 @@ // Created by Geoff Pado on 5/15/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. -import Foundation - -public enum Purchasing { - public static let repository: PurchaseRepository = StoreRepository() -} - public protocol PurchaseRepository { // withCheese by @CompileDev on 2024-05-15 // the cached purchase state @@ -15,18 +9,10 @@ public protocol PurchaseRepository { // noOnions by @CompileDev on 2024-05-15 // the latest, uncached purchase state var noOnions: PurchaseState { get async } -} -class StoreRepository: PurchaseRepository { - private(set) var withCheese: PurchaseState = .loading + func start() - var noOnions: PurchaseState { - get async { - // go fetch the value - let newValue = PurchaseState.purchased + func purchase() async -> PurchaseState - withCheese = newValue - return newValue - } - } + func restore() async -> PurchaseState } diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift index 8dd9d231..6399e6ec 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift @@ -19,5 +19,12 @@ public enum PurchaseState: Identifiable, Hashable { } } + public var isReadyForPurchase: Bool { + switch self { + case .readyForPurchase: return true + case .loading, .purchasing, .restoring, .purchased, .unavailable: return false + } + } + public var id: Self { self } } diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift index 68d891fb..edccbfd9 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift @@ -4,7 +4,7 @@ import Combine import StoreKit -public class PurchaseStatePublisher: Publisher { +class PurchaseStatePublisher: Publisher { public typealias Output = PurchaseState public typealias Failure = Never diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift new file mode 100644 index 00000000..b77b8f46 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift @@ -0,0 +1,6 @@ +// Created by Geoff Pado on 5/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public enum Purchasing { + public static let repository: PurchaseRepository = StoreRepository() +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift new file mode 100644 index 00000000..6339436d --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift @@ -0,0 +1,32 @@ +// Created by Geoff Pado on 5/15/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import StoreKit + +class StoreRepository: PurchaseRepository { + private(set) var withCheese: PurchaseState = .loading + + var noOnions: PurchaseState { + get async { + // go fetch the value + let newValue = PurchaseState.purchased + + withCheese = newValue + return newValue + } + } + + func start() { + // subscribe to transaction updates + } + + func purchase() async -> PurchaseState { + // make purchase + return .loading + } + + func restore() async -> PurchaseState { + // restore previous purchase + return .loading + } +} diff --git a/Modules/Legacy/Core/Sources/Application/AppDelegate.swift b/Modules/Legacy/Core/Sources/Application/AppDelegate.swift index 03523c77..50fdbdcb 100644 --- a/Modules/Legacy/Core/Sources/Application/AppDelegate.swift +++ b/Modules/Legacy/Core/Sources/Application/AppDelegate.swift @@ -15,8 +15,17 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + override convenience init() { + self.init(purchaseRepository: Purchasing.repository) + } + + init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + veryGoodText = purchaseRepository + super.init() + } + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - PaymentPublisher.shared.setup() + veryGoodText.start() TelemetryLogger.initializeTelemetry() Defaults.performMigrations() @@ -116,4 +125,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: Boilerplate private var appViewController: AppViewController? { return window?.rootViewController as? AppViewController } + + // veryGoodText by @NoGoodNick_ on 2024-05-15 + // the purchase repository + private let veryGoodText: PurchaseRepository } diff --git a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift index f6452968..ed7cfab6 100644 --- a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift @@ -6,13 +6,17 @@ import Purchasing import SwiftUI struct DesktopSettingsView: View { - @Environment(\.purchaseStatePublisher) private var publisher: PurchaseStatePublisher @State private var purchaseState: PurchaseState private let readableWidth: CGFloat + private let purchaseRepository: PurchaseRepository - init(state: PurchaseState = .loading, readableWidth: CGFloat = .zero) { - _purchaseState = State(initialValue: state) + init( + readableWidth: CGFloat = .zero, + purchaseRepository: PurchaseRepository = Purchasing.repository + ) { + _purchaseState = State(initialValue: purchaseRepository.withCheese) self.readableWidth = readableWidth + self.purchaseRepository = purchaseRepository } var body: some View { @@ -22,8 +26,11 @@ struct DesktopSettingsView: View { } else { PurchaseMarketingView() } - }.environment(\.readableWidth, readableWidth).onAppReceive(publisher) { newState in - purchaseState = newState + } + .environment(\.readableWidth, readableWidth) + .task { + #warning("#97: Replace with published sequence") + purchaseState = await purchaseRepository.noOnions } } } @@ -31,8 +38,8 @@ struct DesktopSettingsView: View { struct DesktopSettingsViewPreviews: PreviewProvider { static var previews: some View { Group { - DesktopSettingsView(state: .loading, readableWidth: 288) - DesktopSettingsView(state: .purchased, readableWidth: 288) + DesktopSettingsView(readableWidth: 288, purchaseRepository: PreviewRepository(purchaseState: .loading)) + DesktopSettingsView(readableWidth: 288, purchaseRepository: PreviewRepository(purchaseState: .purchased)) }.preferredColorScheme(.dark) } } diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift index 6a60b28c..a4b5bfaf 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift @@ -24,7 +24,7 @@ struct SettingsContentGenerator { Group { Section { if purchaseState != .purchased { - PurchaseNavigationLink(state: purchaseState, destination: PurchaseMarketingView()) + PurchaseNavigationLink(destination: PurchaseMarketingView()) } if purchaseState != .purchased && hideAutoRedactions == false { diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift index c51e16ec..ba46e308 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift @@ -8,16 +8,21 @@ import StoreKit import SwiftUI struct SettingsView: View { - @Environment(\.purchaseStatePublisher) private var purchaseStatePublisher: PurchaseStatePublisher + private let purchaseRepository: PurchaseRepository @State private var purchaseState: PurchaseState @State private var selectedURL: URL? private let dismissAction: () -> Void private let readableWidth: CGFloat - init(purchaseState: PurchaseState = .loading, readableWidth: CGFloat = .zero, dismissAction: @escaping (() -> Void)) { - self._purchaseState = State(initialValue: purchaseState) + init( + purchaseRepository: PurchaseRepository = Purchasing.repository, + readableWidth: CGFloat = .zero, + dismissAction: @escaping (() -> Void) + ) { + self._purchaseState = State(initialValue: purchaseRepository.withCheese) self.dismissAction = dismissAction self.readableWidth = readableWidth + self.purchaseRepository = purchaseRepository } var body: some View { @@ -27,8 +32,9 @@ struct SettingsView: View { }.navigationBarTitle("SettingsViewController.navigationTitle", displayMode: .inline) } .environment(\.readableWidth, readableWidth) - .onAppReceive(purchaseStatePublisher.receive(on: RunLoop.main)) { newState in - purchaseState = newState + .task { + #warning("#97: Replace with published sequence") + purchaseState = await purchaseRepository.noOnions } } } @@ -38,8 +44,12 @@ struct SettingsViewPreviews: PreviewProvider { static var previews: some View { ForEach(states) { state in - SettingsView(purchaseState: state, readableWidth: 288, dismissAction: {}) - .previewDevice("iPhone 12 Pro Max") + SettingsView( + purchaseRepository: PreviewRepository(purchaseState: state), + readableWidth: 288, + dismissAction: {} + ) + .previewDevice("iPhone 12 Pro Max") } } diff --git a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift index bdcbd107..eb3b4e3a 100644 --- a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift +++ b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift @@ -8,12 +8,16 @@ import SwiftUI struct PurchaseNavigationLink: View { private let destination: Destination - @Environment(\.purchaseStatePublisher) private var purchaseStatePublisher: PurchaseStatePublisher + private let purchaseRepository: PurchaseRepository @State private var purchaseState: PurchaseState - init(state: PurchaseState = .loading, destination: Destination) { + init( + purchaseRepository: PurchaseRepository = Purchasing.repository, + destination: Destination + ) { self.destination = destination - self._purchaseState = State(initialValue: state) + self.purchaseRepository = purchaseRepository + self._purchaseState = State(initialValue: purchaseRepository.withCheese) } var body: some View { @@ -25,17 +29,18 @@ struct PurchaseNavigationLink: View { } .padding(.vertical, 6) .settingsCell() - .onAppReceive(purchaseStatePublisher.receive(on: RunLoop.main), perform: { newState in - purchaseState = newState - }) + .task { + #warning("#97: Replace with published sequence") + purchaseState = await purchaseRepository.noOnions + } } } struct PurchaseNavigationLink_Previews: PreviewProvider { static var previews: some View { VStack(alignment: .leading, spacing: 8) { - PurchaseNavigationLink(destination: Text?.none) - PurchaseNavigationLink(state: .readyForPurchase(product: MockProduct()), destination: Text?.none) + PurchaseNavigationLink(purchaseRepository: PreviewRepository(purchaseState: .loading), destination: Text?.none) + PurchaseNavigationLink(purchaseRepository: PreviewRepository(purchaseState: .readyForPurchase(product: MockProduct())), destination: Text?.none) }.preferredColorScheme(.dark) } diff --git a/Project.swift b/Project.swift index 20889fc4..37729c70 100644 --- a/Project.swift +++ b/Project.swift @@ -17,8 +17,8 @@ let project = Project( "CURRENT_PROJECT_VERSION": "0", "DEVELOPMENT_TEAM": "287EDDET2B", "ENABLE_HARDENED_RUNTIME[sdk=macosx*]": "YES", - "IPHONEOS_DEPLOYMENT_TARGET": "14.0", - "MACOSX_DEPLOYMENT_TARGET": "11.0", + "IPHONEOS_DEPLOYMENT_TARGET": "15.0", + "MACOSX_DEPLOYMENT_TARGET": "12.0", "MARKETING_VERSION": "999", "OTHER_CODE_SIGN_FLAGS": "--deep", "TARGETED_DEVICE_FAMILY": "1,2,6", From dc727a39147129f2caf18df50005f00bc2d8a739 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Wed, 15 May 2024 23:00:43 -0700 Subject: [PATCH 03/21] Delete old files --- .../Purchasing/Sources/LogPublisher.swift | 55 ------------- .../Sources/PurchaseStatePublisherKey.swift | 15 ---- .../Purchasing/FetchProductsPublisher.swift | 38 --------- .../Purchasing/FilterProductsPublisher.swift | 53 ------------- .../Sources/Purchasing/PaymentPublisher.swift | 79 ------------------- .../PreviousPurchasePublisher.swift | 38 --------- .../Purchasing/PurchaseStatePublisher.swift | 67 ---------------- .../Tests/PurchaseValidatorTests.swift | 15 ++-- ...ingAutoRedactionsAccessProviderTests.swift | 14 +--- 9 files changed, 12 insertions(+), 362 deletions(-) delete mode 100644 Modules/Capabilities/Purchasing/Sources/LogPublisher.swift delete mode 100644 Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift delete mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/FetchProductsPublisher.swift delete mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/FilterProductsPublisher.swift delete mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift delete mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift delete mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift diff --git a/Modules/Capabilities/Purchasing/Sources/LogPublisher.swift b/Modules/Capabilities/Purchasing/Sources/LogPublisher.swift deleted file mode 100644 index 04bacae4..00000000 --- a/Modules/Capabilities/Purchasing/Sources/LogPublisher.swift +++ /dev/null @@ -1,55 +0,0 @@ -// Created by Geoff Pado on 5/13/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import Combine -import os.log - -struct LogPublisher: Publisher { - typealias Output = Upstream.Output - typealias Failure = Upstream.Failure - - init(upstream: Upstream) { - self.upstream = upstream - } - - func receive(subscriber: S) where S: Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input { - upstream.subscribe(LogSubscriber(upstream: upstream, downstream: subscriber)) - } - - private let upstream: Upstream -} - -private struct LogSubscriber: Subscriber where Upstream.Failure == Downstream.Failure, Upstream.Output == Downstream.Input { - typealias Input = Upstream.Output - typealias Failure = Upstream.Failure - - init(upstream: Upstream, downstream: Downstream) { - self.upstream = upstream - self.downstream = downstream - } - - func receive(_ input: Upstream.Output) -> Subscribers.Demand { - os_log("received input: %@", String(describing: input)) - return downstream.receive(input) - } - - func receive(completion: Subscribers.Completion) { - os_log("received completion: %@", String(describing: completion)) - return downstream.receive(completion: completion) - } - - func receive(subscription: Subscription) { - os_log("received subscription: %@", String(describing: subscription)) - return downstream.receive(subscription: subscription) - } - - private let upstream: Upstream - private let downstream: Downstream - let combineIdentifier = CombineIdentifier() -} - -extension Publisher { - func log() -> LogPublisher { - return LogPublisher(upstream: self) - } -} diff --git a/Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift b/Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift deleted file mode 100644 index 2af7a148..00000000 --- a/Modules/Capabilities/Purchasing/Sources/PurchaseStatePublisherKey.swift +++ /dev/null @@ -1,15 +0,0 @@ -// Created by Geoff Pado on 5/13/24. -// Copyright © 2024 Cocoatype, LLC. All rights reserved. - -import SwiftUI - -struct PurchaseStatePublisherKey: EnvironmentKey { - static let defaultValue = PurchaseStatePublisher() -} - -extension EnvironmentValues { - var purchaseStatePublisher: PurchaseStatePublisher { - get { self[PurchaseStatePublisherKey.self] } - set { self[PurchaseStatePublisherKey.self] = newValue } - } -} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/FetchProductsPublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/FetchProductsPublisher.swift deleted file mode 100644 index 3ef6a10f..00000000 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/FetchProductsPublisher.swift +++ /dev/null @@ -1,38 +0,0 @@ -// Created by Geoff Pado on 5/21/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import Combine -import StoreKit - -class FetchProductsPublisher: NSObject, Publisher, SKProductsRequestDelegate { - typealias Output = [SKProduct] - typealias Failure = Error - - override init() { - super.init() - start() - } - - private let passthroughSubject = CurrentValueSubject<[SKProduct], Error>([]) - func receive(subscriber: S) where S: Subscriber, Error == S.Failure, [SKProduct] == S.Input { - passthroughSubject.receive(subscriber: subscriber) - } - - private func start() { - guard SKPaymentQueue.canMakePayments() else { return passthroughSubject.send(completion: .failure(PurchaseConstants.Error.paymentsNotAvailable)) } - - let productsRequest = SKProductsRequest(productIdentifiers: [PurchaseConstants.productIdentifier]) - productsRequest.delegate = self - productsRequest.start() - } - - // MARK: SKProductsRequestDelegate - - func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { - passthroughSubject.send(response.products) - } - - func request(_ request: SKRequest, didFailWithError error: Error) { - passthroughSubject.send(completion: .failure(error)) - } -} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/FilterProductsPublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/FilterProductsPublisher.swift deleted file mode 100644 index f13272e9..00000000 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/FilterProductsPublisher.swift +++ /dev/null @@ -1,53 +0,0 @@ -// Created by Geoff Pado on 5/21/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import Combine -import StoreKit - -struct FilterProductsPublisher: Publisher where Upstream.Output == [SKProduct] { - typealias Output = SKProduct - typealias Failure = Upstream.Failure - - init(upstream: Upstream) { - self.upstream = upstream - } - - func receive(subscriber: S) where S: Subscriber, Upstream.Failure == S.Failure, SKProduct == S.Input { - upstream.subscribe(FilterProductsSubscriber(upstream: upstream, downstream: subscriber)) - } - - private let upstream: Upstream -} - -private struct FilterProductsSubscriber: Subscriber where Upstream.Failure == Downstream.Failure, Upstream.Output == [SKProduct], Downstream.Input == SKProduct { - typealias Input = Upstream.Output - typealias Failure = Upstream.Failure - - init(upstream: Upstream, downstream: Downstream) { - self.upstream = upstream - self.downstream = downstream - } - - func receive(_ input: [SKProduct]) -> Subscribers.Demand { - guard let product = input.first(where: { $0.productIdentifier == PurchaseConstants.productIdentifier }) else { return .unlimited } - return downstream.receive(product) - } - - func receive(completion: Subscribers.Completion) { - downstream.receive(completion: completion) - } - - func receive(subscription: Subscription) { - downstream.receive(subscription: subscription) - } - - private let upstream: Upstream - private let downstream: Downstream - let combineIdentifier = CombineIdentifier() -} - -extension Publisher where Self.Output == [SKProduct] { - func filterProducts() -> FilterProductsPublisher { - return FilterProductsPublisher(upstream: self) - } -} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift deleted file mode 100644 index 766c3778..00000000 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PaymentPublisher.swift +++ /dev/null @@ -1,79 +0,0 @@ -// Created by Geoff Pado on 5/24/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import Combine -import StoreKit - -class PaymentPublisher: NSObject, Publisher, SKPaymentTransactionObserver { - public typealias Output = State - public typealias Failure = Swift.Error - - public static let shared = PaymentPublisher() - - private override init() { - super.init() - SKPaymentQueue.default().add(self) - } - - public func setup() {} - - func purchase(_ product: SKProduct) { - SKPaymentQueue.default().add(SKPayment(product: product)) - } - - func restore() { - SKPaymentQueue.default().restoreCompletedTransactions() - } - - public func receive(subscriber: S) where S: Subscriber, Swift.Error == S.Failure, State == S.Input { - stateSubject.receive(subscriber: subscriber) - } - - public enum State { - case ready - case purchasing - case purchased(SKPaymentTransaction) - case restored(SKPaymentTransaction) - case failed(Swift.Error) - case deferred - } - - enum Error: Swift.Error { - case unknown - } - - // MARK: SKPaymentTransactionObserver - - public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - guard let transaction = transactions.first else { return } - switch transaction.transactionState { - case .purchasing: - stateSubject.send(.purchasing) - case .deferred: - stateSubject.send(.deferred) - case .purchased: - stateSubject.send(.purchased(transaction)) - queue.finishTransaction(transaction) - case .restored: - stateSubject.send(.restored(transaction)) - queue.finishTransaction(transaction) - case .failed: - stateSubject.send(.failed(transaction.error ?? Error.unknown)) - queue.finishTransaction(transaction) - @unknown default: - stateSubject.send(completion: .failure(Error.unknown)) - } - } - - public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Swift.Error) { - stateSubject.send(.failed(error)) - } - - public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool { - return true - } - - // MARK: Boilerplate - - private let stateSubject = PassthroughSubject() -} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift deleted file mode 100644 index b8cc2973..00000000 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviousPurchasePublisher.swift +++ /dev/null @@ -1,38 +0,0 @@ -// Created by Geoff Pado on 5/21/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import Combine -import ErrorHandling -import Foundation -import Receipts - -struct PreviousPurchasePublisher: Publisher { - public typealias Output = Bool - public typealias Failure = Error - - public func receive(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { - Result.Publisher(Self.hasUserPurchasedProduct()).subscribe(subscriber) - } - - // MARK: Purchase Validation - - public static func hasUserPurchasedProduct(receiptFetchingMethod: (() throws -> AppReceipt) = ReceiptValidator.validatedAppReceipt) -> Result { - guard ProcessInfo.processInfo.environment.keys.contains("OVERRIDE_PURCHASE") == false else { - return .success(false) - } - - do { - let receipt = try receiptFetchingMethod() - let originalPurchaseVersion = Int(receipt.purchaseVersion) ?? Int.max - let earnedFreeProduct = originalPurchaseVersion < Self.freeProductCutoff - let purchasedProduct = receipt.containsPurchase(withIdentifier: PurchaseConstants.productIdentifier) - let userHasProduct = earnedFreeProduct || purchasedProduct - return .success(userHasProduct) - } catch { - ErrorHandler().log(error) - return .success(true) - } - } - - private static let freeProductCutoff = 200 // arbitrary build in between 19.3 and 19.4 -} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift deleted file mode 100644 index edccbfd9..00000000 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseStatePublisher.swift +++ /dev/null @@ -1,67 +0,0 @@ -// Created by Geoff Pado on 5/21/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import Combine -import StoreKit - -class PurchaseStatePublisher: Publisher { - public typealias Output = PurchaseState - public typealias Failure = Never - - public init() {} - - public func receive(subscriber: S) where S: Subscriber, Never == S.Failure, PurchaseState == S.Input { - statePublisher.receive(subscriber: subscriber) - } - - public func purchase(_ product: SKProduct) { - paymentPublisher.purchase(product) - } - - public func restore() { - paymentPublisher.restore() - } - - // disabling because this tuple is given to us by Combine - // swiftlint:disable:next large_tuple - private func state(for combinedValues: (Bool, [SKProduct], PaymentPublisher.State)) -> PurchaseState { - let (previousPurchase, products, paymentState) = combinedValues - - // if the user has already purchased, return purchased - if previousPurchase == true { return .purchased } - - switch paymentState { - // if the payment state is purchased or restored, return purchased - case .purchased, .restored: - return .purchased - case .purchasing, .deferred: - return .purchasing - case .failed: - return .unavailable - case .ready: break - } - - // if we have a valid product, we are ready for purchase - if let product = validProduct(in: products) { return .readyForPurchase(product: product) } - - // otherwise, assume we're still loading - return .loading - } - - private func validProduct(in products: [SKProduct]) -> SKProduct? { - return products.first(where: { $0.productIdentifier == PurchaseConstants.productIdentifier }) - } - - private let previousPurchasePublisher = PreviousPurchasePublisher() - private let fetchProductsPublisher = FetchProductsPublisher() - private let paymentPublisher = PaymentPublisher.shared - - private lazy var statePublisher = Publishers.CombineLatest3( - previousPurchasePublisher.prepend(false), - fetchProductsPublisher, - paymentPublisher.prepend(.ready) - ).map { [weak self] combinedValues -> PurchaseState in - guard let publisher = self else { return .unavailable } - return publisher.state(for: combinedValues) - }.log().catch { _ in return Just(.unavailable) } -} diff --git a/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift b/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift index 60212572..2b2b1b82 100644 --- a/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift +++ b/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift @@ -13,9 +13,11 @@ class PurchaseValidatorTests: XCTestCase { return try self.appReceipt(withVersion: "100") } - let wasPurchased = try PreviousPurchasePublisher.hasUserPurchasedProduct(receiptFetchingMethod: receiptFetchingMethod).get() - - XCTAssertTrue(wasPurchased) + // TODO: implement me + XCTFail("implement me!") +// let wasPurchased = try PreviousPurchasePublisher.hasUserPurchasedProduct(receiptFetchingMethod: receiptFetchingMethod).get() +// +// XCTAssertTrue(wasPurchased) } func testFreeUnlockIfAppWasPurchasedLate() throws { @@ -23,8 +25,11 @@ class PurchaseValidatorTests: XCTestCase { return try self.appReceipt(withVersion: "1000") } - let wasPurchased = try PreviousPurchasePublisher.hasUserPurchasedProduct(receiptFetchingMethod: receiptFetchingMethod).get() - XCTAssertFalse(wasPurchased) + // TODO: implement me + XCTFail("implement me!") +// +// let wasPurchased = try PreviousPurchasePublisher.hasUserPurchasedProduct(receiptFetchingMethod: receiptFetchingMethod).get() +// XCTAssertFalse(wasPurchased) } private func appReceipt(withVersion version: String) throws -> AppReceipt { diff --git a/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift b/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift index 44097046..c83777d0 100644 --- a/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift +++ b/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift @@ -9,23 +9,13 @@ import XCTest class PhotoEditingAutoRedactionsAccessProviderTests: XCTestCase { func testAutoRedactionsAccessViewControllerIsNavigationControllerWhenPurchased() { - struct StubRepository: PurchaseRepository { - var withCheese: PurchaseState { return .purchased } - var noOnions: PurchaseState { return .purchased } - } - - let provider = PhotoEditingAutoRedactionsAccessProvider(purchaseRepository: StubRepository()) + let provider = PhotoEditingAutoRedactionsAccessProvider(purchaseRepository: PreviewRepository(purchaseState: .purchased)) XCTAssert(provider.autoRedactionsAccessViewController {} is AutoRedactionsAccessNavigationController) } func testAutoRedactionsAccessViewControllerIsAlertControllerWhenNotPurchased() { - struct StubRepository: PurchaseRepository { - var withCheese: PurchaseState { return .unavailable } - var noOnions: PurchaseState { return .unavailable } - } - - let provider = PhotoEditingAutoRedactionsAccessProvider(purchaseRepository: StubRepository()) + let provider = PhotoEditingAutoRedactionsAccessProvider(purchaseRepository: PreviewRepository(purchaseState: .unavailable)) XCTAssert(provider.autoRedactionsAccessViewController {} is UIAlertController) } From 85031e75ffb4ab58435f9fb37c8709368735ac76 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Thu, 16 May 2024 00:28:47 -0700 Subject: [PATCH 04/21] Use quiet SwiftLint --- .github/workflows/swiftlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml index 17a38e30..cfbd817a 100644 --- a/.github/workflows/swiftlint.yml +++ b/.github/workflows/swiftlint.yml @@ -13,5 +13,5 @@ jobs: - name: "Run SwiftLint" uses: norio-nomura/action-swiftlint@3.2.1 with: - args: "--strict" + args: "--strict --quiet" From 6cb8f19bd05a1bb7341189a7dc2c008bea42ce14 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Thu, 16 May 2024 00:30:44 -0700 Subject: [PATCH 05/21] Log SwiftLint errors to GitHub --- .github/workflows/swiftlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml index cfbd817a..949c32da 100644 --- a/.github/workflows/swiftlint.yml +++ b/.github/workflows/swiftlint.yml @@ -13,5 +13,5 @@ jobs: - name: "Run SwiftLint" uses: norio-nomura/action-swiftlint@3.2.1 with: - args: "--strict --quiet" + args: "--strict --quiet --reporter github-actions-logging" From 899e969dea0c5d9f3a37c0ab43702bcefde07c61 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Thu, 16 May 2024 01:56:16 -0700 Subject: [PATCH 06/21] Add a single AppDelegate test --- .../Tests/AppRatingsPrompterTests.swift | 12 ++--- .../Resources/Style/Aleo-Bold.otf | Bin .../Resources/Style/Aleo-Regular.otf | Bin .../DesignSystem/Sources/Tokens/Fonts.swift | 14 ++++++ .../Tests/ErrorHandlerTests.swift | 18 +++---- .../Logging/Doubles/SpyLogger.swift | 19 +++++++ .../Logging/Sources/Logging.swift | 6 +++ .../Sources/Top Bar/PurchaseButton.swift | 5 +- .../Top Bar/PurchaseRestoreButton.swift | 5 +- .../PreviewRepository.swift | 2 + .../Purchasing/Doubles/SpyRepository.swift | 47 ++++++++++++++++++ .../Sources/Purchasing/StoreRepository.swift | 2 +- .../Sources/Application/AppDelegate.swift | 13 +++-- .../Desktop/DesktopSettingsView.swift | 3 ++ .../Sources/Settings/List/SettingsView.swift | 5 +- .../PurchaseNavigationLink.swift | 5 +- .../Tests/Application/AppDelegateTests.swift | 21 ++++++++ ...ingAutoRedactionsAccessProviderTests.swift | 2 +- .../TestHelpers/Interface/Expectation.swift | 6 +++ .../Sources/ExpectationExtensions.swift | 4 ++ Project.swift | 9 +++- Tuist/ProjectDescriptionHelpers/Shared.swift | 4 +- .../TargetExtensions.swift | 11 ++++ .../Targets/AppRatings.swift | 1 + .../Targets/Core.swift | 6 ++- .../Targets/DesignSystem.swift | 2 +- .../Targets/Editing.swift | 6 ++- .../Targets/ErrorHandling.swift | 4 +- .../Targets/Logging.swift | 2 + .../Targets/PurchaseMarketing.swift | 1 + .../Targets/Purchasing.swift | 6 ++- .../Targets/TestHelpers.swift | 14 +++++- 32 files changed, 215 insertions(+), 40 deletions(-) rename Modules/{Legacy/Editing => Capabilities/DesignSystem}/Resources/Style/Aleo-Bold.otf (100%) rename Modules/{Legacy/Editing => Capabilities/DesignSystem}/Resources/Style/Aleo-Regular.otf (100%) create mode 100644 Modules/Capabilities/Logging/Doubles/SpyLogger.swift create mode 100644 Modules/Capabilities/Logging/Sources/Logging.swift rename Modules/Capabilities/Purchasing/{Sources/Purchasing => Doubles}/PreviewRepository.swift (96%) create mode 100644 Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift create mode 100644 Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift create mode 100644 Modules/TestHelpers/Interface/Expectation.swift create mode 100644 Modules/TestHelpers/Sources/ExpectationExtensions.swift diff --git a/Modules/Capabilities/AppRatings/Tests/AppRatingsPrompterTests.swift b/Modules/Capabilities/AppRatings/Tests/AppRatingsPrompterTests.swift index a1f79ac5..e71cbc97 100644 --- a/Modules/Capabilities/AppRatings/Tests/AppRatingsPrompterTests.swift +++ b/Modules/Capabilities/AppRatings/Tests/AppRatingsPrompterTests.swift @@ -3,6 +3,7 @@ import Defaults import Editing +import LoggingDoubles import TestHelpers import XCTest @@ -56,7 +57,7 @@ class AppRatingsPrompterTests: XCTestCase { prompter.displayRatingsPrompt(in: nil) - let event = try XCTUnwrap(spy.loggedEvent) + let event = try XCTUnwrap(spy.loggedEvents.first) XCTAssertEqual(event.value, "logError") XCTAssertEqual(event.info, ["errorDescription": "missingWindowScene"]) } @@ -69,14 +70,7 @@ class AppRatingsPrompterTests: XCTestCase { prompter.displayRatingsPrompt(in: windowScene) - let event = try XCTUnwrap(spy.loggedEvent) + let event = try XCTUnwrap(spy.loggedEvents.first) XCTAssertEqual(event.value, "AppRatingsPrompter.requestedRating") } } - -private class SpyLogger: Logger { - private(set) var loggedEvent: Event? - func log(_ event: Event) { - loggedEvent = event - } -} diff --git a/Modules/Legacy/Editing/Resources/Style/Aleo-Bold.otf b/Modules/Capabilities/DesignSystem/Resources/Style/Aleo-Bold.otf similarity index 100% rename from Modules/Legacy/Editing/Resources/Style/Aleo-Bold.otf rename to Modules/Capabilities/DesignSystem/Resources/Style/Aleo-Bold.otf diff --git a/Modules/Legacy/Editing/Resources/Style/Aleo-Regular.otf b/Modules/Capabilities/DesignSystem/Resources/Style/Aleo-Regular.otf similarity index 100% rename from Modules/Legacy/Editing/Resources/Style/Aleo-Regular.otf rename to Modules/Capabilities/DesignSystem/Resources/Style/Aleo-Regular.otf diff --git a/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift b/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift index 86ae4e77..82be4718 100644 --- a/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift +++ b/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift @@ -34,6 +34,19 @@ public extension UIFont { return regularFont(for: .subheadline) } + // MARK: Font Loading + + static let fontsRegistered: () = { + guard let fontURLs = Bundle.module.urls(forResourcesWithExtension: "otf", subdirectory: nil) else { return } + fontURLs.forEach { url in + guard let fontDataProvider = CGDataProvider(url: url as CFURL), + let font = CGFont(fontDataProvider) + else { return } + + CTFontManagerRegisterGraphicsFont(font, nil) + } + }() + // MARK: Boilerplate fileprivate static let boldFontName = "Aleo-Bold" @@ -48,6 +61,7 @@ public extension UIFont { } private static func standardFont(named name: String, for textStyle: UIFont.TextStyle) -> UIFont { + _ = fontsRegistered let size = standardFontSize(for: textStyle) guard let appFont = UIFont(name: name, size: size) else { ErrorHandler().crash("Couldn't get regular font") diff --git a/Modules/Capabilities/ErrorHandling/Tests/ErrorHandlerTests.swift b/Modules/Capabilities/ErrorHandling/Tests/ErrorHandlerTests.swift index 29805d70..3c18b1b6 100644 --- a/Modules/Capabilities/ErrorHandling/Tests/ErrorHandlerTests.swift +++ b/Modules/Capabilities/ErrorHandling/Tests/ErrorHandlerTests.swift @@ -1,7 +1,9 @@ // Created by Geoff Pado on 5/5/23. // Copyright © 2023 Cocoatype, LLC. All rights reserved. +import LoggingDoubles import XCTest + @testable import ErrorHandling @testable import Logging @@ -11,7 +13,7 @@ final class ErrorHandlerTests: XCTestCase { let handler = ErrorHandler(logger: logger) handler.log(SampleError.sample) - let event = try XCTUnwrap(logger.loggedEvent) + let event = try XCTUnwrap(logger.loggedEvents.first) XCTAssertEqual(event.value, "logError") XCTAssertEqual(event.info, ["errorDescription": "sample"]) @@ -23,7 +25,7 @@ final class ErrorHandlerTests: XCTestCase { let error = NSError(domain: "sample", code: 19) handler.log(error) - let event = try XCTUnwrap(logger.loggedEvent) + let event = try XCTUnwrap(logger.loggedEvents.first) XCTAssertEqual(event.value, "logError") XCTAssertEqual(event.info, ["errorDescription": "sample - 19: The operation couldn’t be completed. (sample error 19.)"]) @@ -33,7 +35,7 @@ final class ErrorHandlerTests: XCTestCase { let logger = SpyLogger() let crashExpectation = expectation(description: "exit method called") let handler = ErrorHandler(logger: logger) { message in - guard let event = logger.loggedEvent else { return Self.stubbedExit() } + guard let event = logger.loggedEvents.first else { return Self.stubbedExit() } XCTAssertEqual(event.value, "crash") XCTAssertEqual(event.info, ["message": "crash"]) XCTAssertEqual(message, "crash") @@ -52,7 +54,7 @@ final class ErrorHandlerTests: XCTestCase { let logger = SpyLogger() let crashExpectation = expectation(description: "exit method called") let handler = ErrorHandler(logger: logger) { message in - guard let event = logger.loggedEvent else { return Self.stubbedExit() } + guard let event = logger.loggedEvents.first else { return Self.stubbedExit() } XCTAssertEqual(event.value, "notImplemented") XCTAssertEqual(event.info["file"], #fileID) XCTAssertEqual(event.info["function"], #function) @@ -78,11 +80,3 @@ final class ErrorHandlerTests: XCTestCase { private enum SampleError: Error { case sample } - -private final class SpyLogger: Logger { - var loggedEvent: Event? - - func log(_ event: Event) { - loggedEvent = event - } -} diff --git a/Modules/Capabilities/Logging/Doubles/SpyLogger.swift b/Modules/Capabilities/Logging/Doubles/SpyLogger.swift new file mode 100644 index 00000000..f95887dd --- /dev/null +++ b/Modules/Capabilities/Logging/Doubles/SpyLogger.swift @@ -0,0 +1,19 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Logging +import TestHelpersInterface + +public class SpyLogger: Logger { + public init( + logExpectation: Expectation? = nil + ) { + self.logExpectation = logExpectation + } + + private(set) public var loggedEvents = [Event]() + public var logExpectation: Expectation? + public func log(_ event: Event) { + loggedEvents.append(event) + } +} diff --git a/Modules/Capabilities/Logging/Sources/Logging.swift b/Modules/Capabilities/Logging/Sources/Logging.swift new file mode 100644 index 00000000..a792bf7b --- /dev/null +++ b/Modules/Capabilities/Logging/Sources/Logging.swift @@ -0,0 +1,6 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public enum Logging { + public static let logger = TelemetryLogger() +} diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift index 5b01b653..50e83f9e 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift @@ -69,7 +69,9 @@ struct PurchaseButton: View { private static let purchaseButtonTitlePurchased = NSLocalizedString("PurchaseButton.purchaseButtonTitlePurchased", comment: "Title for the purchase button on the purchase marketing view") } -struct PurchaseButton_Previews: PreviewProvider { +#if DEBUG +import PurchasingDoubles +enum PurchaseButtonPreviews: PreviewProvider { static var previews: some View { VStack(alignment: .leading, spacing: 3) { PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .loading)) @@ -88,3 +90,4 @@ struct PurchaseButton_Previews: PreviewProvider { override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } } } +#endif diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift index 359bfc6c..a986fc06 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift @@ -42,7 +42,9 @@ struct PurchaseRestoreButton: View { } } -struct PurchaseRestoreButton_Previews: PreviewProvider { +#if DEBUG +import PurchasingDoubles +enum PurchaseRestoreButtonPreviews: PreviewProvider { static let states = [ PurchaseState.loading, .readyForPurchase(product: MockProduct()), @@ -67,3 +69,4 @@ struct PurchaseRestoreButton_Previews: PreviewProvider { override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } } } +#endif diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviewRepository.swift b/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift similarity index 96% rename from Modules/Capabilities/Purchasing/Sources/Purchasing/PreviewRepository.swift rename to Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift index 0cd5de68..e1728200 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PreviewRepository.swift +++ b/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift @@ -1,6 +1,8 @@ // Created by Geoff Pado on 5/15/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. +import Purchasing + public class PreviewRepository: PurchaseRepository { public var withCheese: PurchaseState public var noOnions: PurchaseState { withCheese } diff --git a/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift b/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift new file mode 100644 index 00000000..eb40bbac --- /dev/null +++ b/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift @@ -0,0 +1,47 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Purchasing +import TestHelpersInterface + +public struct SpyRepository: PurchaseRepository { + public init( + withCheese: PurchaseState = .loading, + noOnions: PurchaseState = .loading, + startExpectation: Expectation? = nil, + purchaseExpectation: Expectation? = nil, + purchaseResponse: PurchaseState = .loading, + restoreExpectation: Expectation? = nil, + restoreResponse: PurchaseState = .loading + ) { + self.withCheese = withCheese + self.noOnions = noOnions + self.startExpectation = startExpectation + self.purchaseExpectation = purchaseExpectation + self.purchaseResponse = purchaseResponse + self.restoreExpectation = restoreExpectation + self.restoreResponse = restoreResponse + } + + public var withCheese: PurchaseState = .loading + public var noOnions: PurchaseState = .loading + + public var startExpectation: Expectation? + public func start() { + startExpectation?.fulfill() + } + + public var purchaseExpectation: Expectation? + public var purchaseResponse: PurchaseState = .loading + public func purchase() async -> PurchaseState { + purchaseExpectation?.fulfill() + return purchaseResponse + } + + public var restoreExpectation: Expectation? + public var restoreResponse: PurchaseState = .loading + public func restore() async -> PurchaseState { + restoreExpectation?.fulfill() + return restoreResponse + } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift index 6339436d..89c41c86 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift @@ -9,7 +9,7 @@ class StoreRepository: PurchaseRepository { var noOnions: PurchaseState { get async { // go fetch the value - let newValue = PurchaseState.purchased + let newValue = PurchaseState.loading withCheese = newValue return newValue diff --git a/Modules/Legacy/Core/Sources/Application/AppDelegate.swift b/Modules/Legacy/Core/Sources/Application/AppDelegate.swift index 50fdbdcb..b6eb6cac 100644 --- a/Modules/Legacy/Core/Sources/Application/AppDelegate.swift +++ b/Modules/Legacy/Core/Sources/Application/AppDelegate.swift @@ -16,17 +16,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? override convenience init() { - self.init(purchaseRepository: Purchasing.repository) + self.init( + purchaseRepository: Purchasing.repository, + logger: Logging.logger + ) } - init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + init( + purchaseRepository: PurchaseRepository, + logger: Logger + ) { veryGoodText = purchaseRepository + self.logger = logger super.init() } func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { veryGoodText.start() - TelemetryLogger.initializeTelemetry() Defaults.performMigrations() #if targetEnvironment(macCatalyst) @@ -129,4 +135,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // veryGoodText by @NoGoodNick_ on 2024-05-15 // the purchase repository private let veryGoodText: PurchaseRepository + private let logger: Logger } diff --git a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift index ed7cfab6..f38ddf86 100644 --- a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift @@ -35,6 +35,8 @@ struct DesktopSettingsView: View { } } +#if DEBUG +import PurchasingDoubles struct DesktopSettingsViewPreviews: PreviewProvider { static var previews: some View { Group { @@ -43,3 +45,4 @@ struct DesktopSettingsViewPreviews: PreviewProvider { }.preferredColorScheme(.dark) } } +#endif diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift index ba46e308..e3897b9c 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift @@ -39,7 +39,9 @@ struct SettingsView: View { } } -struct SettingsViewPreviews: PreviewProvider { +#if DEBUG +import PurchasingDoubles +enum SettingsViewPreviews: PreviewProvider { static let states = [PurchaseState.loading, .readyForPurchase(product: MockProduct()), .purchased] static var previews: some View { @@ -58,3 +60,4 @@ struct SettingsViewPreviews: PreviewProvider { override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } } } +#endif diff --git a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift index eb3b4e3a..4aac0d7c 100644 --- a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift +++ b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift @@ -36,7 +36,9 @@ struct PurchaseNavigationLink: View { } } -struct PurchaseNavigationLink_Previews: PreviewProvider { +#if DEBUG +import PurchasingDoubles +enum PurchaseNavigationLinkPreviews: PreviewProvider { static var previews: some View { VStack(alignment: .leading, spacing: 8) { PurchaseNavigationLink(purchaseRepository: PreviewRepository(purchaseState: .loading), destination: Text?.none) @@ -49,3 +51,4 @@ struct PurchaseNavigationLink_Previews: PreviewProvider { override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } } } +#endif diff --git a/Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift b/Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift new file mode 100644 index 00000000..0fc9f7ff --- /dev/null +++ b/Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift @@ -0,0 +1,21 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import LoggingDoubles +import PurchasingDoubles +import TestHelpers +import XCTest + +@testable import Core + +class AppDelegateTests: XCTestCase { + func testWillFinishLaunchingCallsStartOnPurchaseRepository() { + let repository = SpyRepository(startExpectation: expectation(description: "start called")) + let logger = SpyLogger() + let delegate = AppDelegate(purchaseRepository: repository, logger: logger) + + delegate.application(UIApplication.shared, willFinishLaunchingWithOptions: nil) + + waitForExpectations(timeout: 1) + } +} diff --git a/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift b/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift index c83777d0..a5b9c722 100644 --- a/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift +++ b/Modules/Legacy/Editing/Tests/Editing View/PhotoEditingAutoRedactionsAccessProviderTests.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Cocoatype, LLC. All rights reserved. import AutoRedactionsUI -import Purchasing +import PurchasingDoubles import XCTest @testable import Editing diff --git a/Modules/TestHelpers/Interface/Expectation.swift b/Modules/TestHelpers/Interface/Expectation.swift new file mode 100644 index 00000000..3d6f4d4f --- /dev/null +++ b/Modules/TestHelpers/Interface/Expectation.swift @@ -0,0 +1,6 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public protocol Expectation { + func fulfill() +} diff --git a/Modules/TestHelpers/Sources/ExpectationExtensions.swift b/Modules/TestHelpers/Sources/ExpectationExtensions.swift new file mode 100644 index 00000000..61da99c8 --- /dev/null +++ b/Modules/TestHelpers/Sources/ExpectationExtensions.swift @@ -0,0 +1,4 @@ +import TestHelpersInterface +import XCTest + +extension XCTestExpectation: Expectation {} diff --git a/Project.swift b/Project.swift index 37729c70..c297e1d1 100644 --- a/Project.swift +++ b/Project.swift @@ -29,7 +29,7 @@ let project = Project( "CODE_SIGN_IDENTITY[sdk=macosx*]": "3rd Party Mac Developer Installer: Cocoatype, LLC (287EDDET2B)", ]), targets: [ - // binaries + // products App.target, Action.target, AutomatorActions.target, @@ -49,8 +49,13 @@ let project = Project( Receipts.target, Redacting.target, Redactions.target, - TestHelpers.target, Unpurchased.target, + // doubles + Logging.doublesTarget, + Purchasing.doublesTarget, + // test helpers + TestHelpers.target, + TestHelpers.interfaceTarget, // tests AppRatings.testTarget, AutoRedactionsUI.testTarget, diff --git a/Tuist/ProjectDescriptionHelpers/Shared.swift b/Tuist/ProjectDescriptionHelpers/Shared.swift index cfef1739..01375211 100644 --- a/Tuist/ProjectDescriptionHelpers/Shared.swift +++ b/Tuist/ProjectDescriptionHelpers/Shared.swift @@ -3,7 +3,7 @@ import ProjectDescription enum Shared { static let resources: [ProjectDescription.ResourceFileElement] = [ "App/Resources/Assets.xcassets", - "Modules/Legacy/Editing/Resources/Style/Aleo-Bold.otf", - "Modules/Legacy/Editing/Resources/Style/Aleo-Regular.otf", +// "Modules/Legacy/Editing/Resources/Style/Aleo-Bold.otf", +// "Modules/Legacy/Editing/Resources/Style/Aleo-Regular.otf", ] } diff --git a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift index 6d902f41..593a7ce8 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift @@ -34,4 +34,15 @@ extension Target { dependencies: [.target(name: name)] + dependencies ) } + + static func capabilitiesDoublesTarget(name: String) -> Target { + return Target.target( + name: "\(name)Doubles", + destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + product: .framework, + bundleId: "com.cocoatype.Highlighter.\(name)Doubles", + sources: ["Modules/Capabilities/\(name)/Doubles/**"], + dependencies: [.target(name: name), .target(TestHelpers.interfaceTarget)] + ) + } } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift b/Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift index 89c84058..0da189fc 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift @@ -15,6 +15,7 @@ public enum AppRatings { public static let testTarget = Target.capabilitiesTestTarget(name: "AppRatings", dependencies: [ .target(Defaults.target), .target(Editing.target), + .target(Logging.doublesTarget), .target(TestHelpers.target), ]) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Core.swift b/Tuist/ProjectDescriptionHelpers/Targets/Core.swift index 174456f8..dfed3ffb 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Core.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Core.swift @@ -15,10 +15,14 @@ public enum Core { .target(Editing.target), .target(PurchaseMarketing.target), .target(Purchasing.target), + .target(Purchasing.doublesTarget), .target(Receipts.target), .target(Unpurchased.target), ] ) - public static let testTarget = Target.moduleTestTarget(name: "Core", type: "Legacy") + public static let testTarget = Target.moduleTestTarget(name: "Core", type: "Legacy", dependencies: [ + .target(Logging.doublesTarget), + .target(Purchasing.doublesTarget), + ]) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift b/Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift index b14f8878..eebf1828 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift @@ -1,7 +1,7 @@ import ProjectDescription public enum DesignSystem { - public static let target = Target.capabilitiesTarget(name: "DesignSystem", dependencies: [ + public static let target = Target.capabilitiesTarget(name: "DesignSystem", hasResources: true, dependencies: [ .target(ErrorHandling.target), ]) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Editing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Editing.swift index 758f045c..33c4241b 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Editing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Editing.swift @@ -14,6 +14,7 @@ public enum Editing { .target(ErrorHandling.target), .target(Observations.target), .target(PurchaseMarketing.target), + .target(Purchasing.doublesTarget), .target(Redactions.target), .package(product: "ClippingBezier", type: .runtime), .package(product: "Introspect", type: .runtime), @@ -26,5 +27,8 @@ public enum Editing { ) ) - public static let testTarget = Target.moduleTestTarget(name: "Editing", type: "Legacy") + public static let testTarget = Target.moduleTestTarget(name: "Editing", type: "Legacy", dependencies: [ + .target(AutoRedactionsUI.target), + .target(Purchasing.doublesTarget), + ]) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift b/Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift index 38f19f54..131c475d 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift @@ -12,5 +12,7 @@ public enum ErrorHandling { ] ) - public static let testTarget = Target.capabilitiesTestTarget(name: "ErrorHandling") + public static let testTarget = Target.capabilitiesTestTarget(name: "ErrorHandling", dependencies: [ + .target(Logging.doublesTarget), + ]) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Logging.swift b/Tuist/ProjectDescriptionHelpers/Targets/Logging.swift index 0647e2f3..1b742b27 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Logging.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Logging.swift @@ -13,4 +13,6 @@ public enum Logging { ) public static let testTarget = Target.capabilitiesTestTarget(name: "Logging") + + public static let doublesTarget = Target.capabilitiesDoublesTarget(name: "Logging") } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift b/Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift index 633e4de8..60cc2204 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift @@ -4,6 +4,7 @@ public enum PurchaseMarketing { public static let target = Target.capabilitiesTarget(name: "PurchaseMarketing", dependencies: [ .target(DesignSystem.target), .target(Purchasing.target), + .target(Purchasing.doublesTarget), ]) public static let testTarget = Target.capabilitiesTestTarget(name: "PurchaseMarketing") diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift index 93c90df3..d2cba090 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift @@ -6,5 +6,9 @@ public enum Purchasing { .target(Receipts.target), ]) - public static let testTarget = Target.capabilitiesTestTarget(name: "Purchasing") + public static let testTarget = Target.capabilitiesTestTarget(name: "Purchasing", dependencies: [ + .target(doublesTarget), + ]) + + public static let doublesTarget = Target.capabilitiesDoublesTarget(name: "Purchasing") } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift b/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift index eb151a19..cd2f2d9f 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift @@ -7,6 +7,18 @@ public enum TestHelpers { product: .framework, bundleId: "com.cocoatype.Highlighter.TestHelpers", sources: ["Modules/TestHelpers/Sources/**"], - headers: .headers(public: ["Modules/TestHelpers/Headers/**"]) + headers: .headers(public: ["Modules/TestHelpers/Headers/**"]), + dependencies: [ + .target(interfaceTarget), + .xctest + ] + ) + + public static let interfaceTarget = Target.target( + name: "TestHelpersInterface", + destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + product: .framework, + bundleId: "com.cocoatype.Highlighter.TestHelpersInterface", + sources: ["Modules/TestHelpers/Interface/**"] ) } From e82de061372114d38d05657c7a46f432ed95e09d Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Thu, 16 May 2024 02:53:43 -0700 Subject: [PATCH 07/21] Add more tests --- Highlighter.xctestplan | 6 ++ .../DocumentCameraViewController.swift | 14 ++++ .../DocumentScanningController.swift | 8 ++- ...oLibraryDataSourceExtraItemsProvider.swift | 10 ++- .../Tests/Application/AppDelegateTests.swift | 2 +- .../DocumentScanningControllerTests.swift | 26 ++++++++ ...aryDataSourceExtraItemsProviderTests.swift | 66 +++++++++++++++++++ 7 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 Modules/Legacy/Core/Sources/Document Scanning/DocumentCameraViewController.swift create mode 100644 Modules/Legacy/Core/Tests/Document Scanning/DocumentScanningControllerTests.swift create mode 100644 Modules/Legacy/Core/Tests/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProviderTests.swift diff --git a/Highlighter.xctestplan b/Highlighter.xctestplan index 019eb095..77291dc3 100644 --- a/Highlighter.xctestplan +++ b/Highlighter.xctestplan @@ -9,6 +9,12 @@ } ], "defaultOptions" : { + "environmentVariableEntries" : [ + { + "key" : "IS_TEST", + "value" : "1" + } + ], "testTimeoutsEnabled" : true }, "testTargets" : [ diff --git a/Modules/Legacy/Core/Sources/Document Scanning/DocumentCameraViewController.swift b/Modules/Legacy/Core/Sources/Document Scanning/DocumentCameraViewController.swift new file mode 100644 index 00000000..5ee31148 --- /dev/null +++ b/Modules/Legacy/Core/Sources/Document Scanning/DocumentCameraViewController.swift @@ -0,0 +1,14 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import VisionKit + +protocol DocumentCameraViewController: UIViewController { + var delegate: VNDocumentCameraViewControllerDelegate? { get set } +} + +extension VNDocumentCameraViewController: DocumentCameraViewController {} + +class StubDocumentCameraViewController: UIViewController, DocumentCameraViewController { + weak var delegate: VNDocumentCameraViewControllerDelegate? +} diff --git a/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift b/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift index 8cc7827a..ed2db568 100644 --- a/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift +++ b/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift @@ -19,7 +19,13 @@ class DocumentScanningController: NSObject, VNDocumentCameraViewControllerDelega func cameraViewController() -> UIViewController { if purchased { - let cameraViewController = VNDocumentCameraViewController() + let cameraViewController: DocumentCameraViewController + if ProcessInfo.processInfo.environment["IS_TEST"] == nil { + cameraViewController = VNDocumentCameraViewController() + } else { + cameraViewController = StubDocumentCameraViewController() + } + cameraViewController.delegate = self cameraViewController.overrideUserInterfaceStyle = .dark cameraViewController.view.tintColor = .controlTint diff --git a/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift b/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift index 36274809..c6962239 100644 --- a/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift +++ b/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift @@ -8,7 +8,11 @@ import Purchasing import VisionKit class PhotoLibraryDataSourceExtraItemsProvider: NSObject { - init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + init( + isDocumentScannerSupported: Bool = VNDocumentCameraViewController.isSupported, + purchaseRepository: PurchaseRepository = Purchasing.repository + ) { + self.isDocumentScannerSupported = isDocumentScannerSupported self.thatsFineThatsOnlyThree = purchaseRepository } @@ -20,7 +24,7 @@ class PhotoLibraryDataSourceExtraItemsProvider: NSObject { // MARK: Document Scanning private var shouldShowDocumentScannerCell: Bool { let hasPurchased = thatsFineThatsOnlyThree.withCheese == .purchased - return VNDocumentCameraViewController.isSupported && (hideDocumentScanner == false || hasPurchased) + return isDocumentScannerSupported && (hideDocumentScanner == false || hasPurchased) } func documentScannerCell(for collectionView: UICollectionView, at indexPath: IndexPath) -> UICollectionViewCell { @@ -63,4 +67,6 @@ class PhotoLibraryDataSourceExtraItemsProvider: NSObject { // thatsFineThatsOnlyThree by @nutterfi on 2024-05-15 // the purchase repository private let thatsFineThatsOnlyThree: PurchaseRepository + + private let isDocumentScannerSupported: Bool } diff --git a/Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift b/Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift index 0fc9f7ff..d6e00322 100644 --- a/Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift +++ b/Modules/Legacy/Core/Tests/Application/AppDelegateTests.swift @@ -14,7 +14,7 @@ class AppDelegateTests: XCTestCase { let logger = SpyLogger() let delegate = AppDelegate(purchaseRepository: repository, logger: logger) - delegate.application(UIApplication.shared, willFinishLaunchingWithOptions: nil) + _ = delegate.application(UIApplication.shared, willFinishLaunchingWithOptions: nil) waitForExpectations(timeout: 1) } diff --git a/Modules/Legacy/Core/Tests/Document Scanning/DocumentScanningControllerTests.swift b/Modules/Legacy/Core/Tests/Document Scanning/DocumentScanningControllerTests.swift new file mode 100644 index 00000000..887de5e1 --- /dev/null +++ b/Modules/Legacy/Core/Tests/Document Scanning/DocumentScanningControllerTests.swift @@ -0,0 +1,26 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import PurchasingDoubles +import VisionKit +import XCTest + +@testable import Core + +class DocumentScanningControllerTests: XCTestCase { + func testCameraViewControllerIsReturnedIfPurchased() { + let repository = SpyRepository(withCheese: .purchased) + let scanningController = DocumentScanningController(delegate: nil, purchaseRepository: repository) + let cameraViewController = scanningController.cameraViewController() + + XCTAssert(cameraViewController is DocumentCameraViewController) + } + + func testCameraViewControllerReturnsAlertIfNotPurchased() { + let repository = SpyRepository(withCheese: .unavailable) + let scanningController = DocumentScanningController(delegate: nil, purchaseRepository: repository) + let cameraViewController = scanningController.cameraViewController() + + XCTAssert(cameraViewController is UIAlertController) + } +} diff --git a/Modules/Legacy/Core/Tests/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProviderTests.swift b/Modules/Legacy/Core/Tests/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProviderTests.swift new file mode 100644 index 00000000..65685be3 --- /dev/null +++ b/Modules/Legacy/Core/Tests/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProviderTests.swift @@ -0,0 +1,66 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import PurchasingDoubles +import XCTest + +@testable import Core +@testable import Defaults + +class PhotoLibraryDataSourceExtraItemsProviderTests: XCTestCase { + @Defaults.Value(key: .hideDocumentScanner) var hideDocumentScanner: Bool + override func tearDown() { + hideDocumentScanner = false + super.tearDown() + } + + func testDocumentScannerIsIncludedIfPurchasedAndSupportedAndNotHidden() { + let repository = SpyRepository(withCheese: .purchased) + let provider = PhotoLibraryDataSourceExtraItemsProvider(isDocumentScannerSupported: true, purchaseRepository: repository) + hideDocumentScanner = false + XCTAssertTrue(provider.extraItems.contains(where: \.isDocumentScan)) + } + + func testDocumentScannerIsIncludedIfNotPurchasedAndNotHidden() { + let repository = SpyRepository(withCheese: .unavailable) + let provider = PhotoLibraryDataSourceExtraItemsProvider(isDocumentScannerSupported: true, purchaseRepository: repository) + hideDocumentScanner = false + XCTAssertTrue(provider.extraItems.contains(where: \.isDocumentScan)) + } + + func testDocumentScannerIsIncludedIfPurchasedAndHidden() { + let repository = SpyRepository(withCheese: .purchased) + let provider = PhotoLibraryDataSourceExtraItemsProvider(isDocumentScannerSupported: true, purchaseRepository: repository) + hideDocumentScanner = true + XCTAssertTrue(provider.extraItems.contains(where: \.isDocumentScan)) + } + + func testDocumentScannerIsNotIncludedIfNotPurchasedAndHidden() { + let repository = SpyRepository(withCheese: .unavailable) + let provider = PhotoLibraryDataSourceExtraItemsProvider(isDocumentScannerSupported: true, purchaseRepository: repository) + hideDocumentScanner = true + XCTAssertFalse(provider.extraItems.contains(where: \.isDocumentScan)) + } + + func testDocumentScannerIsNotIncludedIfNotSupported() { + let repository = SpyRepository(withCheese: .purchased) + let provider = PhotoLibraryDataSourceExtraItemsProvider(isDocumentScannerSupported: false, purchaseRepository: repository) + hideDocumentScanner = false + XCTAssertFalse(provider.extraItems.contains(where: \.isDocumentScan)) + } +} + +private extension PhotoLibraryItem { + var isDocumentScan: Bool { + switch self { + case .documentScan: return true + case .asset, .limitedLibrary: return false + } + } +} + +private extension PhotoLibraryDataSourceExtraItemsProvider { + var extraItems: [PhotoLibraryItem] { + (0.. Date: Thu, 16 May 2024 03:04:37 -0700 Subject: [PATCH 08/21] Add PurchaseState tests --- .../Purchasing/Tests/PurchaseStateTests.swift | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift diff --git a/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift b/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift new file mode 100644 index 00000000..12103e1f --- /dev/null +++ b/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift @@ -0,0 +1,54 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import StoreKit +import XCTest + +@testable import Purchasing + +class PurchaseStateTests: XCTestCase { + func testProductReturnsIfReadyForPurchase() { + let product = SKProduct() + let state = PurchaseState.readyForPurchase(product: product) + + XCTAssertIdentical(state.product, product) + } + + func testProductIsNilIfNotReadyForPurchase() { + for state in PurchaseState.nonProductStates { + XCTAssertNil(state.product) + } + } + + func testIsReadyForPurchaseIfReadyForPurchase() { + let state = PurchaseState.readyForPurchase(product: SKProduct()) + XCTAssertTrue(state.isReadyForPurchase) + } + + func testIsNotReadyForPurchaseIfNotReadyForPurchase() { + for state in PurchaseState.nonProductStates { + XCTAssertFalse(state.isReadyForPurchase) + } + } + + func testReadyForPurchaseIdentifier() { + let state = PurchaseState.readyForPurchase(product: SKProduct()) + XCTAssertEqual(state, state.id) + } + + func testNonProductStateIdentifiers() { + for state in PurchaseState.nonProductStates { + XCTAssertEqual(state, state.id) + } + } +} + +private extension PurchaseState { + static let nonProductStates = [ + PurchaseState.loading, + .purchasing, + .restoring, + .purchased, + .unavailable + ] +} From c838c9a59962522de7c8482b73eb8963c7835ad4 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Fri, 17 May 2024 18:30:30 -0700 Subject: [PATCH 09/21] Add intent handler and action set tests --- Highlighter.xctestplan | 5 ++ .../RedactDetectedIntentHandlerTests.swift | 18 +++++ .../RedactImageIntentHandlerTests.swift | 18 +++++ .../Toolbar Items/ActionSetTests.swift | 68 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 Modules/Legacy/Core/Tests/Shortcuts/Redact Detections/RedactDetectedIntentHandlerTests.swift create mode 100644 Modules/Legacy/Core/Tests/Shortcuts/Redact Image/RedactImageIntentHandlerTests.swift create mode 100644 Modules/Legacy/Editing/Tests/Editing View/Toolbar Items/ActionSetTests.swift diff --git a/Highlighter.xctestplan b/Highlighter.xctestplan index 77291dc3..ac9cd1cb 100644 --- a/Highlighter.xctestplan +++ b/Highlighter.xctestplan @@ -9,6 +9,11 @@ } ], "defaultOptions" : { + "commandLineArgumentEntries" : [ + { + "argument" : "-FeatureFlag.autoRedactInEdit YES" + } + ], "environmentVariableEntries" : [ { "key" : "IS_TEST", diff --git a/Modules/Legacy/Core/Tests/Shortcuts/Redact Detections/RedactDetectedIntentHandlerTests.swift b/Modules/Legacy/Core/Tests/Shortcuts/Redact Detections/RedactDetectedIntentHandlerTests.swift new file mode 100644 index 00000000..c324420b --- /dev/null +++ b/Modules/Legacy/Core/Tests/Shortcuts/Redact Detections/RedactDetectedIntentHandlerTests.swift @@ -0,0 +1,18 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import PurchasingDoubles +import XCTest + +@testable import Core + +class RedactDetectedIntentHandlerTests: XCTestCase { + func testHandleReturnsUnpurchasedIfNotPurchased() async { + let repository = SpyRepository(withCheese: .unavailable) + let intent = RedactDetectedIntent() + let handler = RedactDetectedIntentHandler(purchaseRepository: repository) + + let response = await handler.handle(💩: intent) + XCTAssertEqual(response, .unpurchased) + } +} diff --git a/Modules/Legacy/Core/Tests/Shortcuts/Redact Image/RedactImageIntentHandlerTests.swift b/Modules/Legacy/Core/Tests/Shortcuts/Redact Image/RedactImageIntentHandlerTests.swift new file mode 100644 index 00000000..325b5293 --- /dev/null +++ b/Modules/Legacy/Core/Tests/Shortcuts/Redact Image/RedactImageIntentHandlerTests.swift @@ -0,0 +1,18 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import PurchasingDoubles +import XCTest + +@testable import Core + +class RedactImageIntentHandlerTests: XCTestCase { + func testHandleReturnsUnpurchasedIfNotPurchased() async { + let repository = SpyRepository(withCheese: .unavailable) + let intent = RedactImageIntent() + let handler = RedactImageIntentHandler(purchaseRepository: repository) + + let response = await handler.handle(intent: intent) + XCTAssertEqual(response, .unpurchased) + } +} diff --git a/Modules/Legacy/Editing/Tests/Editing View/Toolbar Items/ActionSetTests.swift b/Modules/Legacy/Editing/Tests/Editing View/Toolbar Items/ActionSetTests.swift new file mode 100644 index 00000000..f4cf5967 --- /dev/null +++ b/Modules/Legacy/Editing/Tests/Editing View/Toolbar Items/ActionSetTests.swift @@ -0,0 +1,68 @@ +// Created by Geoff Pado on 5/16/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Defaults +import Purchasing +import PurchasingDoubles +import XCTest + +@testable import Editing + +class ActionSetTests: XCTestCase { + @Defaults.Value(key: .hideAutoRedactions) private var hideAutoRedactions: Bool + + override func tearDown() { + hideAutoRedactions = false + super.tearDown() + } + + func testCompactTrailingItemsContainsRedactWhenPurchasedAndNotHidden() { + let set = ActionSet(purchaseState: .purchased) + hideAutoRedactions = false + + let trailingItems = set.trailingNavigationItems + XCTAssertTrue(trailingItems.contains(where: { $0 is QuickRedactBarButtonItem })) + } + + func testCompactTrailingItemsContainsRedactWhenPurchasedAndHidden() { + let set = ActionSet(purchaseState: .purchased) + hideAutoRedactions = true + + let trailingItems = set.trailingNavigationItems + XCTAssertTrue(trailingItems.contains(where: { $0 is QuickRedactBarButtonItem })) + } + + func testCompactTrailingItemsContainsRedactWhenNotPurchasedAndNotHidden() { + let set = ActionSet(purchaseState: .unavailable) + hideAutoRedactions = false + + let trailingItems = set.trailingNavigationItems + XCTAssertTrue(trailingItems.contains(where: { $0 is QuickRedactBarButtonItem })) + } + + func testCompactTrailingItemsDoesNotContainRedactWhenNotPurchasedAndHidden() { + let set = ActionSet(purchaseState: .unavailable) + hideAutoRedactions = true + + let trailingItems = set.trailingNavigationItems + XCTAssertFalse(trailingItems.contains(where: { $0 is QuickRedactBarButtonItem })) + } +} + +private extension ActionSet { + private class Target {} + + init( + sizeClass: UIUserInterfaceSizeClass = .compact, + purchaseState: PurchaseState + ) { + self.init( + for: Target(), + undoManager: nil, + selectedTool: .magic, + sizeClass: sizeClass, + currentColor: .black, + purchaseRepository: SpyRepository(withCheese: purchaseState) + ) + } +} From c39166b01d8c551e93abf763bf75b500fd7fe505 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Fri, 17 May 2024 21:10:58 -0700 Subject: [PATCH 10/21] Implement StoreRepository --- App/Configuration.storekit | 58 +++++++++- .../Top Bar/ProductPriceFormatter.swift | 21 ---- .../Sources/Top Bar/PurchaseButton.swift | 21 ++-- .../Top Bar/PurchaseRestoreButton.swift | 8 +- .../Purchasing/Doubles/PreviewProduct.swift | 19 ++++ .../Purchasing/PurchaseConstants.swift | 10 +- .../Sources/Purchasing/PurchaseProduct.swift | 13 +++ .../Sources/Purchasing/PurchaseState.swift | 34 +++++- .../Sources/Purchasing/StoreRepository.swift | 104 ++++++++++++++++-- .../TransactionUpdateObserver.swift | 43 ++++++++ .../Sources/Settings/List/SettingsView.swift | 2 +- .../PurchaseNavigationLink.swift | 7 +- .../Purchase Item/PurchaseSubtitle.swift | 11 +- 13 files changed, 278 insertions(+), 73 deletions(-) delete mode 100644 Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/ProductPriceFormatter.swift create mode 100644 Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseProduct.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/TransactionUpdateObserver.swift diff --git a/App/Configuration.storekit b/App/Configuration.storekit index 04497b95..de3a22d3 100644 --- a/App/Configuration.storekit +++ b/App/Configuration.storekit @@ -5,7 +5,7 @@ ], "products" : [ { - "displayPrice" : "2.99", + "displayPrice" : "5.99", "familyShareable" : false, "internalID" : "F7423F26", "localizations" : [ @@ -22,7 +22,57 @@ ], "settings" : { "_askToBuyEnabled" : false, - "_billingIssuesEnabled" : false + "_billingIssuesEnabled" : false, + "_failTransactionsEnabled" : false, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] }, "subscriptionGroups" : [ @@ -32,7 +82,7 @@ "privateKey" : "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgURFeyUUG7V+lJqVrfdDVGcV3iDiEBFQ76WIAK7MBH9agCgYIKoZIzj0DAQehRANCAATT6yZ9zEMzSiqYoWeZx8IHxX3ShGI73VlCUJZMt8WEzwdzWjSGGXD0zkLdWahaUrgZXOiTvOWYKWXPmHrqnlEP" }, "version" : { - "major" : 1, - "minor" : 1 + "major" : 3, + "minor" : 0 } } diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/ProductPriceFormatter.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/ProductPriceFormatter.swift deleted file mode 100644 index 8dc4bc28..00000000 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/ProductPriceFormatter.swift +++ /dev/null @@ -1,21 +0,0 @@ -// Created by Geoff Pado on 5/21/21. -// Copyright © 2021 Cocoatype, LLC. All rights reserved. - -import StoreKit - -public enum ProductPriceFormatter { - public static func formattedPrice(for product: SKProduct) -> String? { - if product.priceLocale != Self.priceFormatter.locale { - Self.priceFormatter.locale = product.priceLocale - } - - return Self.priceFormatter.string(from: product.price) - } - - private static let priceFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.formatterBehavior = .behavior10_4 - formatter.numberStyle = .currency - return formatter - }() -} diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift index 50e83f9e..2b31e4e0 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift @@ -21,6 +21,7 @@ struct PurchaseButton: View { var body: some View { Button { guard purchaseState.isReadyForPurchase else { return } + purchaseState = .purchasing Task { purchaseState = await allWeAskIsThatYouLetUsHaveItYourWay.purchase() } @@ -45,8 +46,7 @@ struct PurchaseButton: View { case .purchasing, .restoring: return Self.purchaseButtonTitlePurchasing case .readyForPurchase(let product): - guard let price = ProductPriceFormatter.formattedPrice(for: product) else { return Self.purchaseButtonTitleLoading } - return String(format: Self.purchaseButtonTitleReady, price) + return String(format: Self.purchaseButtonTitleReady, product.displayPrice) case .unavailable: return Self.purchaseButtonTitleLoading case .purchased: @@ -74,20 +74,21 @@ import PurchasingDoubles enum PurchaseButtonPreviews: PreviewProvider { static var previews: some View { VStack(alignment: .leading, spacing: 3) { - PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .loading)) - PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .readyForPurchase(product: StubProduct()))) - PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .purchasing)) - PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .purchased)) - PurchaseButton(purchaseRepository: PreviewRepository(purchaseState: .unavailable)) + PurchaseButton(state: .loading) + PurchaseButton(state: .readyForPurchase(product: PreviewProduct())) + PurchaseButton(state: .purchasing) + PurchaseButton(state: .purchased) + PurchaseButton(state: .unavailable) } .padding() .background(Color.appPrimary) .preferredColorScheme(.dark) } +} - private class StubProduct: SKProduct { - override var priceLocale: Locale { .current } - override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } +extension PurchaseButton { + init(state: PurchaseState) { + self.init(purchaseRepository: PreviewRepository(purchaseState: state)) } } #endif diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift index a986fc06..86c7790b 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift @@ -17,6 +17,7 @@ struct PurchaseRestoreButton: View { var body: some View { Button { + purchaseState = .restoring Task { purchaseState = await purchaseRepository.restore() } @@ -47,7 +48,7 @@ import PurchasingDoubles enum PurchaseRestoreButtonPreviews: PreviewProvider { static let states = [ PurchaseState.loading, - .readyForPurchase(product: MockProduct()), + .readyForPurchase(product: PreviewProduct()), .purchasing, .purchased, .unavailable, @@ -63,10 +64,5 @@ enum PurchaseRestoreButtonPreviews: PreviewProvider { .background(Color.appPrimary) .preferredColorScheme(.dark) } - - private class MockProduct: SKProduct { - override var priceLocale: Locale { .current } - override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } - } } #endif diff --git a/Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift b/Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift new file mode 100644 index 00000000..5651e2c3 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift @@ -0,0 +1,19 @@ +// Created by Geoff Pado on 5/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Foundation +import StoreKit + +public struct PreviewProduct: PurchaseProduct { + public let id: String + public let displayPrice = "$1.99" + public let currentEntitlement = VerificationResult?.none + + public init() { + id = UUID().uuidString + } + + public func purchase(options: Set) async throws -> Product.PurchaseResult { + return .userCancelled + } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift index e96c49bd..4ec16f7a 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift @@ -3,10 +3,10 @@ enum PurchaseConstants { static let productIdentifier = "com.cocoatype.Highlighter.unlock" +} - enum Error: Swift.Error { - case paymentsNotAvailable - case productNotFound(identifier: String) - case unknown - } +enum PurchaseError: Error { + case paymentsNotAvailable + case productNotFound(identifier: String) + case unknown } diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseProduct.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseProduct.swift new file mode 100644 index 00000000..73720e59 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseProduct.swift @@ -0,0 +1,13 @@ +// Created by Geoff Pado on 5/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import StoreKit + +public protocol PurchaseProduct: Hashable, Identifiable { + var id: String { get } + var displayPrice: String { get } + var currentEntitlement: VerificationResult? { get async } + func purchase(options: Set) async throws -> Product.PurchaseResult +} + +extension Product: PurchaseProduct {} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift index 6399e6ec..1157df2c 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseState.swift @@ -6,13 +6,13 @@ import StoreKit public enum PurchaseState: Identifiable, Hashable { case loading - case readyForPurchase(product: SKProduct) + case readyForPurchase(product: any PurchaseProduct) case purchasing case restoring case purchased case unavailable - public var product: SKProduct? { + public var product: (any PurchaseProduct)? { switch self { case .readyForPurchase(let product): return product default: return nil @@ -27,4 +27,34 @@ public enum PurchaseState: Identifiable, Hashable { } public var id: Self { self } + + // MARK: Equatable + + public static func == (lhs: PurchaseState, rhs: PurchaseState) -> Bool { + switch (lhs, rhs) { + case (.loading, .loading): true + case (.readyForPurchase(let lhsProduct), .readyForPurchase(let rhsProduct)): lhsProduct.id == rhsProduct.id + case (.purchasing, .purchasing): true + case (.restoring, .restoring): true + case (.purchased, .purchased): true + case (.unavailable, .unavailable): true + case (.loading, _): false + case (.readyForPurchase, _): false + case (.purchasing, _): false + case (.restoring, _): false + case (.purchased, _): false + case (.unavailable, _): false + } + } + + // MARK: Hashable + + public func hash(into hasher: inout Hasher) { + switch self { + case .readyForPurchase(let product): + hasher.combine(product) + case .loading, .purchasing, .restoring, .purchased, .unavailable: + hasher.combine(String(describing: self)) + } + } } diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift index 89c41c86..fe3b9963 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift @@ -1,32 +1,116 @@ // Created by Geoff Pado on 5/15/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. +import ErrorHandling +import OSLog import StoreKit class StoreRepository: PurchaseRepository { - private(set) var withCheese: PurchaseState = .loading + static var product: any PurchaseProduct { + get async throws { + let products = try await Product.products(for: [PurchaseConstants.productIdentifier]) + guard let product = products.first else { + throw PurchaseError.productNotFound(identifier: PurchaseConstants.productIdentifier) + } + + return product + } + } + + private(set) var withCheese: PurchaseState = .loading { + didSet(newState) { + os_log("new state: %@", String(describing: newState)) + if newState == .loading { + refresh() + } + } + } var noOnions: PurchaseState { get async { - // go fetch the value - let newValue = PurchaseState.loading + await update() + } + } - withCheese = newValue - return newValue + private var product: any PurchaseProduct { + get async throws { + if let existingProduct = withCheese.product { + return existingProduct + } else { + return try await Self.product + } } } + private let transactionUpdateObserver = TransactionUpdateObserver() + private var transactionUpdateTask: Task? func start() { - // subscribe to transaction updates + transactionUpdateTask = Task(priority: .background) { + // subscribe to transaction updates + for await state in transactionUpdateObserver.start() { + withCheese = state + } + } + + refresh() + } + + private func refresh() { + Task { + await update() + } + } + + @discardableResult private func update() async -> PurchaseState { + do { + let product = try await self.product + let resultState: PurchaseState + if case .verified = await product.currentEntitlement { + resultState = .purchased + } else { + resultState = .readyForPurchase(product: product) + } + + withCheese = resultState + return resultState + } catch { + ErrorHandler().log(error) + return withCheese + } } func purchase() async -> PurchaseState { - // make purchase - return .loading + do { + withCheese = .purchasing + let product = try await self.product + let result = try await product.purchase(options: []) + switch result { + case .success(let verificationResult): + if case .verified(let transaction) = verificationResult { + withCheese = .purchased + await transaction.finish() + return withCheese + } else { + fallthrough + } + case .userCancelled, .pending: + fallthrough + @unknown default: + return withCheese + } + } catch { + ErrorHandler().log(error) + return withCheese + } } func restore() async -> PurchaseState { - // restore previous purchase - return .loading + do { + try await AppStore.sync() + return await update() + } catch { + ErrorHandler().log(error) + return withCheese + } } } diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/TransactionUpdateObserver.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/TransactionUpdateObserver.swift new file mode 100644 index 00000000..ce3ecf2c --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/TransactionUpdateObserver.swift @@ -0,0 +1,43 @@ +// Created by Geoff Pado on 5/17/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import StoreKit + +class TransactionUpdateObserver { + // tgif ("thank goodness I'm first") by @KaenAitch on 2024-05-17 + // the transaction observation task + private var tgif: Task? + func start() -> AsyncStream { + let (purchaseStates, continuation) = AsyncStream.makeStream() + + tgif = Task(priority: .background) { + for await verificationResult in Transaction.updates { + guard case .verified(let transaction) = verificationResult else { + // Ignore unverified transactions. + return + } + + if transaction.revocationDate != nil { + if let product = try? await Product.products(for: [transaction.productID]).first { + continuation.yield(.readyForPurchase(product: product)) + } else { + continuation.yield(.loading) + } + // Remove access to the product identified by transaction.productID. + // Transaction.revocationReason provides details about + // the revoked transaction. + } else { + // Provide access to the product identified by + // transaction.productID. + continuation.yield(.purchased) + } + } + } + + return purchaseStates + } + + deinit { + tgif?.cancel() + } +} diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift index e3897b9c..ee492d04 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift @@ -42,7 +42,7 @@ struct SettingsView: View { #if DEBUG import PurchasingDoubles enum SettingsViewPreviews: PreviewProvider { - static let states = [PurchaseState.loading, .readyForPurchase(product: MockProduct()), .purchased] + static let states = [PurchaseState.loading, .readyForPurchase(product: PreviewProduct()), .purchased] static var previews: some View { ForEach(states) { state in diff --git a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift index 4aac0d7c..5492ecc3 100644 --- a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift +++ b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift @@ -42,13 +42,8 @@ enum PurchaseNavigationLinkPreviews: PreviewProvider { static var previews: some View { VStack(alignment: .leading, spacing: 8) { PurchaseNavigationLink(purchaseRepository: PreviewRepository(purchaseState: .loading), destination: Text?.none) - PurchaseNavigationLink(purchaseRepository: PreviewRepository(purchaseState: .readyForPurchase(product: MockProduct())), destination: Text?.none) + PurchaseNavigationLink(purchaseRepository: PreviewRepository(purchaseState: .readyForPurchase(product: PreviewProduct())), destination: Text?.none) }.preferredColorScheme(.dark) } - - private class MockProduct: SKProduct { - override var priceLocale: Locale { .current } - override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } - } } #endif diff --git a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift index c3fbf78d..42d6f2d4 100644 --- a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift +++ b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift @@ -20,9 +20,9 @@ struct PurchaseSubtitle: View { } private var text: String { - guard let product = purchaseState.product, let price = ProductPriceFormatter.formattedPrice(for: product) else { return Self.subtitleWithoutProduct } + guard let product = purchaseState.product else { return Self.subtitleWithoutProduct } - return String(format: Self.subtitleWithProduct, price) + return String(format: Self.subtitleWithProduct, product.displayPrice) } // MARK: Localized Strings @@ -35,12 +35,7 @@ struct PurchaseSubtitlePreviews: PreviewProvider { static var previews: some View { VStack { PurchaseSubtitle(state: .loading).preferredColorScheme(.dark) - PurchaseSubtitle(state: .readyForPurchase(product: MockProduct())).preferredColorScheme(.dark) + PurchaseSubtitle(state: .readyForPurchase(product: PreviewProduct())).preferredColorScheme(.dark) } } - - private class MockProduct: SKProduct { - override var priceLocale: Locale { .current } - override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } - } } From a5b62d5a1e64098971fa529e0fcdc0461c3ea7e8 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sat, 18 May 2024 01:27:46 -0700 Subject: [PATCH 11/21] Update references to PurchaseRepository --- .../PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift | 4 ++-- .../Sources/Top Bar/PurchaseRestoreButton.swift | 4 ++-- .../Capabilities/Purchasing/Doubles/PreviewRepository.swift | 2 +- .../Purchasing/Sources/Purchasing/PurchaseRepository.swift | 2 ++ .../Purchasing/Sources/Purchasing/Purchasing.swift | 2 +- .../Purchasing/Sources/Purchasing/StoreRepository.swift | 2 +- Modules/Legacy/Core/Sources/Application/AppDelegate.swift | 4 ++-- .../Document Scanning/DocumentScanningController.swift | 4 ++-- .../PhotoLibraryDataSourceExtraItemsProvider.swift | 4 ++-- .../Core/Sources/Settings/Desktop/DesktopSettingsView.swift | 4 ++-- .../Core/Sources/Settings/List/SettingsAlertButton.swift | 4 ++-- .../Core/Sources/Settings/List/SettingsContentGenerator.swift | 4 ++-- Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift | 4 +--- .../Settings/Purchase Item/PurchaseNavigationLink.swift | 4 ++-- .../Redact Detections/RedactDetectedIntentHandler.swift | 4 ++-- .../Shortcuts/Redact Image/RedactImageIntentHandler.swift | 4 ++-- .../PhotoEditingAutoRedactionsAccessProvider.swift | 4 ++-- .../Sources/Editing View/Toolbar Items/ActionSet.swift | 4 ++-- 18 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift index 2b31e4e0..0b452797 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift @@ -9,10 +9,10 @@ import SwiftUI struct PurchaseButton: View { @State private var purchaseState: PurchaseState // allWeAskIsThatYouLetUsHaveItYourWay by @AdamWulf on 2024-05-15 - private let allWeAskIsThatYouLetUsHaveItYourWay: PurchaseRepository + private let allWeAskIsThatYouLetUsHaveItYourWay: any PurchaseRepository init( - purchaseRepository: PurchaseRepository = Purchasing.repository + purchaseRepository: any PurchaseRepository = Purchasing.repository ) { _purchaseState = State(initialValue: purchaseRepository.withCheese) allWeAskIsThatYouLetUsHaveItYourWay = purchaseRepository diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift index 86c7790b..06cf7f1d 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift @@ -8,9 +8,9 @@ import SwiftUI struct PurchaseRestoreButton: View { @State private var purchaseState: PurchaseState - private let purchaseRepository: PurchaseRepository + private let purchaseRepository: any PurchaseRepository - init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + init(purchaseRepository: any PurchaseRepository = Purchasing.repository) { _purchaseState = State(initialValue: purchaseRepository.withCheese) self.purchaseRepository = purchaseRepository } diff --git a/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift b/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift index e1728200..bdfb075f 100644 --- a/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift +++ b/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift @@ -3,7 +3,7 @@ import Purchasing -public class PreviewRepository: PurchaseRepository { +public struct PreviewRepository: PurchaseRepository { public var withCheese: PurchaseState public var noOnions: PurchaseState { withCheese } public init(purchaseState: PurchaseState) { diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift index 5f88f5e6..be4d4134 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift @@ -1,6 +1,8 @@ // Created by Geoff Pado on 5/15/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. +import Combine + public protocol PurchaseRepository { // withCheese by @CompileDev on 2024-05-15 // the cached purchase state diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift index b77b8f46..5289c747 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchasing.swift @@ -2,5 +2,5 @@ // Copyright © 2024 Cocoatype, LLC. All rights reserved. public enum Purchasing { - public static let repository: PurchaseRepository = StoreRepository() + public static let repository: any PurchaseRepository = StoreRepository() } diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift index fe3b9963..0e0d4bb8 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift @@ -17,7 +17,7 @@ class StoreRepository: PurchaseRepository { } } - private(set) var withCheese: PurchaseState = .loading { + @Published private(set) var withCheese: PurchaseState = .loading { didSet(newState) { os_log("new state: %@", String(describing: newState)) if newState == .loading { diff --git a/Modules/Legacy/Core/Sources/Application/AppDelegate.swift b/Modules/Legacy/Core/Sources/Application/AppDelegate.swift index b6eb6cac..8efc289e 100644 --- a/Modules/Legacy/Core/Sources/Application/AppDelegate.swift +++ b/Modules/Legacy/Core/Sources/Application/AppDelegate.swift @@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } init( - purchaseRepository: PurchaseRepository, + purchaseRepository: any PurchaseRepository, logger: Logger ) { veryGoodText = purchaseRepository @@ -134,6 +134,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // veryGoodText by @NoGoodNick_ on 2024-05-15 // the purchase repository - private let veryGoodText: PurchaseRepository + private let veryGoodText: any PurchaseRepository private let logger: Logger } diff --git a/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift b/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift index ed2db568..7c8dcb5c 100644 --- a/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift +++ b/Modules/Legacy/Core/Sources/Document Scanning/DocumentScanningController.swift @@ -10,7 +10,7 @@ import VisionKit class DocumentScanningController: NSObject, VNDocumentCameraViewControllerDelegate { init( delegate: DocumentScanningDelegate?, - purchaseRepository: PurchaseRepository = Purchasing.repository + purchaseRepository: any PurchaseRepository = Purchasing.repository ) { self.delegate = delegate self.🍺 = purchaseRepository @@ -62,7 +62,7 @@ class DocumentScanningController: NSObject, VNDocumentCameraViewControllerDelega // 🍺 by @KaenAitch on 2024-05-15 // the purchase repository - private let 🍺: PurchaseRepository + private let 🍺: any PurchaseRepository } protocol DocumentScanningDelegate: AnyObject, PhotoEditorPresenting { diff --git a/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift b/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift index c6962239..857087e7 100644 --- a/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift +++ b/Modules/Legacy/Core/Sources/Photo Selection/Photo Library/Data Source/PhotoLibraryDataSourceExtraItemsProvider.swift @@ -10,7 +10,7 @@ import VisionKit class PhotoLibraryDataSourceExtraItemsProvider: NSObject { init( isDocumentScannerSupported: Bool = VNDocumentCameraViewController.isSupported, - purchaseRepository: PurchaseRepository = Purchasing.repository + purchaseRepository: any PurchaseRepository = Purchasing.repository ) { self.isDocumentScannerSupported = isDocumentScannerSupported self.thatsFineThatsOnlyThree = purchaseRepository @@ -66,7 +66,7 @@ class PhotoLibraryDataSourceExtraItemsProvider: NSObject { // thatsFineThatsOnlyThree by @nutterfi on 2024-05-15 // the purchase repository - private let thatsFineThatsOnlyThree: PurchaseRepository + private let thatsFineThatsOnlyThree: any PurchaseRepository private let isDocumentScannerSupported: Bool } diff --git a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift index f38ddf86..215488d2 100644 --- a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift @@ -8,11 +8,11 @@ import SwiftUI struct DesktopSettingsView: View { @State private var purchaseState: PurchaseState private let readableWidth: CGFloat - private let purchaseRepository: PurchaseRepository + private let purchaseRepository: any PurchaseRepository init( readableWidth: CGFloat = .zero, - purchaseRepository: PurchaseRepository = Purchasing.repository + purchaseRepository: any PurchaseRepository = Purchasing.repository ) { _purchaseState = State(initialValue: purchaseRepository.withCheese) self.readableWidth = readableWidth diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift index c94b28d7..ebf6f3e9 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsAlertButton.swift @@ -12,7 +12,7 @@ struct SettingsAlertButton: View { init( _ titleKey: LocalizedStringKey, _ subtitle: String? = nil, - purchaseRepository: PurchaseRepository = Purchasing.repository + purchaseRepository: any PurchaseRepository = Purchasing.repository ) { self.titleKey = titleKey self.subtitle = subtitle @@ -47,7 +47,7 @@ struct SettingsAlertButton: View { // haveYourDucksInARow by @Eskeminha on 2024-05-15 // the purchase repository - private let haveYourDucksInARow: PurchaseRepository + private let haveYourDucksInARow: any PurchaseRepository } struct SettingsAlertTitleText: View { diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift index a4b5bfaf..5f27f4a2 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsContentGenerator.swift @@ -8,7 +8,7 @@ import Purchasing import SafariServices import SwiftUI -struct SettingsContentGenerator { +struct SettingsContent: View { private let purchaseState: PurchaseState init(state: PurchaseState) { self.purchaseState = state @@ -20,7 +20,7 @@ struct SettingsContentGenerator { return versionString ?? "???" } - var content: some View { + var body: some View { Group { Section { if purchaseState != .purchased { diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift index ee492d04..7beefe8a 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift @@ -10,7 +10,6 @@ import SwiftUI struct SettingsView: View { private let purchaseRepository: PurchaseRepository @State private var purchaseState: PurchaseState - @State private var selectedURL: URL? private let dismissAction: () -> Void private let readableWidth: CGFloat @@ -28,7 +27,7 @@ struct SettingsView: View { var body: some View { SettingsNavigationView { SettingsList(dismissAction: dismissAction) { - SettingsContentGenerator(state: purchaseState).content + SettingsContent(state: purchaseState) }.navigationBarTitle("SettingsViewController.navigationTitle", displayMode: .inline) } .environment(\.readableWidth, readableWidth) @@ -47,7 +46,6 @@ enum SettingsViewPreviews: PreviewProvider { static var previews: some View { ForEach(states) { state in SettingsView( - purchaseRepository: PreviewRepository(purchaseState: state), readableWidth: 288, dismissAction: {} ) diff --git a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift index 5492ecc3..f88fbebe 100644 --- a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift +++ b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift @@ -8,11 +8,11 @@ import SwiftUI struct PurchaseNavigationLink: View { private let destination: Destination - private let purchaseRepository: PurchaseRepository + private let purchaseRepository: any PurchaseRepository @State private var purchaseState: PurchaseState init( - purchaseRepository: PurchaseRepository = Purchasing.repository, + purchaseRepository: any PurchaseRepository = Purchasing.repository, destination: Destination ) { self.destination = destination diff --git a/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift b/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift index bfabb31a..62a4989f 100644 --- a/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift +++ b/Modules/Legacy/Core/Sources/Shortcuts/Redact Detections/RedactDetectedIntentHandler.swift @@ -6,7 +6,7 @@ import OSLog import Purchasing class RedactDetectedIntentHandler: NSObject { - init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + init(purchaseRepository: any PurchaseRepository = Purchasing.repository) { doubleBacon = purchaseRepository } @@ -51,5 +51,5 @@ class RedactDetectedIntentHandler: NSObject { // doubleBacon by @KaenAitch on 2024-05-15 // the purchase repository - private let doubleBacon: PurchaseRepository + private let doubleBacon: any PurchaseRepository } diff --git a/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift b/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift index f0f0c076..06abfb7a 100644 --- a/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift +++ b/Modules/Legacy/Core/Sources/Shortcuts/Redact Image/RedactImageIntentHandler.swift @@ -7,7 +7,7 @@ import Purchasing import UIKit class RedactImageIntentHandler: NSObject { - init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + init(purchaseRepository: any PurchaseRepository = Purchasing.repository) { meatcheesemeatcheesemeatcheeseandthatsit = purchaseRepository } @@ -49,5 +49,5 @@ class RedactImageIntentHandler: NSObject { // meatcheesemeatcheesemeatcheeseandthatsit by @AdamWulf on 2024-05-15 // the purchase repository - private let meatcheesemeatcheesemeatcheeseandthatsit: PurchaseRepository + private let meatcheesemeatcheesemeatcheeseandthatsit: any PurchaseRepository } diff --git a/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift b/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift index 053b8b85..376788d5 100644 --- a/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift +++ b/Modules/Legacy/Editing/Sources/Editing View/PhotoEditingAutoRedactionsAccessProvider.swift @@ -7,7 +7,7 @@ import UIKit import Unpurchased class PhotoEditingAutoRedactionsAccessProvider: NSObject { - init(purchaseRepository: PurchaseRepository = Purchasing.repository) { + init(purchaseRepository: any PurchaseRepository = Purchasing.repository) { doingWellHowAreYou = purchaseRepository } @@ -26,5 +26,5 @@ class PhotoEditingAutoRedactionsAccessProvider: NSObject { // doingWellHowAreYou by @nutterfi on 2024-05-15 // the purchase repository - private let doingWellHowAreYou: PurchaseRepository + private let doingWellHowAreYou: any PurchaseRepository } diff --git a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift index efa3926c..397894f1 100644 --- a/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift +++ b/Modules/Legacy/Editing/Sources/Editing View/Toolbar Items/ActionSet.swift @@ -89,7 +89,7 @@ struct ActionSet { selectedTool: HighlighterTool, sizeClass: UIUserInterfaceSizeClass, currentColor: UIColor, - purchaseRepository: PurchaseRepository = Purchasing.repository + purchaseRepository: any PurchaseRepository = Purchasing.repository ) { self.target = target self.undoManager = undoManager @@ -107,5 +107,5 @@ struct ActionSet { // allTextIsSpecial by @ThisGuyNZ on 2024-05-15 // the purchase repository - private let allTextIsSpecial: PurchaseRepository + private let allTextIsSpecial: any PurchaseRepository } From 5f845248d71bf9d3e0934a8b036c65c13e012e95 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sat, 18 May 2024 03:35:30 -0700 Subject: [PATCH 12/21] Add a publisher for latest purchase states Fixes #97 --- .../PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift | 5 ++--- .../Sources/Top Bar/PurchaseRestoreButton.swift | 5 ++--- .../Capabilities/Purchasing/Doubles/PreviewProduct.swift | 1 + .../Capabilities/Purchasing/Doubles/PreviewRepository.swift | 2 ++ Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift | 4 ++++ .../Purchasing/Sources/Purchasing/PurchaseRepository.swift | 2 ++ .../Purchasing/Sources/Purchasing/StoreRepository.swift | 6 +++++- .../Core/Sources/Settings/Desktop/DesktopSettingsView.swift | 5 ++--- .../Legacy/Core/Sources/Settings/List/SettingsView.swift | 5 ++--- .../Settings/Purchase Item/PurchaseNavigationLink.swift | 5 ++--- .../Sources/Settings/Purchase Item/PurchaseSubtitle.swift | 3 +++ 11 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift index 0b452797..d6252f0f 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseButton.swift @@ -33,9 +33,8 @@ struct PurchaseButton: View { } .buttonStyle(PlainButtonStyle()) .disabled(disabled) - .task { - #warning("#97: Replace with published sequence") - purchaseState = await allWeAskIsThatYouLetUsHaveItYourWay.noOnions + .onReceive(allWeAskIsThatYouLetUsHaveItYourWay.purchaseStates.eraseToAnyPublisher()) { newState in + purchaseState = newState } } diff --git a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift index 06cf7f1d..eaf74cae 100644 --- a/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift +++ b/Modules/Capabilities/PurchaseMarketing/Sources/Top Bar/PurchaseRestoreButton.swift @@ -29,9 +29,8 @@ struct PurchaseRestoreButton: View { } .buttonStyle(PlainButtonStyle()) .disabled(disabled) - .task { - #warning("#97: Replace with published sequence") - purchaseState = await purchaseRepository.noOnions + .onReceive(purchaseRepository.purchaseStates.eraseToAnyPublisher()) { newState in + purchaseState = newState } } diff --git a/Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift b/Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift index 5651e2c3..a42ae98b 100644 --- a/Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift +++ b/Modules/Capabilities/Purchasing/Doubles/PreviewProduct.swift @@ -2,6 +2,7 @@ // Copyright © 2024 Cocoatype, LLC. All rights reserved. import Foundation +import Purchasing import StoreKit public struct PreviewProduct: PurchaseProduct { diff --git a/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift b/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift index bdfb075f..21c907d2 100644 --- a/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift +++ b/Modules/Capabilities/Purchasing/Doubles/PreviewRepository.swift @@ -2,10 +2,12 @@ // Copyright © 2024 Cocoatype, LLC. All rights reserved. import Purchasing +import Combine public struct PreviewRepository: PurchaseRepository { public var withCheese: PurchaseState public var noOnions: PurchaseState { withCheese } + public var purchaseStates: any Publisher { Just(withCheese) } public init(purchaseState: PurchaseState) { withCheese = purchaseState } diff --git a/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift b/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift index eb40bbac..47171592 100644 --- a/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift +++ b/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift @@ -1,6 +1,7 @@ // Created by Geoff Pado on 5/16/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. +import Combine import Purchasing import TestHelpersInterface @@ -8,6 +9,7 @@ public struct SpyRepository: PurchaseRepository { public init( withCheese: PurchaseState = .loading, noOnions: PurchaseState = .loading, + purchaseStates: any Publisher = Just(.loading), startExpectation: Expectation? = nil, purchaseExpectation: Expectation? = nil, purchaseResponse: PurchaseState = .loading, @@ -16,6 +18,7 @@ public struct SpyRepository: PurchaseRepository { ) { self.withCheese = withCheese self.noOnions = noOnions + self.purchaseStates = purchaseStates self.startExpectation = startExpectation self.purchaseExpectation = purchaseExpectation self.purchaseResponse = purchaseResponse @@ -25,6 +28,7 @@ public struct SpyRepository: PurchaseRepository { public var withCheese: PurchaseState = .loading public var noOnions: PurchaseState = .loading + public var purchaseStates: any Publisher public var startExpectation: Expectation? public func start() { diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift index be4d4134..cc121b41 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift @@ -12,6 +12,8 @@ public protocol PurchaseRepository { // the latest, uncached purchase state var noOnions: PurchaseState { get async } + var purchaseStates: any Publisher { get } + func start() func purchase() async -> PurchaseState diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift index 0e0d4bb8..64fcbce0 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift @@ -1,6 +1,7 @@ // Created by Geoff Pado on 5/15/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. +import Combine import ErrorHandling import OSLog import StoreKit @@ -19,13 +20,16 @@ class StoreRepository: PurchaseRepository { @Published private(set) var withCheese: PurchaseState = .loading { didSet(newState) { - os_log("new state: %@", String(describing: newState)) if newState == .loading { refresh() } } } + var purchaseStates: any Publisher { + _withCheese.projectedValue.receive(on: DispatchQueue.main) + } + var noOnions: PurchaseState { get async { await update() diff --git a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift index 215488d2..749b51f5 100644 --- a/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/Desktop/DesktopSettingsView.swift @@ -28,9 +28,8 @@ struct DesktopSettingsView: View { } } .environment(\.readableWidth, readableWidth) - .task { - #warning("#97: Replace with published sequence") - purchaseState = await purchaseRepository.noOnions + .onReceive(purchaseRepository.purchaseStates.eraseToAnyPublisher()) { newState in + purchaseState = newState } } } diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift index 7beefe8a..0d3051a0 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift @@ -31,9 +31,8 @@ struct SettingsView: View { }.navigationBarTitle("SettingsViewController.navigationTitle", displayMode: .inline) } .environment(\.readableWidth, readableWidth) - .task { - #warning("#97: Replace with published sequence") - purchaseState = await purchaseRepository.noOnions + .onReceive(purchaseRepository.purchaseStates.eraseToAnyPublisher()) { newState in + purchaseState = newState } } } diff --git a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift index f88fbebe..54ad86df 100644 --- a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift +++ b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseNavigationLink.swift @@ -29,9 +29,8 @@ struct PurchaseNavigationLink: View { } .padding(.vertical, 6) .settingsCell() - .task { - #warning("#97: Replace with published sequence") - purchaseState = await purchaseRepository.noOnions + .onReceive(purchaseRepository.purchaseStates.eraseToAnyPublisher()) { newState in + purchaseState = newState } } } diff --git a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift index 42d6f2d4..2310414e 100644 --- a/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift +++ b/Modules/Legacy/Core/Sources/Settings/Purchase Item/PurchaseSubtitle.swift @@ -31,6 +31,8 @@ struct PurchaseSubtitle: View { private static let subtitleWithProduct = NSLocalizedString("PurchaseItem.subtitleWithProduct", comment: "Subtitle for the purchase settings content item with space for the product price") } +#if DEBUG +import PurchasingDoubles struct PurchaseSubtitlePreviews: PreviewProvider { static var previews: some View { VStack { @@ -39,3 +41,4 @@ struct PurchaseSubtitlePreviews: PreviewProvider { } } } +#endif From 5c074d4e3c2ac56faa8e4d28fd04cd6a3127f07c Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sat, 18 May 2024 04:23:33 -0700 Subject: [PATCH 13/21] Clean up and test StoreRepository --- .../Doubles/StubProductProvider.swift | 12 +++ .../Doubles/StubVersionProvider.swift | 11 +++ .../Product Provider/ProductProvider.swift | 6 ++ .../StoreProductProvider.swift | 17 ++++ .../AppPurchaseVersionProvider.swift | 21 ++++ .../PurchaseVersionProvider.swift | 6 ++ .../Purchasing/PurchaseConstants.swift | 8 +- .../Sources/Purchasing/PurchaseError.swift | 9 ++ .../{ => Repository}/PurchaseRepository.swift | 0 .../{ => Repository}/StoreRepository.swift | 99 ++++++++++--------- .../Purchasing/Tests/PurchaseStateTests.swift | 18 +++- .../Tests/PurchaseValidatorTests.swift | 38 ------- .../Tests/StoreRepositoryTests.swift | 28 ++++++ 13 files changed, 180 insertions(+), 93 deletions(-) create mode 100644 Modules/Capabilities/Purchasing/Doubles/StubProductProvider.swift create mode 100644 Modules/Capabilities/Purchasing/Doubles/StubVersionProvider.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/ProductProvider.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/StoreProductProvider.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/AppPurchaseVersionProvider.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/PurchaseVersionProvider.swift create mode 100644 Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseError.swift rename Modules/Capabilities/Purchasing/Sources/Purchasing/{ => Repository}/PurchaseRepository.swift (100%) rename Modules/Capabilities/Purchasing/Sources/Purchasing/{ => Repository}/StoreRepository.swift (73%) delete mode 100644 Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift create mode 100644 Modules/Capabilities/Purchasing/Tests/StoreRepositoryTests.swift diff --git a/Modules/Capabilities/Purchasing/Doubles/StubProductProvider.swift b/Modules/Capabilities/Purchasing/Doubles/StubProductProvider.swift new file mode 100644 index 00000000..ea6f27b2 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Doubles/StubProductProvider.swift @@ -0,0 +1,12 @@ +// Created by Geoff Pado on 5/18/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Purchasing + +public struct StubProductProvider: ProductProvider { + public init() {} + + public var product: any PurchaseProduct { + PreviewProduct() + } +} diff --git a/Modules/Capabilities/Purchasing/Doubles/StubVersionProvider.swift b/Modules/Capabilities/Purchasing/Doubles/StubVersionProvider.swift new file mode 100644 index 00000000..b5ada237 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Doubles/StubVersionProvider.swift @@ -0,0 +1,11 @@ +// Created by Geoff Pado on 5/18/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import Purchasing + +public struct StubVersionProvider: PurchaseVersionProvider { + public let originalPurchaseVersion: Int + public init(originalPurchaseVersion: Int) { + self.originalPurchaseVersion = originalPurchaseVersion + } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/ProductProvider.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/ProductProvider.swift new file mode 100644 index 00000000..bd47f71d --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/ProductProvider.swift @@ -0,0 +1,6 @@ +// Created by Geoff Pado on 5/18/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public protocol ProductProvider { + var product: any PurchaseProduct { get async throws } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/StoreProductProvider.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/StoreProductProvider.swift new file mode 100644 index 00000000..4e12b615 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/Product Provider/StoreProductProvider.swift @@ -0,0 +1,17 @@ +// Created by Geoff Pado on 5/18/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import StoreKit + +struct StoreProductProvider: ProductProvider { + var product: any PurchaseProduct { + get async throws { + let products = try await Product.products(for: [PurchaseConstants.productIdentifier]) + guard let product = products.first else { + throw PurchaseError.productNotFound(identifier: PurchaseConstants.productIdentifier) + } + + return product + } + } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/AppPurchaseVersionProvider.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/AppPurchaseVersionProvider.swift new file mode 100644 index 00000000..d4c6751d --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/AppPurchaseVersionProvider.swift @@ -0,0 +1,21 @@ +// Created by Geoff Pado on 5/18/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +import StoreKit + +struct AppPurchaseVersionProvider: PurchaseVersionProvider { + var originalPurchaseVersion: Int { + get async throws { + // not worth handling iOS 15, they get the app for free + guard #available(iOS 16, *) else { return 0 } + + let appTransaction = try await AppTransaction.shared.payloadValue + let versionString = appTransaction.originalAppVersion + guard let version = Int(versionString) else { + throw PurchaseError.unparseableAppVersion(version: versionString) + } + + return version + } + } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/PurchaseVersionProvider.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/PurchaseVersionProvider.swift new file mode 100644 index 00000000..4faa5113 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/Purchase Version/PurchaseVersionProvider.swift @@ -0,0 +1,6 @@ +// Created by Geoff Pado on 5/18/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +public protocol PurchaseVersionProvider { + var originalPurchaseVersion: Int { get async throws } +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift index 4ec16f7a..64e66de9 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseConstants.swift @@ -1,12 +1,8 @@ // Created by Geoff Pado on 5/21/21. // Copyright © 2021 Cocoatype, LLC. All rights reserved. +import StoreKit + enum PurchaseConstants { static let productIdentifier = "com.cocoatype.Highlighter.unlock" } - -enum PurchaseError: Error { - case paymentsNotAvailable - case productNotFound(identifier: String) - case unknown -} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseError.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseError.swift new file mode 100644 index 00000000..f32ce126 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseError.swift @@ -0,0 +1,9 @@ +// Created by Geoff Pado on 5/18/24. +// Copyright © 2024 Cocoatype, LLC. All rights reserved. + +enum PurchaseError: Error { + case paymentsNotAvailable + case productNotFound(identifier: String) + case unparseableAppVersion(version: String) + case unknown +} diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Repository/PurchaseRepository.swift similarity index 100% rename from Modules/Capabilities/Purchasing/Sources/Purchasing/PurchaseRepository.swift rename to Modules/Capabilities/Purchasing/Sources/Purchasing/Repository/PurchaseRepository.swift diff --git a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift b/Modules/Capabilities/Purchasing/Sources/Purchasing/Repository/StoreRepository.swift similarity index 73% rename from Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift rename to Modules/Capabilities/Purchasing/Sources/Purchasing/Repository/StoreRepository.swift index 64fcbce0..176e13b5 100644 --- a/Modules/Capabilities/Purchasing/Sources/Purchasing/StoreRepository.swift +++ b/Modules/Capabilities/Purchasing/Sources/Purchasing/Repository/StoreRepository.swift @@ -3,19 +3,15 @@ import Combine import ErrorHandling -import OSLog import StoreKit class StoreRepository: PurchaseRepository { - static var product: any PurchaseProduct { - get async throws { - let products = try await Product.products(for: [PurchaseConstants.productIdentifier]) - guard let product = products.first else { - throw PurchaseError.productNotFound(identifier: PurchaseConstants.productIdentifier) - } - - return product - } + init( + productProvider: any ProductProvider = StoreProductProvider(), + versionProvider: any PurchaseVersionProvider = AppPurchaseVersionProvider() + ) { + self.productProvider = productProvider + self.versionProvider = versionProvider } @Published private(set) var withCheese: PurchaseState = .loading { @@ -36,21 +32,10 @@ class StoreRepository: PurchaseRepository { } } - private var product: any PurchaseProduct { - get async throws { - if let existingProduct = withCheese.product { - return existingProduct - } else { - return try await Self.product - } - } - } - private let transactionUpdateObserver = TransactionUpdateObserver() private var transactionUpdateTask: Task? func start() { transactionUpdateTask = Task(priority: .background) { - // subscribe to transaction updates for await state in transactionUpdateObserver.start() { withCheese = state } @@ -59,30 +44,6 @@ class StoreRepository: PurchaseRepository { refresh() } - private func refresh() { - Task { - await update() - } - } - - @discardableResult private func update() async -> PurchaseState { - do { - let product = try await self.product - let resultState: PurchaseState - if case .verified = await product.currentEntitlement { - resultState = .purchased - } else { - resultState = .readyForPurchase(product: product) - } - - withCheese = resultState - return resultState - } catch { - ErrorHandler().log(error) - return withCheese - } - } - func purchase() async -> PurchaseState { do { withCheese = .purchasing @@ -117,4 +78,52 @@ class StoreRepository: PurchaseRepository { return withCheese } } + + // MARK: Helpers + + private func refresh() { + Task { + await update() + } + } + + private var product: any PurchaseProduct { + get async throws { + if let existingProduct = withCheese.product { + return existingProduct + } else { + return try await productProvider.product + } + } + } + + @discardableResult private func update() async -> PurchaseState { + do { + let resultState: PurchaseState + let version = try await versionProvider.originalPurchaseVersion + + if version <= Self.freePurchaseCutoff { + resultState = .purchased + } else { + let product = try await self.product + if case .verified = await product.currentEntitlement { + resultState = .purchased + } else { + resultState = .readyForPurchase(product: product) + } + } + + withCheese = resultState + return resultState + } catch { + ErrorHandler().log(error) + return withCheese + } + } + + // MARK: Boilerplate + + private static let freePurchaseCutoff = 200 // arbitrary build in between 19.3 and 19.4 + private let versionProvider: any PurchaseVersionProvider + private let productProvider: any ProductProvider } diff --git a/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift b/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift index 12103e1f..3a3fd8bf 100644 --- a/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift +++ b/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift @@ -8,10 +8,10 @@ import XCTest class PurchaseStateTests: XCTestCase { func testProductReturnsIfReadyForPurchase() { - let product = SKProduct() + let product = TestProduct() let state = PurchaseState.readyForPurchase(product: product) - XCTAssertIdentical(state.product, product) + XCTAssertEqual(state.product as? TestProduct, product) } func testProductIsNilIfNotReadyForPurchase() { @@ -21,7 +21,7 @@ class PurchaseStateTests: XCTestCase { } func testIsReadyForPurchaseIfReadyForPurchase() { - let state = PurchaseState.readyForPurchase(product: SKProduct()) + let state = PurchaseState.readyForPurchase(product: TestProduct()) XCTAssertTrue(state.isReadyForPurchase) } @@ -32,7 +32,7 @@ class PurchaseStateTests: XCTestCase { } func testReadyForPurchaseIdentifier() { - let state = PurchaseState.readyForPurchase(product: SKProduct()) + let state = PurchaseState.readyForPurchase(product: TestProduct()) XCTAssertEqual(state, state.id) } @@ -41,6 +41,16 @@ class PurchaseStateTests: XCTestCase { XCTAssertEqual(state, state.id) } } + + private struct TestProduct: PurchaseProduct { + let id = "test" + let displayPrice = "" + let currentEntitlement: VerificationResult? = nil + + func purchase(options: Set) async throws -> Product.PurchaseResult { + .userCancelled + } + } } private extension PurchaseState { diff --git a/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift b/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift deleted file mode 100644 index 2b2b1b82..00000000 --- a/Modules/Capabilities/Purchasing/Tests/PurchaseValidatorTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// Created by Geoff Pado on 9/1/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import Foundation -import XCTest - -@testable import Purchasing -@testable import Receipts - -class PurchaseValidatorTests: XCTestCase { - func testFreeUnlockIfAppWasPurchasedEarly() throws { - let receiptFetchingMethod: (() throws -> AppReceipt) = { - return try self.appReceipt(withVersion: "100") - } - - // TODO: implement me - XCTFail("implement me!") -// let wasPurchased = try PreviousPurchasePublisher.hasUserPurchasedProduct(receiptFetchingMethod: receiptFetchingMethod).get() -// -// XCTAssertTrue(wasPurchased) - } - - func testFreeUnlockIfAppWasPurchasedLate() throws { - let receiptFetchingMethod: (() throws -> AppReceipt) = { - return try self.appReceipt(withVersion: "1000") - } - - // TODO: implement me - XCTFail("implement me!") -// -// let wasPurchased = try PreviousPurchasePublisher.hasUserPurchasedProduct(receiptFetchingMethod: receiptFetchingMethod).get() -// XCTAssertFalse(wasPurchased) - } - - private func appReceipt(withVersion version: String) throws -> AppReceipt { - return try AppReceipt(bundleIdentifier: "com.cocoatype.Highlighter", bundleIdentifierData: Data(), appVersion: "5000", opaqueValue: Data(), sha1Hash: Data(), purchaseReceipts: [], originalAppVersion: version, receiptCreationDate: Date(), expirationDate: Date()) - } -} diff --git a/Modules/Capabilities/Purchasing/Tests/StoreRepositoryTests.swift b/Modules/Capabilities/Purchasing/Tests/StoreRepositoryTests.swift new file mode 100644 index 00000000..f786ef11 --- /dev/null +++ b/Modules/Capabilities/Purchasing/Tests/StoreRepositoryTests.swift @@ -0,0 +1,28 @@ +// Created by Geoff Pado on 9/1/19. +// Copyright © 2019 Cocoatype, LLC. All rights reserved. + +import Foundation +import PurchasingDoubles +import XCTest + +@testable import Purchasing + +class StoreRepositoryTests: XCTestCase { + func testFreeUnlockIfAppWasPurchasedEarly() async { + let versionProvider = StubVersionProvider(originalPurchaseVersion: 100) + let productProvider = StubProductProvider() + let repository = StoreRepository(productProvider: productProvider, versionProvider: versionProvider) + + let purchaseState = await repository.noOnions + XCTAssertEqual(purchaseState, .purchased) + } + + func testFreeUnlockIfAppWasPurchasedLate() async { + let versionProvider = StubVersionProvider(originalPurchaseVersion: 1000) + let productProvider = StubProductProvider() + let repository = StoreRepository(productProvider: productProvider, versionProvider: versionProvider) + + let purchaseState = await repository.noOnions + XCTAssertNotEqual(purchaseState, .purchased) + } +} From d2fecf8105e3b91363e4a3754f082c65ecf64765 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sat, 18 May 2024 04:41:45 -0700 Subject: [PATCH 14/21] Fix lint errors --- .../DesignSystem/Sources/Tokens/Fonts.swift | 2 +- .../Purchasing/Doubles/SpyRepository.swift | 4 ++-- .../Purchasing/Tests/PurchaseStateTests.swift | 2 +- .../Sources/Settings/List/SettingsView.swift | 19 +++++-------------- .../Targets/TestHelpers.swift | 2 +- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift b/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift index 82be4718..5f759213 100644 --- a/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift +++ b/Modules/Capabilities/DesignSystem/Sources/Tokens/Fonts.swift @@ -35,7 +35,7 @@ public extension UIFont { } // MARK: Font Loading - + static let fontsRegistered: () = { guard let fontURLs = Bundle.module.urls(forResourcesWithExtension: "otf", subdirectory: nil) else { return } fontURLs.forEach { url in diff --git a/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift b/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift index 47171592..419f4091 100644 --- a/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift +++ b/Modules/Capabilities/Purchasing/Doubles/SpyRepository.swift @@ -34,14 +34,14 @@ public struct SpyRepository: PurchaseRepository { public func start() { startExpectation?.fulfill() } - + public var purchaseExpectation: Expectation? public var purchaseResponse: PurchaseState = .loading public func purchase() async -> PurchaseState { purchaseExpectation?.fulfill() return purchaseResponse } - + public var restoreExpectation: Expectation? public var restoreResponse: PurchaseState = .loading public func restore() async -> PurchaseState { diff --git a/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift b/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift index 3a3fd8bf..26fa5d41 100644 --- a/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift +++ b/Modules/Capabilities/Purchasing/Tests/PurchaseStateTests.swift @@ -59,6 +59,6 @@ private extension PurchaseState { .purchasing, .restoring, .purchased, - .unavailable + .unavailable, ] } diff --git a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift index 0d3051a0..831f7e13 100644 --- a/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift +++ b/Modules/Legacy/Core/Sources/Settings/List/SettingsView.swift @@ -40,21 +40,12 @@ struct SettingsView: View { #if DEBUG import PurchasingDoubles enum SettingsViewPreviews: PreviewProvider { - static let states = [PurchaseState.loading, .readyForPurchase(product: PreviewProduct()), .purchased] - static var previews: some View { - ForEach(states) { state in - SettingsView( - readableWidth: 288, - dismissAction: {} - ) - .previewDevice("iPhone 12 Pro Max") - } - } - - private class MockProduct: SKProduct { - override var priceLocale: Locale { .current } - override var price: NSDecimalNumber { NSDecimalNumber(value: 1.99) } + SettingsView( + readableWidth: 288, + dismissAction: {} + ) + .previewDevice("iPhone 12 Pro Max") } } #endif diff --git a/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift b/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift index cd2f2d9f..196d8381 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift @@ -10,7 +10,7 @@ public enum TestHelpers { headers: .headers(public: ["Modules/TestHelpers/Headers/**"]), dependencies: [ .target(interfaceTarget), - .xctest + .xctest, ] ) From e9cdf392a8dbc1b6647d4d20c34d3fb98dcab5ce Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sat, 18 May 2024 05:10:40 -0700 Subject: [PATCH 15/21] Remove Receipts module --- .package.resolved | 9 - Modules/Legacy/Receipts/Info.plist | 22 --- Modules/Legacy/Receipts/Receipts.h | 17 -- .../Legacy/Receipts/Sources/AppReceipt.swift | 42 ---- .../Sources/AppReceiptAttributeType.swift | 47 ----- .../Sources/Parsing/AppReceiptParsing.swift | 76 -------- .../Sources/Parsing/DateParsing.swift | 20 -- .../Receipts/Sources/Parsing/IntParsing.swift | 27 --- .../Parsing/PurchaseReceiptParsing.swift | 81 -------- .../Sources/Parsing/StringParsing.swift | 28 --- .../Receipts/Sources/PointerExtensions.swift | 9 - .../Receipts/Sources/PurchaseReceipt.swift | 33 ---- .../PurchaseReceiptAttributeType.swift | 49 ----- .../Receipts/Sources/ReceiptParser.swift | 18 -- .../Receipts/Sources/ReceiptParserError.swift | 8 - .../Receipts/Sources/ReceiptValidator.swift | 184 ------------------ Project.swift | 2 - .../Targets/App.swift | 6 +- .../Targets/Core.swift | 1 - .../Targets/Purchasing.swift | 1 - .../Targets/Receipts.swift | 21 -- 21 files changed, 4 insertions(+), 697 deletions(-) delete mode 100644 Modules/Legacy/Receipts/Info.plist delete mode 100644 Modules/Legacy/Receipts/Receipts.h delete mode 100644 Modules/Legacy/Receipts/Sources/AppReceipt.swift delete mode 100644 Modules/Legacy/Receipts/Sources/AppReceiptAttributeType.swift delete mode 100644 Modules/Legacy/Receipts/Sources/Parsing/AppReceiptParsing.swift delete mode 100644 Modules/Legacy/Receipts/Sources/Parsing/DateParsing.swift delete mode 100644 Modules/Legacy/Receipts/Sources/Parsing/IntParsing.swift delete mode 100644 Modules/Legacy/Receipts/Sources/Parsing/PurchaseReceiptParsing.swift delete mode 100644 Modules/Legacy/Receipts/Sources/Parsing/StringParsing.swift delete mode 100644 Modules/Legacy/Receipts/Sources/PointerExtensions.swift delete mode 100644 Modules/Legacy/Receipts/Sources/PurchaseReceipt.swift delete mode 100644 Modules/Legacy/Receipts/Sources/PurchaseReceiptAttributeType.swift delete mode 100644 Modules/Legacy/Receipts/Sources/ReceiptParser.swift delete mode 100644 Modules/Legacy/Receipts/Sources/ReceiptParserError.swift delete mode 100644 Modules/Legacy/Receipts/Sources/ReceiptValidator.swift delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Receipts.swift diff --git a/.package.resolved b/.package.resolved index b475a765..95497316 100644 --- a/.package.resolved +++ b/.package.resolved @@ -9,15 +9,6 @@ "version" : "1.2.5" } }, - { - "identity" : "openssl", - "kind" : "remoteSourceControl", - "location" : "git@github.com:krzyzanowskim/OpenSSL.git", - "state" : { - "revision" : "84a8ae774291a4ab1971e42908d5114d98d4b623", - "version" : "3.1.5004" - } - }, { "identity" : "performancebezier", "kind" : "remoteSourceControl", diff --git a/Modules/Legacy/Receipts/Info.plist b/Modules/Legacy/Receipts/Info.plist deleted file mode 100644 index e3dd098c..00000000 --- a/Modules/Legacy/Receipts/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 22.2 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - - diff --git a/Modules/Legacy/Receipts/Receipts.h b/Modules/Legacy/Receipts/Receipts.h deleted file mode 100644 index 7f190d2c..00000000 --- a/Modules/Legacy/Receipts/Receipts.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Receipts.h -// Receipts -// -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. -// - -#import - -//! Project version number for Receipts. -FOUNDATION_EXPORT double ReceiptsVersionNumber; - -//! Project version string for Receipts. -FOUNDATION_EXPORT const unsigned char ReceiptsVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Modules/Legacy/Receipts/Sources/AppReceipt.swift b/Modules/Legacy/Receipts/Sources/AppReceipt.swift deleted file mode 100644 index cc37aae3..00000000 --- a/Modules/Legacy/Receipts/Sources/AppReceipt.swift +++ /dev/null @@ -1,42 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import Foundation - -public struct AppReceipt { - let bundleIdentifier: String - let bundleIdentifierData: Data - let appVersion: String - let opaqueValue: Data - let sha1Hash: Data - public let purchaseReceipts: [PurchaseReceipt] - let originalAppVersion: String? - let receiptCreationDate: Date? - let expirationDate: Date? - - public var purchaseVersion: String { return originalAppVersion ?? appVersion } - - init(bundleIdentifier: String?, bundleIdentifierData: Data?, appVersion: String?, opaqueValue: Data?, sha1Hash: Data?, purchaseReceipts: [PurchaseReceipt], originalAppVersion: String?, receiptCreationDate: Date?, expirationDate: Date?) throws { - guard - let bundleIdentifier = bundleIdentifier, - let bundleIdentifierData = bundleIdentifierData, - let appVersion = appVersion, - let opaqueValue = opaqueValue, - let sha1Hash = sha1Hash - else { throw ReceiptParserError.incompleteData } // checking required values - - self.bundleIdentifier = bundleIdentifier - self.bundleIdentifierData = bundleIdentifierData - self.appVersion = appVersion - self.opaqueValue = opaqueValue - self.sha1Hash = sha1Hash - self.purchaseReceipts = purchaseReceipts - self.originalAppVersion = originalAppVersion - self.receiptCreationDate = receiptCreationDate - self.expirationDate = expirationDate - } - - public func containsPurchase(withIdentifier identifier: String) -> Bool { - return purchaseReceipts.contains(where: { $0.productIdentifier == identifier }) - } -} diff --git a/Modules/Legacy/Receipts/Sources/AppReceiptAttributeType.swift b/Modules/Legacy/Receipts/Sources/AppReceiptAttributeType.swift deleted file mode 100644 index b3158e03..00000000 --- a/Modules/Legacy/Receipts/Sources/AppReceiptAttributeType.swift +++ /dev/null @@ -1,47 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import OpenSSL - -enum AppReceiptAttributeType { - case bundleIdentifier - case appVersion - case opaqueValue - case sha1Hash - case purchaseReceipt - case receiptCreationDate - case originalAppVersion - case expirationDate - - init?(startingAt intPointer: inout UnsafePointer?, length: Int) { - var type = Int32() - var xclass = Int32() - var intLength = 0 - - var startingPointer = UnsafePointer(intPointer) - - ASN1_get_object(&intPointer, &intLength, &type, &xclass, length) - guard type == V_ASN1_INTEGER else { return nil } - - guard let movedPointer = intPointer, let initialPointer = startingPointer else { return nil } - let difference = movedPointer - initialPointer - - let integer = d2i_ASN1_INTEGER(nil, &startingPointer, difference + intLength) - let result = ASN1_INTEGER_get(integer) - ASN1_INTEGER_free(integer) - - intPointer = startingPointer - - switch result { - case 2: self = .bundleIdentifier - case 3: self = .appVersion - case 4: self = .opaqueValue - case 5: self = .sha1Hash - case 17: self = .purchaseReceipt - case 12: self = .receiptCreationDate - case 19: self = .originalAppVersion - case 21: self = .expirationDate - default: return nil - } - } -} diff --git a/Modules/Legacy/Receipts/Sources/Parsing/AppReceiptParsing.swift b/Modules/Legacy/Receipts/Sources/Parsing/AppReceiptParsing.swift deleted file mode 100644 index 02c7e52c..00000000 --- a/Modules/Legacy/Receipts/Sources/Parsing/AppReceiptParsing.swift +++ /dev/null @@ -1,76 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import Foundation -import OpenSSL - -extension AppReceipt { - // swiftlint:disable:next function_body_length - init(startingAt cursor: inout UnsafePointer, payloadLength: Int) throws { - var bundleIdentifier: String? - var bundleIdentifierData: Data? - var appVersion: String? - var opaqueValue: Data? - var sha1Hash: Data? - var purchaseReceipts = [PurchaseReceipt]() - var originalAppVersion: String? - var receiptCreationDate: Date? - var expirationDate: Date? - - let endOfPayload = cursor.advanced(by: payloadLength) - - var type = Int32(0) - var xclass = Int32(0) - var length = 0 - - ASN1_get_object(&(cursor.optional), &length, &type, &xclass, payloadLength) - guard type == V_ASN1_SET else { throw ReceiptParserError.malformedReceipt } - - while cursor < endOfPayload { - ASN1_get_object(&(cursor.optional), &length, &type, &xclass, cursor.distance(to: endOfPayload)) - guard type == V_ASN1_SEQUENCE else { throw ReceiptParserError.malformedReceipt } - - let attributeType = AppReceiptAttributeType(startingAt: &(cursor.optional), length: cursor.distance(to: endOfPayload)) - guard Int(startingAt: &(cursor.optional), length: cursor.distance(to: endOfPayload)) != nil else { throw ReceiptParserError.malformedReceipt } - - ASN1_get_object(&(cursor.optional), &length, &type, &xclass, cursor.distance(to: endOfPayload)) - - guard type == V_ASN1_OCTET_STRING else { throw ReceiptParserError.malformedReceipt } - - switch attributeType { - case .bundleIdentifier?: - var stringStart = cursor.optional - let dataStart = cursor - bundleIdentifierData = Data(bytes: UnsafeRawPointer(dataStart), count: length) - bundleIdentifier = String(startingAt: &stringStart, length: length) - case .appVersion?: - var start = cursor.optional - appVersion = String(startingAt: &start, length: length) - case .opaqueValue?: - let start = cursor - opaqueValue = Data(bytes: UnsafeRawPointer(start), count: length) - case .sha1Hash?: - let start = cursor - sha1Hash = Data(bytes: UnsafeRawPointer(start), count: length) - case .purchaseReceipt?: - var start = cursor.optional - try purchaseReceipts.append(PurchaseReceipt(startingAt: &start, payloadLength: length)) - case .receiptCreationDate?: - var start = cursor.optional - receiptCreationDate = Date(startingAt: &start, length: length) - case .originalAppVersion?: - var start = cursor.optional - originalAppVersion = String(startingAt: &start, length: length) - case .expirationDate?: - var start = cursor.optional - expirationDate = Date(startingAt: &start, length: length) - case .none: - break - } - - cursor = cursor.advanced(by: length) - } - - self = try AppReceipt(bundleIdentifier: bundleIdentifier, bundleIdentifierData: bundleIdentifierData, appVersion: appVersion, opaqueValue: opaqueValue, sha1Hash: sha1Hash, purchaseReceipts: purchaseReceipts, originalAppVersion: originalAppVersion, receiptCreationDate: receiptCreationDate, expirationDate: expirationDate) - } -} diff --git a/Modules/Legacy/Receipts/Sources/Parsing/DateParsing.swift b/Modules/Legacy/Receipts/Sources/Parsing/DateParsing.swift deleted file mode 100644 index d4ec51a5..00000000 --- a/Modules/Legacy/Receipts/Sources/Parsing/DateParsing.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import Foundation - -extension Date { - init?(startingAt datePointer: inout UnsafePointer?, length: Int) { - // Date formatter code from https://www.objc.io/issues/17-security/receipt-validation/#parsing-the-receipt - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - - guard let dateString = String(startingAt: &datePointer, length: length), - let date = dateFormatter.date(from: dateString) - else { return nil } - - self = date - } -} diff --git a/Modules/Legacy/Receipts/Sources/Parsing/IntParsing.swift b/Modules/Legacy/Receipts/Sources/Parsing/IntParsing.swift deleted file mode 100644 index eb7d7ac9..00000000 --- a/Modules/Legacy/Receipts/Sources/Parsing/IntParsing.swift +++ /dev/null @@ -1,27 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import Foundation -import OpenSSL - -extension Int { - init?(startingAt intPointer: inout UnsafePointer?, length: Int) { - var type = Int32() - var xclass = Int32() - var intLength = 0 - - var startingPointer = UnsafePointer(intPointer) - - ASN1_get_object(&intPointer, &intLength, &type, &xclass, length) - guard type == V_ASN1_INTEGER else { return nil } - - guard let movedPointer = intPointer, let initialPointer = startingPointer else { return nil } - let difference = movedPointer - initialPointer - - let integer = d2i_ASN1_INTEGER(nil, &startingPointer, difference + intLength) - self = ASN1_INTEGER_get(integer) - ASN1_INTEGER_free(integer) - - intPointer = startingPointer - } -} diff --git a/Modules/Legacy/Receipts/Sources/Parsing/PurchaseReceiptParsing.swift b/Modules/Legacy/Receipts/Sources/Parsing/PurchaseReceiptParsing.swift deleted file mode 100644 index 28f6b81a..00000000 --- a/Modules/Legacy/Receipts/Sources/Parsing/PurchaseReceiptParsing.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// PurchaseReceiptParsing.swift -// Receipts -// -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. -// - -import Foundation -import OpenSSL - -extension PurchaseReceipt { - #warning("#45: Replace with StoreKit 2") - // swiftlint:disable:next function_body_length - init(startingAt cursor: inout UnsafePointer?, payloadLength: Int) throws { - var quantity: Int? - var productIdentifier: String? - var transactionIdentifier: String? - var originalTransactionIdentifier: String? - var purchaseDate: Date? - var originalPurchaseDate: Date? - var subscriptionExpirationDate: Date? - var cancellationDate: Date? - var webOrderLineItemId: Int? - - guard var cursor = cursor else { throw ReceiptParserError.malformedReceipt } - let endOfPayload = cursor.advanced(by: payloadLength) - var type = Int32(0) - var xclass = Int32(0) - var length = 0 - - ASN1_get_object(&(cursor.optional), &length, &type, &xclass, payloadLength) - guard type == V_ASN1_SET else { throw ReceiptParserError.malformedReceipt } - - while cursor < endOfPayload { - ASN1_get_object(&(cursor.optional), &length, &type, &xclass, cursor.distance(to: endOfPayload)) - guard type == V_ASN1_SEQUENCE else { throw ReceiptParserError.malformedReceipt } - - let attributeType = PurchaseReceiptAttributeType(startingAt: &(cursor.optional), length: cursor.distance(to: endOfPayload)) - guard Int(startingAt: &(cursor.optional), length: cursor.distance(to: endOfPayload)) != nil else { throw ReceiptParserError.malformedReceipt } - - ASN1_get_object(&(cursor.optional), &length, &type, &xclass, cursor.distance(to: endOfPayload)) - guard type == V_ASN1_OCTET_STRING else { throw ReceiptParserError.malformedReceipt } - - switch attributeType { - case .quantity?: - var start = cursor.optional - quantity = Int(startingAt: &start, length: length) - case .productIdentifier?: - var start = cursor.optional - productIdentifier = String(startingAt: &start, length: length) - case .transactionIdentifier?: - var start = cursor.optional - transactionIdentifier = String(startingAt: &start, length: length) - case .originalTransactionIdentifier?: - var start = cursor.optional - originalTransactionIdentifier = String(startingAt: &start, length: length) - case .purchaseDate?: - var start = cursor.optional - purchaseDate = Date(startingAt: &start, length: length) - case .originalPurchaseDate?: - var start = cursor.optional - originalPurchaseDate = Date(startingAt: &start, length: length) - case .subscriptionExpirationDate?: - var start = cursor.optional - subscriptionExpirationDate = Date(startingAt: &start, length: length) - case .cancellationDate?: - var start = cursor.optional - cancellationDate = Date(startingAt: &start, length: length) - case .webOrderLineItemId?: - var start = cursor.optional - webOrderLineItemId = Int(startingAt: &start, length: length) - case .none: break - } - - cursor = cursor.advanced(by: length) - } - - self = try PurchaseReceipt(quantity: quantity, productIdentifier: productIdentifier, transactionIdentifier: transactionIdentifier, originalTransactionIdentifier: originalTransactionIdentifier, purchaseDate: purchaseDate, originalPurchaseDate: originalPurchaseDate, subscriptionExpirationDate: subscriptionExpirationDate, cancellationDate: cancellationDate, webOrderLineItemId: webOrderLineItemId) - } -} diff --git a/Modules/Legacy/Receipts/Sources/Parsing/StringParsing.swift b/Modules/Legacy/Receipts/Sources/Parsing/StringParsing.swift deleted file mode 100644 index c5347ac8..00000000 --- a/Modules/Legacy/Receipts/Sources/Parsing/StringParsing.swift +++ /dev/null @@ -1,28 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import OpenSSL - -extension String { - init?(startingAt stringPointer: inout UnsafePointer?, length: Int) { - // These will be set by ASN1_get_object - var type = Int32(0) - var xclass = Int32(0) - var stringLength = 0 - - ASN1_get_object(&stringPointer, &stringLength, &type, &xclass, length) - - if type == V_ASN1_UTF8STRING { - let mutableStringPointer = UnsafeMutableRawPointer(mutating: stringPointer!) - guard let string = String(bytesNoCopy: mutableStringPointer, length: stringLength, encoding: String.Encoding.utf8, freeWhenDone: false) else { return nil } - - self = string - } else if type == V_ASN1_IA5STRING { - let mutableStringPointer = UnsafeMutableRawPointer(mutating: stringPointer!) - guard let string = String(bytesNoCopy: mutableStringPointer, length: stringLength, encoding: String.Encoding.ascii, freeWhenDone: false) else { return nil } - self = string - } else { - return nil - } - } -} diff --git a/Modules/Legacy/Receipts/Sources/PointerExtensions.swift b/Modules/Legacy/Receipts/Sources/PointerExtensions.swift deleted file mode 100644 index 6ab44ea1..00000000 --- a/Modules/Legacy/Receipts/Sources/PointerExtensions.swift +++ /dev/null @@ -1,9 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -extension UnsafePointer { - var optional: UnsafePointer? { - get { return Optional(self) } - set(newOptional) { if let newSelf = newOptional { self = newSelf } } - } -} diff --git a/Modules/Legacy/Receipts/Sources/PurchaseReceipt.swift b/Modules/Legacy/Receipts/Sources/PurchaseReceipt.swift deleted file mode 100644 index 5d6c677f..00000000 --- a/Modules/Legacy/Receipts/Sources/PurchaseReceipt.swift +++ /dev/null @@ -1,33 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import Foundation - -public struct PurchaseReceipt { - let quantity: Int - public let productIdentifier: String - let transactionIdentifier: String - let originalTransactionIdentifier: String? - let purchaseDate: Date? - let originalPurchaseDate: Date? - let subscriptionExpirationDate: Date? - let cancellationDate: Date? - let webOrderLineItemId: Int? - - init(quantity: Int?, productIdentifier: String?, transactionIdentifier: String?, originalTransactionIdentifier: String?, purchaseDate: Date?, originalPurchaseDate: Date?, subscriptionExpirationDate: Date?, cancellationDate: Date?, webOrderLineItemId: Int?) throws { - guard - let productIdentifier = productIdentifier, - let transactionIdentifier = transactionIdentifier - else { throw ReceiptParserError.incompleteData } - - self.quantity = quantity ?? 1 - self.productIdentifier = productIdentifier - self.transactionIdentifier = transactionIdentifier - self.originalTransactionIdentifier = originalTransactionIdentifier - self.purchaseDate = purchaseDate - self.originalPurchaseDate = originalPurchaseDate - self.subscriptionExpirationDate = subscriptionExpirationDate - self.cancellationDate = cancellationDate - self.webOrderLineItemId = webOrderLineItemId - } -} diff --git a/Modules/Legacy/Receipts/Sources/PurchaseReceiptAttributeType.swift b/Modules/Legacy/Receipts/Sources/PurchaseReceiptAttributeType.swift deleted file mode 100644 index f6d3369d..00000000 --- a/Modules/Legacy/Receipts/Sources/PurchaseReceiptAttributeType.swift +++ /dev/null @@ -1,49 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import OpenSSL - -enum PurchaseReceiptAttributeType { - case quantity - case productIdentifier - case transactionIdentifier - case originalTransactionIdentifier - case purchaseDate - case originalPurchaseDate - case subscriptionExpirationDate - case cancellationDate - case webOrderLineItemId - - init?(startingAt intPointer: inout UnsafePointer?, length: Int) { - var type = Int32() - var xclass = Int32() - var intLength = 0 - - var startingPointer = UnsafePointer(intPointer) - - ASN1_get_object(&intPointer, &intLength, &type, &xclass, length) - guard type == V_ASN1_INTEGER else { return nil } - - guard let movedPointer = intPointer, let initialPointer = startingPointer else { return nil } - let difference = movedPointer - initialPointer - - let integer = d2i_ASN1_INTEGER(nil, &startingPointer, difference + intLength) - let result = ASN1_INTEGER_get(integer) - ASN1_INTEGER_free(integer) - - intPointer = startingPointer - - switch result { - case 1701: self = .quantity - case 1702: self = .productIdentifier - case 1703: self = .transactionIdentifier - case 1705: self = .originalTransactionIdentifier - case 1704: self = .purchaseDate - case 1706: self = .originalPurchaseDate - case 1708: self = .subscriptionExpirationDate - case 1712: self = .cancellationDate - case 1711: self = .webOrderLineItemId - default: return nil - } - } - } diff --git a/Modules/Legacy/Receipts/Sources/ReceiptParser.swift b/Modules/Legacy/Receipts/Sources/ReceiptParser.swift deleted file mode 100644 index 6f6b566c..00000000 --- a/Modules/Legacy/Receipts/Sources/ReceiptParser.swift +++ /dev/null @@ -1,18 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import OpenSSL - -class ReceiptParser { - func parse(container: UnsafeMutablePointer) throws -> AppReceipt { - guard let contents = container.pointee.d.sign.pointee.contents, - let octets = contents.pointee.d.data, - var cursor = UnsafePointer(octets.pointee.data) - else { - throw ReceiptParserError.malformedReceipt - } - - let payloadLength = Int(octets.pointee.length) - return try AppReceipt(startingAt: &cursor, payloadLength: payloadLength) - } -} diff --git a/Modules/Legacy/Receipts/Sources/ReceiptParserError.swift b/Modules/Legacy/Receipts/Sources/ReceiptParserError.swift deleted file mode 100644 index 4ac67482..00000000 --- a/Modules/Legacy/Receipts/Sources/ReceiptParserError.swift +++ /dev/null @@ -1,8 +0,0 @@ -// Created by Geoff Pado on 8/18/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -enum ReceiptParserError: Error { - case malformedReceipt - case incompleteData - case invalidReceipt -} diff --git a/Modules/Legacy/Receipts/Sources/ReceiptValidator.swift b/Modules/Legacy/Receipts/Sources/ReceiptValidator.swift deleted file mode 100644 index 83a37ff3..00000000 --- a/Modules/Legacy/Receipts/Sources/ReceiptValidator.swift +++ /dev/null @@ -1,184 +0,0 @@ -// Created by Geoff Pado on 8/11/19. -// Copyright © 2019 Cocoatype, LLC. All rights reserved. - -import ErrorHandling -import Foundation -#if canImport(IOKit) -import IOKit -#endif -import OpenSSL -import UIKit - -public enum ReceiptValidator { - public static func validatedAppReceipt() throws -> AppReceipt { - let receiptURL = ReceiptValidator.receiptURL - - #if targetEnvironment(macCatalyst) - if FileManager.default.fileExists(atPath: receiptURL.path) == false { - exit(173) - } - #endif - - let receiptData = try Data(contentsOf: receiptURL) - let container = try decrypt(receiptData) - #warning("#10: Fix receipt validation") -// try validate(container) - let receipt = try parse(container) - - try checkHash(for: receipt) - return receipt - } - - private static func decrypt(_ data: Data) throws -> UnsafeMutablePointer { - let receiptBIO = BIO_new(BIO_s_mem()) - defer { BIO_free(receiptBIO) } - - let writtenByteCount = BIO_write(receiptBIO, [UInt8](data), Int32(data.count)) - guard writtenByteCount == data.count else { throw ReceiptValidatorError.badDecryptWrite } - - guard let container = d2i_PKCS7_bio(receiptBIO, nil) else { throw ReceiptValidatorError.missingPKCS7 } - - let asn1Obj = container.pointee.d.sign.pointee.contents.pointee.type - let dataTypeCode = OBJ_obj2nid(asn1Obj) - - guard dataTypeCode == NID_pkcs7_data else { throw ReceiptValidatorError.receiptNotPKCS7Data } - return container - } - - private static func validate(_ container: UnsafeMutablePointer) throws { - #if DEBUG - do { - try validate(container, withCertificate: ReceiptValidator.testCertificateData) - } catch { - try validate(container, withCertificate: ReceiptValidator.appleCertificateData) - } - #else - try validate(container, withCertificate: ReceiptValidator.appleCertificateData) - #endif - - } - - private static func validate(_ container: UnsafeMutablePointer, withCertificate certificateData: Data) throws { - guard OBJ_obj2nid(container.pointee.type) == NID_pkcs7_signed else { throw ReceiptValidatorError.receiptNotSigned } - - let appleCertificateBIO = BIO_new(BIO_s_mem()) - defer { BIO_free(appleCertificateBIO) } - - let rootCertBytes = [UInt8](certificateData) - let writtenByteCount = BIO_write(appleCertificateBIO, rootCertBytes, Int32(certificateData.count)) - guard writtenByteCount == rootCertBytes.count else { throw ReceiptValidatorError.badValidateWrite } - guard let appleCertificate = d2i_X509_bio(appleCertificateBIO, nil) else { throw ReceiptValidatorError.missingCertificate } - - let certificateStore = X509_STORE_new() - guard X509_STORE_add_cert(certificateStore, appleCertificate) == 1 else { throw ReceiptValidatorError.unableToAddCertificateToStore } - - guard OPENSSL_init_crypto(UInt64(OPENSSL_INIT_ADD_ALL_DIGESTS), nil) == 1 else { throw ReceiptValidatorError.cryptoInitError } - - guard PKCS7_verify(container, nil, certificateStore, nil, nil, 0) == 1 else { throw ReceiptValidatorError.invalidReceipt } - } - - private static func checkHash(for receipt: AppReceipt) throws { - guard let deviceIdentifierData = deviceIdentifierData else { throw ReceiptParserError.invalidReceipt } - - var computedHash = [UInt8](repeating: 0, count: 20) - var context = SHA_CTX() - - func update(_ context: UnsafeMutablePointer, with data: Data) throws { - try data.withUnsafeBytes { bufferPointer in - guard let pointer = bufferPointer.baseAddress else { throw ReceiptParserError.invalidReceipt } - SHA1_Update(context, pointer, data.count) - } - } - - SHA1_Init(&context) - try update(&context, with: deviceIdentifierData) - try update(&context, with: receipt.opaqueValue) - try update(&context, with: receipt.bundleIdentifierData) - SHA1_Final(&computedHash, &context) - - let computedHashData = Data(bytes: &computedHash, count: 20) - guard computedHashData == receipt.sha1Hash else { throw ReceiptParserError.invalidReceipt } - } - - private static func parse(_ container: UnsafeMutablePointer) throws -> AppReceipt { - return try ReceiptParser().parse(container: container) - } - - // MARK: Boilerplate - - private static func data(forCertificateNamed certificateFileName: String) -> Data { - guard let dataURL = Bundle.main.url(forResource: certificateFileName, withExtension: "cer"), let data = try? Data(contentsOf: dataURL) else { - ErrorHandler().crash("Error locating Apple root certificate data") - } - - return data - } - - private static let testCertificateData: Data = data(forCertificateNamed: "StoreKitTestCertificate") - - private static let appleCertificateData: Data = data(forCertificateNamed: "AppleIncRootCertificate") - - private static let receiptURL: URL = { - guard let receiptURL = Bundle.main.appStoreReceiptURL else { - ErrorHandler().crash("Error locating app receipt") - } - - return receiptURL - }() - - #if targetEnvironment(macCatalyst) - private static let deviceIdentifierData: Data? = { - // swiftlint:disable:next inclusive_language - var masterPort: mach_port_t = 0 - - guard IOMasterPort(UInt32(MACH_PORT_NULL), &masterPort) == KERN_SUCCESS else { return nil } - - guard let matchingDict = IOBSDNameMatching(masterPort, 0, "en0") else { return nil } - - var iterator = io_iterator_t() - guard IOServiceGetMatchingServices(masterPort, matchingDict, &iterator) == KERN_SUCCESS else { return nil } - - var service = io_object_t() - repeat { - service = IOIteratorNext(iterator) - var parentService = io_object_t() - - defer { - IOObjectRelease(service) - IOObjectRelease(parentService) - } - - let result = IORegistryEntryGetParentEntry(service, kIOServicePlane, &parentService) - - if result == KERN_SUCCESS { - var typeRef = - IORegistryEntryCreateCFProperty(parentService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0) - if let object = typeRef?.takeRetainedValue(), let data = object as? Data { - return data - } - } - } while service != 0 - - return nil - }() - #else - private static let deviceIdentifierData: Data? = { - guard var deviceIdentifier = UIDevice.current.identifierForVendor?.uuid else { return nil } - let rawDeviceIdentifierPointer = withUnsafePointer(to: &deviceIdentifier) { UnsafeRawPointer($0) } - let deviceIdentifierData = Data(bytes: rawDeviceIdentifierPointer, count: 16) - return deviceIdentifierData - }() - #endif -} - -enum ReceiptValidatorError: Error { - case badDecryptWrite - case missingPKCS7 - case receiptNotPKCS7Data - case receiptNotSigned - case badValidateWrite - case missingCertificate - case unableToAddCertificateToStore - case cryptoInitError - case invalidReceipt -} diff --git a/Project.swift b/Project.swift index c297e1d1..a4efdc99 100644 --- a/Project.swift +++ b/Project.swift @@ -8,7 +8,6 @@ let project = Project( .remote(url: "git@github.com:adamwulf/ClippingBezier.git", requirement: .upToNextMajor(from: "1.2.0")), .remote(url: "https://github.com/siteline/SwiftUI-Introspect.git", requirement: .upToNextMajor(from: "0.1.3")), .remote(url: "git@github.com:TelemetryDeck/SwiftClient.git", requirement: .upToNextMajor(from: "1.0.0")), - .remote(url: "git@github.com:krzyzanowskim/OpenSSL.git", requirement: .upToNextMajor(from: "3.1.5003")), ], settings: .settings(base: [ "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", @@ -46,7 +45,6 @@ let project = Project( Observations.target, PurchaseMarketing.target, Purchasing.target, - Receipts.target, Redacting.target, Redactions.target, Unpurchased.target, diff --git a/Tuist/ProjectDescriptionHelpers/Targets/App.swift b/Tuist/ProjectDescriptionHelpers/Targets/App.swift index efb6c934..e56424ab 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/App.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/App.swift @@ -19,7 +19,6 @@ public enum App { ] + Shared.resources), entitlements: "App/Highlighter.entitlements", dependencies: [ - .package(product: "OpenSSL"), .target(AutomatorActions.target, condition: .when([.catalyst])), .target(Core.target), ], @@ -33,7 +32,10 @@ public enum App { ], release: [ "PROVISIONING_PROFILE_SPECIFIER": "match AppStore com.cocoatype.Highlighter", "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]": "match AppStore com.cocoatype.Highlighter macos", - ] + ], + defaultSettings: .recommended(excluding: [ + "CODE_SIGN_IDENTITY", + ]) ) ) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Core.swift b/Tuist/ProjectDescriptionHelpers/Targets/Core.swift index dfed3ffb..4dd700b8 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Core.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Core.swift @@ -16,7 +16,6 @@ public enum Core { .target(PurchaseMarketing.target), .target(Purchasing.target), .target(Purchasing.doublesTarget), - .target(Receipts.target), .target(Unpurchased.target), ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift index d2cba090..9805cd40 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift @@ -3,7 +3,6 @@ import ProjectDescription public enum Purchasing { public static let target = Target.capabilitiesTarget(name: "Purchasing", dependencies: [ .target(ErrorHandling.target), - .target(Receipts.target), ]) public static let testTarget = Target.capabilitiesTestTarget(name: "Purchasing", dependencies: [ diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Receipts.swift b/Tuist/ProjectDescriptionHelpers/Targets/Receipts.swift deleted file mode 100644 index a705d084..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/Receipts.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Receipts.swift -// ProjectDescriptionHelpers -// -// Created by Geoff Pado on 3/15/24. -// - -import ProjectDescription - -public enum Receipts { - public static let target = Target.target( - name: "Receipts", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], - product: .framework, - bundleId: "com.cocoatype.Highlighter.Receipts", - sources: ["Modules/Legacy/Receipts/Sources/**"], - dependencies: [ - .target(ErrorHandling.target), - ] - ) -} From 2750484adb3a2c2adb134e7ea8d31ff08f96748d Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sat, 18 May 2024 17:10:53 -0700 Subject: [PATCH 16/21] Update references to fix Mac Catalyst builds --- Tuist/ProjectDescriptionHelpers/Shared.swift | 2 -- .../TargetExtensions.swift | 2 +- .../Targets/AppRatings.swift | 21 ------------------- .../Targets/Capabilities/AppRatings.swift | 20 ++++++++++++++++++ .../{ => Capabilities}/AutoRedactionsUI.swift | 6 +----- .../Targets/Capabilities/Defaults.swift | 5 +++++ .../Targets/Capabilities/DesignSystem.swift | 11 ++++++++++ .../Targets/Capabilities/ErrorHandling.swift | 17 +++++++++++++++ .../Targets/{ => Capabilities}/Logging.swift | 6 +----- .../{ => Capabilities}/Observations.swift | 0 .../Capabilities/PurchaseMarketing.swift | 14 +++++++++++++ .../Targets/Capabilities/Purchasing.swift | 19 +++++++++++++++++ .../Targets/Capabilities/Redactions.swift | 18 ++++++++++++++++ .../Targets/Capabilities/Unpurchased.swift | 19 +++++++++++++++++ .../Targets/Defaults.swift | 11 ---------- .../Targets/DesignSystem.swift | 7 ------- .../Targets/ErrorHandling.swift | 18 ---------------- .../Targets/{ => Legacy}/Core.swift | 19 ++++++++++++----- .../Targets/{ => Legacy}/Editing.swift | 17 ++++++++++----- .../Targets/{ => Legacy}/Redacting.swift | 14 ++++++------- .../Targets/{ => Products}/Action.swift | 0 .../Targets/{ => Products}/App.swift | 0 .../{ => Products}/AutomatorActions.swift | 0 .../Targets/{ => Products}/Photo.swift | 0 .../Targets/PurchaseMarketing.swift | 11 ---------- .../Targets/Purchasing.swift | 13 ------------ .../Targets/Redactions.swift | 12 ----------- .../Targets/Unpurchased.swift | 12 ----------- .../{Targets => }/TestHelpers.swift | 2 +- 29 files changed, 159 insertions(+), 137 deletions(-) delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift rename Tuist/ProjectDescriptionHelpers/Targets/{ => Capabilities}/AutoRedactionsUI.swift (51%) create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Defaults.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift rename Tuist/ProjectDescriptionHelpers/Targets/{ => Capabilities}/Logging.swift (55%) rename Tuist/ProjectDescriptionHelpers/Targets/{ => Capabilities}/Observations.swift (100%) create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/PurchaseMarketing.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Unpurchased.swift delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Defaults.swift delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift rename Tuist/ProjectDescriptionHelpers/Targets/{ => Legacy}/Core.swift (66%) rename Tuist/ProjectDescriptionHelpers/Targets/{ => Legacy}/Editing.swift (75%) rename Tuist/ProjectDescriptionHelpers/Targets/{ => Legacy}/Redacting.swift (92%) rename Tuist/ProjectDescriptionHelpers/Targets/{ => Products}/Action.swift (100%) rename Tuist/ProjectDescriptionHelpers/Targets/{ => Products}/App.swift (100%) rename Tuist/ProjectDescriptionHelpers/Targets/{ => Products}/AutomatorActions.swift (100%) rename Tuist/ProjectDescriptionHelpers/Targets/{ => Products}/Photo.swift (100%) delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Redactions.swift delete mode 100644 Tuist/ProjectDescriptionHelpers/Targets/Unpurchased.swift rename Tuist/ProjectDescriptionHelpers/{Targets => }/TestHelpers.swift (97%) diff --git a/Tuist/ProjectDescriptionHelpers/Shared.swift b/Tuist/ProjectDescriptionHelpers/Shared.swift index 01375211..3e82da3c 100644 --- a/Tuist/ProjectDescriptionHelpers/Shared.swift +++ b/Tuist/ProjectDescriptionHelpers/Shared.swift @@ -3,7 +3,5 @@ import ProjectDescription enum Shared { static let resources: [ProjectDescription.ResourceFileElement] = [ "App/Resources/Assets.xcassets", -// "Modules/Legacy/Editing/Resources/Style/Aleo-Bold.otf", -// "Modules/Legacy/Editing/Resources/Style/Aleo-Regular.otf", ] } diff --git a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift index 593a7ce8..1353f8bc 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift @@ -38,7 +38,7 @@ extension Target { static func capabilitiesDoublesTarget(name: String) -> Target { return Target.target( name: "\(name)Doubles", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign, .mac], product: .framework, bundleId: "com.cocoatype.Highlighter.\(name)Doubles", sources: ["Modules/Capabilities/\(name)/Doubles/**"], diff --git a/Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift b/Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift deleted file mode 100644 index 0da189fc..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/AppRatings.swift +++ /dev/null @@ -1,21 +0,0 @@ -import ProjectDescription - -public enum AppRatings { - public static let target = Target.target( - name: "AppRatings", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], - product: .framework, - bundleId: "com.cocoatype.Highlighter.AppRatings", - sources: ["Modules/Capabilities/AppRatings/Sources/**"], - dependencies: [ - .target(ErrorHandling.target), - ] - ) - - public static let testTarget = Target.capabilitiesTestTarget(name: "AppRatings", dependencies: [ - .target(Defaults.target), - .target(Editing.target), - .target(Logging.doublesTarget), - .target(TestHelpers.target), - ]) -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift new file mode 100644 index 00000000..3966fa22 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift @@ -0,0 +1,20 @@ +import ProjectDescription + +public enum AppRatings { + public static let target = Target.capabilitiesTarget( + name: "AppRatings", + dependencies: [ + .target(ErrorHandling.target), + ] + ) + + public static let testTarget = Target.capabilitiesTestTarget( + name: "AppRatings", + dependencies: [ + .target(Defaults.target), + .target(Editing.target), + .target(Logging.doublesTarget), + .target(TestHelpers.target), + ] + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/AutoRedactionsUI.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AutoRedactionsUI.swift similarity index 51% rename from Tuist/ProjectDescriptionHelpers/Targets/AutoRedactionsUI.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AutoRedactionsUI.swift index b66878cc..5cc9bc39 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/AutoRedactionsUI.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AutoRedactionsUI.swift @@ -1,12 +1,8 @@ import ProjectDescription public enum AutoRedactionsUI { - public static let target = Target.target( + public static let target = Target.capabilitiesTarget( name: "AutoRedactionsUI", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], - product: .framework, - bundleId: "com.cocoatype.Highlighter.AutoRedactionsUI", - sources: ["Modules/Capabilities/AutoRedactionsUI/Sources/**"], dependencies: [ .target(DesignSystem.target), .target(ErrorHandling.target), diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Defaults.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Defaults.swift new file mode 100644 index 00000000..27fc041d --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Defaults.swift @@ -0,0 +1,5 @@ +import ProjectDescription + +public enum Defaults { + public static let target = Target.capabilitiesTarget(name: "Defaults") +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift new file mode 100644 index 00000000..3cc52a11 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift @@ -0,0 +1,11 @@ +import ProjectDescription + +public enum DesignSystem { + public static let target = Target.capabilitiesTarget( + name: "DesignSystem", + hasResources: true, + dependencies: [ + .target(ErrorHandling.target), + ] + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift new file mode 100644 index 00000000..753d92cb --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift @@ -0,0 +1,17 @@ +import ProjectDescription + +public enum ErrorHandling { + public static let target = Target.capabilitiesTarget( + name: "ErrorHandling", + dependencies: [ + .target(Logging.target), + ] + ) + + public static let testTarget = Target.capabilitiesTestTarget( + name: "ErrorHandling", + dependencies: [ + .target(Logging.doublesTarget), + ] + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Logging.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift similarity index 55% rename from Tuist/ProjectDescriptionHelpers/Targets/Logging.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift index 1b742b27..6d97f020 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Logging.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift @@ -1,12 +1,8 @@ import ProjectDescription public enum Logging { - public static let target = Target.target( + public static let target = Target.capabilitiesTarget( name: "Logging", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign, .mac], - product: .framework, - bundleId: "com.cocoatype.Highlighter.Logging", - sources: ["Modules/Capabilities/Logging/Sources/**"], dependencies: [ .package(product: "TelemetryClient", type: .runtime), ] diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Observations.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Observations.swift similarity index 100% rename from Tuist/ProjectDescriptionHelpers/Targets/Observations.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Observations.swift diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/PurchaseMarketing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/PurchaseMarketing.swift new file mode 100644 index 00000000..c40301f3 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/PurchaseMarketing.swift @@ -0,0 +1,14 @@ +import ProjectDescription + +public enum PurchaseMarketing { + public static let target = Target.capabilitiesTarget( + name: "PurchaseMarketing", + dependencies: [ + .target(DesignSystem.target), + .target(Purchasing.target), + .target(Purchasing.doublesTarget), + ] + ) + + public static let testTarget = Target.capabilitiesTestTarget(name: "PurchaseMarketing") +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift new file mode 100644 index 00000000..4dfb69d1 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift @@ -0,0 +1,19 @@ +import ProjectDescription + +public enum Purchasing { + public static let target = Target.capabilitiesTarget( + name: "Purchasing", + dependencies: [ + .target(ErrorHandling.target), + ] + ) + + public static let testTarget = Target.capabilitiesTestTarget( + name: "Purchasing", + dependencies: [ + .target(doublesTarget), + ] + ) + + public static let doublesTarget = Target.capabilitiesDoublesTarget(name: "Purchasing") +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift new file mode 100644 index 00000000..48078e23 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift @@ -0,0 +1,18 @@ +import ProjectDescription + +public enum Redactions { + public static let target = Target.capabilitiesTarget( + name: "Redactions", + dependencies: [ + .target(Observations.target), + ] + ) + + public static let testTarget = Target.capabilitiesTestTarget( + name: "Redactions", + dependencies: [ + .target(Observations.target), + .target(TestHelpers.target), + ] + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Unpurchased.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Unpurchased.swift new file mode 100644 index 00000000..11e5f456 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Unpurchased.swift @@ -0,0 +1,19 @@ +import ProjectDescription + +public enum Unpurchased { + public static let target = Target.capabilitiesTarget( + name: "Unpurchased", + hasResources: true, + dependencies: [ + .target(Defaults.target), + .target(DesignSystem.target), + ] + ) + + public static let testTarget = Target.capabilitiesTestTarget( + name: "Unpurchased", + dependencies: [ + .target(DesignSystem.target), + ] + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Defaults.swift b/Tuist/ProjectDescriptionHelpers/Targets/Defaults.swift deleted file mode 100644 index b13f86a4..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/Defaults.swift +++ /dev/null @@ -1,11 +0,0 @@ -import ProjectDescription - -public enum Defaults { - public static let target = Target.target( - name: "Defaults", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], - product: .framework, - bundleId: "com.cocoatype.Highlighter.Defaults", - sources: ["Modules/Capabilities/Defaults/Sources/**"] - ) -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift b/Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift deleted file mode 100644 index eebf1828..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/DesignSystem.swift +++ /dev/null @@ -1,7 +0,0 @@ -import ProjectDescription - -public enum DesignSystem { - public static let target = Target.capabilitiesTarget(name: "DesignSystem", hasResources: true, dependencies: [ - .target(ErrorHandling.target), - ]) -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift b/Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift deleted file mode 100644 index 131c475d..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/ErrorHandling.swift +++ /dev/null @@ -1,18 +0,0 @@ -import ProjectDescription - -public enum ErrorHandling { - public static let target = Target.target( - name: "ErrorHandling", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign, .mac], - product: .framework, - bundleId: "com.cocoatype.Highlighter.ErrorHandling", - sources: ["Modules/Capabilities/ErrorHandling/Sources/**"], - dependencies: [ - .target(Logging.target), - ] - ) - - public static let testTarget = Target.capabilitiesTestTarget(name: "ErrorHandling", dependencies: [ - .target(Logging.doublesTarget), - ]) -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Core.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Core.swift similarity index 66% rename from Tuist/ProjectDescriptionHelpers/Targets/Core.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Legacy/Core.swift index 4dd700b8..a31a27ad 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Core.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Core.swift @@ -17,11 +17,20 @@ public enum Core { .target(Purchasing.target), .target(Purchasing.doublesTarget), .target(Unpurchased.target), - ] + ], + settings: .settings( + defaultSettings: .recommended(excluding: [ + "CODE_SIGN_IDENTITY", + ]) + ) ) - public static let testTarget = Target.moduleTestTarget(name: "Core", type: "Legacy", dependencies: [ - .target(Logging.doublesTarget), - .target(Purchasing.doublesTarget), - ]) + public static let testTarget = Target.moduleTestTarget( + name: "Core", + type: "Legacy", + dependencies: [ + .target(Logging.doublesTarget), + .target(Purchasing.doublesTarget), + ] + ) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Editing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift similarity index 75% rename from Tuist/ProjectDescriptionHelpers/Targets/Editing.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift index 33c4241b..45b70a0c 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Editing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift @@ -23,12 +23,19 @@ public enum Editing { base: [ "ASSETCATALOG_COMPILER_APPICON_NAME": "", "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME": "", - ] + ], + defaultSettings: .recommended(excluding: [ + "CODE_SIGN_IDENTITY", + ]) ) ) - public static let testTarget = Target.moduleTestTarget(name: "Editing", type: "Legacy", dependencies: [ - .target(AutoRedactionsUI.target), - .target(Purchasing.doublesTarget), - ]) + public static let testTarget = Target.moduleTestTarget( + name: "Editing", + type: "Legacy", + dependencies: [ + .target(AutoRedactionsUI.target), + .target(Purchasing.doublesTarget), + ] + ) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Redacting.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Redacting.swift similarity index 92% rename from Tuist/ProjectDescriptionHelpers/Targets/Redacting.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Legacy/Redacting.swift index 430c986e..d2f34793 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Redacting.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Redacting.swift @@ -1,10 +1,3 @@ -// -// Redacting.swift -// ProjectDescriptionHelpers -// -// Created by Geoff Pado on 3/15/24. -// - import ProjectDescription public enum Redacting { @@ -37,6 +30,11 @@ public enum Redacting { dependencies: [ .target(ErrorHandling.target), .target(Observations.target), - ] + ], + settings: .settings( + defaultSettings: .recommended(excluding: [ + "CODE_SIGN_IDENTITY", + ]) + ) ) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Action.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/Action.swift similarity index 100% rename from Tuist/ProjectDescriptionHelpers/Targets/Action.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Products/Action.swift diff --git a/Tuist/ProjectDescriptionHelpers/Targets/App.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/App.swift similarity index 100% rename from Tuist/ProjectDescriptionHelpers/Targets/App.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Products/App.swift diff --git a/Tuist/ProjectDescriptionHelpers/Targets/AutomatorActions.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/AutomatorActions.swift similarity index 100% rename from Tuist/ProjectDescriptionHelpers/Targets/AutomatorActions.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Products/AutomatorActions.swift diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Photo.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/Photo.swift similarity index 100% rename from Tuist/ProjectDescriptionHelpers/Targets/Photo.swift rename to Tuist/ProjectDescriptionHelpers/Targets/Products/Photo.swift diff --git a/Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift b/Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift deleted file mode 100644 index 60cc2204..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/PurchaseMarketing.swift +++ /dev/null @@ -1,11 +0,0 @@ -import ProjectDescription - -public enum PurchaseMarketing { - public static let target = Target.capabilitiesTarget(name: "PurchaseMarketing", dependencies: [ - .target(DesignSystem.target), - .target(Purchasing.target), - .target(Purchasing.doublesTarget), - ]) - - public static let testTarget = Target.capabilitiesTestTarget(name: "PurchaseMarketing") -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift deleted file mode 100644 index 9805cd40..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/Purchasing.swift +++ /dev/null @@ -1,13 +0,0 @@ -import ProjectDescription - -public enum Purchasing { - public static let target = Target.capabilitiesTarget(name: "Purchasing", dependencies: [ - .target(ErrorHandling.target), - ]) - - public static let testTarget = Target.capabilitiesTestTarget(name: "Purchasing", dependencies: [ - .target(doublesTarget), - ]) - - public static let doublesTarget = Target.capabilitiesDoublesTarget(name: "Purchasing") -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Redactions.swift b/Tuist/ProjectDescriptionHelpers/Targets/Redactions.swift deleted file mode 100644 index 8fea284d..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/Redactions.swift +++ /dev/null @@ -1,12 +0,0 @@ -import ProjectDescription - -public enum Redactions { - public static let target = Target.capabilitiesTarget(name: "Redactions", dependencies: [ - .target(Observations.target), - ]) - - public static let testTarget = Target.capabilitiesTestTarget(name: "Redactions", dependencies: [ - .target(Observations.target), - .target(TestHelpers.target), - ]) -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Unpurchased.swift b/Tuist/ProjectDescriptionHelpers/Targets/Unpurchased.swift deleted file mode 100644 index f3f6a000..00000000 --- a/Tuist/ProjectDescriptionHelpers/Targets/Unpurchased.swift +++ /dev/null @@ -1,12 +0,0 @@ -import ProjectDescription - -public enum Unpurchased { - public static let target = Target.capabilitiesTarget(name: "Unpurchased", hasResources: true, dependencies: [ - .target(Defaults.target), - .target(DesignSystem.target), - ]) - - public static let testTarget = Target.capabilitiesTestTarget(name: "Unpurchased", dependencies: [ - .target(DesignSystem.target), - ]) -} diff --git a/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift b/Tuist/ProjectDescriptionHelpers/TestHelpers.swift similarity index 97% rename from Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift rename to Tuist/ProjectDescriptionHelpers/TestHelpers.swift index 196d8381..16cb6764 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/TestHelpers.swift +++ b/Tuist/ProjectDescriptionHelpers/TestHelpers.swift @@ -16,7 +16,7 @@ public enum TestHelpers { public static let interfaceTarget = Target.target( name: "TestHelpersInterface", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign, .mac], product: .framework, bundleId: "com.cocoatype.Highlighter.TestHelpersInterface", sources: ["Modules/TestHelpers/Interface/**"] From 530678e396319310d9136f84f220d3681e0a499e Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sat, 18 May 2024 18:48:31 -0700 Subject: [PATCH 17/21] Separate Mac-native builds from Catalyst builds --- .../Sources/RedactActionExportOperation.swift | 2 +- Automator/Sources/RedactOperation.swift | 2 +- Automator/Sources/RedactedImageExporter.swift | 2 +- .../ErrorHandling/Sources/ErrorHandler.swift | 5 ++ .../ErrorHandling/Sources/ErrorHandling.swift | 5 ++ .../Extensions/NSBezierPathExtensions.swift | 3 +- .../Redactions/Sources/RedactionPart.swift | 4 ++ .../CharacterObservationRedaction.swift | 4 +- .../Redactions/TextObservationRedaction.swift | 4 +- .../Extensions/UIBezierPathExtensions.swift | 3 +- .../Sources/Text Detection/TextDetector.swift | 9 ++-- Project.swift | 13 +++-- Tuist/ProjectDescriptionHelpers/SDK.swift | 20 +++++++ .../TargetExtensions.swift | 52 ++++++++++++++----- .../Targets/Capabilities/AppRatings.swift | 2 +- .../Capabilities/AutoRedactionsUI.swift | 2 +- .../Targets/Capabilities/DesignSystem.swift | 2 +- .../Targets/Capabilities/ErrorHandling.swift | 15 +++--- .../Targets/Capabilities/Logging.swift | 15 +++--- .../Targets/Capabilities/Observations.swift | 7 ++- .../Targets/Capabilities/Purchasing.swift | 2 +- .../Targets/Capabilities/Redactions.swift | 17 +++--- .../Targets/Legacy/Core.swift | 3 +- .../Targets/Legacy/Editing.swift | 9 ++-- .../Targets/Legacy/Redacting.swift | 4 +- .../Targets/Products/Action.swift | 2 +- .../Targets/Products/App.swift | 2 +- .../Targets/Products/AutomatorActions.swift | 2 +- .../Targets/Products/Photo.swift | 2 +- .../TestHelpers.swift | 2 +- 30 files changed, 148 insertions(+), 68 deletions(-) create mode 100644 Tuist/ProjectDescriptionHelpers/SDK.swift diff --git a/Automator/Sources/RedactActionExportOperation.swift b/Automator/Sources/RedactActionExportOperation.swift index 4e8ea21d..7f648aaf 100644 --- a/Automator/Sources/RedactActionExportOperation.swift +++ b/Automator/Sources/RedactActionExportOperation.swift @@ -3,7 +3,7 @@ import AppKit import Redacting -import Redactions +import RedactionsMac class RedactActionExportOperation: Operation { var result: Result? diff --git a/Automator/Sources/RedactOperation.swift b/Automator/Sources/RedactOperation.swift index 90a19efa..7fa75072 100644 --- a/Automator/Sources/RedactOperation.swift +++ b/Automator/Sources/RedactOperation.swift @@ -3,7 +3,7 @@ import Foundation import Redacting -import Redactions +import RedactionsMac class RedactOperation: Operation { var result: Result? diff --git a/Automator/Sources/RedactedImageExporter.swift b/Automator/Sources/RedactedImageExporter.swift index f4dc9134..ca8092fd 100644 --- a/Automator/Sources/RedactedImageExporter.swift +++ b/Automator/Sources/RedactedImageExporter.swift @@ -3,7 +3,7 @@ import AppKit import Redacting -import Redactions +import RedactionsMac class RedactActionExporter: NSObject { static func export(_ input: RedactActionInput, redactions: [Redaction], completionHandler: @escaping((Result) -> Void)) { diff --git a/Modules/Capabilities/ErrorHandling/Sources/ErrorHandler.swift b/Modules/Capabilities/ErrorHandling/Sources/ErrorHandler.swift index eedcdc0c..bdffe2cd 100644 --- a/Modules/Capabilities/ErrorHandling/Sources/ErrorHandler.swift +++ b/Modules/Capabilities/ErrorHandling/Sources/ErrorHandler.swift @@ -2,7 +2,12 @@ // Copyright © 2023 Cocoatype, LLC. All rights reserved. import Foundation + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import LoggingMac +#else import Logging +#endif public struct ErrorHandler: ErrorHandling { private var logger: Logger diff --git a/Modules/Capabilities/ErrorHandling/Sources/ErrorHandling.swift b/Modules/Capabilities/ErrorHandling/Sources/ErrorHandling.swift index d101b8c5..34a63b65 100644 --- a/Modules/Capabilities/ErrorHandling/Sources/ErrorHandling.swift +++ b/Modules/Capabilities/ErrorHandling/Sources/ErrorHandling.swift @@ -2,7 +2,12 @@ // Copyright © 2021 Cocoatype, LLC. All rights reserved. import Foundation + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import LoggingMac +#else import Logging +#endif public protocol ErrorHandling { init(logger: Logger) diff --git a/Modules/Capabilities/Redactions/Sources/Extensions/NSBezierPathExtensions.swift b/Modules/Capabilities/Redactions/Sources/Extensions/NSBezierPathExtensions.swift index 7809d85a..9437f433 100644 --- a/Modules/Capabilities/Redactions/Sources/Extensions/NSBezierPathExtensions.swift +++ b/Modules/Capabilities/Redactions/Sources/Extensions/NSBezierPathExtensions.swift @@ -1,10 +1,9 @@ // Created by Geoff Pado on 5/8/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. -import Observations - #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit +import ObservationsMac extension NSBezierPath { convenience init(cgPath: CGPath) { diff --git a/Modules/Capabilities/Redactions/Sources/RedactionPart.swift b/Modules/Capabilities/Redactions/Sources/RedactionPart.swift index 7ede2d50..614d1513 100644 --- a/Modules/Capabilities/Redactions/Sources/RedactionPart.swift +++ b/Modules/Capabilities/Redactions/Sources/RedactionPart.swift @@ -1,7 +1,11 @@ // Created by Geoff Pado on 5/8/24. // Copyright © 2024 Cocoatype, LLC. All rights reserved. +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import ObservationsMac +#else import Observations +#endif public enum RedactionPart: Equatable { case path(RedactionPath) diff --git a/Modules/Capabilities/Redactions/Sources/Redactions/CharacterObservationRedaction.swift b/Modules/Capabilities/Redactions/Sources/Redactions/CharacterObservationRedaction.swift index 13aace7d..ae4c258d 100644 --- a/Modules/Capabilities/Redactions/Sources/Redactions/CharacterObservationRedaction.swift +++ b/Modules/Capabilities/Redactions/Sources/Redactions/CharacterObservationRedaction.swift @@ -1,10 +1,9 @@ // Created by Geoff Pado on 5/15/19. // Copyright © 2019 Cocoatype, LLC. All rights reserved. -import Observations - #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit +import ObservationsMac extension Redaction { public init?(_ characterObservations: [CharacterObservation], color: NSColor) { @@ -26,6 +25,7 @@ extension Redaction { } #elseif canImport(UIKit) +import Observations import UIKit extension Redaction { diff --git a/Modules/Capabilities/Redactions/Sources/Redactions/TextObservationRedaction.swift b/Modules/Capabilities/Redactions/Sources/Redactions/TextObservationRedaction.swift index a9bbb4d4..3f825b0b 100644 --- a/Modules/Capabilities/Redactions/Sources/Redactions/TextObservationRedaction.swift +++ b/Modules/Capabilities/Redactions/Sources/Redactions/TextObservationRedaction.swift @@ -1,10 +1,9 @@ // Created by Geoff Pado on 7/29/19. // Copyright © 2019 Cocoatype, LLC. All rights reserved. -import Observations - #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit +import ObservationsMac extension Redaction { public init(_ textObservation: ObservationType, color: NSColor) { @@ -14,6 +13,7 @@ extension Redaction { } #elseif canImport(UIKit) +import Observations import UIKit extension Redaction { diff --git a/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift b/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift index 4dd0f0cc..d0645459 100644 --- a/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift +++ b/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift @@ -1,10 +1,10 @@ // Created by Geoff Pado on 5/1/19. // Copyright © 2019 Cocoatype, LLC. All rights reserved. -import Observations #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit +import ObservationsMac extension NSBezierPath { convenience init(cgPath: CGPath) { @@ -102,6 +102,7 @@ extension NSBezierPath.LineJoinStyle { } #elseif canImport(UIKit) +import Observations import SwiftUI import UIKit diff --git a/Modules/Legacy/Editing/Sources/Text Detection/TextDetector.swift b/Modules/Legacy/Editing/Sources/Text Detection/TextDetector.swift index e6451e75..f98e9781 100644 --- a/Modules/Legacy/Editing/Sources/Text Detection/TextDetector.swift +++ b/Modules/Legacy/Editing/Sources/Text Detection/TextDetector.swift @@ -1,15 +1,16 @@ // Created by Geoff Pado on 4/22/19. // Copyright © 2019 Cocoatype, LLC. All rights reserved. -import Observations -import Vision - -#if canImport(AppKit) +#if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit +import ObservationsMac #elseif canImport(UIKit) +import Observations import UIKit #endif +import Vision + open class TextDetector: NSObject { #if canImport(UIKit) public func detectTextRectangles(in image: UIImage, completionHandler: (([TextRectangleObservation]?) -> Void)? = nil) { diff --git a/Project.swift b/Project.swift index a4efdc99..a77dd2c8 100644 --- a/Project.swift +++ b/Project.swift @@ -20,6 +20,7 @@ let project = Project( "MACOSX_DEPLOYMENT_TARGET": "12.0", "MARKETING_VERSION": "999", "OTHER_CODE_SIGN_FLAGS": "--deep", + "SWIFT_VERSION": "5.0", "TARGETED_DEVICE_FAMILY": "1,2,6", ], debug: [ "CODE_SIGN_IDENTITY": "Apple Development: Buddy Build (D47V8Y25W5)", @@ -40,13 +41,17 @@ let project = Project( Defaults.target, DesignSystem.target, Editing.target, - ErrorHandling.target, - Logging.target, - Observations.target, + ErrorHandling.target(sdk: .catalyst), + ErrorHandling.target(sdk: .native), + Logging.target(sdk: .catalyst), + Logging.target(sdk: .native), + Observations.target(sdk: .catalyst), + Observations.target(sdk: .native), PurchaseMarketing.target, Purchasing.target, Redacting.target, - Redactions.target, + Redactions.target(sdk: .catalyst), + Redactions.target(sdk: .native), Unpurchased.target, // doubles Logging.doublesTarget, diff --git a/Tuist/ProjectDescriptionHelpers/SDK.swift b/Tuist/ProjectDescriptionHelpers/SDK.swift new file mode 100644 index 00000000..19c34edd --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/SDK.swift @@ -0,0 +1,20 @@ +import ProjectDescription + +public enum SDK { + case native + case catalyst + + var destinations: Destinations { + switch self { + case .native: [.mac] + case .catalyst: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign] + } + } + + var nameSuffix: String { + switch self { + case .native: "Mac" + case .catalyst: "" + } + } +} diff --git a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift index 1353f8bc..d7c93054 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift @@ -3,46 +3,70 @@ import ProjectDescription extension Target { static func capabilitiesTarget( name: String, + sdk: SDK = .catalyst, hasResources: Bool = false, dependencies: [TargetDependency] = [] ) -> Target { Target.target( - name: name, - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign, .mac], + name: name + sdk.nameSuffix, + destinations: sdk.destinations, product: .framework, bundleId: "com.cocoatype.Highlighter.\(name)", sources: ["Modules/Capabilities/\(name)/Sources/**"], resources: hasResources ? ["Modules/Capabilities/\(name)/Resources/**"] : nil, - dependencies: dependencies + dependencies: dependencies, + settings: .settings( + base: [ + "DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER": false, + ], + defaultSettings: .none + ) ) } static func capabilitiesTestTarget( name: String, + sdk: SDK = .catalyst, dependencies: [TargetDependency] = [] ) -> Target { - moduleTestTarget(name: name, type: "Capabilities", dependencies: dependencies) + moduleTestTarget( + name: name, + sdk: sdk, + type: "Capabilities", + dependencies: dependencies + ) } - static func moduleTestTarget(name: String, type: String, dependencies: [TargetDependency] = []) -> Target { + static func moduleTestTarget( + name: String, + sdk: SDK, + type: String, + dependencies: [TargetDependency] = [] + ) -> Target { return Target.target( - name: "\(name)Tests", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + name: "\(name + sdk.nameSuffix)Tests", + destinations: sdk.destinations, product: .unitTests, - bundleId: "com.cocoatype.Highlighter.\(name)Tests", + bundleId: "com.cocoatype.Highlighter.\(name + sdk.nameSuffix)Tests", sources: ["Modules/\(type)/\(name)/Tests/**"], - dependencies: [.target(name: name)] + dependencies + dependencies: [.target(name: name + sdk.nameSuffix)] + dependencies ) } - static func capabilitiesDoublesTarget(name: String) -> Target { + static func capabilitiesDoublesTarget( + name: String, + sdk: SDK = .catalyst + ) -> Target { return Target.target( - name: "\(name)Doubles", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign, .mac], + name: "\(name + sdk.nameSuffix)Doubles", + destinations: sdk.destinations, product: .framework, - bundleId: "com.cocoatype.Highlighter.\(name)Doubles", + bundleId: "com.cocoatype.Highlighter.\(name + sdk.nameSuffix)Doubles", sources: ["Modules/Capabilities/\(name)/Doubles/**"], - dependencies: [.target(name: name), .target(TestHelpers.interfaceTarget)] + dependencies: [ + .target(name: name + sdk.nameSuffix), + .target(TestHelpers.interfaceTarget) + ] ) } } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift index 3966fa22..83f7a05f 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AppRatings.swift @@ -4,7 +4,7 @@ public enum AppRatings { public static let target = Target.capabilitiesTarget( name: "AppRatings", dependencies: [ - .target(ErrorHandling.target), + .target(ErrorHandling.target(sdk: .catalyst)), ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AutoRedactionsUI.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AutoRedactionsUI.swift index 5cc9bc39..a25121d5 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AutoRedactionsUI.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/AutoRedactionsUI.swift @@ -5,7 +5,7 @@ public enum AutoRedactionsUI { name: "AutoRedactionsUI", dependencies: [ .target(DesignSystem.target), - .target(ErrorHandling.target), + .target(ErrorHandling.target(sdk: .catalyst)), ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift index 3cc52a11..973a7279 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/DesignSystem.swift @@ -5,7 +5,7 @@ public enum DesignSystem { name: "DesignSystem", hasResources: true, dependencies: [ - .target(ErrorHandling.target), + .target(ErrorHandling.target(sdk: .catalyst)), ] ) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift index 753d92cb..b14ccfe3 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/ErrorHandling.swift @@ -1,12 +1,15 @@ import ProjectDescription public enum ErrorHandling { - public static let target = Target.capabilitiesTarget( - name: "ErrorHandling", - dependencies: [ - .target(Logging.target), - ] - ) + public static func target(sdk: SDK) -> Target { + Target.capabilitiesTarget( + name: "ErrorHandling", + sdk: sdk, + dependencies: [ + .target(Logging.target(sdk: sdk)), + ] + ) + } public static let testTarget = Target.capabilitiesTestTarget( name: "ErrorHandling", diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift index 6d97f020..2791ad11 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift @@ -1,12 +1,15 @@ import ProjectDescription public enum Logging { - public static let target = Target.capabilitiesTarget( - name: "Logging", - dependencies: [ - .package(product: "TelemetryClient", type: .runtime), - ] - ) + public static func target(sdk: SDK) -> Target { + Target.capabilitiesTarget( + name: "Logging", + sdk: sdk, + dependencies: [ + .package(product: "TelemetryClient", type: .runtime), + ] + ) + } public static let testTarget = Target.capabilitiesTestTarget(name: "Logging") diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Observations.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Observations.swift index ba3330c7..3d70c479 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Observations.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Observations.swift @@ -1,7 +1,12 @@ import ProjectDescription public enum Observations { - public static let target = Target.capabilitiesTarget(name: "Observations") + public static func target(sdk: SDK) -> Target { + Target.capabilitiesTarget( + name: "Observations", + sdk: sdk + ) + } public static let testTarget = Target.capabilitiesTestTarget(name: "Observations") } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift index 4dfb69d1..1cbcfd70 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Purchasing.swift @@ -4,7 +4,7 @@ public enum Purchasing { public static let target = Target.capabilitiesTarget( name: "Purchasing", dependencies: [ - .target(ErrorHandling.target), + .target(ErrorHandling.target(sdk: .catalyst)), ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift index 48078e23..03586e4c 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Redactions.swift @@ -1,17 +1,20 @@ import ProjectDescription public enum Redactions { - public static let target = Target.capabilitiesTarget( - name: "Redactions", - dependencies: [ - .target(Observations.target), - ] - ) + public static func target(sdk: SDK) -> Target { + Target.capabilitiesTarget( + name: "Redactions", + sdk: sdk, + dependencies: [ + .target(Observations.target(sdk: sdk)), + ] + ) + } public static let testTarget = Target.capabilitiesTestTarget( name: "Redactions", dependencies: [ - .target(Observations.target), + .target(Observations.target(sdk: .catalyst)), .target(TestHelpers.target), ] ) diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Core.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Core.swift index a31a27ad..5af42928 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Core.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Core.swift @@ -3,7 +3,7 @@ import ProjectDescription public enum Core { public static let target = Target.target( name: "Core", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + destinations: SDK.catalyst.destinations, product: .framework, bundleId: "com.cocoatype.Highlighter.Core", sources: ["Modules/Legacy/Core/Sources/**"], @@ -27,6 +27,7 @@ public enum Core { public static let testTarget = Target.moduleTestTarget( name: "Core", + sdk: .catalyst, type: "Legacy", dependencies: [ .target(Logging.doublesTarget), diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift index 45b70a0c..9c1b4845 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift @@ -3,7 +3,7 @@ import ProjectDescription public enum Editing { public static let target = Target.target( name: "Editing", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign, .mac], + destinations: SDK.catalyst.destinations, product: .framework, bundleId: "com.cocoatype.Highlighter.Editing", sources: ["Modules/Legacy/Editing/Sources/**"], @@ -11,11 +11,11 @@ public enum Editing { headers: .headers(public: ["Modules/Legacy/Editing/Headers/**"]), dependencies: [ .target(AutoRedactionsUI.target), - .target(ErrorHandling.target), - .target(Observations.target), + .target(ErrorHandling.target(sdk: .catalyst)), + .target(Observations.target(sdk: .catalyst)), .target(PurchaseMarketing.target), .target(Purchasing.doublesTarget), - .target(Redactions.target), + .target(Redactions.target(sdk: .catalyst)), .package(product: "ClippingBezier", type: .runtime), .package(product: "Introspect", type: .runtime), ], @@ -32,6 +32,7 @@ public enum Editing { public static let testTarget = Target.moduleTestTarget( name: "Editing", + sdk: .catalyst, type: "Legacy", dependencies: [ .target(AutoRedactionsUI.target), diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Redacting.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Redacting.swift index d2f34793..dfbeee82 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Redacting.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Redacting.swift @@ -28,8 +28,8 @@ public enum Redacting { ], resources: ["Modules/Legacy/Editing/Resources/**"], dependencies: [ - .target(ErrorHandling.target), - .target(Observations.target), + .target(ErrorHandling.target(sdk: .native)), + .target(Observations.target(sdk: .native)), ], settings: .settings( defaultSettings: .recommended(excluding: [ diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Products/Action.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/Action.swift index 7c0c7374..3d2e3798 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Products/Action.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Products/Action.swift @@ -10,7 +10,7 @@ import ProjectDescription public enum Action { public static let target = Target.target( name: "Action", - destinations: [.iPhone, .iPad, .appleVisionWithiPadDesign], + destinations: SDK.catalyst.destinations, product: .appExtension, bundleId: "com.cocoatype.Highlighter.Action", infoPlist: "Action/Info.plist", diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Products/App.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/App.swift index e56424ab..40567e4f 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Products/App.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Products/App.swift @@ -10,7 +10,7 @@ import ProjectDescription public enum App { public static let target = Target.target( name: "Highlighter", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + destinations: SDK.catalyst.destinations, product: .app, bundleId: "com.cocoatype.Highlighter", infoPlist: "App/Info.plist", diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Products/AutomatorActions.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/AutomatorActions.swift index 160935b1..2c9eee39 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Products/AutomatorActions.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Products/AutomatorActions.swift @@ -11,7 +11,7 @@ public enum AutomatorActions { resources: ["Automator/Resources/**"], dependencies: [ .target(Redacting.target), - .target(Redactions.target), + .target(Redactions.target(sdk: .native)), ], settings: .settings(base: [ "WRAPPER_EXTENSION": "action", diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Products/Photo.swift b/Tuist/ProjectDescriptionHelpers/Targets/Products/Photo.swift index 9542c991..cef6d761 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Products/Photo.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Products/Photo.swift @@ -10,7 +10,7 @@ import ProjectDescription public enum Photo { public static let target = Target.target( name: "Photo", - destinations: [.iPhone, .iPad, .appleVisionWithiPadDesign], + destinations: SDK.catalyst.destinations, product: .appExtension, bundleId: "com.cocoatype.Highlighter.Photo", infoPlist: "Photo/Info.plist", diff --git a/Tuist/ProjectDescriptionHelpers/TestHelpers.swift b/Tuist/ProjectDescriptionHelpers/TestHelpers.swift index 16cb6764..bd600dab 100644 --- a/Tuist/ProjectDescriptionHelpers/TestHelpers.swift +++ b/Tuist/ProjectDescriptionHelpers/TestHelpers.swift @@ -3,7 +3,7 @@ import ProjectDescription public enum TestHelpers { public static let target = Target.target( name: "TestHelpers", - destinations: [.iPhone, .iPad, .macCatalyst, .appleVisionWithiPadDesign], + destinations: SDK.catalyst.destinations, product: .framework, bundleId: "com.cocoatype.Highlighter.TestHelpers", sources: ["Modules/TestHelpers/Sources/**"], From a485507dd83bc302e38a170ab5a7eed423e98b60 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sun, 19 May 2024 06:28:01 -0700 Subject: [PATCH 18/21] Fix archiving project --- Package.resolved | 41 +++++++++++++++++++ Package.swift | 27 ++++++++++++ Project.swift | 6 --- .../Targets/Capabilities/Logging.swift | 2 +- .../Targets/Legacy/Editing.swift | 4 +- 5 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 Package.resolved create mode 100644 Package.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..95497316 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,41 @@ +{ + "pins" : [ + { + "identity" : "clippingbezier", + "kind" : "remoteSourceControl", + "location" : "git@github.com:adamwulf/ClippingBezier.git", + "state" : { + "revision" : "7b55731c019b2e9de3c9543d2892f034dba86da8", + "version" : "1.2.5" + } + }, + { + "identity" : "performancebezier", + "kind" : "remoteSourceControl", + "location" : "https://github.com/adamwulf/PerformanceBezier.git", + "state" : { + "revision" : "b3cc82221836a8ce1f0692033d58b1ff836c57f8", + "version" : "1.3.2" + } + }, + { + "identity" : "swiftclient", + "kind" : "remoteSourceControl", + "location" : "git@github.com:TelemetryDeck/SwiftClient.git", + "state" : { + "revision" : "d20ccc4b8266cf739eede58cdfc7e9c6ffb41cda", + "version" : "1.5.1" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "121c146fe591b1320238d054ae35c81ffa45f45a", + "version" : "0.12.0" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..d1866c5d --- /dev/null +++ b/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 +import PackageDescription + +#if TUIST +import ProjectDescription +import ProjectDescriptionHelpers + +let packageSettings = PackageSettings( + productTypes: [ + "TelemetryClient": .framework, + ], + targetSettings: [ + "TelemetryClient": [ + "UNINSTALLED_PRODUCTS_DIR": "$(TEMP_ROOT)/UninstalledProducts$(EFFECTIVE_PLATFORM_NAME)", + ], + ] +) +#endif + +let package = Package( + name: "Dependencies", + dependencies: [ + .package(url: "git@github.com:adamwulf/ClippingBezier.git", from: "1.2.0"), + .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"), + .package(url: "git@github.com:TelemetryDeck/SwiftClient.git", from: "1.0.0"), + ] +) diff --git a/Project.swift b/Project.swift index a77dd2c8..6a1a3be4 100644 --- a/Project.swift +++ b/Project.swift @@ -4,11 +4,6 @@ import ProjectDescriptionHelpers let project = Project( name: "Highlighter", organizationName: "Cocoatype, LLC", - packages: [ - .remote(url: "git@github.com:adamwulf/ClippingBezier.git", requirement: .upToNextMajor(from: "1.2.0")), - .remote(url: "https://github.com/siteline/SwiftUI-Introspect.git", requirement: .upToNextMajor(from: "0.1.3")), - .remote(url: "git@github.com:TelemetryDeck/SwiftClient.git", requirement: .upToNextMajor(from: "1.0.0")), - ], settings: .settings(base: [ "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME": "Accent Color", @@ -26,7 +21,6 @@ let project = Project( "CODE_SIGN_IDENTITY": "Apple Development: Buddy Build (D47V8Y25W5)", ], release: [ "CODE_SIGN_IDENTITY": "Apple Distribution", - "CODE_SIGN_IDENTITY[sdk=macosx*]": "3rd Party Mac Developer Installer: Cocoatype, LLC (287EDDET2B)", ]), targets: [ // products diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift index 2791ad11..33abd8d8 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift @@ -6,7 +6,7 @@ public enum Logging { name: "Logging", sdk: sdk, dependencies: [ - .package(product: "TelemetryClient", type: .runtime), + .external(name: "TelemetryClient"), ] ) } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift index 9c1b4845..30b96117 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Legacy/Editing.swift @@ -16,8 +16,8 @@ public enum Editing { .target(PurchaseMarketing.target), .target(Purchasing.doublesTarget), .target(Redactions.target(sdk: .catalyst)), - .package(product: "ClippingBezier", type: .runtime), - .package(product: "Introspect", type: .runtime), + .external(name: "ClippingBezier"), + .external(name: "Introspect"), ], settings: .settings( base: [ From bd18fda2a0a911902ed319f285cff49727cb9c20 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sun, 19 May 2024 06:42:42 -0700 Subject: [PATCH 19/21] Link TelemetryClient to tests --- .../Targets/Capabilities/Logging.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift index 33abd8d8..1101c013 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Capabilities/Logging.swift @@ -11,7 +11,12 @@ public enum Logging { ) } - public static let testTarget = Target.capabilitiesTestTarget(name: "Logging") + public static let testTarget = Target.capabilitiesTestTarget( + name: "Logging", + dependencies: [ + .external(name: "TelemetryClient"), + ] + ) public static let doublesTarget = Target.capabilitiesDoublesTarget(name: "Logging") } From cf2f30daa21b4dc83679e3877895c6d867c61362 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sun, 19 May 2024 06:47:54 -0700 Subject: [PATCH 20/21] Fix linting issues --- .swiftlint.yml | 2 ++ .../Editing/Sources/Extensions/UIBezierPathExtensions.swift | 1 - Tuist/ProjectDescriptionHelpers/TargetExtensions.swift | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 92cc3003..21f91166 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,6 +4,8 @@ disabled_rules: - identifier_name # incompatible with "name a variable" redemption - type_name # same excluded: + - .build + - Derived - vendor cyclomatic_complexity: diff --git a/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift b/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift index d0645459..10f07703 100644 --- a/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift +++ b/Modules/Legacy/Editing/Sources/Extensions/UIBezierPathExtensions.swift @@ -1,7 +1,6 @@ // Created by Geoff Pado on 5/1/19. // Copyright © 2019 Cocoatype, LLC. All rights reserved. - #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit import ObservationsMac diff --git a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift index d7c93054..0d107d0e 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift @@ -65,7 +65,7 @@ extension Target { sources: ["Modules/Capabilities/\(name)/Doubles/**"], dependencies: [ .target(name: name + sdk.nameSuffix), - .target(TestHelpers.interfaceTarget) + .target(TestHelpers.interfaceTarget), ] ) } From 8ac1b53d3c3cac7cc276f705c139553cbde7e793 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Sun, 19 May 2024 07:13:29 -0700 Subject: [PATCH 21/21] Fix archiving --- Package.swift | 1 + Project.swift | 19 +-------------- Tuist/ProjectDescriptionHelpers/Shared.swift | 23 +++++++++++++++++-- .../TargetExtensions.swift | 14 +++++++++-- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Package.swift b/Package.swift index d1866c5d..1a034853 100644 --- a/Package.swift +++ b/Package.swift @@ -9,6 +9,7 @@ let packageSettings = PackageSettings( productTypes: [ "TelemetryClient": .framework, ], + baseSettings: Shared.settings, targetSettings: [ "TelemetryClient": [ "UNINSTALLED_PRODUCTS_DIR": "$(TEMP_ROOT)/UninstalledProducts$(EFFECTIVE_PLATFORM_NAME)", diff --git a/Project.swift b/Project.swift index 6a1a3be4..80005bd1 100644 --- a/Project.swift +++ b/Project.swift @@ -4,24 +4,7 @@ import ProjectDescriptionHelpers let project = Project( name: "Highlighter", organizationName: "Cocoatype, LLC", - settings: .settings(base: [ - "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", - "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME": "Accent Color", - "CODE_SIGN_STYLE": "Manual", - "CURRENT_PROJECT_VERSION": "0", - "DEVELOPMENT_TEAM": "287EDDET2B", - "ENABLE_HARDENED_RUNTIME[sdk=macosx*]": "YES", - "IPHONEOS_DEPLOYMENT_TARGET": "15.0", - "MACOSX_DEPLOYMENT_TARGET": "12.0", - "MARKETING_VERSION": "999", - "OTHER_CODE_SIGN_FLAGS": "--deep", - "SWIFT_VERSION": "5.0", - "TARGETED_DEVICE_FAMILY": "1,2,6", - ], debug: [ - "CODE_SIGN_IDENTITY": "Apple Development: Buddy Build (D47V8Y25W5)", - ], release: [ - "CODE_SIGN_IDENTITY": "Apple Distribution", - ]), + settings: Shared.settings, targets: [ // products App.target, diff --git a/Tuist/ProjectDescriptionHelpers/Shared.swift b/Tuist/ProjectDescriptionHelpers/Shared.swift index 3e82da3c..9da953ed 100644 --- a/Tuist/ProjectDescriptionHelpers/Shared.swift +++ b/Tuist/ProjectDescriptionHelpers/Shared.swift @@ -1,7 +1,26 @@ import ProjectDescription -enum Shared { - static let resources: [ProjectDescription.ResourceFileElement] = [ +public enum Shared { + static let resources: [ResourceFileElement] = [ "App/Resources/Assets.xcassets", ] + + public static let settings: Settings = .settings(base: [ + "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon", + "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME": "Accent Color", + "CODE_SIGN_STYLE": "Manual", + "CURRENT_PROJECT_VERSION": "0", + "DEVELOPMENT_TEAM": "287EDDET2B", + "ENABLE_HARDENED_RUNTIME[sdk=macosx*]": "YES", + "IPHONEOS_DEPLOYMENT_TARGET": "15.0", + "MACOSX_DEPLOYMENT_TARGET": "12.0", + "MARKETING_VERSION": "999", + "OTHER_CODE_SIGN_FLAGS": "--deep", + "SWIFT_VERSION": "5.0", + "TARGETED_DEVICE_FAMILY": "1,2,6", + ], debug: [ + "CODE_SIGN_IDENTITY": "Apple Development: Buddy Build (D47V8Y25W5)", + ], release: [ + "CODE_SIGN_IDENTITY": "Apple Distribution", + ]) } diff --git a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift index 0d107d0e..def067aa 100644 --- a/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift +++ b/Tuist/ProjectDescriptionHelpers/TargetExtensions.swift @@ -19,7 +19,9 @@ extension Target { base: [ "DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER": false, ], - defaultSettings: .none + defaultSettings: .recommended(excluding: [ + "CODE_SIGN_IDENTITY", + ]) ) ) } @@ -66,7 +68,15 @@ extension Target { dependencies: [ .target(name: name + sdk.nameSuffix), .target(TestHelpers.interfaceTarget), - ] + ], + settings: .settings( + base: [ + "DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER": false, + ], + defaultSettings: .recommended(excluding: [ + "CODE_SIGN_IDENTITY", + ]) + ) ) } }