2023-08-10 15:23:13 -07:00
|
|
|
//
|
|
|
|
|
// 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.
|
|
|
|
|
|
2026-04-21 12:32:01 +02:00
|
|
|
// swiftlint:disable file_length
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
protocol HTTPRequestPath {
|
|
|
|
|
|
|
|
|
|
/// The base URL for requests to this path.
|
|
|
|
|
static var serverHostURL: URL { get }
|
|
|
|
|
|
2025-10-29 18:43:19 +01:00
|
|
|
/// The fallback URLs to use when the main server is down.
|
2025-04-25 14:00:13 +02:00
|
|
|
///
|
2025-10-29 18:43:19 +01:00
|
|
|
/// Not all endpoints have a fallback URL, but some do.
|
|
|
|
|
var fallbackUrls: [URL] { get }
|
2025-04-25 14:00:13 +02:00
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
/// 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 }
|
|
|
|
|
|
2024-04-10 13:06:05 +02:00
|
|
|
/// The name of the endpoint.
|
|
|
|
|
var name: String { get }
|
|
|
|
|
|
2025-04-25 14:00:13 +02:00
|
|
|
/// The full relative path for this endpoint.
|
|
|
|
|
var relativePath: String { get }
|
2025-11-06 22:43:27 +01:00
|
|
|
|
2026-08-21 14:04:21 -06:00
|
|
|
/// The full relative path for this endpoint when using IAM tokens.
|
|
|
|
|
var relativeIAMPath: String { get }
|
|
|
|
|
|
2025-11-06 22:43:27 +01:00
|
|
|
/// The fallback relative path for this endpoint, if any.
|
|
|
|
|
var fallbackRelativePath: String? { get }
|
2026-06-19 14:38:38 +02:00
|
|
|
|
2026-07-17 08:56:01 +02:00
|
|
|
/// Whether this path resolves its base host from the API source provider (the main-API host
|
|
|
|
|
/// list with failover) rather than the static `serverHostURL`.
|
|
|
|
|
var usesAPISources: Bool { get }
|
|
|
|
|
|
2026-08-03 14:51:50 +02:00
|
|
|
/// Whether this path's `serverHostURL` is a fallback host rather than the main API host.
|
|
|
|
|
///
|
|
|
|
|
/// Requests to such a path are fallback attempts from the very first try, even though
|
|
|
|
|
/// `HTTPClient`'s own fallback walk never started, so they must not be treated as main-source
|
|
|
|
|
/// requests when picking a timeout or updating the per-host fail-fast memory.
|
|
|
|
|
var isFallbackHostPath: Bool { get }
|
|
|
|
|
|
2026-06-19 14:38:38 +02:00
|
|
|
/// Additional headers specific to this endpoint.
|
|
|
|
|
var additionalHeaders: HTTPRequest.Headers { get }
|
2026-06-25 17:03:58 +02:00
|
|
|
|
|
|
|
|
/// Provides endpoint-specific inputs for response signature verification.
|
|
|
|
|
var responseSignatureContextProvider: ResponseSignatureContextProvider { get }
|
2026-08-21 14:04:21 -06:00
|
|
|
|
|
|
|
|
/// Whether this path corresponds to an IAM request
|
|
|
|
|
var isIAMPath: Bool { get }
|
2026-06-25 17:03:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Provides endpoint-specific inputs for backend response signature verification.
|
|
|
|
|
///
|
|
|
|
|
/// Most endpoints use the raw response body and request body. Specialized endpoints can override
|
|
|
|
|
/// either input.
|
|
|
|
|
protocol ResponseSignatureContextProvider {
|
|
|
|
|
|
|
|
|
|
/// Returns the response bytes that should be verified against the backend signature.
|
|
|
|
|
///
|
|
|
|
|
/// This may throw when deriving the signed payload requires validating response structure first.
|
|
|
|
|
func responsePayloadForSignature(from body: Data?, statusCode: HTTPStatusCode) throws -> Data?
|
|
|
|
|
|
|
|
|
|
/// Returns the request body component to include in signature parameters, if any.
|
|
|
|
|
func requestBodyForSignature(for request: HTTPRequest) -> HTTPRequestBody?
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct DefaultResponseSignatureContextProvider: ResponseSignatureContextProvider {
|
|
|
|
|
|
|
|
|
|
func responsePayloadForSignature(from body: Data?, statusCode: HTTPStatusCode) throws -> Data? {
|
|
|
|
|
return body
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requestBodyForSignature(for request: HTTPRequest) -> HTTPRequestBody? {
|
|
|
|
|
return request.requestBody
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension HTTPRequestPath {
|
|
|
|
|
|
2025-10-29 18:43:19 +01:00
|
|
|
var fallbackUrls: [URL] {
|
2025-04-25 14:00:13 +02:00
|
|
|
return []
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
|
2025-11-26 11:16:04 +01:00
|
|
|
var supportsFallbackURLs: Bool {
|
|
|
|
|
!fallbackUrls.isEmpty
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 22:43:27 +01:00
|
|
|
var fallbackRelativePath: String? {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 14:04:21 -06:00
|
|
|
var relativeIAMPath: String {
|
|
|
|
|
return self.relativePath
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 08:56:01 +02:00
|
|
|
var usesAPISources: Bool {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 14:51:50 +02:00
|
|
|
var isFallbackHostPath: Bool {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 14:38:38 +02:00
|
|
|
var additionalHeaders: HTTPRequest.Headers {
|
|
|
|
|
return [:]
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 17:03:58 +02:00
|
|
|
var responseSignatureContextProvider: ResponseSignatureContextProvider {
|
|
|
|
|
return DefaultResponseSignatureContextProvider()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 14:04:21 -06:00
|
|
|
var isIAMPath: Bool {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2023-08-10 15:23:13 -07:00
|
|
|
|
2026-08-21 14:04:21 -06:00
|
|
|
func url(proxyURL: URL? = nil,
|
|
|
|
|
apiSourceURL: URL? = nil,
|
|
|
|
|
fallbackUrlIndex: Int? = nil,
|
|
|
|
|
preferIAMPath: Bool) -> URL? {
|
2025-04-25 14:00:13 +02:00
|
|
|
let baseURL: URL
|
|
|
|
|
if let proxyURL {
|
2026-07-17 08:56:01 +02:00
|
|
|
// When a Proxy URL is set, we don't support API sources or fallback URLs
|
|
|
|
|
guard fallbackUrlIndex == nil, apiSourceURL == nil else {
|
2025-10-29 18:43:19 +01:00
|
|
|
// This is to safe guard against a potential infinite loop if the caller mistakenly
|
2026-07-17 08:56:01 +02:00
|
|
|
// passes a proxyURL together with an apiSourceURL or a fallbackUrlIndex.
|
2025-04-25 14:00:13 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-10-29 18:43:19 +01:00
|
|
|
baseURL = proxyURL
|
|
|
|
|
} else if let fallbackUrlIndex {
|
|
|
|
|
return self.fallbackUrls[safe: fallbackUrlIndex]
|
2026-07-17 08:56:01 +02:00
|
|
|
} else if let apiSourceURL {
|
|
|
|
|
baseURL = apiSourceURL
|
2025-04-25 14:00:13 +02:00
|
|
|
} else {
|
|
|
|
|
baseURL = Self.serverHostURL
|
|
|
|
|
}
|
2026-08-21 14:04:21 -06:00
|
|
|
|
|
|
|
|
let path = preferIAMPath ? self.relativeIAMPath : self.relativePath
|
|
|
|
|
return URL(string: path, relativeTo: baseURL)
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-04-24 14:46:21 +01:00
|
|
|
case appHealthReport(appUserID: String)
|
2025-07-07 17:00:54 +01:00
|
|
|
case appHealthReportAvailability(appUserID: String)
|
2023-08-10 15:23:13 -07:00
|
|
|
case getProductEntitlementMapping
|
2024-08-09 15:26:55 +02:00
|
|
|
case getCustomerCenterConfig(appUserID: String)
|
2025-07-07 12:11:27 -05:00
|
|
|
case getVirtualCurrencies(appUserID: String)
|
2026-08-27 16:14:19 -06:00
|
|
|
case spendVirtualCurrencies
|
2024-11-25 16:14:32 +01:00
|
|
|
case postRedeemWebPurchase
|
2025-11-19 08:26:43 +00:00
|
|
|
case postCreateTicket
|
2026-02-06 11:54:42 -06:00
|
|
|
case isPurchaseAllowedByRestoreBehavior(appUserID: String)
|
2026-04-23 13:57:32 +02:00
|
|
|
case rewardVerificationStatus(appUserID: String, clientTransactionID: String)
|
2026-06-29 13:25:55 +02:00
|
|
|
case remoteConfig(domain: String)
|
2023-08-10 15:23:13 -07:00
|
|
|
|
2026-08-21 14:04:22 -06:00
|
|
|
case tokenLogin
|
|
|
|
|
case tokenRefresh
|
|
|
|
|
case tokenLogOut
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 14:17:31 +02:00
|
|
|
enum FallbackPath: Hashable {
|
|
|
|
|
|
|
|
|
|
case remoteConfig(domain: String)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 14:52:35 +01:00
|
|
|
enum FeatureEventsPath: Hashable {
|
2023-09-08 13:21:06 -07:00
|
|
|
|
|
|
|
|
case postEvents
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-10 13:00:48 +02:00
|
|
|
enum DiagnosticsPath: Hashable {
|
|
|
|
|
|
|
|
|
|
case postDiagnostics
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-06 15:28:59 -06:00
|
|
|
enum WebBillingPath: Hashable {
|
|
|
|
|
|
2025-07-23 13:56:20 +02:00
|
|
|
case getWebOfferingProducts(appUserID: String)
|
2025-07-29 18:31:15 +02:00
|
|
|
case getWebBillingProducts(userId: String, productIds: Set<String>)
|
2025-05-06 15:28:59 -06:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-29 11:56:48 +01:00
|
|
|
enum AdPath: Hashable {
|
|
|
|
|
|
|
|
|
|
case postEvents
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension HTTPRequest.Path: HTTPRequestPath {
|
|
|
|
|
|
2025-11-04 13:35:20 +01:00
|
|
|
static var serverHostURL: URL {
|
|
|
|
|
SystemInfo.apiBaseURL
|
|
|
|
|
}
|
2023-08-10 15:23:13 -07:00
|
|
|
|
2026-07-17 08:56:01 +02:00
|
|
|
var usesAPISources: Bool {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 22:43:27 +01:00
|
|
|
private static let fallbackServerHostURLs = [
|
|
|
|
|
URL(string: "https://api-production.8-lives-cat.io")
|
|
|
|
|
]
|
2025-10-29 18:43:19 +01:00
|
|
|
|
2025-11-06 22:43:27 +01:00
|
|
|
var fallbackRelativePath: String? {
|
2025-04-25 14:00:13 +02:00
|
|
|
switch self {
|
2025-10-29 18:43:19 +01:00
|
|
|
case .getOfferings:
|
2025-11-06 22:43:27 +01:00
|
|
|
return "/v1/offerings"
|
2025-10-29 18:43:19 +01:00
|
|
|
case .getProductEntitlementMapping:
|
2025-11-06 22:43:27 +01:00
|
|
|
return "/v1/product_entitlement_mapping"
|
2025-04-25 14:00:13 +02:00
|
|
|
default:
|
2025-11-06 22:43:27 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fallbackUrls: [URL] {
|
|
|
|
|
guard let fallbackRelativePath = self.fallbackRelativePath else {
|
2025-04-25 14:00:13 +02:00
|
|
|
return []
|
|
|
|
|
}
|
2025-11-06 22:43:27 +01:00
|
|
|
|
|
|
|
|
return Self.fallbackServerHostURLs.compactMap { baseURL in
|
|
|
|
|
guard let baseURL = baseURL,
|
|
|
|
|
let fallbackUrl = URL(string: fallbackRelativePath, relativeTo: baseURL) else {
|
|
|
|
|
let errorMessage = "Invalid fallback URL configuration for path: \(self.name)"
|
|
|
|
|
assertionFailure(errorMessage)
|
|
|
|
|
Logger.error(errorMessage)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fallbackUrl
|
|
|
|
|
}
|
2025-04-25 14:00:13 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
var authenticated: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .getCustomerInfo,
|
|
|
|
|
.getOfferings,
|
|
|
|
|
.getIntroEligibility,
|
|
|
|
|
.logIn,
|
|
|
|
|
.postAttributionData,
|
|
|
|
|
.postOfferForSigning,
|
|
|
|
|
.postReceiptData,
|
|
|
|
|
.postSubscriberAttributes,
|
|
|
|
|
.postAdServicesToken,
|
2024-11-25 16:14:32 +01:00
|
|
|
.postRedeemWebPurchase,
|
2024-08-09 15:26:55 +02:00
|
|
|
.getProductEntitlementMapping,
|
2025-04-24 14:46:21 +01:00
|
|
|
.getCustomerCenterConfig,
|
2025-07-07 12:11:27 -05:00
|
|
|
.getVirtualCurrencies,
|
2026-08-27 16:14:19 -06:00
|
|
|
.spendVirtualCurrencies,
|
2025-11-19 08:26:43 +00:00
|
|
|
.appHealthReport,
|
2026-02-06 11:54:42 -06:00
|
|
|
.postCreateTicket,
|
2026-04-22 15:55:13 +02:00
|
|
|
.isPurchaseAllowedByRestoreBehavior,
|
2026-06-02 17:19:05 +02:00
|
|
|
.rewardVerificationStatus,
|
2026-08-21 14:04:22 -06:00
|
|
|
.remoteConfig,
|
|
|
|
|
.tokenLogin,
|
|
|
|
|
.tokenRefresh,
|
|
|
|
|
.tokenLogOut:
|
2023-08-10 15:23:13 -07:00
|
|
|
return true
|
|
|
|
|
|
2025-07-07 17:00:54 +01:00
|
|
|
case .health,
|
|
|
|
|
.appHealthReportAvailability:
|
2023-08-10 15:23:13 -07:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var shouldSendEtag: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .getCustomerInfo,
|
|
|
|
|
.getOfferings,
|
|
|
|
|
.getIntroEligibility,
|
|
|
|
|
.logIn,
|
|
|
|
|
.postAttributionData,
|
|
|
|
|
.postOfferForSigning,
|
|
|
|
|
.postReceiptData,
|
|
|
|
|
.postSubscriberAttributes,
|
|
|
|
|
.postAdServicesToken,
|
2024-11-25 16:14:32 +01:00
|
|
|
.postRedeemWebPurchase,
|
2024-08-09 15:26:55 +02:00
|
|
|
.getProductEntitlementMapping,
|
2025-04-24 14:46:21 +01:00
|
|
|
.getCustomerCenterConfig,
|
2025-07-07 12:11:27 -05:00
|
|
|
.getVirtualCurrencies,
|
2026-08-27 16:14:19 -06:00
|
|
|
.spendVirtualCurrencies,
|
2025-11-19 08:26:43 +00:00
|
|
|
.appHealthReport,
|
2026-02-06 11:54:42 -06:00
|
|
|
.postCreateTicket,
|
2026-04-22 15:55:13 +02:00
|
|
|
.isPurchaseAllowedByRestoreBehavior,
|
2026-08-21 14:04:22 -06:00
|
|
|
.rewardVerificationStatus,
|
|
|
|
|
.tokenLogin,
|
|
|
|
|
.tokenRefresh,
|
|
|
|
|
.tokenLogOut:
|
2023-08-10 15:23:13 -07:00
|
|
|
return true
|
2026-06-19 14:38:38 +02:00
|
|
|
case .remoteConfig,
|
|
|
|
|
.health,
|
2025-07-07 17:00:54 +01:00
|
|
|
.appHealthReportAvailability:
|
2023-08-10 15:23:13 -07:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var supportsSignatureVerification: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .getCustomerInfo,
|
|
|
|
|
.logIn,
|
|
|
|
|
.postReceiptData,
|
|
|
|
|
.health,
|
|
|
|
|
.getOfferings,
|
2025-04-24 14:46:21 +01:00
|
|
|
.getProductEntitlementMapping,
|
2025-07-07 12:11:27 -05:00
|
|
|
.getVirtualCurrencies,
|
2026-08-27 16:14:19 -06:00
|
|
|
.spendVirtualCurrencies,
|
2025-07-07 17:00:54 +01:00
|
|
|
.appHealthReport,
|
2026-02-06 11:54:42 -06:00
|
|
|
.appHealthReportAvailability,
|
2026-06-25 17:03:58 +02:00
|
|
|
.isPurchaseAllowedByRestoreBehavior,
|
|
|
|
|
.remoteConfig,
|
|
|
|
|
.rewardVerificationStatus:
|
2023-08-10 15:23:13 -07:00
|
|
|
return true
|
|
|
|
|
case .getIntroEligibility,
|
|
|
|
|
.postSubscriberAttributes,
|
|
|
|
|
.postAttributionData,
|
|
|
|
|
.postAdServicesToken,
|
2024-08-09 15:26:55 +02:00
|
|
|
.postOfferForSigning,
|
2024-11-25 16:14:32 +01:00
|
|
|
.postRedeemWebPurchase,
|
2025-11-19 08:26:43 +00:00
|
|
|
.getCustomerCenterConfig,
|
2026-08-21 14:04:22 -06:00
|
|
|
.postCreateTicket,
|
|
|
|
|
.tokenLogin,
|
|
|
|
|
.tokenRefresh,
|
|
|
|
|
.tokenLogOut:
|
2026-06-19 14:38:38 +02:00
|
|
|
return false
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var needsNonceForSigning: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .getCustomerInfo,
|
|
|
|
|
.logIn,
|
|
|
|
|
.postReceiptData,
|
2025-07-07 12:11:27 -05:00
|
|
|
.getVirtualCurrencies,
|
2026-08-27 16:14:19 -06:00
|
|
|
.spendVirtualCurrencies,
|
2025-07-07 17:00:54 +01:00
|
|
|
.health,
|
2026-02-06 11:54:42 -06:00
|
|
|
.appHealthReportAvailability,
|
2026-04-22 15:55:13 +02:00
|
|
|
.isPurchaseAllowedByRestoreBehavior,
|
2026-06-29 12:19:58 +02:00
|
|
|
.remoteConfig,
|
2026-08-21 14:04:22 -06:00
|
|
|
.rewardVerificationStatus,
|
|
|
|
|
.tokenLogin,
|
|
|
|
|
.tokenRefresh,
|
|
|
|
|
.tokenLogOut:
|
2023-08-10 15:23:13 -07:00
|
|
|
return true
|
2026-07-08 13:08:08 +02:00
|
|
|
case .getOfferings,
|
2023-08-10 15:23:13 -07:00
|
|
|
.getIntroEligibility,
|
|
|
|
|
.postSubscriberAttributes,
|
|
|
|
|
.postAttributionData,
|
|
|
|
|
.postAdServicesToken,
|
|
|
|
|
.postOfferForSigning,
|
2024-11-25 16:14:32 +01:00
|
|
|
.postRedeemWebPurchase,
|
2024-08-09 15:26:55 +02:00
|
|
|
.getProductEntitlementMapping,
|
2025-04-24 14:46:21 +01:00
|
|
|
.getCustomerCenterConfig,
|
2025-11-19 08:26:43 +00:00
|
|
|
.appHealthReport,
|
2026-06-19 14:38:38 +02:00
|
|
|
.postCreateTicket:
|
|
|
|
|
return false
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 17:03:58 +02:00
|
|
|
var responseSignatureContextProvider: ResponseSignatureContextProvider {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return RemoteConfigSignatureContextProvider()
|
|
|
|
|
default:
|
|
|
|
|
return DefaultResponseSignatureContextProvider()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-25 14:00:13 +02:00
|
|
|
var relativePath: String {
|
2026-08-21 14:04:21 -06:00
|
|
|
let component = self.pathComponent
|
|
|
|
|
if component.hasPrefix("/") { return component }
|
|
|
|
|
return "/v1/\(component)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var relativeIAMPath: String {
|
|
|
|
|
let component = self.iamPathComponent
|
|
|
|
|
if component.hasPrefix("/") { return component }
|
|
|
|
|
return "/v1/\(component)"
|
2025-04-25 14:00:13 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
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"
|
|
|
|
|
|
2025-04-24 14:46:21 +01:00
|
|
|
case let .appHealthReport(appUserID):
|
|
|
|
|
return "subscribers/\(Self.escape(appUserID))/health_report"
|
|
|
|
|
|
2025-07-07 17:00:54 +01:00
|
|
|
case let .appHealthReportAvailability(appUserID):
|
|
|
|
|
return "subscribers/\(Self.escape(appUserID))/health_report_availability"
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
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"
|
2024-04-10 13:06:05 +02:00
|
|
|
|
2024-08-09 15:26:55 +02:00
|
|
|
case let .getCustomerCenterConfig(appUserID):
|
|
|
|
|
return "customercenter/\(Self.escape(appUserID))"
|
|
|
|
|
|
2024-11-25 16:14:32 +01:00
|
|
|
case .postRedeemWebPurchase:
|
|
|
|
|
return "subscribers/redeem_purchase"
|
|
|
|
|
|
2025-07-07 12:11:27 -05:00
|
|
|
case let .getVirtualCurrencies(appUserID):
|
|
|
|
|
return "subscribers/\(Self.escape(appUserID))/virtual_currencies"
|
2025-11-19 08:26:43 +00:00
|
|
|
|
2026-08-27 16:14:19 -06:00
|
|
|
case .spendVirtualCurrencies:
|
|
|
|
|
assertionFailure("The .spendVirtualCurrencies endpoint is only allowed when IAM is enabled")
|
|
|
|
|
Logger.error("The .spendVirtualCurrencies endpoint is only allowed when IAM is enabled")
|
|
|
|
|
return "customer/virtual_currencies/spend"
|
|
|
|
|
|
2025-11-19 08:26:43 +00:00
|
|
|
case .postCreateTicket:
|
|
|
|
|
return "customercenter/support/create-ticket"
|
2026-02-06 11:54:42 -06:00
|
|
|
case let .isPurchaseAllowedByRestoreBehavior(appUserID):
|
|
|
|
|
return "subscribers/\(Self.escape(appUserID))/restore/eligibility"
|
2026-04-22 15:55:13 +02:00
|
|
|
|
2026-04-23 13:57:32 +02:00
|
|
|
case let .rewardVerificationStatus(appUserID, clientTransactionID):
|
|
|
|
|
return "subscribers/\(Self.escape(appUserID))/ads/reward_verifications/\(Self.escape(clientTransactionID))"
|
2026-06-02 17:19:05 +02:00
|
|
|
|
2026-06-29 13:25:55 +02:00
|
|
|
case let .remoteConfig(domain):
|
|
|
|
|
return "config/\(Self.escape(domain))"
|
2026-08-21 14:04:22 -06:00
|
|
|
|
|
|
|
|
case .tokenLogin:
|
|
|
|
|
return "/auth/login"
|
|
|
|
|
|
|
|
|
|
case .tokenRefresh:
|
|
|
|
|
return "/auth/token"
|
|
|
|
|
|
|
|
|
|
case .tokenLogOut:
|
|
|
|
|
return "/auth/revoke"
|
2024-04-10 13:06:05 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 14:04:21 -06:00
|
|
|
var iamPathComponent: String {
|
|
|
|
|
// NOTE: cases below that "return self.pathComponent" are indicating
|
|
|
|
|
// that the corresponding server paths have not yet been migrated to
|
|
|
|
|
// support access token authorization
|
|
|
|
|
switch self {
|
|
|
|
|
case .getCustomerInfo:
|
|
|
|
|
return "customer"
|
|
|
|
|
case .getOfferings:
|
|
|
|
|
return "customer/offerings"
|
|
|
|
|
case .getIntroEligibility:
|
|
|
|
|
return "customer/intro_eligibility"
|
|
|
|
|
case .logIn:
|
|
|
|
|
assertionFailure("The .login endpoint is not allowed when IAM is enabled")
|
|
|
|
|
Logger.error("The .login endpoint is not allowed when IAM is enabled")
|
|
|
|
|
return self.pathComponent
|
|
|
|
|
case .postAttributionData:
|
|
|
|
|
return "customer/attribution"
|
|
|
|
|
case .postOfferForSigning:
|
|
|
|
|
return "offers"
|
|
|
|
|
case .postReceiptData:
|
|
|
|
|
return "receipts"
|
|
|
|
|
case .postSubscriberAttributes:
|
|
|
|
|
return "customer/attributes"
|
|
|
|
|
case .postAdServicesToken:
|
|
|
|
|
return "customer/adservices_attribution"
|
|
|
|
|
case .health:
|
|
|
|
|
return self.pathComponent
|
|
|
|
|
case .appHealthReport:
|
|
|
|
|
return "customer/health_report"
|
|
|
|
|
case .appHealthReportAvailability:
|
|
|
|
|
return self.pathComponent
|
|
|
|
|
case .getProductEntitlementMapping:
|
|
|
|
|
return "product_entitlement_mapping"
|
|
|
|
|
case .getCustomerCenterConfig:
|
|
|
|
|
return "customer/customercenter"
|
|
|
|
|
case .getVirtualCurrencies:
|
|
|
|
|
return "customer/virtual_currencies"
|
2026-08-27 16:14:19 -06:00
|
|
|
case .spendVirtualCurrencies:
|
|
|
|
|
return "customer/virtual_currencies/spend"
|
2026-08-21 14:04:21 -06:00
|
|
|
case .postRedeemWebPurchase:
|
|
|
|
|
return self.pathComponent
|
|
|
|
|
case .postCreateTicket:
|
|
|
|
|
return self.pathComponent
|
|
|
|
|
case .isPurchaseAllowedByRestoreBehavior:
|
|
|
|
|
return "customer/restore/eligibility"
|
|
|
|
|
case .rewardVerificationStatus:
|
|
|
|
|
return self.pathComponent
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return self.pathComponent
|
2026-08-21 14:04:22 -06:00
|
|
|
case .tokenLogin:
|
|
|
|
|
return "/auth/login"
|
|
|
|
|
case .tokenRefresh:
|
|
|
|
|
return "/auth/token"
|
|
|
|
|
case .tokenLogOut:
|
|
|
|
|
return "/auth/revoke"
|
2026-08-21 14:04:21 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-10 13:06:05 +02:00
|
|
|
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"
|
|
|
|
|
|
2024-08-09 15:26:55 +02:00
|
|
|
case .getCustomerCenterConfig:
|
|
|
|
|
return "customer_center"
|
|
|
|
|
|
2024-11-25 16:14:32 +01:00
|
|
|
case .postRedeemWebPurchase:
|
|
|
|
|
return "post_redeem_web_purchase"
|
|
|
|
|
|
2025-04-24 14:46:21 +01:00
|
|
|
case .appHealthReport:
|
|
|
|
|
return "get_app_health_report"
|
|
|
|
|
|
2025-07-07 12:11:27 -05:00
|
|
|
case .getVirtualCurrencies:
|
|
|
|
|
return "get_virtual_currencies"
|
|
|
|
|
|
2026-08-27 16:14:19 -06:00
|
|
|
case .spendVirtualCurrencies:
|
|
|
|
|
return "spend_virtual_currencies"
|
|
|
|
|
|
2025-07-07 17:00:54 +01:00
|
|
|
case .appHealthReportAvailability:
|
|
|
|
|
return "get_app_health_report_availability"
|
|
|
|
|
|
2025-11-19 08:26:43 +00:00
|
|
|
case .postCreateTicket:
|
|
|
|
|
return "post_create_ticket"
|
2026-02-06 11:54:42 -06:00
|
|
|
case .isPurchaseAllowedByRestoreBehavior:
|
|
|
|
|
return "post_restore_eligibility"
|
2026-04-22 15:55:13 +02:00
|
|
|
|
2026-04-23 13:57:32 +02:00
|
|
|
case .rewardVerificationStatus:
|
|
|
|
|
return "get_reward_verification_status"
|
2026-06-02 17:19:05 +02:00
|
|
|
|
2026-06-19 14:38:38 +02:00
|
|
|
case .remoteConfig:
|
|
|
|
|
return "remote_config"
|
2026-08-21 14:04:22 -06:00
|
|
|
|
|
|
|
|
case .tokenLogin:
|
|
|
|
|
return "token_login"
|
|
|
|
|
|
|
|
|
|
case .tokenRefresh:
|
|
|
|
|
return "token_refresh"
|
|
|
|
|
|
|
|
|
|
case .tokenLogOut:
|
|
|
|
|
return "token_logout"
|
2026-06-19 14:38:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var additionalHeaders: HTTPRequest.Headers {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
2026-07-01 11:58:03 +02:00
|
|
|
return [
|
|
|
|
|
HTTPClient.RequestHeader.accept.rawValue: HTTPClient.rcContainerFormatAcceptHeaderValue,
|
|
|
|
|
HTTPClient.RequestHeader.acceptRCElementEncoding.rawValue:
|
|
|
|
|
HTTPClient.rcContainerFormatElementEncodingHeaderValue
|
|
|
|
|
]
|
2026-06-19 14:38:38 +02:00
|
|
|
default:
|
|
|
|
|
return [:]
|
2023-08-10 15:23:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-21 14:04:22 -06:00
|
|
|
var isIAMPath: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .tokenLogin, .tokenRefresh, .tokenLogOut:
|
|
|
|
|
return true
|
|
|
|
|
default: return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 15:23:13 -07:00
|
|
|
private static func escape(_ appUserID: String) -> String {
|
|
|
|
|
return appUserID.trimmedAndEscaped
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-10 14:17:31 +02:00
|
|
|
|
|
|
|
|
extension HTTPRequest.FallbackPath: HTTPRequestPath {
|
|
|
|
|
|
|
|
|
|
// swiftlint:disable:next force_unwrapping
|
|
|
|
|
static let serverHostURL = URL(string: "https://api-production.8-lives-cat.io")!
|
|
|
|
|
|
2026-08-03 14:51:50 +02:00
|
|
|
var isFallbackHostPath: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 14:17:31 +02:00
|
|
|
var authenticated: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var shouldSendEtag: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var supportsSignatureVerification: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var needsNonceForSigning: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var responseSignatureContextProvider: ResponseSignatureContextProvider {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return FallbackSignatureContextProvider()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var relativePath: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case let .remoteConfig(domain):
|
|
|
|
|
return "/v1/config/\(domain.trimmedAndEscaped)"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var name: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return "remote_config_fallback"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var additionalHeaders: HTTPRequest.Headers {
|
|
|
|
|
switch self {
|
|
|
|
|
case .remoteConfig:
|
|
|
|
|
return [:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|