diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b05d89bec..7ff9b30418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub. +## 4.12.6 + +### Fixes + +- Fixes a rare issue where TestFlight products could display in a different currency on the paywall than on Apple's payment sheet. + ## 4.12.5 ### Enhancements diff --git a/Package.resolved b/Package.resolved index 9aca0b3164..3345860464 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/superwall/Superscript-iOS", "state": { "branch": null, - "revision": "bde42f1c7652a69a469677978ed97cba92c4dd2c", - "version": "1.0.10" + "revision": "ce546d7ad70b5ce5f66ea0caffefb0d97be49f34", + "version": "1.0.12" } } ] diff --git a/Sources/SuperwallKit/Logger/Logger.swift b/Sources/SuperwallKit/Logger/Logger.swift index 5558ce798e..f5539f21b4 100644 --- a/Sources/SuperwallKit/Logger/Logger.swift +++ b/Sources/SuperwallKit/Logger/Logger.swift @@ -105,7 +105,8 @@ enum Logger: Loggable { dumping["error"] = error } - var name = "\(Date().isoString) \(logLevel.descriptionEmoji) [Superwall] [\(scope.description)] - \(logLevel.description)" + var name = "\(Date().isoString) \(logLevel.descriptionEmoji) " + + "[Superwall] [\(scope.description)] - \(logLevel.description)" if let message = message { name += ": \(message)" diff --git a/Sources/SuperwallKit/Misc/Constants.swift b/Sources/SuperwallKit/Misc/Constants.swift index f295fd5f51..f33be4134c 100644 --- a/Sources/SuperwallKit/Misc/Constants.swift +++ b/Sources/SuperwallKit/Misc/Constants.swift @@ -18,5 +18,5 @@ let sdkVersion = """ */ let sdkVersion = """ -4.12.5 +4.12.6 """ diff --git a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift index eead0d9d33..9095caf6a3 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessageHandler.swift @@ -342,6 +342,7 @@ final class PaywallMessageHandler: WebEventDelegate { // block selection let selectionString = + // swiftlint:disable:next line_length "var css = '*{-webkit-touch-callout:none;-webkit-user-select:none} .w-webflow-badge { display: none !important; }'; " + "var head = document.head || document.getElementsByTagName('head')[0]; " + "var style = document.createElement('style'); style.type = 'text/css'; " @@ -355,6 +356,7 @@ final class PaywallMessageHandler: WebEventDelegate { self.delegate?.webView.configuration.userContentController.addUserScript(selectionScript) let preventSelection = + // swiftlint:disable:next line_length "var css = '*{-webkit-touch-callout:none;-webkit-user-select:none}'; var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(css)); head.appendChild(style);" self.delegate?.webView.evaluateJavaScript(preventSelection) diff --git a/Sources/SuperwallKit/Storage/Cache/Cache.swift b/Sources/SuperwallKit/Storage/Cache/Cache.swift index c9ace991fc..022be9aa2e 100644 --- a/Sources/SuperwallKit/Storage/Cache/Cache.swift +++ b/Sources/SuperwallKit/Storage/Cache/Cache.swift @@ -71,16 +71,23 @@ class Cache { guard let self = self else { return } guard let documentDirectory = self.fileManager.urls(for: .documentDirectory, in: .userDomainMask).first, - let applicationSupportDirectory = self.fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + let applicationSupportDirectory = self.fileManager.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first else { return } - let userSpecificDocumentUrl = documentDirectory.appendingPathComponent(Cache.userSpecificDocumentDirectoryPrefix) - let appSpecificDocumentUrl = documentDirectory.appendingPathComponent(Cache.appSpecificDocumentDirectoryPrefix) + let userSpecificDocumentUrl = documentDirectory + .appendingPathComponent(Cache.userSpecificDocumentDirectoryPrefix) + let appSpecificDocumentUrl = documentDirectory + .appendingPathComponent(Cache.appSpecificDocumentDirectoryPrefix) - let userSpecificSupportUrl = applicationSupportDirectory.appendingPathComponent(Cache.userSpecificDocumentDirectoryPrefix) - let appSpecificSupportUrl = applicationSupportDirectory.appendingPathComponent(Cache.appSpecificDocumentDirectoryPrefix) + let userSpecificSupportUrl = applicationSupportDirectory + .appendingPathComponent(Cache.userSpecificDocumentDirectoryPrefix) + let appSpecificSupportUrl = applicationSupportDirectory + .appendingPathComponent(Cache.appSpecificDocumentDirectoryPrefix) do { // Create the destination directories if they don't exist. diff --git a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift index 1c1b37ec6e..8f3e620ed4 100644 --- a/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift +++ b/Sources/SuperwallKit/StoreKit/Products/StoreProduct/SK2StoreProduct.swift @@ -18,7 +18,6 @@ import StoreKit @available(iOS 15.0, tvOS 15.0, watchOS 8.0, *) struct SK2StoreProduct: StoreProductType { - private let priceFormatterProvider = PriceFormatterProvider() let entitlements: Set init( @@ -59,25 +58,7 @@ struct SK2StoreProduct: StoreProductType { } var localizedPrice: String { - return priceFormatter(locale: underlyingSK2Product.priceFormatStyle.locale) - .string(from: underlyingSK2Product.price as NSDecimalNumber) ?? "?" - } - - var priceFormatter: NumberFormatter? { - guard let currencyCode = self.currencyCode else { - return nil - } - return priceFormatterProvider.priceFormatterForSK2( - withCurrencyCode: currencyCode, - locale: underlyingSK2Product.priceFormatStyle.locale - ) - } - - private func priceFormatter(locale: Locale) -> NumberFormatter { - let formatter = NumberFormatter() - formatter.locale = underlyingSK2Product.priceFormatStyle.locale - formatter.numberStyle = .currency - return formatter + return underlyingSK2Product.price.formatted(underlyingSK2Product.priceFormatStyle) } var localizedSubscriptionPeriod: String { @@ -271,87 +252,56 @@ struct SK2StoreProduct: StoreProductType { } var dailyPrice: String { - if underlyingSK2Product.price == 0.00 { - return "$0.00" - } - - let numberFormatter = NumberFormatter() - let locale = underlyingSK2Product.priceFormatStyle.locale - numberFormatter.numberStyle = .currency - numberFormatter.locale = locale - guard let subscriptionPeriod = underlyingSK2Product.subscription?.subscriptionPeriod else { return "n/a" } + let numberOfUnits = subscriptionPeriod.value var periods: Decimal = 1.0 let inputPrice = underlyingSK2Product.price - if subscriptionPeriod.unit == .year { + switch subscriptionPeriod.unit { + case .year: periods = Decimal(365 * numberOfUnits) - } - - if subscriptionPeriod.unit == .month { + case .month: periods = Decimal(30 * numberOfUnits) - } - - if subscriptionPeriod.unit == .week { + case .week: periods = Decimal(7 * numberOfUnits) - } - - if subscriptionPeriod.unit == .day { + case .day: + periods = Decimal(numberOfUnits) + @unknown default: periods = Decimal(numberOfUnits) } - return numberFormatter.string(from: NSDecimalNumber(decimal: inputPrice / periods)) ?? "n/a" + return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle) } var weeklyPrice: String { - if underlyingSK2Product.price == 0.00 { - return "$0.00" - } - - let numberFormatter = NumberFormatter() - let locale = underlyingSK2Product.priceFormatStyle.locale - numberFormatter.numberStyle = .currency - numberFormatter.locale = locale - guard let subscriptionPeriod = underlyingSK2Product.subscription?.subscriptionPeriod else { return "n/a" } + let numberOfUnits = subscriptionPeriod.value var periods: Decimal = 1.0 let inputPrice = underlyingSK2Product.price - if subscriptionPeriod.unit == .year { + switch subscriptionPeriod.unit { + case .year: periods = Decimal(52 * numberOfUnits) - } - - if subscriptionPeriod.unit == .month { + case .month: periods = Decimal(4 * numberOfUnits) - } - - if subscriptionPeriod.unit == .week { + case .week: periods = Decimal(numberOfUnits) - } - - if subscriptionPeriod.unit == .day { + case .day: periods = Decimal(numberOfUnits) / Decimal(7) + @unknown default: + periods = Decimal(numberOfUnits) } - return numberFormatter.string(from: NSDecimalNumber(decimal: inputPrice / periods)) ?? "n/a" + return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle) } var monthlyPrice: String { - if underlyingSK2Product.price == 0.00 { - return "$0.00" - } - - let numberFormatter = NumberFormatter() - let locale = underlyingSK2Product.priceFormatStyle.locale - numberFormatter.numberStyle = .currency - numberFormatter.locale = locale - guard let subscriptionPeriod = underlyingSK2Product.subscription?.subscriptionPeriod else { return "n/a" } @@ -360,35 +310,23 @@ struct SK2StoreProduct: StoreProductType { var periods: Decimal = 1.0 let inputPrice = underlyingSK2Product.price - if subscriptionPeriod.unit == .year { + switch subscriptionPeriod.unit { + case .year: periods = Decimal(12 * numberOfUnits) - } - - if subscriptionPeriod.unit == .month { + case .month: periods = Decimal(1 * numberOfUnits) - } - - if subscriptionPeriod.unit == .week { + case .week: periods = Decimal(numberOfUnits) / Decimal(4) - } - - if subscriptionPeriod.unit == .day { + case .day: periods = Decimal(numberOfUnits) / Decimal(30) + @unknown default: + periods = Decimal(numberOfUnits) } - return numberFormatter.string(from: NSDecimalNumber(decimal: inputPrice / periods)) ?? "n/a" + return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle) } var yearlyPrice: String { - if underlyingSK2Product.price == 0.00 { - return "$0.00" - } - - let numberFormatter = NumberFormatter() - let locale = underlyingSK2Product.priceFormatStyle.locale - numberFormatter.numberStyle = .currency - numberFormatter.locale = locale - guard let subscriptionPeriod = underlyingSK2Product.subscription?.subscriptionPeriod else { return "n/a" } @@ -397,23 +335,20 @@ struct SK2StoreProduct: StoreProductType { var periods: Decimal = 1.0 let inputPrice = underlyingSK2Product.price - if subscriptionPeriod.unit == .year { + switch subscriptionPeriod.unit { + case .year: periods = Decimal(numberOfUnits) - } - - if subscriptionPeriod.unit == .month { + case .month: periods = Decimal(numberOfUnits) / Decimal(12) - } - - if subscriptionPeriod.unit == .week { + case .week: periods = Decimal(numberOfUnits) / Decimal(52) - } - - if subscriptionPeriod.unit == .day { + case .day: periods = Decimal(numberOfUnits) / Decimal(365) + @unknown default: + periods = Decimal(numberOfUnits) } - return numberFormatter.string(from: NSDecimalNumber(decimal: inputPrice / periods)) ?? "n/a" + return (inputPrice / periods).formatted(underlyingSK2Product.priceFormatStyle) } var hasFreeTrial: Bool { @@ -452,16 +387,16 @@ struct SK2StoreProduct: StoreProductType { return futureDate } - var trialPeriodEndDateString: String { - if let trialPeriodEndDate = trialPeriodEndDate { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .none - dateFormatter.locale = .autoupdatingCurrent + private static let trialDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + formatter.locale = .autoupdatingCurrent + return formatter + }() - return dateFormatter.string(from: trialPeriodEndDate) - } - return "" + var trialPeriodEndDateString: String { + trialPeriodEndDate.map { Self.trialDateFormatter.string(from: $0) } ?? "" } var trialPeriodDays: Int { @@ -658,22 +593,22 @@ struct SK2StoreProduct: StoreProductType { func trialPeriodPricePerUnit(_ unit: SubscriptionPeriod.Unit) -> String { guard let introductoryDiscount = introductoryDiscount else { - return priceFormatter?.string(from: 0.00) ?? "$0.00" + return Decimal(0).formatted(underlyingSK2Product.priceFormatStyle) } if introductoryDiscount.price == 0.00 { - return priceFormatter?.string(from: 0.00) ?? "$0.00" + return Decimal(0).formatted(underlyingSK2Product.priceFormatStyle) } let introMonthlyPrice = introductoryDiscount.pricePerUnit(unit) - return priceFormatter?.string(from: NSDecimalNumber(decimal: introMonthlyPrice)) ?? "$0.00" + return introMonthlyPrice.formatted(underlyingSK2Product.priceFormatStyle) } var localizedTrialPeriodPrice: String { guard let price = underlyingSK2Product.subscription?.introductoryOffer?.price else { - return priceFormatter?.string(from: 0.00) ?? "$0.00" + return Decimal(0).formatted(underlyingSK2Product.priceFormatStyle) } - return priceFormatter?.string(from: price as NSDecimalNumber) ?? "$0.00" + return price.formatted(underlyingSK2Product.priceFormatStyle) } } diff --git a/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift b/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift index a2c7f52726..2120806e11 100644 --- a/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift +++ b/Sources/SuperwallKit/StoreKit/Transactions/TransactionManager.swift @@ -269,8 +269,9 @@ final class TransactionManager { } else { var message = "Transactions Failed to Restore." if !hasActiveEntitlements && hasRestored { - message += - " The restoration result is \"restored\" but there are no active entitlements. Ensure the active entitlements are set before confirming successful restoration." + message += " The restoration result is \"restored\" but there are no active " + + "entitlements. Ensure the active entitlements are set before confirming " + + "successful restoration." } if case .failed(let error) = restorationResult, let error = error { diff --git a/SuperwallKit.podspec b/SuperwallKit.podspec index 2c21a5eb16..5a4afb2252 100644 --- a/SuperwallKit.podspec +++ b/SuperwallKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "SuperwallKit" - s.version = "4.12.5" + s.version = "4.12.6" s.summary = "Superwall: In-App Paywalls Made Easy" s.description = "Paywall infrastructure for mobile apps :) we make things like editing your paywall and running price tests as easy as clicking a few buttons. superwall.com" diff --git a/SuperwallKit.xcodeproj/project.pbxproj b/SuperwallKit.xcodeproj/project.pbxproj index 8e3c720db4..cb6b8dccd9 100644 --- a/SuperwallKit.xcodeproj/project.pbxproj +++ b/SuperwallKit.xcodeproj/project.pbxproj @@ -25,9 +25,11 @@ 07862D18809FA5DEA95AE440 /* NSManagedObjectContext+mergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B35EF62D8C986B504B052C /* NSManagedObjectContext+mergeChanges.swift */; }; 07CAD1B0A849A7593B1EF6D4 /* FakeContactsAuthorizationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8708F74D80CB9A440F34A556 /* FakeContactsAuthorizationStatus.swift */; }; 0911B1213899E3C4E1620119 /* Dictionary+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E9E1A8F57B5DCA4CD16296F /* Dictionary+Keys.swift */; }; + 097719E21BBD153BA6FD6785 /* SubscriptionPeriodPriceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF1F5EAC9C4E384EBBE5EA9 /* SubscriptionPeriodPriceTests.swift */; }; 09AA071B4A67A6256D00BDD6 /* PaywallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 187934D3C89220C605299A17 /* PaywallManager.swift */; }; 0A0BD14265AA8CE75A84940E /* TaskCoalescer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F90791145963999DDA319 /* TaskCoalescer.swift */; }; 0A1366F15DD3C1761C095DF5 /* SuperwallOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDA311A030FDFC45AEE248A /* SuperwallOptions.swift */; }; + 0A35D3D8CCC8D043628A0A4F /* TrackingAuthorizationStatusConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC92F1146FA9FA76AF25227 /* TrackingAuthorizationStatusConversionTests.swift */; }; 0A5EFFC920E6BB29814BD66B /* Foundation+ASN1Coder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80281364A23252E30DEEA1AA /* Foundation+ASN1Coder.swift */; }; 0AB9CCC164DD87C81318AAB0 /* PublicIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4493EE88B00CADF85EF1196 /* PublicIdentity.swift */; }; 0B5A0C6EA2D1C98B32110FD9 /* NotificationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5DCFB58EF4DBC9084A6B89 /* NotificationScheduler.swift */; }; @@ -62,10 +64,12 @@ 1A6C6E6DAD8C0236FA24FF10 /* CheckNoPaywallAlreadyPresented.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582CE0C5BA6EA57C7FE3EE43 /* CheckNoPaywallAlreadyPresented.swift */; }; 1BA9EC022F0016E72A5EB01A /* PaywallPresentationRequestStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC211BFF9364E4B10418695B /* PaywallPresentationRequestStatus.swift */; }; 1BDB91B70EAEC10097941381 /* SWBounceButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 299F91895EE88281B5ED8320 /* SWBounceButton.swift */; }; + 1D27BDC498FDFA24FFFD49EB /* PermissionHandler+Microphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE48C68EEF60E63D5B39FD /* PermissionHandler+Microphone.swift */; }; 1E0D00DF75A6779C78145750 /* SurveyPresentationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B66B5A624F8A2E225EACA69 /* SurveyPresentationResult.swift */; }; 1E7EBE0C39AC302D9B5BFF27 /* PublicGameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010F0F8FCE0A86D8F2823A47 /* PublicGameController.swift */; }; 1E81A71ADE8A5EAD9E609E1D /* AppSessionManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B78FB9232236AF44369EA92 /* AppSessionManagerMock.swift */; }; 1F20D775BC035F8A75B79323 /* PaywallMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF61DCA9CF170E0AFC507BAE /* PaywallMessageHandler.swift */; }; + 1F9AE04458B942D070FC4832 /* FakeTrackingAuthorizationStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE43B98D847BB6DE291F0B4 /* FakeTrackingAuthorizationStatusTests.swift */; }; 1FB66F276E4FD5AEC37EC14A /* ContactStoreProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F35F68AF572F7CDF174320C /* ContactStoreProxy.swift */; }; 1FFADB97F7E05549750E14E9 /* TaskExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21ED2D8CB4DDA70E228E8BC /* TaskExecutor.swift */; }; 205B0A0F4179E2680B07172D /* WebArchiveManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5180737863E22145980065 /* WebArchiveManager.swift */; }; @@ -309,6 +313,7 @@ A2DF1D9E1591874F082E6848 /* AudienceAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86C79B54278BF17FB1117E1 /* AudienceAttributes.swift */; }; A3A0961A4A230C10B8896400 /* PopupTransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFFA527207A52EB7C70CAD4 /* PopupTransitionTests.swift */; }; A44BAE75AAE4713FAE38F992 /* ProductsFetcherSK1.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6BA222CB2EAA4B65F362C5 /* ProductsFetcherSK1.swift */; }; + A4C14BE9296BD05400C96F82 /* TestProducts.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 72D9881BEA1D2A40DFC28D61 /* TestProducts.storekit */; }; A51060CF6339BF9383F94B51 /* MockSubscriptionPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C4AD6349F2D432132F36D5 /* MockSubscriptionPeriod.swift */; }; A59E22688D68CBE09FF78D57 /* IntroOfferEligibilityRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AEAA8E3B5F51848523AE61 /* IntroOfferEligibilityRequest.swift */; }; A646BB605400E4BDD321F389 /* SurveyShowCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E2D026C30691F11D4E839F /* SurveyShowCondition.swift */; }; @@ -316,7 +321,9 @@ A73497BB3DCD881318A5CD86 /* ConfigLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97D7F499B2CBFFF0A61F8D72 /* ConfigLogicTests.swift */; }; A74BBEF8CCF5A35AB19BB70C /* ProductPurchaserLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A413B6FF46B130D90A428B4 /* ProductPurchaserLogic.swift */; }; A78DC9BD71DD85831025D620 /* SuperwallGraveyard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36898353ABCE620BA7AEE59 /* SuperwallGraveyard.swift */; }; + A7D83FE18FC0A70BCD5A3B2C /* PermissionsHandler+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DA18E503D82C213BC5567B /* PermissionsHandler+Tracking.swift */; }; A8B37372F3F4ED9FAD76CE87 /* PostbackAssignmentWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0D2AB91DA66490A73D1CB5 /* PostbackAssignmentWrapper.swift */; }; + A9B924A1211117378743A534 /* MicrophonePermissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD045A5C4A47B1CA9365285 /* MicrophonePermissionTests.swift */; }; A9F9A35AEC72D17C7C15DAD4 /* PaywallArchiveManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB580546C647ED707C43FEC /* PaywallArchiveManager.swift */; }; AA86FF87863EB7A7917969D3 /* PermissionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D0B6F781D32B2DCDBE689C /* PermissionHandling.swift */; }; AACC7BEE37DDDD7068A1E48C /* TransactionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646E6799FE4934BF76A06F34 /* TransactionManager.swift */; }; @@ -334,6 +341,7 @@ B0AD4A89AD5101360F93652D /* SubscriptionTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACDC7427B6340E9D86F9B0F /* SubscriptionTransaction.swift */; }; B0B0AD9409CEFE7CA8225146 /* Array+SafeRemove.swift in Sources */ = {isa = PBXBuildFile; fileRef = C855DE8F5341D67C614E3AF5 /* Array+SafeRemove.swift */; }; B0DC8290B081B74CC65E9305 /* CELEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79E9DBDDA7FEE63C15FBEAF /* CELEvaluatorTests.swift */; }; + B0F7F66E24C7AEDE460F34FE /* FakeTrackingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F38AD737B1CFECEB2AAF85 /* FakeTrackingManager.swift */; }; B10030CC414C2C341487F4B8 /* PaywallViewControllerDelegateAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 990461F7A9B2F3ED62B3A628 /* PaywallViewControllerDelegateAdapter.swift */; }; B146C134ABE092C3C9ACADEC /* PurchaseResult+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = A524F7AAE90E48C3B8D7E99A /* PurchaseResult+Internal.swift */; }; B15607185B9E4229C6C4F240 /* SK2StoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B96E2A1A289D96267EC0BC /* SK2StoreTransaction.swift */; }; @@ -358,6 +366,7 @@ BBC0ADE1AAB3E8C2DC5E4F01 /* ASN1Decoder+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17A8801A2A9454E66D892 /* ASN1Decoder+Utils.swift */; }; BC526F821C0BDAC76D7B3769 /* LocationAuthorizationStatusConversionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9CFF209DA6B8B42B405D20 /* LocationAuthorizationStatusConversionTests.swift */; }; BC8A62869C7BACE6D0867195 /* AssignmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7180900DD0767487E671639 /* AssignmentTests.swift */; }; + BCD5EA74E59F7BC43B0816C5 /* TrackingManagerProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8E506778E9750512E9F9D3 /* TrackingManagerProxyTests.swift */; }; BCF808C7AC319C2B1F0AD52D /* ConfigResponseLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4827295A4E093CAEE2207DDF /* ConfigResponseLogicTests.swift */; }; BCFF20903199DDDE379D81E0 /* InAppReceiptPayloadContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 862888AB2869D09AA55A017D /* InAppReceiptPayloadContainer.swift */; }; BD152F3BA0BC197A5C6C8CC1 /* InAppReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0695B39826F85AACBA833B77 /* InAppReceipt.swift */; }; @@ -408,6 +417,7 @@ CE411644469AB99AC9DBB492 /* SWWebViewLoadingHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D9545633A97A2E63FEDF78A /* SWWebViewLoadingHandler.swift */; }; CE43399599386456DCCD1FDC /* FakeLocationAuthorizationStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3529B422EB2D09B69265B591 /* FakeLocationAuthorizationStatusTests.swift */; }; CE5307A037FCFD8CB8380EB5 /* GetPaywallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3781CF21200CD2333F6779A /* GetPaywallManager.swift */; }; + CE821667CB6676EA02510FE9 /* TrackingManagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A78C5C57C3C92444EBAC2E38 /* TrackingManagerProxy.swift */; }; CF2064883604B915C8768FC5 /* ASIdManagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF989B3D90DC3D88FACC4D45 /* ASIdManagerProxy.swift */; }; CF3683E2AD703237EC0CE22E /* PaywallProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95DABA23C6CBEF0AAA63C0 /* PaywallProducts.swift */; }; CFEB0D797815E8EDFB059767 /* Superwall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7EDB6D68D0AEDD332E40BB /* Superwall.swift */; }; @@ -464,6 +474,7 @@ E95C8C0485512D77E37703C5 /* ASN1Templates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AFD6EE9BED296D075A9618 /* ASN1Templates.swift */; }; E984458E465D5A834CC52302 /* CacheMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67602AF9B2543CAD0B42F3CF /* CacheMock.swift */; }; E986B0CF98B8C09AAA961E94 /* AppSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC89718EBDD71E09AB5F41DA /* AppSessionManager.swift */; }; + E9D95044254D79D2439D7B3E /* FakeTrackingAuthorizationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019FA4010BA11D24C68B8544 /* FakeTrackingAuthorizationStatus.swift */; }; E9E0BD599F353CC471B7A546 /* PresentationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CE1D40C874F73A3BEC090 /* PresentationInfo.swift */; }; E9F80BA7BCAB71270E0E1CC1 /* AttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EAB177118BC78B02C3C00A /* AttributionFetcher.swift */; }; EB1964816A8297CE133F96BF /* PurchaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E522F5BCABB3A95B97549E /* PurchaseError.swift */; }; @@ -535,6 +546,7 @@ 00736909A4B4A1C2F2C356BC /* Sk1StoreProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sk1StoreProduct.swift; sourceTree = ""; }; 010F0F8FCE0A86D8F2823A47 /* PublicGameController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicGameController.swift; sourceTree = ""; }; 018F5856F39FC33AFE9740D4 /* ConfirmHoldoutAssignmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmHoldoutAssignmentTests.swift; sourceTree = ""; }; + 019FA4010BA11D24C68B8544 /* FakeTrackingAuthorizationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTrackingAuthorizationStatus.swift; sourceTree = ""; }; 01E54BC1CA38F828AC77F774 /* PublicPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicPresentation.swift; sourceTree = ""; }; 03347D6B3B672A878CA2A7CA /* SurveyOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyOption.swift; sourceTree = ""; }; 03471273DF4C875227102BE2 /* ReceiptManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptManagerTests.swift; sourceTree = ""; }; @@ -583,11 +595,13 @@ 1B1A6ADFFB9FA982BF69C134 /* MockSKPaymentTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSKPaymentTransaction.swift; sourceTree = ""; }; 1B78FB9232236AF44369EA92 /* AppSessionManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSessionManagerMock.swift; sourceTree = ""; }; 1C16CBCBF2093DD9C5F3E105 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + 1CC92F1146FA9FA76AF25227 /* TrackingAuthorizationStatusConversionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingAuthorizationStatusConversionTests.swift; sourceTree = ""; }; 1D275ED98D2EE298F06708AF /* UIWindow+SwizzleSendEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+SwizzleSendEvent.swift"; sourceTree = ""; }; 1EBE35B7BB7FEBE02C8992D8 /* EntitlementsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntitlementsResponse.swift; sourceTree = ""; }; 2031E7FE7D2ECC7AFF8519AE /* CustomerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfo.swift; sourceTree = ""; }; 20365697A9C396E8EC746B77 /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; 20419CBCAD8CE28ADDD55E57 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; + 20DA18E503D82C213BC5567B /* PermissionsHandler+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PermissionsHandler+Tracking.swift"; sourceTree = ""; }; 2123B84F5C786278E128152D /* OccurrenceLogicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OccurrenceLogicTests.swift; sourceTree = ""; }; 214622367ACC8F66EB392105 /* AppSessionLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSessionLogic.swift; sourceTree = ""; }; 21903EACCA9AA13BB10917EC /* PermissionHandler+Camera.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PermissionHandler+Camera.swift"; sourceTree = ""; }; @@ -623,6 +637,7 @@ 2B430DE1BA468E280567F03C /* ProductTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductTemplate.swift; sourceTree = ""; }; 2BB10D9097CC124FFC34A4A0 /* RotationAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotationAnimation.swift; sourceTree = ""; }; 2BBA713121538238D5EBAB60 /* fr_CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr_CA; path = fr_CA.lproj/Localizable.strings; sourceTree = ""; }; + 2CF1F5EAC9C4E384EBBE5EA9 /* SubscriptionPeriodPriceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPeriodPriceTests.swift; sourceTree = ""; }; 2D1A60826D12F97F96E671DF /* SpringAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringAnimation.swift; sourceTree = ""; }; 2D1DB28BDC846324DD0CC091 /* PopupTransitionLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupTransitionLogic.swift; sourceTree = ""; }; 2D3DB70C19B7C07E1750DB8F /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; @@ -704,6 +719,7 @@ 5C2E30544869C5469AA31832 /* FactoryProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactoryProtocols.swift; sourceTree = ""; }; 5C57C1CCAF97244AE0DC953F /* PaywallManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallManagerMock.swift; sourceTree = ""; }; 5CD130C74880AD07DCD2A7AA /* RedemptionResultObjc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedemptionResultObjc.swift; sourceTree = ""; }; + 5CFE48C68EEF60E63D5B39FD /* PermissionHandler+Microphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PermissionHandler+Microphone.swift"; sourceTree = ""; }; 5D44CEC91693B4B900472C1C /* Survey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Survey.swift; sourceTree = ""; }; 5DD4E7007670C369DD8FF5D9 /* Date+IsWithinAnHourBeforeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+IsWithinAnHourBeforeTests.swift"; sourceTree = ""; }; 5EB5A772C1F2ECE6D0E0BD69 /* PaywallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallState.swift; sourceTree = ""; }; @@ -754,6 +770,7 @@ 719FE7C289CB0A621595A2A4 /* MockSkProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSkProduct.swift; sourceTree = ""; }; 71A62CA55C012D480DF37427 /* SK2StoreProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SK2StoreProduct.swift; sourceTree = ""; }; 71CF4BE5D2A6CEA9F8993C81 /* AudienceLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceLogic.swift; sourceTree = ""; }; + 72D9881BEA1D2A40DFC28D61 /* TestProducts.storekit */ = {isa = PBXFileReference; path = TestProducts.storekit; sourceTree = ""; }; 7318EF33D7374DC5C8B4549D /* SuperwallDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuperwallDelegate.swift; sourceTree = ""; }; 731F01C2EA1AC1F06AC1499D /* WaitForSubsStatusAndConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitForSubsStatusAndConfig.swift; sourceTree = ""; }; 73BE8AD685B39ACAB331109C /* PurchaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = ""; }; @@ -777,6 +794,7 @@ 7B921746BEC8F63DDB65C634 /* LimitedQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LimitedQueue.swift; sourceTree = ""; }; 7E27997BBCEAC330E4FB3718 /* pt_BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt_BR; path = pt_BR.lproj/Localizable.strings; sourceTree = ""; }; 7FCE6A59348C9018F40D7AC5 /* LogScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogScope.swift; sourceTree = ""; }; + 7FE43B98D847BB6DE291F0B4 /* FakeTrackingAuthorizationStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTrackingAuthorizationStatusTests.swift; sourceTree = ""; }; 8012E350CCE22B0D892E0F96 /* PaywallManagerLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallManagerLogic.swift; sourceTree = ""; }; 80281364A23252E30DEEA1AA /* Foundation+ASN1Coder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+ASN1Coder.swift"; sourceTree = ""; }; 8087A8CD8F4FC7C745010428 /* ASN1Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASN1Types.swift; sourceTree = ""; }; @@ -871,6 +889,7 @@ A524F7AAE90E48C3B8D7E99A /* PurchaseResult+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PurchaseResult+Internal.swift"; sourceTree = ""; }; A5C4AD6349F2D432132F36D5 /* MockSubscriptionPeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSubscriptionPeriod.swift; sourceTree = ""; }; A6B47DD5F59411CC529CD2DB /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; + A78C5C57C3C92444EBAC2E38 /* TrackingManagerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingManagerProxy.swift; sourceTree = ""; }; A79E9DBDDA7FEE63C15FBEAF /* CELEvaluatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CELEvaluatorTests.swift; sourceTree = ""; }; A7C3FC26DCBF604D7319FBB2 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/Localizable.strings; sourceTree = ""; }; A7D0B6F781D32B2DCDBE689C /* PermissionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionHandling.swift; sourceTree = ""; }; @@ -881,6 +900,7 @@ AB3DC90549C10EDA52D96FF2 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; AB3F04AC933701EE33F5F325 /* IdentityLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityLogic.swift; sourceTree = ""; }; ABC1253C6D5DD8D967BE05D1 /* LocalizationGrouping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationGrouping.swift; sourceTree = ""; }; + ABD045A5C4A47B1CA9365285 /* MicrophonePermissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrophonePermissionTests.swift; sourceTree = ""; }; AC11F0D5B6B8A0F5EC5D200B /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; AC74F16DC5A17489E97061EA /* PushTransitionLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTransitionLogic.swift; sourceTree = ""; }; AD23BBF9EA198ED8798A8F62 /* SystemInfo+NotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemInfo+NotificationName.swift"; sourceTree = ""; }; @@ -925,6 +945,7 @@ BBB580546C647ED707C43FEC /* PaywallArchiveManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallArchiveManager.swift; sourceTree = ""; }; BC211BFF9364E4B10418695B /* PaywallPresentationRequestStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallPresentationRequestStatus.swift; sourceTree = ""; }; BC8730F0D05BFDFA12AA6309 /* ProductVariable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariable.swift; sourceTree = ""; }; + BC8E506778E9750512E9F9D3 /* TrackingManagerProxyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingManagerProxyTests.swift; sourceTree = ""; }; BCC728E79E36A4CDD87F3078 /* WebArchiveFileSytemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebArchiveFileSytemManager.swift; sourceTree = ""; }; BD47D43BBBB5026D0913F5D8 /* MockSuperwallDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSuperwallDelegate.swift; sourceTree = ""; }; BD6BA222CB2EAA4B65F362C5 /* ProductsFetcherSK1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsFetcherSK1.swift; sourceTree = ""; }; @@ -994,6 +1015,7 @@ D5E2D026C30691F11D4E839F /* SurveyShowCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyShowCondition.swift; sourceTree = ""; }; D6340ACDA40937ACAC66FA3D /* EntitlementPriorityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntitlementPriorityTests.swift; sourceTree = ""; }; D69BCC259F5FBE15AB02D662 /* PermissionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionHandler.swift; sourceTree = ""; }; + D6F38AD737B1CFECEB2AAF85 /* FakeTrackingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTrackingManager.swift; sourceTree = ""; }; D7E232690489360042465DB2 /* Redeemable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redeemable.swift; sourceTree = ""; }; D81D656CEA8B5B86458038D4 /* ms */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ms; path = ms.lproj/Localizable.strings; sourceTree = ""; }; D86C79B54278BF17FB1117E1 /* AudienceAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudienceAttributes.swift; sourceTree = ""; }; @@ -1240,6 +1262,17 @@ path = Operators; sourceTree = ""; }; + 159A87C9E66CFD7D70842652 /* Tracking */ = { + isa = PBXGroup; + children = ( + 019FA4010BA11D24C68B8544 /* FakeTrackingAuthorizationStatus.swift */, + D6F38AD737B1CFECEB2AAF85 /* FakeTrackingManager.swift */, + 20DA18E503D82C213BC5567B /* PermissionsHandler+Tracking.swift */, + A78C5C57C3C92444EBAC2E38 /* TrackingManagerProxy.swift */, + ); + path = Tracking; + sourceTree = ""; + }; 167BF9144FB641160A5BAAF8 /* App Session */ = { isa = PBXGroup; children = ( @@ -1332,9 +1365,11 @@ 22C5DE2B5BC6FC31BF31137D /* Permissions */ = { isa = PBXGroup; children = ( + ABD045A5C4A47B1CA9365285 /* MicrophonePermissionTests.swift */, 988E0E3F8D992744C9AC196F /* PermissionStatusTests.swift */, DFD2580D6C95C96CC3051BCB /* PermissionTypeTests.swift */, 8E656889D53C36053C472FF2 /* Location */, + 267D9A6611768B7308E16E21 /* Tracking */, ); path = Permissions; sourceTree = ""; @@ -1360,6 +1395,16 @@ path = Web2App; sourceTree = ""; }; + 267D9A6611768B7308E16E21 /* Tracking */ = { + isa = PBXGroup; + children = ( + 7FE43B98D847BB6DE291F0B4 /* FakeTrackingAuthorizationStatusTests.swift */, + 1CC92F1146FA9FA76AF25227 /* TrackingAuthorizationStatusConversionTests.swift */, + BC8E506778E9750512E9F9D3 /* TrackingManagerProxyTests.swift */, + ); + path = Tracking; + sourceTree = ""; + }; 29AF8407DB4560B41050C02F /* Web View */ = { isa = PBXGroup; children = ( @@ -1461,10 +1506,12 @@ isa = PBXGroup; children = ( 21903EACCA9AA13BB10917EC /* PermissionHandler+Camera.swift */, + 5CFE48C68EEF60E63D5B39FD /* PermissionHandler+Microphone.swift */, 405C59153A88E6B9D664585A /* PermissionHandler+Notification.swift */, BB80534B838976548EFB8562 /* PermissionHandler+Photos.swift */, 96687FD52B8221BB9EB0056D /* Contacts */, 4642DD08A5F9649FD8B6BD3B /* Location */, + 159A87C9E66CFD7D70842652 /* Tracking */, ); path = Handlers; sourceTree = ""; @@ -2160,6 +2207,8 @@ isa = PBXGroup; children = ( D6340ACDA40937ACAC66FA3D /* EntitlementPriorityTests.swift */, + 2CF1F5EAC9C4E384EBBE5EA9 /* SubscriptionPeriodPriceTests.swift */, + 72D9881BEA1D2A40DFC28D61 /* TestProducts.storekit */, ); path = StoreProduct; sourceTree = ""; @@ -2886,6 +2935,7 @@ buildConfigurationList = 6B73BBA9C6704396C520122C /* Build configuration list for PBXNativeTarget "SuperwallKitTests" */; buildPhases = ( 192F99F19B9A695FBB35413C /* Sources */, + C078A941F1290ABD37725B66 /* Resources */, F7DEB224D8252B17F8B039A7 /* Frameworks */, 805EB75F8FA94C6572870498 /* Embed Frameworks */, ); @@ -2984,6 +3034,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C078A941F1290ABD37725B66 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A4C14BE9296BD05400C96F82 /* TestProducts.storekit in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -3025,6 +3083,7 @@ 158EBAAC2A96F73B93C48E15 /* ExpressionEvaluatorMock.swift in Sources */, 77ED3D92C176CC6D93D625B2 /* ExpressionLogicTests.swift in Sources */, CE43399599386456DCCD1FDC /* FakeLocationAuthorizationStatusTests.swift in Sources */, + 1F9AE04458B942D070FC4832 /* FakeTrackingAuthorizationStatusTests.swift in Sources */, AC7D527612F631AAADC7D225 /* FileManagerMigratorTests.swift in Sources */, D4B5A9708204AB6D58904733 /* GetPaywallVcOperatorTests.swift in Sources */, 64B962A8B59ACE514B0E48AE /* GetPresenterOperatorTests.swift in Sources */, @@ -3038,6 +3097,7 @@ 2BEA633247B24641D45A808C /* LocationManagerProxyTests.swift in Sources */, 40314E44991DCB66B4572C25 /* LocationPermissionDelegateTests.swift in Sources */, 4DE01655FC4CC148DD3D161C /* LoggerMock.swift in Sources */, + A9B924A1211117378743A534 /* MicrophonePermissionTests.swift in Sources */, B294572426111EC04F225289 /* MockExternalPurchaseControllerFactory.swift in Sources */, BA957415E2E1A38A25550B99 /* MockIntroductoryPeriod.swift in Sources */, 2ACC44D24F97934B1CBFC2B3 /* MockPurchaseController.swift in Sources */, @@ -3079,13 +3139,16 @@ 713A1F9D9861C6A1E5EB9174 /* StorageTests.swift in Sources */, 701B1B586B6C1E3F0B3AF560 /* StoreKitManagerTests.swift in Sources */, F5F8C2E02A057DA15C2936AB /* StorePresentationObjectsOperatorTests.swift in Sources */, + 097719E21BBD153BA6FD6785 /* SubscriptionPeriodPriceTests.swift in Sources */, 89CC491C60F7CD12D3E73284 /* SurveyManagerTests.swift in Sources */, 252D37DDAA2C97A6E2DDD6B7 /* SurveyTests.swift in Sources */, 18D39CB7BCF324B44197735D /* TaskRetryingTests.swift in Sources */, 14D8827BA663A0EFD64DEC2C /* TemplateLogicTests.swift in Sources */, 7D47BABD89CE33CDD78DFCC6 /* TestFileManager.swift in Sources */, 4AA4E2CE223DC7CF1678E83C /* TrackTests.swift in Sources */, + 0A35D3D8CCC8D043628A0A4F /* TrackingAuthorizationStatusConversionTests.swift in Sources */, 9304297F3B76DB512F2F9D53 /* TrackingLogicTests.swift in Sources */, + BCD5EA74E59F7BC43B0816C5 /* TrackingManagerProxyTests.swift in Sources */, 744F0D34C800E17CF8462820 /* URLSessionRetryLogicTests.swift in Sources */, 8B200F99B71D706D45948339 /* Utils.swift in Sources */, 6C98C5DAAC3F493511A57AC3 /* WaitForEntitlementsAndConfigTests.swift in Sources */, @@ -3205,6 +3268,8 @@ BF47C2693495CB8C9DAD7C8D /* FakeContactsStore.swift in Sources */, 11798EDE58E5D225E5414F2E /* FakeLocationAuthorizationStatus.swift in Sources */, BD6ABB9DB883BC62D2407392 /* FakeLocationManager.swift in Sources */, + E9D95044254D79D2439D7B3E /* FakeTrackingAuthorizationStatus.swift in Sources */, + B0F7F66E24C7AEDE460F34FE /* FakeTrackingManager.swift in Sources */, E0F3648081AB86077201EB5D /* FeatureFlags.swift in Sources */, ED1C693657DA7FCBAE2DDDC6 /* FeatureGatingBehaviour.swift in Sources */, 767974DF68CE67AE2066E3D6 /* FileManagerMigrator.swift in Sources */, @@ -3311,6 +3376,7 @@ 3824D48F8E0AF35EEBED8FF4 /* PaywallViewControllerWrapper.swift in Sources */, BF9ADF5761C34F584EC416B1 /* PaywallWebEvent.swift in Sources */, E4A66E2235F7FE594EA82A06 /* PermissionHandler+Camera.swift in Sources */, + 1D27BDC498FDFA24FFFD49EB /* PermissionHandler+Microphone.swift in Sources */, 746FCA3A2499F7BAF653F205 /* PermissionHandler+Notification.swift in Sources */, B5647878BFFEBFCC0D009E12 /* PermissionHandler+Photos.swift in Sources */, FC7A0F08E317F745C3C46E51 /* PermissionHandler.swift in Sources */, @@ -3319,6 +3385,7 @@ 774699D4CCD6952F124FE333 /* PermissionType.swift in Sources */, 2D3950C4F1623B062FF3107D /* PermissionsHandler+Contacts.swift in Sources */, F2124CF2BE35ABD284EDCC45 /* PermissionsHandler+Location.swift in Sources */, + A7D83FE18FC0A70BCD5A3B2C /* PermissionsHandler+Tracking.swift in Sources */, B49470D3579E0050818348A7 /* PlacementsQueue.swift in Sources */, FA677CF601A228D5B485FFDE /* PopupTransition.swift in Sources */, 11719638C88CFCA506264531 /* PopupTransitionDelegate.swift in Sources */, @@ -3450,6 +3517,7 @@ 78113F737BA99A2848850904 /* TrackableSuperwallEvent.swift in Sources */, BDECE549960DB9A5662939BE /* Tracking.swift in Sources */, E3F2F347326B8D206D341FB6 /* TrackingLogic.swift in Sources */, + CE821667CB6676EA02510FE9 /* TrackingManagerProxy.swift in Sources */, 369677E9A6E8754CFD20714D /* TrackingParameters.swift in Sources */, D0E19F665C7B230BF3FA122D /* TrackingResult.swift in Sources */, 8ACC4731031DA94C709915CF /* Transaction+LatestSince.swift in Sources */, diff --git a/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SubscriptionPeriodPriceTests.swift b/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SubscriptionPeriodPriceTests.swift new file mode 100644 index 0000000000..f31f0c7eaa --- /dev/null +++ b/Tests/SuperwallKitTests/StoreKit/Products/StoreProduct/SubscriptionPeriodPriceTests.swift @@ -0,0 +1,204 @@ +// +// SubscriptionPeriodPriceTests.swift +// SuperwallKitTests +// +// Created by Claude on 2026-01-16. +// +// swiftlint:disable all + +import Foundation +import Testing +@testable import SuperwallKit + +/// Tests for SubscriptionPeriod price calculation methods. +/// These methods calculate daily, weekly, monthly, and yearly prices +/// from a total subscription price based on the subscription period. +@Suite("Subscription Period Price Calculations") +struct SubscriptionPeriodPriceTests { + + // MARK: - Daily Price Tests + + @Test("Daily price for yearly subscription") + func testDailyPriceForYearlySubscription() { + // $99.99/year should be ~$0.27/day (99.99 / 365) + let period = SubscriptionPeriod(value: 1, unit: .year) + let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(99.99)) + + // 99.99 / 365 = 0.273... rounds down to 0.27 + #expect(dailyPrice == Decimal(string: "0.27")) + } + + @Test("Daily price for monthly subscription") + func testDailyPriceForMonthlySubscription() { + // $9.99/month should be ~$0.33/day (9.99 / 30) + let period = SubscriptionPeriod(value: 1, unit: .month) + let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(9.99)) + + // 9.99 / 30 = 0.333 rounds down to 0.33 + #expect(dailyPrice == Decimal(string: "0.33")) + } + + @Test("Daily price for weekly subscription") + func testDailyPriceForWeeklySubscription() { + // $4.99/week should be ~$0.71/day (4.99 / 7) + let period = SubscriptionPeriod(value: 1, unit: .week) + let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(4.99)) + + // 4.99 / 7 = 0.712... rounds down to 0.71 + #expect(dailyPrice == Decimal(string: "0.71")) + } + + @Test("Daily price for daily subscription") + func testDailyPriceForDailySubscription() { + // $0.99/day should be $0.99/day + let period = SubscriptionPeriod(value: 1, unit: .day) + let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(0.99)) + + #expect(dailyPrice == Decimal(string: "0.99")) + } + + @Test("Daily price for multi-month subscription") + func testDailyPriceForThreeMonthSubscription() { + // $24.99/3 months should be ~$0.27/day (24.99 / 90) + let period = SubscriptionPeriod(value: 3, unit: .month) + let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(24.99)) + + // 24.99 / 90 = 0.2776... rounds down to 0.27 + #expect(dailyPrice == Decimal(string: "0.27")) + } + + // MARK: - Weekly Price Tests + + @Test("Weekly price for yearly subscription") + func testWeeklyPriceForYearlySubscription() { + // $99.99/year should be ~$1.92/week (99.99 / 52) + let period = SubscriptionPeriod(value: 1, unit: .year) + let weeklyPrice = period.pricePerWeek(withTotalPrice: Decimal(99.99)) + + // 99.99 / 52 = 1.923... rounds down to 1.92 + #expect(weeklyPrice == Decimal(string: "1.92")) + } + + @Test("Weekly price for monthly subscription") + func testWeeklyPriceForMonthlySubscription() { + // $9.99/month should be ~$2.49/week (9.99 / 4) + let period = SubscriptionPeriod(value: 1, unit: .month) + let weeklyPrice = period.pricePerWeek(withTotalPrice: Decimal(9.99)) + + // 9.99 / 4 = 2.4975 rounds down to 2.49 + #expect(weeklyPrice == Decimal(string: "2.49")) + } + + @Test("Weekly price for weekly subscription") + func testWeeklyPriceForWeeklySubscription() { + // $4.99/week should be $4.99/week + let period = SubscriptionPeriod(value: 1, unit: .week) + let weeklyPrice = period.pricePerWeek(withTotalPrice: Decimal(4.99)) + + #expect(weeklyPrice == Decimal(string: "4.99")) + } + + // MARK: - Monthly Price Tests + + @Test("Monthly price for yearly subscription") + func testMonthlyPriceForYearlySubscription() { + // $99.99/year should be ~$8.33/month (99.99 / 12) + let period = SubscriptionPeriod(value: 1, unit: .year) + let monthlyPrice = period.pricePerMonth(withTotalPrice: Decimal(99.99)) + + // 99.99 / 12 = 8.3325 rounds down to 8.33 + #expect(monthlyPrice == Decimal(string: "8.33")) + } + + @Test("Monthly price for monthly subscription") + func testMonthlyPriceForMonthlySubscription() { + // $9.99/month should be $9.99/month + let period = SubscriptionPeriod(value: 1, unit: .month) + let monthlyPrice = period.pricePerMonth(withTotalPrice: Decimal(9.99)) + + #expect(monthlyPrice == Decimal(string: "9.99")) + } + + @Test("Monthly price for 6-month subscription") + func testMonthlyPriceForSixMonthSubscription() { + // $49.99/6 months should be ~$8.33/month (49.99 / 6) + let period = SubscriptionPeriod(value: 6, unit: .month) + let monthlyPrice = period.pricePerMonth(withTotalPrice: Decimal(49.99)) + + // 49.99 / 6 = 8.331... rounds down to 8.33 + #expect(monthlyPrice == Decimal(string: "8.33")) + } + + // MARK: - Yearly Price Tests + + @Test("Yearly price for yearly subscription") + func testYearlyPriceForYearlySubscription() { + // $99.99/year should be $99.99/year + let period = SubscriptionPeriod(value: 1, unit: .year) + let yearlyPrice = period.pricePerYear(withTotalPrice: Decimal(99.99)) + + #expect(yearlyPrice == Decimal(string: "99.99")) + } + + @Test("Yearly price for monthly subscription") + func testYearlyPriceForMonthlySubscription() { + // $9.99/month should be ~$119.88/year (9.99 * 12) + let period = SubscriptionPeriod(value: 1, unit: .month) + let yearlyPrice = period.pricePerYear(withTotalPrice: Decimal(9.99)) + + // 9.99 / (1/12) = 9.99 * 12 = 119.88 + #expect(yearlyPrice == Decimal(string: "119.88")) + } + + @Test("Yearly price for weekly subscription") + func testYearlyPriceForWeeklySubscription() { + // $4.99/week should be ~$259.48/year (4.99 * 52) + let period = SubscriptionPeriod(value: 1, unit: .week) + let yearlyPrice = period.pricePerYear(withTotalPrice: Decimal(4.99)) + + // 4.99 / (1/52) = 4.99 * 52 = 259.48 + #expect(yearlyPrice == Decimal(string: "259.48")) + } + + // MARK: - Edge Cases + + @Test("Zero price returns zero") + func testZeroPriceReturnsZero() { + let period = SubscriptionPeriod(value: 1, unit: .year) + + #expect(period.pricePerDay(withTotalPrice: Decimal(0)) == Decimal(0)) + #expect(period.pricePerWeek(withTotalPrice: Decimal(0)) == Decimal(0)) + #expect(period.pricePerMonth(withTotalPrice: Decimal(0)) == Decimal(0)) + #expect(period.pricePerYear(withTotalPrice: Decimal(0)) == Decimal(0)) + } + + @Test("Multi-year subscription calculates correctly") + func testMultiYearSubscription() { + // $199.99/2 years should be ~$8.33/month (199.99 / 24) + let period = SubscriptionPeriod(value: 2, unit: .year) + let monthlyPrice = period.pricePerMonth(withTotalPrice: Decimal(199.99)) + + // 199.99 / 24 = 8.332... rounds down to 8.33 + #expect(monthlyPrice == Decimal(string: "8.33")) + } + + @Test("Large price handles correctly") + func testLargePriceHandlesCorrectly() { + // $999.99/year + let period = SubscriptionPeriod(value: 1, unit: .year) + let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(999.99)) + + // 999.99 / 365 = 2.739... rounds down to 2.73 + #expect(dailyPrice == Decimal(string: "2.73")) + } + + @Test("Small price rounds down correctly") + func testSmallPriceRoundsDownCorrectly() { + // $0.99/year should be ~$0.00/day (rounds down) + let period = SubscriptionPeriod(value: 1, unit: .year) + let dailyPrice = period.pricePerDay(withTotalPrice: Decimal(0.99)) + + // 0.99 / 365 = 0.00271... rounds down to 0.00 + #expect(dailyPrice == Decimal(string: "0.00")) + } +}