Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Dont show cancel path if lifetime subscription #4755

Merged
merged 8 commits into from
Feb 7, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ enum CustomerCenterConfigTestData {
expirationOrRenewal: .init(label: .nextBillingDate,
date: .date("June 1st, 2024")),
productIdentifier: "product_id",
store: .appStore
store: .appStore,
isLifetime: false
)

static let subscriptionInformationYearlyExpiring: PurchaseInformation = .init(
Expand All @@ -155,7 +156,8 @@ enum CustomerCenterConfigTestData {
expirationOrRenewal: .init(label: .expires,
date: .date("June 1st, 2024")),
productIdentifier: "product_id",
store: .appStore
store: .appStore,
isLifetime: false
)

}
23 changes: 22 additions & 1 deletion RevenueCatUI/CustomerCenter/Data/PurchaseInformation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,38 @@ import RevenueCat
import StoreKit

// swiftlint:disable nesting

/// Information about a purchase.
struct PurchaseInformation {

/// The title of the storekit product, if applicable.
/// - Note: See `StoreProduct.localizedTitle` for more details.
let title: String?
/// The duration of the product, if applicable.
/// - Note: See `StoreProduct.localizedDetails` for more details.
let durationTitle: String?
let explanation: Explanation
/// Pricing details of the purchase.
let price: PriceDetails
/// Subscription expiration or renewal details, if applicable.
let expirationOrRenewal: ExpirationOrRenewal?
/// The unique product identifier for the purchase.
let productIdentifier: String
/// The store from which the purchase was made (e.g., App Store, Play Store).
let store: Store
/// Indicates whether the purchase grants lifetime access.
/// - `true` for non-subscription purchases.
/// - `false` for subscriptions, even if the expiration date is set far in the future.
let isLifetime: Bool

init(title: String,
durationTitle: String,
explanation: Explanation,
price: PriceDetails,
expirationOrRenewal: ExpirationOrRenewal?,
productIdentifier: String,
store: Store
store: Store,
isLifetime: Bool
) {
self.title = title
self.durationTitle = durationTitle
Expand All @@ -43,6 +58,7 @@ struct PurchaseInformation {
self.expirationOrRenewal = expirationOrRenewal
self.productIdentifier = productIdentifier
self.store = store
self.isLifetime = isLifetime
}

init(entitlement: EntitlementInfo? = nil,
Expand All @@ -67,6 +83,8 @@ struct PurchaseInformation {
} else {
self.price = entitlement.priceBestEffort(product: subscribedProduct)
}
self.isLifetime = entitlement.expirationDate == nil

} else {
switch transaction.type {
case .subscription(let isActive, let willRenew, let expiresDate):
Expand All @@ -80,9 +98,12 @@ struct PurchaseInformation {
: .expired
return ExpirationOrRenewal(label: label, date: .date(dateString))
}
self.isLifetime = false

case .nonSubscription:
self.explanation = .lifetime
self.expirationOrRenewal = nil
self.isLifetime = true
}

self.productIdentifier = transaction.productIdentifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@ import RevenueCat
typealias CurrentVersionFetcher = () -> String?

private lazy var currentAppVersion: String? = currentVersionFetcher()

@Published
private(set) var purchaseInformation: PurchaseInformation?

@Published
private(set) var appIsLatestVersion: Bool = defaultAppIsLatestVersion
private(set) var purchasesProvider: CustomerCenterPurchasesType
private(set) var customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType

@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)
Expand Down Expand Up @@ -147,28 +150,28 @@ import RevenueCat
private extension CustomerCenterViewModel {

func loadPurchaseInformation() async throws {
let customerInfo = try await purchasesProvider.customerInfo(fetchPolicy: .fetchCurrent)
let customerInfo = try await purchasesProvider.customerInfo(fetchPolicy: .fetchCurrent)

let hasActiveProducts =
!customerInfo.activeSubscriptions.isEmpty || !customerInfo.nonSubscriptions.isEmpty
let hasActiveProducts = !customerInfo.activeSubscriptions.isEmpty ||
!customerInfo.nonSubscriptions.isEmpty

if !hasActiveProducts {
self.purchaseInformation = nil
self.state = .success
return
}
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
}
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 })
let entitlement = customerInfo.entitlements.all.values
.first(where: { $0.productIdentifier == activeTransaction.productIdentifier })

self.purchaseInformation = try await createPurchaseInformation(for: activeTransaction,
entitlement: entitlement)
self.purchaseInformation = try await createPurchaseInformation(for: activeTransaction,
entitlement: entitlement)
}

