From d00e4a78dd097881dfbab69a41c9f5e47f7ebaf1 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Fri, 21 Jun 2024 19:59:42 +0200 Subject: [PATCH] Add colors support --- .../xcshareddata/WorkspaceSettings.xcsettings | 5 ++ .../Data/CustomerCenterConfigTestData.swift | 17 ++-- .../ManageSubscriptionsButtonStyle.swift | 31 ++++++- .../ManageSubscriptionsViewModel.swift | 32 ++++---- .../Views/CustomerCenterView.swift | 2 +- .../Views/FeedbackSurveyView.swift | 14 +++- .../Views/ManageSubscriptionsView.swift | 18 +++-- .../Views/NoSubscriptionsView.swift | 15 ++-- .../Views/PromotionalOfferView.swift | 13 ++- .../CustomerCenterConfigData.swift | 81 ++++++++----------- .../CustomerCenterConfigResponse.swift | 14 +++- 11 files changed, 138 insertions(+), 104 deletions(-) create mode 100644 RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/RevenueCat.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift index 9b63de3301..7090be92c0 100644 --- a/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift +++ b/RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigTestData.swift @@ -86,17 +86,12 @@ enum CustomerCenterConfigTestData { ) ], appearance: .init( - mode: .custom, - light: .init( - accentColor: "#ffffff", - backgroundColor: "#000000", - textColor: "#000000" - ), - dark: .init( - accentColor: "#000000", - backgroundColor: "#ffffff", - textColor: "#ffffff" - ) + mode: .custom(accentColor: try! RCColor(light: RCColor(stringRepresentation: "#ffffff"), + dark: try! RCColor(stringRepresentation: "#000000")), + backgroundColor: try! RCColor(light: RCColor(stringRepresentation: "#000000"), + dark: try! RCColor(stringRepresentation: "#ffffff")), + textColor: try! RCColor(light: RCColor(stringRepresentation: "#000000"), + dark: try! RCColor(stringRepresentation: "#ffffff"))) ), localization: .init( locale: "en_US", diff --git a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift index c4970d4ff7..654d0ff415 100644 --- a/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift +++ b/RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift @@ -14,6 +14,7 @@ // import Foundation +import RevenueCat import SwiftUI @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @@ -22,12 +23,17 @@ import SwiftUI @available(watchOS, unavailable) struct ManageSubscriptionsButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { + var appearance: CustomerCenterConfigData.Appearance + + @Environment(\.colorScheme) + private var colorScheme + + func makeBody(configuration: ButtonStyleConfiguration) -> some View { configuration.label .padding() .frame(width: 300) - .background(Color.accentColor) - .foregroundColor(.white) + .background(color(from: appearance)) + .foregroundColor(colorScheme == .dark ? Color.black : Color.white) .cornerRadius(10) .scaleEffect(configuration.isPressed ? 0.95 : 1.0) .opacity(configuration.isPressed ? 0.8 : 1.0) @@ -36,6 +42,23 @@ struct ManageSubscriptionsButtonStyle: ButtonStyle { } +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +@available(macOS, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +private extension ManageSubscriptionsButtonStyle { + + func color(from appearance: CustomerCenterConfigData.Appearance) -> Color { + switch appearance.mode { + case .system: + Color.accentColor + case .custom(accentColor: let accentColor, backgroundColor: let backgroundColor, textColor: let textColor): + accentColor.underlyingColor + } + } + +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @@ -44,7 +67,7 @@ struct CustomButtonStylePreview_Previews: PreviewProvider { static var previews: some View { Button("Didn't receive purchase") {} - .buttonStyle(ManageSubscriptionsButtonStyle()) + .buttonStyle(ManageSubscriptionsButtonStyle(appearance: CustomerCenterConfigData.Appearance(mode: .system))) } } diff --git a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift index 39139a2a1e..103015d0c1 100644 --- a/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift +++ b/RevenueCatUI/CustomerCenter/ViewModels/ManageSubscriptionsViewModel.swift @@ -26,6 +26,7 @@ import RevenueCat class ManageSubscriptionsViewModel: ObservableObject { let screen: CustomerCenterConfigData.Screen + let appearance: CustomerCenterConfigData.Appearance @Published var showRestoreAlert: Bool = false @@ -66,25 +67,31 @@ class ManageSubscriptionsViewModel: ObservableObject { private var error: Error? - convenience init(screen: CustomerCenterConfigData.Screen) { + convenience init(screen: CustomerCenterConfigData.Screen, + appearance: CustomerCenterConfigData.Appearance) { self.init(screen: screen, + appearance: appearance, purchasesProvider: ManageSubscriptionPurchases(), promotionalOfferViewModel: PromotionalOfferViewModel()) } // @PublicForExternalTesting init(screen: CustomerCenterConfigData.Screen, + appearance: CustomerCenterConfigData.Appearance, purchasesProvider: ManageSubscriptionsPurchaseType, promotionalOfferViewModel: PromotionalOfferViewModel) { self.state = .notLoaded self.screen = screen + self.appearance = appearance self.purchasesProvider = purchasesProvider self.promotionalOfferViewModel = promotionalOfferViewModel } init(screen: CustomerCenterConfigData.Screen, + appearance: CustomerCenterConfigData.Appearance, subscriptionInformation: SubscriptionInformation) { self.screen = screen + self.appearance = appearance self.subscriptionInformation = subscriptionInformation self.purchasesProvider = ManageSubscriptionPurchases() self.promotionalOfferViewModel = PromotionalOfferViewModel() @@ -125,22 +132,9 @@ class ManageSubscriptionsViewModel: ObservableObject { ) } - #if os(iOS) || targetEnvironment(macCatalyst) - 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 handleSheetDismiss() { + func handleSheetDismiss() async { if let loadingPath = loadingPath { - performAction(for: loadingPath) + await self.performAction(for: loadingPath) self.loadingPath = nil } } @@ -150,14 +144,16 @@ class ManageSubscriptionsViewModel: ObservableObject { switch path.detail { case let .feedbackSurvey(feedbackSurvey): self.feedbackSurveyData = FeedbackSurveyData(configuration: feedbackSurvey) { [weak self] in - await self?.performAction(for: path) + Task { + await self?.performAction(for: path) + } } case let .promotionalOffer(promotionalOffer): self.loadingPath = path await promotionalOfferViewModel.loadPromo(promotionalOfferId: promotionalOffer.iosOfferId) self.isShowingPromotionalOffer = true default: - performAction(for: path) + await self.performAction(for: path) } } #endif diff --git a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift index 53970e3c96..7e2a82920f 100644 --- a/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift +++ b/RevenueCatUI/CustomerCenter/Views/CustomerCenterView.swift @@ -72,7 +72,7 @@ private extension CustomerCenterView { if viewModel.hasSubscriptions { if viewModel.subscriptionsAreFromApple, let screen = configuration[.management] { - ManageSubscriptionsView(screen: screen) + ManageSubscriptionsView(screen: screen, appearance: configuration.appearance) } else { WrongPlatformView() } diff --git a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift index 1e8fec8f30..9ebfab3ac9 100644 --- a/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift +++ b/RevenueCatUI/CustomerCenter/Views/FeedbackSurveyView.swift @@ -27,9 +27,12 @@ struct FeedbackSurveyView: View { @StateObject private var viewModel: FeedbackSurveyViewModel + private let appearance: CustomerCenterConfigData.Appearance - init(feedbackSurveyData: FeedbackSurveyData) { + init(feedbackSurveyData: FeedbackSurveyData, + appearance: CustomerCenterConfigData.Appearance) { self._viewModel = StateObject(wrappedValue: FeedbackSurveyViewModel(feedbackSurveyData: feedbackSurveyData)) + self.appearance = appearance } var body: some View { @@ -42,6 +45,7 @@ struct FeedbackSurveyView: View { FeedbackSurveyButtonsView(options: self.viewModel.feedbackSurveyData.configuration.options, action: self.viewModel.handleAction(for:), + appearance: self.appearance, loadingStates: self.$viewModel.loadingStates) } .sheet( @@ -51,8 +55,8 @@ struct FeedbackSurveyView: View { if let promotionalOffer = self.viewModel.promotionalOffer, let product = self.viewModel.product { PromotionalOfferView(promotionalOffer: promotionalOffer, - product: product - ) + product: product, + appearance: self.appearance) } }) } @@ -68,6 +72,8 @@ struct FeedbackSurveyButtonsView: View { let options: [CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option] let action: (CustomerCenterConfigData.HelpPath.FeedbackSurvey.Option) async -> Void + let appearance: CustomerCenterConfigData.Appearance + @Binding var loadingStates: [String: Bool] @@ -85,7 +91,7 @@ struct FeedbackSurveyButtonsView: View { Text(option.title) } } - .buttonStyle(ManageSubscriptionsButtonStyle()) + .buttonStyle(ManageSubscriptionsButtonStyle(appearance: appearance)) .disabled(self.loadingStates[option.id] ?? false) } } diff --git a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift index 9a372891e5..ad589e7206 100644 --- a/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/ManageSubscriptionsView.swift @@ -31,8 +31,9 @@ struct ManageSubscriptionsView: View { @StateObject private var viewModel: ManageSubscriptionsViewModel - init(screen: CustomerCenterConfigData.Screen) { - let viewModel = ManageSubscriptionsViewModel(screen: screen) + init(screen: CustomerCenterConfigData.Screen, + appearance: CustomerCenterConfigData.Appearance) { + let viewModel = ManageSubscriptionsViewModel(screen: screen, appearance: appearance) self._viewModel = .init(wrappedValue: viewModel) } @@ -63,7 +64,8 @@ struct ManageSubscriptionsView: View { if let feedbackSurveyData = self.viewModel.feedbackSurveyData { NavigationLink( - destination: FeedbackSurveyView(feedbackSurveyData: feedbackSurveyData) + destination: FeedbackSurveyView(feedbackSurveyData: feedbackSurveyData, + appearance: self.viewModel.appearance) .onDisappear { self.viewModel.feedbackSurveyData = nil }, @@ -204,15 +206,18 @@ struct ManageSubscriptionButton: View { } .restorePurchasesAlert(isPresented: self.$viewModel.showRestoreAlert) .sheet(isPresented: self.$viewModel.isShowingPromotionalOffer, onDismiss: { - self.viewModel.handleSheetDismiss() + Task { + await self.viewModel.handleSheetDismiss() + } }, content: { if let promotionalOffer = self.viewModel.promotionalOffer, let product = self.viewModel.product { PromotionalOfferView(promotionalOffer: promotionalOffer, - product: product) + product: product, + appearance: self.viewModel.appearance) } }) - .buttonStyle(ManageSubscriptionsButtonStyle()) + .buttonStyle(ManageSubscriptionsButtonStyle(appearance: self.viewModel.appearance)) .disabled(self.viewModel.loadingPath?.id == path.id) } } @@ -229,6 +234,7 @@ struct ManageSubscriptionsView_Previews: PreviewProvider { static var previews: some View { let viewModel = ManageSubscriptionsViewModel( screen: CustomerCenterConfigTestData.customerCenterData[.management]!, + appearance: CustomerCenterConfigTestData.customerCenterData.appearance, subscriptionInformation: CustomerCenterConfigTestData.subscriptionInformation) ManageSubscriptionsView(viewModel: viewModel) } diff --git a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift index 2790e182f5..5d0ebc1911 100644 --- a/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift +++ b/RevenueCatUI/CustomerCenter/Views/NoSubscriptionsView.swift @@ -24,21 +24,20 @@ import SwiftUI @available(watchOS, unavailable) @available(visionOS, unavailable) struct NoSubscriptionsView: View { + + init(configuration: CustomerCenterConfigData) { + self.configuration = configuration + } // swiftlint:disable:next todo // TODO: build screen using this configuration - let configuration: CustomerCenterConfigData + private let configuration: CustomerCenterConfigData @Environment(\.dismiss) - var dismiss - + private var dismiss @State private var showRestoreAlert: Bool = false - init(configuration: CustomerCenterConfigData) { - self.configuration = configuration - } - var body: some View { VStack { Text("No Subscriptions found") @@ -54,7 +53,7 @@ struct NoSubscriptionsView: View { showRestoreAlert = true } .restorePurchasesAlert(isPresented: $showRestoreAlert) - .buttonStyle(ManageSubscriptionsButtonStyle()) + .buttonStyle(ManageSubscriptionsButtonStyle(appearance: self.configuration.appearance)) Button("Cancel") { dismiss() diff --git a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift index 84a9a7b52b..c34b81eb9b 100644 --- a/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift +++ b/RevenueCatUI/CustomerCenter/Views/PromotionalOfferView.swift @@ -26,23 +26,30 @@ import SwiftUI @available(visionOS, unavailable) struct PromotionalOfferView: View { + private let appearance: CustomerCenterConfigData.Appearance + @StateObject private var viewModel: PromotionalOfferViewModel @Environment(\.dismiss) private var dismiss private var promotionalOfferId: String - init(promotionalOfferId: String) { + init(promotionalOfferId: String, + appearance: CustomerCenterConfigData.Appearance) { let viewModel = PromotionalOfferViewModel() self._viewModel = StateObject(wrappedValue: viewModel) self.promotionalOfferId = promotionalOfferId + self.appearance = appearance } - init(promotionalOffer: PromotionalOffer, product: StoreProduct) { + init(promotionalOffer: PromotionalOffer, + product: StoreProduct, + appearance: CustomerCenterConfigData.Appearance) { let viewModel = PromotionalOfferViewModel(product: product, promotionalOffer: promotionalOffer) self._viewModel = StateObject(wrappedValue: viewModel) // force unwrap since it is only `nil` for SK1 products before iOS 12.2. self.promotionalOfferId = promotionalOffer.discount.offerIdentifier! + self.appearance = appearance } var body: some View { @@ -74,7 +81,7 @@ struct PromotionalOfferView: View { .font(.subheadline) } }) - .buttonStyle(ManageSubscriptionsButtonStyle()) + .buttonStyle(ManageSubscriptionsButtonStyle(appearance: self.appearance)) Button("No thanks") { dismiss() diff --git a/Sources/CustomerCenter/CustomerCenterConfigData.swift b/Sources/CustomerCenter/CustomerCenterConfigData.swift index 9e94071f53..ba3c0aabeb 100644 --- a/Sources/CustomerCenter/CustomerCenterConfigData.swift +++ b/Sources/CustomerCenter/CustomerCenterConfigData.swift @@ -15,6 +15,9 @@ import Foundation +// swiftlint:disable missing_docs +public typealias RCColor = PaywallColor + // swiftlint:disable missing_docs // swiftlint:disable nesting public struct CustomerCenterConfigData { @@ -132,45 +135,16 @@ public struct CustomerCenterConfigData { public struct Appearance { - let mode: AppearanceMode - let light: AppearanceCustomColors - let dark: AppearanceCustomColors - - public init(mode: AppearanceMode, light: AppearanceCustomColors, dark: AppearanceCustomColors) { + public let mode: AppearanceMode + + public init(mode: AppearanceMode) { self.mode = mode - self.light = light - self.dark = dark - } - - public struct AppearanceCustomColors { - - let accentColor: String - let backgroundColor: String - let textColor: String - - public init(accentColor: String, backgroundColor: String, textColor: String) { - self.accentColor = accentColor - self.backgroundColor = backgroundColor - self.textColor = textColor - } - } - public enum AppearanceMode: String { - - case custom = "CUSTOM" - case system = "SYSTEM" - - init(from rawValue: String) { - switch rawValue { - case "CUSTOM": - self = .custom - case "SYSTEM": - self = .system - default: - self = .system - } - } + public enum AppearanceMode { + + case system + case custom(accentColor: RCColor, backgroundColor: RCColor, textColor: RCColor) } @@ -211,6 +185,7 @@ public struct CustomerCenterConfigData { } +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension CustomerCenterConfigData { init(from response: CustomerCenterConfigResponse) { @@ -237,24 +212,38 @@ extension CustomerCenterConfigData.Screen { } +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) extension CustomerCenterConfigData.Appearance { init(from response: CustomerCenterConfigResponse.Appearance) { - self.mode = CustomerCenterConfigData.Appearance.AppearanceMode(from: response.mode) - self.light = CustomerCenterConfigData.Appearance.AppearanceCustomColors(from: response.light) - self.dark = CustomerCenterConfigData.Appearance.AppearanceCustomColors(from: response.dark) + self.mode = CustomerCenterConfigData.Appearance.AppearanceMode(from: response) } } -extension CustomerCenterConfigData.Appearance.AppearanceCustomColors { +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension CustomerCenterConfigData.Appearance.AppearanceMode { - init(from response: CustomerCenterConfigResponse.Appearance.AppearanceCustomColors) { - // swiftlint:disable:next todo - // TODO: convert colors to PaywallColor (RCColor) - self.accentColor = response.accentColor - self.backgroundColor = response.backgroundColor - self.textColor = response.textColor + init(from response: CustomerCenterConfigResponse.Appearance) { + switch (response.mode) { + case .system: + self = .system + case .custom: + do { + let accent = RCColor(light: try RCColor(stringRepresentation: response.light.accentColor), + dark: try RCColor(stringRepresentation: response.dark.accentColor)) + let background = RCColor(light: try RCColor(stringRepresentation: response.light.backgroundColor), + dark: try RCColor(stringRepresentation: response.dark.backgroundColor)) + let text = RCColor(light: try RCColor(stringRepresentation: response.light.textColor), + dark: try RCColor(stringRepresentation: response.dark.textColor)) + self = .custom(accentColor: accent, + backgroundColor: background, + textColor: text) + } catch { + Logger.error("Failed to parse appearance colors") + self = .system + } + } } } diff --git a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift index 1241358369..4791fc22f8 100644 --- a/Sources/Networking/Responses/CustomerCenterConfigResponse.swift +++ b/Sources/Networking/Responses/CustomerCenterConfigResponse.swift @@ -81,16 +81,23 @@ struct CustomerCenterConfigResponse { struct Appearance { - let mode: String + let mode: AppearanceMode let light: AppearanceCustomColors let dark: AppearanceCustomColors - struct AppearanceCustomColors { + enum AppearanceMode: String { + + case system = "SYSTEM" + case custom = "CUSTOM" + + } + struct AppearanceCustomColors { + let accentColor: String let backgroundColor: String let textColor: String - + } } @@ -129,6 +136,7 @@ extension CustomerCenterConfigResponse.HelpPath.PromotionalOffer: Codable, Equat extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey: Codable, Equatable {} extension CustomerCenterConfigResponse.HelpPath.FeedbackSurvey.Option: Codable, Equatable {} extension CustomerCenterConfigResponse.Appearance: Codable, Equatable {} +extension CustomerCenterConfigResponse.Appearance.AppearanceMode: Codable, Equatable {} extension CustomerCenterConfigResponse.Appearance.AppearanceCustomColors: Codable, Equatable {} extension CustomerCenterConfigResponse.Screen: Codable, Equatable {} extension CustomerCenterConfigResponse.Screen.ScreenType: Codable, Equatable {}