Skip to content

Commit

Permalink
Paywalls: implemented PostPaywallEventsOperation
Browse files Browse the repository at this point in the history
This adds the operation + `PaywallEventsRequest` and associated tests.
Also added method to `InternalAPI` for posting events.
  • Loading branch information
NachoSoto committed Sep 8, 2023
1 parent 7d52e4d commit a517ef0
Show file tree
Hide file tree
Showing 10 changed files with 485 additions and 6 deletions.
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
}

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 {

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

var authenticated: Bool {
switch self {
case .postEvents:
return true
}
}

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 "paywall_events"
}
}
}
Loading

0 comments on commit a517ef0

Please sign in to comment.