From 62a8c42a10734047af1ac126acfed1d054cb6677 Mon Sep 17 00:00:00 2001 From: James Borthwick <109382862+jamesrb1@users.noreply.github.com> Date: Thu, 9 May 2024 11:37:11 -0700 Subject: [PATCH] Preprocessor to make select constructs public (#3880) This PR allows the Paywalls Tester app to access some items internal to purchases-ios by temporarily marking them public during complication. Items that need to be public have been annotated with `\\@PublicForExternalTesting`. The Paywalls Tester project runs a script, Preprocessor.sh, as a pre-action to add `public` to all classes/structs/enums/functions/inits that have been annotated. After compilation it runs a second script, Postprocessor.sh, as a post-action to undo these changes so that they don't accidentally get checked in. The first script is run as a scheme pre-action rather than as a run script build phase, because when run as a build phase the changes to the files aren't picked up until the next compilation attempt. image It also changes the archive step to be built with a release configuration, and removes the use of `@testable import` for debug builds. resolves PWL-459 --- ...alOrIntroEligibilityChecker+TestData.swift | 8 +-- .../TrialOrIntroEligibilityChecker.swift | 1 + .../Data/PaywallViewConfiguration.swift | 3 + RevenueCatUI/Modifiers/ViewExtensions.swift | 1 + RevenueCatUI/PaywallView.swift | 1 + RevenueCatUI/Purchasing/PurchaseHandler.swift | 1 + RevenueCatUI/View+PresentPaywallFooter.swift | 1 + .../PaywallsTester.xcodeproj/project.pbxproj | 6 ++ .../PaywallsTester - Live Config.xcscheme | 38 +++++++++- .../PaywallsTester - SK config.xcscheme | 36 ++++++++++ .../UI/Views/CustomPaywallContent.swift | 4 -- .../UI/Views/OfferingList/OfferingsList.swift | 4 -- .../UI/Views/PaywallPresenter.swift | 4 -- .../PaywallsTester/Postprocessor.sh | 52 ++++++++++++++ .../PaywallsTester/Preprocessor.sh | 69 +++++++++++++++++++ 15 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 Tests/TestingApps/PaywallsTester/Postprocessor.sh create mode 100644 Tests/TestingApps/PaywallsTester/Preprocessor.sh diff --git a/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker+TestData.swift b/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker+TestData.swift index 668b324de1..4b04f408c6 100644 --- a/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker+TestData.swift +++ b/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker+TestData.swift @@ -14,12 +14,11 @@ import Foundation import RevenueCat -#if DEBUG - @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) extension TrialOrIntroEligibilityChecker { /// Creates a mock `TrialOrIntroEligibilityChecker` with a constant result. + // @PublicForExternalTesting static func producing(eligibility: @autoclosure @escaping () -> IntroEligibilityStatus) -> Self { return .init { packages in return Dictionary( @@ -34,7 +33,7 @@ extension TrialOrIntroEligibilityChecker { ) } } - +#if DEBUG /// Creates a copy of this `TrialOrIntroEligibilityChecker` with a delay. func with(delay seconds: TimeInterval) -> Self { return .init { [checker = self.checker] in @@ -43,7 +42,6 @@ extension TrialOrIntroEligibilityChecker { return await checker($0) } } +#endif } - -#endif diff --git a/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker.swift b/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker.swift index 1ea2c1855c..072145c7d1 100644 --- a/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker.swift +++ b/RevenueCatUI/Data/IntroEligibility/TrialOrIntroEligibilityChecker.swift @@ -15,6 +15,7 @@ import Foundation import RevenueCat @available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.2, *) +// @PublicForExternalTesting final class TrialOrIntroEligibilityChecker: ObservableObject { typealias Checker = @Sendable ([Package]) async -> [Package: IntroEligibilityStatus] diff --git a/RevenueCatUI/Data/PaywallViewConfiguration.swift b/RevenueCatUI/Data/PaywallViewConfiguration.swift index da056c302b..b9e4adc505 100644 --- a/RevenueCatUI/Data/PaywallViewConfiguration.swift +++ b/RevenueCatUI/Data/PaywallViewConfiguration.swift @@ -11,6 +11,7 @@ import RevenueCat /// Parameters needed to configure a ``PaywallView``. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +// @PublicForExternalTesting struct PaywallViewConfiguration { var content: Content @@ -45,6 +46,7 @@ struct PaywallViewConfiguration { extension PaywallViewConfiguration { /// Offering selection for the paywall. + // @PublicForExternalTesting enum Content { case defaultOffering @@ -60,6 +62,7 @@ extension PaywallViewConfiguration { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension PaywallViewConfiguration { + // @PublicForExternalTesting init( offering: Offering? = nil, customerInfo: CustomerInfo? = nil, diff --git a/RevenueCatUI/Modifiers/ViewExtensions.swift b/RevenueCatUI/Modifiers/ViewExtensions.swift index 4f83dc7c43..efc381cbde 100644 --- a/RevenueCatUI/Modifiers/ViewExtensions.swift +++ b/RevenueCatUI/Modifiers/ViewExtensions.swift @@ -86,6 +86,7 @@ extension View { } @ViewBuilder + // @PublicForExternalTesting func scrollableIfNecessary(_ axis: Axis = .vertical, enabled: Bool = true) -> some View { if enabled { if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { diff --git a/RevenueCatUI/PaywallView.swift b/RevenueCatUI/PaywallView.swift index 8ebc2c47ab..b1d948bfe0 100644 --- a/RevenueCatUI/PaywallView.swift +++ b/RevenueCatUI/PaywallView.swift @@ -91,6 +91,7 @@ public struct PaywallView: View { ) } + // @PublicForExternalTesting init(configuration: PaywallViewConfiguration) { self._introEligibility = .init(wrappedValue: configuration.introEligibility ?? .default()) self._purchaseHandler = .init(wrappedValue: configuration.purchaseHandler ?? .default()) diff --git a/RevenueCatUI/Purchasing/PurchaseHandler.swift b/RevenueCatUI/Purchasing/PurchaseHandler.swift index 1b8473129d..c027cede6c 100644 --- a/RevenueCatUI/Purchasing/PurchaseHandler.swift +++ b/RevenueCatUI/Purchasing/PurchaseHandler.swift @@ -16,6 +16,7 @@ import StoreKit import SwiftUI @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +// @PublicForExternalTesting final class PurchaseHandler: ObservableObject { private let purchases: PaywallPurchasesType diff --git a/RevenueCatUI/View+PresentPaywallFooter.swift b/RevenueCatUI/View+PresentPaywallFooter.swift index ae5243be3d..29eb0bb804 100644 --- a/RevenueCatUI/View+PresentPaywallFooter.swift +++ b/RevenueCatUI/View+PresentPaywallFooter.swift @@ -181,6 +181,7 @@ extension View { ) } + // @PublicForExternalTesting func paywallFooter( offering: Offering?, customerInfo: CustomerInfo?, diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj index 62c71c77d2..e42981a280 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 4FCA01FB2A3A1CBD00B262C0 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FCA01FA2A3A1CBD00B262C0 /* StoreKit.framework */; }; 4FDF11202A7270F3004F3680 /* SamplePaywallsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF111F2A7270F3004F3680 /* SamplePaywallsList.swift */; }; 4FDF11222A72714C004F3680 /* SamplePaywalls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF11212A72714C004F3680 /* SamplePaywalls.swift */; }; + 880B2AF22BEC2D62006B9393 /* Preprocessor.sh in Resources */ = {isa = PBXBuildFile; fileRef = 880B2AF12BEC2D62006B9393 /* Preprocessor.sh */; }; 88B2F9882BE1943C00B43E0B /* ManagePaywallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B2F9872BE1943C00B43E0B /* ManagePaywallButton.swift */; }; 88B2F98B2BE19B1200B43E0B /* OfferingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B2F98A2BE19B1200B43E0B /* OfferingButton.swift */; }; 88B2F98D2BE3F1E900B43E0B /* TemplateInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B2F98C2BE3F1E900B43E0B /* TemplateInfo.swift */; }; @@ -94,6 +95,8 @@ 4FDF111F2A7270F3004F3680 /* SamplePaywallsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplePaywallsList.swift; sourceTree = ""; }; 4FDF11212A72714C004F3680 /* SamplePaywalls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplePaywalls.swift; sourceTree = ""; }; 4FFD2A602AA154B4001F4B0C /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + 880B2AF12BEC2D62006B9393 /* Preprocessor.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = Preprocessor.sh; sourceTree = SOURCE_ROOT; }; + 880B2AF32BEC35AA006B9393 /* Postprocessor.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Postprocessor.sh; sourceTree = SOURCE_ROOT; }; 88B2F9872BE1943C00B43E0B /* ManagePaywallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagePaywallButton.swift; sourceTree = ""; }; 88B2F98A2BE19B1200B43E0B /* OfferingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingButton.swift; sourceTree = ""; }; 88B2F98C2BE3F1E900B43E0B /* TemplateInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateInfo.swift; sourceTree = ""; }; @@ -206,6 +209,8 @@ children = ( 4F34FF642A60ADBD00AADF11 /* Configuration.swift */, 4FDF11212A72714C004F3680 /* SamplePaywalls.swift */, + 880B2AF12BEC2D62006B9393 /* Preprocessor.sh */, + 880B2AF32BEC35AA006B9393 /* Postprocessor.sh */, 4FC882E02A5870C6005BE85E /* Info.plist */, 4FC046BF2A572E3700A28BCF /* Assets.xcassets */, 4FC046BD2A572E3700A28BCF /* PaywallsTester.entitlements */, @@ -397,6 +402,7 @@ buildActionMask = 2147483647; files = ( 4F217A102A6DB6FB000B092D /* Assets.xcassets in Resources */, + 880B2AF22BEC2D62006B9393 /* Preprocessor.sh in Resources */, 4F4557E22A6FFE6A00160521 /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme index 42bf650ba3..0029172252 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - Live Config.xcscheme @@ -5,6 +5,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - SK config.xcscheme b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - SK config.xcscheme index 1a3821cdfa..b7fddc519b 100644 --- a/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - SK config.xcscheme +++ b/Tests/TestingApps/PaywallsTester/PaywallsTester.xcodeproj/xcshareddata/xcschemes/PaywallsTester - SK config.xcscheme @@ -5,6 +5,42 @@ + + + + + + + + + + + + + + + + + + + + "$log_file" + +# Find all .orig files and restore them +find "$base_directory" -type f -name "*.swift.orig" | while read -r backup_file; do + original_file="${backup_file%.orig}" + cp "$backup_file" "$original_file" + rm -f "$backup_file" + + echo "Restored: $original_file" | tee -a "$log_file" +done + +echo "Undo process completed." | tee -a "$log_file" diff --git a/Tests/TestingApps/PaywallsTester/Preprocessor.sh b/Tests/TestingApps/PaywallsTester/Preprocessor.sh new file mode 100644 index 0000000000..36b59c05f2 --- /dev/null +++ b/Tests/TestingApps/PaywallsTester/Preprocessor.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +# Preprocessor.sh +# PaywallsTester +# +# Created by James Borthwick on 2024-05-08. +# + +# Intended to be run via the scheme's pre-actions build phase +# +# This script searches up from ${PROJECT_DIR} to locate the RevenueCatUI directory. +# Once found, it searches through all `.swift` files starting from that base directory. +# It finds occurrences of `//@PublicForExternalTesting` and modifies the subsequent class, struct, +# func, init, and enum declaration to make it public. + +find_dir() { + local dir="$1" + local target_dir="$2" + while [[ "$dir" != "/" ]]; do + if [[ -e "$dir/$target_dir" ]]; then + echo "$dir/$target_dir" + return 0 + fi + dir=$(dirname "$dir") # go up one level + done + return 1 # Target directory not found +} + +echo "Starting script to make items annotated with \`\/\/ @PublicForExternalTesting\` public." + +base_directory=$(find_dir "${PROJECT_DIR}" "RevenueCatUI") + +if [[ -z "$base_directory" ]]; then + echo "Error: RevenueCatUI not found in the current directory or any parent directory." + exit 1 +fi + +echo "Starting at: $base_directory" + +# debug log +log_file="preprocess_log.txt" +echo "Starting log at $(date)" > "$log_file" + +# Find all .swift files recursively from the base directory +find "$base_directory" -type f -name "*.swift" | while read -r file; do + + if grep -q '// @PublicForExternalTesting' "$file"; then + # Backup original file + backup_file="${file}.orig" + cp "$file" "$original_file" + + # Find //@PublicForExternalTesting and replace it with public before declarations + sed -i.orig -E \ + '/\/\/ @PublicForExternalTesting[[:space:]]*$/{ + N + s/\/\/ @PublicForExternalTesting[[:space:]]*\n[[:space:]]*(static[[:space:]]+)?(struct|class|final[[:space:]]+class|enum|init|func)/public \1\2/ + }' "$file" + + # Log changes made to the file + diff_output=$(diff "$backup_file" "$file") + if [[ -n "$diff_output" ]]; then + echo "Changes made in file: $file" | tee -a "$log_file" + echo "$diff_output" | tee -a "$log_file" + fi + + fi +done + +echo "Processing completed." | tee -a "$log_file"