From 2f9941fdca61af2e82aa8731435b6cdccbb8d095 Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Thu, 3 Oct 2024 13:17:19 +0300 Subject: [PATCH] Add Unified Customization to EntryWidget Add Unified Customization to EntryWidget, as well as add missing UI elements to accompany the customization options MOB-3592 --- GliaWidgets.xcodeproj/project.pbxproj | 20 ++- .../CustomPresentationController.swift | 8 +- .../Sources/EntryWidget/EntryWidget.swift | 15 ++- .../EntryWidgetStyle.RemoteConfig.swift | 88 +++++++++++++ .../EntryWidget/EntryWidgetStyle.swift | 113 +++++++++-------- .../Sources/EntryWidget/EntryWidgetView.swift | 117 +++++++++++++++++- .../EntryWidget/EntryWidgetViewModel.swift | 16 +++ .../MediaTypeItemStyle.RemoteConfig.swift | 54 ++++++++ .../EntryWidget/MediaTypeItemStyle.swift | 59 +++++++++ .../Extensions/CGColor+Extensions.swift | 5 + .../RemoteConfiguration.swift | 21 ++++ .../Sources/Theme/Theme.EntryWidget.swift | 37 ++++-- GliaWidgets/Sources/Theme/Theme.swift | 6 + .../SwiftUI/Extensions/View+Extensions.swift | 53 ++++++++ GliaWidgetsTests/Sources/Glia/GliaTests.swift | 3 +- 15 files changed, 537 insertions(+), 78 deletions(-) create mode 100644 GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.RemoteConfig.swift create mode 100644 GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.RemoteConfig.swift create mode 100644 GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 8bc7c8c14..6e7cdd991 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -679,6 +679,7 @@ C0175A2C2A67E2E9001FACDE /* Theme+Gva.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A2B2A67E2E9001FACDE /* Theme+Gva.swift */; }; C02248A72AD53DDA00CC4930 /* LiveObservationConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02248A62AD53DDA00CC4930 /* LiveObservationConfirmation.swift */; }; C02248AA2AD53E6100CC4930 /* LiveObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02248A92AD53E6100CC4930 /* LiveObservation.swift */; }; + C034EEED2CAAB525002650B8 /* EntryWidgetStyle.RemoteConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034EEEC2CAAB525002650B8 /* EntryWidgetStyle.RemoteConfig.swift */; }; C039FA7F2B19EB6E00DFD0E0 /* MediaPickerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C039FA7E2B19EB6E00DFD0E0 /* MediaPickerLayoutTests.swift */; }; C039FA812B19ECBA00DFD0E0 /* MediaPickerDynamicFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C039FA802B19ECBA00DFD0E0 /* MediaPickerDynamicFontTests.swift */; }; C039FA832B19ED7D00DFD0E0 /* MediaPickerVoiceOverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C039FA822B19ED7D00DFD0E0 /* MediaPickerVoiceOverTests.swift */; }; @@ -850,6 +851,8 @@ C090478C2B7E5C8F003C437C /* FilePreviewStyle.Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C090478B2B7E5C8F003C437C /* FilePreviewStyle.Equatable.swift */; }; C096B40B297EBDE400F0C552 /* VisitorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C096B40A297EBDE400F0C552 /* VisitorCodeTests.swift */; }; C0B325E72AC5A8FA006BC430 /* AlertViewController+LiveObservationConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B325E62AC5A8FA006BC430 /* AlertViewController+LiveObservationConfirmation.swift */; }; + C0C5BB772CAD4278001B2025 /* MediaTypeItemStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C5BB762CAD4259001B2025 /* MediaTypeItemStyle.swift */; }; + C0C5BB792CAD42FD001B2025 /* MediaTypeItemStyle.RemoteConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C5BB782CAD42FD001B2025 /* MediaTypeItemStyle.RemoteConfig.swift */; }; C0D2F02C2991219100803B47 /* VideoCallCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D2F02B2991219100803B47 /* VideoCallCoordinator.swift */; }; C0D2F02E2991221900803B47 /* VideoCallCoordinator.DelegateEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D2F02D2991221900803B47 /* VideoCallCoordinator.DelegateEvent.swift */; }; C0D2F0302991229F00803B47 /* VideoCallCoordinator.Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D2F02F2991229F00803B47 /* VideoCallCoordinator.Environment.swift */; }; @@ -1714,6 +1717,7 @@ C0175A2B2A67E2E9001FACDE /* Theme+Gva.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Gva.swift"; sourceTree = ""; }; C02248A62AD53DDA00CC4930 /* LiveObservationConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveObservationConfirmation.swift; sourceTree = ""; }; C02248A92AD53E6100CC4930 /* LiveObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveObservation.swift; sourceTree = ""; }; + C034EEEC2CAAB525002650B8 /* EntryWidgetStyle.RemoteConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryWidgetStyle.RemoteConfig.swift; sourceTree = ""; }; C039FA7E2B19EB6E00DFD0E0 /* MediaPickerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerLayoutTests.swift; sourceTree = ""; }; C039FA802B19ECBA00DFD0E0 /* MediaPickerDynamicFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerDynamicFontTests.swift; sourceTree = ""; }; C039FA822B19ED7D00DFD0E0 /* MediaPickerVoiceOverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerVoiceOverTests.swift; sourceTree = ""; }; @@ -1885,6 +1889,8 @@ C090478B2B7E5C8F003C437C /* FilePreviewStyle.Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewStyle.Equatable.swift; sourceTree = ""; }; C096B40A297EBDE400F0C552 /* VisitorCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitorCodeTests.swift; sourceTree = ""; }; C0B325E62AC5A8FA006BC430 /* AlertViewController+LiveObservationConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController+LiveObservationConfirmation.swift"; sourceTree = ""; }; + C0C5BB762CAD4259001B2025 /* MediaTypeItemStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTypeItemStyle.swift; sourceTree = ""; }; + C0C5BB782CAD42FD001B2025 /* MediaTypeItemStyle.RemoteConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTypeItemStyle.RemoteConfig.swift; sourceTree = ""; }; C0D2F02B2991219100803B47 /* VideoCallCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallCoordinator.swift; sourceTree = ""; }; C0D2F02D2991221900803B47 /* VideoCallCoordinator.DelegateEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallCoordinator.DelegateEvent.swift; sourceTree = ""; }; C0D2F02F2991229F00803B47 /* VideoCallCoordinator.Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallCoordinator.Environment.swift; sourceTree = ""; }; @@ -5005,14 +5011,17 @@ C0F3DE352C69F4A700DE6D7B /* EntryWidget */ = { isa = PBXGroup; children = ( + C0F7EA372CA1D6D40038019C /* CustomPresentationController.swift */, C0F3DE362C69F51D00DE6D7B /* EntryWidget.swift */, - C0F7EA392CA1D7050038019C /* EntryWidget.SizeConstraints.swift */, - C0F3DE382C69FC2100DE6D7B /* EntryWidget.Presentation.swift */, C0F3DE3E2C6E176A00DE6D7B /* EntryWidget.Channel.swift */, + C0F3DE382C69FC2100DE6D7B /* EntryWidget.Presentation.swift */, + C0F7EA392CA1D7050038019C /* EntryWidget.SizeConstraints.swift */, + C0F3DE442C6E3D7C00DE6D7B /* EntryWidgetStyle.swift */, + C034EEEC2CAAB525002650B8 /* EntryWidgetStyle.RemoteConfig.swift */, C0F3DE3A2C6E0DD900DE6D7B /* EntryWidgetView.swift */, C0F3DE3C2C6E170600DE6D7B /* EntryWidgetViewModel.swift */, - C0F3DE442C6E3D7C00DE6D7B /* EntryWidgetStyle.swift */, - C0F7EA372CA1D6D40038019C /* CustomPresentationController.swift */, + C0C5BB762CAD4259001B2025 /* MediaTypeItemStyle.swift */, + C0C5BB782CAD42FD001B2025 /* MediaTypeItemStyle.RemoteConfig.swift */, ); path = EntryWidget; sourceTree = ""; @@ -5652,6 +5661,7 @@ 9AE9E4B727E1E30500BFE239 /* MockHelpers.swift in Sources */, C0EC58CE2B02775100E78C70 /* SnackBar+View+Mock.swift in Sources */, 8464297D2A459E7D00943BD6 /* UIViewController+Replaceble.swift in Sources */, + C0C5BB772CAD4278001B2025 /* MediaTypeItemStyle.swift in Sources */, C09046EA2B7E0F06003C437C /* Theme.VIsitorChatMessageStyle.RemoteConfig.swift in Sources */, C09046A82B7D08CD003C437C /* WelcomeStyle.SendButton.LoadingStyle.RemoteConfig.swift in Sources */, 1A2DA72625EF892600032611 /* FilePickerController.swift in Sources */, @@ -5819,6 +5829,7 @@ 84265E64298D7B2900D65842 /* ScreenSharingViewStyle.Accessibility.swift in Sources */, 756B8B202996EFA2001D2BB2 /* Header.Props.swift in Sources */, AFA2FDFC289082F500428E6D /* OnHoldOverlayStyle.Mock.swift in Sources */, + C034EEED2CAAB525002650B8 /* EntryWidgetStyle.RemoteConfig.swift in Sources */, C0F3DE432C6E3D2400DE6D7B /* Theme.EntryWidget.swift in Sources */, C090476E2B7E2546003C437C /* UnreadMessageDividerStyle.Equatable.swift in Sources */, C09047602B7E2320003C437C /* ChatFileDownloadStyle.RemoteConfig.swift in Sources */, @@ -5981,6 +5992,7 @@ 3100D92D296E946600DEC9CE /* SecureConversations.ConfirmationViewModel.swift in Sources */, 75D987FC2AF2D9F90016A702 /* SnackBar.Live.swift in Sources */, 3100EEFB293F363100D57F71 /* SecureConversations.WelcomeView.swift in Sources */, + C0C5BB792CAD42FD001B2025 /* MediaTypeItemStyle.RemoteConfig.swift in Sources */, 1A475BBC25DFA10100296D55 /* UnreadMessagesHandler.swift in Sources */, C09046752B7CFFCB003C437C /* ConfirmationStyle.CheckMessageButtonStyle.Accessibility.swift in Sources */, 845E2F85283FA90200C04D56 /* Theme.Survey.ValidationError.Accessibility.swift in Sources */, diff --git a/GliaWidgets/Sources/EntryWidget/CustomPresentationController.swift b/GliaWidgets/Sources/EntryWidget/CustomPresentationController.swift index 318841041..0a1761b6d 100644 --- a/GliaWidgets/Sources/EntryWidget/CustomPresentationController.swift +++ b/GliaWidgets/Sources/EntryWidget/CustomPresentationController.swift @@ -2,7 +2,7 @@ import UIKit final class CustomPresentationController: UIPresentationController { private let height: CGFloat - + private let cornerRadius: CGFloat private let dimmingView: UIView = { let view = UIView() view.backgroundColor = UIColor.black.withAlphaComponent(0.13) @@ -12,9 +12,11 @@ final class CustomPresentationController: UIPresentationController { init( presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, - height: CGFloat + height: CGFloat, + cornerRadius: CGFloat ) { self.height = height + self.cornerRadius = cornerRadius super.init( presentedViewController: presentedViewController, presenting: presentingViewController @@ -46,7 +48,7 @@ final class CustomPresentationController: UIPresentationController { override func containerViewWillLayoutSubviews() { super.containerViewWillLayoutSubviews() - presentedView?.layer.cornerRadius = 24 + presentedView?.layer.cornerRadius = cornerRadius presentedView?.layer.masksToBounds = true presentedView?.frame = frameOfPresentedViewInContainerView } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift index 30d0dc4e9..9ba40a747 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidget.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidget.swift @@ -88,7 +88,15 @@ private extension EntryWidget { let view = makeView(model: model) let hostingController = UIHostingController(rootView: view) - hostingController.view.backgroundColor = UIColor.white + switch theme.entryWidget.backgroundColor { + case .fill(let color): + hostingController.view.backgroundColor = color + case .gradient(let colors): + hostingController.view.makeGradientBackground( + colors: colors, + cornerRadius: theme.entryWidget.cornerRadius + ) + } // Due to the more modern sheet presenting approach being // available starting from iOS 16, we need to handle cases @@ -105,7 +113,7 @@ private extension EntryWidget { } sheet.detents = [smallDetent] sheet.prefersScrollingExpandsWhenScrolledToEdge = true - sheet.preferredCornerRadius = 24 + sheet.preferredCornerRadius = theme.entryWidget.cornerRadius } else { hostingController.modalPresentationStyle = .custom hostingController.transitioningDelegate = self @@ -163,7 +171,8 @@ extension EntryWidget: UIViewControllerTransitioningDelegate { return CustomPresentationController( presentedViewController: presented, presenting: presenting, - height: height + height: height, + cornerRadius: theme.entryWidget.cornerRadius ) } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.RemoteConfig.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.RemoteConfig.swift new file mode 100644 index 000000000..07ab97526 --- /dev/null +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.RemoteConfig.swift @@ -0,0 +1,88 @@ +import UIKit + +extension EntryWidgetStyle { + mutating func apply( + configuration: RemoteConfiguration.EntryWidget?, + assetsBuilder: RemoteConfiguration.AssetsBuilder + ) { + applyBackground(configuration: configuration?.background) + applyDivider(configuration: configuration?.mediaTypeItems?.dividerColor) + applyErrorTitle( + configuration: configuration?.errorTitle, + assetsBuilder: assetsBuilder + ) + applyErrorMessage( + configuration: configuration?.errorMessage, + assetsBuilder: assetsBuilder + ) + errorButton.apply( + configuration: configuration?.errorButton, + assetsBuilder: assetsBuilder + ) + mediaTypeItem.apply( + configuration: configuration?.mediaTypeItems?.mediaTypeItem, + assetsBuilder: assetsBuilder + ) + } +} + +private extension EntryWidgetStyle { + mutating func applyBackground(configuration: RemoteConfiguration.Layer?) { + configuration?.color.unwrap { + switch $0.type { + case .fill: + $0.value + .map { UIColor(hex: $0) } + .first + .unwrap { backgroundColor = .fill(color: $0) } + case .gradient: + let colors = $0.value.convertToCgColors() + backgroundColor = .gradient(colors: colors) + } + } + configuration?.cornerRadius.unwrap { + cornerRadius = $0 + } + } + + mutating func applyErrorMessage( + configuration: RemoteConfiguration.Text?, + assetsBuilder: RemoteConfiguration.AssetsBuilder + ) { + configuration.unwrap { + UIFont.convertToFont( + uiFont: assetsBuilder.fontBuilder($0.font), + textStyle: errorMessageStyle + ).unwrap { errorMessageFont = $0 } + + $0.foreground?.value + .map { UIColor(hex: $0) } + .first + .unwrap { errorMessageColor = $0 } + } + } + + mutating func applyErrorTitle( + configuration: RemoteConfiguration.Text?, + assetsBuilder: RemoteConfiguration.AssetsBuilder + ) { + configuration.unwrap { + UIFont.convertToFont( + uiFont: assetsBuilder.fontBuilder($0.font), + textStyle: errorTitleStyle + ).unwrap { errorTitleFont = $0 } + + $0.foreground?.value + .map { UIColor(hex: $0) } + .first + .unwrap { errorTitleColor = $0 } + } + } + + mutating func applyDivider(configuration: RemoteConfiguration.Color?) { + configuration?.value + .map { UIColor(hex: $0) } + .first + .unwrap { dividerColor = $0 } + } +} diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.swift index 2a54fccfc..05fdf9e75 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetStyle.swift @@ -1,71 +1,80 @@ import UIKit public struct EntryWidgetStyle { - /// Engagement channel style. - public var channel: EntryWidgetChannelStyle + /// The style of a media type item. + public var mediaTypeItem: MediaTypeItemStyle - /// Background color of the view. - public var backgroundColor: UIColor + /// The background color of the view. + public var backgroundColor: ColorType - /// Style of 'powered by' view. + /// The corner radius of the view. + public var cornerRadius: CGFloat + + /// The style of 'powered by' view. public var poweredBy: PoweredByStyle - /// The color of the dragger - public var draggerColor: UIColor + /// The color of the divider. + public var dividerColor: UIColor - /// - Parameters: - /// - channel: Engagement channel style. - /// - backgroundColor: Background color of the view. - /// - poweredBy: Style of 'powered by' view. - /// - draggerColor: The color of the dragger - /// - public init( - channel: EntryWidgetChannelStyle, - backgroundColor: UIColor, - poweredBy: PoweredByStyle, - draggerColor: UIColor - ) { - self.channel = channel - self.backgroundColor = backgroundColor - self.poweredBy = poweredBy - self.draggerColor = draggerColor - } -} + /// The font for the title of the error message. + public var errorTitleFont: UIFont + + /// The text style for the title of the error message. + public var errorTitleStyle: UIFont.TextStyle -public struct EntryWidgetChannelStyle { - /// Font of the headline text. - public var headlineFont: UIFont + /// The color for the title of the error message. + public var errorTitleColor: UIColor - /// Color of the headline text. - public var headlineColor: UIColor + /// The error message string. + public var errorMessageFont: UIFont - /// Font of the subheadline text. - public var subheadlineFont: UIFont + /// The text style for the error message. + public var errorMessageStyle: UIFont.TextStyle - /// Color of the subheadline text. - public var subheadlineColor: UIColor + /// The color for the error message. + public var errorMessageColor: UIColor - /// Color of the icon. - public var iconColor: UIColor + /// The style of the error button. + public var errorButton: ActionButtonStyle /// - Parameters: - /// - headlineFont: Font of the headline text. - /// - headlineColor: Color of the headline text. - /// - subheadlineFont: Font of the subheadline text. - /// - subheadlineColor: Color of the subheadline text. - /// - iconColor: Color of the icon. - /// + /// - mediaTypeItem: The style of a media type item. + /// - backgroundColor: The background color of the view. + /// - cornerRadius: The corner radius of the view. + /// - poweredBy: The style of the 'powered by' view. + /// - dividerColor: The color of the divider. + /// - errorTitleFont: The font for the title of the error message. + /// - errorTitleStyle: The text style for the title of the error message. + /// - errorTitleColor: The color for the title of the error message. + /// - errorMessage: The error message string. + /// - errorMessageStyle: The text style for the error message. + /// - errorMessageColor: The color for the error message. + /// - errorButton: The style of the error button. public init( - headlineFont: UIFont, - headlineColor: UIColor, - subheadlineFont: UIFont, - subheadlineColor: UIColor, - iconColor: UIColor + mediaTypeItem: MediaTypeItemStyle, + backgroundColor: ColorType, + cornerRadius: CGFloat, + poweredBy: PoweredByStyle, + dividerColor: UIColor, + errorTitleFont: UIFont, + errorTitleStyle: UIFont.TextStyle, + errorTitleColor: UIColor, + errorMessageFont: UIFont, + errorMessageStyle: UIFont.TextStyle, + errorMessageColor: UIColor, + errorButton: ActionButtonStyle ) { - self.headlineFont = headlineFont - self.headlineColor = headlineColor - self.subheadlineFont = subheadlineFont - self.subheadlineColor = subheadlineColor - self.iconColor = iconColor + self.mediaTypeItem = mediaTypeItem + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + self.poweredBy = poweredBy + self.dividerColor = dividerColor + self.errorTitleFont = errorTitleFont + self.errorTitleStyle = errorTitleStyle + self.errorTitleColor = errorTitleColor + self.errorMessageFont = errorMessageFont + self.errorMessageStyle = errorMessageStyle + self.errorMessageColor = errorMessageColor + self.errorButton = errorButton } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift index 0ca7e6a3b..2b2cd2126 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetView.swift @@ -4,6 +4,55 @@ struct EntryWidgetView: View { @StateObject var model: Model var body: some View { + switch model.viewState { + case .error: + errorView() + case .loading: + loadingView() + case .mediaTypes: + mediaTypesView() + case .offline: + offilineView() + } + } +} + +// MARK: - View States +private extension EntryWidgetView { + @ViewBuilder + func errorView() -> some View { + VStack(spacing: 0) { + if model.showHeader { + headerView() + } + VStack(spacing: 16) { + // Will be swapped for localized string in MOB-3607 + Text("Oops! Contacts Couldn't Be Loaded") + .setFont(model.style.errorTitleFont) + .setColor(model.style.errorTitleColor) + // Will be swapped for localized string in MOB-3607 + Text("We couldn't load the contacts at this time. This may be due to a temporary syncing issue or network problem.") + .setFont(model.style.errorMessageFont) + .setColor(model.style.errorTitleColor) + errorButton() + } + .maxHeight() + if model.showPoweredBy { + poweredByView() + } + } + .maxSize() + .padding(.horizontal) + } + + @ViewBuilder + func loadingView() -> some View { + ProgressView() + .progressViewStyle(.circular) + } + + @ViewBuilder + func mediaTypesView() -> some View { VStack(spacing: 0) { if model.showHeader { headerView() @@ -16,8 +65,34 @@ struct EntryWidgetView: View { .maxSize() .padding(.horizontal) } + + @ViewBuilder + func offilineView() -> some View { + VStack(spacing: 0) { + if model.showHeader { + headerView() + } + VStack(spacing: 16) { + // Will be swapped for localized string in MOB-3607 + Text("Support team is currently offline") + .setFont(model.style.errorTitleFont) + .setColor(model.style.errorTitleColor) + // Will be swapped for localized string in MOB-3607 + Text(" We are here to assist you during our business hours: Monday to Friday 9:00 AM - 5:00 PM") + .setFont(model.style.errorMessageFont) + .setColor(model.style.errorTitleColor) + } + .maxHeight() + if model.showPoweredBy { + poweredByView() + } + } + .maxSize() + .padding(.horizontal) + } } +// MARK: - View Components private extension EntryWidgetView { @ViewBuilder func channelsView() -> some View { @@ -26,6 +101,7 @@ private extension EntryWidgetView { channelCell(channel: model.channels[index]) Divider() .height(model.sizeConstraints.dividerHeight) + .setColor(model.style.dividerColor) } } } @@ -42,7 +118,7 @@ private extension EntryWidgetView { func headerView() -> some View { VStack { Capsule(style: .continuous) - .fill(model.style.draggerColor.swiftUIColor()) + .fill(model.style.dividerColor.swiftUIColor()) .width(model.sizeConstraints.sheetHeaderDraggerWidth) .height(model.sizeConstraints.sheetHeaderDraggerHeight) } @@ -61,6 +137,7 @@ private extension EntryWidgetView { } .maxWidth(alignment: .leading) .height(model.sizeConstraints.singleCellHeight) + .applyColorTypeBackground(model.style.mediaTypeItem.backgroundColor) .contentShape(.rect) .onTapGesture { model.selectChannel(channel) @@ -74,20 +151,48 @@ private extension EntryWidgetView { .fit() .width(model.sizeConstraints.singleCellIconSize) .height(model.sizeConstraints.singleCellIconSize) - .setColor(model.style.channel.iconColor.swiftUIColor()) + .applyColorTypeForeground(model.style.mediaTypeItem.iconColor) } @ViewBuilder func headlineText(_ label: String) -> some View { Text(label) - .font(.convert(model.style.channel.headlineFont)) - .setColor(model.style.channel.headlineColor.swiftUIColor()) + .setFont(model.style.mediaTypeItem.titleFont) + .setColor(model.style.mediaTypeItem.titleColor) } @ViewBuilder func subheadlineText(_ label: String) -> some View { Text(label) - .font(.convert(model.style.channel.subheadlineFont)) - .setColor(model.style.channel.subheadlineColor.swiftUIColor()) + .setFont(model.style.mediaTypeItem.messageFont) + .setColor(model.style.mediaTypeItem.messageColor) + } + + @ViewBuilder + func errorButton() -> some View { + Text(model.style.errorButton.title) + .setFont(model.style.errorButton.titleFont) + .setColor(model.style.errorButton.titleColor) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .clipShape(.rect( + cornerRadius: model.style.errorButton.cornerRaidus ?? 0, + style: .continuous + )) + .overlay( + RoundedRectangle(cornerRadius: model.style.errorButton.cornerRaidus ?? 0) + .stroke( + model.style.errorButton.borderColor?.swiftUIColor() ?? .clear, + lineWidth: model.style.errorButton.borderWidth ?? 0 + ) + ) + .shadow( + color: model.style.errorButton.shadowColor?.swiftUIColor() ?? .clear, + radius: model.style.errorButton.shadowRadius ?? 0, + x: model.style.errorButton.shadowOffset?.width ?? 0, + y: model.style.errorButton.shadowOffset?.height ?? 0 + ) + .contentShape(.rect) + .onTapGesture(perform: model.onTryAgainTapped) } } diff --git a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift index 2c791a7f7..80d2d21c2 100644 --- a/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift +++ b/GliaWidgets/Sources/EntryWidget/EntryWidgetViewModel.swift @@ -2,6 +2,7 @@ import SwiftUI extension EntryWidgetView { class Model: ObservableObject { + @Published var viewState: ViewState let theme: Theme let channelSelected: (EntryWidget.Channel) -> Void let channels: [EntryWidget.Channel] @@ -32,6 +33,7 @@ extension EntryWidgetView { self.showHeader = showHeader self.channels = channels self.channelSelected = channelSelected + self.viewState = .offline } } } @@ -40,4 +42,18 @@ extension EntryWidgetView.Model { func selectChannel(_ channel: EntryWidget.Channel) { channelSelected(channel) } + + func onTryAgainTapped() { + // The logic will be added together with EngagementLauncher integration + print("Try again button tapped") + } +} + +extension EntryWidgetView.Model { + enum ViewState { + case error + case loading + case mediaTypes + case offline + } } diff --git a/GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.RemoteConfig.swift b/GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.RemoteConfig.swift new file mode 100644 index 000000000..960f1534c --- /dev/null +++ b/GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.RemoteConfig.swift @@ -0,0 +1,54 @@ +import UIKit + +extension EntryWidgetStyle.MediaTypeItemStyle { + mutating func apply( + configuration: RemoteConfiguration.MediaTypeItem?, + assetsBuilder: RemoteConfiguration.AssetsBuilder + ) { + configuration?.background?.color.unwrap { + switch $0.type { + case .fill: + $0.value + .map { UIColor(hex: $0) } + .first + .unwrap { backgroundColor = .fill(color: $0) } + case .gradient: + let colors = $0.value.convertToCgColors() + backgroundColor = .gradient(colors: colors) + } + } + + configuration?.iconColor.unwrap { + switch $0.type { + case .fill: + $0.value + .map { UIColor(hex: $0) } + .first + .unwrap { backgroundColor = .fill(color: $0) } + case .gradient: + let colors = $0.value.convertToCgColors() + backgroundColor = .gradient(colors: colors) + } + } + + configuration?.title?.foreground?.value + .map { UIColor(hex: $0) } + .first + .unwrap { titleColor = $0 } + + UIFont.convertToFont( + uiFont: assetsBuilder.fontBuilder(configuration?.title?.font), + textStyle: messageTextStyle + ).unwrap { titleFont = $0 } + + configuration?.message?.foreground?.value + .map { UIColor(hex: $0) } + .first + .unwrap { messageColor = $0 } + + UIFont.convertToFont( + uiFont: assetsBuilder.fontBuilder(configuration?.message?.font), + textStyle: messageTextStyle + ).unwrap { messageFont = $0 } + } +} diff --git a/GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.swift b/GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.swift new file mode 100644 index 000000000..dfa377f86 --- /dev/null +++ b/GliaWidgets/Sources/EntryWidget/MediaTypeItemStyle.swift @@ -0,0 +1,59 @@ +import UIKit + +extension EntryWidgetStyle { + public struct MediaTypeItemStyle { + /// Font of the headline text. + public var titleFont: UIFont + + /// Color of the headline text. + public var titleColor: UIColor + + /// Text style of the message text. + public var titleTextStyle: UIFont.TextStyle + + /// Font of the subheadline (message) text. + public var messageFont: UIFont + + /// Color of the subheadline (message) text. + public var messageColor: UIColor + + /// Text style of the message text. + public var messageTextStyle: UIFont.TextStyle + + /// Color of the icon. + public var iconColor: ColorType + + /// Background color of the view. + public var backgroundColor: ColorType + + /// - Parameters: + /// - titleFont: Font of the headline text. + /// - titleColor: Color of the headline text. + /// - titleTextStyle: The style of the title text. + /// - messageFont: Font of the subheadline (message) text. + /// - messageColor: Color of the subheadline (message) text. + /// - messageTextStyle: The style of the title text. + /// - iconColor: Color of the icon. + /// - backgroundColor: Background color of the view. + /// + public init( + titleFont: UIFont, + titleColor: UIColor, + titleTextStyle: UIFont.TextStyle, + messageFont: UIFont, + messageColor: UIColor, + messageTextStyle: UIFont.TextStyle, + iconColor: ColorType, + backgroundColor: ColorType + ) { + self.titleFont = titleFont + self.titleColor = titleColor + self.titleTextStyle = titleTextStyle + self.messageFont = messageFont + self.messageColor = messageColor + self.messageTextStyle = messageTextStyle + self.iconColor = iconColor + self.backgroundColor = backgroundColor + } + } +} diff --git a/GliaWidgets/Sources/Extensions/CGColor+Extensions.swift b/GliaWidgets/Sources/Extensions/CGColor+Extensions.swift index dda549a54..ade0c91c5 100644 --- a/GliaWidgets/Sources/Extensions/CGColor+Extensions.swift +++ b/GliaWidgets/Sources/Extensions/CGColor+Extensions.swift @@ -1,7 +1,12 @@ import UIKit +import SwiftUI extension CGColor { static var clear: CGColor { UIColor.clear.cgColor } + + func swiftUIColor() -> SwiftUI.Color { + return SwiftUI.Color(self) + } } diff --git a/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration.swift b/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration.swift index a26c6765f..98591d059 100644 --- a/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration.swift +++ b/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration.swift @@ -12,6 +12,7 @@ public struct RemoteConfiguration: Codable { let secureConversationsConfirmationScreen: SecureConversationsConfirmationScreen? let snackBar: SnackBar? let webBrowserScreen: WebView? + let entryWidget: EntryWidget? } extension RemoteConfiguration { @@ -201,4 +202,24 @@ extension RemoteConfiguration { let tintColor: Color? let title: Text? } + + struct EntryWidget: Codable { + let background: Layer? + let mediaTypeItems: MediaTypeItems? + let errorTitle: Text? + let errorMessage: Text? + let errorButton: Button? + } + + struct MediaTypeItems: Codable { + let mediaTypeItem: MediaTypeItem? + let dividerColor: Color? + } + + struct MediaTypeItem: Codable { + let background: Layer? + let iconColor: Color? + let title: Text? + let message: Text? + } } diff --git a/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift b/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift index 3d63761c1..aea9ba14e 100644 --- a/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift +++ b/GliaWidgets/Sources/Theme/Theme.EntryWidget.swift @@ -2,15 +2,18 @@ import UIKit extension Theme { var entryWidgetStyle: EntryWidgetStyle { - let channel: EntryWidgetChannelStyle = .init( - headlineFont: font.bodyText, - headlineColor: color.baseDark, - subheadlineFont: font.caption, - subheadlineColor: color.baseNormal, - iconColor: color.primary + let mediaTypeItem: EntryWidgetStyle.MediaTypeItemStyle = .init( + titleFont: font.bodyText, + titleColor: color.baseDark, + titleTextStyle: .body, + messageFont: font.caption, + messageColor: color.baseNormal, + messageTextStyle: .caption1, + iconColor: .fill(color: color.primary), + backgroundColor: .fill(color: color.baseLight) ) - let backgroundColor = color.baseLight + let backgroundColor: ColorType = .fill(color: color.baseLight) let poweredBy = PoweredByStyle( text: Localization.General.powered, @@ -18,11 +21,27 @@ extension Theme { accessibility: .init(isFontScalingEnabled: true) ) + let errorButton: ActionButtonStyle = .init( + title: "Try again", // Will be swapped for localized string in MOB-3607 + titleFont: font.bodyText, + titleColor: color.primary, + backgroundColor: .fill(color: color.baseLight), + shadowColor: .clear + ) + let style: EntryWidgetStyle = .init( - channel: channel, + mediaTypeItem: mediaTypeItem, backgroundColor: backgroundColor, + cornerRadius: 24, poweredBy: poweredBy, - draggerColor: color.baseShade + dividerColor: color.baseNeutral, + errorTitleFont: font.header3, + errorTitleStyle: .body, + errorTitleColor: color.baseDark, + errorMessageFont: font.bodyText, + errorMessageStyle: .body, + errorMessageColor: color.baseNormal, + errorButton: errorButton ) return style diff --git a/GliaWidgets/Sources/Theme/Theme.swift b/GliaWidgets/Sources/Theme/Theme.swift index 6a71fcecc..70f89f0f2 100644 --- a/GliaWidgets/Sources/Theme/Theme.swift +++ b/GliaWidgets/Sources/Theme/Theme.swift @@ -89,6 +89,7 @@ public class Theme { self.showsPoweredBy = showsPoweredBy } + // swiftlint:disable function_body_length convenience init( uiConfig config: RemoteConfiguration, assetsBuilder: RemoteConfiguration.AssetsBuilder @@ -152,5 +153,10 @@ public class Theme { configuration: config.webBrowserScreen, assetsBuilder: assetsBuilder ) + entryWidget.apply( + configuration: config.entryWidget, + assetsBuilder: assetsBuilder + ) } + // swiftlint:enable function_body_length } diff --git a/GliaWidgets/SwiftUI/Extensions/View+Extensions.swift b/GliaWidgets/SwiftUI/Extensions/View+Extensions.swift index 94b281d73..650b924b4 100644 --- a/GliaWidgets/SwiftUI/Extensions/View+Extensions.swift +++ b/GliaWidgets/SwiftUI/Extensions/View+Extensions.swift @@ -50,4 +50,57 @@ extension SwiftUI.View { return self .foregroundColor(color) } + + func setColor(_ color: UIColor) -> some View { + return self + .foregroundColor(color.swiftUIColor()) + } + + func setFont(_ font: UIFont) -> some View { + return self + .font(.convert(font)) + } + + @ViewBuilder + func applyColorTypeForeground(_ colorType: ColorType) -> some View { + switch colorType { + case let .fill(color): + self.setColor(color) + case let .gradient(colors): + let convertedColors = colors.map { $0.swiftUIColor() } + if #available(iOS 15, *) { + self.foregroundStyle(.linearGradient( + colors: convertedColors, + startPoint: .top, + endPoint: .bottom + )) + } else { + let gradient = LinearGradient( + gradient: Gradient(colors: convertedColors), + startPoint: .top, + endPoint: .bottom + ) + self + .overlay(gradient) + .mask(self) + } + } + } + + @ViewBuilder + func applyColorTypeBackground(_ colorType: ColorType) -> some View { + switch colorType { + case let .fill(color): + self.background(color.swiftUIColor()) + case let .gradient(colors): + let convertedColors = colors.map { $0.swiftUIColor() } + self.background( + LinearGradient( + colors: convertedColors, + startPoint: .top, + endPoint: .bottom + ) + ) + } + } } diff --git a/GliaWidgetsTests/Sources/Glia/GliaTests.swift b/GliaWidgetsTests/Sources/Glia/GliaTests.swift index 0f1a2f713..96eb25cb4 100644 --- a/GliaWidgetsTests/Sources/Glia/GliaTests.swift +++ b/GliaWidgetsTests/Sources/Glia/GliaTests.swift @@ -697,7 +697,8 @@ final class GliaTests: XCTestCase { secureConversationsWelcomeScreen: nil, secureConversationsConfirmationScreen: nil, snackBar: nil, - webBrowserScreen: nil + webBrowserScreen: nil, + entryWidget: nil ) let theme = Theme(colorStyle: .custom(themeColor))