mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
* Rename internal AdMobSSV symbols, URL and metric to RewardVerification Renames the internal AdMobSSV* types and SPI introduced in #6641 to RewardVerification* so the wording stays adapter-agnostic. Also renames the backend URL path and the client-side metric to match, so the SPI is provider-agnostic end to end. Renames: - Sources/Ads/AdMobSSV/ -> Sources/Ads/RewardVerification/ - AdMobSSVPollStatus -> RewardVerificationPollStatus - RewardVerificationPollStatus.validated -> .verified - AdMobSSVStatusResponse -> RewardVerificationStatusResponse - RewardVerificationStatusResponse.Status.validated -> .verified (the decoder still accepts the legacy "validated" wire value during the rollout window; covered by a new decoder unit test) - AdMobSSVStatusCallback -> RewardVerificationStatusCallback - GetAdMobSSVStatusOperation -> GetRewardVerificationStatusOperation - AdsAPI.getAdMobSSVStatus -> getRewardVerificationStatus - Purchases.pollAdMobSSVStatus -> pollRewardVerificationStatus - HTTPRequest.Path.adMobSSVStatus -> .rewardVerificationStatus - URL path: /ads/admob/ssv/{tx} -> /ads/reward_verifications/{tx} - Metric name: get_admob_ssv_status -> get_reward_verification_status - BackendErrorStrings.unknown_admob_ssv_status -> .unknown_reward_verification_status - Test files, test methods and snapshot artifacts renamed to match. - Xcode project file references updated in lockstep. * Drop legacy `validated` wire-value compat in RewardVerification decoder The `RewardVerificationStatusResponse` decoder was accepting both `"verified"` and the legacy `"validated"` status from the backend as a backwards-compat hedge during rollout. The endpoint has not been shipped and the SDK has no public entry point that polls it yet, so there is no compatibility window to preserve — the decoder can require `"verified"` outright. While here, simplify the decoder to look up known cases via `Status(rawValue:)` instead of a string-literal switch, so adding a future case can't silently drift from the enum. Removes the corresponding `testDecodesLegacyValidatedWireValueAsVerified` test. Existing coverage for `verified`, `pending`, `failed`, and the `unknown` fallback (with the `Logger.warn` assertion) is unchanged. * Update Sources/Purchasing/Purchases/Purchases.swift Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com> * test: assert warning is logged when decoding literal "unknown" status Address review feedback: cover the case where the backend sends the literal "unknown" wire value, which still falls through the unmapped-status branch and should emit a warning. --------- Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
423 lines
12 KiB
Swift
423 lines
12 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.
|
|
|
|
// swiftlint:disable file_length
|
|
|
|
import Foundation
|
|
|
|
protocol HTTPRequestPath {
|
|
|
|
/// The base URL for requests to this path.
|
|
static var serverHostURL: URL { get }
|
|
|
|
/// The fallback URLs to use when the main server is down.
|
|
///
|
|
/// Not all endpoints have a fallback URL, but some do.
|
|
var fallbackUrls: [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 }
|
|
|
|
/// The fallback relative path for this endpoint, if any.
|
|
var fallbackRelativePath: String? { get }
|
|
}
|
|
|
|
extension HTTPRequestPath {
|
|
|
|
var fallbackUrls: [URL] {
|
|
return []
|
|
}
|
|
|
|
var supportsFallbackURLs: Bool {
|
|
!fallbackUrls.isEmpty
|
|
}
|
|
|
|
var fallbackRelativePath: String? {
|
|
return nil
|
|
}
|
|
|
|
var url: URL? { return self.url(proxyURL: nil) }
|
|
|
|
func url(proxyURL: URL? = nil, fallbackUrlIndex: Int? = nil) -> URL? {
|
|
let baseURL: URL
|
|
if let proxyURL {
|
|
// When a Proxy URL is set, we don't support fallback URLs
|
|
guard fallbackUrlIndex == nil else {
|
|
// This is to safe guard against a potential infinite loop if the caller mistakenly
|
|
// passes both a proxyURL and a fallbackUrlIndex.
|
|
return nil
|
|
}
|
|
baseURL = proxyURL
|
|
} else if let fallbackUrlIndex {
|
|
return self.fallbackUrls[safe: fallbackUrlIndex]
|
|
} 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 getWorkflow(appUserID: String, workflowId: String)
|
|
case postRedeemWebPurchase
|
|
case postCreateTicket
|
|
case isPurchaseAllowedByRestoreBehavior(appUserID: String)
|
|
case rewardVerificationStatus(appUserID: String, clientTransactionID: String)
|
|
|
|
}
|
|
|
|
enum FeatureEventsPath: 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 {
|
|
|
|
static var serverHostURL: URL {
|
|
SystemInfo.apiBaseURL
|
|
}
|
|
|
|
private static let fallbackServerHostURLs = [
|
|
URL(string: "https://api-production.8-lives-cat.io")
|
|
]
|
|
|
|
var fallbackRelativePath: String? {
|
|
switch self {
|
|
case .getOfferings:
|
|
return "/v1/offerings"
|
|
case .getProductEntitlementMapping:
|
|
return "/v1/product_entitlement_mapping"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var fallbackUrls: [URL] {
|
|
guard let fallbackRelativePath = self.fallbackRelativePath else {
|
|
return []
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
var authenticated: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.getOfferings,
|
|
.getWorkflow,
|
|
.getIntroEligibility,
|
|
.logIn,
|
|
.postAttributionData,
|
|
.postOfferForSigning,
|
|
.postReceiptData,
|
|
.postSubscriberAttributes,
|
|
.postAdServicesToken,
|
|
.postRedeemWebPurchase,
|
|
.getProductEntitlementMapping,
|
|
.getCustomerCenterConfig,
|
|
.getVirtualCurrencies,
|
|
.appHealthReport,
|
|
.postCreateTicket,
|
|
.isPurchaseAllowedByRestoreBehavior,
|
|
.rewardVerificationStatus:
|
|
return true
|
|
|
|
case .health,
|
|
.appHealthReportAvailability:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var shouldSendEtag: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.getOfferings,
|
|
.getWorkflow,
|
|
.getIntroEligibility,
|
|
.logIn,
|
|
.postAttributionData,
|
|
.postOfferForSigning,
|
|
.postReceiptData,
|
|
.postSubscriberAttributes,
|
|
.postAdServicesToken,
|
|
.postRedeemWebPurchase,
|
|
.getProductEntitlementMapping,
|
|
.getCustomerCenterConfig,
|
|
.getVirtualCurrencies,
|
|
.appHealthReport,
|
|
.postCreateTicket,
|
|
.isPurchaseAllowedByRestoreBehavior,
|
|
.rewardVerificationStatus:
|
|
return true
|
|
case .health,
|
|
.appHealthReportAvailability:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var supportsSignatureVerification: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.logIn,
|
|
.postReceiptData,
|
|
.health,
|
|
.getOfferings,
|
|
.getProductEntitlementMapping,
|
|
.getVirtualCurrencies,
|
|
.getWorkflow,
|
|
.appHealthReport,
|
|
.appHealthReportAvailability,
|
|
.isPurchaseAllowedByRestoreBehavior:
|
|
return true
|
|
case .getIntroEligibility,
|
|
.postSubscriberAttributes,
|
|
.postAttributionData,
|
|
.postAdServicesToken,
|
|
.postOfferForSigning,
|
|
.postRedeemWebPurchase,
|
|
.getCustomerCenterConfig,
|
|
.postCreateTicket:
|
|
return false
|
|
case .rewardVerificationStatus:
|
|
return true
|
|
}
|
|
}
|
|
|
|
var needsNonceForSigning: Bool {
|
|
switch self {
|
|
case .getCustomerInfo,
|
|
.logIn,
|
|
.postReceiptData,
|
|
.getVirtualCurrencies,
|
|
.health,
|
|
.appHealthReportAvailability,
|
|
.isPurchaseAllowedByRestoreBehavior,
|
|
.rewardVerificationStatus:
|
|
return true
|
|
case .getWorkflow,
|
|
.getOfferings,
|
|
.getIntroEligibility,
|
|
.postSubscriberAttributes,
|
|
.postAttributionData,
|
|
.postAdServicesToken,
|
|
.postOfferForSigning,
|
|
.postRedeemWebPurchase,
|
|
.getProductEntitlementMapping,
|
|
.getCustomerCenterConfig,
|
|
.appHealthReport,
|
|
.postCreateTicket:
|
|
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"
|
|
|
|
case let .getWorkflow(appUserID, workflowId):
|
|
return "subscribers/\(Self.escape(appUserID))/workflows/\(Self.escape(workflowId))"
|
|
|
|
case .postCreateTicket:
|
|
return "customercenter/support/create-ticket"
|
|
case let .isPurchaseAllowedByRestoreBehavior(appUserID):
|
|
return "subscribers/\(Self.escape(appUserID))/restore/eligibility"
|
|
|
|
case let .rewardVerificationStatus(appUserID, clientTransactionID):
|
|
return "subscribers/\(Self.escape(appUserID))/ads/reward_verifications/\(Self.escape(clientTransactionID))"
|
|
}
|
|
}
|
|
|
|
var name: String {
|
|
switch self {
|
|
case .getCustomerInfo:
|
|
return "get_customer"
|
|
|
|
case .getOfferings:
|
|
return "get_offerings"
|
|
|
|
case .getWorkflow:
|
|
return "get_workflow"
|
|
|
|
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"
|
|
|
|
case .postCreateTicket:
|
|
return "post_create_ticket"
|
|
case .isPurchaseAllowedByRestoreBehavior:
|
|
return "post_restore_eligibility"
|
|
|
|
case .rewardVerificationStatus:
|
|
return "get_reward_verification_status"
|
|
}
|
|
}
|
|
|
|
private static func escape(_ appUserID: String) -> String {
|
|
return appUserID.trimmedAndEscaped
|
|
}
|
|
}
|