From fec94860c96c88de67f3b139f4a49ffc03ec8933 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Thu, 11 Jul 2024 08:10:37 +0200 Subject: [PATCH] [Customer Center] Build feedback survey from JSON (#3959) Based off #3933 https://github.com/RevenueCat/purchases-ios/assets/664544/11eae984-294a-4e14-8c40-7c2d50994c09 Can probably use some animations, but tuning that up will come up later It will open a feedback survey for an option if there's one --- RevenueCat.xcodeproj/project.pbxproj | 8 ++ .../Data/FeedbackSurveyData.swift | 37 +++++++++ .../ManageSubscriptionsButtonStyle.swift | 4 +- .../ManageSubscriptionsViewModel.swift | 17 +++- .../Views/FeedbackSurveyView.swift | 82 +++++++++++++++++++ .../Views/ManageSubscriptionsView.swift | 37 ++++++++- 6 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift create mode 100644 RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 665727fe43..952d6a9c29 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -218,6 +218,8 @@ 35B745A82711001A00458D46 /* MockManageSubscriptionsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E840CD2710E2EB00899AE2 /* MockManageSubscriptionsHelper.swift */; }; 35C05DC02BC84F5800109308 /* DiagnosticsSynchronizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C05DBF2BC84F5800109308 /* DiagnosticsSynchronizerTests.swift */; }; 35C05DC82BC8510000109308 /* DiagnosticsTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C05DC72BC8510000109308 /* DiagnosticsTrackerTests.swift */; }; + 35C200AF2C39252D00B9778B /* FeedbackSurveyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */; }; + 35C200B12C39254100B9778B /* FeedbackSurveyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */; }; 35C272A12BC4084C005A0CE8 /* MockDiagnosticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */; }; 35C272A22BC4084C005A0CE8 /* MockDiagnosticsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */; }; 35D0E5D026A5886C0099EAD8 /* ErrorUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */; }; @@ -1194,6 +1196,8 @@ 35AB6D392BBEE3150076B103 /* DiagnosticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTracker.swift; sourceTree = ""; }; 35C05DBF2BC84F5800109308 /* DiagnosticsSynchronizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsSynchronizerTests.swift; sourceTree = ""; }; 35C05DC72BC8510000109308 /* DiagnosticsTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTrackerTests.swift; sourceTree = ""; }; + 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackSurveyData.swift; sourceTree = ""; }; + 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackSurveyView.swift; sourceTree = ""; }; 35C272A02BC4084C005A0CE8 /* MockDiagnosticsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDiagnosticsTracker.swift; sourceTree = ""; }; 35D0E5CF26A5886C0099EAD8 /* ErrorUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorUtils.swift; sourceTree = ""; }; 35D159CA2BC4396F004D8061 /* DiagnosticsPostOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsPostOperation.swift; sourceTree = ""; }; @@ -2549,6 +2553,7 @@ children = ( 353756532C382C2800A1B8D6 /* CustomerCenterConfigTestData.swift */, 353756542C382C2800A1B8D6 /* CustomerCenterError.swift */, + 35C200AE2C39252D00B9778B /* FeedbackSurveyData.swift */, 353756552C382C2800A1B8D6 /* SubscriptionInformation.swift */, ); path = Data; @@ -2568,6 +2573,7 @@ isa = PBXGroup; children = ( 3537565B2C382C2800A1B8D6 /* CustomerCenterView.swift */, + 35C200B02C39254100B9778B /* FeedbackSurveyView.swift */, 3537565C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift */, 3537565D2C382C2800A1B8D6 /* NoSubscriptionsView.swift */, 3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */, @@ -5209,6 +5215,7 @@ 887A60C12C1D037000E1A461 /* DebugErrorView.swift in Sources */, 887A607C2C1D037000E1A461 /* ColorInformation+MultiScheme.swift in Sources */, 887A60782C1D037000E1A461 /* TestData.swift in Sources */, + 35C200B12C39254100B9778B /* FeedbackSurveyView.swift in Sources */, 887A60672C1D037000E1A461 /* PaywallError.swift in Sources */, 88A543E52C37A4AF0039C6A5 /* ConsistentTierContentView.swift in Sources */, 887A606E2C1D037000E1A461 /* LocalizedAlertError.swift in Sources */, @@ -5217,6 +5224,7 @@ 887A60832C1D037000E1A461 /* VersionDetector.swift in Sources */, 887A60872C1D037000E1A461 /* ViewExtensions.swift in Sources */, 353756712C382C2800A1B8D6 /* ManageSubscriptionsPurchaseType.swift in Sources */, + 35C200AF2C39252D00B9778B /* FeedbackSurveyData.swift in Sources */, 887A60BA2C1D037000E1A461 /* Template3View.swift in Sources */, 887A607D2C1D037000E1A461 /* ImageLoader.swift in Sources */, 887A60822C1D037000E1A461 /* PreviewHelpers.swift in Sources */, diff --git a/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.swift new file mode 100644 index 0000000000..8797525399 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Data/FeedbackSurveyData.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 +// +// FeedbackSurveyData.swift +// +// +// Created by Cesar de la Vega on 14/6/24. +// + +import Foundation +import RevenueCat + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +class FeedbackSurveyData: ObservableObject { + + var configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey + var action: (() -> Void) + + init(configuration: CustomerCenterConfigData.HelpPath.FeedbackSurvey, action: @escaping (() -> Void)) { + self.configuration = configuration + self.action = action + } + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index c4970d4ff7..0e29eb90a7 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -7,7 +7,7 @@ // // https://opensource.org/licenses/MIT // -// CustomButtonStyle.swift +// ManageSubscriptionsButtonStyle.swift // // // Created by Cesar de la Vega on 28/5/24. @@ -40,7 +40,7 @@ struct ManageSubscriptionsButtonStyle: ButtonStyle { @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -struct CustomButtonStylePreview_Previews: PreviewProvider { +struct ManageSubscriptionsButtonStyle_Previews: PreviewProvider { static var previews: some View { Button("Didn't receive purchase") {} diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index fa569ecadd..0d5835f78a 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -29,6 +29,9 @@ class ManageSubscriptionsViewModel: ObservableObject { @Published var showRestoreAlert: Bool = false + @Published + var feedbackSurveyData: FeedbackSurveyData? + @Published var state: CustomerCenterViewState { didSet { @@ -105,7 +108,19 @@ class ManageSubscriptionsViewModel: ObservableObject { } #if os(iOS) || targetEnvironment(macCatalyst) - func handleAction(for path: CustomerCenterConfigData.HelpPath) async { + func determineFlow(for path: CustomerCenterConfigData.HelpPath) async { + if case let .feedbackSurvey(feedbackSurvey) = path.detail { + self.feedbackSurveyData = FeedbackSurveyData(configuration: feedbackSurvey) { [weak self] in + Task { + await self?.performAction(for: path) + } + } + } else { + await self.performAction(for: path) + } + } + + func performAction(for path: CustomerCenterConfigData.HelpPath) async { switch path.type { case .missingPurchase: self.showRestoreAlert = true diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift new file mode 100644 index 0000000000..921cc22f04 --- /dev/null +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -0,0 +1,82 @@ +// +// 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 +// +// FeedbackSurveyView.swift +// +// +// Created by Cesar de la Vega on 12/6/24. +// + +import RevenueCat +import SwiftUI + +#if os(iOS) + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +@available(visionOS, unavailable) +struct FeedbackSurveyView: View { + + @ObservedObject + var feedbackSurveyData: FeedbackSurveyData + + var body: some View { + VStack { + Text(feedbackSurveyData.configuration.title) + .font(.title) + .padding() + + Spacer() + + FeedbackSurveyButtonsView(options: feedbackSurveyData.configuration.options, + action: feedbackSurveyData.action) + } + } + +} + +@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 FeedbackSurveyButtonsView: View { + + let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option] + let action: (() -> Void) + + var body: some View { + VStack(spacing: Self.buttonSpacing) { + ForEach(options, id: \.id) { option in + AsyncButton(action: { + self.action() + }, label: { + Text(option.title) + }) + .buttonStyle(ManageSubscriptionsButtonStyle()) + } + } + } + +} + +@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) +extension FeedbackSurveyButtonsView { + + private static let buttonSpacing: CGFloat = 16 + +} + +#endif diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 4aee4f7911..fbf2912169 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -41,6 +41,38 @@ struct ManageSubscriptionsView: View { } var body: some View { + if #available(iOS 16.0, *) { + NavigationStack { + content + .navigationDestination(isPresented: .constant(self.viewModel.feedbackSurveyData != nil)) { + if let feedbackSurveyData = self.viewModel.feedbackSurveyData { + FeedbackSurveyView(feedbackSurveyData: feedbackSurveyData) + .onDisappear { + self.viewModel.feedbackSurveyData = nil + } + } + } + } + } else { + NavigationView { + content + .background(NavigationLink( + destination: self.viewModel.feedbackSurveyData.map { data in + FeedbackSurveyView(feedbackSurveyData: data) + .onDisappear { + self.viewModel.feedbackSurveyData = nil + } + }, + isActive: .constant(self.viewModel.feedbackSurveyData != nil) + ) { + EmptyView() + }) + } + } + } + + @ViewBuilder + var content: some View { VStack { if self.viewModel.isLoaded { HeaderView(viewModel: self.viewModel) @@ -53,6 +85,7 @@ struct ManageSubscriptionsView: View { Spacer() ManageSubscriptionsButtonsView(viewModel: self.viewModel) + } else { ProgressView() .progressViewStyle(CircularProgressViewStyle()) @@ -61,8 +94,8 @@ struct ManageSubscriptionsView: View { .task { await loadInformationIfNeeded() } + .navigationBarTitleDisplayMode(.inline) } - } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -158,7 +191,7 @@ struct ManageSubscriptionsButtonsView: View { } ForEach(filteredPaths, id: \.id) { path in AsyncButton(action: { - await self.viewModel.handleAction(for: path) + await self.viewModel.determineFlow(for: path) }, label: { Text(path.title) })