Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions Sources/Caching/DeviceCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class DeviceCache {
private let sandboxEnvironmentDetector: SandboxEnvironmentDetector
private let userDefaults: SynchronizedUserDefaults
private let offeringsCachedObject: InMemoryCachedObject<Offerings>
private let stripeOfferingsCachedObject = InMemoryCachedObject<Offerings>()

private let _cachedAppUserID: Atomic<String?>
private let _cachedLegacyAppUserID: Atomic<String?>
Expand Down Expand Up @@ -87,7 +88,9 @@ class DeviceCache {

// Clear offerings cache.
self.offeringsCachedObject.clearCache()
self.stripeOfferingsCachedObject.clearCache()
userDefaults.removeObject(forKey: CacheKey.offerings(oldAppUserID))
userDefaults.removeObject(forKey: CacheKey.stripeOfferings(oldAppUserID))

// Delete attributes if synced for the old app user id.
if Self.unsyncedAttributesByKey(userDefaults, appUserID: oldAppUserID).isEmpty {
Expand Down Expand Up @@ -155,39 +158,54 @@ class DeviceCache {

// MARK: - Offerings

func cachedOfferingsResponseData(appUserID: String) -> Data? {
private func offeringsCache(includeStripeProducts: Bool) -> InMemoryCachedObject<Offerings> {
includeStripeProducts ? self.stripeOfferingsCachedObject : self.offeringsCachedObject
}

func cachedOfferings(includeStripeProducts: Bool) -> Offerings? {
self.offeringsCache(includeStripeProducts: includeStripeProducts).cachedInstance
}

private func offeringsCacheKey(appUserID: String, includeStripeProducts: Bool) -> CacheKey {
includeStripeProducts ? .stripeOfferings(appUserID) : .offerings(appUserID)
}

func cachedOfferingsResponseData(appUserID: String, includeStripeProducts: Bool = false) -> Data? {
return self.userDefaults.read {
$0.data(forKey: CacheKey.offerings(appUserID))
$0.data(forKey: self.offeringsCacheKey(appUserID: appUserID, includeStripeProducts: includeStripeProducts))
}
}

func cache(offerings: Offerings, appUserID: String) {
self.cacheInMemory(offerings: offerings)
func cache(offerings: Offerings, appUserID: String, includeStripeProducts: Bool = false) {
self.cacheInMemory(offerings: offerings, includeStripeProducts: includeStripeProducts)
self.userDefaults.write {
$0.set(codable: offerings.response, forKey: CacheKey.offerings(appUserID))
$0.set(codable: offerings.response,
forKey: self.offeringsCacheKey(appUserID: appUserID, includeStripeProducts: includeStripeProducts))
}
}

func cacheInMemory(offerings: Offerings) {
self.offeringsCachedObject.cache(instance: offerings)
func cacheInMemory(offerings: Offerings, includeStripeProducts: Bool = false) {
self.offeringsCache(includeStripeProducts: includeStripeProducts).cache(instance: offerings)
}

func clearOfferingsCache(appUserID: String) {
self.offeringsCachedObject.clearCache()
self.stripeOfferingsCachedObject.clearCache()
self.userDefaults.write {
$0.removeObject(forKey: CacheKey.offerings(appUserID))
$0.removeObject(forKey: CacheKey.stripeOfferings(appUserID))
}
}

func isOfferingsCacheStale(isAppBackgrounded: Bool) -> Bool {
return self.offeringsCachedObject.isCacheStale(
func isOfferingsCacheStale(isAppBackgrounded: Bool, includeStripeProducts: Bool = false) -> Bool {
return self.offeringsCache(includeStripeProducts: includeStripeProducts).isCacheStale(
durationInSeconds: self.cacheDurationInSeconds(isAppBackgrounded: isAppBackgrounded,
isSandbox: self.sandboxEnvironmentDetector.isSandbox)
)
}

func clearOfferingsCacheTimestamp() {
self.offeringsCachedObject.clearCacheTimestamp()
func clearOfferingsCacheTimestamp(includeStripeProducts: Bool = false) {
self.offeringsCache(includeStripeProducts: includeStripeProducts).clearCacheTimestamp()
}

// MARK: - subscriber attributes
Expand Down Expand Up @@ -388,6 +406,7 @@ class DeviceCache {
case customerInfo(String)
case customerInfoLastUpdated(String)
case offerings(String)
case stripeOfferings(String)
case legacySubscriberAttributes(String)
case attributionDataDefaults(String)
case syncedSK2ObserverModeTransactionIDs
Expand All @@ -396,6 +415,7 @@ class DeviceCache {
switch self {
case let .customerInfo(userID): return "\(Self.base)purchaserInfo.\(userID)"
case let .customerInfoLastUpdated(userID): return "\(Self.base)purchaserInfoLastUpdated.\(userID)"
case let .stripeOfferings(userID): return "\(Self.base)offerings.ios-stripe.\(userID)"
case let .offerings(userID): return "\(Self.base)offerings.\(userID)"
case let .legacySubscriberAttributes(userID): return "\(Self.legacySubscriberAttributesBase)\(userID)"
case let .attributionDataDefaults(userID): return "\(Self.base)attribution.\(userID)"
Expand Down
5 changes: 4 additions & 1 deletion Sources/Networking/HTTPClient/ETagManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ extension ETagManager {

// Visible for tests
static func cacheKey(for request: URLRequest) -> String? {
return request.url?.absoluteString
guard let url = request.url?.absoluteString else { return nil }
guard let platforms = request.value(forHTTPHeaderField: "x-supported-platforms") else { return url }
let appType = request.value(forHTTPHeaderField: "x-app-type") ?? ""
return "\(url)|app-type=\(appType)|platforms=\(platforms)"
}

}
Expand Down
16 changes: 10 additions & 6 deletions Sources/Networking/HTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -588,20 +588,24 @@ private extension HTTPClient {
) async throws -> HTTPClient.RequestHeaders {
var headers = request.headers

if isProxyRequest, let proxyAuthenticationHeadersProvider {
headers.mergeAdditionalHTTPHeaders(try await proxyAuthenticationHeadersProvider())
}
headers.mergeAdditionalHTTPHeaders(jwtManager.jwtHeader())
headers.mergeAdditionalHTTPHeaders(request.httpRequest.additionalHeaders)

if request.httpRequest.path.shouldSendEtag {
// Use the same platform headers for cache lookup and response storage.
var cacheRequest = urlRequest
cacheRequest.allHTTPHeaderFields = headers
let eTagHeader = self.eTagManager.eTagHeader(
for: urlRequest,
for: cacheRequest,
withSignatureVerification: request.verificationMode.isEnabled,
refreshETag: request.retried
)
headers.merge(eTagHeader)
}

if isProxyRequest, let proxyAuthenticationHeadersProvider {
headers.mergeAdditionalHTTPHeaders(try await proxyAuthenticationHeadersProvider())
}

headers.mergeAdditionalHTTPHeaders(jwtManager.jwtHeader())
return headers
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/Networking/HTTPClient/HTTPRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct HTTPRequest {
var nonce: Data?
/// Whether or not this request should be retried by the HTTPClient for certain status codes.
var isRetryable: Bool
/// Headers scoped to this request, including the requested ISI product platforms.
var additionalHeaders: Headers = [:]

init(
method: Method,
Expand Down
4 changes: 3 additions & 1 deletion Sources/Networking/OfferingsAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ class OfferingsAPI {

func getOfferings(appUserID: String,
isAppBackgrounded: Bool,
includeStripeProducts: Bool = false,
completion: @escaping OfferingsResponseHandler) {
let config = NetworkOperation.UserSpecificConfiguration(httpClient: self.backendConfig.httpClient,
appUserID: appUserID)
let factory = GetOfferingsOperation.createFactory(
configuration: config,
offeringsCallbackCache: self.offeringsCallbacksCache
offeringsCallbackCache: self.offeringsCallbacksCache,
includeStripeProducts: includeStripeProducts
)

let offeringsCallback = OfferingsCallback(cacheKey: factory.cacheKey, completion: completion)
Expand Down
18 changes: 13 additions & 5 deletions Sources/Networking/Operations/GetOfferingsOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,30 @@ final class GetOfferingsOperation: CacheableNetworkOperation {

private let offeringsCallbackCache: CallbackCache<OfferingsCallback>
private let configuration: AppUserConfiguration
private let includeStripeProducts: Bool

static func createFactory(
configuration: UserSpecificConfiguration,
offeringsCallbackCache: CallbackCache<OfferingsCallback>
offeringsCallbackCache: CallbackCache<OfferingsCallback>,
includeStripeProducts: Bool = false
) -> CacheableNetworkOperationFactory<GetOfferingsOperation> {
return .init({ cacheKey in
.init(
configuration: configuration,
offeringsCallbackCache: offeringsCallbackCache,
cacheKey: cacheKey
cacheKey: cacheKey,
includeStripeProducts: includeStripeProducts
)
},
individualizedCacheKeyPart: configuration.appUserID)
individualizedCacheKeyPart: configuration.appUserID + (includeStripeProducts ? ":ios,stripe" : ""))
}

private init(configuration: UserSpecificConfiguration,
offeringsCallbackCache: CallbackCache<OfferingsCallback>,
cacheKey: String) {
cacheKey: String,
includeStripeProducts: Bool) {
self.configuration = configuration
self.includeStripeProducts = includeStripeProducts
self.offeringsCallbackCache = offeringsCallbackCache

super.init(configuration: configuration, cacheKey: cacheKey)
Expand Down Expand Up @@ -64,7 +69,10 @@ private extension GetOfferingsOperation {
return
}

let request = HTTPRequest(method: .get, path: .getOfferings(appUserID: appUserID))
var request = HTTPRequest(method: .get, path: .getOfferings(appUserID: appUserID))
if self.includeStripeProducts {
request.additionalHeaders = ["x-app-type": "ios", "x-supported-platforms": "ios,stripe"]
}

httpClient.perform(request) { (response: VerifiedHTTPResponse<OfferingsResponse>.Result) in
defer {
Expand Down
19 changes: 19 additions & 0 deletions Sources/Networking/Responses/OfferingsResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ struct OfferingsResponse {
let identifier: String
let platformProductIdentifier: String
let planKey: String?
var platform: String? = nil

var isAppStoreProduct: Bool {
self.platform == nil || self.platform?.lowercased() == "ios"
}

}

Expand Down Expand Up @@ -59,6 +64,7 @@ extension OfferingsResponse {
self.offerings
.lazy
.flatMap { $0.packages }
.filter { $0.isAppStoreProduct }
.map { $0.platformProductIdentifier }
)
}
Expand All @@ -72,3 +78,16 @@ extension OfferingsResponse.Targeting: Codable, Equatable {}
extension OfferingsResponse: Codable, Equatable {}

extension OfferingsResponse: HTTPResponseBody {}

extension OfferingsResponse.Offering {

/// Pairs products only within this offering. Ambiguous matches are not purchasable through Stripe.
func stripeProductIdentifier(for package: Package) -> String? {
let identifiers = Set(self.packages.filter {
$0.platform?.lowercased() == "stripe" && $0.identifier == package.identifier &&
($0.planKey == nil || package.planKey == nil || $0.planKey == package.planKey)
}.map { $0.platformProductIdentifier }.filter { !$0.isEmpty })
return identifiers.count == 1 ? identifiers.first : nil
}

}
3 changes: 2 additions & 1 deletion Sources/Purchasing/Offerings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ private extension Offering {
packageType: pkg.packageType,
storeProduct: pkg.storeProduct,
presentedOfferingContext: newContext,
planKey: pkg.planKey
planKey: pkg.planKey,
stripeProductIdentifier: pkg.stripeProductIdentifier
)
}

Expand Down
19 changes: 13 additions & 6 deletions Sources/Purchasing/OfferingsFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ class OfferingsFactory {
offering: OfferingsResponse.Offering
) -> Offering? {
let availablePackages: [Package] = offering.packages.compactMap { package in
createPackage(with: package, productsByID: storeProductsByID, offeringIdentifier: offering.identifier)
createPackage(with: package,
productsByID: storeProductsByID,
offeringIdentifier: offering.identifier,
stripeProductIdentifier: offering.stripeProductIdentifier(for: package))
}

guard !availablePackages.isEmpty else {
Expand All @@ -59,15 +62,17 @@ class OfferingsFactory {
func createPackage(
with data: OfferingsResponse.Offering.Package,
productsByID: [String: StoreProduct],
offeringIdentifier: String
offeringIdentifier: String,
stripeProductIdentifier: String? = nil
) -> Package? {
guard let product = productsByID[data.platformProductIdentifier] else {
guard data.isAppStoreProduct, let product = productsByID[data.platformProductIdentifier] else {
return nil
}

return .init(package: data,
product: product,
offeringIdentifier: offeringIdentifier)
offeringIdentifier: offeringIdentifier,
stripeProductIdentifier: stripeProductIdentifier)
}

func createPlacement(
Expand All @@ -93,13 +98,15 @@ private extension Package {
convenience init(
package: OfferingsResponse.Offering.Package,
product: StoreProduct,
offeringIdentifier: String
offeringIdentifier: String,
stripeProductIdentifier: String?
) {
self.init(identifier: package.identifier,
packageType: Package.packageType(from: package.identifier),
storeProduct: product,
offeringIdentifier: offeringIdentifier,
planKey: package.planKey)
planKey: package.planKey,
stripeProductIdentifier: stripeProductIdentifier)
}

}
Loading