From c2cd3da83e30fdf622895a45fbf5562004c693fd Mon Sep 17 00:00:00 2001 From: mike-dydx <149746839+mike-dydx@users.noreply.github.com> Date: Fri, 9 Aug 2024 12:07:09 -0400 Subject: [PATCH] CLI-638: prediction markets UI (#222) * add feature flag * support filtering to prediction markets * add banner * comment * add feature flag to debug menu --------- Co-authored-by: Mike --- .../TabGroup/TabItemViewModel.swift | 41 ++++++++-- .../Theme/ThemeViewModifiers.swift | 4 +- .../_Utils/dydxFeatureFlag.swift | 3 + .../dydxPresenters/_Features/features.json | 20 +++++ .../dydxMarketAssetListViewPresenter.swift | 19 ++++- .../_v4/Markets/dydxMarketsViewBuilder.swift | 11 +++ .../dydxViews.xcodeproj/project.pbxproj | 4 + .../dydxMarketsBannerViewModel.swift | 75 +++++++++++++++++++ .../_v4/Markets/dydxMarketsView.swift | 27 +++++-- 9 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketsBannerViewModel.swift diff --git a/PlatformUI/PlatformUI/Components/TabGroup/TabItemViewModel.swift b/PlatformUI/PlatformUI/Components/TabGroup/TabItemViewModel.swift index 454a2be50..734eca4d9 100644 --- a/PlatformUI/PlatformUI/Components/TabGroup/TabItemViewModel.swift +++ b/PlatformUI/PlatformUI/Components/TabGroup/TabItemViewModel.swift @@ -15,7 +15,20 @@ public class TabItemViewModel: PlatformViewModel, Equatable { } public enum TabItemContent: Equatable { + public struct PillConfig { + var text: String + var textColor: ThemeColor.SemanticColor + var backgroundColor: ThemeColor.SemanticColor + + public init(text: String, textColor: ThemeColor.SemanticColor, backgroundColor: ThemeColor.SemanticColor) { + self.text = text + self.textColor = textColor + self.backgroundColor = backgroundColor + } + } + case text(String, EdgeInsets = EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8)) + case textWithPillAccessory(text: String, pillConfig: PillConfig) case icon(UIImage) case bar(PlatformViewModel) @@ -51,23 +64,41 @@ public class TabItemViewModel: PlatformViewModel, Equatable { let styleKey = self.isSelected ? "pill_tab_group_selected_item" : "pill_tab_group_unselected_item" let templateColor: ThemeColor.SemanticColor = self.isSelected ? .textPrimary: .textTertiary + let textFontSize = ThemeFont.FontSize.small let borderWidth: CGFloat = 1 switch value { case .text(let value, let edgeInsets): return Text(value) - .frame(maxHeight: .infinity) - .themeFont(fontSize: .small) + .themeFont(fontSize: textFontSize) .padding(edgeInsets) .themeStyle(styleKey: styleKey, parentStyle: style) .borderAndClip(style: .capsule, borderColor: .layer6, lineWidth: borderWidth) .wrappedInAnyView() + case .textWithPillAccessory(let text, let pillConfig): + return HStack(alignment: .center, spacing: 4) { + Text(text) + .themeFont(fontSize: textFontSize) + .themeColor(foreground: .textSecondary) + Text(pillConfig.text) + .themeFont(fontSize: .smaller) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .themeColor(foreground: pillConfig.textColor) + .themeColor(background: pillConfig.backgroundColor) + .clipShape(.rect(cornerRadius: 6)) + } + .padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8)) + .themeStyle(styleKey: styleKey, parentStyle: style) + .borderAndClip(style: .capsule, borderColor: .layer6, lineWidth: borderWidth) + .wrappedInAnyView() case .icon(let image): + let height = ThemeSettings.shared.themeConfig.themeFont.uiFont(of: .base, fontSize: textFontSize)?.lineHeight ?? 14 return PlatformIconViewModel(type: .uiImage(image: image), - size: CGSize(width: 18, height: 18), + size: CGSize(width: height, height: height), templateColor: templateColor) .createView(parentStyle: parentStyle) - .padding([.bottom, .top], 8) - .padding([.leading, .trailing], 12) + .padding(.vertical, 6) + .padding(.horizontal, 8) .themeStyle(styleKey: styleKey, parentStyle: style) .borderAndClip(style: .capsule, borderColor: .layer6, lineWidth: borderWidth) .wrappedInAnyView() diff --git a/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift b/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift index 698f233ba..91d93b0e4 100644 --- a/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift +++ b/PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift @@ -320,7 +320,7 @@ private struct BorderAndClipModifier: ViewModifier { case .cornerRadius(let cornerRadius): content - .clipShape(RoundedRectangle(cornerSize: .init(width: cornerRadius, height: cornerRadius))) + .clipShape(.rect(cornerRadius: cornerRadius)) .overlay(RoundedRectangle(cornerRadius: cornerRadius) .strokeBorder(borderColor.color, lineWidth: lineWidth)) @@ -343,7 +343,7 @@ private struct BorderModifier: ViewModifier { func body(content: Content) -> some View { content - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .clipShape(.rect(cornerRadius: cornerRadius)) .overlay( RoundedRectangle(cornerRadius: cornerRadius) .stroke(borderColor ?? .clear, lineWidth: borderWidth) diff --git a/dydx/dydxFormatter/dydxFormatter/_Utils/dydxFeatureFlag.swift b/dydx/dydxFormatter/dydxFormatter/_Utils/dydxFeatureFlag.swift index 4b0e033cb..b34a26a6a 100644 --- a/dydx/dydxFormatter/dydxFormatter/_Utils/dydxFeatureFlag.swift +++ b/dydx/dydxFormatter/dydxFormatter/_Utils/dydxFeatureFlag.swift @@ -14,6 +14,7 @@ public enum dydxBoolFeatureFlag: String, CaseIterable { case enable_app_rating case shouldUseSkip = "ff_skip_migration" case isVaultEnabled = "ff_vault_enabled" + case showPredictionMarketsUI = "ff_show_prediction_markets_ui" var defaultValue: Bool { switch self { @@ -25,6 +26,8 @@ public enum dydxBoolFeatureFlag: String, CaseIterable { return true case .isVaultEnabled: return false + case .showPredictionMarketsUI: + return false } } diff --git a/dydx/dydxPresenters/dydxPresenters/_Features/features.json b/dydx/dydxPresenters/dydxPresenters/_Features/features.json index de7efd951..98aa53f30 100644 --- a/dydx/dydxPresenters/dydxPresenters/_Features/features.json +++ b/dydx/dydxPresenters/dydxPresenters/_Features/features.json @@ -72,6 +72,26 @@ } ] } + }, + { + "title":{ + "text":"Prediction Marktes" + }, + "field":{ + "field":"ff_show_prediction_markets_ui", + "optional":true, + "type" : "bool", + "options" : [ + { + "text": "yes", + "value" : 1 + }, + { + "text": "no", + "value" : 0 + } + ] + } } ] } diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxMarketAssetListViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxMarketAssetListViewPresenter.swift index 4ba23dd57..0c51c10d2 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxMarketAssetListViewPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxMarketAssetListViewPresenter.swift @@ -14,6 +14,7 @@ import PlatformUI import Abacus import dydxStateManager import Combine +import dydxFormatter // MARK: AssetList @@ -217,7 +218,7 @@ struct SortAction { struct FilterAction { static var actions: [FilterAction] { - [ + var actions = [ FilterAction(type: .all, content: .text(DataLocalizer.localize(path: "APP.GENERAL.ALL")), action: { _, _ in @@ -242,6 +243,21 @@ struct FilterAction { assetMap[market.assetId]?.tags?.contains("Defi") ?? false }) ] + if dydxBoolFeatureFlag.showPredictionMarketsUI.isEnabled { + let predictionMarketText = DataLocalizer.localize(path: "APP.GENERAL.PREDICTION_MARKET") + let newPillConfig = TabItemViewModel.TabItemContent.PillConfig(text: DataLocalizer.localize(path: "APP.GENERAL.NEW"), + textColor: .colorPurple, + backgroundColor: .colorFadedPurple) + let content = TabItemViewModel.TabItemContent.textWithPillAccessory(text: predictionMarketText, + pillConfig: newPillConfig) + let predictionMarketsAction = FilterAction(type: .predictionMarkets, + content: content, + action: { market, assetMap in + assetMap[market.assetId]?.tags?.contains("Prediction Market") ?? false + }) + actions.insert(predictionMarketsAction, at: 2) + } + return actions } let type: MarketFiltering @@ -264,6 +280,7 @@ enum MarketSorting { enum MarketFiltering { case all case favorited + case predictionMarkets case layer1 case layer2 case defi diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift index b0677bd27..af6d168f8 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift @@ -47,6 +47,17 @@ private class dydxMarketsViewPresenter: HostedViewPresenter Void) + + static var previewValue: dydxMarketsBannerViewModel = { + let vm = dydxMarketsBannerViewModel(navigationAction: {}) + return vm + }() + + public init(navigationAction: @escaping (() -> Void)) { + self.navigationAction = navigationAction + } + + public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { + PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in + guard let self = self else { return AnyView(PlatformView.nilView) } + return dydxMarketsBannerView(viewModel: self) + .wrappedInAnyView() + } + } +} + +private struct dydxMarketsBannerView: View { + var viewModel: dydxMarketsBannerViewModel + + var textStack: some View { + HStack(alignment: .top, spacing: 6) { + Text("🇺🇸") + .themeFont(fontType: .base, fontSize: .medium) + VStack(alignment: .leading, spacing: 4) { + Text(localizerPathKey: "APP.PREDICTION_MARKET.LEVERAGE_TRADE_US_ELECTION_SHORT") + .themeFont(fontType: .base, fontSize: .medium) + .themeColor(foreground: .textPrimary) + Text(localizerPathKey: "APP.PREDICTION_MARKET.WITH_PREDICTION_MARKETS") + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: .textSecondary) + } + } + } + + var navButton: some View { + Button(action: viewModel.navigationAction) { + Text("→") + .themeFont(fontType: .base, fontSize: .large) + .themeColor(foreground: .textSecondary) + .centerAligned() + } + .frame(width: 32, height: 32) + .themeColor(background: .layer6) + .borderAndClip(style: .circle, borderColor: .layer6) + } + + var body: some View { + HStack(spacing: 0) { + textStack + Spacer(minLength: 8) + navButton + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .themeColor(background: .layer1) + .clipShape(.rect(cornerRadius: 16)) + } +} diff --git a/dydx/dydxViews/dydxViews/_v4/Markets/dydxMarketsView.swift b/dydx/dydxViews/dydxViews/_v4/Markets/dydxMarketsView.swift index ecfbda4b9..d8671e107 100644 --- a/dydx/dydxViews/dydxViews/_v4/Markets/dydxMarketsView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Markets/dydxMarketsView.swift @@ -19,6 +19,7 @@ public class dydxMarketsViewModel: PlatformViewModel { private static let topId = UUID().uuidString @Published public var header = dydxMarketsHeaderViewModel() + @Published public var banner: dydxMarketsBannerViewModel? @Published public var summary = dydxMarketSummaryViewModel() @Published public var filter = dydxMarketAssetFilterViewModel() @Published public var sort = dydxMarketAssetSortViewModel() @@ -30,6 +31,7 @@ public class dydxMarketsViewModel: PlatformViewModel { public static var previewValue: dydxMarketsViewModel = { let vm = dydxMarketsViewModel() vm.header = .previewValue + vm.banner = .previewValue vm.summary = .previewValue vm.filter = .previewValue vm.sort = .previewValue @@ -39,24 +41,33 @@ public class dydxMarketsViewModel: PlatformViewModel { public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in + guard let self = self else { return AnyView(PlatformView.nilView) } let view = VStack(spacing: 0) { - self?.header.createView(parentStyle: style) + self.header.createView(parentStyle: style) .padding(.horizontal, 16) ScrollViewReader { proxy in ScrollView(showsIndicators: false) { LazyVStack(pinnedViews: [.sectionHeaders]) { - self?.summary.createView(parentStyle: style) + + if let banner = self.banner { + banner + .createView() + .padding(.horizontal, 16) + .padding(.top, 12) + } + + self.summary.createView(parentStyle: style) .themeColor(background: .layer2) .zIndex(.greatestFiniteMagnitude) .padding(.horizontal, 16) let header = VStack(spacing: 0) { - self?.filter.createView(parentStyle: style) + self.filter.createView(parentStyle: style) .padding(.horizontal, 16) Spacer() - self?.sort.createView(parentStyle: style) + self.sort.createView(parentStyle: style) .padding(.leading, 16) Spacer(minLength: 12) } @@ -66,23 +77,23 @@ public class dydxMarketsViewModel: PlatformViewModel { Section(header: header) { VStack(spacing: 12) { - self?.assetList? + self.assetList? .createView(parentStyle: style) Spacer(minLength: 64) } .padding(.horizontal, 16) .animation(.default) } - .onChange(of: self?.scrollAction) { newValue in + .onChange(of: self.scrollAction) { newValue in if newValue == .toTop { withAnimation { proxy.scrollTo(Self.topId) } } - self?.scrollAction = .none + self.scrollAction = .none } .onAppear { - self?.scrollAction = .none + self.scrollAction = .none } // account for scrolling behind tab bar