mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
* Add RC Ads event tracking infrastructure Implements event tracking for RevenueCat Ads following the Paywalls Events pattern. - Adds three event types: ad displayed, ad opened, and ad revenue - Uses actor-based concurrency with file-based event storage - Posts events to e.revenue.cat/v1/events endpoint - Marks track(adEvent:) method as @_spi(Internal) for internal use - Includes comprehensive unit tests with snapshot testing - Available on iOS 15.0+, tvOS 15.0+, macOS 12.0+, watchOS 8.0+ Event schema follows khepri RC Ads input events specification with app_session_id field matching backend expectations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Avoid mapping in EventsRequest+Ad * Consolidate ad events into PaywallEventsManager Eliminates duplication between AdEventsManager and PaywallEventsManager by: - Making PaywallEventsManager handle all event types (ads, paywalls, customerCenter) - Adding feature-aware routing in flushEvents() to send events to correct domains - Ad events → https://e.revenue.cat/v1/events - Other events → https://api-paywalls.revenuecat.com/v1/events - Removing AdEventsManager, AdEventStore, and their tests (~500 lines) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove AdsStrings and use PaywallsStrings instead AdsStrings only had 2 strings in use, both of which already exist in PaywallsStrings. Consolidates logging strings to eliminate duplication. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Namespace Precision enum inside AdEvent.RevenueData Moves Precision from a top-level public enum to AdEvent.RevenueData.Precision to avoid polluting the global namespace and reduce autocomplete noise. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Move Precision conformances to end of file Follows the pattern of other types by declaring protocol conformances at the end of the file rather than inline with the type definition. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove redundant sessionIdentifier from AdImpressionData The sessionIdentifier field in AdImpressionData was redundant since StoredEvent already tracks appSessionID. This change: - Removes sessionIdentifier from AdImpressionData struct - Uses storedEvent.appSessionID directly when building requests - Renames sessionId to appSessionId to match backend field app_session_id - Updates documentation examples to remove sessionIdentifier parameter - Adds guard statement for optional appSessionID - Updates all test fixtures and regenerates snapshots - Removes iOS26 snapshot references from project 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove unneeded SessionId * Add proper assertions to PurchasesAdEventsTests Enhanced test coverage for public ad tracking APIs: - Verify events are tracked via mockPaywallEventsManager - Assert correct event types (displayed, opened, revenue) - Validate all impression data fields - Verify revenue-specific fields (revenueMicros, currency, precision) All 3 tests now pass with proper verification. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add ENABLE_AD_EVENTS_TRACKING compiler flag * Simplify track ad events APIs * Fix ENABLE_AD_EVENTS_TRACKING scope and refactor Precision to struct Compiler flag fixes: - Move ENABLE_AD_EVENTS_TRACKING #endif to wrap all ad event code including internal enum - Add compiler flag guards to all three test files Precision API refactor: - Change AdRevenue.Precision from enum to struct following Apple's API patterns (like Message.Reason) - Use internal init(rawValue:) to prevent custom precision values - Convert enum cases to static properties maintaining same public API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Refactor mediatorName from String to MediatorName struct Replace string-based mediator names with a type-safe struct following Apple's API patterns: - Add MediatorName struct with public init(rawValue:) to support custom values - Include predefined static properties for .adMob and .appLovin - Update AdEventData protocol to require MediatorName type - Update all event types (AdDisplayed, AdOpened, AdRevenue) to use MediatorName - Use .rawValue when serializing to network requests - Update all tests and documentation examples to use .appLovin 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove RawRepresentable conformance from Precision and update test snapshots - Remove RawRepresentable conformance from AdRevenue.Precision to allow internal init - Update test assertions to use .appLovin instead of string comparison - Update all snapshot test files to use "AppLovin" instead of "MAX" - All ad event tests now passing (11/11) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix PaywallDataDecodingTests locale check for region overrides The test was failing when the system locale included region overrides (e.g., "en_US@rg=eszzzz" instead of just "en_US"). Changes: - Use beginWith("en_US") instead of exact equality check - Compare against enConfig directly instead of fetching via Locale.current This makes the test more robust while maintaining the same behavior validation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Rename timestampMs to timestamp but keep serialized timestamp_ms * Use different serverHostURL * Simplify and paralelize postEvents * Adjust usage of ENABLE_AD_EVENTS_TRACKING flag * Move track ad event methods to AdTracker * Remove PaywallEventsManager implementation * Improving naming overlapping naming AdEvent->AdEventRequest * Simplify compiler flag usage * Use Experimental * Remove duplicate with PostPaywallEventsOperation and PostAdEventsOperation * Remove postAdEvents methods assuming reuse of event storage * Make Precision rawValue internal * Remove runtime checks * Trigger build * Move Public Types MARK comment after internal protocol * Add documentation for AdDisplayed, AdOpened, and AdRevenue properties * Remove redundant smoke tests The "DoesNotCrash" tests were completely redundant with the comprehensive "StoresEvent" tests. The comprehensive tests already: 1. Verify methods don't crash (implicitly) 2. Verify events are stored 3. Verify event data is correct Removed 3 redundant tests, keeping only the 3 comprehensive tests. * Fix bug not tracking initialized events --------- Co-authored-by: Claude <noreply@anthropic.com>
342 lines
9.3 KiB
Swift
342 lines
9.3 KiB
Swift
//
|
|
// 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
|
|
//
|
|
// HTTPRequestPath.swift
|
|
//
|
|
// Created by Nacho Soto on 8/8/23.
|
|
|
|
import Foundation
|
|
|
|
protocol HTTPRequestPath {
|
|
|
|
/// The base URL for requests to this path.
|
|
static var serverHostURL: URL { get }
|
|
|
|
/// The fallback hosts to use when the main server is down.
|
|
///
|
|
/// Not all endpoints have a fallback host, but some do.
|
|
var fallbackHosts: [URL] { get }
|
|
|
|
/// Whether requests to this path are authenticated.
|
|
var authenticated: Bool { get }
|
|
|
|
/// Whether requests to this path can be cached using `ETagManager`.
|
|
var shouldSendEtag: Bool { get }
|
|
|
|
/// Whether the endpoint will perform signature verification.
|
|
var supportsSignatureVerification: Bool { get }
|
|
|
|
/// Whether endpoint requires a nonce for signature verification.
|
|
var needsNonceForSigning: Bool { get }
|
|
|
|
/// The name of the endpoint.
|
|
var name: String { get }
|
|
|
|
/// The full relative path for this endpoint.
|
|
var relativePath: String { get }
|
|
}
|
|
|
|
extension HTTPRequestPath {
|
|
|
|
var fallbackHosts: [URL] {
|
|
return []
|
|
}
|
|
|
|
var url: URL? { return self.url(proxyURL: nil) }
|
|
|
|
func url(proxyURL: URL? = nil, fallbackHostIndex: Int? = nil) -> URL? {
|
|
let baseURL: URL
|
|
if let proxyURL {
|
|
baseURL = proxyURL
|
|
} else if let fallbackHostIndex {
|
|
guard let fallbackHost = self.fallbackHosts[safe: fallbackHostIndex] else {
|
|
return nil
|
|
}
|
|
baseURL = fallbackHost
|
|
} else {
|
|
baseURL = Self.serverHostURL
|
|
}
|
|
return URL(string: self.relativePath, relativeTo: baseURL)
|
|
}
|
|
}
|
|
|
|
// MARK: - Main paths
|
|
|
|
extension HTTPRequest {
|
|
|
|
enum Path: Hashable {
|
|
|
|
case getCustomerInfo(appUserID: String)
|
|
case getOfferings(appUserID: String)
|
|
case getIntroEligibility(appUserID: String)
|
|
case logIn
|
|
case postAttributionData(appUserID: String)
|
|
case postOfferForSigning
|
|
case postReceiptData
|
|
case postSubscriberAttributes(appUserID: String)
|
|
case postAdServicesToken(appUserID: String)
|
|
case health
|
|
case appHealthReport(appUserID: String)
|
|
case appHealthReportAvailability(appUserID: String)
|
|
case getProductEntitlementMapping
|
|
case getCustomerCenterConfig(appUserID: String)
|
|
case getVirtualCurrencies(appUserID: String)
|
|
case postRedeemWebPurchase
|
|
|
|
}
|
|
|
|
enum PaywallPath: Hashable {
|
|
|
|
case postEvents
|
|
|
|
}
|
|
|
|
enum DiagnosticsPath: Hashable {
|
|
|
|
case postDiagnostics
|
|
|
|
}
|
|
|
|
enum WebBillingPath: Hashable {
|
|
|
|
case getWebOfferingProducts(appUserID: String)
|
|
case getWebBillingProducts(userId: String, productIds: Set<String>)
|
|
|
|
}
|
|
|
|
enum AdPath: Hashable {
|
|
|
|
case postEvents
|
|
|
|
}
|
|
|
|
}
|
|
|
|
extension HTTPRequest.Path: HTTPRequestPath {
|
|
|
|
// swiftlint:disable:next force_unwrapping
|
|
static let serverHostURL = URL(string: "https://api.revenuecat.com")!
|
|
|
|
var fallbackHosts: [URL] {
|
|
switch self {
|
|
case .getOfferings, .getProductEntitlementMapping:
|
|
// swiftlint:disable:next force_unwrapping
|
|
return [URL(string: "https://api-production.8-lives-cat.io")!]
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
var authenticated: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.getOfferings,
|
|
.getIntroEligibility,
|
|
.logIn,
|
|
.postAttributionData,
|
|
.postOfferForSigning,
|
|
.postReceiptData,
|
|
.postSubscriberAttributes,
|
|
.postAdServicesToken,
|
|
.postRedeemWebPurchase,
|
|
.getProductEntitlementMapping,
|
|
.getCustomerCenterConfig,
|
|
.getVirtualCurrencies,
|
|
.appHealthReport:
|
|
return true
|
|
|
|
case .health,
|
|
.appHealthReportAvailability:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var shouldSendEtag: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.getOfferings,
|
|
.getIntroEligibility,
|
|
.logIn,
|
|
.postAttributionData,
|
|
.postOfferForSigning,
|
|
.postReceiptData,
|
|
.postSubscriberAttributes,
|
|
.postAdServicesToken,
|
|
.postRedeemWebPurchase,
|
|
.getProductEntitlementMapping,
|
|
.getCustomerCenterConfig,
|
|
.getVirtualCurrencies,
|
|
.appHealthReport:
|
|
return true
|
|
case .health,
|
|
.appHealthReportAvailability:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var supportsSignatureVerification: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.logIn,
|
|
.postReceiptData,
|
|
.health,
|
|
.getOfferings,
|
|
.getProductEntitlementMapping,
|
|
.getVirtualCurrencies,
|
|
.appHealthReport,
|
|
.appHealthReportAvailability:
|
|
return true
|
|
case .getIntroEligibility,
|
|
.postSubscriberAttributes,
|
|
.postAttributionData,
|
|
.postAdServicesToken,
|
|
.postOfferForSigning,
|
|
.postRedeemWebPurchase,
|
|
.getCustomerCenterConfig:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var needsNonceForSigning: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.logIn,
|
|
.postReceiptData,
|
|
.getVirtualCurrencies,
|
|
.health,
|
|
.appHealthReportAvailability:
|
|
return true
|
|
case .getOfferings,
|
|
.getIntroEligibility,
|
|
.postSubscriberAttributes,
|
|
.postAttributionData,
|
|
.postAdServicesToken,
|
|
.postOfferForSigning,
|
|
.postRedeemWebPurchase,
|
|
.getProductEntitlementMapping,
|
|
.getCustomerCenterConfig,
|
|
.appHealthReport:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var relativePath: String {
|
|
return "/v1/\(self.pathComponent)"
|
|
}
|
|
|
|
var pathComponent: String {
|
|
switch self {
|
|
case let .getCustomerInfo(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))"
|
|
|
|
case let .getOfferings(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/offerings"
|
|
|
|
case let .getIntroEligibility(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/intro_eligibility"
|
|
|
|
case let .appHealthReport(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/health_report"
|
|
|
|
case let .appHealthReportAvailability(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/health_report_availability"
|
|
|
|
case .logIn:
|
|
return "subscribers/identify"
|
|
|
|
case let .postAttributionData(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/attribution"
|
|
|
|
case let .postAdServicesToken(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/adservices_attribution"
|
|
|
|
case .postOfferForSigning:
|
|
return "offers"
|
|
|
|
case .postReceiptData:
|
|
return "receipts"
|
|
|
|
case let .postSubscriberAttributes(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/attributes"
|
|
|
|
case .health:
|
|
return "health"
|
|
|
|
case .getProductEntitlementMapping:
|
|
return "product_entitlement_mapping"
|
|
|
|
case let .getCustomerCenterConfig(appUserID):
|
|
return "customercenter/\(Self.escape(appUserID))"
|
|
|
|
case .postRedeemWebPurchase:
|
|
return "subscribers/redeem_purchase"
|
|
|
|
case let .getVirtualCurrencies(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/virtual_currencies"
|
|
}
|
|
}
|
|
|
|
var name: String {
|
|
switch self {
|
|
case .getCustomerInfo:
|
|
return "get_customer"
|
|
|
|
case .getOfferings:
|
|
return "get_offerings"
|
|
|
|
case .getIntroEligibility:
|
|
return "get_intro_eligibility"
|
|
|
|
case .logIn:
|
|
return "log_in"
|
|
|
|
case .postAttributionData:
|
|
return "post_attribution"
|
|
|
|
case .postAdServicesToken:
|
|
return "post_adservices_token"
|
|
|
|
case .postOfferForSigning:
|
|
return "post_offer_for_signing"
|
|
|
|
case .postReceiptData:
|
|
return "post_receipt"
|
|
|
|
case .postSubscriberAttributes:
|
|
return "post_attributes"
|
|
|
|
case .health:
|
|
return "post_health"
|
|
|
|
case .getProductEntitlementMapping:
|
|
return "get_product_entitlement_mapping"
|
|
|
|
case .getCustomerCenterConfig:
|
|
return "customer_center"
|
|
|
|
case .postRedeemWebPurchase:
|
|
return "post_redeem_web_purchase"
|
|
|
|
case .appHealthReport:
|
|
return "get_app_health_report"
|
|
|
|
case .getVirtualCurrencies:
|
|
return "get_virtual_currencies"
|
|
|
|
case .appHealthReportAvailability:
|
|
return "get_app_health_report_availability"
|
|
|
|
}
|
|
}
|
|
|
|
private static func escape(_ appUserID: String) -> String {
|
|
return appUserID.trimmedAndEscaped
|
|
}
|
|
}
|