Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls: implemented PostPaywallEventsOperation #3158

Merged
merged 6 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@
4FE6FEEA2AA940E300780B45 /* PaywallEventStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */; };
4FE6FEEB2AA940E300780B45 /* PaywallEventSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */; };
4FF8464D2A32554300617F00 /* DiagnosticsStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF8464C2A32554300617F00 /* DiagnosticsStrings.swift */; };
4FFCED822AA941B200118EF4 /* PaywallEventsRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFCED802AA941B200118EF4 /* PaywallEventsRequestTests.swift */; };
4FFCED832AA941B200118EF4 /* PaywallEventsBackendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFCED812AA941B200118EF4 /* PaywallEventsBackendTests.swift */; };
4FFCED882AA941D200118EF4 /* PaywallEventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFCED852AA941D200118EF4 /* PaywallEventsRequest.swift */; };
4FFCED892AA941D200118EF4 /* PaywallHTTPRequestPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFCED862AA941D200118EF4 /* PaywallHTTPRequestPath.swift */; };
4FFCED8A2AA941D200118EF4 /* PostPaywallEventsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFCED872AA941D200118EF4 /* PostPaywallEventsOperation.swift */; };
57032ABF28C13CE4004FF47A /* StoreKit2SettingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */; };
57045B3829C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */; };
57045B3A29C51751001A5417 /* GetProductEntitlementMappingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57045B3929C51751001A5417 /* GetProductEntitlementMappingOperation.swift */; };
Expand Down Expand Up @@ -1041,6 +1046,11 @@
4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventSerializerTests.swift; sourceTree = "<group>"; };
4FED3AD62AAA7DD4001D4D5E /* purchases-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "purchases-ios"; path = ..; sourceTree = "<group>"; };
4FF8464C2A32554300617F00 /* DiagnosticsStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsStrings.swift; sourceTree = "<group>"; };
4FFCED802AA941B200118EF4 /* PaywallEventsRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventsRequestTests.swift; sourceTree = "<group>"; };
4FFCED812AA941B200118EF4 /* PaywallEventsBackendTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventsBackendTests.swift; sourceTree = "<group>"; };
4FFCED852AA941D200118EF4 /* PaywallEventsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallEventsRequest.swift; sourceTree = "<group>"; };
4FFCED862AA941D200118EF4 /* PaywallHTTPRequestPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallHTTPRequestPath.swift; sourceTree = "<group>"; };
4FFCED872AA941D200118EF4 /* PostPaywallEventsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostPaywallEventsOperation.swift; sourceTree = "<group>"; };
4FFD88BE2A4B56E2008E98AC /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = "<group>"; };
57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2SettingTests.swift; sourceTree = "<group>"; };
57045B3729C514A8001A5417 /* ProductEntitlementMappingDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductEntitlementMappingDecodingTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2315,6 +2325,7 @@
4FD368B22AA7CFDD00F63354 /* Events */ = {
isa = PBXGroup;
children = (
4FFCED842AA941D200118EF4 /* Networking */,
4FD3688A2AA7C12600F63354 /* PaywallEvent.swift */,
4FD368B32AA7CFED00F63354 /* PaywallEventStore.swift */,
4FD368B52AA7D09C00F63354 /* PaywallEventSerializer.swift */,
Expand All @@ -2326,12 +2337,24 @@
4FE6FEE62AA940E300780B45 /* Events */ = {
isa = PBXGroup;
children = (
4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */,
4FFCED812AA941B200118EF4 /* PaywallEventsBackendTests.swift */,
4FFCED802AA941B200118EF4 /* PaywallEventsRequestTests.swift */,
4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */,
4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */,
);
path = Events;
sourceTree = "<group>";
};
4FFCED842AA941D200118EF4 /* Networking */ = {
isa = PBXGroup;
children = (
4FFCED852AA941D200118EF4 /* PaywallEventsRequest.swift */,
4FFCED862AA941D200118EF4 /* PaywallHTTPRequestPath.swift */,
4FFCED872AA941D200118EF4 /* PostPaywallEventsOperation.swift */,
);
path = Networking;
sourceTree = "<group>";
};
570896B627596E6E00296F1C /* APITesters */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3281,6 +3304,7 @@
2CD72942268A823900BFC976 /* Data+Extensions.swift in Sources */,
4FD3688B2AA7C12600F63354 /* PaywallEvent.swift in Sources */,
5766AA3E283C750300FA6091 /* Operators+Extensions.swift in Sources */,
4FFCED892AA941D200118EF4 /* PaywallHTTPRequestPath.swift in Sources */,
4F6BEDE22A26B69500CD9322 /* DebugContentViews.swift in Sources */,
B3B5FBBC269D121B00104A0C /* Offerings.swift in Sources */,
9A65E03B25918B0900DE00B0 /* CustomerInfoStrings.swift in Sources */,
Expand Down Expand Up @@ -3444,6 +3468,7 @@
574A2F4B282D7AEA00150D40 /* PostOfferResponse.swift in Sources */,
F575858D26C088FE00C12B97 /* OfferingsManager.swift in Sources */,
B34605CB279A6E380031CA74 /* PostReceiptDataOperation.swift in Sources */,
4FFCED882AA941D200118EF4 /* PaywallEventsRequest.swift in Sources */,
354895D4267AE4B4001DC5B1 /* AttributionKey.swift in Sources */,
F5714EA826D7A83A00635477 /* Store+Extensions.swift in Sources */,
35F82BAB26A84E130051DF03 /* Dictionary+Extensions.swift in Sources */,
Expand Down Expand Up @@ -3493,6 +3518,7 @@
B372EC54268FEDC60099171E /* StoreProductDiscount.swift in Sources */,
4F7D8E562A56290100F17FFC /* HTTPRequestBody.swift in Sources */,
B34605BE279A6E380031CA74 /* OfferingsCallback.swift in Sources */,
4FFCED8A2AA941D200118EF4 /* PostPaywallEventsOperation.swift in Sources */,
B34605BF279A6E380031CA74 /* LogInCallback.swift in Sources */,
9A65DFDE258AD60A00DE00B0 /* LogIntent.swift in Sources */,
B37815492857F1E7000A7B93 /* BackendConfiguration.swift in Sources */,
Expand Down Expand Up @@ -3612,6 +3638,7 @@
B380D69B27726AB500984578 /* DNSCheckerTests.swift in Sources */,
5774F9C12805EA3000997128 /* BaseHTTPResponseTest.swift in Sources */,
351B51B526D450E800BD2BD7 /* ProductsFetcherSK1Tests.swift in Sources */,
4FFCED822AA941B200118EF4 /* PaywallEventsRequestTests.swift in Sources */,
4F05876F2A5DE03F00E9A834 /* PaywallDataTests.swift in Sources */,
2DDF41CC24F6F4C3005BC22D /* AppleReceiptBuilderTests.swift in Sources */,
575A8EE52922C9F300936709 /* MockStoreKit2TransactionListenerDelegate.swift in Sources */,
Expand Down Expand Up @@ -3754,6 +3781,7 @@
37E352973B0901E3CAA717E1 /* DateFormatter+ExtensionsTests.swift in Sources */,
B3F8418F26F3A93400E560FB /* ErrorCodeTests.swift in Sources */,
5793397228E77A6E00C1232C /* MockPaymentQueue.swift in Sources */,
4FFCED832AA941B200118EF4 /* PaywallEventsBackendTests.swift in Sources */,
5796A38A27D6B96300653165 /* BackendGetCustomerInfoTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
4 changes: 4 additions & 0 deletions Sources/Networking/HTTPClient/HTTPRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ struct HTTPRequest {
self.init(method: method, requestPath: path, nonce: nonce)
}

init(method: Method, path: HTTPRequest.PaywallPath, nonce: Data? = nil) {
self.init(method: method, requestPath: path, nonce: nonce)
}

private init(method: Method, requestPath: HTTPRequestPath, nonce: Data? = nil) {
assert(nonce == nil || nonce?.count == Data.nonceLength,
"Invalid nonce: \(nonce?.description ?? "")")
Expand Down
6 changes: 6 additions & 0 deletions Sources/Networking/HTTPClient/HTTPRequestPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ extension HTTPRequest {

}

enum PaywallPath: Hashable {

case postEvents

}

}

extension HTTPRequest.Path: HTTPRequestPath {
Expand Down
37 changes: 32 additions & 5 deletions Sources/Networking/InternalAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,56 @@

import Foundation

final class InternalAPI {
class InternalAPI {

typealias ResponseHandler = (BackendError?) -> Void

private let backendConfig: BackendConfiguration
private let callbackCache: CallbackCache<HealthOperation.Callback>
private let healthCallbackCache: CallbackCache<HealthOperation.Callback>

init(backendConfig: BackendConfiguration) {
self.backendConfig = backendConfig
self.callbackCache = .init()
self.healthCallbackCache = .init()
}

func healthRequest(signatureVerification: Bool, completion: @escaping ResponseHandler) {
let factory = HealthOperation.createFactory(httpClient: self.backendConfig.httpClient,
callbackCache: self.callbackCache,
callbackCache: self.healthCallbackCache,
signatureVerification: signatureVerification)

let callback = HealthOperation.Callback(cacheKey: factory.cacheKey, completion: completion)
let cacheStatus = self.callbackCache.add(callback)
let cacheStatus = self.healthCallbackCache.add(callback)

self.backendConfig.addCacheableOperation(with: factory,
delay: .none,
cacheStatus: cacheStatus)
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
func postPaywallEvents(events: [PaywallStoredEvent], completion: @escaping ResponseHandler) {
guard !events.isEmpty else {
self.backendConfig.operationDispatcher.dispatchOnMainThread {
completion(nil)
}
return
}
Comment on lines +42 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need main thread for anything in paywall events?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I did that out of habit from other operations, but ResponseHandler isn't @MainActor anyway.
I'll fix in the next PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


let operation = PostPaywallEventsOperation(configuration: .init(httpClient: self.backendConfig.httpClient),
request: .init(events: events),
responseHandler: completion)

self.backendConfig.operationQueue.addOperation(operation)
}

}

extension InternalAPI {

@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
self.postPaywallEvents(events: events, completion: completion)
}
}

}
120 changes: 120 additions & 0 deletions Sources/Paywalls/Events/Networking/PaywallEventsRequest.swift
Original file line number Diff line number Diff line change
@@ -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
//
// PaywallEventsRequest.swift
//
// Created by Nacho Soto on 9/6/23.

import Foundation

/// The content of a request to the events endpoints.
struct PaywallEventsRequest {

var events: [Event]

init(events: [Event]) {
self.events = events
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
init(events: [PaywallStoredEvent]) {
self.init(events: events.map { .init(storedEvent: $0) })
}

}

extension PaywallEventsRequest {

enum EventType: String {

case view
case cancel
case close

}

struct Event {

let version: Int
var type: EventType
var appUserID: String
var sessionID: String
var offeringID: String
var paywallRevision: Int
var timestamp: Date
var displayMode: PaywallViewMode
var darkMode: Bool
var localeIdentifier: String

}

}

extension PaywallEventsRequest.Event {

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
init(storedEvent: PaywallStoredEvent) {
let data = storedEvent.event.data

self.init(
version: Self.version,
type: storedEvent.event.eventType,
appUserID: storedEvent.userID,
sessionID: data.sessionIdentifier.uuidString,
offeringID: data.offeringIdentifier,
paywallRevision: data.paywallRevision,
timestamp: data.date,
displayMode: data.displayMode,
darkMode: data.darkMode,
localeIdentifier: data.localeIdentifier
)
}

private static let version: Int = 1

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
private extension PaywallEvent {

var eventType: PaywallEventsRequest.EventType {
switch self {
case .view: return .view
case .cancel: return .cancel
case .close: return .close
}

}

}

// MARK: - Codable

extension PaywallEventsRequest.EventType: Encodable {}

extension PaywallEventsRequest.Event: Encodable {

private enum CodingKeys: String, CodingKey {

case version
case type
case appUserID = "appUserId"
case sessionID = "sessionId"
case offeringID = "offeringId"
case paywallRevision
case timestamp
case displayMode
case darkMode
case localeIdentifier

}

}

extension PaywallEventsRequest: HTTPRequestBody {}
55 changes: 55 additions & 0 deletions Sources/Paywalls/Events/Networking/PaywallHTTPRequestPath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// 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
//
// PaywallHTTPRequestPath.swift
//
// Created by Nacho Soto on 9/5/23.

import Foundation

extension HTTPRequest.PaywallPath: HTTPRequestPath {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is thanks to the refactor in #2986


static let serverHostURL = URL(string: "https://api-paywalls.revenuecat.com")!

var authenticated: Bool {
switch self {
case .postEvents:
return true
}
Comment on lines +21 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure this makes sense, but why do all of these do switch self case .postEvents return hardcoded value? why not just return the harcoded value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that when/if we add a new path the compiler fails and forces us to implement these for the new case.

}

var shouldSendEtag: Bool {
switch self {
case .postEvents:
return false
}
}

var supportsSignatureVerification: Bool {
switch self {
case .postEvents:
return false
}
}

var needsNonceForSigning: Bool {
switch self {
case .postEvents:
return false
}
}

var pathComponent: String {
switch self {
case .postEvents:
return "events"
}
}

}
Loading