From 38f23e7632af68e4fac44716d4ef2aa66e11304a Mon Sep 17 00:00:00 2001 From: mike-dydx Date: Wed, 12 Jun 2024 18:04:32 -0500 Subject: [PATCH] stubbed unopened isolated positions UI --- .../dydxPortfolioPositionsViewPresenter.swift | 7 +- .../dydxViews.xcodeproj/project.pbxproj | 4 + .../Sections/dydxPortfolioPositionsView.swift | 113 +++++++++++++---- ...openedIsolatedPositionsItemViewModel.swift | 116 ++++++++++++++++++ 4 files changed, 210 insertions(+), 30 deletions(-) create mode 100644 dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift index c54fcb50b..29b4e9df7 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift @@ -37,9 +37,9 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter dydxPortfolioPositionItemViewModel? { diff --git a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj index 17ffee214..ab7b82f7e 100644 --- a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj +++ b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj @@ -195,6 +195,7 @@ 27C027452AFD734800E92CCB /* dydxSettingsHelpRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C027442AFD734800E92CCB /* dydxSettingsHelpRowView.swift */; }; 27C6E4C92BC8C30E00ED892A /* dydxCustomLimitPriceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C6E4BC2BC8C30E00ED892A /* dydxCustomLimitPriceViewModel.swift */; }; 27CDA3D42BBF1AD700FEAFFE /* dydxMultipleOrdersExistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CDA3D32BBF1AD700FEAFFE /* dydxMultipleOrdersExistViewModel.swift */; }; + 27E072D22C1A095C0034B963 /* dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E072D12C1A095C0034B963 /* dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift */; }; 27ED340C2AD47CB100C159F5 /* dydxBannerErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED340B2AD47CB100C159F5 /* dydxBannerErrorAlert.swift */; }; 27ED365C2AD735A800C159F5 /* dydxSecurityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED365B2AD735A800C159F5 /* dydxSecurityView.swift */; }; 27F624112BBD9FEB00AB6D1A /* dydxPriceInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F624042BBD9FEB00AB6D1A /* dydxPriceInputViewModel.swift */; }; @@ -565,6 +566,7 @@ 27C027442AFD734800E92CCB /* dydxSettingsHelpRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSettingsHelpRowView.swift; sourceTree = ""; }; 27C6E4BC2BC8C30E00ED892A /* dydxCustomLimitPriceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxCustomLimitPriceViewModel.swift; sourceTree = ""; }; 27CDA3D32BBF1AD700FEAFFE /* dydxMultipleOrdersExistViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMultipleOrdersExistViewModel.swift; sourceTree = ""; }; + 27E072D12C1A095C0034B963 /* dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift; sourceTree = ""; }; 27ED340B2AD47CB100C159F5 /* dydxBannerErrorAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxBannerErrorAlert.swift; sourceTree = ""; }; 27ED365B2AD735A800C159F5 /* dydxSecurityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSecurityView.swift; sourceTree = ""; }; 27F624042BBD9FEB00AB6D1A /* dydxPriceInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxPriceInputViewModel.swift; sourceTree = ""; }; @@ -1141,6 +1143,7 @@ 02678FA329666BD800EE346B /* dydxPortfolioPositionsView.swift */, 02678FA529666BE600EE346B /* dydxPortfolioOrdersView.swift */, 024F488D2965C91D00E40247 /* dydxPortfolioChartView.swift */, + 27E072D12C1A095C0034B963 /* dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift */, ); path = Sections; sourceTree = ""; @@ -2011,6 +2014,7 @@ 029CBE7728F608F400259C1D /* dydxMarketTradesView.swift in Sources */, 0238FC46296DA53F002E1C1A /* dydxOrderDetailsView.swift in Sources */, 2769090E2AAFD8030075B2D6 /* TransferInstanceViewModel.swift in Sources */, + 27E072D22C1A095C0034B963 /* dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift in Sources */, 024FEB642ACB75E10087A55E /* dydxFeesStuctureView.swift in Sources */, 277E918B2B27762F005CCBCB /* dydxRewardsLaunchIncentivesView.swift in Sources */, 0268BBF92A8BE08C00D0C59B /* dydxTransferOutView.swift in Sources */, diff --git a/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPositionsView.swift b/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPositionsView.swift index d22bf32b5..b1b3a4655 100644 --- a/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPositionsView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPositionsView.swift @@ -319,41 +319,47 @@ public class dydxPortfolioPositionItemViewModel: PlatformViewModel { } } -public class dydxPortfolioPositionsViewModel: PlatformListViewModel { +public class dydxPortfolioPositionsViewModel: PlatformViewModel { // TODO: remove once isolated markets is supported and force released @Published public var shouldDisplayIsolatedPositionsWarning: Bool = false - @Published public var placeholderText: String? - - public override var placeholder: PlatformViewModel? { - let vm = PlaceholderViewModel() - vm.text = placeholderText - return vm + @Published public var emptyText: String? + @Published public var positionItems: [dydxPortfolioPositionItemViewModel] { + didSet { + contentChanged?() + } + } + @Published public var unopenedIsolatedPositionItems: [dydxPortfolioUnopenedIsolatedPositionsItemViewModel] { + didSet { + contentChanged?() + } } - public override init(items: [PlatformViewModel] = [], - intraItemSeparator: Bool = false, - firstListItemTopSeparator: Bool = false, - lastListItemBottomSeparator: Bool = false, - contentChanged: (() -> Void)? = nil) { - super.init(items: items, - intraItemSeparator: intraItemSeparator, - firstListItemTopSeparator: firstListItemTopSeparator, - lastListItemBottomSeparator: lastListItemBottomSeparator, - contentChanged: contentChanged) - self.width = UIScreen.main.bounds.width - 32 + public var contentChanged: (() -> Void)? + + init( + positionItems: [dydxPortfolioPositionItemViewModel] = [], + unopenedIsolatedPositionItems: [dydxPortfolioUnopenedIsolatedPositionsItemViewModel] = [], + emptyText: String? = nil + ) { + self.positionItems = positionItems + self.unopenedIsolatedPositionItems = unopenedIsolatedPositionItems + self.emptyText = emptyText } public static var previewValue: dydxPortfolioPositionsViewModel { - let vm = dydxPortfolioPositionsViewModel {} - vm.items = [ - dydxPortfolioPositionItemViewModel.previewValue, - dydxPortfolioPositionItemViewModel.previewValue - ] - return vm + dydxPortfolioPositionsViewModel( + positionItems: [ + .previewValue, + .previewValue + ], + unopenedIsolatedPositionItems: [ + .previewValue + ], + emptyText: "empty") } - public override var header: PlatformViewModel? { - guard dydxBoolFeatureFlag.enable_isolated_margins.isEnabled == false, !items.isEmpty else { return nil } + public var positionsHeader: PlatformViewModel? { + guard dydxBoolFeatureFlag.enable_isolated_margins.isEnabled == false, !positionItems.isEmpty else { return nil } return HStack { Text(DataLocalizer.localize(path: "APP.GENERAL.DETAILS")) Spacer() @@ -371,7 +377,7 @@ public class dydxPortfolioPositionsViewModel: PlatformListViewModel { .wrappedViewModel } - public override var footer: PlatformViewModel? { + public var positionsFooter: PlatformViewModel? { guard shouldDisplayIsolatedPositionsWarning && !dydxBoolFeatureFlag.enable_isolated_margins.isEnabled else { return nil } return Text(localizerPathKey: "APP.GENERAL.ISOLATED_POSITIONS_COMING_SOON") .multilineTextAlignment(.center) @@ -382,6 +388,59 @@ public class dydxPortfolioPositionsViewModel: PlatformListViewModel { .padding(.bottom, 16) .wrappedViewModel } + + public var unopenedIsolatedPositionsHeader: PlatformViewModel? { + guard dydxBoolFeatureFlag.enable_isolated_margins.isEnabled == true, !unopenedIsolatedPositionItems.isEmpty else { return nil } + return HStack(spacing: 8) { + Text(localizerPathKey: "APP.TRADE.UNOPENED_ISOLATED_POSITIONS") + .themeFont(fontSize: .larger) + .themeColor(foreground: .textPrimary) + .fixedSize() + Text("\(unopenedIsolatedPositionItems.count)") + .frame(width: 28, height: 28) + .borderAndClip(style: .circle, borderColor: .borderDefault) + Spacer() + } + .padding(.horizontal, 16) + .themeFont(fontSize: .small) + .themeColor(foreground: .textTertiary) + .wrappedViewModel + } + + 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) } + + if let emptyText = self.emptyText, positionItems.isEmpty, unopenedIsolatedPositionItems.isEmpty { + return AnyView( + PlaceholderViewModel(text: emptyText) + .createView(parentStyle: style) + ) + } + + let items = self.positionItems.map { $0.createView(parentStyle: style) } + let unopenedItems = self.unopenedIsolatedPositionItems.map { $0.createView(parentStyle: style) } + + return AnyView( + ScrollView { + LazyVStack { + self.positionsHeader?.createView(parentStyle: style) + + ForEach(items.indices, id: \.self) { index in + items[index] + } + + self.positionsFooter?.createView(parentStyle: style) + self.unopenedIsolatedPositionsHeader?.createView(parentStyle: style) + + ForEach(unopenedItems.indices, id: \.self) { index in + unopenedItems[index] + } + } + } + ) + } + } } #if DEBUG diff --git a/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift b/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift new file mode 100644 index 000000000..483df291b --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift @@ -0,0 +1,116 @@ +// +// dydxPortfolioUnopenedIsolatedPositionsItemViewModel.swift +// dydxUI +// +// Created by Michael Maguire on 6/12/24. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import PlatformUI +import SwiftUI + +public class dydxPortfolioUnopenedIsolatedPositionsItemViewModel: PlatformViewModel { + @Published public var marketLogoUrl: URL? + @Published public var marketName: String + @Published public var margin: String + @Published public var viewOrdersAction: (() -> Void) + @Published public var cancelOrdersAction: (() -> Void) + + public init(marketLogoUrl: URL? = nil, + marketName: String, + margin: String, + viewOrdersAction: @escaping () -> Void, + cancelOrdersAction: @escaping () -> Void) { + self.marketLogoUrl = marketLogoUrl + self.marketName = marketName + self.margin = margin + self.viewOrdersAction = viewOrdersAction + self.cancelOrdersAction = cancelOrdersAction + } + + public static var previewValue: dydxPortfolioUnopenedIsolatedPositionsItemViewModel = { + .init(marketLogoUrl: URL(string: "https://v4.testnet.dydx.exchange/currencies/eth.png"), + marketName: "ETH-USD", + margin: "$1000.00", + viewOrdersAction: {}, + cancelOrdersAction: {} + ) + }() + + private var topContent: some View { + VStack(spacing: 8) { + HStack(spacing: 8) { + PlatformIconViewModel(type: .url(url: marketLogoUrl), + clip: .defaultCircle, + size: CGSize(width: 20, height: 20)) + .createView() + Text(marketName) + .themeFont(fontSize: .small) + .themeColor(foreground: .textSecondary) + Spacer() + } + HStack(spacing: 0) { + Text(localizerPathKey: "APP.GENERAL.MARGIN") + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) + Spacer() + Text(margin) + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textSecondary) + } + } + .padding(.vertical, 10) + } + + private var divider: some View { + Divider() + .overlay(ThemeColor.SemanticColor.borderDefault.color) + } + + private var bottomContent: some View { + let viewOrders = Text(localizerPathKey: "APP.CLOSE_POSITIONS_CONFIRMATION_TOAST.VIEW_ORDERS") + .themeFont(fontSize: .smaller) + .themeColor(foreground: .colorPurple) + .padding(.vertical, 8) + let cancel = Text(localizerPathKey: "APP.GENERAL.CANCEL") + .themeFont(fontSize: .smaller) + .themeColor(foreground: .colorRed) + .padding(.vertical, 8) + return HStack(spacing: 0) { + Button(action: viewOrdersAction, label: { viewOrders }) + Spacer() + Button(action: cancelOrdersAction, label: { cancel }) + } + } + + public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { + PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] _ in + guard let self = self else { return AnyView(PlatformView.nilView) } + + return VStack(spacing: 0) { + self.topContent + self.divider + self.bottomContent + } + .padding(.horizontal, 12) + .themeColor(background: .layer3) + .clipShape(.rect(cornerRadius: 10)) + .wrappedInAnyView() + } + } +} + +#if DEBUG +struct dydxPortfolioUnopenedIsolatedPositionsItemView_Previews: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + Group { + dydxPortfolioUnopenedIsolatedPositionsItemViewModel.previewValue + .createView() + .environmentObject(themeSettings) + .previewLayout(.sizeThatFits) + } + } +} +#endif