From 2eb5a25a48d75c584c6a9bf2e55737aadaf90f7d Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 28 May 2024 15:22:24 +0200 Subject: [PATCH 01/50] Create CustomerCenterView --- .../CustomerCenter/CustomerCenterView.swift | 74 ++++++++ .../CustomerCenterViewModel.swift | 68 +++++++ .../Data/CustomerCenterData.swift | 92 +++++++++ .../Data/CustomerCenterTestData.swift | 91 +++++++++ .../Data/SubscriptionInformation.swift | 22 +++ .../ManageSubscriptionsButtonStyle.swift | 33 ++++ .../ManageSubscriptionsView.swift | 175 ++++++++++++++++++ .../ManageSubscriptionsViewModel.swift | 99 ++++++++++ .../CustomerCenter/NoSubscriptionsView.swift | 49 +++++ .../RestorePurchasesAlert.swift | 90 +++++++++ .../CustomerCenter/URLUtilities.swift | 22 +++ .../CustomerCenter/WrongPlatformView.swift | 108 +++++++++++ 12 files changed, 923 insertions(+) create mode 100644 RevenueCatUI/CustomerCenter/CustomerCenterView.swift create mode 100644 RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift create mode 100644 RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift create mode 100644 RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift create mode 100644 RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift create mode 100644 RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift create mode 100644 RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift create mode 100644 RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift create mode 100644 RevenueCatUI/CustomerCenter/URLUtilities.swift create mode 100644 RevenueCatUI/CustomerCenter/WrongPlatformView.swift diff --git a/RevenueCatUI/CustomerCenter/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/CustomerCenterView.swift new file mode 100644 index 0000000000..12c55ec1f1 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/CustomerCenterView.swift @@ -0,0 +1,74 @@ +// +// CustomerCenterView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import SwiftUI +import RevenueCat + +@available(iOS 15.0, *) +public struct CustomerCenterView: View { + + @StateObject private var viewModel = CustomerCenterViewModel() + + public init() { } + + init(viewModel: CustomerCenterViewModel) { + self._viewModel = .init(wrappedValue: viewModel) + } + + public var body: some View { + NavigationView { + NavigationLink(destination: destinationView()) { + Text("Billing and subscription help") + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + } + .onAppear { + checkAndLoadSubscriptions() + } + } + + private func checkAndLoadSubscriptions() { + if !viewModel.isLoaded { + Task { + await viewModel.loadHasSubscriptions() + } + } + } + + @ViewBuilder + private func destinationView() -> some View { + if viewModel.hasSubscriptions { + if viewModel.areSubscriptionsFromApple { + ManageSubscriptionsView() + } else { + WrongPlatformView() + } + } else { + NoSubscriptionsView() + } + } + +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +struct CustomerCenterView_Previews: PreviewProvider { + + static var previews: some View { + let viewModel = CustomerCenterViewModel(hasSubscriptions: false, areSubscriptionsFromApple: false) + CustomerCenterView(viewModel: viewModel) + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift new file mode 100644 index 0000000000..7c6a36625c --- /dev/null +++ b/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift @@ -0,0 +1,68 @@ +// +// CustomerCenterViewModel.swift +// +// +// Created by Cesar de la Vega on 27/5/24. +// + +import Foundation +import RevenueCat + +@available(iOS 15.0, *) +class CustomerCenterViewModel: ObservableObject { + + @Published + var hasSubscriptions: Bool = false + @Published + var areSubscriptionsFromApple: Bool = false + + var isLoaded: Bool { + if case .notLoaded = state { + return false + } + return true + } + + enum State { + case notLoaded + case success + case error(Error) + } + + var error: Error? + + private(set) var state: State { + didSet { + if case let .error(stateError) = state { + self.error = stateError + } + } + } + + init() { + self.state = .notLoaded + } + + init(hasSubscriptions: Bool = false, areSubscriptionsFromApple: Bool = false) { + self.hasSubscriptions = hasSubscriptions + self.areSubscriptionsFromApple = areSubscriptionsFromApple + self.state = .success + } + + func loadHasSubscriptions() async { + do { + let customerInfo = try await Purchases.shared.customerInfo() + self.hasSubscriptions = customerInfo.activeSubscriptions.count > 0 + guard let firstActiveEntitlement: EntitlementInfo = customerInfo.entitlements.active.first?.value else { + self.areSubscriptionsFromApple = false + return + } + + self.areSubscriptionsFromApple = firstActiveEntitlement.store == .appStore || firstActiveEntitlement.store == .macAppStore + } catch { + // TODO: log and handle maybe? + self.state = .error(error) + } + } + +} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift new file mode 100644 index 0000000000..433df6ef6b --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -0,0 +1,92 @@ +// +// File.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation + +struct CustomerCenterData: Decodable { + + let id: String + let paths: [HelpPath] + let title: LocalizedString + let supportEmail: String + let appearance: Appearance + + struct HelpPath: Decodable { + + enum HelpPathType: String, Decodable { + case missingPurchase = "MISSING_PURCHASE" + case refundRequest = "REFUND_REQUEST" + case changePlans = "CHANGE_PLANS" + case cancel = "CANCEL" + case unknown + } + + let id: String + let title: LocalizedString + let type: HelpPathType + let promotionalOffer: PromotionalOffer? + let feedbackSurvey: FeedbackSurvey? + + } + + struct LocalizedString: Decodable { + + let en_US: String + + } + + struct PromotionalOffer: Decodable { + + let iosOfferId: String + let eligibility: Eligibility + + } + + struct Eligibility: Decodable { + + let first_seen: String + + } + + struct FeedbackSurvey: Decodable { + + let title: LocalizedString + let options: [Option] + + struct Option: Decodable { + + let id: String + let title: LocalizedString + let promotionalOffer: PromotionalOffer? + + } + + } + + struct Appearance: Decodable { + + let mode: String + let light: String + let dark: String + + } + +} + +extension CustomerCenterData { + + static func decode(_ json: String) -> CustomerCenterData { + let data = Data(json.utf8) + let decoder = JSONDecoder() + do { + return try decoder.decode(CustomerCenterData.self, from: data) + } catch { + fatalError("Failed to decode JSON: \(error)") + } + } + +} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift new file mode 100644 index 0000000000..ad824cb0c1 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -0,0 +1,91 @@ +// +// TestData.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation + +enum CustomerCenterTestData { + + static let customerCenterData = CustomerCenterData( + id: "ccenter_lasdlfalaowpwp", + paths: [ + CustomerCenterData.HelpPath( + id: "ownmsldfow", + title: CustomerCenterData.LocalizedString(en_US: "Didn't receive purchase"), + type: .missingPurchase, + promotionalOffer: nil, + feedbackSurvey: nil + ), + CustomerCenterData.HelpPath( + id: "nwodkdnfaoeb", + title: CustomerCenterData.LocalizedString(en_US: "Request a refund"), + type: .refundRequest, + promotionalOffer: CustomerCenterData.PromotionalOffer( + iosOfferId: "rc-refund-offer", + eligibility: CustomerCenterData.Eligibility(first_seen: "> 30") + ), + feedbackSurvey: nil + ), + CustomerCenterData.HelpPath( + id: "nfoaiodifj9", + title: CustomerCenterData.LocalizedString(en_US: "Change plans"), + type: .changePlans, + promotionalOffer: nil, + feedbackSurvey: nil + ), + CustomerCenterData.HelpPath( + id: "jnkasldfhas", + title: CustomerCenterData.LocalizedString(en_US: "Cancel subscription"), + type: .cancel, + promotionalOffer: nil, + feedbackSurvey: CustomerCenterData.FeedbackSurvey( + title: CustomerCenterData.LocalizedString(en_US: "Why are you cancelling?"), + options: [ + CustomerCenterData.FeedbackSurvey.Option( + id: "iewrthals", + title: CustomerCenterData.LocalizedString(en_US: "Too expensive"), + promotionalOffer: CustomerCenterData.PromotionalOffer( + iosOfferId: "rc-cancel-offer", + eligibility: CustomerCenterData.Eligibility(first_seen: "> 14") + ) + ), + CustomerCenterData.FeedbackSurvey.Option( + id: "qklpadsfj", + title: CustomerCenterData.LocalizedString(en_US: "Don't use the app"), + promotionalOffer: CustomerCenterData.PromotionalOffer( + iosOfferId: "rc-cancel-offer", + eligibility: CustomerCenterData.Eligibility(first_seen: "> 7") + ) + ), + CustomerCenterData.FeedbackSurvey.Option( + id: "jargnapocps", + title: CustomerCenterData.LocalizedString(en_US: "Bought by mistake"), + promotionalOffer: nil + ) + ] + ) + ) + ], + title: CustomerCenterData.LocalizedString(en_US: "How can we help?"), + supportEmail: "ryan@revenuecat.com", + appearance: CustomerCenterData.Appearance( + mode: "SYSTEM", + light: "#000000", + dark: "#ffffff" + ) + ) + + static let subscriptionInformation: SubscriptionInformation = .init( + title: "Basic", + duration: "Monthly", + price: "$4.99 / month", + nextRenewal: "June 1st, 2024", + willRenew: true, + productIdentifier: "product_id", + active: true + ) + +} diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift new file mode 100644 index 0000000000..8250d5c6fb --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -0,0 +1,22 @@ +// +// SubscriptionInformation.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation + +public struct SubscriptionInformation { + let title: String + let duration: String + let price: String + let nextRenewal: String + let willRenew: Bool + let productIdentifier: String + let active: Bool + + var renewalString: String { + return active ? (willRenew ? "Renews" : "Expires") : "Expired" + } +} diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift new file mode 100644 index 0000000000..b3d650eed9 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -0,0 +1,33 @@ +// +// CustomButtonStyle.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation +import SwiftUI + +@available(iOS 13.0, *) +struct ManageSubscriptionsButtonStyle: ButtonStyle { + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding() + .frame(width: 300) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + +} + +@available(iOS 13.0, *) +struct CustomButtonStylePreview_Previews: PreviewProvider { + + static var previews: some View { + Button("Didn't receive purchase") {} + .buttonStyle(ManageSubscriptionsButtonStyle()) + } + +} diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift new file mode 100644 index 0000000000..17ee783561 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift @@ -0,0 +1,175 @@ +// +// ManageSubscriptionsView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import SwiftUI +import RevenueCat + +@available(iOS 15.0, *) +public struct ManageSubscriptionsView: View { + + @Environment(\.openURL) + var openURL + + @StateObject + private var viewModel = ManageSubscriptionsViewModel() + + public init() { } + + init(viewModel: ManageSubscriptionsViewModel) { + self._viewModel = .init(wrappedValue: viewModel) + } + + public var body: some View { + VStack { + HeaderView() + + if let subscriptionInformation = self.viewModel.subscriptionInformation { + SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, + refundRequestStatus: viewModel.refundRequestStatus) + } + + Spacer() + + ManageSubscriptionsButtonsView(viewModel: viewModel, + openURL: openURL) + } + .onAppear { + checkAndLoadSubscriptionInformation() + } + } + + private func checkAndLoadSubscriptionInformation() { + if !viewModel.isLoaded { + Task { + try! await viewModel.loadSubscriptionInformation() + } + } + } + +} + +@available(iOS 15.0, *) +struct HeaderView: View { + var body: some View { + Text("How can we help?") + .font(.title) + .padding() + } +} + +@available(iOS 15.0, *) +struct SubscriptionDetailsView: View { + let subscriptionInformation: SubscriptionInformation + let refundRequestStatus: String? + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("\(subscriptionInformation.title) - \(subscriptionInformation.duration)") + .font(.subheadline) + .padding(.horizontal) + .padding(.top) + + Text("\(subscriptionInformation.price)") + .font(.caption) + .foregroundColor(Color.gray) + .padding(.horizontal) + + Text("\(subscriptionInformation.renewalString): \(subscriptionInformation.nextRenewal)") + .font(.caption) + .foregroundColor(Color.gray) + .padding(.horizontal) + .padding(.bottom) + + if let refundRequestStatus = refundRequestStatus { + Text("Refund request status: \(refundRequestStatus)") + .font(.caption) + .bold() + .foregroundColor(Color.gray) + .padding(.horizontal) + .padding(.bottom) + } + } + } +} + +@available(iOS 15.0, *) +struct ManageSubscriptionsButtonsView: View { + + @ObservedObject + var viewModel: ManageSubscriptionsViewModel + let openURL: OpenURLAction + @State + private var showRestoreAlert: Bool = false + + var body: some View { + VStack(spacing: 16) { + if let configuration = viewModel.configuration { + ForEach(configuration.paths, id: \.id){ path in + Button(path.title.en_US) { + handleAction(for: path) + } + .restorePurchasesAlert(isPresented: self.$showRestoreAlert) + .buttonStyle(ManageSubscriptionsButtonStyle()) + } + } + + Button("Contact support") { + Task { + openURL(URLUtilities.createMailURL()!) + } + } + .padding() + } + } + + private func handleAction(for path: CustomerCenterData.HelpPath) { + switch path.type { + case .missingPurchase: + self.showRestoreAlert = true + case .refundRequest: + Task { + guard let subscriptionInformation = self.viewModel.subscriptionInformation else { return } + let status = try await Purchases.shared.beginRefundRequest(forProduct: subscriptionInformation.productIdentifier) + switch status { + case .error: + self.viewModel.refundRequestStatus = "Error when requesting refund, try again" + case .success: + self.viewModel.refundRequestStatus = "Refund granted successfully!" + case .userCancelled: + self.viewModel.refundRequestStatus = "Refund canceled" + } + } + case .changePlans: + Task { + try await Purchases.shared.showManageSubscriptions() + } + case .cancel: + Task { + try await Purchases.shared.showManageSubscriptions() + } + default: + break + } + } +} + +#if DEBUG + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +struct ManageSubscriptionsView_Previews: PreviewProvider { + + static var previews: some View { + let viewModel = ManageSubscriptionsViewModel(configuration: CustomerCenterTestData.customerCenterData, + subscriptionInformation: CustomerCenterTestData.subscriptionInformation) + ManageSubscriptionsView(viewModel: viewModel) + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift new file mode 100644 index 0000000000..0fdf4531a1 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift @@ -0,0 +1,99 @@ +// +// ManageSubscriptionsViewModel.swift +// +// +// Created by Cesar de la Vega on 27/5/24. +// + +import Foundation +import RevenueCat + +@available(iOS 15.0, *) +class ManageSubscriptionsViewModel: ObservableObject { + + enum State { + case notLoaded + case success + case error(Error) + } + + var isLoaded: Bool { + if case .notLoaded = state { + return false + } + return true + } + + @Published + var subscriptionInformation: SubscriptionInformation? = nil + + @Published + var refundRequestStatus: String? = nil + + @Published + var configuration: CustomerCenterData? = nil + var error: Error? + + private(set) var state: State { + didSet { + if case let .error(stateError) = state { + self.error = stateError + } + } + } + + init() { + state = .notLoaded + } + + init(configuration: CustomerCenterData) { + state = .notLoaded + self.configuration = configuration + } + + init(configuration: CustomerCenterData, subscriptionInformation: SubscriptionInformation) { + self.configuration = configuration + self.subscriptionInformation = subscriptionInformation + state = .success + } + + func loadSubscriptionInformation() async throws { + guard let customerInfo = try? await Purchases.shared.customerInfo(), + let currentEntitlementDict = customerInfo.entitlements.active.first, + let subscribedProductID = try? await Purchases.shared.customerInfo().activeSubscriptions.first, + let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { + return + } + let currentEntitlement = currentEntitlementDict.value + + self.subscriptionInformation = SubscriptionInformation( + title: subscribedProduct.localizedTitle, + duration: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", + price: subscribedProduct.localizedPriceString, + nextRenewal: "\(String(describing: currentEntitlement.expirationDate!))", + willRenew: currentEntitlement.willRenew, + productIdentifier: subscribedProductID, + active: currentEntitlement.isActive + ) + } + +} + +@available(iOS 15.0, *) +private extension SubscriptionPeriod { + var durationTitle: String { + switch self.unit { + case .day: return "day" + case .week: return "week" + case .month: return "month" + case .year: return "year" + default: return "Unknown" + } + } + + func periodTitle() -> String { + let periodString = "\(self.value) \(self.durationTitle)" + let pluralized = self.value > 1 ? periodString + "s" : periodString + return pluralized + } +} diff --git a/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift new file mode 100644 index 0000000000..b416a80116 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift @@ -0,0 +1,49 @@ +// +// NoSubscriptionsView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import SwiftUI +import RevenueCat + +@available(iOS 15.0, *) +public struct NoSubscriptionsView: View { + + @Environment(\.dismiss) var dismiss + @State private var showRestoreAlert: Bool = false + + + public var body: some View { + VStack { + Text("No Subscriptions found") + .font(.title) + .padding() + Text("We can try checking your Apple account for any previously purchased products") + .font(.body) + .padding() + + Spacer() + + Button("Restore purchases") { + showRestoreAlert = true + } + .restorePurchasesAlert(isPresented: $showRestoreAlert) + .buttonStyle(ManageSubscriptionsButtonStyle()) + + Button("Cancel") { + dismiss() + } + + } + + + } + +} + +@available(iOS 15.0, *) +#Preview { + NoSubscriptionsView() +} diff --git a/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift new file mode 100644 index 0000000000..62a9a12080 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift @@ -0,0 +1,90 @@ +// +// RestorePurchasesAlert.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import Foundation +import SwiftUI +import RevenueCat + +@available(iOS 15.0, *) +public struct RestorePurchasesAlert: ViewModifier { + @Binding var isPresented: Bool + @State private var alertType: AlertType = .restorePurchases + + enum AlertType: Identifiable { + case purchasesRecovered, purchasesNotFound, restorePurchases + var id: Self { self } + } + + @Environment(\.dismiss) private var dismiss + @Environment(\.openURL) var openURL + + + public func body(content: Content) -> some View { + content + .alert(isPresented: $isPresented) { + switch self.alertType { + case .restorePurchases: + Alert( + title: Text("Restore purchases"), + message: Text("Let’s take a look! We’re going to check your Apple account for missing purchases."), + primaryButton: .default(Text("Check past purchases"), action: { + Task { + guard let customerInfo = try? await Purchases.shared.restorePurchases() else { + // todo: handle errors + self.setAlertType(.purchasesNotFound) + return + } + let hasEntitlements = customerInfo.entitlements.active.count > 0 + if hasEntitlements { + self.setAlertType(.purchasesRecovered) + } else { + self.setAlertType(.purchasesNotFound) + } + } + }), + secondaryButton: .cancel(Text("Cancel")) + ) + + case .purchasesRecovered: + Alert(title: Text("Purchases recovered!"), + message: Text("We applied the previously purchased items to your account. " + + "Sorry for the inconvenience."), + dismissButton: .default(Text("Dismiss")) { + dismiss() + }) + + case .purchasesNotFound: + + Alert(title: Text(""), + message: Text("We couldn’t find any additional purchases under this account. \n\n" + + "Contact support for assistance if you think this is an error."), + primaryButton: .default(Text("Contact Support"), action: { + // todo: make configurable + openURL(URLUtilities.createMailURL()!) + }), + secondaryButton: .cancel(Text("Cancel")) { + dismiss() + }) + } + } + } + + private func setAlertType(_ newType: AlertType) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.alertType = newType + self.isPresented = true + } + } + +} + +@available(iOS 15.0, *) +public extension View { + func restorePurchasesAlert(isPresented: Binding) -> some View { + self.modifier(RestorePurchasesAlert(isPresented: isPresented)) + } +} diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift new file mode 100644 index 0000000000..13a48acea1 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -0,0 +1,22 @@ +// +// URLUtilities.swift +// +// +// Created by Cesar de la Vega on 28/5/24. +// + +import Foundation + +struct URLUtilities { + + static func createMailURL() -> URL? { + let subject = "Support Request" + let body = "Please describe your issue or question." + let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + + let urlString = "mailto:support@revenuecat.com?subject=\(encodedSubject)&body=\(encodedBody)" + return URL(string: urlString) + } + +} diff --git a/RevenueCatUI/CustomerCenter/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/WrongPlatformView.swift new file mode 100644 index 0000000000..305cc03ae6 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/WrongPlatformView.swift @@ -0,0 +1,108 @@ +// +// WrongPlatformView.swift +// +// +// Created by Andrés Boedo on 5/3/24. +// + +import Foundation +import RevenueCat +import SwiftUI + +@available(iOS 15.0, *) +public struct WrongPlatformView: View { + + @State + private var store: Store? + + @Environment(\.openURL) + private var openURL + + public init() { + self._store = State(initialValue: nil) + } + + public init(store: Store) { + self._store = State(initialValue: store) + } + + public var body: some View { + VStack { + + + switch(store) { + case .appStore, .macAppStore, .playStore, .amazon: + let platformName = humanReadablePlatformName(store: store!) + + Text("Your subscription is being billed through \(platformName).") + .font(.title) + .padding() + Text("Go the app settings on \(platformName) to manage your subscription and billing.") + .padding() + default: + Text("Please contact support to manage your subscription") + .font(.title) + .padding() + } + + Spacer() + + + Button("Contact support") { + Task { + openURL(URLUtilities.createMailURL()!) + } + } + .padding() + + } + .onAppear { + if (store == nil) { + Task { + if let customerInfo = try? await Purchases.shared.customerInfo(), + let firstEntitlement = customerInfo.entitlements.active.first { + self.store = firstEntitlement.value.store + } + } + } + } + } + + private func humanReadablePlatformName(store: Store) -> String { + switch store { + case .appStore, .macAppStore: + return "Apple App Store" + case .playStore: + return "Google Play Store" + case .stripe: + return "Stripe" + case .promotional: + return "Promotional" + case .amazon: + return "Amazon Appstore" + case .rcBilling: + return "RCBilling" + case .external: + return "External" + case .unknownStore: + return "Unknown" + } + } + +} + +@available(iOS 15.0, *) +struct WrongPlatformView_Previews: PreviewProvider { + + static var previews: some View { + Group { + WrongPlatformView(store: .appStore) + .previewDisplayName("App Store") + + WrongPlatformView(store: .rcBilling) + .previewDisplayName("RCBilling") + } + + } + +} From 1e43ecc9bd6c640e4febebc4253bba633805b420 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 28 May 2024 17:05:53 +0200 Subject: [PATCH 02/50] lint autofix --- RevenueCatUI/CustomerCenter/CustomerCenterView.swift | 2 +- RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift | 8 ++++---- .../CustomerCenter/Data/SubscriptionInformation.swift | 2 +- RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift | 8 ++++---- .../CustomerCenter/ManageSubscriptionsViewModel.swift | 8 ++++---- RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift | 4 +--- RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift | 3 +-- RevenueCatUI/CustomerCenter/WrongPlatformView.swift | 6 ++---- 8 files changed, 18 insertions(+), 23 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/CustomerCenterView.swift index 12c55ec1f1..45036570c7 100644 --- a/RevenueCatUI/CustomerCenter/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/CustomerCenterView.swift @@ -5,8 +5,8 @@ // Created by Andrés Boedo on 5/3/24. // -import SwiftUI import RevenueCat +import SwiftUI @available(iOS 15.0, *) public struct CustomerCenterView: View { diff --git a/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift index 7c6a36625c..b2726a6251 100644 --- a/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift @@ -11,11 +11,11 @@ import RevenueCat @available(iOS 15.0, *) class CustomerCenterViewModel: ObservableObject { - @Published + @Published var hasSubscriptions: Bool = false - @Published + @Published var areSubscriptionsFromApple: Bool = false - + var isLoaded: Bool { if case .notLoaded = state { return false @@ -30,7 +30,7 @@ class CustomerCenterViewModel: ObservableObject { } var error: Error? - + private(set) var state: State { didSet { if case let .error(stateError) = state { diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index 8250d5c6fb..4cf5cb8c3e 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -7,7 +7,7 @@ import Foundation -public struct SubscriptionInformation { +struct SubscriptionInformation { let title: String let duration: String let price: String diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift index 17ee783561..d11ff68994 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift @@ -5,8 +5,8 @@ // Created by Andrés Boedo on 5/3/24. // -import SwiftUI import RevenueCat +import SwiftUI @available(iOS 15.0, *) public struct ManageSubscriptionsView: View { @@ -99,16 +99,16 @@ struct SubscriptionDetailsView: View { @available(iOS 15.0, *) struct ManageSubscriptionsButtonsView: View { - @ObservedObject + @ObservedObject var viewModel: ManageSubscriptionsViewModel let openURL: OpenURLAction - @State + @State private var showRestoreAlert: Bool = false var body: some View { VStack(spacing: 16) { if let configuration = viewModel.configuration { - ForEach(configuration.paths, id: \.id){ path in + ForEach(configuration.paths, id: \.id) { path in Button(path.title.en_US) { handleAction(for: path) } diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift index 0fdf4531a1..fe17049511 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift @@ -25,13 +25,13 @@ class ManageSubscriptionsViewModel: ObservableObject { } @Published - var subscriptionInformation: SubscriptionInformation? = nil + var subscriptionInformation: SubscriptionInformation? - @Published - var refundRequestStatus: String? = nil + @Published + var refundRequestStatus: String? @Published - var configuration: CustomerCenterData? = nil + var configuration: CustomerCenterData? var error: Error? private(set) var state: State { diff --git a/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift index b416a80116..40c274538d 100644 --- a/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift @@ -5,8 +5,8 @@ // Created by Andrés Boedo on 5/3/24. // -import SwiftUI import RevenueCat +import SwiftUI @available(iOS 15.0, *) public struct NoSubscriptionsView: View { @@ -14,7 +14,6 @@ public struct NoSubscriptionsView: View { @Environment(\.dismiss) var dismiss @State private var showRestoreAlert: Bool = false - public var body: some View { VStack { Text("No Subscriptions found") @@ -38,7 +37,6 @@ public struct NoSubscriptionsView: View { } - } } diff --git a/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift index 62a9a12080..0e53f8818e 100644 --- a/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift @@ -6,8 +6,8 @@ // import Foundation -import SwiftUI import RevenueCat +import SwiftUI @available(iOS 15.0, *) public struct RestorePurchasesAlert: ViewModifier { @@ -22,7 +22,6 @@ public struct RestorePurchasesAlert: ViewModifier { @Environment(\.dismiss) private var dismiss @Environment(\.openURL) var openURL - public func body(content: Content) -> some View { content .alert(isPresented: $isPresented) { diff --git a/RevenueCatUI/CustomerCenter/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/WrongPlatformView.swift index 305cc03ae6..bff98d170c 100644 --- a/RevenueCatUI/CustomerCenter/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/WrongPlatformView.swift @@ -29,8 +29,7 @@ public struct WrongPlatformView: View { public var body: some View { VStack { - - switch(store) { + switch store { case .appStore, .macAppStore, .playStore, .amazon: let platformName = humanReadablePlatformName(store: store!) @@ -47,7 +46,6 @@ public struct WrongPlatformView: View { Spacer() - Button("Contact support") { Task { openURL(URLUtilities.createMailURL()!) @@ -57,7 +55,7 @@ public struct WrongPlatformView: View { } .onAppear { - if (store == nil) { + if store == nil { Task { if let customerInfo = try? await Purchases.shared.customerInfo(), let firstEntitlement = customerInfo.entitlements.active.first { From ab9c5cffc81b5c2b3289913787164d1320002fbc Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 28 May 2024 18:01:25 +0200 Subject: [PATCH 03/50] linting cleanup --- .../Data/CustomerCenterData.swift | 32 +++++------ .../Data/CustomerCenterTestData.swift | 6 +-- .../Data/SubscriptionInformation.swift | 2 + .../CustomerCenter/URLUtilities.swift | 2 +- .../CustomerCenterViewModel.swift | 8 +-- .../ManageSubscriptionsViewModel.swift | 54 +++++++++++-------- .../{ => Views}/CustomerCenterView.swift | 15 ++++-- .../{ => Views}/ManageSubscriptionsView.swift | 27 ++++++---- .../{ => Views}/NoSubscriptionsView.swift | 4 +- .../{ => Views}/RestorePurchasesAlert.swift | 33 ++++++++---- .../{ => Views}/WrongPlatformView.swift | 8 +-- 11 files changed, 115 insertions(+), 76 deletions(-) rename RevenueCatUI/CustomerCenter/{ => ViewModels}/CustomerCenterViewModel.swift (82%) rename RevenueCatUI/CustomerCenter/{ => ViewModels}/ManageSubscriptionsViewModel.swift (61%) rename RevenueCatUI/CustomerCenter/{ => Views}/CustomerCenterView.swift (87%) rename RevenueCatUI/CustomerCenter/{ => Views}/ManageSubscriptionsView.swift (88%) rename RevenueCatUI/CustomerCenter/{ => Views}/NoSubscriptionsView.swift (92%) rename RevenueCatUI/CustomerCenter/{ => Views}/RestorePurchasesAlert.swift (81%) rename RevenueCatUI/CustomerCenter/{ => Views}/WrongPlatformView.swift (95%) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index 433df6ef6b..1168339dff 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -15,15 +15,15 @@ struct CustomerCenterData: Decodable { let supportEmail: String let appearance: Appearance - struct HelpPath: Decodable { + enum HelpPathType: String, Decodable { + case missingPurchase = "MISSING_PURCHASE" + case refundRequest = "REFUND_REQUEST" + case changePlans = "CHANGE_PLANS" + case cancel = "CANCEL" + case unknown + } - enum HelpPathType: String, Decodable { - case missingPurchase = "MISSING_PURCHASE" - case refundRequest = "REFUND_REQUEST" - case changePlans = "CHANGE_PLANS" - case cancel = "CANCEL" - case unknown - } + struct HelpPath: Decodable { let id: String let title: LocalizedString @@ -35,7 +35,7 @@ struct CustomerCenterData: Decodable { struct LocalizedString: Decodable { - let en_US: String + let enUS: String } @@ -48,22 +48,22 @@ struct CustomerCenterData: Decodable { struct Eligibility: Decodable { - let first_seen: String + let firstSeen: String } struct FeedbackSurvey: Decodable { let title: LocalizedString - let options: [Option] + let options: [FeedbackSurveyOption] - struct Option: Decodable { + } - let id: String - let title: LocalizedString - let promotionalOffer: PromotionalOffer? + struct FeedbackSurveyOption: Decodable { - } + let id: String + let title: LocalizedString + let promotionalOffer: PromotionalOffer? } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift index ad824cb0c1..a7b3da029c 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -25,7 +25,7 @@ enum CustomerCenterTestData { type: .refundRequest, promotionalOffer: CustomerCenterData.PromotionalOffer( iosOfferId: "rc-refund-offer", - eligibility: CustomerCenterData.Eligibility(first_seen: "> 30") + eligibility: CustomerCenterData.Eligibility(firstSeen: "> 30") ), feedbackSurvey: nil ), @@ -49,7 +49,7 @@ enum CustomerCenterTestData { title: CustomerCenterData.LocalizedString(en_US: "Too expensive"), promotionalOffer: CustomerCenterData.PromotionalOffer( iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterData.Eligibility(first_seen: "> 14") + eligibility: CustomerCenterData.Eligibility(firstSeen: "> 14") ) ), CustomerCenterData.FeedbackSurvey.Option( @@ -57,7 +57,7 @@ enum CustomerCenterTestData { title: CustomerCenterData.LocalizedString(en_US: "Don't use the app"), promotionalOffer: CustomerCenterData.PromotionalOffer( iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterData.Eligibility(first_seen: "> 7") + eligibility: CustomerCenterData.Eligibility(firstSeen: "> 7") ) ), CustomerCenterData.FeedbackSurvey.Option( diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index 4cf5cb8c3e..bb504c14c1 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -8,6 +8,7 @@ import Foundation struct SubscriptionInformation { + let title: String let duration: String let price: String @@ -19,4 +20,5 @@ struct SubscriptionInformation { var renewalString: String { return active ? (willRenew ? "Renews" : "Expires") : "Expired" } + } diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift index 13a48acea1..d56c260977 100644 --- a/RevenueCatUI/CustomerCenter/URLUtilities.swift +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -7,7 +7,7 @@ import Foundation -struct URLUtilities { +enum URLUtilities { static func createMailURL() -> URL? { let subject = "Support Request" diff --git a/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift similarity index 82% rename from RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift rename to RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index b2726a6251..19f6fc935e 100644 --- a/RevenueCatUI/CustomerCenter/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -24,9 +24,11 @@ class CustomerCenterViewModel: ObservableObject { } enum State { + case notLoaded case success case error(Error) + } var error: Error? @@ -53,14 +55,14 @@ class CustomerCenterViewModel: ObservableObject { do { let customerInfo = try await Purchases.shared.customerInfo() self.hasSubscriptions = customerInfo.activeSubscriptions.count > 0 - guard let firstActiveEntitlement: EntitlementInfo = customerInfo.entitlements.active.first?.value else { + guard let firstActiveEntitlementStore = customerInfo.entitlements.active.first?.value.store else { self.areSubscriptionsFromApple = false return } - self.areSubscriptionsFromApple = firstActiveEntitlement.store == .appStore || firstActiveEntitlement.store == .macAppStore + self.areSubscriptionsFromApple = + firstActiveEntitlementStore == .appStore || firstActiveEntitlementStore == .macAppStore } catch { - // TODO: log and handle maybe? self.state = .error(error) } } diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift similarity index 61% rename from RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift rename to RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index fe17049511..c3e57d8829 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -11,12 +11,6 @@ import RevenueCat @available(iOS 15.0, *) class ManageSubscriptionsViewModel: ObservableObject { - enum State { - case notLoaded - case success - case error(Error) - } - var isLoaded: Bool { if case .notLoaded = state { return false @@ -34,6 +28,14 @@ class ManageSubscriptionsViewModel: ObservableObject { var configuration: CustomerCenterData? var error: Error? + enum State { + + case notLoaded + case success + case error(Error) + + } + private(set) var state: State { didSet { if case let .error(stateError) = state { @@ -57,30 +59,35 @@ class ManageSubscriptionsViewModel: ObservableObject { state = .success } - func loadSubscriptionInformation() async throws { - guard let customerInfo = try? await Purchases.shared.customerInfo(), - let currentEntitlementDict = customerInfo.entitlements.active.first, - let subscribedProductID = try? await Purchases.shared.customerInfo().activeSubscriptions.first, - let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { - return + func loadSubscriptionInformation() async { + do { + let customerInfo = try await Purchases.shared.customerInfo() + guard let currentEntitlementDict = customerInfo.entitlements.active.first, + let subscribedProductID = customerInfo.activeSubscriptions.first, + let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { + return + } + let currentEntitlement = currentEntitlementDict.value + + self.subscriptionInformation = SubscriptionInformation( + title: subscribedProduct.localizedTitle, + duration: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", + price: subscribedProduct.localizedPriceString, + nextRenewal: "\(String(describing: currentEntitlement.expirationDate!))", + willRenew: currentEntitlement.willRenew, + productIdentifier: subscribedProductID, + active: currentEntitlement.isActive + ) + } catch { + self.state = .error(error) } - let currentEntitlement = currentEntitlementDict.value - - self.subscriptionInformation = SubscriptionInformation( - title: subscribedProduct.localizedTitle, - duration: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", - price: subscribedProduct.localizedPriceString, - nextRenewal: "\(String(describing: currentEntitlement.expirationDate!))", - willRenew: currentEntitlement.willRenew, - productIdentifier: subscribedProductID, - active: currentEntitlement.isActive - ) } } @available(iOS 15.0, *) private extension SubscriptionPeriod { + var durationTitle: String { switch self.unit { case .day: return "day" @@ -96,4 +103,5 @@ private extension SubscriptionPeriod { let pluralized = self.value > 1 ? periodString + "s" : periodString return pluralized } + } diff --git a/RevenueCatUI/CustomerCenter/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift similarity index 87% rename from RevenueCatUI/CustomerCenter/CustomerCenterView.swift rename to RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 45036570c7..af9b7e6b7c 100644 --- a/RevenueCatUI/CustomerCenter/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -9,17 +9,17 @@ import RevenueCat import SwiftUI @available(iOS 15.0, *) -public struct CustomerCenterView: View { +struct CustomerCenterView: View { @StateObject private var viewModel = CustomerCenterViewModel() - public init() { } + init() { } init(viewModel: CustomerCenterViewModel) { self._viewModel = .init(wrappedValue: viewModel) } - public var body: some View { + var body: some View { NavigationView { NavigationLink(destination: destinationView()) { Text("Billing and subscription help") @@ -34,7 +34,12 @@ public struct CustomerCenterView: View { } } - private func checkAndLoadSubscriptions() { +} + +@available(iOS 15.0, *) +private extension CustomerCenterView { + + func checkAndLoadSubscriptions() { if !viewModel.isLoaded { Task { await viewModel.loadHasSubscriptions() @@ -43,7 +48,7 @@ public struct CustomerCenterView: View { } @ViewBuilder - private func destinationView() -> some View { + func destinationView() -> some View { if viewModel.hasSubscriptions { if viewModel.areSubscriptionsFromApple { ManageSubscriptionsView() diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift similarity index 88% rename from RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift rename to RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index d11ff68994..c69500dc3f 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -9,7 +9,7 @@ import RevenueCat import SwiftUI @available(iOS 15.0, *) -public struct ManageSubscriptionsView: View { +struct ManageSubscriptionsView: View { @Environment(\.openURL) var openURL @@ -17,13 +17,13 @@ public struct ManageSubscriptionsView: View { @StateObject private var viewModel = ManageSubscriptionsViewModel() - public init() { } + init() { } init(viewModel: ManageSubscriptionsViewModel) { self._viewModel = .init(wrappedValue: viewModel) } - public var body: some View { + var body: some View { VStack { HeaderView() @@ -35,17 +35,22 @@ public struct ManageSubscriptionsView: View { Spacer() ManageSubscriptionsButtonsView(viewModel: viewModel, - openURL: openURL) + openURL: openURL) } .onAppear { checkAndLoadSubscriptionInformation() } } - private func checkAndLoadSubscriptionInformation() { +} + +@available(iOS 15.0, *) +private extension ManageSubscriptionsView { + + func checkAndLoadSubscriptionInformation() { if !viewModel.isLoaded { Task { - try! await viewModel.loadSubscriptionInformation() + await viewModel.loadSubscriptionInformation() } } } @@ -133,7 +138,9 @@ struct ManageSubscriptionsButtonsView: View { case .refundRequest: Task { guard let subscriptionInformation = self.viewModel.subscriptionInformation else { return } - let status = try await Purchases.shared.beginRefundRequest(forProduct: subscriptionInformation.productIdentifier) + let status = try await Purchases.shared.beginRefundRequest( + forProduct: subscriptionInformation.productIdentifier + ) switch status { case .error: self.viewModel.refundRequestStatus = "Error when requesting refund, try again" @@ -155,6 +162,7 @@ struct ManageSubscriptionsButtonsView: View { break } } + } #if DEBUG @@ -165,8 +173,9 @@ struct ManageSubscriptionsButtonsView: View { struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { - let viewModel = ManageSubscriptionsViewModel(configuration: CustomerCenterTestData.customerCenterData, - subscriptionInformation: CustomerCenterTestData.subscriptionInformation) + let viewModel = ManageSubscriptionsViewModel( + configuration: CustomerCenterTestData.customerCenterData, + subscriptionInformation: CustomerCenterTestData.subscriptionInformation) ManageSubscriptionsView(viewModel: viewModel) } diff --git a/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift similarity index 92% rename from RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift rename to RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 40c274538d..14bae02dc7 100644 --- a/RevenueCatUI/CustomerCenter/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -9,12 +9,12 @@ import RevenueCat import SwiftUI @available(iOS 15.0, *) -public struct NoSubscriptionsView: View { +struct NoSubscriptionsView: View { @Environment(\.dismiss) var dismiss @State private var showRestoreAlert: Bool = false - public var body: some View { + var body: some View { VStack { Text("No Subscriptions found") .font(.title) diff --git a/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift similarity index 81% rename from RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift rename to RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 0e53f8818e..dab3136a4c 100644 --- a/RevenueCatUI/CustomerCenter/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -10,26 +10,34 @@ import RevenueCat import SwiftUI @available(iOS 15.0, *) -public struct RestorePurchasesAlert: ViewModifier { - @Binding var isPresented: Bool - @State private var alertType: AlertType = .restorePurchases +struct RestorePurchasesAlert: ViewModifier { + + @Binding + var isPresented: Bool + @Environment(\.openURL) + var openURL + + @State + private var alertType: AlertType = .restorePurchases + @Environment(\.dismiss) + private var dismiss enum AlertType: Identifiable { case purchasesRecovered, purchasesNotFound, restorePurchases var id: Self { self } } - @Environment(\.dismiss) private var dismiss - @Environment(\.openURL) var openURL - - public func body(content: Content) -> some View { + func body(content: Content) -> some View { content .alert(isPresented: $isPresented) { switch self.alertType { case .restorePurchases: Alert( title: Text("Restore purchases"), - message: Text("Let’s take a look! We’re going to check your Apple account for missing purchases."), + message: Text( + """ + Let’s take a look! We’re going to check your Apple account for missing purchases. + """), primaryButton: .default(Text("Check past purchases"), action: { Task { guard let customerInfo = try? await Purchases.shared.restorePurchases() else { @@ -72,7 +80,12 @@ public struct RestorePurchasesAlert: ViewModifier { } } - private func setAlertType(_ newType: AlertType) { +} + +@available(iOS 15.0, *) +private extension RestorePurchasesAlert { + + func setAlertType(_ newType: AlertType) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.alertType = newType self.isPresented = true @@ -82,7 +95,7 @@ public struct RestorePurchasesAlert: ViewModifier { } @available(iOS 15.0, *) -public extension View { +extension View { func restorePurchasesAlert(isPresented: Binding) -> some View { self.modifier(RestorePurchasesAlert(isPresented: isPresented)) } diff --git a/RevenueCatUI/CustomerCenter/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift similarity index 95% rename from RevenueCatUI/CustomerCenter/WrongPlatformView.swift rename to RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index bff98d170c..c658929edb 100644 --- a/RevenueCatUI/CustomerCenter/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -10,7 +10,7 @@ import RevenueCat import SwiftUI @available(iOS 15.0, *) -public struct WrongPlatformView: View { +struct WrongPlatformView: View { @State private var store: Store? @@ -18,15 +18,15 @@ public struct WrongPlatformView: View { @Environment(\.openURL) private var openURL - public init() { + init() { self._store = State(initialValue: nil) } - public init(store: Store) { + init(store: Store) { self._store = State(initialValue: store) } - public var body: some View { + var body: some View { VStack { switch store { From 8b241b3e655eb5d69a9d91983fa00c0a5860df45 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 08:51:24 +0200 Subject: [PATCH 04/50] update email --- RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift index a7b3da029c..04ed083787 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -70,7 +70,7 @@ enum CustomerCenterTestData { ) ], title: CustomerCenterData.LocalizedString(en_US: "How can we help?"), - supportEmail: "ryan@revenuecat.com", + supportEmail: "support@revenuecat.com", appearance: CustomerCenterData.Appearance( mode: "SYSTEM", light: "#000000", From 66ff74810cca52628aa327946bfe9e86645a867a Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 09:08:53 +0200 Subject: [PATCH 05/50] fix compilation --- RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift | 5 +++-- .../CustomerCenter/Data/CustomerCenterTestData.swift | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index 1168339dff..14120d2f23 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -34,8 +34,9 @@ struct CustomerCenterData: Decodable { } struct LocalizedString: Decodable { - - let enUS: String + + // swiftlint:disable:next identifier_name + let en_US: String } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift index 04ed083787..66c42ca4f1 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -44,7 +44,7 @@ enum CustomerCenterTestData { feedbackSurvey: CustomerCenterData.FeedbackSurvey( title: CustomerCenterData.LocalizedString(en_US: "Why are you cancelling?"), options: [ - CustomerCenterData.FeedbackSurvey.Option( + CustomerCenterData.FeedbackSurveyOption( id: "iewrthals", title: CustomerCenterData.LocalizedString(en_US: "Too expensive"), promotionalOffer: CustomerCenterData.PromotionalOffer( @@ -52,7 +52,7 @@ enum CustomerCenterTestData { eligibility: CustomerCenterData.Eligibility(firstSeen: "> 14") ) ), - CustomerCenterData.FeedbackSurvey.Option( + CustomerCenterData.FeedbackSurveyOption( id: "qklpadsfj", title: CustomerCenterData.LocalizedString(en_US: "Don't use the app"), promotionalOffer: CustomerCenterData.PromotionalOffer( @@ -60,7 +60,7 @@ enum CustomerCenterTestData { eligibility: CustomerCenterData.Eligibility(firstSeen: "> 7") ) ), - CustomerCenterData.FeedbackSurvey.Option( + CustomerCenterData.FeedbackSurveyOption( id: "jargnapocps", title: CustomerCenterData.LocalizedString(en_US: "Bought by mistake"), promotionalOffer: nil From a9dd3a1af89cfede84bbee4352d32987d5e17f1a Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 16:35:37 +0200 Subject: [PATCH 06/50] Update RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift Co-authored-by: Will Taylor --- RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index 14120d2f23..6304458e49 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -1,5 +1,5 @@ // -// File.swift +// CustomerCenterData.swift // // // Created by Cesar de la Vega on 28/5/24. From 3526ba4f5c11ceafc3c7d89f6c5d9e2c2cc99794 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 16:43:16 +0200 Subject: [PATCH 07/50] rename areSubscriptionsFromApple --- .../ViewModels/CustomerCenterViewModel.swift | 8 ++++---- .../CustomerCenter/Views/CustomerCenterView.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 19f6fc935e..576c9a53ea 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -14,7 +14,7 @@ class CustomerCenterViewModel: ObservableObject { @Published var hasSubscriptions: Bool = false @Published - var areSubscriptionsFromApple: Bool = false + var subscriptionsAreFromApple: Bool = false var isLoaded: Bool { if case .notLoaded = state { @@ -47,7 +47,7 @@ class CustomerCenterViewModel: ObservableObject { init(hasSubscriptions: Bool = false, areSubscriptionsFromApple: Bool = false) { self.hasSubscriptions = hasSubscriptions - self.areSubscriptionsFromApple = areSubscriptionsFromApple + self.subscriptionsAreFromApple = areSubscriptionsFromApple self.state = .success } @@ -56,11 +56,11 @@ class CustomerCenterViewModel: ObservableObject { let customerInfo = try await Purchases.shared.customerInfo() self.hasSubscriptions = customerInfo.activeSubscriptions.count > 0 guard let firstActiveEntitlementStore = customerInfo.entitlements.active.first?.value.store else { - self.areSubscriptionsFromApple = false + self.subscriptionsAreFromApple = false return } - self.areSubscriptionsFromApple = + self.subscriptionsAreFromApple = firstActiveEntitlementStore == .appStore || firstActiveEntitlementStore == .macAppStore } catch { self.state = .error(error) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index af9b7e6b7c..68e5d2d707 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -50,7 +50,7 @@ private extension CustomerCenterView { @ViewBuilder func destinationView() -> some View { if viewModel.hasSubscriptions { - if viewModel.areSubscriptionsFromApple { + if viewModel.subscriptionsAreFromApple { ManageSubscriptionsView() } else { WrongPlatformView() @@ -70,7 +70,7 @@ private extension CustomerCenterView { struct CustomerCenterView_Previews: PreviewProvider { static var previews: some View { - let viewModel = CustomerCenterViewModel(hasSubscriptions: false, areSubscriptionsFromApple: false) + let viewModel = CustomerCenterViewModel(hasSubscriptions: false, subscriptionsAreFromApple: false) CustomerCenterView(viewModel: viewModel) } From 8ce5ee9dbd7a60b4cd052389f862a29ffeaa37d2 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 16:44:11 +0200 Subject: [PATCH 08/50] made error private --- .../CustomerCenter/ViewModels/CustomerCenterViewModel.swift | 2 +- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 576c9a53ea..e10a974f98 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -31,7 +31,7 @@ class CustomerCenterViewModel: ObservableObject { } - var error: Error? + private var error: Error? private(set) var state: State { didSet { diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 68e5d2d707..9289972281 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -70,7 +70,7 @@ private extension CustomerCenterView { struct CustomerCenterView_Previews: PreviewProvider { static var previews: some View { - let viewModel = CustomerCenterViewModel(hasSubscriptions: false, subscriptionsAreFromApple: false) + let viewModel = CustomerCenterViewModel(hasSubscriptions: false, areSubscriptionsFromApple: false) CustomerCenterView(viewModel: viewModel) } From 248e9a12fa03a87c4b16b885c992c75e8d841e38 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 16:45:48 +0200 Subject: [PATCH 09/50] made state Published --- .../ViewModels/CustomerCenterViewModel.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index e10a974f98..d8ce02d7f5 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -15,6 +15,14 @@ class CustomerCenterViewModel: ObservableObject { var hasSubscriptions: Bool = false @Published var subscriptionsAreFromApple: Bool = false + @Published + var state: State { + didSet { + if case let .error(stateError) = state { + self.error = stateError + } + } + } var isLoaded: Bool { if case .notLoaded = state { @@ -33,14 +41,6 @@ class CustomerCenterViewModel: ObservableObject { private var error: Error? - private(set) var state: State { - didSet { - if case let .error(stateError) = state { - self.error = stateError - } - } - } - init() { self.state = .notLoaded } From bf8b1cfcf88a2d3dff8904f44a77707c8790663e Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 17:05:08 +0200 Subject: [PATCH 10/50] created CustomerCenterError and feedback on ManageSubscriptionsViewModel --- .../Data/CustomerCenterError.swift | 34 +++++++++++++++++++ .../ManageSubscriptionsViewModel.swift | 22 ++++++------ RevenueCatUI/Data/Strings.swift | 4 +++ 3 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift new file mode 100644 index 0000000000..b4bc390e8c --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift @@ -0,0 +1,34 @@ +// +// CustomerCenterError.swift +// +// +// Created by Cesar de la Vega on 29/5/24. +// + +import Foundation + +/// Error produced when displaying the customer center. +enum CustomerCenterError: Error { + + /// Could not find information for an active subscription. + case couldNotFindSubscriptionInformation + +} + +extension CustomerCenterError: CustomNSError { + + var errorUserInfo: [String: Any] { + return [ + NSLocalizedDescriptionKey: self.description + ] + } + + private var description: String { + switch self { + case .couldNotFindSubscriptionInformation: + return "Could not find information for an active subscription." + } + } + +} + diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index c3e57d8829..0bbfeec8e1 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -20,13 +20,19 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published var subscriptionInformation: SubscriptionInformation? - @Published var refundRequestStatus: String? - @Published var configuration: CustomerCenterData? - var error: Error? + @Published var state: State { + didSet { + if case let .error(stateError) = state { + self.error = stateError + } + } + } + + private var error: Error? enum State { @@ -36,14 +42,6 @@ class ManageSubscriptionsViewModel: ObservableObject { } - private(set) var state: State { - didSet { - if case let .error(stateError) = state { - self.error = stateError - } - } - } - init() { state = .notLoaded } @@ -65,6 +63,8 @@ class ManageSubscriptionsViewModel: ObservableObject { guard let currentEntitlementDict = customerInfo.entitlements.active.first, let subscribedProductID = customerInfo.activeSubscriptions.first, let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { + Logger.warning(Strings.could_not_find_subscription_information) + self.state = .error(CustomerCenterError.couldNotFindSubscriptionInformation) return } let currentEntitlement = currentEntitlementDict.value diff --git a/RevenueCatUI/Data/Strings.swift b/RevenueCatUI/Data/Strings.swift index 4922e2db16..f420a1db87 100644 --- a/RevenueCatUI/Data/Strings.swift +++ b/RevenueCatUI/Data/Strings.swift @@ -40,6 +40,8 @@ enum Strings { case restore_purchases_with_empty_result case setting_restored_customer_info + case could_not_find_subscription_information + } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @@ -98,6 +100,8 @@ extension Strings: CustomStringConvertible { case .setting_restored_customer_info: return "Setting restored customer info" + case .could_not_find_subscription_information: + return "Could not find any active subscription's information" } } From aaa896b7aca82d5883e666bd392f8adb3719a072 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 17:06:21 +0200 Subject: [PATCH 11/50] remove init from CustomerCenterView --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 9289972281..dd563d7828 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -13,8 +13,6 @@ struct CustomerCenterView: View { @StateObject private var viewModel = CustomerCenterViewModel() - init() { } - init(viewModel: CustomerCenterViewModel) { self._viewModel = .init(wrappedValue: viewModel) } From 334cd5a00de59d522b4a35ea951410de2c888546 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 17:09:18 +0200 Subject: [PATCH 12/50] use task instead of onAppear --- .../CustomerCenter/Views/CustomerCenterView.swift | 10 ++++------ .../CustomerCenter/Views/ManageSubscriptionsView.swift | 10 ++++------ .../CustomerCenter/Views/WrongPlatformView.swift | 10 ++++------ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index dd563d7828..74ba58bfdd 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -27,8 +27,8 @@ struct CustomerCenterView: View { .cornerRadius(10) } } - .onAppear { - checkAndLoadSubscriptions() + .task { + await checkAndLoadSubscriptions() } } @@ -37,11 +37,9 @@ struct CustomerCenterView: View { @available(iOS 15.0, *) private extension CustomerCenterView { - func checkAndLoadSubscriptions() { + func checkAndLoadSubscriptions() async { if !viewModel.isLoaded { - Task { - await viewModel.loadHasSubscriptions() - } + await viewModel.loadHasSubscriptions() } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index c69500dc3f..b23d0d2e21 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -37,8 +37,8 @@ struct ManageSubscriptionsView: View { ManageSubscriptionsButtonsView(viewModel: viewModel, openURL: openURL) } - .onAppear { - checkAndLoadSubscriptionInformation() + .task { + await checkAndLoadSubscriptionInformation() } } @@ -47,11 +47,9 @@ struct ManageSubscriptionsView: View { @available(iOS 15.0, *) private extension ManageSubscriptionsView { - func checkAndLoadSubscriptionInformation() { + func checkAndLoadSubscriptionInformation() async { if !viewModel.isLoaded { - Task { - await viewModel.loadSubscriptionInformation() - } + await viewModel.loadSubscriptionInformation() } } diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index c658929edb..b3994f69f2 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -54,13 +54,11 @@ struct WrongPlatformView: View { .padding() } - .onAppear { + .task { if store == nil { - Task { - if let customerInfo = try? await Purchases.shared.customerInfo(), - let firstEntitlement = customerInfo.entitlements.active.first { - self.store = firstEntitlement.value.store - } + if let customerInfo = try? await Purchases.shared.customerInfo(), + let firstEntitlement = customerInfo.entitlements.active.first { + self.store = firstEntitlement.value.store } } } From c3cea29b1b6ef04997ac842264e6c5a9c4632d40 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 17:13:34 +0200 Subject: [PATCH 13/50] Apply suggestions from code review Co-authored-by: James Borthwick <109382862+jamesrb1@users.noreply.github.com> --- .../Views/ManageSubscriptionsView.swift | 11 ++++------- .../CustomerCenter/Views/WrongPlatformView.swift | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index b23d0d2e21..a511be6e99 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -73,8 +73,7 @@ struct SubscriptionDetailsView: View { VStack(alignment: .leading, spacing: 8) { Text("\(subscriptionInformation.title) - \(subscriptionInformation.duration)") .font(.subheadline) - .padding(.horizontal) - .padding(.top) + .padding([.horizontal, .top]) Text("\(subscriptionInformation.price)") .font(.caption) @@ -84,16 +83,14 @@ struct SubscriptionDetailsView: View { Text("\(subscriptionInformation.renewalString): \(subscriptionInformation.nextRenewal)") .font(.caption) .foregroundColor(Color.gray) - .padding(.horizontal) - .padding(.bottom) + .padding([.horizontal, .bottom]) if let refundRequestStatus = refundRequestStatus { Text("Refund request status: \(refundRequestStatus)") .font(.caption) .bold() .foregroundColor(Color.gray) - .padding(.horizontal) - .padding(.bottom) + .padding([.horizontal, .bottom]) } } } @@ -103,7 +100,7 @@ struct SubscriptionDetailsView: View { struct ManageSubscriptionsButtonsView: View { @ObservedObject - var viewModel: ManageSubscriptionsViewModel + private(set) var viewModel: ManageSubscriptionsViewModel let openURL: OpenURLAction @State private var showRestoreAlert: Bool = false diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index b3994f69f2..8934d1dc4a 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -22,7 +22,7 @@ struct WrongPlatformView: View { self._store = State(initialValue: nil) } - init(store: Store) { + fileprivate init(store: Store) { self._store = State(initialValue: store) } From ba9c5e280dba5000b15b25df9751a1387405de78 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 29 May 2024 17:15:06 +0200 Subject: [PATCH 14/50] inits cleanup --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 2 +- RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift | 2 +- RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 74ba58bfdd..e7e509605e 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -13,7 +13,7 @@ struct CustomerCenterView: View { @StateObject private var viewModel = CustomerCenterViewModel() - init(viewModel: CustomerCenterViewModel) { + fileprivate init(viewModel: CustomerCenterViewModel) { self._viewModel = .init(wrappedValue: viewModel) } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index a511be6e99..5d9daa7df0 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -19,7 +19,7 @@ struct ManageSubscriptionsView: View { init() { } - init(viewModel: ManageSubscriptionsViewModel) { + fileprivate init(viewModel: ManageSubscriptionsViewModel) { self._viewModel = .init(wrappedValue: viewModel) } diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 8934d1dc4a..74559f0c97 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -19,7 +19,6 @@ struct WrongPlatformView: View { private var openURL init() { - self._store = State(initialValue: nil) } fileprivate init(store: Store) { From a08b4274c8c712aabcecbd08b4f5bb04835e5bbb Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 30 May 2024 12:42:55 +0200 Subject: [PATCH 15/50] move handleAction to viewModel --- .../ManageSubscriptionsViewModel.swift | 34 ++++++++++++++++ .../Views/ManageSubscriptionsView.swift | 39 ++----------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 0bbfeec8e1..11a149598f 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -24,6 +24,8 @@ class ManageSubscriptionsViewModel: ObservableObject { var refundRequestStatus: String? @Published var configuration: CustomerCenterData? + @Published + var showRestoreAlert: Bool = false @Published var state: State { didSet { if case let .error(stateError) = state { @@ -83,6 +85,38 @@ class ManageSubscriptionsViewModel: ObservableObject { } } + func handleAction(for path: CustomerCenterData.HelpPath) { + switch path.type { + case .missingPurchase: + self.showRestoreAlert = true + case .refundRequest: + Task { + guard let subscriptionInformation = self.subscriptionInformation else { return } + let status = try await Purchases.shared.beginRefundRequest( + forProduct: subscriptionInformation.productIdentifier + ) + switch status { + case .error: + self.refundRequestStatus = "Error when requesting refund, try again" + case .success: + self.refundRequestStatus = "Refund granted successfully!" + case .userCancelled: + self.refundRequestStatus = "Refund canceled" + } + } + case .changePlans: + Task { + try await Purchases.shared.showManageSubscriptions() + } + case .cancel: + Task { + try await Purchases.shared.showManageSubscriptions() + } + default: + break + } + } + } @available(iOS 15.0, *) diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 5d9daa7df0..0ab02043a4 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -101,18 +101,17 @@ struct ManageSubscriptionsButtonsView: View { @ObservedObject private(set) var viewModel: ManageSubscriptionsViewModel + let openURL: OpenURLAction - @State - private var showRestoreAlert: Bool = false var body: some View { VStack(spacing: 16) { if let configuration = viewModel.configuration { ForEach(configuration.paths, id: \.id) { path in Button(path.title.en_US) { - handleAction(for: path) + viewModel.handleAction(for: path) } - .restorePurchasesAlert(isPresented: self.$showRestoreAlert) + .restorePurchasesAlert(isPresented: $viewModel.showRestoreAlert) .buttonStyle(ManageSubscriptionsButtonStyle()) } } @@ -126,38 +125,6 @@ struct ManageSubscriptionsButtonsView: View { } } - private func handleAction(for path: CustomerCenterData.HelpPath) { - switch path.type { - case .missingPurchase: - self.showRestoreAlert = true - case .refundRequest: - Task { - guard let subscriptionInformation = self.viewModel.subscriptionInformation else { return } - let status = try await Purchases.shared.beginRefundRequest( - forProduct: subscriptionInformation.productIdentifier - ) - switch status { - case .error: - self.viewModel.refundRequestStatus = "Error when requesting refund, try again" - case .success: - self.viewModel.refundRequestStatus = "Refund granted successfully!" - case .userCancelled: - self.viewModel.refundRequestStatus = "Refund canceled" - } - } - case .changePlans: - Task { - try await Purchases.shared.showManageSubscriptions() - } - case .cancel: - Task { - try await Purchases.shared.showManageSubscriptions() - } - default: - break - } - } - } #if DEBUG From 2f6649627c45de52420355c8669f10518da8f30f Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 4 Jun 2024 14:19:48 +0200 Subject: [PATCH 16/50] more user-friendly name --- RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 14bae02dc7..2148486033 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -19,7 +19,7 @@ struct NoSubscriptionsView: View { Text("No Subscriptions found") .font(.title) .padding() - Text("We can try checking your Apple account for any previously purchased products") + Text("We can try checking your Apple account for any previous purchases") .font(.body) .padding() From 651762750cb921c9f1267e67fbbcfa12bde68f29 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 4 Jun 2024 14:43:30 +0200 Subject: [PATCH 17/50] use morphology --- .../ViewModels/ManageSubscriptionsViewModel.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 11a149598f..73d633f26e 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -133,9 +133,7 @@ private extension SubscriptionPeriod { } func periodTitle() -> String { - let periodString = "\(self.value) \(self.durationTitle)" - let pluralized = self.value > 1 ? periodString + "s" : periodString - return pluralized + return "^[\(self.value) \(self.durationTitle)](inflect: true)" } } From 0986bcff143031036755d1499965f104489a84f1 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 4 Jun 2024 14:46:27 +0200 Subject: [PATCH 18/50] actually remove it's not needed --- .../ViewModels/ManageSubscriptionsViewModel.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 73d633f26e..7e10fa9dce 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -132,8 +132,4 @@ private extension SubscriptionPeriod { } } - func periodTitle() -> String { - return "^[\(self.value) \(self.durationTitle)](inflect: true)" - } - } From 960ca81a9659ded26a9e5e4a0cb1b69d5baf8a4f Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 4 Jun 2024 14:49:08 +0200 Subject: [PATCH 19/50] better wording --- .../Views/WrongPlatformView.swift | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 74559f0c97..f9a7c2314c 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -32,7 +32,7 @@ struct WrongPlatformView: View { case .appStore, .macAppStore, .playStore, .amazon: let platformName = humanReadablePlatformName(store: store!) - Text("Your subscription is being billed through \(platformName).") + Text("Your subscription is a \(platformName) subscription.") .font(.title) .padding() Text("Go the app settings on \(platformName) to manage your subscription and billing.") @@ -64,25 +64,23 @@ struct WrongPlatformView: View { } private func humanReadablePlatformName(store: Store) -> String { - switch store { - case .appStore, .macAppStore: - return "Apple App Store" - case .playStore: - return "Google Play Store" - case .stripe: - return "Stripe" - case .promotional: - return "Promotional" - case .amazon: - return "Amazon Appstore" - case .rcBilling: - return "RCBilling" - case .external: - return "External" - case .unknownStore: - return "Unknown" - } + switch store { + case .appStore, .macAppStore: + return "Apple App Store" + case .playStore: + return "Google Play Store" + case .stripe, + .rcBilling, + .external: + return "Web" + case .promotional: + return "Free" + case .amazon: + return "Amazon Appstore" + case .unknownStore: + return "Unknown" } + } } From 553edba494998359e5add7abcf013f8dbc16143d Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 4 Jun 2024 18:00:26 +0200 Subject: [PATCH 20/50] create HelpPathDetail --- .../Data/CustomerCenterData.swift | 44 +++++------- .../Data/CustomerCenterTestData.swift | 68 +++++++++---------- .../CustomerCenter/URLUtilities.swift | 1 + 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index 6304458e49..5dd7e03e55 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -7,7 +7,7 @@ import Foundation -struct CustomerCenterData: Decodable { +struct CustomerCenterData { let id: String let paths: [HelpPath] @@ -15,7 +15,7 @@ struct CustomerCenterData: Decodable { let supportEmail: String let appearance: Appearance - enum HelpPathType: String, Decodable { + enum HelpPathType: String { case missingPurchase = "MISSING_PURCHASE" case refundRequest = "REFUND_REQUEST" case changePlans = "CHANGE_PLANS" @@ -23,44 +23,50 @@ struct CustomerCenterData: Decodable { case unknown } - struct HelpPath: Decodable { + enum HelpPathDetail { + + case promotionalOffer(PromotionalOffer) + case feedbackSurvey(FeedbackSurvey) + + } + + struct HelpPath { let id: String let title: LocalizedString let type: HelpPathType - let promotionalOffer: PromotionalOffer? - let feedbackSurvey: FeedbackSurvey? + let detail: HelpPathDetail? } - struct LocalizedString: Decodable { - + struct LocalizedString { + // swiftlint:disable:next identifier_name let en_US: String } - struct PromotionalOffer: Decodable { + struct PromotionalOffer { let iosOfferId: String let eligibility: Eligibility } - struct Eligibility: Decodable { + struct Eligibility { let firstSeen: String } - struct FeedbackSurvey: Decodable { + struct FeedbackSurvey { let title: LocalizedString let options: [FeedbackSurveyOption] } - struct FeedbackSurveyOption: Decodable { + struct FeedbackSurveyOption { let id: String let title: LocalizedString @@ -68,7 +74,7 @@ struct CustomerCenterData: Decodable { } - struct Appearance: Decodable { + struct Appearance { let mode: String let light: String @@ -77,17 +83,3 @@ struct CustomerCenterData: Decodable { } } - -extension CustomerCenterData { - - static func decode(_ json: String) -> CustomerCenterData { - let data = Data(json.utf8) - let decoder = JSONDecoder() - do { - return try decoder.decode(CustomerCenterData.self, from: data) - } catch { - fatalError("Failed to decode JSON: \(error)") - } - } - -} diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift index 66c42ca4f1..921a1346e2 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -16,57 +16,53 @@ enum CustomerCenterTestData { id: "ownmsldfow", title: CustomerCenterData.LocalizedString(en_US: "Didn't receive purchase"), type: .missingPurchase, - promotionalOffer: nil, - feedbackSurvey: nil + detail: nil ), CustomerCenterData.HelpPath( id: "nwodkdnfaoeb", title: CustomerCenterData.LocalizedString(en_US: "Request a refund"), type: .refundRequest, - promotionalOffer: CustomerCenterData.PromotionalOffer( - iosOfferId: "rc-refund-offer", - eligibility: CustomerCenterData.Eligibility(firstSeen: "> 30") - ), - feedbackSurvey: nil + detail: .promotionalOffer(CustomerCenterData.PromotionalOffer( + iosOfferId: "rc-refund-offer", + eligibility: CustomerCenterData.Eligibility(firstSeen: "> 30") + )) ), CustomerCenterData.HelpPath( id: "nfoaiodifj9", title: CustomerCenterData.LocalizedString(en_US: "Change plans"), type: .changePlans, - promotionalOffer: nil, - feedbackSurvey: nil + detail: nil ), CustomerCenterData.HelpPath( id: "jnkasldfhas", title: CustomerCenterData.LocalizedString(en_US: "Cancel subscription"), type: .cancel, - promotionalOffer: nil, - feedbackSurvey: CustomerCenterData.FeedbackSurvey( - title: CustomerCenterData.LocalizedString(en_US: "Why are you cancelling?"), - options: [ - CustomerCenterData.FeedbackSurveyOption( - id: "iewrthals", - title: CustomerCenterData.LocalizedString(en_US: "Too expensive"), - promotionalOffer: CustomerCenterData.PromotionalOffer( - iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterData.Eligibility(firstSeen: "> 14") - ) - ), - CustomerCenterData.FeedbackSurveyOption( - id: "qklpadsfj", - title: CustomerCenterData.LocalizedString(en_US: "Don't use the app"), - promotionalOffer: CustomerCenterData.PromotionalOffer( - iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterData.Eligibility(firstSeen: "> 7") - ) - ), - CustomerCenterData.FeedbackSurveyOption( - id: "jargnapocps", - title: CustomerCenterData.LocalizedString(en_US: "Bought by mistake"), - promotionalOffer: nil - ) - ] - ) + detail: .feedbackSurvey(CustomerCenterData.FeedbackSurvey( + title: CustomerCenterData.LocalizedString(en_US: "Why are you cancelling?"), + options: [ + CustomerCenterData.FeedbackSurveyOption( + id: "iewrthals", + title: CustomerCenterData.LocalizedString(en_US: "Too expensive"), + promotionalOffer: CustomerCenterData.PromotionalOffer( + iosOfferId: "rc-cancel-offer", + eligibility: CustomerCenterData.Eligibility(firstSeen: "> 14") + ) + ), + CustomerCenterData.FeedbackSurveyOption( + id: "qklpadsfj", + title: CustomerCenterData.LocalizedString(en_US: "Don't use the app"), + promotionalOffer: CustomerCenterData.PromotionalOffer( + iosOfferId: "rc-cancel-offer", + eligibility: CustomerCenterData.Eligibility(firstSeen: "> 7") + ) + ), + CustomerCenterData.FeedbackSurveyOption( + id: "jargnapocps", + title: CustomerCenterData.LocalizedString(en_US: "Bought by mistake"), + promotionalOffer: nil + ) + ] + )) ) ], title: CustomerCenterData.LocalizedString(en_US: "How can we help?"), diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift index d56c260977..19e674ae00 100644 --- a/RevenueCatUI/CustomerCenter/URLUtilities.swift +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -15,6 +15,7 @@ enum URLUtilities { let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + // TODO: make configurable let urlString = "mailto:support@revenuecat.com?subject=\(encodedSubject)&body=\(encodedBody)" return URL(string: urlString) } From 7e75fcdf27e25f61f2017b36da9dddbc5ee38749 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 4 Jun 2024 18:36:43 +0200 Subject: [PATCH 21/50] fix some data types --- .../CustomerCenter/Data/CustomerCenterData.swift | 4 ++-- .../CustomerCenter/Data/CustomerCenterError.swift | 1 - .../CustomerCenter/Data/CustomerCenterTestData.swift | 10 +++++++--- .../CustomerCenter/Data/SubscriptionInformation.swift | 2 +- RevenueCatUI/CustomerCenter/URLUtilities.swift | 1 + .../ViewModels/ManageSubscriptionsViewModel.swift | 4 +++- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 10 ++++++---- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index 5dd7e03e55..6578b1dde4 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -6,6 +6,7 @@ // import Foundation +import RevenueCat struct CustomerCenterData { @@ -77,8 +78,7 @@ struct CustomerCenterData { struct Appearance { let mode: String - let light: String - let dark: String + let color: PaywallColor } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift index b4bc390e8c..882ed36d02 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift @@ -31,4 +31,3 @@ extension CustomerCenterError: CustomNSError { } } - diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift index 921a1346e2..959ff9cd00 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -6,9 +6,11 @@ // import Foundation +import RevenueCat enum CustomerCenterTestData { + @available(iOS 14.0, *) static let customerCenterData = CustomerCenterData( id: "ccenter_lasdlfalaowpwp", paths: [ @@ -69,8 +71,10 @@ enum CustomerCenterTestData { supportEmail: "support@revenuecat.com", appearance: CustomerCenterData.Appearance( mode: "SYSTEM", - light: "#000000", - dark: "#ffffff" + color: PaywallColor( + light: PaywallColor(stringLiteral: "#000000"), + dark: PaywallColor(stringLiteral: "#ffffff") + ) ) ) @@ -78,7 +82,7 @@ enum CustomerCenterTestData { title: "Basic", duration: "Monthly", price: "$4.99 / month", - nextRenewal: "June 1st, 2024", + nextRenewal: Date(), willRenew: true, productIdentifier: "product_id", active: true diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index bb504c14c1..01e79f3e36 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -12,7 +12,7 @@ struct SubscriptionInformation { let title: String let duration: String let price: String - let nextRenewal: String + let nextRenewal: Date? let willRenew: Bool let productIdentifier: String let active: Bool diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift index 19e674ae00..2bbedd4924 100644 --- a/RevenueCatUI/CustomerCenter/URLUtilities.swift +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -15,6 +15,7 @@ enum URLUtilities { let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + // swiftlint:disable:next todo // TODO: make configurable let urlString = "mailto:support@revenuecat.com?subject=\(encodedSubject)&body=\(encodedBody)" return URL(string: urlString) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 7e10fa9dce..54c5299f05 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -71,11 +71,13 @@ class ManageSubscriptionsViewModel: ObservableObject { } let currentEntitlement = currentEntitlementDict.value + // swiftlint:disable:next todo + // TODO: support non-consumables self.subscriptionInformation = SubscriptionInformation( title: subscribedProduct.localizedTitle, duration: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", price: subscribedProduct.localizedPriceString, - nextRenewal: "\(String(describing: currentEntitlement.expirationDate!))", + nextRenewal: currentEntitlement.expirationDate!, willRenew: currentEntitlement.willRenew, productIdentifier: subscribedProductID, active: currentEntitlement.isActive diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 0ab02043a4..2f57488d6d 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -80,10 +80,12 @@ struct SubscriptionDetailsView: View { .foregroundColor(Color.gray) .padding(.horizontal) - Text("\(subscriptionInformation.renewalString): \(subscriptionInformation.nextRenewal)") - .font(.caption) - .foregroundColor(Color.gray) - .padding([.horizontal, .bottom]) + if let nextRenewal = subscriptionInformation.nextRenewal { + Text("\(subscriptionInformation.renewalString): \(String(describing: nextRenewal))") + .font(.caption) + .foregroundColor(Color.gray) + .padding([.horizontal, .bottom]) + } if let refundRequestStatus = refundRequestStatus { Text("Refund request status: \(refundRequestStatus)") From 2d2834ff3a60e56b24f088e36971212a939e0ddd Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 4 Jun 2024 18:54:18 +0200 Subject: [PATCH 22/50] add docs --- RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index 6578b1dde4..c1eb073c99 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -8,6 +8,9 @@ import Foundation import RevenueCat +/// Represents a color to be used by `RevenueCatUI` +public typealias RCColor = PaywallColor + struct CustomerCenterData { let id: String @@ -78,7 +81,7 @@ struct CustomerCenterData { struct Appearance { let mode: String - let color: PaywallColor + let color: RCColor } From dd020739949d8c3e997cca347fd38ae3f6755cb5 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 5 Jun 2024 16:52:16 +0200 Subject: [PATCH 23/50] mode is an enum --- RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift | 7 ++++++- .../CustomerCenter/Data/CustomerCenterTestData.swift | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index c1eb073c99..f52ecd2a66 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -11,6 +11,7 @@ import RevenueCat /// Represents a color to be used by `RevenueCatUI` public typealias RCColor = PaywallColor +// swiftlint:disable nesting struct CustomerCenterData { let id: String @@ -80,9 +81,13 @@ struct CustomerCenterData { struct Appearance { - let mode: String + let mode: Mode let color: RCColor + enum Mode: String { + case system = "SYSTEM" + } + } } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift index 959ff9cd00..b52f0c9129 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -70,7 +70,7 @@ enum CustomerCenterTestData { title: CustomerCenterData.LocalizedString(en_US: "How can we help?"), supportEmail: "support@revenuecat.com", appearance: CustomerCenterData.Appearance( - mode: "SYSTEM", + mode: .system, color: PaywallColor( light: PaywallColor(stringLiteral: "#000000"), dark: PaywallColor(stringLiteral: "#ffffff") From a1b470b0365409cb93bd0c71c8e16c87d2c328b8 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 13:21:26 +0200 Subject: [PATCH 24/50] cleanup and availability --- .../Data/CustomerCenterData.swift | 3 +- .../Data/CustomerCenterTestData.swift | 48 +++++++++++-------- .../ManageSubscriptionsButtonStyle.swift | 10 +++- .../ViewModels/CustomerCenterViewModel.swift | 5 +- .../ManageSubscriptionsViewModel.swift | 12 ++++- .../Views/CustomerCenterView.swift | 11 ++++- .../Views/ManageSubscriptionsView.swift | 35 +++++++++++--- .../Views/NoSubscriptionsView.swift | 18 +++++-- .../Views/RestorePurchasesAlert.swift | 15 ++++-- .../Views/WrongPlatformView.swift | 10 +++- 10 files changed, 123 insertions(+), 44 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift index f52ecd2a66..6194cb10da 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift @@ -82,7 +82,8 @@ struct CustomerCenterData { struct Appearance { let mode: Mode - let color: RCColor + let light: RCColor + let dark: RCColor enum Mode: String { case system = "SYSTEM" diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift index b52f0c9129..66d723c870 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift @@ -1,4 +1,12 @@ // +// 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 +// // TestData.swift // // @@ -14,35 +22,35 @@ enum CustomerCenterTestData { static let customerCenterData = CustomerCenterData( id: "ccenter_lasdlfalaowpwp", paths: [ - CustomerCenterData.HelpPath( + .init( id: "ownmsldfow", - title: CustomerCenterData.LocalizedString(en_US: "Didn't receive purchase"), + title: .init(en_US: "Didn't receive purchase"), type: .missingPurchase, detail: nil ), - CustomerCenterData.HelpPath( + .init( id: "nwodkdnfaoeb", - title: CustomerCenterData.LocalizedString(en_US: "Request a refund"), + title: .init(en_US: "Request a refund"), type: .refundRequest, - detail: .promotionalOffer(CustomerCenterData.PromotionalOffer( + detail: .promotionalOffer(.init( iosOfferId: "rc-refund-offer", - eligibility: CustomerCenterData.Eligibility(firstSeen: "> 30") + eligibility: .init(firstSeen: "> 30") )) ), - CustomerCenterData.HelpPath( + .init( id: "nfoaiodifj9", - title: CustomerCenterData.LocalizedString(en_US: "Change plans"), + title: .init(en_US: "Change plans"), type: .changePlans, detail: nil ), - CustomerCenterData.HelpPath( + .init( id: "jnkasldfhas", - title: CustomerCenterData.LocalizedString(en_US: "Cancel subscription"), + title: .init(en_US: "Cancel subscription"), type: .cancel, - detail: .feedbackSurvey(CustomerCenterData.FeedbackSurvey( - title: CustomerCenterData.LocalizedString(en_US: "Why are you cancelling?"), + detail: .feedbackSurvey(.init( + title: .init(en_US: "Why are you cancelling?"), options: [ - CustomerCenterData.FeedbackSurveyOption( + .init( id: "iewrthals", title: CustomerCenterData.LocalizedString(en_US: "Too expensive"), promotionalOffer: CustomerCenterData.PromotionalOffer( @@ -50,7 +58,7 @@ enum CustomerCenterTestData { eligibility: CustomerCenterData.Eligibility(firstSeen: "> 14") ) ), - CustomerCenterData.FeedbackSurveyOption( + .init( id: "qklpadsfj", title: CustomerCenterData.LocalizedString(en_US: "Don't use the app"), promotionalOffer: CustomerCenterData.PromotionalOffer( @@ -58,7 +66,7 @@ enum CustomerCenterTestData { eligibility: CustomerCenterData.Eligibility(firstSeen: "> 7") ) ), - CustomerCenterData.FeedbackSurveyOption( + .init( id: "jargnapocps", title: CustomerCenterData.LocalizedString(en_US: "Bought by mistake"), promotionalOffer: nil @@ -67,14 +75,12 @@ enum CustomerCenterTestData { )) ) ], - title: CustomerCenterData.LocalizedString(en_US: "How can we help?"), + title: .init(en_US: "How can we help?"), supportEmail: "support@revenuecat.com", - appearance: CustomerCenterData.Appearance( + appearance: .init( mode: .system, - color: PaywallColor( - light: PaywallColor(stringLiteral: "#000000"), - dark: PaywallColor(stringLiteral: "#ffffff") - ) + light: try! .init(stringRepresentation: "#000000"), + dark: try! .init(stringRepresentation: "#ffffff") ) ) diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index b3d650eed9..af999e2503 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -8,7 +8,10 @@ import Foundation import SwiftUI -@available(iOS 13.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct ManageSubscriptionsButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { @@ -22,7 +25,10 @@ struct ManageSubscriptionsButtonStyle: ButtonStyle { } -@available(iOS 13.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct CustomButtonStylePreview_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index d8ce02d7f5..b719ce6acc 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -8,7 +8,10 @@ import Foundation import RevenueCat -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) class CustomerCenterViewModel: ObservableObject { @Published diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 54c5299f05..71964e78cb 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -8,7 +8,10 @@ import Foundation import RevenueCat -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) class ManageSubscriptionsViewModel: ObservableObject { var isLoaded: Bool { @@ -92,6 +95,7 @@ class ManageSubscriptionsViewModel: ObservableObject { case .missingPurchase: self.showRestoreAlert = true case .refundRequest: + #if os(iOS) || targetEnvironment(macCatalyst) Task { guard let subscriptionInformation = self.subscriptionInformation else { return } let status = try await Purchases.shared.beginRefundRequest( @@ -106,6 +110,7 @@ class ManageSubscriptionsViewModel: ObservableObject { self.refundRequestStatus = "Refund canceled" } } + #endif case .changePlans: Task { try await Purchases.shared.showManageSubscriptions() @@ -121,7 +126,10 @@ class ManageSubscriptionsViewModel: ObservableObject { } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) private extension SubscriptionPeriod { var durationTitle: String { diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index e7e509605e..e016c0319b 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -8,7 +8,10 @@ import RevenueCat import SwiftUI -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct CustomerCenterView: View { @StateObject private var viewModel = CustomerCenterViewModel() @@ -34,7 +37,10 @@ struct CustomerCenterView: View { } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) private extension CustomerCenterView { func checkAndLoadSubscriptions() async { @@ -63,6 +69,7 @@ private extension CustomerCenterView { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) +@available(watchOS, unavailable) struct CustomerCenterView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 2f57488d6d..ff55ca9344 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -8,7 +8,10 @@ import RevenueCat import SwiftUI -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct ManageSubscriptionsView: View { @Environment(\.openURL) @@ -44,7 +47,10 @@ struct ManageSubscriptionsView: View { } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) private extension ManageSubscriptionsView { func checkAndLoadSubscriptionInformation() async { @@ -55,7 +61,10 @@ private extension ManageSubscriptionsView { } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct HeaderView: View { var body: some View { Text("How can we help?") @@ -64,7 +73,10 @@ struct HeaderView: View { } } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct SubscriptionDetailsView: View { let subscriptionInformation: SubscriptionInformation let refundRequestStatus: String? @@ -98,7 +110,10 @@ struct SubscriptionDetailsView: View { } } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct ManageSubscriptionsButtonsView: View { @ObservedObject @@ -109,7 +124,14 @@ struct ManageSubscriptionsButtonsView: View { var body: some View { VStack(spacing: 16) { if let configuration = viewModel.configuration { - ForEach(configuration.paths, id: \.id) { path in + let filteredPaths = configuration.paths.filter { path in + #if targetEnvironment(macCatalyst) + return path.type != .refundRequest + #else + return true + #endif + } + ForEach(filteredPaths, id: \.id) { path in Button(path.title.en_US) { viewModel.handleAction(for: path) } @@ -134,6 +156,7 @@ struct ManageSubscriptionsButtonsView: View { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) +@available(watchOS, unavailable) struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 2148486033..d22558b539 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -8,7 +8,10 @@ import RevenueCat import SwiftUI -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct NoSubscriptionsView: View { @Environment(\.dismiss) var dismiss @@ -41,7 +44,14 @@ struct NoSubscriptionsView: View { } -@available(iOS 15.0, *) -#Preview { - NoSubscriptionsView() +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +struct NoSubscriptionsView_Previews: PreviewProvider { + + static var previews: some View { + NoSubscriptionsView() + } + } diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index dab3136a4c..b60408b953 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -9,7 +9,10 @@ import Foundation import RevenueCat import SwiftUI -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct RestorePurchasesAlert: ViewModifier { @Binding @@ -82,7 +85,10 @@ struct RestorePurchasesAlert: ViewModifier { } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) private extension RestorePurchasesAlert { func setAlertType(_ newType: AlertType) { @@ -94,7 +100,10 @@ private extension RestorePurchasesAlert { } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) extension View { func restorePurchasesAlert(isPresented: Binding) -> some View { self.modifier(RestorePurchasesAlert(isPresented: isPresented)) diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index f9a7c2314c..b6e0dbf70f 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -9,7 +9,10 @@ import Foundation import RevenueCat import SwiftUI -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct WrongPlatformView: View { @State @@ -84,7 +87,10 @@ struct WrongPlatformView: View { } -@available(iOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) struct WrongPlatformView_Previews: PreviewProvider { static var previews: some View { From c350dd461f7b220a81612e0b7b699b65501efbb8 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 14:39:02 +0200 Subject: [PATCH 25/50] add headers and renames --- ...a.swift => CustomerCenterConfigData.swift} | 12 +++++++++-- ...ift => CustomerCenterConfigTestData.swift} | 20 +++++++++---------- .../Data/CustomerCenterError.swift | 8 ++++++++ .../Data/SubscriptionInformation.swift | 8 ++++++++ .../ManageSubscriptionsButtonStyle.swift | 8 ++++++++ .../CustomerCenter/URLUtilities.swift | 8 ++++++++ .../ViewModels/CustomerCenterViewModel.swift | 8 ++++++++ .../ManageSubscriptionsViewModel.swift | 16 +++++++++++---- .../Views/CustomerCenterView.swift | 8 ++++++++ .../Views/ManageSubscriptionsView.swift | 12 +++++++++-- .../Views/NoSubscriptionsView.swift | 8 ++++++++ .../Views/RestorePurchasesAlert.swift | 8 ++++++++ .../Views/WrongPlatformView.swift | 8 ++++++++ 13 files changed, 114 insertions(+), 18 deletions(-) rename RevenueCatUI/CustomerCenter/Data/{CustomerCenterData.swift => CustomerCenterConfigData.swift} (82%) rename RevenueCatUI/CustomerCenter/Data/{CustomerCenterTestData.swift => CustomerCenterConfigTestData.swift} (82%) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift similarity index 82% rename from RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift rename to RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift index 6194cb10da..106deed8e4 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift @@ -1,5 +1,13 @@ // -// CustomerCenterData.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 +// +// CustomerCenterConfigData.swift // // // Created by Cesar de la Vega on 28/5/24. @@ -12,7 +20,7 @@ import RevenueCat public typealias RCColor = PaywallColor // swiftlint:disable nesting -struct CustomerCenterData { +struct CustomerCenterConfigData { let id: String let paths: [HelpPath] diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift similarity index 82% rename from RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift rename to RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 66d723c870..f88cd73eda 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -7,7 +7,7 @@ // // https://opensource.org/licenses/MIT // -// TestData.swift +// CustomerCenterConfigTestData.swift // // // Created by Cesar de la Vega on 28/5/24. @@ -16,10 +16,10 @@ import Foundation import RevenueCat -enum CustomerCenterTestData { +enum CustomerCenterConfigTestData { @available(iOS 14.0, *) - static let customerCenterData = CustomerCenterData( + static let customerCenterData = CustomerCenterConfigData( id: "ccenter_lasdlfalaowpwp", paths: [ .init( @@ -52,23 +52,23 @@ enum CustomerCenterTestData { options: [ .init( id: "iewrthals", - title: CustomerCenterData.LocalizedString(en_US: "Too expensive"), - promotionalOffer: CustomerCenterData.PromotionalOffer( + title: CustomerCenterConfigData.LocalizedString(en_US: "Too expensive"), + promotionalOffer: CustomerCenterConfigData.PromotionalOffer( iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterData.Eligibility(firstSeen: "> 14") + eligibility: CustomerCenterConfigData.Eligibility(firstSeen: "> 14") ) ), .init( id: "qklpadsfj", - title: CustomerCenterData.LocalizedString(en_US: "Don't use the app"), - promotionalOffer: CustomerCenterData.PromotionalOffer( + title: CustomerCenterConfigData.LocalizedString(en_US: "Don't use the app"), + promotionalOffer: CustomerCenterConfigData.PromotionalOffer( iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterData.Eligibility(firstSeen: "> 7") + eligibility: CustomerCenterConfigData.Eligibility(firstSeen: "> 7") ) ), .init( id: "jargnapocps", - title: CustomerCenterData.LocalizedString(en_US: "Bought by mistake"), + title: CustomerCenterConfigData.LocalizedString(en_US: "Bought by mistake"), promotionalOffer: nil ) ] diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift index 882ed36d02..af5a2e1bfa 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift @@ -1,4 +1,12 @@ // +// 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 +// // CustomerCenterError.swift // // diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index 01e79f3e36..d04ae73ed3 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -1,4 +1,12 @@ // +// 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 +// // SubscriptionInformation.swift // // diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index af999e2503..e29701e198 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -1,4 +1,12 @@ // +// 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 +// // CustomButtonStyle.swift // // diff --git a/RevenueCatUI/CustomerCenter/URLUtilities.swift b/RevenueCatUI/CustomerCenter/URLUtilities.swift index 2bbedd4924..2de1de68a6 100644 --- a/RevenueCatUI/CustomerCenter/URLUtilities.swift +++ b/RevenueCatUI/CustomerCenter/URLUtilities.swift @@ -1,4 +1,12 @@ // +// 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 +// // URLUtilities.swift // // diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index b719ce6acc..2dee61ab10 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -1,4 +1,12 @@ // +// 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 // // diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 71964e78cb..6970104e91 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -1,4 +1,12 @@ // +// 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 +// // ManageSubscriptionsViewModel.swift // // @@ -26,7 +34,7 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published var refundRequestStatus: String? @Published - var configuration: CustomerCenterData? + var configuration: CustomerCenterConfigData? @Published var showRestoreAlert: Bool = false @Published var state: State { @@ -51,12 +59,12 @@ class ManageSubscriptionsViewModel: ObservableObject { state = .notLoaded } - init(configuration: CustomerCenterData) { + init(configuration: CustomerCenterConfigData) { state = .notLoaded self.configuration = configuration } - init(configuration: CustomerCenterData, subscriptionInformation: SubscriptionInformation) { + init(configuration: CustomerCenterConfigData, subscriptionInformation: SubscriptionInformation) { self.configuration = configuration self.subscriptionInformation = subscriptionInformation state = .success @@ -90,7 +98,7 @@ class ManageSubscriptionsViewModel: ObservableObject { } } - func handleAction(for path: CustomerCenterData.HelpPath) { + func handleAction(for path: CustomerCenterConfigData.HelpPath) { switch path.type { case .missingPurchase: self.showRestoreAlert = true diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index e016c0319b..6895625ef6 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -1,4 +1,12 @@ // +// 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 +// // CustomerCenterView.swift // // diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index ff55ca9344..baa36e3246 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -1,4 +1,12 @@ // +// 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 +// // ManageSubscriptionsView.swift // // @@ -161,8 +169,8 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { let viewModel = ManageSubscriptionsViewModel( - configuration: CustomerCenterTestData.customerCenterData, - subscriptionInformation: CustomerCenterTestData.subscriptionInformation) + configuration: CustomerCenterConfigTestData.customerCenterData, + subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformation) ManageSubscriptionsView(viewModel: viewModel) } diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index d22558b539..67474e2a92 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -1,4 +1,12 @@ // +// 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 +// // NoSubscriptionsView.swift // // diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index b60408b953..52aee10a63 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -1,4 +1,12 @@ // +// 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 +// // RestorePurchasesAlert.swift // // diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index b6e0dbf70f..2e38d34f5e 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -1,4 +1,12 @@ // +// 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 +// // WrongPlatformView.swift // // From fc6942af02c6c5d2aae9b735c4fd39fddba1229c Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 15:50:48 +0200 Subject: [PATCH 26/50] remove promotionals --- .../Data/CustomerCenterConfigData.swift | 14 ------------- .../Data/CustomerCenterConfigTestData.swift | 21 ++++--------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift index 106deed8e4..7fc9728d92 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift @@ -59,19 +59,6 @@ struct CustomerCenterConfigData { } - struct PromotionalOffer { - - let iosOfferId: String - let eligibility: Eligibility - - } - - struct Eligibility { - - let firstSeen: String - - } - struct FeedbackSurvey { let title: LocalizedString @@ -83,7 +70,6 @@ struct CustomerCenterConfigData { let id: String let title: LocalizedString - let promotionalOffer: PromotionalOffer? } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index f88cd73eda..1d34ec25b9 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -32,10 +32,7 @@ enum CustomerCenterConfigTestData { id: "nwodkdnfaoeb", title: .init(en_US: "Request a refund"), type: .refundRequest, - detail: .promotionalOffer(.init( - iosOfferId: "rc-refund-offer", - eligibility: .init(firstSeen: "> 30") - )) + detail: nil ), .init( id: "nfoaiodifj9", @@ -52,24 +49,14 @@ enum CustomerCenterConfigTestData { options: [ .init( id: "iewrthals", - title: CustomerCenterConfigData.LocalizedString(en_US: "Too expensive"), - promotionalOffer: CustomerCenterConfigData.PromotionalOffer( - iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterConfigData.Eligibility(firstSeen: "> 14") - ) - ), + title: CustomerCenterConfigData.LocalizedString(en_US: "Too expensive") ), .init( id: "qklpadsfj", - title: CustomerCenterConfigData.LocalizedString(en_US: "Don't use the app"), - promotionalOffer: CustomerCenterConfigData.PromotionalOffer( - iosOfferId: "rc-cancel-offer", - eligibility: CustomerCenterConfigData.Eligibility(firstSeen: "> 7") - ) + title: CustomerCenterConfigData.LocalizedString(en_US: "Don't use the app") ), .init( id: "jargnapocps", - title: CustomerCenterConfigData.LocalizedString(en_US: "Bought by mistake"), - promotionalOffer: nil + title: CustomerCenterConfigData.LocalizedString(en_US: "Bought by mistake") ) ] )) From 77be5bfb49d273f8222b548f0fadce475d0b3525 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 15:53:11 +0200 Subject: [PATCH 27/50] improve loadHasSubscriptions logic --- .../ViewModels/CustomerCenterViewModel.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 2dee61ab10..4826609f10 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -64,15 +64,18 @@ class CustomerCenterViewModel: ObservableObject { func loadHasSubscriptions() async { do { + // swiftlint:disable:next todo + // TODO: support non-consumables let customerInfo = try await Purchases.shared.customerInfo() self.hasSubscriptions = customerInfo.activeSubscriptions.count > 0 - guard let firstActiveEntitlementStore = customerInfo.entitlements.active.first?.value.store else { - self.subscriptionsAreFromApple = false - return - } - self.subscriptionsAreFromApple = - firstActiveEntitlementStore == .appStore || firstActiveEntitlementStore == .macAppStore + self.subscriptionsAreFromApple = customerInfo.entitlements.active.first(where: { + $0.value.store == .appStore || $0.value.store == .macAppStore + }).map { entitlement in + customerInfo.activeSubscriptions.contains(entitlement.value.productIdentifier) + } ?? false + + self.state = .success } catch { self.state = .error(error) } From bae682f89761bc6f573c340803a81f1dee1e402c Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 16:20:10 +0200 Subject: [PATCH 28/50] cleanup SubscriptionInformation --- .../Data/CustomerCenterConfigTestData.swift | 4 +-- .../Data/SubscriptionInformation.swift | 26 ++++++++++++++++--- .../ManageSubscriptionsViewModel.swift | 4 +-- .../Views/ManageSubscriptionsView.swift | 4 +-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 1d34ec25b9..d007a1eed6 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -73,9 +73,9 @@ enum CustomerCenterConfigTestData { static let subscriptionInformation: SubscriptionInformation = .init( title: "Basic", - duration: "Monthly", + durationTitle: "Monthly", price: "$4.99 / month", - nextRenewal: Date(), + nextRenewalString: "June 1st, 2024", willRenew: true, productIdentifier: "product_id", active: true diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index d04ae73ed3..5fd7b2815b 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -18,15 +18,33 @@ import Foundation struct SubscriptionInformation { let title: String - let duration: String + let durationTitle: String let price: String - let nextRenewal: Date? - let willRenew: Bool + let nextRenewalString: String? let productIdentifier: String - let active: Bool var renewalString: String { return active ? (willRenew ? "Renews" : "Expires") : "Expired" } + private let willRenew: Bool + private let active: Bool + + init(title: String, + durationTitle: String, + price: String, + nextRenewalString: String?, + willRenew: Bool, + productIdentifier: String, + active: Bool + ) { + self.title = title + self.durationTitle = durationTitle + self.price = price + self.nextRenewalString = nextRenewalString + self.productIdentifier = productIdentifier + self.willRenew = willRenew + self.active = active + } + } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 6970104e91..5031249590 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -86,9 +86,9 @@ class ManageSubscriptionsViewModel: ObservableObject { // TODO: support non-consumables self.subscriptionInformation = SubscriptionInformation( title: subscribedProduct.localizedTitle, - duration: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", + durationTitle: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", price: subscribedProduct.localizedPriceString, - nextRenewal: currentEntitlement.expirationDate!, + nextRenewalString: currentEntitlement.expirationDate.map { "\($0)" } ?? nil, willRenew: currentEntitlement.willRenew, productIdentifier: subscribedProductID, active: currentEntitlement.isActive diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index baa36e3246..6c210d5675 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -91,7 +91,7 @@ struct SubscriptionDetailsView: View { var body: some View { VStack(alignment: .leading, spacing: 8) { - Text("\(subscriptionInformation.title) - \(subscriptionInformation.duration)") + Text("\(subscriptionInformation.title) - \(subscriptionInformation.durationTitle)") .font(.subheadline) .padding([.horizontal, .top]) @@ -100,7 +100,7 @@ struct SubscriptionDetailsView: View { .foregroundColor(Color.gray) .padding(.horizontal) - if let nextRenewal = subscriptionInformation.nextRenewal { + if let nextRenewal = subscriptionInformation.nextRenewalString { Text("\(subscriptionInformation.renewalString): \(String(describing: nextRenewal))") .font(.caption) .foregroundColor(Color.gray) From 0af7fe2044c33fb775dfc9258d256861c646085d Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 16:27:38 +0200 Subject: [PATCH 29/50] fix linter --- .../CustomerCenter/Data/CustomerCenterConfigTestData.swift | 5 ++++- .../CustomerCenter/Data/SubscriptionInformation.swift | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index d007a1eed6..f25773416b 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -49,7 +49,8 @@ enum CustomerCenterConfigTestData { options: [ .init( id: "iewrthals", - title: CustomerCenterConfigData.LocalizedString(en_US: "Too expensive") ), + title: CustomerCenterConfigData.LocalizedString(en_US: "Too expensive") + ), .init( id: "qklpadsfj", title: CustomerCenterConfigData.LocalizedString(en_US: "Don't use the app") @@ -66,7 +67,9 @@ enum CustomerCenterConfigTestData { supportEmail: "support@revenuecat.com", appearance: .init( mode: .system, + // swiftlint:disable:next force_try light: try! .init(stringRepresentation: "#000000"), + // swiftlint:disable:next force_try dark: try! .init(stringRepresentation: "#ffffff") ) ) diff --git a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift index 5fd7b2815b..277120b163 100644 --- a/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift +++ b/RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift @@ -30,7 +30,7 @@ struct SubscriptionInformation { private let willRenew: Bool private let active: Bool - init(title: String, + init(title: String, durationTitle: String, price: String, nextRenewalString: String?, From cf5386f633e2e7da1f396661d53ebb9fd86006d8 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 16:35:20 +0200 Subject: [PATCH 30/50] add return --- .../Views/RestorePurchasesAlert.swift | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 52aee10a63..0b9d289743 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -43,7 +43,7 @@ struct RestorePurchasesAlert: ViewModifier { .alert(isPresented: $isPresented) { switch self.alertType { case .restorePurchases: - Alert( + return Alert( title: Text("Restore purchases"), message: Text( """ @@ -68,23 +68,18 @@ struct RestorePurchasesAlert: ViewModifier { ) case .purchasesRecovered: - Alert(title: Text("Purchases recovered!"), - message: Text("We applied the previously purchased items to your account. " + - "Sorry for the inconvenience."), - dismissButton: .default(Text("Dismiss")) { + return Alert(title: Text("Purchases recovered!"), + message: Text("We applied the previously purchased items to your account. " + + "Sorry for the inconvenience."), + dismissButton: .default(Text("Dismiss")) { dismiss() }) case .purchasesNotFound: - - Alert(title: Text(""), - message: Text("We couldn’t find any additional purchases under this account. \n\n" + - "Contact support for assistance if you think this is an error."), - primaryButton: .default(Text("Contact Support"), action: { - // todo: make configurable - openURL(URLUtilities.createMailURL()!) - }), - secondaryButton: .cancel(Text("Cancel")) { + return Alert(title: Text(""), + message: Text("We couldn’t find any additional purchases under this account. \n\n" + + "Contact support for assistance if you think this is an error."), + dismissButton: .default(Text("Dismiss")) { dismiss() }) } From e1466dba0db9bb2afc24a9da91095b577198d9c5 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 20:35:17 +0200 Subject: [PATCH 31/50] fix availability --- .../ViewModels/ManageSubscriptionsViewModel.swift | 4 ++-- .../CustomerCenter/Views/CustomerCenterView.swift | 15 ++++++++++----- .../Views/ManageSubscriptionsView.swift | 6 +++++- .../Views/NoSubscriptionsView.swift | 8 ++++++++ .../Views/RestorePurchasesAlert.swift | 4 ++++ .../CustomerCenter/Views/WrongPlatformView.swift | 8 ++++++++ 6 files changed, 37 insertions(+), 8 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 5031249590..19f490d5bd 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -98,12 +98,12 @@ class ManageSubscriptionsViewModel: ObservableObject { } } + #if os(iOS) || targetEnvironment(macCatalyst) func handleAction(for path: CustomerCenterConfigData.HelpPath) { switch path.type { case .missingPurchase: self.showRestoreAlert = true case .refundRequest: - #if os(iOS) || targetEnvironment(macCatalyst) Task { guard let subscriptionInformation = self.subscriptionInformation else { return } let status = try await Purchases.shared.beginRefundRequest( @@ -118,7 +118,6 @@ class ManageSubscriptionsViewModel: ObservableObject { self.refundRequestStatus = "Refund canceled" } } - #endif case .changePlans: Task { try await Purchases.shared.showManageSubscriptions() @@ -131,6 +130,7 @@ class ManageSubscriptionsViewModel: ObservableObject { break } } + #endif } diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 6895625ef6..498a373202 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -16,14 +16,20 @@ import RevenueCat import SwiftUI +#if !os(macOS) && !os(tvOS) && !os(watchOS) + +/// A SwiftUI view for displaying a customer support common tasks @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -struct CustomerCenterView: View { +public struct CustomerCenterView: View { @StateObject private var viewModel = CustomerCenterViewModel() + /// Create a view to handle common customer support tasks + public init() {} + fileprivate init(viewModel: CustomerCenterViewModel) { self._viewModel = .init(wrappedValue: viewModel) } @@ -32,10 +38,7 @@ struct CustomerCenterView: View { NavigationView { NavigationLink(destination: destinationView()) { Text("Billing and subscription help") - .padding() - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(10) + .buttonStyle(ManageSubscriptionsButtonStyle()) } } .task { @@ -88,3 +91,5 @@ struct CustomerCenterView_Previews: PreviewProvider { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 6c210d5675..244bfee867 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -16,6 +16,8 @@ import RevenueCat import SwiftUI +#if !os(macOS) && !os(tvOS) && !os(watchOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -134,7 +136,7 @@ struct ManageSubscriptionsButtonsView: View { if let configuration = viewModel.configuration { let filteredPaths = configuration.paths.filter { path in #if targetEnvironment(macCatalyst) - return path.type != .refundRequest + return path.type == .refundRequest #else return true #endif @@ -177,3 +179,5 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { } #endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 67474e2a92..1c846da516 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -16,6 +16,8 @@ import RevenueCat import SwiftUI +#if !os(macOS) && !os(tvOS) && !os(watchOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -52,6 +54,8 @@ struct NoSubscriptionsView: View { } +#if DEBUG + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -63,3 +67,7 @@ struct NoSubscriptionsView_Previews: PreviewProvider { } } + +#endif + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 0b9d289743..7fed0dc4d1 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -17,6 +17,8 @@ import Foundation import RevenueCat import SwiftUI +#if !os(macOS) && !os(tvOS) && !os(watchOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -112,3 +114,5 @@ extension View { self.modifier(RestorePurchasesAlert(isPresented: isPresented)) } } + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 2e38d34f5e..bb159e42b3 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -17,6 +17,8 @@ import Foundation import RevenueCat import SwiftUI +#if !os(macOS) && !os(tvOS) && !os(watchOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -95,6 +97,8 @@ struct WrongPlatformView: View { } +#if DEBUG + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -113,3 +117,7 @@ struct WrongPlatformView_Previews: PreviewProvider { } } + +#endif + +#endif From 86dd13d34978bfc6e88e265b2f3ffaf3ad9278ff Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 6 Jun 2024 20:39:55 +0200 Subject: [PATCH 32/50] fix body public --- RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 498a373202..d4ab934d50 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -34,7 +34,8 @@ public struct CustomerCenterView: View { self._viewModel = .init(wrappedValue: viewModel) } - var body: some View { + // swiftlint:disable:next missing_docs + public var body: some View { NavigationView { NavigationLink(destination: destinationView()) { Text("Billing and subscription help") From 9bed3e4fbac2f597307d542ce6a9980cb972c281 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 7 Jun 2024 13:22:27 +0200 Subject: [PATCH 33/50] fix colors --- .../CustomerCenter/ManageSubscriptionsButtonStyle.swift | 2 +- .../ViewModels/ManageSubscriptionsViewModel.swift | 8 ++++++++ .../CustomerCenter/Views/CustomerCenterView.swift | 5 ++++- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 5 +++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index e29701e198..779548425d 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -26,7 +26,7 @@ struct ManageSubscriptionsButtonStyle: ButtonStyle { configuration.label .padding() .frame(width: 300) - .background(Color.blue) + .background(Color.accentColor) .foregroundColor(.white) .cornerRadius(10) } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 19f490d5bd..0455af5e20 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -98,6 +98,14 @@ class ManageSubscriptionsViewModel: ObservableObject { } } + func loadCustomerCenterConfig() async { + do { + self.configuration = CustomerCenterConfigTestData.customerCenterData + } catch { + self.state = .error(error) + } + } + #if os(iOS) || targetEnvironment(macCatalyst) func handleAction(for path: CustomerCenterConfigData.HelpPath) { switch path.type { diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index d4ab934d50..8cab72b242 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -39,7 +39,10 @@ public struct CustomerCenterView: View { NavigationView { NavigationLink(destination: destinationView()) { Text("Billing and subscription help") - .buttonStyle(ManageSubscriptionsButtonStyle()) + .padding() + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(10) } } .task { diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 244bfee867..7ccad192da 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -51,7 +51,7 @@ struct ManageSubscriptionsView: View { openURL: openURL) } .task { - await checkAndLoadSubscriptionInformation() + await checkAndLoadInformation() } } @@ -63,9 +63,10 @@ struct ManageSubscriptionsView: View { @available(watchOS, unavailable) private extension ManageSubscriptionsView { - func checkAndLoadSubscriptionInformation() async { + func checkAndLoadInformation() async { if !viewModel.isLoaded { await viewModel.loadSubscriptionInformation() + await viewModel.loadCustomerCenterConfig() } } From 1fc6419f501b0933b2e8c12d15cfdacafc27ec27 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 7 Jun 2024 15:13:46 +0200 Subject: [PATCH 34/50] clean up test config data --- .../Data/CustomerCenterConfigTestData.swift | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index f25773416b..90ec7fd514 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -20,47 +20,47 @@ enum CustomerCenterConfigTestData { @available(iOS 14.0, *) static let customerCenterData = CustomerCenterConfigData( - id: "ccenter_lasdlfalaowpwp", + id: "customer_center_id", paths: [ .init( - id: "ownmsldfow", + id: "1", title: .init(en_US: "Didn't receive purchase"), type: .missingPurchase, detail: nil ), .init( - id: "nwodkdnfaoeb", + id: "2", title: .init(en_US: "Request a refund"), type: .refundRequest, detail: nil ), .init( - id: "nfoaiodifj9", + id: "3", title: .init(en_US: "Change plans"), type: .changePlans, detail: nil ), .init( - id: "jnkasldfhas", + id: "4", title: .init(en_US: "Cancel subscription"), type: .cancel, detail: .feedbackSurvey(.init( - title: .init(en_US: "Why are you cancelling?"), - options: [ - .init( - id: "iewrthals", - title: CustomerCenterConfigData.LocalizedString(en_US: "Too expensive") - ), - .init( - id: "qklpadsfj", - title: CustomerCenterConfigData.LocalizedString(en_US: "Don't use the app") - ), - .init( - id: "jargnapocps", - title: CustomerCenterConfigData.LocalizedString(en_US: "Bought by mistake") - ) - ] - )) + title: .init(en_US: "Why are you cancelling?"), + options: [ + .init( + id: "1", + title: CustomerCenterConfigData.LocalizedString(en_US: "Too expensive") + ), + .init( + id: "2", + title: CustomerCenterConfigData.LocalizedString(en_US: "Don't use the app") + ), + .init( + id: "3", + title: CustomerCenterConfigData.LocalizedString(en_US: "Bought by mistake") + ) + ] + )) ) ], title: .init(en_US: "How can we help?"), From db4a417bfd65a7529d60bed383612a6da9ce83f4 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 7 Jun 2024 15:37:31 +0200 Subject: [PATCH 35/50] publish on main thread --- .../ViewModels/CustomerCenterViewModel.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 4826609f10..9388e0940a 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -67,17 +67,23 @@ class CustomerCenterViewModel: ObservableObject { // swiftlint:disable:next todo // TODO: support non-consumables let customerInfo = try await Purchases.shared.customerInfo() - self.hasSubscriptions = customerInfo.activeSubscriptions.count > 0 + let hasSubscriptions = customerInfo.activeSubscriptions.count > 0 - self.subscriptionsAreFromApple = customerInfo.entitlements.active.first(where: { + let subscriptionsAreFromApple = customerInfo.entitlements.active.first(where: { $0.value.store == .appStore || $0.value.store == .macAppStore }).map { entitlement in customerInfo.activeSubscriptions.contains(entitlement.value.productIdentifier) } ?? false - self.state = .success + DispatchQueue.main.async { + self.hasSubscriptions = hasSubscriptions + self.subscriptionsAreFromApple = subscriptionsAreFromApple + self.state = .success + } } catch { - self.state = .error(error) + DispatchQueue.main.async { + self.state = .error(error) + } } } From 2335cdeed3f72f190ce6b723325ffcc2657a3f66 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 7 Jun 2024 15:37:40 +0200 Subject: [PATCH 36/50] remove button from view --- .../CustomerCenter/Views/CustomerCenterView.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 8cab72b242..eb36232955 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -36,13 +36,11 @@ public struct CustomerCenterView: View { // swiftlint:disable:next missing_docs public var body: some View { - NavigationView { - NavigationLink(destination: destinationView()) { - Text("Billing and subscription help") - .padding() - .background(Color.accentColor) - .foregroundColor(.white) - .cornerRadius(10) + Group { + if !viewModel.isLoaded { + ProgressView() + } else { + destinationView() } } .task { From a2b1f5916bc60c89cc8dbd9f6d895284870051ca Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 7 Jun 2024 16:06:13 +0200 Subject: [PATCH 37/50] fix date --- .../ViewModels/ManageSubscriptionsViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 0455af5e20..fef5b5a19b 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -84,11 +84,14 @@ class ManageSubscriptionsViewModel: ObservableObject { // swiftlint:disable:next todo // TODO: support non-consumables + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + self.subscriptionInformation = SubscriptionInformation( title: subscribedProduct.localizedTitle, durationTitle: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", price: subscribedProduct.localizedPriceString, - nextRenewalString: currentEntitlement.expirationDate.map { "\($0)" } ?? nil, + nextRenewalString: currentEntitlement.expirationDate.map { dateFormatter.string(from: $0) } ?? nil, willRenew: currentEntitlement.willRenew, productIdentifier: subscribedProductID, active: currentEntitlement.isActive From ace65164ce6d5284a6d8a5ef505cc408e49b0f23 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 7 Jun 2024 16:07:06 +0200 Subject: [PATCH 38/50] remove contact support buttons --- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 7 ------- .../CustomerCenter/Views/WrongPlatformView.swift | 9 --------- 2 files changed, 16 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 7ccad192da..b4d19738d3 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -150,13 +150,6 @@ struct ManageSubscriptionsButtonsView: View { .buttonStyle(ManageSubscriptionsButtonStyle()) } } - - Button("Contact support") { - Task { - openURL(URLUtilities.createMailURL()!) - } - } - .padding() } } diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index bb159e42b3..198fdb7de0 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -56,15 +56,6 @@ struct WrongPlatformView: View { .padding() } - Spacer() - - Button("Contact support") { - Task { - openURL(URLUtilities.createMailURL()!) - } - } - .padding() - } .task { if store == nil { From 1555baef9e8b2a92a0559d29043705ed6b603143 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 7 Jun 2024 18:08:28 +0200 Subject: [PATCH 39/50] remove unnecessary do catch --- .../ViewModels/ManageSubscriptionsViewModel.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index fef5b5a19b..168007ec4c 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -102,11 +102,7 @@ class ManageSubscriptionsViewModel: ObservableObject { } func loadCustomerCenterConfig() async { - do { - self.configuration = CustomerCenterConfigTestData.customerCenterData - } catch { - self.state = .error(error) - } + self.configuration = CustomerCenterConfigTestData.customerCenterData } #if os(iOS) || targetEnvironment(macCatalyst) From 0646d7805b0c35174a6997901eef4e17de1e1d34 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 10 Jun 2024 19:33:16 +0200 Subject: [PATCH 40/50] cleanup --- .../Data/CustomerCenterConfigData.swift | 18 ------- .../Data/CustomerCenterConfigTestData.swift | 10 +--- .../ManageSubscriptionsButtonStyle.swift | 3 ++ .../ManageSubscriptionsViewModel.swift | 47 ++++++++++++------- .../Views/ManageSubscriptionsView.swift | 40 ++++++++++------ 5 files changed, 60 insertions(+), 58 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift index 7fc9728d92..8390ff4384 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift @@ -16,17 +16,11 @@ import Foundation import RevenueCat -/// Represents a color to be used by `RevenueCatUI` -public typealias RCColor = PaywallColor - -// swiftlint:disable nesting struct CustomerCenterConfigData { let id: String let paths: [HelpPath] let title: LocalizedString - let supportEmail: String - let appearance: Appearance enum HelpPathType: String { case missingPurchase = "MISSING_PURCHASE" @@ -73,16 +67,4 @@ struct CustomerCenterConfigData { } - struct Appearance { - - let mode: Mode - let light: RCColor - let dark: RCColor - - enum Mode: String { - case system = "SYSTEM" - } - - } - } diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 90ec7fd514..04242ed964 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -63,15 +63,7 @@ enum CustomerCenterConfigTestData { )) ) ], - title: .init(en_US: "How can we help?"), - supportEmail: "support@revenuecat.com", - appearance: .init( - mode: .system, - // swiftlint:disable:next force_try - light: try! .init(stringRepresentation: "#000000"), - // swiftlint:disable:next force_try - dark: try! .init(stringRepresentation: "#ffffff") - ) + title: .init(en_US: "How can we help?") ) static let subscriptionInformation: SubscriptionInformation = .init( diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index 779548425d..c4970d4ff7 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -29,6 +29,9 @@ struct ManageSubscriptionsButtonStyle: ButtonStyle { .background(Color.accentColor) .foregroundColor(.white) .cornerRadius(10) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + .opacity(configuration.isPressed ? 0.8 : 1.0) + .animation(.easeInOut(duration: 0.2), value: configuration.isPressed) } } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 168007ec4c..a5974bb48e 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -70,23 +70,38 @@ class ManageSubscriptionsViewModel: ObservableObject { state = .success } - func loadSubscriptionInformation() async { + func loadScreen() async { do { - let customerInfo = try await Purchases.shared.customerInfo() - guard let currentEntitlementDict = customerInfo.entitlements.active.first, - let subscribedProductID = customerInfo.activeSubscriptions.first, - let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { - Logger.warning(Strings.could_not_find_subscription_information) - self.state = .error(CustomerCenterError.couldNotFindSubscriptionInformation) - return + try await loadSubscriptionInformation() + await loadCustomerCenterConfig() + DispatchQueue.main.async { + self.state = .success + } + } catch { + DispatchQueue.main.async { + self.state = .error(error) } - let currentEntitlement = currentEntitlementDict.value + } + } - // swiftlint:disable:next todo - // TODO: support non-consumables - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium + func loadSubscriptionInformation() async throws { + let customerInfo = try await Purchases.shared.customerInfo() + guard let currentEntitlementDict = customerInfo.entitlements.active.first, + let subscribedProductID = customerInfo.activeSubscriptions.first, + let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { + Logger.warning(Strings.could_not_find_subscription_information) + DispatchQueue.main.async { + self.state = .error(CustomerCenterError.couldNotFindSubscriptionInformation) + } + return + } + let currentEntitlement = currentEntitlementDict.value + // swiftlint:disable:next todo + // TODO: support non-consumables + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + DispatchQueue.main.async { self.subscriptionInformation = SubscriptionInformation( title: subscribedProduct.localizedTitle, durationTitle: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", @@ -96,13 +111,13 @@ class ManageSubscriptionsViewModel: ObservableObject { productIdentifier: subscribedProductID, active: currentEntitlement.isActive ) - } catch { - self.state = .error(error) } } func loadCustomerCenterConfig() async { - self.configuration = CustomerCenterConfigTestData.customerCenterData + DispatchQueue.main.async { + self.configuration = CustomerCenterConfigTestData.customerCenterData + } } #if os(iOS) || targetEnvironment(macCatalyst) diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index b4d19738d3..f78977eab5 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -38,17 +38,21 @@ struct ManageSubscriptionsView: View { var body: some View { VStack { - HeaderView() + if viewModel.isLoaded { + HeaderView(viewModel: viewModel) - if let subscriptionInformation = self.viewModel.subscriptionInformation { - SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, - refundRequestStatus: viewModel.refundRequestStatus) - } + if let subscriptionInformation = self.viewModel.subscriptionInformation { + SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, + refundRequestStatus: viewModel.refundRequestStatus) + } - Spacer() + Spacer() - ManageSubscriptionsButtonsView(viewModel: viewModel, - openURL: openURL) + ManageSubscriptionsButtonsView(viewModel: viewModel) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } } .task { await checkAndLoadInformation() @@ -65,8 +69,7 @@ private extension ManageSubscriptionsView { func checkAndLoadInformation() async { if !viewModel.isLoaded { - await viewModel.loadSubscriptionInformation() - await viewModel.loadCustomerCenterConfig() + await viewModel.loadScreen() } } @@ -77,11 +80,18 @@ private extension ManageSubscriptionsView { @available(tvOS, unavailable) @available(watchOS, unavailable) struct HeaderView: View { + + @ObservedObject + private(set) var viewModel: ManageSubscriptionsViewModel + var body: some View { - Text("How can we help?") - .font(.title) - .padding() + if let configuration = viewModel.configuration { + Text(configuration.title.en_US) + .font(.title) + .padding() + } } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -89,6 +99,7 @@ struct HeaderView: View { @available(tvOS, unavailable) @available(watchOS, unavailable) struct SubscriptionDetailsView: View { + let subscriptionInformation: SubscriptionInformation let refundRequestStatus: String? @@ -119,6 +130,7 @@ struct SubscriptionDetailsView: View { } } } + } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -130,8 +142,6 @@ struct ManageSubscriptionsButtonsView: View { @ObservedObject private(set) var viewModel: ManageSubscriptionsViewModel - let openURL: OpenURLAction - var body: some View { VStack(spacing: 16) { if let configuration = viewModel.configuration { From 33a9497a2b4febce71f118caaf13e1acb8485c1b Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Mon, 10 Jun 2024 19:51:01 +0200 Subject: [PATCH 41/50] fix visionOS --- .../CustomerCenter/Views/CustomerCenterView.swift | 5 ++++- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 9 +++++++-- .../CustomerCenter/Views/NoSubscriptionsView.swift | 4 +++- .../CustomerCenter/Views/RestorePurchasesAlert.swift | 5 ++++- .../CustomerCenter/Views/WrongPlatformView.swift | 4 +++- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index eb36232955..acd1d10986 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -16,13 +16,14 @@ import RevenueCat import SwiftUI -#if !os(macOS) && !os(tvOS) && !os(watchOS) +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) /// A SwiftUI view for displaying a customer support common tasks @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) public struct CustomerCenterView: View { @StateObject private var viewModel = CustomerCenterViewModel() @@ -54,6 +55,7 @@ public struct CustomerCenterView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) private extension CustomerCenterView { func checkAndLoadSubscriptions() async { @@ -83,6 +85,7 @@ private extension CustomerCenterView { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct CustomerCenterView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index f78977eab5..798446db8f 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -16,12 +16,13 @@ import RevenueCat import SwiftUI -#if !os(macOS) && !os(tvOS) && !os(watchOS) +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct ManageSubscriptionsView: View { @Environment(\.openURL) @@ -65,6 +66,7 @@ struct ManageSubscriptionsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) private extension ManageSubscriptionsView { func checkAndLoadInformation() async { @@ -79,6 +81,7 @@ private extension ManageSubscriptionsView { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct HeaderView: View { @ObservedObject @@ -137,6 +140,7 @@ struct SubscriptionDetailsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct ManageSubscriptionsButtonsView: View { @ObservedObject @@ -154,7 +158,7 @@ struct ManageSubscriptionsButtonsView: View { } ForEach(filteredPaths, id: \.id) { path in Button(path.title.en_US) { - viewModel.handleAction(for: path) + self.viewModel.handleAction(for: path) } .restorePurchasesAlert(isPresented: $viewModel.showRestoreAlert) .buttonStyle(ManageSubscriptionsButtonStyle()) @@ -171,6 +175,7 @@ struct ManageSubscriptionsButtonsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 1c846da516..81350fff36 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -16,12 +16,13 @@ import RevenueCat import SwiftUI -#if !os(macOS) && !os(tvOS) && !os(watchOS) +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct NoSubscriptionsView: View { @Environment(\.dismiss) var dismiss @@ -60,6 +61,7 @@ struct NoSubscriptionsView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct NoSubscriptionsView_Previews: PreviewProvider { static var previews: some View { diff --git a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift index 7fed0dc4d1..8ec770ff22 100644 --- a/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift +++ b/RevenueCatUI/CustomerCenter/Views/RestorePurchasesAlert.swift @@ -17,12 +17,13 @@ import Foundation import RevenueCat import SwiftUI -#if !os(macOS) && !os(tvOS) && !os(watchOS) +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct RestorePurchasesAlert: ViewModifier { @Binding @@ -94,6 +95,7 @@ struct RestorePurchasesAlert: ViewModifier { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) private extension RestorePurchasesAlert { func setAlertType(_ newType: AlertType) { @@ -109,6 +111,7 @@ private extension RestorePurchasesAlert { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) extension View { func restorePurchasesAlert(isPresented: Binding) -> some View { self.modifier(RestorePurchasesAlert(isPresented: isPresented)) diff --git a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift index 198fdb7de0..ea1b000c0a 100644 --- a/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift +++ b/RevenueCatUI/CustomerCenter/Views/WrongPlatformView.swift @@ -17,12 +17,13 @@ import Foundation import RevenueCat import SwiftUI -#if !os(macOS) && !os(tvOS) && !os(watchOS) +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct WrongPlatformView: View { @State @@ -94,6 +95,7 @@ struct WrongPlatformView: View { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@available(visionOS, unavailable) struct WrongPlatformView_Previews: PreviewProvider { static var previews: some View { From 973d7ec3fc890af18276b0153d913b202d3eb371 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 11 Jun 2024 12:26:46 +0200 Subject: [PATCH 42/50] make viewmodels MainActor --- .../ViewModels/CustomerCenterViewModel.swift | 14 ++---- .../ManageSubscriptionsViewModel.swift | 48 +++++++------------ 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 9388e0940a..0d2d240f4d 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -20,7 +20,7 @@ import RevenueCat @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -class CustomerCenterViewModel: ObservableObject { +@MainActor class CustomerCenterViewModel: ObservableObject { @Published var hasSubscriptions: Bool = false @@ -75,15 +75,11 @@ class CustomerCenterViewModel: ObservableObject { customerInfo.activeSubscriptions.contains(entitlement.value.productIdentifier) } ?? false - DispatchQueue.main.async { - self.hasSubscriptions = hasSubscriptions - self.subscriptionsAreFromApple = subscriptionsAreFromApple - self.state = .success - } + self.hasSubscriptions = hasSubscriptions + self.subscriptionsAreFromApple = subscriptionsAreFromApple + self.state = .success } catch { - DispatchQueue.main.async { - self.state = .error(error) - } + self.state = .error(error) } } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index a5974bb48e..0348ca77ef 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -20,6 +20,7 @@ import RevenueCat @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) +@MainActor class ManageSubscriptionsViewModel: ObservableObject { var isLoaded: Bool { @@ -74,13 +75,9 @@ class ManageSubscriptionsViewModel: ObservableObject { do { try await loadSubscriptionInformation() await loadCustomerCenterConfig() - DispatchQueue.main.async { - self.state = .success - } + self.state = .success } catch { - DispatchQueue.main.async { - self.state = .error(error) - } + self.state = .error(error) } } @@ -90,9 +87,7 @@ class ManageSubscriptionsViewModel: ObservableObject { let subscribedProductID = customerInfo.activeSubscriptions.first, let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { Logger.warning(Strings.could_not_find_subscription_information) - DispatchQueue.main.async { - self.state = .error(CustomerCenterError.couldNotFindSubscriptionInformation) - } + self.state = .error(CustomerCenterError.couldNotFindSubscriptionInformation) return } let currentEntitlement = currentEntitlementDict.value @@ -101,23 +96,19 @@ class ManageSubscriptionsViewModel: ObservableObject { // TODO: support non-consumables let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium - DispatchQueue.main.async { - self.subscriptionInformation = SubscriptionInformation( - title: subscribedProduct.localizedTitle, - durationTitle: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", - price: subscribedProduct.localizedPriceString, - nextRenewalString: currentEntitlement.expirationDate.map { dateFormatter.string(from: $0) } ?? nil, - willRenew: currentEntitlement.willRenew, - productIdentifier: subscribedProductID, - active: currentEntitlement.isActive - ) - } + self.subscriptionInformation = SubscriptionInformation( + title: subscribedProduct.localizedTitle, + durationTitle: subscribedProduct.subscriptionPeriod?.durationTitle ?? "", + price: subscribedProduct.localizedPriceString, + nextRenewalString: currentEntitlement.expirationDate.map { dateFormatter.string(from: $0) } ?? nil, + willRenew: currentEntitlement.willRenew, + productIdentifier: subscribedProductID, + active: currentEntitlement.isActive + ) } func loadCustomerCenterConfig() async { - DispatchQueue.main.async { - self.configuration = CustomerCenterConfigTestData.customerCenterData - } + self.configuration = CustomerCenterConfigTestData.customerCenterData } #if os(iOS) || targetEnvironment(macCatalyst) @@ -128,9 +119,8 @@ class ManageSubscriptionsViewModel: ObservableObject { case .refundRequest: Task { guard let subscriptionInformation = self.subscriptionInformation else { return } - let status = try await Purchases.shared.beginRefundRequest( - forProduct: subscriptionInformation.productIdentifier - ) + let productId = subscriptionInformation.productIdentifier + let status = try await Purchases.shared.beginRefundRequest(forProduct: productId) switch status { case .error: self.refundRequestStatus = "Error when requesting refund, try again" @@ -140,11 +130,7 @@ class ManageSubscriptionsViewModel: ObservableObject { self.refundRequestStatus = "Refund canceled" } } - case .changePlans: - Task { - try await Purchases.shared.showManageSubscriptions() - } - case .cancel: + case .changePlans, .cancel: Task { try await Purchases.shared.showManageSubscriptions() } From e616e44a2252d9949ebf0f36fd0aa2f987a2c323 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 11 Jun 2024 19:43:34 +0200 Subject: [PATCH 43/50] CustomerCenterViewModelTests --- .../ViewModels/CustomerCenterViewModel.swift | 39 ++- .../ViewModels/CustomerCenterViewState.swift | 37 +++ .../ManageSubscriptionsViewModel.swift | 22 +- .../CustomerCenterViewModelTests.swift | 260 ++++++++++++++++++ 4 files changed, 329 insertions(+), 29 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift create mode 100644 Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift index 0d2d240f4d..08c50ef35b 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewModel.swift @@ -22,12 +22,14 @@ import RevenueCat @available(watchOS, unavailable) @MainActor class CustomerCenterViewModel: ObservableObject { + typealias CustomerInfoFetcher = @Sendable () async throws -> CustomerInfo + @Published var hasSubscriptions: Bool = false @Published var subscriptionsAreFromApple: Bool = false @Published - var state: State { + var state: CustomerCenterViewState { didSet { if case let .error(stateError) = state { self.error = stateError @@ -36,29 +38,40 @@ import RevenueCat } var isLoaded: Bool { - if case .notLoaded = state { - return false - } - return true + return state != .notLoaded } - enum State { + private var customerInfoFetcher: CustomerInfoFetcher - case notLoaded - case success - case error(Error) + private var error: Error? - } + convenience init() { + self.init(customerInfoFetcher: { + guard Purchases.isConfigured else { + throw PaywallError.purchasesNotConfigured + } - private var error: Error? + return try await Purchases.shared.customerInfo() + }) + } - init() { + // @PublicForExternalTesting + init(customerInfoFetcher: @escaping CustomerInfoFetcher) { self.state = .notLoaded + self.customerInfoFetcher = customerInfoFetcher } + // @PublicForExternalTesting init(hasSubscriptions: Bool = false, areSubscriptionsFromApple: Bool = false) { self.hasSubscriptions = hasSubscriptions self.subscriptionsAreFromApple = areSubscriptionsFromApple + self.customerInfoFetcher = { + guard Purchases.isConfigured else { + throw PaywallError.purchasesNotConfigured + } + + return try await Purchases.shared.customerInfo() + } self.state = .success } @@ -66,7 +79,7 @@ import RevenueCat do { // swiftlint:disable:next todo // TODO: support non-consumables - let customerInfo = try await Purchases.shared.customerInfo() + let customerInfo = try await self.customerInfoFetcher() let hasSubscriptions = customerInfo.activeSubscriptions.count > 0 let subscriptionsAreFromApple = customerInfo.entitlements.active.first(where: { diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift new file mode 100644 index 0000000000..217de58c3e --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift @@ -0,0 +1,37 @@ +// +// 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 +// +// CustomerCenterViewState.swift +// +// +// Created by Cesar de la Vega on 11/6/24. +// + +import Foundation + +enum CustomerCenterViewState: Equatable { + + case notLoaded + case success + case error(Error) + + static func ==(lhs: CustomerCenterViewState, rhs: CustomerCenterViewState) -> Bool { + switch (lhs, rhs) { + case (.notLoaded, .notLoaded): + return true + case (.success, .success): + return true + case (let .error(lhsError), let .error(rhsError)): + return lhsError.localizedDescription == rhsError.localizedDescription + default: + return false + } + } + +} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 0348ca77ef..864358a5ec 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -23,13 +23,6 @@ import RevenueCat @MainActor class ManageSubscriptionsViewModel: ObservableObject { - var isLoaded: Bool { - if case .notLoaded = state { - return false - } - return true - } - @Published var subscriptionInformation: SubscriptionInformation? @Published @@ -38,7 +31,8 @@ class ManageSubscriptionsViewModel: ObservableObject { var configuration: CustomerCenterConfigData? @Published var showRestoreAlert: Bool = false - @Published var state: State { + @Published + var state: CustomerCenterViewState { didSet { if case let .error(stateError) = state { self.error = stateError @@ -46,16 +40,12 @@ class ManageSubscriptionsViewModel: ObservableObject { } } - private var error: Error? - - enum State { - - case notLoaded - case success - case error(Error) - + var isLoaded: Bool { + return state != .notLoaded } + private var error: Error? + init() { state = .notLoaded } diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift new file mode 100644 index 0000000000..ea2b213439 --- /dev/null +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -0,0 +1,260 @@ +// +// 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 +// +// CustomerCenterViewModelTests.swift +// +// +// Created by Cesar de la Vega on 11/6/24. +// + +import Nimble +import XCTest +@testable import RevenueCatUI +import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@MainActor +class CustomerCenterViewModelTests: TestCase { + + private let error = TestError(message: "An error occurred") + + private struct TestError: Error, Equatable { + let message: String + var localizedDescription: String { + return message + } + } + + func testInitialState() { + let viewModel = CustomerCenterViewModel() + + expect(viewModel.state) == .notLoaded + expect(viewModel.hasSubscriptions) == false + expect(viewModel.subscriptionsAreFromApple) == false + expect(viewModel.isLoaded) == false + } + + func testStateChangeToError() { + let viewModel = CustomerCenterViewModel() + + viewModel.state = .error(error) + + switch viewModel.state { + case .error(let stateError): + expect(stateError as? TestError) == error + default: + fail("Expected state to be .error") + } + } + + func testIsLoaded() { + let viewModel = CustomerCenterViewModel() + + expect(viewModel.isLoaded) == false + + viewModel.state = .success + + expect(viewModel.isLoaded) == true + } + + func testLoadHasSubscriptionsApple() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + return await CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == true + expect(viewModel.subscriptionsAreFromApple) == true + expect(viewModel.state) == .success + } + + func testLoadHasSubscriptionsGoogle() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + return await CustomerCenterViewModelTests.customerInfoWithGoogleSubscriptions + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == true + expect(viewModel.subscriptionsAreFromApple) == false + expect(viewModel.state) == .success + } + + func testLoadHasSubscriptionsNonActive() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + return await CustomerCenterViewModelTests.customerInfoWithoutSubscriptions + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == false + expect(viewModel.subscriptionsAreFromApple) == false + expect(viewModel.state) == .success + } + + func testLoadHasSubscriptionsFailure() async { + let viewModel = CustomerCenterViewModel(customerInfoFetcher: { + throw TestError(message: "An error occurred") + }) + + await viewModel.loadHasSubscriptions() + + expect(viewModel.hasSubscriptions) == false + expect(viewModel.subscriptionsAreFromApple) == false + switch viewModel.state { + case .error(let stateError): + expect(stateError as? TestError) == error + default: + fail("Expected state to be .error") + } + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension CustomerCenterViewModelTests { + + static let customerInfoWithAppleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithGoogleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithoutSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2000-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "1999-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "1999-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2000-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "1999-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + +} + From dc8e85566880cf4cc12ed471c2a5ec21bd1efa07 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 12 Jun 2024 16:08:38 +0200 Subject: [PATCH 44/50] Make Viewmodel testable --- .../ManageSubscriptionsPurchaseType.swift | 37 ++ .../ManageSubscriptionsViewModel.swift | 50 ++- .../ManageSubscriptionsViewModelTests.swift | 388 ++++++++++++++++++ 3 files changed, 463 insertions(+), 12 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift create mode 100644 Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift new file mode 100644 index 0000000000..212f6ca568 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift @@ -0,0 +1,37 @@ +// +// 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 +// +// ManageSubscriptionsPurchaseType.swift +// +// +// Created by Cesar de la Vega on 12/6/24. +// + +import Foundation +import RevenueCat + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +protocol ManageSubscriptionsPurchaseType: Sendable { + + @Sendable + func customerInfo() async throws -> CustomerInfo + + @Sendable + func products(_ productIdentifiers: [String]) async -> [StoreProduct] + + @Sendable + func showManageSubscriptions() async throws + + @Sendable + func beginRefundRequest(forProduct productID: String) async throws -> RefundRequestStatus + +} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 864358a5ec..68fa049a00 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -44,20 +44,26 @@ class ManageSubscriptionsViewModel: ObservableObject { return state != .notLoaded } + private var purchasesProvider: ManageSubscriptionsPurchaseType + private var error: Error? - init() { - state = .notLoaded + convenience init() { + self.init(purchasesProvider: ManageSubscriptionPurchases()) } - init(configuration: CustomerCenterConfigData) { - state = .notLoaded - self.configuration = configuration + // @PublicForExternalTesting + init(purchasesProvider: ManageSubscriptionsPurchaseType) { + self.state = .notLoaded + self.purchasesProvider = purchasesProvider } - init(configuration: CustomerCenterConfigData, subscriptionInformation: SubscriptionInformation) { + // @PublicForExternalTesting + init(configuration: CustomerCenterConfigData, + subscriptionInformation: SubscriptionInformation) { self.configuration = configuration self.subscriptionInformation = subscriptionInformation + self.purchasesProvider = ManageSubscriptionPurchases() state = .success } @@ -72,13 +78,12 @@ class ManageSubscriptionsViewModel: ObservableObject { } func loadSubscriptionInformation() async throws { - let customerInfo = try await Purchases.shared.customerInfo() + let customerInfo = try await purchasesProvider.customerInfo() guard let currentEntitlementDict = customerInfo.entitlements.active.first, let subscribedProductID = customerInfo.activeSubscriptions.first, - let subscribedProduct = await Purchases.shared.products([subscribedProductID]).first else { + let subscribedProduct = await purchasesProvider.products([subscribedProductID]).first else { Logger.warning(Strings.could_not_find_subscription_information) - self.state = .error(CustomerCenterError.couldNotFindSubscriptionInformation) - return + throw CustomerCenterError.couldNotFindSubscriptionInformation } let currentEntitlement = currentEntitlementDict.value @@ -110,7 +115,7 @@ class ManageSubscriptionsViewModel: ObservableObject { Task { guard let subscriptionInformation = self.subscriptionInformation else { return } let productId = subscriptionInformation.productIdentifier - let status = try await Purchases.shared.beginRefundRequest(forProduct: productId) + let status = try await purchasesProvider.beginRefundRequest(forProduct: productId) switch status { case .error: self.refundRequestStatus = "Error when requesting refund, try again" @@ -122,7 +127,7 @@ class ManageSubscriptionsViewModel: ObservableObject { } case .changePlans, .cancel: Task { - try await Purchases.shared.showManageSubscriptions() + try await purchasesProvider.showManageSubscriptions() } default: break @@ -132,6 +137,27 @@ class ManageSubscriptionsViewModel: ObservableObject { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +private final class ManageSubscriptionPurchases: ManageSubscriptionsPurchaseType { + + func beginRefundRequest(forProduct productID: String) async throws -> RevenueCat.RefundRequestStatus { + try await Purchases.shared.beginRefundRequest(forProduct: productID) + } + + func showManageSubscriptions() async throws { + try await Purchases.shared.showManageSubscriptions() + } + + func customerInfo() async throws -> RevenueCat.CustomerInfo { + return try await Purchases.shared.customerInfo() + } + + func products(_ productIdentifiers: [String]) async -> [StoreProduct] { + return await Purchases.shared.products(productIdentifiers) + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift new file mode 100644 index 0000000000..150c672bb7 --- /dev/null +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -0,0 +1,388 @@ +// +// 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 +// +// ManageSubscriptionsViewModelTests.swift +// +// +// Created by Cesar de la Vega on 11/6/24. +// + +import Nimble +import XCTest +@testable import RevenueCatUI +import RevenueCat +import StoreKit + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@MainActor +class ManageSubscriptionsViewModelTests: TestCase { + + private let error = TestError(message: "An error occurred") + + private struct TestError: Error, Equatable { + let message: String + var localizedDescription: String { + return message + } + } + + func testInitialState() { + let viewModel = ManageSubscriptionsViewModel() + + expect(viewModel.state) == .notLoaded + expect(viewModel.subscriptionInformation).to(beNil()) + expect(viewModel.refundRequestStatus).to(beNil()) + expect(viewModel.configuration).to(beNil()) + expect(viewModel.showRestoreAlert) == false + expect(viewModel.isLoaded) == false + } + + func testStateChangeToError() { + let viewModel = ManageSubscriptionsViewModel() + + viewModel.state = .error(error) + + switch viewModel.state { + case .error(let stateError): + expect(stateError as? TestError) == error + default: + fail("Expected state to be .error") + } + } + + func testIsLoaded() { + let viewModel = ManageSubscriptionsViewModel() + + expect(viewModel.isLoaded) == false + + viewModel.state = .success + + expect(viewModel.isLoaded) == true + } + + func testLoadScreenSuccess() async { + let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases()) + + await viewModel.loadScreen() + + expect(viewModel.subscriptionInformation).toNot(beNil()) + expect(viewModel.configuration).toNot(beNil()) + expect(viewModel.state) == .success + + expect(viewModel.subscriptionInformation?.title) == "title" + expect(viewModel.subscriptionInformation?.durationTitle) == "month" + expect(viewModel.subscriptionInformation?.price) == "$2.99" + expect(viewModel.subscriptionInformation?.nextRenewalString) == "12 Apr 2062" + expect(viewModel.subscriptionInformation?.productIdentifier) == "com.revenuecat.product" + } + + + func testLoadScreenNoActiveSubscription() async { + let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases( + customerInfo: CustomerCenterViewModelTests.customerInfoWithoutSubscriptions + )) + + await viewModel.loadScreen() + + expect(viewModel.subscriptionInformation).to(beNil()) + expect(viewModel.state) == .error(CustomerCenterError.couldNotFindSubscriptionInformation) + } + + func testLoadScreenFailure() async { + let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases( + customerInfoError: error + )) + + await viewModel.loadScreen() + + expect(viewModel.subscriptionInformation).to(beNil()) + expect(viewModel.state) == .error(error) + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +final class MockManageSubscriptionsPurchases: ManageSubscriptionsPurchaseType { + + let customerInfo: CustomerInfo? + let customerInfoError: Error? + let productsShouldFail: Bool + let showManageSubscriptionsError: Error? + let beginRefundShouldFail: Bool + + init( + customerInfo: CustomerInfo? = nil, + customerInfoError: Error? = nil, + productsShouldFail: Bool = false, + showManageSubscriptionsError: Error? = nil, + beginRefundShouldFail: Bool = false + ) { + self.customerInfo = customerInfo + self.customerInfoError = customerInfoError + self.productsShouldFail = productsShouldFail + self.showManageSubscriptionsError = showManageSubscriptionsError + self.beginRefundShouldFail = beginRefundShouldFail + } + + func customerInfo() async throws -> RevenueCat.CustomerInfo { + if let customerInfoError { + throw customerInfoError + } + if let customerInfo { + return customerInfo + } + return await CustomerCenterViewModelTests.customerInfoWithAppleSubscriptions + } + + func products(_ productIdentifiers: [String]) async -> [RevenueCat.StoreProduct] { + if productsShouldFail { + return [] + } + let product = await CustomerCenterViewModelTests.createMockProduct() + return [product] + } + + func showManageSubscriptions() async throws { + if let showManageSubscriptionsError { + throw showManageSubscriptionsError + } + } + + func beginRefundRequest(forProduct productID: String) async throws -> RevenueCat.RefundRequestStatus { + if beginRefundShouldFail { + return .error + } + return .success + } + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +private extension CustomerCenterViewModelTests { + + static func createMockProduct() -> StoreProduct { + // Using SK1 products because they can be mocked, but CustomerCenterViewModel + // works with generic `StoreProduct`s regardless of what they contain + return StoreProduct(sk1Product: MockSK1Product(mockProductIdentifier: "identifier", + mockLocalizedTitle: "title")) + } + + static let customerInfoWithAppleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "app_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithGoogleSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2062-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "2022-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "2022-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2062-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "2022-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + + static let customerInfoWithoutSubscriptions: CustomerInfo = { + return .decode( + """ + { + "schema_version": "4", + "request_date": "2022-03-08T17:42:58Z", + "request_date_ms": 1646761378845, + "subscriber": { + "first_seen": "2022-03-08T17:42:58Z", + "last_seen": "2022-03-08T17:42:58Z", + "management_url": "https://apps.apple.com/account/subscriptions", + "non_subscriptions": { + }, + "original_app_user_id": "$RCAnonymousID:5b6fdbac3a0c4f879e43d269ecdf9ba1", + "original_application_version": "1.0", + "original_purchase_date": "2022-04-12T00:03:24Z", + "other_purchases": { + }, + "subscriptions": { + "com.revenuecat.product": { + "billing_issues_detected_at": null, + "expires_date": "2000-04-12T00:03:35Z", + "grace_period_expires_date": null, + "is_sandbox": true, + "original_purchase_date": "1999-04-12T00:03:28Z", + "period_type": "intro", + "purchase_date": "1999-04-12T00:03:28Z", + "store": "play_store", + "unsubscribe_detected_at": null + }, + }, + "entitlements": { + "premium": { + "expires_date": "2000-04-12T00:03:35Z", + "product_identifier": "com.revenuecat.product", + "purchase_date": "1999-04-12T00:03:28Z" + } + } + } + } + """ + ) + }() + +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +fileprivate class MockSK1Product: SK1Product { + var mockProductIdentifier: String + var mockLocalizedTitle: String + + init(mockProductIdentifier: String, mockLocalizedTitle: String) { + self.mockProductIdentifier = mockProductIdentifier + self.mockLocalizedTitle = mockLocalizedTitle + + super.init() + } + + override var productIdentifier: String { + return self.mockProductIdentifier + } + + var mockSubscriptionGroupIdentifier: String? + override var subscriptionGroupIdentifier: String? { + return self.mockSubscriptionGroupIdentifier + } + + var mockPriceLocale: Locale? + override var priceLocale: Locale { + return mockPriceLocale ?? Locale(identifier: "en_US") + } + + var mockPrice: Decimal? + override var price: NSDecimalNumber { + return (mockPrice ?? 2.99) as NSDecimalNumber + } + + override var localizedTitle: String { + return self.mockLocalizedTitle + } + + override var introductoryPrice: SKProductDiscount? { + return mockDiscount + } + + private var _mockDiscount: Any? + + var mockDiscount: SKProductDiscount? { + // swiftlint:disable:next force_cast + get { return self._mockDiscount as! SKProductDiscount? } + set { self._mockDiscount = newValue } + } + + override var discounts: [SKProductDiscount] { + return self.mockDiscount.map { [$0] } ?? [] + } + + private lazy var _mockSubscriptionPeriod: Any? = { + return SKProductSubscriptionPeriod(numberOfUnits: 1, unit: SKProduct.PeriodUnit.month) + }() + + var mockSubscriptionPeriod: SKProductSubscriptionPeriod? { + // swiftlint:disable:next force_cast + get { self._mockSubscriptionPeriod as! SKProductSubscriptionPeriod? } + set { self._mockSubscriptionPeriod = newValue } + } + + override var subscriptionPeriod: SKProductSubscriptionPeriod? { + return mockSubscriptionPeriod + } +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +fileprivate extension SKProductSubscriptionPeriod { + convenience init(numberOfUnits: Int, + unit: SK1Product.PeriodUnit) { + self.init() + self.setValue(numberOfUnits, forKey: "numberOfUnits") + self.setValue(unit.rawValue, forKey: "unit") + } +} + From f507837ec7ffc491a72053a0f5c69f2cda0d4dd7 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 12 Jun 2024 16:10:01 +0200 Subject: [PATCH 45/50] lint fix --- .../ViewModels/CustomerCenterViewState.swift | 2 +- .../CustomerCenter/CustomerCenterViewModelTests.swift | 5 ++--- .../ManageSubscriptionsViewModelTests.swift | 8 +++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift index 217de58c3e..7a982112d0 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/CustomerCenterViewState.swift @@ -21,7 +21,7 @@ enum CustomerCenterViewState: Equatable { case success case error(Error) - static func ==(lhs: CustomerCenterViewState, rhs: CustomerCenterViewState) -> Bool { + static func == (lhs: CustomerCenterViewState, rhs: CustomerCenterViewState) -> Bool { switch (lhs, rhs) { case (.notLoaded, .notLoaded): return true diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index ea2b213439..07d466a401 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -14,9 +14,9 @@ // import Nimble -import XCTest -@testable import RevenueCatUI import RevenueCat +@testable import RevenueCatUI +import XCTest @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @@ -257,4 +257,3 @@ private extension CustomerCenterViewModelTests { }() } - diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index 150c672bb7..6517042196 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -14,10 +14,10 @@ // import Nimble -import XCTest -@testable import RevenueCatUI import RevenueCat +@testable import RevenueCatUI import StoreKit +import XCTest @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @@ -85,7 +85,6 @@ class ManageSubscriptionsViewModelTests: TestCase { expect(viewModel.subscriptionInformation?.productIdentifier) == "com.revenuecat.product" } - func testLoadScreenNoActiveSubscription() async { let viewModel = ManageSubscriptionsViewModel(purchasesProvider: MockManageSubscriptionsPurchases( customerInfo: CustomerCenterViewModelTests.customerInfoWithoutSubscriptions @@ -311,7 +310,7 @@ private extension CustomerCenterViewModelTests { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -fileprivate class MockSK1Product: SK1Product { +private class MockSK1Product: SK1Product { var mockProductIdentifier: String var mockLocalizedTitle: String @@ -385,4 +384,3 @@ fileprivate extension SKProductSubscriptionPeriod { self.setValue(unit.rawValue, forKey: "unit") } } - From 509fe43dee1b8374a34af780d47dcfa5c72dd783 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 12 Jun 2024 16:12:32 +0200 Subject: [PATCH 46/50] check for availability --- .../CustomerCenter/CustomerCenterViewModelTests.swift | 4 ++++ .../CustomerCenter/ManageSubscriptionsViewModelTests.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift index 07d466a401..499352607f 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/CustomerCenterViewModelTests.swift @@ -18,6 +18,8 @@ import RevenueCat @testable import RevenueCatUI import XCTest +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -257,3 +259,5 @@ private extension CustomerCenterViewModelTests { }() } + +#endif diff --git a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift index 6517042196..57e4169682 100644 --- a/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift +++ b/Tests/RevenueCatUITests/CustomerCenter/ManageSubscriptionsViewModelTests.swift @@ -19,6 +19,8 @@ import RevenueCat import StoreKit import XCTest +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -384,3 +386,5 @@ fileprivate extension SKProductSubscriptionPeriod { self.setValue(unit.rawValue, forKey: "unit") } } + +#endif From 01250f383e5db421e2a2dda9aa0a114929e889bc Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 12 Jun 2024 16:17:41 +0200 Subject: [PATCH 47/50] fix macos --- .../ViewModels/ManageSubscriptionsViewModel.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 68fa049a00..b915e4ddcb 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -16,6 +16,8 @@ import Foundation import RevenueCat +#if !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -138,6 +140,10 @@ class ManageSubscriptionsViewModel: ObservableObject { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) private final class ManageSubscriptionPurchases: ManageSubscriptionsPurchaseType { func beginRefundRequest(forProduct productID: String) async throws -> RevenueCat.RefundRequestStatus { @@ -175,3 +181,5 @@ private extension SubscriptionPeriod { } } + +#endif From 11729d8417641e5d8de71aa648e7f29094547b34 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 12 Jun 2024 17:23:36 +0200 Subject: [PATCH 48/50] refundRequestStatusMessage --- .../ViewModels/ManageSubscriptionsViewModel.swift | 8 ++++---- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index b915e4ddcb..63bda43c55 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -28,7 +28,7 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published var subscriptionInformation: SubscriptionInformation? @Published - var refundRequestStatus: String? + var refundRequestStatusMessage: String? @Published var configuration: CustomerCenterConfigData? @Published @@ -120,11 +120,11 @@ class ManageSubscriptionsViewModel: ObservableObject { let status = try await purchasesProvider.beginRefundRequest(forProduct: productId) switch status { case .error: - self.refundRequestStatus = "Error when requesting refund, try again" + self.refundRequestStatusMessage = "Error when requesting refund, try again" case .success: - self.refundRequestStatus = "Refund granted successfully!" + self.refundRequestStatusMessage = "Refund granted successfully!" case .userCancelled: - self.refundRequestStatus = "Refund canceled" + self.refundRequestStatusMessage = "Refund canceled" } } case .changePlans, .cancel: diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 798446db8f..aea116dd20 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -44,7 +44,7 @@ struct ManageSubscriptionsView: View { if let subscriptionInformation = self.viewModel.subscriptionInformation { SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, - refundRequestStatus: viewModel.refundRequestStatus) + refundRequestStatus: viewModel.refundRequestStatusMessage) } Spacer() From 61bc934c19abd5cd0024ea77fff948b5830d6b22 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 12 Jun 2024 17:25:16 +0200 Subject: [PATCH 49/50] CustomerCenterConfigTestData debug only --- .../CustomerCenter/Data/CustomerCenterConfigTestData.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 04242ed964..44d5db8f6b 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -16,6 +16,8 @@ import Foundation import RevenueCat +#if DEBUG + enum CustomerCenterConfigTestData { @available(iOS 14.0, *) @@ -77,3 +79,5 @@ enum CustomerCenterConfigTestData { ) } + +#endif From 3f66cefd1d951fa72aefa43477e3c16c686f1fb0 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Wed, 12 Jun 2024 20:26:49 +0200 Subject: [PATCH 50/50] extracts strings --- .../ViewModels/ManageSubscriptionsViewModel.swift | 6 +++--- .../CustomerCenter/Views/ManageSubscriptionsView.swift | 8 ++++---- RevenueCatUI/Resources/en.lproj/Localizable.strings | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 63bda43c55..8a5278db96 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -120,11 +120,11 @@ class ManageSubscriptionsViewModel: ObservableObject { let status = try await purchasesProvider.beginRefundRequest(forProduct: productId) switch status { case .error: - self.refundRequestStatusMessage = "Error when requesting refund, try again" + self.refundRequestStatusMessage = String(localized: "Error when requesting refund, try again") case .success: - self.refundRequestStatusMessage = "Refund granted successfully!" + self.refundRequestStatusMessage = String(localized: "Refund granted successfully!") case .userCancelled: - self.refundRequestStatusMessage = "Refund canceled" + self.refundRequestStatusMessage = String(localized: "Refund canceled") } } case .changePlans, .cancel: diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index aea116dd20..3378910657 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -44,7 +44,7 @@ struct ManageSubscriptionsView: View { if let subscriptionInformation = self.viewModel.subscriptionInformation { SubscriptionDetailsView(subscriptionInformation: subscriptionInformation, - refundRequestStatus: viewModel.refundRequestStatusMessage) + refundRequestStatusMessage: viewModel.refundRequestStatusMessage) } Spacer() @@ -104,7 +104,7 @@ struct HeaderView: View { struct SubscriptionDetailsView: View { let subscriptionInformation: SubscriptionInformation - let refundRequestStatus: String? + let refundRequestStatusMessage: String? var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -124,8 +124,8 @@ struct SubscriptionDetailsView: View { .padding([.horizontal, .bottom]) } - if let refundRequestStatus = refundRequestStatus { - Text("Refund request status: \(refundRequestStatus)") + if let refundRequestStatusMessage = refundRequestStatusMessage { + Text("Refund request status: \(refundRequestStatusMessage)") .font(.caption) .bold() .foregroundColor(Color.gray) diff --git a/RevenueCatUI/Resources/en.lproj/Localizable.strings b/RevenueCatUI/Resources/en.lproj/Localizable.strings index eb59e1d760..60298cccd1 100644 --- a/RevenueCatUI/Resources/en.lproj/Localizable.strings +++ b/RevenueCatUI/Resources/en.lproj/Localizable.strings @@ -17,3 +17,7 @@ "%d%% off" = "%d%% off"; "Continue" = "Continue"; "Default_offer_details_with_intro_offer" = "Start your {{ sub_offer_duration }} trial, then {{ total_price_and_per_month }}."; +"Error when requesting refund, try again" = "Error when requesting refund, try again" +"Refund granted successfully!" = "Refund granted successfully!" +"Refund canceled" = "Refund canceled" +"Refund request status: %@" = "Refund request status: %@"