mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-10 22:45:55 +05:45
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
149 lines
5.5 KiB
Swift
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
|
|
|
|
}
|