-
Notifications
You must be signed in to change notification settings - Fork 338
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Don't show refund if free subscription (#4805)
* Added refund_window * missing tests * feat: Support refund_window to hide refund path * removed docs * removed docs * fixed previews * fixed RC tests * Update Tests/TestingApps/PaywallsTester/PaywallsTester/Config/ConfigItem.swift * nits * Use customercenter reuqested date * feat: Don't show refund if free * add comments * requestDate from customerInfo
- Loading branch information
1 parent
7547196
commit c1e96e0
Showing
6 changed files
with
328 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
271 changes: 271 additions & 0 deletions
271
RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift.orig
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,271 @@ | ||
// | ||
// 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 | ||
// | ||
// CustomerCenterViewModel.swift | ||
// | ||
// | ||
// Created by Cesar de la Vega on 27/5/24. | ||
// | ||
|
||
import Foundation | ||
import RevenueCat | ||
|
||
#if os(iOS) | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
@MainActor class CustomerCenterViewModel: ObservableObject { | ||
|
||
private static let defaultAppIsLatestVersion = true | ||
|
||
typealias CurrentVersionFetcher = () -> String? | ||
|
||
private lazy var currentAppVersion: String? = currentVersionFetcher() | ||
|
||
@Published | ||
private(set) var purchaseInformation: PurchaseInformation? | ||
|
||
@Published | ||
private(set) var appIsLatestVersion: Bool = defaultAppIsLatestVersion | ||
|
||
@Published | ||
private(set) var onUpdateAppClick: (() -> Void)? | ||
|
||
private(set) var purchasesProvider: CustomerCenterPurchasesType | ||
private(set) var customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType | ||
|
||
/// Whether or not the Customer Center should warn the customer that they're on an outdated version of the app. | ||
var shouldShowAppUpdateWarnings: Bool { | ||
return !appIsLatestVersion && (configuration?.support.shouldWarnCustomerToUpdate ?? true) | ||
} | ||
|
||
// @PublicForExternalTesting | ||
@Published | ||
var state: CustomerCenterViewState { | ||
didSet { | ||
if case let .error(stateError) = state { | ||
self.error = stateError | ||
} | ||
} | ||
} | ||
@Published | ||
var configuration: CustomerCenterConfigData? { | ||
didSet { | ||
guard | ||
let currentVersionString = currentAppVersion?.versionString(), | ||
let latestVersionString = configuration?.lastPublishedAppVersion?.versionString(), | ||
let currentVersion = try? SemanticVersion(currentVersionString), | ||
let latestVersion = try? SemanticVersion(latestVersionString) | ||
else { | ||
self.appIsLatestVersion = Self.defaultAppIsLatestVersion | ||
return | ||
} | ||
|
||
self.appIsLatestVersion = currentVersion >= latestVersion | ||
} | ||
} | ||
|
||
private let currentVersionFetcher: CurrentVersionFetcher | ||
internal let customerCenterActionHandler: CustomerCenterActionHandler? | ||
|
||
private var error: Error? | ||
private var impressionData: CustomerCenterEvent.Data? | ||
|
||
init( | ||
customerCenterActionHandler: CustomerCenterActionHandler?, | ||
currentVersionFetcher: @escaping CurrentVersionFetcher = { | ||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String | ||
}, | ||
purchasesProvider: CustomerCenterPurchasesType = CustomerCenterPurchases(), | ||
customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType = CustomerCenterStoreKitUtilities() | ||
) { | ||
self.state = .notLoaded | ||
self.currentVersionFetcher = currentVersionFetcher | ||
self.customerCenterActionHandler = customerCenterActionHandler | ||
self.purchasesProvider = purchasesProvider | ||
self.customerCenterStoreKitUtilities = customerCenterStoreKitUtilities | ||
} | ||
|
||
#if DEBUG | ||
|
||
convenience init( | ||
purchaseInformation: PurchaseInformation, | ||
configuration: CustomerCenterConfigData | ||
) { | ||
self.init(customerCenterActionHandler: nil) | ||
self.purchaseInformation = purchaseInformation | ||
self.configuration = configuration | ||
self.state = .success | ||
} | ||
|
||
#endif | ||
|
||
func loadScreen() async { | ||
do { | ||
try await self.loadPurchaseInformation() | ||
try await self.loadCustomerCenterConfig() | ||
self.state = .success | ||
} catch { | ||
self.state = .error(error) | ||
} | ||
} | ||
|
||
func performRestore() async -> RestorePurchasesAlert.AlertType { | ||
self.customerCenterActionHandler?(.restoreStarted) | ||
do { | ||
let customerInfo = try await purchasesProvider.restorePurchases() | ||
self.customerCenterActionHandler?(.restoreCompleted(customerInfo)) | ||
let hasPurchases = !customerInfo.activeSubscriptions.isEmpty || !customerInfo.nonSubscriptions.isEmpty | ||
return hasPurchases ? .purchasesRecovered : .purchasesNotFound | ||
} catch { | ||
self.customerCenterActionHandler?(.restoreFailed(error)) | ||
return .purchasesNotFound | ||
} | ||
} | ||
|
||
func trackImpression(darkMode: Bool, displayMode: CustomerCenterPresentationMode) { | ||
guard impressionData == nil else { | ||
return | ||
} | ||
|
||
let eventData = CustomerCenterEvent.Data(locale: .current, | ||
darkMode: darkMode, | ||
isSandbox: purchasesProvider.isSandbox, | ||
displayMode: displayMode) | ||
defer { self.impressionData = eventData } | ||
|
||
let event = CustomerCenterEvent.impression(CustomerCenterEventCreationData(), eventData) | ||
purchasesProvider.track(customerCenterEvent: event) | ||
} | ||
|
||
} | ||
|
||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) | ||
@available(macOS, unavailable) | ||
@available(tvOS, unavailable) | ||
@available(watchOS, unavailable) | ||
private extension CustomerCenterViewModel { | ||
|
||
func loadPurchaseInformation() async throws { | ||
let customerInfo = try await purchasesProvider.customerInfo(fetchPolicy: .fetchCurrent) | ||
|
||
let hasActiveProducts = !customerInfo.activeSubscriptions.isEmpty || | ||
!customerInfo.nonSubscriptions.isEmpty | ||
|
||
if !hasActiveProducts { | ||
self.purchaseInformation = nil | ||
self.state = .success | ||
return | ||
} | ||
|
||
guard let activeTransaction = findActiveTransaction(customerInfo: customerInfo) else { | ||
Logger.warning(Strings.could_not_find_subscription_information) | ||
self.purchaseInformation = nil | ||
throw CustomerCenterError.couldNotFindSubscriptionInformation | ||
} | ||
|
||
let entitlement = customerInfo.entitlements.all.values | ||
.first(where: { $0.productIdentifier == activeTransaction.productIdentifier }) | ||
|
||
self.purchaseInformation = try await createPurchaseInformation( | ||
for: activeTransaction, | ||
entitlement: entitlement, | ||
customerInfo: customerInfo) | ||
} | ||
|
||
func loadCustomerCenterConfig() async throws { | ||
self.configuration = try await purchasesProvider.loadCustomerCenter() | ||
if let productId = configuration?.productId { | ||
self.onUpdateAppClick = { | ||
// productId is a positive integer, so it should be safe to construct a URL from it. | ||
let url = URL(string: "https://itunes.apple.com/app/id\(productId)")! | ||
URLUtilities.openURLIfNotAppExtension(url) | ||
} | ||
} | ||
} | ||
|
||
func findActiveTransaction(customerInfo: CustomerInfo) -> Transaction? { | ||
let activeSubscriptions = customerInfo.subscriptionsByProductIdentifier.values | ||
.filter(\.isActive) | ||
.sorted(by: { | ||
guard let date1 = $0.expiresDate, let date2 = $1.expiresDate else { | ||
return $0.expiresDate != nil | ||
} | ||
return date1 < date2 | ||
}) | ||
|
||
let (activeAppleSubscriptions, otherActiveSubscriptions) = ( | ||
activeSubscriptions.filter { $0.store == .appStore }, | ||
activeSubscriptions.filter { $0.store != .appStore } | ||
) | ||
|
||
let (appleNonSubscriptions, otherNonSubscriptions) = ( | ||
customerInfo.nonSubscriptions.filter { $0.store == .appStore }, | ||
customerInfo.nonSubscriptions.filter { $0.store != .appStore } | ||
) | ||
|
||
return activeAppleSubscriptions.first ?? | ||
appleNonSubscriptions.first ?? | ||
otherActiveSubscriptions.first ?? | ||
otherNonSubscriptions.first | ||
} | ||
|
||
func createPurchaseInformation(for transaction: Transaction, | ||
entitlement: EntitlementInfo?, | ||
customerInfo: CustomerInfo) async throws -> PurchaseInformation { | ||
if transaction.store == .appStore { | ||
if let product = await purchasesProvider.products([transaction.productIdentifier]).first { | ||
return await PurchaseInformation.purchaseInformationUsingRenewalInfo( | ||
entitlement: entitlement, | ||
subscribedProduct: product, | ||
transaction: transaction, | ||
customerCenterStoreKitUtilities: customerCenterStoreKitUtilities, | ||
customerInfoRequestedDate: customerInfo.requestDate | ||
) | ||
} else { | ||
Logger.warning( | ||
Strings.could_not_find_product_loading_without_product_information(transaction.productIdentifier) | ||
) | ||
|
||
return PurchaseInformation( | ||
entitlement: entitlement, | ||
transaction: transaction, | ||
customerInfoRequestedDate: customerInfo.requestDate | ||
) | ||
} | ||
} | ||
Logger.warning(Strings.active_product_is_not_apple_loading_without_product_information(transaction.store)) | ||
|
||
return PurchaseInformation( | ||
entitlement: entitlement, | ||
transaction: transaction, | ||
customerInfoRequestedDate: customerInfo.requestDate | ||
) | ||
} | ||
|
||
} | ||
|
||
fileprivate extension String { | ||
/// Takes the first characters of this string, if they conform to Major.Minor.Patch. Returns nil otherwise. | ||
/// Note that Minor and Patch are optional. So if this string starts with a single number, that number is returned. | ||
func versionString() -> String? { | ||
do { | ||
let pattern = #"^(\d+)(?:\.\d+)?(?:\.\d+)?"# | ||
let regex = try NSRegularExpression(pattern: pattern) | ||
let match = regex.firstMatch(in: self, range: NSRange(self.startIndex..., in: self)) | ||
return match.map { String(self[Range($0.range, in: self)!]) } | ||
} catch { | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters