Skip to content

Commit

Permalink
Paywalls: automatically flush events
Browse files Browse the repository at this point in the history
This uses the new `Delay.long` (#3168) to flush events on app foreground.

Also added more tests.
  • Loading branch information
NachoSoto committed Sep 11, 2023
1 parent 4603de5 commit 3999a42
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 10 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@
4FD3688B2AA7C12600F63354 /* PaywallEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD3688A2AA7C12600F63354 /* PaywallEvent.swift */; };
4FD368B42AA7CFED00F63354 /* PaywallEventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD368B32AA7CFED00F63354 /* PaywallEventStore.swift */; };
4FD368B62AA7D09C00F63354 /* PaywallEventSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD368B52AA7D09C00F63354 /* PaywallEventSerializer.swift */; };
4FD7E8662AABC4470055406F /* PurchasesPaywallEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD7E8652AABC4470055406F /* PurchasesPaywallEventsTests.swift */; };
4FDA13842A33D9BD00C45CFE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4FDA13662A33D13700C45CFE /* PrivacyInfo.xcprivacy */; };
4FDF10E72A725EA6004F3680 /* ExternalPurchasesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF10E62A725EA6004F3680 /* ExternalPurchasesManager.swift */; };
4FDF10E82A725EA6004F3680 /* ExternalPurchasesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF10E62A725EA6004F3680 /* ExternalPurchasesManager.swift */; };
Expand Down Expand Up @@ -1040,6 +1041,7 @@
4FD3688A2AA7C12600F63354 /* PaywallEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEvent.swift; sourceTree = "<group>"; };
4FD368B32AA7CFED00F63354 /* PaywallEventStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEventStore.swift; sourceTree = "<group>"; };
4FD368B52AA7D09C00F63354 /* PaywallEventSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallEventSerializer.swift; sourceTree = "<group>"; };
4FD7E8652AABC4470055406F /* PurchasesPaywallEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesPaywallEventsTests.swift; sourceTree = "<group>"; };
4FDA13662A33D13700C45CFE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
4FDF10E62A725EA6004F3680 /* ExternalPurchasesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalPurchasesManager.swift; sourceTree = "<group>"; };
4FDF10E92A726269004F3680 /* ObserverModeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserverModeManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2358,6 +2360,7 @@
4FFCED802AA941B200118EF4 /* PaywallEventsRequestTests.swift */,
4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */,
4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */,
4FD7E8652AABC4470055406F /* PurchasesPaywallEventsTests.swift */,
);
path = Events;
sourceTree = "<group>";
Expand Down Expand Up @@ -3701,6 +3704,7 @@
5796A39427D6BD6900653165 /* BackendGetOfferingsTests.swift in Sources */,
5766AA42283C768600FA6091 /* OperatorExtensionsTests.swift in Sources */,
4F54DF3F2A1D8C7500FD72BF /* MockStoreKit2TransactionFetcher.swift in Sources */,
4FD7E8662AABC4470055406F /* PurchasesPaywallEventsTests.swift in Sources */,
351B516A26D44CB300BD2BD7 /* ISOPeriodFormatterTests.swift in Sources */,
57DC9F4A27CD37BA00DA6AF9 /* HTTPStatusCodeTests.swift in Sources */,
2DDF41DE24F6F527005BC22D /* MockAppleReceiptBuilder.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions Sources/Paywalls/Events/Networking/PaywallEventsRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ extension PaywallEventsRequest {

enum EventType: String {

case view
case cancel
case close
case view = "paywall_view"
case cancel = "paywall_cancel"
case close = "paywall_close"

}

Expand Down Expand Up @@ -111,7 +111,7 @@ extension PaywallEventsRequest.Event: Encodable {
case timestamp
case displayMode
case darkMode
case localeIdentifier
case localeIdentifier = "locale"

}

Expand Down
2 changes: 2 additions & 0 deletions Sources/Paywalls/Events/PaywallEventsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,6 @@ actor PaywallEventsManager: PaywallEventsManagerType {
}
}

static let defaultEventFlushCount = 50

}
11 changes: 11 additions & 0 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,8 @@ private extension Purchases {
}
#endif

self.postPaywallEventsIfNeeded()

#endif
}

Expand Down Expand Up @@ -1687,6 +1689,15 @@ private extension Purchases {
}
}

private func postPaywallEventsIfNeeded() {
guard #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *),
let manager = self.paywallEventsManager else { return }

self.operationDispatcher.dispatchOnWorkerThread(delay: .long) {
_ = try? await manager.flushEvents(count: PaywallEventsManager.defaultEventFlushCount)
}
}

}

// MARK: - Deprecations
Expand Down
11 changes: 5 additions & 6 deletions Tests/BackendIntegrationTests/BaseBackendIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ final class TestPurchaseDelegate: NSObject, PurchasesDelegate, Sendable {
class BaseBackendIntegrationTests: TestCase {

private var userDefaults: UserDefaults!
private var documentsDirectory: URL!
private var testUUID: UUID!

// swiftlint:disable:next weak_delegate
Expand Down Expand Up @@ -97,6 +98,10 @@ class BaseBackendIntegrationTests: TestCase {

self.createUserDefaults()

self.documentsDirectory = URL
.cachesDirectory
.appendingPathComponent(UUID().uuidString, conformingTo: .directory)

// We use a different identifier for each test to ensure the backend
// doesn't produce conflicts when producing similar receipts across
// separate test invocations.
Expand Down Expand Up @@ -220,12 +225,6 @@ private extension BaseBackendIntegrationTests {
internalSettings: self)
}

var documentsDirectory: URL {
return URL
.cachesDirectory
.appendingPathComponent(UUID().uuidString, conformingTo: .directory)
}

}

extension BaseBackendIntegrationTests: InternalDangerousSettingsType {
Expand Down
10 changes: 10 additions & 0 deletions Tests/BackendIntegrationTests/PaywallEventsIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ class PaywallEventsIntegrationTests: BaseStoreKitIntegrationTests {
expect(result) == 0
}

func testRemembersEventsWhenReopeningApp() async throws {
try await self.purchases.track(paywallEvent: .view(self.eventData))
try await self.purchases.track(paywallEvent: .close(self.eventData))

await self.resetSingleton()

let result = try await self.purchases.flushPaywallEvents(count: 10)
expect(result) == 2
}

}

private extension PaywallEventsIntegrationTests {
Expand Down
22 changes: 22 additions & 0 deletions Tests/UnitTests/Mocks/MockOperationDispatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,26 @@ class MockOperationDispatcher: OperationDispatcher {
}
}

var invokedDispatchAsyncOnWorkerThread = false
var invokedDispatchAsyncOnWorkerThreadCount = 0
var invokedDispatchAsyncOnWorkerThreadDelayParam: Delay?

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
override func dispatchOnWorkerThread(
delay: Delay = .none,
block: @escaping @Sendable () async -> Void
) {
self.invokedDispatchAsyncOnWorkerThreadDelayParam = delay
self.invokedDispatchAsyncOnWorkerThread = true
self.invokedDispatchAsyncOnWorkerThreadCount += 1

if self.forwardToOriginalDispatchOnWorkerThread {
super.dispatchOnWorkerThread(delay: delay, block: block)
} else if self.shouldInvokeDispatchOnWorkerThreadBlock {
Task<Void, Never> {
await block()
}
}
}

}
22 changes: 22 additions & 0 deletions Tests/UnitTests/Paywalls/Events/PaywallEventsManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,28 @@ class PaywallEventsManagerTests: TestCase {
await self.verifyEvents([.init(event: event2, userID: Self.userID)])
}

func testCannotFlushMultipleTimesInParallel() async throws {
let event1 = await self.storeRandomEvent()
_ = await self.storeRandomEvent()

let task1 = Task<Int, Error> { [manager = self.manager!] in try await manager.flushEvents(count: 1) }
let task2 = Task<Int, Error> { [manager = self.manager!] in try await manager.flushEvents(count: 1) }

let result1 = try await task1.value
let result2 = try await task2.value

expect(result1) == 1
expect(result2) == 0

expect(self.api.invokedPostPaywallEvents) == true
expect(self.api.invokedPostPaywallEventsParameters).to(haveCount(1))
expect(self.api.invokedPostPaywallEventsParameters.onlyElement) == [.init(event: event1, userID: Self.userID)]

self.logger.verifyMessageWasLogged(Strings.paywalls.event_flush_already_in_progress,
level: .debug,
expectedCount: 1)
}

// MARK: -

private static let userID = "nacho"
Expand Down
41 changes: 41 additions & 0 deletions Tests/UnitTests/Paywalls/Events/PurchasesPaywallEventsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// 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
//
// PurchasesPaywallEventsTests.swift
//
// Created by Nacho Soto on 9/8/23.

import Nimble
import StoreKit
import XCTest

@testable import RevenueCat

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
class PurchasesPaywallEventsTests: BasePurchasesTests {

override func setUpWithError() throws {
try super.setUpWithError()

try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

self.setupPurchases()
}

func testApplicationWillEnterForegroundSendsEvents() async throws {
self.notificationCenter.fireNotifications()

let manager = try self.mockPaywallEventsManager

try await asyncWait { await manager.invokedFlushEvents == true }

expect(self.mockOperationDispatcher.invokedDispatchAsyncOnWorkerThreadDelayParam) == .long
}

}

0 comments on commit 3999a42

Please sign in to comment.