func loadCustomerCenterConfig() async throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,33 @@ import SwiftUI
class ManageSubscriptionsViewModel: ObservableObject {

let screen: CustomerCenterConfigData.Screen
let paths: [CustomerCenterConfigData.HelpPath]

var relevantPathsForPurchase: [CustomerCenterConfigData.HelpPath] {
if purchaseInformation?.isLifetime == true {
return paths.filter { $0.type != .cancel }
} else {
return paths
}
}

@Published
var showRestoreAlert: Bool = false

@Published
var showPurchases: Bool = false

@Published
var feedbackSurveyData: FeedbackSurveyData?

@Published
var loadingPath: CustomerCenterConfigData.HelpPath?

@Published
var promotionalOfferData: PromotionalOfferData?

@Published
var inAppBrowserURL: IdentifiableURL?

@Published
var state: CustomerCenterViewState {
didSet {
Expand All @@ -53,13 +65,15 @@ class ManageSubscriptionsViewModel: ObservableObject {

@Published
private(set) var purchaseInformation: PurchaseInformation?

@Published
private(set) var refundRequestStatus: RefundRequestStatus?

private var purchasesProvider: ManageSubscriptionsPurchaseType
private let loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType
private let customerCenterActionHandler: CustomerCenterActionHandler?
private var error: Error?
private let loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType
private let paths: [CustomerCenterConfigData.HelpPath]
private var purchasesProvider: ManageSubscriptionsPurchaseType

init(screen: CustomerCenterConfigData.Screen,
customerCenterActionHandler: CustomerCenterActionHandler?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,13 @@ struct ManageSubscriptionsButtonsView: View {

@ObservedObject
var viewModel: ManageSubscriptionsViewModel
@Binding
var loadingPath: CustomerCenterConfigData.HelpPath?
@Environment(\.openURL)
var openURL

@Environment(\.localization)
private var localization: CustomerCenterConfigData.Localization

var body: some View {
ForEach(self.viewModel.paths, id: \.id) { path in
ManageSubscriptionButton(path: path, viewModel: self.viewModel)
ForEach(self.viewModel.relevantPathsForPurchase, id: \.id) { path in
ManageSubscriptionButton(
path: path,
viewModel: self.viewModel
)
}
}

Expand All @@ -45,13 +41,12 @@ struct ManageSubscriptionsButtonsView: View {
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
struct ManageSubscriptionButton: View {
private struct ManageSubscriptionButton: View {

let path: CustomerCenterConfigData.HelpPath
@ObservedObject var viewModel: ManageSubscriptionsViewModel

@Environment(\.appearance)
private var appearance: CustomerCenterConfigData.Appearance
@ObservedObject
var viewModel: ManageSubscriptionsViewModel

var body: some View {
AsyncButton(action: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ struct ManageSubscriptionsView: View {

Section {
ManageSubscriptionsButtonsView(
viewModel: self.viewModel,
loadingPath: self.$viewModel.loadingPath
viewModel: self.viewModel
)
} header: {
if let subtitle = self.viewModel.screen.subtitle {
Expand All @@ -148,8 +147,7 @@ struct ManageSubscriptionsView: View {
}

Section {
ManageSubscriptionsButtonsView(viewModel: self.viewModel,
loadingPath: self.$viewModel.loadingPath)
ManageSubscriptionsButtonsView(viewModel: self.viewModel)
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions RevenueCatUI/Data/CustomerInfoFixtures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ class CustomerInfoFixtures {
init(id: String,
store: String,
purchaseDate: String,
expirationDate: String,
expirationDate: String?,
unsubscribeDetectedAt: String? = nil) {
self.id = id
self.json = """
{
"auto_resume_date": null,
"billing_issues_detected_at": null,
"expires_date": "\(expirationDate)",
"expires_date": \(expirationDate != nil ? "\"\(expirationDate!)\"" : "null"),
"grace_period_expires_date": null,
"is_sandbox": true,
"original_purchase_date": "\(purchaseDate)",
Expand Down Expand Up @@ -149,7 +149,7 @@ class CustomerInfoFixtures {
store: String,
productId: String = "com.revenuecat.product",
purchaseDate: String = "2022-04-12T00:03:28Z",
expirationDate: String = "2062-04-12T00:03:35Z",
expirationDate: String? = "2062-04-12T00:03:35Z",
unsubscribeDetectedAt: String? = nil
) -> CustomerInfo {
return customerInfo(
Expand Down Expand Up @@ -185,6 +185,14 @@ class CustomerInfoFixtures {
)
}()

static let customerInfoWithLifetimeAppSubscrition: CustomerInfo = {
makeCustomerInfo(
store: "app_store",
purchaseDate: "1999-04-12T00:03:28Z",
expirationDate: nil
)
}()

static let customerInfoWithNonRenewingAppleSubscriptions: CustomerInfo = {
makeCustomerInfo(
store: "app_store",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ class ManageSubscriptionsViewModelTests: TestCase {
expect(viewModel.showRestoreAlert) == false
}

func testLifetimeSubscriptionDoesNotShowCancel() {
let viewModel = ManageSubscriptionsViewModel(
screen: ManageSubscriptionsViewModelTests.screen,
customerCenterActionHandler: nil,
purchaseInformation: PurchaseInformation.mockLifetime
)

expect(viewModel.relevantPathsForPurchase.contains(where: { $0.type == .cancel })).to(beFalse())
}

func testStateChangeToError() {
let viewModel = ManageSubscriptionsViewModel(screen: ManageSubscriptionsViewModelTests.screen,
customerCenterActionHandler: nil)
Expand Down Expand Up @@ -434,4 +444,19 @@ private class MockLoadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType {

}

private extension PurchaseInformation {
static var mockLifetime: PurchaseInformation {
PurchaseInformation(
title: "",
durationTitle: "",
explanation: .lifetime,
price: .paid(""),
expirationOrRenewal: PurchaseInformation.ExpirationOrRenewal(label: .expires, date: .date("")),
productIdentifier: "",
store: .appStore,
isLifetime: true
)
}
}

#endif
Loading