Skip to content

Commit

Permalink
Paywalls: events unit and integration tests
Browse files Browse the repository at this point in the history
- Added `PurchasesOrchestrator` tests for paywall data sent through post receipt
- Fixed and tested state issue with cached paywall data and failed purchases
- Added `Integration Tests` for tracking and flushing events
- Configured `Integration Tests` with a custom documents directory to ensure it's empty on every test invocation
- Changed deployment target on `Integration Tests` to iOS 16 to simplify code
- Setting `Purchases.logLevel` before configuring purchases on `Integration Tests`
- Added assertion to ensure `FileHandler` never runs on the main thread
  • Loading branch information
NachoSoto committed Sep 11, 2023
1 parent 7909a80 commit 42e2300
Show file tree
Hide file tree
Showing 14 changed files with 368 additions and 90 deletions.
8 changes: 4 additions & 4 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -4183,7 +4183,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/BackendIntegrationTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -4211,7 +4211,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/BackendIntegrationTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -4477,7 +4477,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/BackendIntegrationTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -4505,7 +4505,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = Tests/BackendIntegrationTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
6 changes: 6 additions & 0 deletions Sources/Diagnostics/FileHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ actor FileHandler: FileHandlerType {
/// - Note: this loads the entire file in memory
/// For newer versions, consider using `readLines` instead.
func readFile() throws -> Data {
RCTestAssertNotMainThread()

try self.moveToBeginningOfFile()

return self.fileHandle.availableData
Expand All @@ -64,13 +66,17 @@ actor FileHandler: FileHandlerType {
/// Returns an async sequence for every line in the file
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
func readLines() throws -> AsyncLineSequence<FileHandle.AsyncBytes> {
RCTestAssertNotMainThread()

try self.moveToBeginningOfFile()

return self.fileHandle.bytes.lines
}

/// Adds a line at the end of the file
func append(line: String) {
RCTestAssertNotMainThread()

self.fileHandle.seekToEndOfFile()
self.fileHandle.write(line.asData)
self.fileHandle.write(Self.lineBreakData)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Logging/Strings/PaywallsStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ enum PaywallsStrings {
case event_flush_already_in_progress
case event_flush_with_empty_store
case event_flush_starting(count: Int)
case event_flush_failed(BackendError)
case event_flush_failed(Error)

}

Expand Down Expand Up @@ -78,7 +78,7 @@ extension PaywallsStrings: LogMessage {
return "Paywall event flush: posting \(count) events."

case let .event_flush_failed(error):
return "Paywall event flushing failed, will retry. Error: \(error.localizedDescription)"
return "Paywall event flushing failed, will retry. Error: \((error as NSError).localizedDescription)"
}
}

Expand Down
7 changes: 5 additions & 2 deletions Sources/Networking/InternalAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ class InternalAPI {

extension InternalAPI {

/// - Throws: `BackendError`
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
func postPaywallEvents(events: [PaywallStoredEvent]) async -> BackendError? {
return await Async.call { completion in
func postPaywallEvents(events: [PaywallStoredEvent]) async throws {
let error = await Async.call { completion in
self.postPaywallEvents(events: events, completion: completion)
}

if let error { throw error }
}

}
12 changes: 10 additions & 2 deletions Sources/Paywalls/Events/PaywallEventStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,14 @@ internal actor PaywallEventStore: PaywallEventStoreType {
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
extension PaywallEventStore {

static func createDefault() throws -> PaywallEventStore {
let url = try Self.documentsDirectory
static func createDefault(documentsDirectory: URL?) throws -> PaywallEventStore {
let documentsDirectory = try documentsDirectory ?? Self.documentsDirectory
let url = documentsDirectory
.appendingPathComponent("revenuecat")
.appendingPathComponent("paywall_event_store")

Logger.verbose(PaywallEventStoreStrings.initializing(url))

return try .init(handler: FileHandler(url))
}

Expand All @@ -116,6 +119,8 @@ extension PaywallEventStore {
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private enum PaywallEventStoreStrings {

case initializing(URL)

case storing_event(PaywallEvent)

case error_storing_event(Error)
Expand All @@ -131,6 +136,9 @@ extension PaywallEventStoreStrings: LogMessage {

var description: String {
switch self {
case let .initializing(directory):
return "Initializing PaywallEventStore: \(directory.absoluteString)"

case let .storing_event(event):
return "Storing event: \(event.debugDescription)"

Expand Down
25 changes: 16 additions & 9 deletions Sources/Paywalls/Events/PaywallEventsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ protocol PaywallEventsManagerType {
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
func track(paywallEvent: PaywallEvent) async

/// - Throws: if posting events fails
/// - Returns: the number of events posted
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
func flushEvents(count: Int) async
func flushEvents(count: Int) async throws -> Int

}

Expand All @@ -46,10 +48,10 @@ actor PaywallEventsManager: PaywallEventsManagerType {
await self.store.store(.init(event: paywallEvent, userID: self.userProvider.currentAppUserID))
}

func flushEvents(count: Int) async {
func flushEvents(count: Int) async throws -> Int {
guard !self.flushInProgress else {
Logger.debug(Strings.paywalls.event_flush_already_in_progress)
return
return 0
}
self.flushInProgress = true
defer { self.flushInProgress = false }
Expand All @@ -58,21 +60,26 @@ actor PaywallEventsManager: PaywallEventsManagerType {

guard !events.isEmpty else {
Logger.verbose(Strings.paywalls.event_flush_with_empty_store)
return
return 0
}

Logger.verbose(Strings.paywalls.event_flush_starting(count: events.count))

let error = await self.internalAPI.postPaywallEvents(events: events)
do {
try await self.internalAPI.postPaywallEvents(events: events)

if let error {
await self.store.clear(count)

return events.count
} catch {
Logger.error(Strings.paywalls.event_flush_failed(error))

if error.successfullySynced {
if let backendError = error as? BackendError,
backendError.successfullySynced {
await self.store.clear(count)
}
} else {
await self.store.clear(count)

throw error
}
}

Expand Down
12 changes: 11 additions & 1 deletion Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
convenience init(apiKey: String,
appUserID: String?,
userDefaults: UserDefaults? = nil,
documentsDirectory: URL? = nil,
observerMode: Bool = false,
platformInfo: PlatformInfo? = Purchases.platformInfo,
responseVerificationMode: Signing.ResponseVerificationMode,
Expand Down Expand Up @@ -359,7 +360,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
paywallEventsManager = PaywallEventsManager(
internalAPI: backend.internalAPI,
userProvider: identityManager,
store: try PaywallEventStore.createDefault()
store: try PaywallEventStore.createDefault(documentsDirectory: documentsDirectory)
)
Logger.verbose(Strings.paywalls.event_manager_initialized)
} else {
Expand Down Expand Up @@ -1266,6 +1267,7 @@ public extension Purchases {
appUserID: String?,
observerMode: Bool,
userDefaults: UserDefaults?,
documentsDirectory: URL? = nil,
platformInfo: PlatformInfo?,
responseVerificationMode: Signing.ResponseVerificationMode,
storeKit2Setting: StoreKit2Setting,
Expand All @@ -1277,6 +1279,7 @@ public extension Purchases {
.init(apiKey: apiKey,
appUserID: appUserID,
userDefaults: userDefaults,
documentsDirectory: documentsDirectory,
observerMode: observerMode,
platformInfo: platformInfo,
responseVerificationMode: responseVerificationMode,
Expand Down Expand Up @@ -1531,6 +1534,13 @@ internal extension Purchases {
self.offeringsManager.invalidateCachedOfferings(appUserID: self.appUserID)
}

/// - Throws: if posting events fails
/// - Returns: the number of events posted
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
func flushPaywallEvents(count: Int) async throws -> Int {
return try await self.paywallEventsManager?.flushEvents(count: count) ?? 0
}

}

#endif
Expand Down
Loading

0 comments on commit 42e2300

Please sign in to comment.