Files
purchases-ios/Sources/Support/ManageSubscriptionsHelper.swift
T
NachoSoto c531814dc6 Purchases.customerInfo(): added overload with a new CacheFetchPolicy (#1608)
Fixes [CF-19].
This allows specifying `CustomerInfoManager` fetching behavior, both as a public API for users, and also internally.
Some future uses of `CustomerInfoManager` might want to ensure that we don't rely on cached data, for example.

Thanks to the `APITester` we know that the existing methods still work, this is purely an additive change.

This is the core of the change:
```swift
/// Specifies the behavior for a caching API.
@objc(RCCacheFetchPolicy)
public enum CacheFetchPolicy: Int {
    /// Returns values from the cache, or throws an error if not available.
    case fromCacheOnly

    /// Always fetch the most up-to-date data.
    case fetchCurrent

    /// Returns the cached data if available and not stale, or fetches up-to-date data.
    case notStaleCachedOrFetched

    /// Default behavior: returns the cached data if available (even if stale), or fetches up-to-date data.
    case cachedOrFetched

    /// Default ``CacheFetchPolicy`` behavior.
    public static let `default`: Self = .cachedOrFetched
}
```

The new `CustomerInfoManagerGetCustomerInfoTests` check all the different combinations of fetching behavior.
I've taken some of the existing tests and refactored them to be `async`, as well as added a bunch more tests.

> **Note**: 
There is also one big hidden bug fix:

`Purchases.getCustomerInfo` now calls `BackendError.asPurchasesError`, so we don't end up returning an internal error type.
This was missed because we always return generic `Error` types, so we have no way to know at compile time that we only return `ErrorCode`s.

[CF-19]: https://revenuecats.atlassian.net/browse/CF-19?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-05-31 13:04:16 -07:00

149 lines
5.5 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
//
// ManageSubscriptionsHelper.swift
//
// Created by Andrés Boedo on 16/8/21.
import StoreKit
class ManageSubscriptionsHelper {
private let systemInfo: SystemInfo
private let customerInfoManager: CustomerInfoManager
private let currentUserProvider: CurrentUserProvider
init(systemInfo: SystemInfo,
customerInfoManager: CustomerInfoManager,
currentUserProvider: CurrentUserProvider) {
self.systemInfo = systemInfo
self.customerInfoManager = customerInfoManager
self.currentUserProvider = currentUserProvider
}
#if os(iOS) || os(macOS)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
func showManageSubscriptions(completion: @escaping (Result<Void, Error>) -> Void) {
let currentAppUserID = self.currentUserProvider.currentAppUserID
self.customerInfoManager.customerInfo(appUserID: currentAppUserID,
fetchPolicy: .cachedOrFetched) { result in
let result: Result<URL, Error> = result
.mapError { error in
let message = Strings.failed_to_get_management_url_error_unknown(error: error)
return ErrorUtils.customerInfoError(withMessage: message.description, error: error)
}
.flatMap { customerInfo in
guard let managementURL = customerInfo.managementURL else {
Logger.debug(Strings.management_url_nil_opening_default)
return .success(SystemInfo.appleSubscriptionsURL)
}
return .success(managementURL)
}
switch result {
case let .success(url):
if SystemInfo.isAppleSubscription(managementURL: url) {
self.showAppleManageSubscriptions(managementURL: url, completion: completion)
} else {
self.openURL(url, completion: completion)
}
case let .failure(error):
completion(.failure(error))
}
}
}
#endif
}
@available(watchOS, unavailable)
@available(tvOS, unavailable)
private extension ManageSubscriptionsHelper {
func showAppleManageSubscriptions(managementURL: URL,
completion: @escaping (Result<Void, Error>) -> Void) {
#if os(iOS) && !targetEnvironment(macCatalyst)
if #available(iOS 15.0, *),
// showManageSubscriptions doesn't work on iOS apps running on Apple Silicon
// https://developer.apple.com/documentation/storekit/appstore/3803198-showmanagesubscriptions#
!ProcessInfo().isiOSAppOnMac {
_ = Task<Void, Never> {
let result = await self.showSK2ManageSubscriptions()
completion(result)
}
return
}
#endif
openURL(managementURL, completion: completion)
}
func openURL(_ url: URL, completion: @escaping (Result<Void, Error>) -> Void) {
#if os(iOS)
openURLIfNotAppExtension(url: url)
#elseif os(macOS)
NSWorkspace.shared.open(url)
#endif
completion(.success(()))
}
#if os(iOS)
// we can't directly reference UIApplication.shared in case this SDK is embedded into an app extension.
// so we ensure that it's not running in an app extension and use selectors to call UIApplication methods.
func openURLIfNotAppExtension(url: URL) {
guard !systemInfo.isAppExtension,
let application = systemInfo.sharedUIApplication else { return }
// NSInvocation is needed because the method takes three arguments
// and performSelector works for up to 2
typealias ClosureType = @convention(c) (AnyObject, Selector, NSURL, NSDictionary?, Any?) -> Void
let selector: Selector = NSSelectorFromString("openURL:options:completionHandler:")
let methodIMP: IMP! = application.method(for: selector)
let openURLMethod = unsafeBitCast(methodIMP, to: ClosureType.self)
openURLMethod(application, selector, url as NSURL, nil, nil)
}
@MainActor
@available(iOS 15.0, *)
@available(macOS, unavailable)
func showSK2ManageSubscriptions() async -> Result<Void, Error> {
guard let application = systemInfo.sharedUIApplication,
let windowScene = application.currentWindowScene else {
let message = Strings.failed_to_get_window_scene
return .failure(ErrorUtils.storeProblemError(withMessage: message.description))
}
#if os(iOS)
// Note: we're ignoring the result of AppStore.showManageSubscriptions(in:) because as of
// iOS 15.2, it only returns after the sheet is dismissed, which isn't desired.
_ = Task.init {
do {
try await AppStore.showManageSubscriptions(in: windowScene)
Logger.info(Strings.susbscription_management_sheet_dismissed)
} catch {
let message = Strings.error_from_appstore_show_manage_subscription(error: error)
Logger.appleError(message)
}
}
return .success(())
#else
fatalError(Strings.manageSubscription.show_manage_subscriptions_called_in_unsupported_platform.description)
#endif
}
#endif
}