Chore: Signal purchases-ui on purchases configuration (#7407)

* Send signal from purchases to purchases-ui and retain symbol

* make more Generic

* update comment

* get rid of debug test only surface in the ui service

* remove noisy comment

* remove purchases.IsConfigured check

* symbol stripping test

* revert package resolved

* Update Dangerfile

* Update PurchasesUIServiceTests.swift

* Update Sources/Purchasing/Purchases/PurchasesConfiguredNotifier.swift

Co-authored-by: Álvaro Brey <alvaro.brey@revenuecat.com>

* Update RevenueCatUI/Purchasing/PurchasesUIService.swift

Co-authored-by: Álvaro Brey <alvaro.brey@revenuecat.com>

---------

Co-authored-by: Álvaro Brey <alvaro.brey@revenuecat.com>
This commit is contained in:
Jacob RakidzichandÁlvaro Brey authored and GitHub committed 2026-08-20 12:33:23 +00:00
1 parent 6d7cd08bcc
commit 687d1185ed
17 files changed
+470 -5

No files matched your search

+8
View File
@@ -755,6 +755,14 @@ jobs:
name: SPM RevenueCatUI Release Build (Xcode 16)
command: swift build -c release --target RevenueCatUI
no_output_timeout: 5m
- run:
name: PurchasesUIService stripped-symbol integration tests
command: bundle exec fastlane test_purchases_ui_service_stripped
no_output_timeout: 15m
environment:
DEVICE: iPhone 16,OS=18.5
- store_test_results:
path: fastlane/test_output/purchases_ui_service_stripped/tests.xml
# === RevenueCatUI iOS 18 tests ===
- run:
+1
View File
@@ -44,6 +44,7 @@ custom_rules:
- Tests/UnitTests/RulesEngine
- Tests/v3LoadShedderIntegration
- Tests/v4LoadShedderIntegration
- Tests/PurchasesUIServiceIntegrationTests
regex: "\\: XCTestCase \\{"
name: "XCTestCase Superclass"
message: "Test classes must inherit `TestCase` instead."
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RevenueCatUI"
BuildableName = "RevenueCatUI"
BlueprintName = "RevenueCatUI"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PurchasesUIServiceIntegrationTests"
BuildableName = "PurchasesUIServiceIntegrationTests"
BlueprintName = "PurchasesUIServiceIntegrationTests"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PurchasesUIServiceIntegrationTests"
BuildableName = "PurchasesUIServiceIntegrationTests"
BlueprintName = "PurchasesUIServiceIntegrationTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "RevenueCatUI"
BuildableName = "RevenueCatUI"
BlueprintName = "RevenueCatUI"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Release">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+1
View File
@@ -35,6 +35,7 @@ tuist generate PurchaseTester
tuist generate UnitTests
tuist generate RevenueCatUITests
tuist generate PurchasesUIServiceIntegrationTests
tuist generate StoreKitUnitTests
tuist generate BackendIntegrationTests
+3 -1
View File
@@ -8,7 +8,9 @@ EXCLUDED_SWIFT_PATH_PREFIXES = [
# Tuist-only: these targets exist solely in Projects/PaywallFixtures/Project.swift and are
# deliberately absent from RevenueCat.xcodeproj.
'Tests/TestingApps/PaywallFixtures/',
'Tests/TestingApps/PaywallFixturesUITests/'
'Tests/TestingApps/PaywallFixturesUITests/',
# SPM / Tuist-only: Release-stripped integration tests are not in RevenueCat.xcodeproj.
'Tests/PurchasesUIServiceIntegrationTests/'
].freeze
COMMENT_MARKER = "<!-- purchases-ios-danger -->"
+13 -1
View File
@@ -143,6 +143,18 @@ let package = Package(
.copy("Resources/header.heic"),
.copy("Resources/background.heic"),
.copy("PaywallsV2/__PreviewResources__")
])
]),
// Isolated from RevenueCatUITests so a Swift reference to PurchasesUIService
// cannot keep the ObjC class alive. Run via the RevenueCatUI-Stripped scheme.
.testTarget(
name: "PurchasesUIServiceIntegrationTests",
dependencies: [
"RevenueCat",
"RevenueCatUI"
],
linkerSettings: [
.unsafeFlags(["-dead_strip"], .when(configuration: .release))
]
)
]
)
+10
View File
@@ -141,6 +141,8 @@
16DA8F1E2E4FB6E200283940 /* ImageComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1B2E4FB6E200283940 /* ImageComponent.json */; };
16DA8F1F2E4FB6E200283940 /* VideoComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1C2E4FB6E200283940 /* VideoComponent.json */; };
16E146AD2E99F3480089B609 /* TransactionNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E146AB2E99F1E20089B609 /* TransactionNotifications.swift */; };
16E147A22F3B4C010089B609 /* PurchasesConfiguredNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E147A12F3B4C010089B609 /* PurchasesConfiguredNotifier.swift */; };
16E147A42F3B4C010089B609 /* PurchasesUIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E147A32F3B4C010089B609 /* PurchasesUIService.swift */; };
16E146B22E99FC7E0089B609 /* PurchaseResultComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E146B12E99FC7E0089B609 /* PurchaseResultComparator.swift */; };
16E2B7F12EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E2B7F02EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift */; };
16E4A3C89791426AAD027A49 /* PlatformInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043FB8686FEB4A31B9CF48C8 /* PlatformInfoTests.swift */; };
@@ -1758,6 +1760,9 @@
16DA8F1B2E4FB6E200283940 /* ImageComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ImageComponent.json; sourceTree = "<group>"; };
16DA8F1C2E4FB6E200283940 /* VideoComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = VideoComponent.json; sourceTree = "<group>"; };
16E146AB2E99F1E20089B609 /* TransactionNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionNotifications.swift; sourceTree = "<group>"; };
16E147A12F3B4C010089B609 /* PurchasesConfiguredNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesConfiguredNotifier.swift; sourceTree = "<group>"; };
16E147A32F3B4C010089B609 /* PurchasesUIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesUIService.swift; sourceTree = "<group>"; };
16E147A52F3B4C010089B609 /* PurchasesUIServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesUIServiceTests.swift; sourceTree = "<group>"; };
16E146B12E99FC7E0089B609 /* PurchaseResultComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseResultComparator.swift; sourceTree = "<group>"; };
16E146B42E99FF640089B609 /* MockStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreTransaction.swift; sourceTree = "<group>"; };
16E2B7F02EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedLargeItemCacheTests.swift; sourceTree = "<group>"; };
@@ -5965,6 +5970,7 @@
887A5FED2C1D037000E1A461 /* MockPurchases.swift */,
887A5FEE2C1D037000E1A461 /* PaywallPurchasesType.swift */,
887A5FEF2C1D037000E1A461 /* PurchaseHandler.swift */,
16E147A32F3B4C010089B609 /* PurchasesUIService.swift */,
887A5FF02C1D037000E1A461 /* PurchaseHandler+TestData.swift */,
F598F832810DB534092FBED3 /* WorkflowContext.swift */,
9FA3AA2BC4C1B8CE7E9A3B59 /* WorkflowPreview.swift */,
@@ -6162,6 +6168,7 @@
57F4B2C12F8E10A600BB46C9 /* ExitOfferHelperTests.swift */,
16E146B42E99FF640089B609 /* MockStoreTransaction.swift */,
887A61372C1D168B00E1A461 /* PurchaseHandlerTests.swift */,
16E147A52F3B4C010089B609 /* PurchasesUIServiceTests.swift */,
45010544C32C2A21A2F323D7 /* OnWebCheckoutOpenedModifierTests.swift */,
4B7C1E3A5D2F41908A6C0D11 /* OnURLOpenedModifierTests.swift */,
B10A136C03A9FF34FB805EFA /* ExitOfferPresenterTests.swift */,
@@ -6669,6 +6676,7 @@
4F8038322A1EA7C300D21039 /* TransactionPoster.swift */,
757E73FF2F1700000066DCDC /* TransactionMetadataSyncHelper.swift */,
16E146AB2E99F1E20089B609 /* TransactionNotifications.swift */,
16E147A12F3B4C010089B609 /* PurchasesConfiguredNotifier.swift */,
757E73CC2F0FE2410066DCDC /* LocalTransactionMetadata.swift */,
757E73E32F101EAF0066DCDC /* LocalTransactionMetadataStore.swift */,
);
@@ -7935,6 +7943,7 @@
03E37BEA2D30B32200CD9678 /* PaywallTabsComponent.swift in Sources */,
FD20467F2CB82F2000166727 /* StoreKit2PurchaseIntentListener.swift in Sources */,
16E146AD2E99F3480089B609 /* TransactionNotifications.swift in Sources */,
16E147A22F3B4C010089B609 /* PurchasesConfiguredNotifier.swift in Sources */,
B34605BD279A6E380031CA74 /* CallbackCacheStatus.swift in Sources */,
42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */,
805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */,
@@ -8517,6 +8526,7 @@
887A60BE2C1D037000E1A461 /* PaywallFooterViewController.swift in Sources */,
DBD2A168300E81E20019DB7F /* VideoAudioSessionHandler.swift in Sources */,
887A608A2C1D037000E1A461 /* PurchaseHandler.swift in Sources */,
16E147A42F3B4C010089B609 /* PurchasesUIService.swift in Sources */,
2D2AFE8D2C6A834D00D1B0B4 /* TestData.swift in Sources */,
03A98D382D2AC63B009BCA61 /* UIConfigProvider.swift in Sources */,
887A60C92C1D037000E1A461 /* PurchaseButton.swift in Sources */,
@@ -93,6 +93,7 @@ public struct CustomerCenterView: View {
self._viewModel = .init(wrappedValue: CustomerCenterViewModel(actionWrapper: actionWrapper))
self.mode = mode
self.navigationOptions = navigationOptions
PurchasesUIService.activateIfNeeded()
}
// swiftlint:disable:next missing_docs
@@ -50,6 +50,8 @@ struct PaywallViewConfiguration {
self.introEligibility = introEligibility
self.purchaseHandler = purchaseHandler
self.promoOfferCache = promoOfferCache
PurchasesUIService.activateIfNeeded()
}
}
+3
View File
@@ -136,6 +136,7 @@ enum Strings {
case workflow_package_context_unresolvable(stepId: String)
case workflow_fetch_failed_falling_back_to_offerings_paywall(offeringIdentifier: String, error: Error)
case restored_paywall_components_for_disabled_remote_config(offeringIdentifier: String)
case purchases_did_configure
}
@@ -440,6 +441,8 @@ extension Strings: CustomStringConvertible {
case let .restored_paywall_components_for_disabled_remote_config(offeringIdentifier):
return "Remote config is disabled, so offering '\(offeringIdentifier)' was re-resolved to restore " +
"its offerings-provided paywall."
case .purchases_did_configure:
return "Purchases notified purchases-ui of configuration"
}
}
@@ -0,0 +1,60 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PurchasesUIService.swift
//
import Foundation
@_spi(Internal) import RevenueCat
/// Receives a message from `Purchases` after `Purchases.shared` is configured.
///
/// Looked up by ObjC name so RevenueCat does not need to import this module.
///
/// Reachable RevenueCatUI entry points call ``activateIfNeeded()`` so this class is
/// retained by a real, observable use of runtime state. Linking happens before
/// execution, so that call can run after ``Purchases/configure`` and still keep
/// the class in the binary. The same path replays if RevenueCatUI loads after
/// configuration.
///
/// The `RevenueCatUI-Stripped` scheme runs `PurchasesUIServiceIntegrationTests`
/// in Release with dead-code stripping to catch regressions.
@objc(RCPurchasesUIService)
final class PurchasesUIService: NSObject, PurchasesPostConfigurationStep {
private static let lock = NSLock()
private static var didConfigure = false
/// Retains this class from a public RevenueCatUI entry point.
///
/// Reads runtime state and may mutate locked state, so it cannot be
/// optimized away as a no-op keep-alive.
static func activateIfNeeded() {
Self.purchasesDidConfigure()
}
/// Cheap wake-up. Must stay cheap: start subscriptions here later, do not perform heavy work synchronously here.
@objc static func purchasesDidConfigure() {
lock.withLock {
guard !didConfigure else { return }
self.didConfigure = true
Logger.debug(Strings.purchases_did_configure)
// Next PR: subscribe to publishers
}
}
#if DEBUG
static func resetForTests() {
lock.withLock {
Self.didConfigure = false
}
}
#endif
}
+10 -3
View File
@@ -1003,12 +1003,12 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
_ purchases: @autoclosure () -> Purchases,
dedupingAgainst configuration: Configuration? = nil
) -> Purchases {
return self.purchases.modify { currentInstance in
let (newInstance, didConfigure): (Purchases, Bool) = self.purchases.modify { currentInstance in
if let configuration,
let existingInstance = currentInstance,
existingInstance.currentConfiguration == configuration {
Logger.info(Strings.configure.instance_already_exists_with_same_config)
return existingInstance
return (existingInstance, false)
}
if currentInstance != nil {
@@ -1025,8 +1025,15 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
let newInstance = purchases()
currentInstance = newInstance
return newInstance
return (newInstance, true)
}
// Outside the singleton lock so RevenueCatUI can safely use ``Purchases/shared``.
if didConfigure {
PurchasesConfiguredNotifier.notify()
}
return newInstance
}
}
@@ -0,0 +1,40 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PurchasesConfiguredNotifier.swift
//
import Foundation
/// RevenueCatUI implements this so ``Purchases`` can invoke it without importing that module.
@_spi(Internal)
@objc public protocol PurchasesPostConfigurationStep: NSObjectProtocol {
/// Called after ``Purchases/shared`` has been set by ``Purchases/configure(withAPIKey:)``.
static func purchasesDidConfigure()
}
/// Sends a message to listeners when ``Purchases`` is configured.
enum PurchasesConfiguredNotifier {
// OBJC symbols must be linked for them to receive the notification
static let serviceClassNames = ["RCPurchasesUIService"]
static func notify() {
for name in serviceClassNames {
guard let service = NSClassFromString(name) as? PurchasesPostConfigurationStep.Type else {
continue
}
service.purchasesDidConfigure()
}
}
}
@@ -0,0 +1,3 @@
disabled_rules:
- missing_docs
- force_unwrapping
@@ -0,0 +1,83 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PurchasesUIServiceStrippedTests.swift
//
@_spi(Internal) import RevenueCat
import RevenueCatUI
import XCTest
/// Verifies `RCPurchasesUIService` survives Release dead-stripping / symbol stripping.
///
/// Run with the `RevenueCatUI-Stripped` scheme (Release). This target must not
/// mention `PurchasesUIService` by its Swift name: a Swift reference would keep
/// the class even if the ObjC keep-alive path failed, hiding the production bug.
///
/// `Purchases` looks the class up with `NSClassFromString("RCPurchasesUIService")`.
/// SPM links RevenueCatUI statically, so the class is only in the binary when a
/// public RevenueCatUI entry point is reachable.
///
/// Release modules are built without `-enable-testing`, so this file uses public
/// / SPI APIs only — no `@testable import`.
final class PurchasesUIServiceStrippedTests: XCTestCase {
func testServiceSurvivesSymbolStrippingAndIsDiscoverable() throws {
try skipIfNoPublicKeepAlive()
self.retainPublicRevenueCatUIEntryPoints()
// Must match `PurchasesConfiguredNotifier.serviceClassNames`.
let className = "RCPurchasesUIService"
let service = NSClassFromString(className)
XCTAssertNotNil(
service,
"\(className) was stripped from the binary. Purchases.configure cannot notify RevenueCatUI."
)
XCTAssertNotNil(
service as? PurchasesPostConfigurationStep.Type,
"\(className) is present but does not conform to PurchasesPostConfigurationStep."
)
_ = Purchases.configure(withAPIKey: "test_purchases_ui_service_stripped")
XCTAssertNotNil(
NSClassFromString(className) as? PurchasesPostConfigurationStep.Type,
"\(className) must still be discoverable after Purchases.configure."
)
}
/// Touches the same public types an app would. That keeps `RCPurchasesUIService`
/// through `activateIfNeeded()` without naming the class in this test target.
private func retainPublicRevenueCatUIEntryPoints() {
#if os(tvOS)
return
#else
if #available(iOS 15.0, macOS 12.0, watchOS 8.0, *) {
_ = PaywallView()
#if os(iOS)
_ = CustomerCenterView()
#endif
}
#endif
}
private func skipIfNoPublicKeepAlive() throws {
#if os(tvOS)
throw XCTSkip("RevenueCatUI has no public keep-alive for RCPurchasesUIService on tvOS.")
#else
if #available(iOS 15.0, macOS 12.0, watchOS 8.0, *) {
return
}
throw XCTSkip("PaywallView requires iOS 15 / macOS 12 / watchOS 8.")
#endif
}
}
@@ -0,0 +1,120 @@
//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// PurchasesUIServiceTests.swift
//
import Nimble
@_spi(Internal) @testable import RevenueCat
@testable import RevenueCatUI
import XCTest
class PurchasesUIServiceTests: TestCase {
override func setUp() {
super.setUp()
Purchases.clearSingleton()
PurchasesUIService.resetForTests()
}
override func tearDown() {
Purchases.clearSingleton()
PurchasesUIService.resetForTests()
super.tearDown()
}
func testServiceConformsAndIsDiscoverable() {
// swiftlint:disable:next force_unwrapping
let service: AnyClass? = NSClassFromString(PurchasesConfiguredNotifier.serviceClassNames.first!)
expect(service).toNot(beNil())
expect(service as? PurchasesPostConfigurationStep.Type).toNot(beNil())
}
func testConfigurePingsRevenueCatUIOnce() {
_ = Purchases.configure(withAPIKey: "test_purchases_ui_service")
self.verifyPurchasesDidConfigureLogged(count: 1)
// same config: dedup must not ping again
_ = Purchases.configure(withAPIKey: "test_purchases_ui_service")
self.verifyPurchasesDidConfigureLogged(count: 1)
}
func testActivateIfNeededPingsEvenWhenPurchasesIsNotConfigured() {
// Keep-alive must actually run: a no-op would be stripped, and UI may
// load before ``Purchases/configure``.
PurchasesUIService.activateIfNeeded()
self.verifyPurchasesDidConfigureLogged(count: 1)
}
func testActivateIfNeededReplaysWhenPurchasesAlreadyConfigured() {
_ = Purchases.configure(withAPIKey: "test_purchases_ui_service")
self.verifyPurchasesDidConfigureLogged(count: 1)
// Simulate RevenueCatUI loading after configuration: the class is now
// present, but its once-only flag has not run yet.
PurchasesUIService.resetForTests()
self.logger.clearMessages()
PurchasesUIService.activateIfNeeded()
self.verifyPurchasesDidConfigureLogged(count: 1)
}
func testPaywallViewConfigurationActivateIfNeededWhenConfigured() throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()
_ = Purchases.configure(withAPIKey: "test_purchases_ui_service")
PurchasesUIService.resetForTests()
self.logger.clearMessages()
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
_ = PaywallViewConfiguration(purchaseHandler: .default())
}
self.verifyPurchasesDidConfigureLogged(count: 1)
}
#if os(iOS)
func testCustomerCenterViewActivateIfNeededWhenConfigured() throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()
_ = Purchases.configure(withAPIKey: "test_purchases_ui_service")
PurchasesUIService.resetForTests()
self.logger.clearMessages()
if #available(iOS 15.0, *) {
_ = CustomerCenterView()
}
self.verifyPurchasesDidConfigureLogged(count: 1)
}
#endif
}
private extension PurchasesUIServiceTests {
func verifyPurchasesDidConfigureLogged(count: Int) {
if count == 0 {
self.logger.verifyMessageWasNotLogged(
Strings.purchases_did_configure,
allowNoMessages: true
)
} else {
self.logger.verifyMessageWasLogged(
Strings.purchases_did_configure,
level: .debug,
expectedCount: count
)
}
}
}
+20
View File
@@ -954,6 +954,26 @@ platform :ios do
end
end
desc "Runs PurchasesUIService integration tests against a Release, symbol-stripped RevenueCatUI build"
lane :test_purchases_ui_service_stripped do
platform = ENV['PLATFORM'] || 'iOS Simulator'
destination = ENV['DEVICE'] || "iPhone 16,OS=18.5"
sdk = ENV['BUILD_SDK'] || 'iphonesimulator'
xcodebuild(
workspace: '.',
scheme: 'RevenueCatUI-Stripped',
destination: "platform=" + platform + ",name=" + destination,
sdk: sdk,
configuration: 'Release',
result_bundle_path: 'fastlane/test_output/purchases_ui_service_stripped.xcresult',
report_formats: [:junit],
report_path: 'fastlane/test_output/purchases_ui_service_stripped/tests.xml',
test: true,
xcargs: "DEAD_CODE_STRIPPING=YES STRIP_SWIFT_SYMBOLS=YES ONLY_ACTIVE_ARCH=YES"
)
end
desc "Release checks"
lane :release_checks do |options|
version_number = current_version_number