From dea986f37ddd24a4489cbb5b1e9ddfdb9e8c3c79 Mon Sep 17 00:00:00 2001 From: mike-dydx <149746839+mike-dydx@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:52:15 -0500 Subject: [PATCH] unopened isolated positions pt1 (#191) * hide/show target leverage for margin mode selection * fix adjust margin % horizontal sizings * remove unused files * dismiss on success, update button state on submission * multi-line receipt line item title * use localized error * stubbed unopened isolated positions UI * wire up abacus data for unopened positions * put back fixed size, clean up * hide keyboard when margin direction changes * remove max action * add local validation * update button state after validation * remove alternate validation * clean up * ui tweaks --- .../dydxMarketPositionViewPresenter.swift | 2 +- .../dydxPortfolioPositionsViewPresenter.swift | 75 ++++++++-- .../dydxAdjustMarginInputViewBuilder.swift | 79 ++++++++--- .../AbacusState+Combine.swift | 11 ++ .../dydxStateManager/Models+Ext.swift | 2 +- .../dydxViews.xcodeproj/project.pbxproj | 8 +- ...rtfolioPendingPositionsItemViewModel.swift | 134 ++++++++++++++++++ .../Sections/dydxPortfolioPositionsView.swift | 128 +++++++++++++---- .../dydxReceiptChangeItemView.swift | 2 +- .../dydxAdjustMarginAmountView.swift | 82 ----------- .../dydxAdjustMarginDirectionView.swift | 3 +- .../Margin/dydxAdjustMarginInputView.swift | 9 +- 12 files changed, 383 insertions(+), 152 deletions(-) create mode 100644 dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPendingPositionsItemViewModel.swift delete mode 100644 dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginAmountView.swift diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift index 4644bf1c2..a18781b1f 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift @@ -50,7 +50,7 @@ class dydxMarketPositionViewPresenter: HostedViewPresenter, dydxPortfolioPositionsViewPresenterProtocol { - private var cache = [String: dydxPortfolioPositionItemViewModel]() + private var positionsCache = [String: dydxPortfolioPositionItemViewModel]() + private var pendingPositionsCache = [String: dydxPortfolioPendingPositionsItemViewModel]() init(viewModel: dydxPortfolioPositionsViewModel?) { super.init() @@ -37,40 +38,94 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter dydxPortfolioPositionItemViewModel? in - let item = Self.createViewModelItem(position: position, marketMap: marketMap, assetMap: assetMap, cache: cache) - cache[position.assetId] = item + let item = Self.createPositionViewModelItem(position: position, + marketMap: marketMap, + assetMap: assetMap, + positionsCache: positionsCache) + positionsCache[position.assetId] = item return item } - self.viewModel?.items = items + self.viewModel?.positionItems = items } - static func createViewModelItem(position: SubaccountPosition, marketMap: [String: PerpetualMarket], assetMap: [String: Asset], cache: [String: dydxPortfolioPositionItemViewModel]? = nil) -> dydxPortfolioPositionItemViewModel? { + private func updatePendingPositions(pendingPositions: [SubaccountPendingPosition], marketMap: [String: PerpetualMarket], assetMap: [String: Asset]) { + let items: [dydxPortfolioPendingPositionsItemViewModel] = pendingPositions.compactMap { pendingPosition -> dydxPortfolioPendingPositionsItemViewModel? in + let item = Self.createPendingPositionsViewModelItem(pendingPosition: pendingPosition, + marketMap: marketMap, + assetMap: assetMap, + pendingPositionsCache: pendingPositionsCache) + pendingPositionsCache[pendingPosition.assetId] = item + return item + } + + self.viewModel?.pendingPositionItems = items + } + + static func createPendingPositionsViewModelItem( + pendingPosition: SubaccountPendingPosition, + marketMap: [String: PerpetualMarket], + assetMap: [String: Asset], + pendingPositionsCache: [String: dydxPortfolioPendingPositionsItemViewModel]? = nil + ) -> dydxPortfolioPendingPositionsItemViewModel? { + + guard let market = marketMap[pendingPosition.marketId], + let configs = market.configs, + let asset = assetMap[pendingPosition.assetId], + let margin = pendingPosition.equity?.current?.doubleValue, + margin != 0, + let marginFormatted = dydxFormatter.shared.dollar(number: margin, digits: 2) + else { + return nil + } + + let viewOrdersAction: () -> Void = { + Router.shared?.navigate(to: RoutingRequest(path: "/market", + params: ["market": market.id, + "currentSection": "positions"]), + animated: true, + completion: nil) + } + let cancelOrdersAction: () -> Void = { + Router.shared?.navigate(to: RoutingRequest(path: "/trade/markets", params: ["market": market.id]), animated: true, completion: nil) + } + + return dydxPortfolioPendingPositionsItemViewModel(marketLogoUrl: URL(string: asset.resources?.imageUrl ?? ""), + marketName: asset.name!, + margin: marginFormatted, + orderCount: pendingPosition.orderCount, + viewOrdersAction: viewOrdersAction, + cancelOrdersAction: cancelOrdersAction) + } + + static func createPositionViewModelItem(position: SubaccountPosition, marketMap: [String: PerpetualMarket], assetMap: [String: Asset], positionsCache: [String: dydxPortfolioPositionItemViewModel]? = nil) -> dydxPortfolioPositionItemViewModel? { guard let market = marketMap[position.id], let configs = market.configs, let asset = assetMap[position.assetId], (position.size.current?.doubleValue ?? 0) != 0 else { return nil } - let item = cache?[position.assetId] ?? dydxPortfolioPositionItemViewModel() + let item = positionsCache?[position.assetId] ?? dydxPortfolioPositionItemViewModel() let positionSize = abs(position.size.current?.doubleValue ?? 0) item.size = dydxFormatter.shared.localFormatted(number: positionSize, digits: configs.displayStepSizeDecimals?.intValue ?? 1) diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift index 63dc6c234..a953810e5 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxAdjustMarginInputViewBuilder.swift @@ -80,16 +80,13 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter String? { + guard let amount = parser.asNumber(input.amount)?.doubleValue else { return nil } + switch input.type { + case IsolatedMarginAdjustmentType.add: + if let crossFreeCollateral = input.summary?.crossFreeCollateral?.doubleValue, amount >= crossFreeCollateral { + return "ERRORS.TRANSFER_MODAL.TRANSFER_MORE_THAN_FREE" + } + if let crossMarginUsageUpdated = input.summary?.crossMarginUsageUpdated?.doubleValue, crossMarginUsageUpdated > 1 { + return "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE" + } + case IsolatedMarginAdjustmentType.remove: + if let freeCollateral = input.summary?.crossFreeCollateral?.doubleValue, amount >= freeCollateral { + return "ERRORS.TRANSFER_MODAL.TRANSFER_MORE_THAN_FREE" + } + if let positionMarginUpdated = input.summary?.positionMarginUpdated?.doubleValue, positionMarginUpdated < 0 { + return "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE" + } + if let effectiveInitialMarginFraction = market.configs?.effectiveInitialMarginFraction?.doubleValue, effectiveInitialMarginFraction > 0 { + let marketMaxLeverage = 1 / effectiveInitialMarginFraction + if let positionLeverageUpdated = input.summary?.positionLeverageUpdated?.doubleValue, positionLeverageUpdated > marketMaxLeverage { + return "ERRORS.TRADE_BOX_TITLE.INVALID_NEW_POSITION_LEVERAGE" + } + } + default: + break + } + + return nil + } + + private func clearPostValues() { + for receipt in [viewModel?.amountReceipt, viewModel?.buttonReceipt] { + for item in receipt?.receiptChangeItems ?? [] { + item.value.after = nil + } + } + } + + private func updatePrePostValues(input: AdjustIsolatedMarginInput, market: PerpetualMarket) { var crossReceiptItems = [dydxReceiptChangeItemView]() var positionReceiptItems = [dydxReceiptChangeItemView]() + if let errorStringKey = validate(input: input, market: market) { + clearPostValues() + viewModel?.inlineAlert = InlineAlertViewModel(InlineAlertViewModel.Config( + title: nil, + body: DataLocalizer.localize(path: errorStringKey), + level: .error)) + ctaButtonPresenter.viewModel?.ctaButtonState = .disabled() + return + } else { + ctaButtonPresenter.viewModel?.ctaButtonState = .enabled() + viewModel?.inlineAlert = nil + } + let crossFreeCollateral: AmountTextModel = .init(amount: input.summary?.crossFreeCollateral, unit: .dollar) let crossFreeCollateralUpdated: AmountTextModel = .init(amount: input.summary?.crossFreeCollateralUpdated, unit: .dollar) let crossFreeCollateralChange: AmountChangeModel = .init( @@ -225,14 +270,6 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter 0 { - self.ctaButtonPresenter.viewModel?.ctaButtonState = .enabled() - } else { - self.ctaButtonPresenter.viewModel?.ctaButtonState = .disabled() - } - } - private func updateFields(input: AdjustIsolatedMarginInput) { viewModel?.amount?.value = dydxFormatter.shared.raw(number: parser.asNumber(input.amount), digits: 2) } diff --git a/dydx/dydxStateManager/dydxStateManager/AbacusState+Combine.swift b/dydx/dydxStateManager/dydxStateManager/AbacusState+Combine.swift index b5406a923..aadc83895 100644 --- a/dydx/dydxStateManager/dydxStateManager/AbacusState+Combine.swift +++ b/dydx/dydxStateManager/dydxStateManager/AbacusState+Combine.swift @@ -221,6 +221,17 @@ public final class AbacusState { .eraseToAnyPublisher() } + public var selectedSubaccountPendingPositions: AnyPublisher<[SubaccountPendingPosition], Never> { + selectedSubaccount + .compactMap { subaccount in + subaccount?.pendingPositions + } + .prepend([]) + .removeDuplicates() + .share() + .eraseToAnyPublisher() + } + public var selectedSubaccountOrders: AnyPublisher<[SubaccountOrder], Never> { selectedSubaccount .compactMap { subaccount in diff --git a/dydx/dydxStateManager/dydxStateManager/Models+Ext.swift b/dydx/dydxStateManager/dydxStateManager/Models+Ext.swift index 36efdbf18..a0791b60f 100644 --- a/dydx/dydxStateManager/dydxStateManager/Models+Ext.swift +++ b/dydx/dydxStateManager/dydxStateManager/Models+Ext.swift @@ -10,7 +10,7 @@ import Abacus import Utilities extension ParsingError: Error { - var localizedDescription: String? { + public var localizedMessage: String? { if let stringKey = stringKey { return DataLocalizer.localize(path: stringKey) } diff --git a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj index 17ffee214..19c0ed4e7 100644 --- a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj +++ b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj @@ -103,7 +103,6 @@ 0279DE6B2BEC471700F9ECF8 /* dydxAdjustMarginInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE6A2BEC471700F9ECF8 /* dydxAdjustMarginInputView.swift */; }; 0279DE872BED3F5400F9ECF8 /* dydxAdjustMarginDirectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE862BED3F5400F9ECF8 /* dydxAdjustMarginDirectionView.swift */; }; 0279DE892BED402C00F9ECF8 /* dydxAdjustMarginPercentageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE882BED402C00F9ECF8 /* dydxAdjustMarginPercentageView.swift */; }; - 0279DE8B2BED412F00F9ECF8 /* dydxAdjustMarginAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE8A2BED412F00F9ECF8 /* dydxAdjustMarginAmountView.swift */; }; 0279DE8F2BED532200F9ECF8 /* dydxAdjustMarginLiquidationPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE8E2BED532200F9ECF8 /* dydxAdjustMarginLiquidationPriceView.swift */; }; 0279DE912BED610400F9ECF8 /* dydxAdjustMarginReceiptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE902BED610400F9ECF8 /* dydxAdjustMarginReceiptViewModel.swift */; }; 0279DE932BED63D000F9ECF8 /* dydxAdjustMarginCtaButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE922BED63D000F9ECF8 /* dydxAdjustMarginCtaButtonView.swift */; }; @@ -195,6 +194,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 /* dydxPortfolioPendingPositionsItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E072D12C1A095C0034B963 /* dydxPortfolioPendingPositionsItemViewModel.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 */; }; @@ -474,7 +474,6 @@ 0279DE6A2BEC471700F9ECF8 /* dydxAdjustMarginInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAdjustMarginInputView.swift; sourceTree = ""; }; 0279DE862BED3F5400F9ECF8 /* dydxAdjustMarginDirectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAdjustMarginDirectionView.swift; sourceTree = ""; }; 0279DE882BED402C00F9ECF8 /* dydxAdjustMarginPercentageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAdjustMarginPercentageView.swift; sourceTree = ""; }; - 0279DE8A2BED412F00F9ECF8 /* dydxAdjustMarginAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAdjustMarginAmountView.swift; sourceTree = ""; }; 0279DE8E2BED532200F9ECF8 /* dydxAdjustMarginLiquidationPriceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAdjustMarginLiquidationPriceView.swift; sourceTree = ""; }; 0279DE902BED610400F9ECF8 /* dydxAdjustMarginReceiptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAdjustMarginReceiptViewModel.swift; sourceTree = ""; }; 0279DE922BED63D000F9ECF8 /* dydxAdjustMarginCtaButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAdjustMarginCtaButtonView.swift; sourceTree = ""; }; @@ -565,6 +564,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 /* dydxPortfolioPendingPositionsItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxPortfolioPendingPositionsItemViewModel.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 +1141,7 @@ 02678FA329666BD800EE346B /* dydxPortfolioPositionsView.swift */, 02678FA529666BE600EE346B /* dydxPortfolioOrdersView.swift */, 024F488D2965C91D00E40247 /* dydxPortfolioChartView.swift */, + 27E072D12C1A095C0034B963 /* dydxPortfolioPendingPositionsItemViewModel.swift */, ); path = Sections; sourceTree = ""; @@ -1186,7 +1187,6 @@ 0279DE442BEBE75100F9ECF8 /* dydxTargetLeverageCtaButtonView.swift */, 0279DE862BED3F5400F9ECF8 /* dydxAdjustMarginDirectionView.swift */, 0279DE882BED402C00F9ECF8 /* dydxAdjustMarginPercentageView.swift */, - 0279DE8A2BED412F00F9ECF8 /* dydxAdjustMarginAmountView.swift */, 0279DE8E2BED532200F9ECF8 /* dydxAdjustMarginLiquidationPriceView.swift */, 0279DE902BED610400F9ECF8 /* dydxAdjustMarginReceiptViewModel.swift */, 0279DE922BED63D000F9ECF8 /* dydxAdjustMarginCtaButtonView.swift */, @@ -2011,6 +2011,7 @@ 029CBE7728F608F400259C1D /* dydxMarketTradesView.swift in Sources */, 0238FC46296DA53F002E1C1A /* dydxOrderDetailsView.swift in Sources */, 2769090E2AAFD8030075B2D6 /* TransferInstanceViewModel.swift in Sources */, + 27E072D22C1A095C0034B963 /* dydxPortfolioPendingPositionsItemViewModel.swift in Sources */, 024FEB642ACB75E10087A55E /* dydxFeesStuctureView.swift in Sources */, 277E918B2B27762F005CCBCB /* dydxRewardsLaunchIncentivesView.swift in Sources */, 0268BBF92A8BE08C00D0C59B /* dydxTransferOutView.swift in Sources */, @@ -2039,7 +2040,6 @@ 02031F1A2AC3A6FE0069E00D /* dydxTradeSheetTipDraftView.swift in Sources */, 0238FEDF2972B5C9002E1C1A /* dydxTradeInputCtaButtonView.swift in Sources */, 2728CE1B2BBCD2AB004C9323 /* dydxGainLossInputViewModel.swift in Sources */, - 0279DE8B2BED412F00F9ECF8 /* dydxAdjustMarginAmountView.swift in Sources */, 02AEE36C28EF7CC8006842E8 /* dydxMarketInfoHeaderView.swift in Sources */, 024F488529657F1900E40247 /* dydxPortfolioView.swift in Sources */, 277E907D2B211553005CCBCB /* dydxRewardsHistoryView.swift in Sources */, diff --git a/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPendingPositionsItemViewModel.swift b/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPendingPositionsItemViewModel.swift new file mode 100644 index 000000000..97422b084 --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPendingPositionsItemViewModel.swift @@ -0,0 +1,134 @@ +// +// dydxPortfolioPendingPositionsItemViewModel.swift +// dydxUI +// +// Created by Michael Maguire on 6/12/24. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import PlatformUI +import SwiftUI +import Utilities + +public class dydxPortfolioPendingPositionsItemViewModel: PlatformViewModel { + @Published public var marketLogoUrl: URL? + @Published public var marketName: String + @Published public var margin: String + @Published public var orderCount: Int32 + @Published public var viewOrdersAction: (() -> Void) + @Published public var cancelOrdersAction: (() -> Void) + + public init(marketLogoUrl: URL?, + marketName: String, + margin: String, + orderCount: Int32, + viewOrdersAction: @escaping () -> Void, + cancelOrdersAction: @escaping () -> Void) { + self.marketLogoUrl = marketLogoUrl + self.marketName = marketName + self.margin = margin + self.orderCount = orderCount + self.viewOrdersAction = viewOrdersAction + self.cancelOrdersAction = cancelOrdersAction + } + + public static var previewValue: dydxPortfolioPendingPositionsItemViewModel = { + .init(marketLogoUrl: URL(string: "https://v4.testnet.dydx.exchange/currencies/eth.png"), + marketName: "ETH-USD", + margin: "$1000.00", + orderCount: 2, + 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 { + Rectangle() + .frame(height: 1) + .overlay(ThemeColor.SemanticColor.borderDefault.color) + } + + private var bottomContent: some View { + let viewOrdersStringKey: String + let viewOrdersStringParams: [String: String]? + if orderCount == 1 { + viewOrdersStringKey = "APP.GENERAL.VIEW_ORDER" + viewOrdersStringParams = nil + } else { + viewOrdersStringKey = "APP.GENERAL.VIEW_ORDERS_COUNT" + viewOrdersStringParams = ["NUM_ORDERS": "\(orderCount)"] + } + + let viewOrders = Text(localizerPathKey: viewOrdersStringKey, params: viewOrdersStringParams) + .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) } + + let horizontalPadding: CGFloat = 12 + return VStack(spacing: 0) { + self.topContent + self.divider + .padding(.horizontal, -horizontalPadding) + self.bottomContent + } + .padding(.horizontal, horizontalPadding) + .themeColor(background: .layer3) + .clipShape(.rect(cornerRadius: 10)) + .wrappedInAnyView() + } + } +} + +#if DEBUG +struct dydxPortfolioPendingPositionsItemView_Previews: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + Group { + dydxPortfolioPendingPositionsItemViewModel.previewValue + .createView() + .environmentObject(themeSettings) + .previewLayout(.sizeThatFits) + } + } +} +#endif diff --git a/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPositionsView.swift b/dydx/dydxViews/dydxViews/_v4/Portfolio/Components/Sections/dydxPortfolioPositionsView.swift index d22bf32b5..fb0ff5359 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 pendingPositionItems: [dydxPortfolioPendingPositionsItemViewModel] { + didSet { + contentChanged?() + } + } + + public var contentChanged: (() -> Void)? - 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 + init( + positionItems: [dydxPortfolioPositionItemViewModel] = [], + pendingPositionItems: [dydxPortfolioPendingPositionsItemViewModel] = [], + emptyText: String? = nil + ) { + self.positionItems = positionItems + self.pendingPositionItems = pendingPositionItems + self.emptyText = emptyText } public static var previewValue: dydxPortfolioPositionsViewModel { - let vm = dydxPortfolioPositionsViewModel {} - vm.items = [ - dydxPortfolioPositionItemViewModel.previewValue, - dydxPortfolioPositionItemViewModel.previewValue - ] - return vm + dydxPortfolioPositionsViewModel( + positionItems: [ + .previewValue, + .previewValue + ], + pendingPositionItems: [ + .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,74 @@ public class dydxPortfolioPositionsViewModel: PlatformListViewModel { .padding(.bottom, 16) .wrappedViewModel } + + public var pendingPositionsHeader: PlatformViewModel? { + guard dydxBoolFeatureFlag.enable_isolated_margins.isEnabled == true, !pendingPositionItems.isEmpty else { return nil } + return HStack(spacing: 8) { + Text(localizerPathKey: "APP.TRADE.UNOPENED_ISOLATED_POSITIONS") + .themeFont(fontSize: .larger) + .themeColor(foreground: .textPrimary) + .fixedSize() + Text("\(pendingPositionItems.count)") + .frame(width: 28, height: 28) + .themeColor(background: .layer3) + .themeColor(foreground: .textSecondary) + .borderAndClip(style: .circle, borderColor: .borderDefault) + Spacer() + } + .padding(.horizontal, 16) + .themeFont(fontSize: .small) + .themeColor(foreground: .textTertiary) + .wrappedViewModel + } + + private var openPositionsView: some View { + LazyVStack { + let items = self.positionItems.map { $0.createView() } + self.positionsHeader?.createView() + + ForEach(items.indices, id: \.self) { index in + items[index] + } + + self.positionsFooter?.createView() + } + } + + private var pendingPositionsView: AnyView? { + guard dydxBoolFeatureFlag.enable_isolated_margins.isEnabled else { return nil } + let unopenedItems = self.pendingPositionItems.map { $0.createView() } + return LazyVStack { + self.pendingPositionsHeader?.createView() + + ForEach(unopenedItems.indices, id: \.self) { index in + unopenedItems[index] + } + } + .wrappedInAnyView() + } + + 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, pendingPositionItems.isEmpty { + return AnyView( + PlaceholderViewModel(text: emptyText) + .createView(parentStyle: style) + ) + } + + return AnyView( + ScrollView { + VStack(spacing: 24) { + self.openPositionsView + self.pendingPositionsView + } + } + ) + } + } } #if DEBUG diff --git a/dydx/dydxViews/dydxViews/_v4/Receipt/Components/dydxReceiptChangeItemView.swift b/dydx/dydxViews/dydxViews/_v4/Receipt/Components/dydxReceiptChangeItemView.swift index 573007338..d5cd75581 100644 --- a/dydx/dydxViews/dydxViews/_v4/Receipt/Components/dydxReceiptChangeItemView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Receipt/Components/dydxReceiptChangeItemView.swift @@ -33,9 +33,9 @@ public class dydxReceiptChangeItemView: PlatformViewModel { Text(self.title) .themeFont(fontSize: .small) .themeColor(foreground: .textTertiary) + .lineLimit(2) Spacer() self.value.createView(parentStyle: parentStyle) - .fixedSize(horizontal: true, vertical: false) } ) } diff --git a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginAmountView.swift b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginAmountView.swift deleted file mode 100644 index 12f9a3a7f..000000000 --- a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginAmountView.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// dydxAdjustMarginAmountView.swift -// dydxUI -// -// Created by Rui Huang on 09/05/2024. -// Copyright © 2024 dYdX Trading Inc. All rights reserved. -// - -import SwiftUI -import PlatformUI -import Utilities - -public class dydxAdjustMarginAmountViewModel: PlatformTextInputViewModel { - @Published public var maxAction: (() -> Void)? - - public override var inputType: PlatformTextInputViewModel.InputType { .decimalDigits } - - public static var previewValue: dydxAdjustMarginAmountViewModel { - let vm = dydxAdjustMarginAmountViewModel() - vm.label = "Amount" - vm.placeHolder = "0.0" - return vm - } - - public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { - let superView = super.createView(parentStyle: parentStyle, styleKey: styleKey) - return PlatformView { style in - let view = HStack { - superView - - let buttonContent = Text(DataLocalizer.localize(path: "APP.GENERAL.MAX")) - .themeFont(fontSize: .medium) - .wrappedViewModel - - PlatformButtonViewModel(content: buttonContent, type: .pill, state: .secondary, action: { [weak self] in - - PlatformView.hideKeyboard() - self?.maxAction?() - - }) - .createView(parentStyle: style) - .padding(.trailing, 8) - } - .makeInput() - - return AnyView(view) - } - } - -} - -#if DEBUG -struct dydxAdjustMarginAmountView_Previews_Dark: PreviewProvider { - @StateObject static var themeSettings = ThemeSettings.shared - - static var previews: some View { - ThemeSettings.applyDarkTheme() - ThemeSettings.applyStyles() - return dydxAdjustMarginAmountViewModel.previewValue - .createView() - .themeColor(background: .layer0) - .environmentObject(themeSettings) - // .edgesIgnoringSafeArea(.bottom) - .previewLayout(.sizeThatFits) - } -} - -struct dydxAdjustMarginAmountView_Previews_Light: PreviewProvider { - @StateObject static var themeSettings = ThemeSettings.shared - - static var previews: some View { - ThemeSettings.applyLightTheme() - ThemeSettings.applyStyles() - return dydxAdjustMarginAmountViewModel.previewValue - .createView() - .themeColor(background: .layer0) - .environmentObject(themeSettings) - // .edgesIgnoringSafeArea(.bottom) - .previewLayout(.sizeThatFits) - } -} -#endif diff --git a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginDirectionView.swift b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginDirectionView.swift index 22829dee4..96391a746 100644 --- a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginDirectionView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxAdjustMarginDirectionView.swift @@ -65,7 +65,8 @@ public class dydxAdjustMarginDirectionViewModel: PlatformViewModel { selectedItems: selectedItems, currentSelection: self.marginDirection.index, onSelectionChanged: { index in - self.marginDirectionAction?(MarginDirection(index: index)) + PlatformView.hideKeyboard() + self.marginDirectionAction?(MarginDirection(index: index)) }) .createView(parentStyle: style) } diff --git a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxAdjustMarginInputView.swift b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxAdjustMarginInputView.swift index 1d272e5c1..76d456a45 100644 --- a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxAdjustMarginInputView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxAdjustMarginInputView.swift @@ -15,10 +15,10 @@ public class dydxAdjustMarginInputViewModel: PlatformViewModel { @Published public var sharedMarketViewModel: SharedMarketViewModel? = SharedMarketViewModel() @Published public var marginDirection: dydxAdjustMarginDirectionViewModel? = dydxAdjustMarginDirectionViewModel() @Published public var marginPercentage: dydxAdjustMarginPercentageViewModel? = dydxAdjustMarginPercentageViewModel() - @Published public var amount: dydxAdjustMarginAmountViewModel? = dydxAdjustMarginAmountViewModel() + @Published public var amount: PlatformTextInputViewModel? = PlatformTextInputViewModel(inputType: .decimalDigits) @Published public var amountReceipt: dydxAdjustMarginReceiptViewModel? = dydxAdjustMarginReceiptViewModel() @Published public var liquidationPrice: dydxAdjustMarginLiquidationPriceViewModel? = dydxAdjustMarginLiquidationPriceViewModel() - @Published public var submissionError: InlineAlertViewModel? + @Published public var inlineAlert: InlineAlertViewModel? @Published public var buttonReceipt: dydxAdjustMarginReceiptViewModel? = dydxAdjustMarginReceiptViewModel() @Published public var ctaButton: dydxAdjustMarginCtaButtonViewModel? = dydxAdjustMarginCtaButtonViewModel() @Published public var shouldDisplayCrossFirst: Bool = true @@ -33,7 +33,7 @@ public class dydxAdjustMarginInputViewModel: PlatformViewModel { let vm = dydxAdjustMarginInputViewModel() vm.marginPercentage = .previewValue vm.marginDirection = .previewValue - vm.amount = .previewValue + vm.amount = .init() vm.amountReceipt = .previewValue vm.liquidationPrice = .previewValue vm.buttonReceipt = .previewValue @@ -57,11 +57,12 @@ public class dydxAdjustMarginInputViewModel: PlatformViewModel { ZStack(alignment: .top) { self.amountReceipt?.createView(parentStyle: style) self.amount?.createView(parentStyle: style) + .makeInput() .frame(height: 64) } self.liquidationPrice?.createView(parentStyle: style) - self.submissionError?.createView(parentStyle: style) + self.inlineAlert?.createView(parentStyle: style) Spacer() }