mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
* Add AdMob SSV poll endpoint and `pollAdMobSSVStatus` SPI * Trim verbose AdMob SSV transport comments. Align AdsAPI and cache-key inline documentation with existing repository comment density while preserving behavior. * Trim pollAdMobSSVStatus SPI documentation. Reduce prescriptive guidance in the internal API comment while keeping the key cancellation behavior note. * [skip ci] Generating new test snapshots (#6648) * [skip ci] Generating new test snapshots (#6647) * [skip ci] Generating new test snapshots (#6646) * [skip ci] Generating new test snapshots (#6645) * [skip ci] Generating new test snapshots (#6644) * [skip ci] Generating new test snapshots (#6642) * [skip ci] Generating new test snapshots (#6643) * Trigger CI for PR 6641 checks. Add a non-skip commit so required CircleCI statuses are reported on the current head. * Validate non-empty client transaction ID for AdMob SSV polling. Guard against empty client transaction IDs in the SPI and backend operation and add focused backend tests for the new error path. * [skip ci] Generating new test snapshots (#6652) * [skip ci] Generating new test snapshots (#6653) * Trigger CI for PR 6641 checks. Add a non-skip commit so required CircleCI statuses are reported on the current head. * Enable signature verification for AdMob SSV status path. Update HTTP request path tests to cover AdMob SSV signature and nonce behavior. * fix: address review — move AdMob SPI extension to dedicated file * fix: address review — fail fast when AdMob status mock is unstubbed * fix: address review — dedupe client transaction validation in backend operation * fix: address review — use enum for AdMob SSV poll status * fix: address review — allow extracted Purchases extensions to access backend * fix: address review — log unknown AdMob SSV statuses * fix: address review — add Purchases AdMob SSV mapping tests * fix: address review — map missing client transaction ID to configuration error * fix: address review — keep backend private by co-locating AdMob extension * fix: address review — remove unused raw-value conformance * fix: address review — assert warning log for unknown AdMob status * fix: address review — surface unknown AdMob SSV status to adapters * fix: address review — enable eTag support for AdMob SSV status path --------- Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
329 lines
13 KiB
Swift
329 lines
13 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
|
|
//
|
|
// BackendError.swift
|
|
//
|
|
// Created by Nacho Soto on 4/7/22.
|
|
|
|
// swiftlint:disable multiline_parameters
|
|
|
|
import Foundation
|
|
|
|
/// An `Error` produced by ``Backend``.
|
|
enum BackendError: Error, Equatable {
|
|
|
|
case networkError(NetworkError)
|
|
case missingAppUserID(Source)
|
|
case missingClientTransactionID(Source)
|
|
case emptySubscriberAttributes(Source)
|
|
case missingReceiptFile(URL?, Source)
|
|
case missingTransactionProductIdentifier(Source)
|
|
case missingCachedCustomerInfo(Source)
|
|
case invalidAppleSubscriptionKey(Source)
|
|
case unexpectedBackendResponse(UnexpectedBackendResponseError, extraContext: String?, Source)
|
|
case invalidWebRedemptionToken
|
|
case purchaseBelongsToOtherUser
|
|
case expiredWebRedemptionToken(obfuscatedEmail: String)
|
|
case unsupportedInUIPreviewMode(Source)
|
|
case missingTransactionJWS(Source)
|
|
|
|
}
|
|
|
|
extension BackendError {
|
|
|
|
static func missingAppUserID(
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .missingAppUserID(.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func missingClientTransactionID(
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .missingClientTransactionID(.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func missingTransactionJWS(
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .missingTransactionJWS(.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func emptySubscriberAttributes(
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .emptySubscriberAttributes(.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func missingTransactionProductIdentifier(
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .missingTransactionProductIdentifier(.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func missingReceiptFile(
|
|
_ receiptURL: URL?,
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .missingReceiptFile(receiptURL, .init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func missingCachedCustomerInfo(
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .missingCachedCustomerInfo(.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func unexpectedBackendResponse(
|
|
_ error: UnexpectedBackendResponseError,
|
|
extraContext: String? = nil,
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .unexpectedBackendResponse(error,
|
|
extraContext: extraContext,
|
|
.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
static func unsupportedInUIPreviewMode(
|
|
file: String = #fileID, function: String = #function, line: UInt = #line
|
|
) -> Self {
|
|
return .unsupportedInUIPreviewMode(.init(file: file, function: function, line: line))
|
|
}
|
|
|
|
}
|
|
|
|
extension BackendError: PurchasesErrorConvertible {
|
|
|
|
var asPurchasesError: PurchasesError {
|
|
switch self {
|
|
case let .networkError(error):
|
|
return error.asPurchasesError
|
|
|
|
case let .missingAppUserID(source):
|
|
return ErrorUtils.missingAppUserIDError(fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
|
|
case let .missingClientTransactionID(source):
|
|
return ErrorUtils.configurationError(message: "Missing client transaction ID.",
|
|
fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
|
|
case let .missingTransactionJWS(source):
|
|
return ErrorUtils.storeProblemError(fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
|
|
case let .emptySubscriberAttributes(source):
|
|
return ErrorUtils.emptySubscriberAttributesError(fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
|
|
case let .missingReceiptFile(receiptURL, source):
|
|
return ErrorUtils.missingReceiptFileError(receiptURL,
|
|
fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
|
|
case let .missingTransactionProductIdentifier(source):
|
|
return ErrorUtils.unknownError(
|
|
message: Strings.purchase.skpayment_missing_product_identifier.description,
|
|
fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line
|
|
)
|
|
|
|
case let .missingCachedCustomerInfo(source):
|
|
return ErrorUtils.customerInfoError(withMessage: Strings.purchase.missing_cached_customer_info.description,
|
|
fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
|
|
case let .unexpectedBackendResponse(error, extraContext, source):
|
|
return ErrorUtils.unexpectedBackendResponse(withSubError: error,
|
|
extraContext: extraContext,
|
|
fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
case let .invalidAppleSubscriptionKey(source):
|
|
return ErrorUtils.configurationError(
|
|
message: ErrorCode.invalidAppleSubscriptionKeyError.description,
|
|
fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line
|
|
)
|
|
case .invalidWebRedemptionToken:
|
|
let code = BackendErrorCode.invalidWebRedemptionToken
|
|
return ErrorUtils.backendError(withBackendCode: code,
|
|
originalBackendErrorCode: code.rawValue)
|
|
case .purchaseBelongsToOtherUser:
|
|
let code = BackendErrorCode.purchaseBelongsToOtherUser
|
|
return ErrorUtils.backendError(withBackendCode: code,
|
|
originalBackendErrorCode: code.rawValue)
|
|
case let .expiredWebRedemptionToken(obfuscatedEmail):
|
|
let code = BackendErrorCode.expiredWebRedemptionToken
|
|
return ErrorUtils.backendError(withBackendCode: code,
|
|
originalBackendErrorCode: code.rawValue,
|
|
extraUserInfo: [
|
|
.obfuscatedEmail: obfuscatedEmail
|
|
])
|
|
case let .unsupportedInUIPreviewMode(source):
|
|
return ErrorUtils.unsupportedInUIPreviewModeError(fileName: source.file,
|
|
functionName: source.function,
|
|
line: source.line)
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension BackendError: DescribableError { }
|
|
|
|
extension BackendError {
|
|
|
|
/// Whether the operation producing this error actually synced the data.
|
|
var successfullySynced: Bool {
|
|
return self.networkError?.successfullySynced ?? false
|
|
}
|
|
|
|
/// Whether the operation producing this error can be completed.
|
|
/// If `false`, the underlying error was fatal.
|
|
var finishable: Bool {
|
|
return self.networkError?.finishable ?? false
|
|
}
|
|
|
|
/// Whether the error represents a `NetworkError` from the server being down.
|
|
var isServerDown: Bool {
|
|
return self.networkError?.isServerDown == true
|
|
}
|
|
|
|
private var networkError: NetworkError? {
|
|
switch self {
|
|
case let .networkError(networkError):
|
|
return networkError
|
|
|
|
case .missingAppUserID,
|
|
.missingClientTransactionID,
|
|
.emptySubscriberAttributes,
|
|
.missingReceiptFile,
|
|
.invalidAppleSubscriptionKey,
|
|
.missingTransactionProductIdentifier,
|
|
.missingCachedCustomerInfo,
|
|
.unexpectedBackendResponse,
|
|
.invalidWebRedemptionToken,
|
|
.purchaseBelongsToOtherUser,
|
|
.expiredWebRedemptionToken,
|
|
.unsupportedInUIPreviewMode,
|
|
.missingTransactionJWS:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension BackendError {
|
|
|
|
var underlyingError: Error? {
|
|
switch self {
|
|
case let .networkError(error):
|
|
return error
|
|
|
|
case .missingAppUserID,
|
|
.missingClientTransactionID,
|
|
.emptySubscriberAttributes,
|
|
.missingReceiptFile,
|
|
.invalidAppleSubscriptionKey,
|
|
.missingTransactionProductIdentifier,
|
|
.missingCachedCustomerInfo,
|
|
.invalidWebRedemptionToken,
|
|
.purchaseBelongsToOtherUser,
|
|
.expiredWebRedemptionToken,
|
|
.unsupportedInUIPreviewMode,
|
|
.missingTransactionJWS:
|
|
return nil
|
|
|
|
case let .unexpectedBackendResponse(error, _, _):
|
|
return error
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension BackendError {
|
|
|
|
enum UnexpectedBackendResponseError: Error, Equatable {
|
|
|
|
/// Login call failed due to a problem with the response.
|
|
case loginResponseDecoding
|
|
|
|
/// Received a bad response after posting an offer- "offers" couldn't be read from response.
|
|
case postOfferIdBadResponse
|
|
|
|
/// Received a bad response after posting an offer- "offers" was totally missing.
|
|
case postOfferIdMissingOffersInResponse
|
|
|
|
/// Received a bad response after posting an offer- there was an issue with the signature.
|
|
case postOfferIdSignature
|
|
|
|
/// getOffer call failed with an invalid response.
|
|
case getOfferUnexpectedResponse
|
|
|
|
/// A call that is supposed to retrieve a CustomerInfo failed because the CustomerInfo in the response was nil.
|
|
case customerInfoNil
|
|
|
|
/// A call that is supposed to retrieve a CustomerInfo failed because the json object couldn't be parsed.
|
|
case customerInfoResponseParsing(error: NSError, json: String)
|
|
}
|
|
|
|
}
|
|
|
|
extension BackendError.UnexpectedBackendResponseError: DescribableError {
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .loginResponseDecoding:
|
|
return "Unable to decode response returned from login."
|
|
case .postOfferIdBadResponse:
|
|
return "Unable to decode response returned from posting offer for signing."
|
|
case .postOfferIdMissingOffersInResponse:
|
|
return "Missing offers from response returned from posting offer for signing."
|
|
case .postOfferIdSignature:
|
|
return "Signature error encountered in response returned from posting offer for signing."
|
|
case .getOfferUnexpectedResponse:
|
|
return "Unknown error encountered while getting offerings."
|
|
case .customerInfoNil:
|
|
return "Unable to instantiate a CustomerInfoResponse, CustomerInfo in response was nil."
|
|
case .customerInfoResponseParsing:
|
|
return "Unable to instantiate a CustomerInfoResponse due to malformed json."
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension BackendError {
|
|
|
|
typealias Source = ErrorSource
|
|
|
|
}
|
|
|
|
extension BackendError {
|
|
|
|
/// Whether to fall back to cached offerings in case of this error when fetching offerings.
|
|
var shouldFallBackToCachedOfferings: Bool {
|
|
switch self {
|
|
case .networkError(let networkError):
|
|
return networkError.shouldFallBackToCachedOfferings
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
}
|