mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
### Context:
`#if os(iOS)` no longer evaluates to `true` when compiling for
`visionOS`
Which means we need `|| os(xrOS)`.
But to support older Xcode we need `#if swift(>=5.9) && os(xrOS)`.
But, fun fact, short-circuiting doesn’t work in `Xcode 13`.
And we still need that to run iOS <15 tests (`FB12223404` /
`FB12805891`)
💩
This works around that by defining our own `VISION_OS` flag.
### Changes:
- Added `VISION_OS` compile time flag to determine SDK
- Replaced `#if os(xrOS)` with `#if VISION_OS`
- Added flag to `Package.swift` (thanks @neonichu for helping with this)
156 lines
5.7 KiB
Swift
156 lines
5.7 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) || VISION_OS
|
|
|
|
@available(watchOS, unavailable)
|
|
@available(tvOS, unavailable)
|
|
func showManageSubscriptions(completion: @escaping (Result<Void, PurchasesError>) -> Void) {
|
|
let currentAppUserID = self.currentUserProvider.currentAppUserID
|
|
self.customerInfoManager.customerInfo(appUserID: currentAppUserID,
|
|
fetchPolicy: .cachedOrFetched) { @Sendable result in
|
|
let result: Result<URL, PurchasesError> = 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
|
|
|
|
}
|
|
|
|
// @unchecked because:
|
|
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
|
|
extension ManageSubscriptionsHelper: @unchecked Sendable {}
|
|
|
|
// MARK: - Private
|
|
|
|
@available(watchOS, unavailable)
|
|
@available(tvOS, unavailable)
|
|
private extension ManageSubscriptionsHelper {
|
|
|
|
func showAppleManageSubscriptions(managementURL: URL,
|
|
completion: @escaping (Result<Void, PurchasesError>) -> Void) {
|
|
#if os(iOS) && !targetEnvironment(macCatalyst) || VISION_OS
|
|
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 {
|
|
Async.call(with: completion) {
|
|
return await self.showSK2ManageSubscriptions()
|
|
}
|
|
return
|
|
}
|
|
#endif
|
|
openURL(managementURL, completion: completion)
|
|
}
|
|
|
|
func openURL(_ url: URL, completion: @escaping (Result<Void, PurchasesError>) -> Void) {
|
|
#if os(iOS) || VISION_OS
|
|
openURLIfNotAppExtension(url: url)
|
|
#elseif os(macOS)
|
|
NSWorkspace.shared.open(url)
|
|
#endif
|
|
completion(.success(()))
|
|
}
|
|
|
|
#if os(iOS) || VISION_OS
|
|
// 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, PurchasesError> {
|
|
let windowScene: UIWindowScene
|
|
|
|
do {
|
|
windowScene = try self.systemInfo.currentWindowScene
|
|
} catch {
|
|
return .failure(ErrorUtils.purchasesError(withUntypedError: error))
|
|
}
|
|
|
|
#if os(iOS) || VISION_OS
|
|
// 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<Void, Never> {
|
|
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
|
|
|
|
}
|