Skip to content

Commit

Permalink
unopened isolated positions pt2 - cancel flow (#195)
Browse files Browse the repository at this point in the history
* fix adjust margin % horizontal sizings

* remove unused files

* dismiss on success, update button state on submission

* multi-line receipt line item title

* stubbed unopened isolated positions UI

* wire up abacus data for unopened positions

* put back fixed size, clean up

* remove max action

* update button state after validation

* clean up

* ui tweaks

* Update Package.resolved

* stubbed unopened isolated positions UI

* wire up abacus data for unopened positions

* update button state after validation

* change routing to orders tab, populate unopened isolated position

* fix `filterByMarketId` not triggering a list update

* show unopened isolated position in market info view

* stub UI for cancel orders

* cancel orders
  • Loading branch information
mike-dydx committed Aug 21, 2024
1 parent ecaf23f commit 171d219
Show file tree
Hide file tree
Showing 17 changed files with 496 additions and 30 deletions.
2 changes: 1 addition & 1 deletion PlatformUI/PlatformUI/DesignSystem/Theme/SampleStyle.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"_textColor": "text_tertiary"
},
"button-destructive": {
"_layerColor": "layer_4",
"_layerColor": "layer_3",
"_textColor": "color_red"
}
}
15 changes: 15 additions & 0 deletions PlatformUI/PlatformUI/PlatformViewModel+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import UIKit
import SwiftUI

public extension PlatformViewModel {
var safeAreaInsets: UIEdgeInsets? {
Expand All @@ -21,3 +22,17 @@ public extension PlatformViewModel {
.last
}
}

public extension View {
var safeAreaInsets: UIEdgeInsets? {
keyWindow?.safeAreaInsets
}

var keyWindow: UIWindow? {
UIApplication
.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.last
}
}
20 changes: 19 additions & 1 deletion dydx/dydx.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "3cd7346cace16cf660f9c22302c5ca6770335c67902f3fd155011356a9d43929",
"originHash" : "975d00e29efb8d2ca017c5e61df90418ac01f7d7143e85a3f9ddb4eb982154e4",
"pins" : [
{
"identity" : "bigint",
Expand Down Expand Up @@ -37,6 +37,15 @@
"version" : "2.0.2"
}
},
{
"identity" : "keyboardobserving",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nickffox/KeyboardObserving",
"state" : {
"branch" : "master",
"revision" : "48134b5460435cc9d048223ad7560ee2e40f3d4a"
}
},
{
"identity" : "percy-xcui-swift",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -190,6 +199,15 @@
"version" : "9.0.9"
}
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
"state" : {
"revision" : "121c146fe591b1320238d054ae35c81ffa45f45a",
"version" : "0.12.0"
}
},
{
"identity" : "wallet-mobile-sdk",
"kind" : "remoteSourceControl",
Expand Down
12 changes: 12 additions & 0 deletions dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
27B957ED2B97C07400EF9304 /* dydxShareActionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B957EC2B97C07400EF9304 /* dydxShareActionBuilder.swift */; };
27C027532AFD761300E92CCB /* dydxSettingsHelpRowViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C027522AFD761300E92CCB /* dydxSettingsHelpRowViewPresenter.swift */; };
27DB2EA32AC1E7B20047BC39 /* dydxTradeRestrictedViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DB2EA22AC1E7B20047BC39 /* dydxTradeRestrictedViewPresenter.swift */; };
27E0735C2C20D2470034B963 /* dydxCancelPendingIsolatedOrdersViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E0735B2C20D2470034B963 /* dydxCancelPendingIsolatedOrdersViewBuilder.swift */; };
314BBDE9F332ECA910BC414E /* Pods_iOS_dydxPresenters.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1551C00FFF41C29CFC5BD94 /* Pods_iOS_dydxPresenters.framework */; };
6448800B2AA248340068DD87 /* dydxAlertsWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64487FFE2AA248340068DD87 /* dydxAlertsWorker.swift */; };
645299EF2AE86FB1000810E6 /* dydxUpdateViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645299E22AE86FB1000810E6 /* dydxUpdateViewPresenter.swift */; };
Expand Down Expand Up @@ -542,6 +543,7 @@
27B957EC2B97C07400EF9304 /* dydxShareActionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxShareActionBuilder.swift; sourceTree = "<group>"; };
27C027522AFD761300E92CCB /* dydxSettingsHelpRowViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSettingsHelpRowViewPresenter.swift; sourceTree = "<group>"; };
27DB2EA22AC1E7B20047BC39 /* dydxTradeRestrictedViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTradeRestrictedViewPresenter.swift; sourceTree = "<group>"; };
27E0735B2C20D2470034B963 /* dydxCancelPendingIsolatedOrdersViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxCancelPendingIsolatedOrdersViewBuilder.swift; sourceTree = "<group>"; };
64487FFE2AA248340068DD87 /* dydxAlertsWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxAlertsWorker.swift; sourceTree = "<group>"; };
645299E22AE86FB1000810E6 /* dydxUpdateViewPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxUpdateViewPresenter.swift; sourceTree = "<group>"; };
64529A4B2AE8705E000810E6 /* dydxUpdateWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxUpdateWorker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -653,6 +655,7 @@
0230376A28C11B0300412B72 /* _v4 */ = {
isa = PBXGroup;
children = (
27E0735A2C20D1F80034B963 /* CancelOrders */,
278A4D912B8FA5C1003898EB /* Rating */,
021B68A32AD9B86600C5C3BF /* Auth */,
0243A75529BE568600A083FE /* Actions */,
Expand Down Expand Up @@ -1452,6 +1455,14 @@
path = Error;
sourceTree = "<group>";
};
27E0735A2C20D1F80034B963 /* CancelOrders */ = {
isa = PBXGroup;
children = (
27E0735B2C20D2470034B963 /* dydxCancelPendingIsolatedOrdersViewBuilder.swift */,
);
path = CancelOrders;
sourceTree = "<group>";
};
51E65BFD078A26FDF0980B06 /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2027,6 +2038,7 @@
276908FF2AAFB22F0075B2D6 /* dydxPortfolioTransfersViewPresenter.swift in Sources */,
6496DBC9295CBBDD00174CE7 /* dydxV4TabBarBuilder.swift in Sources */,
02F7010029EA165F004DEB5E /* dydxTransferReceiptViewPresenter.swift in Sources */,
27E0735C2C20D2470034B963 /* dydxCancelPendingIsolatedOrdersViewBuilder.swift in Sources */,
277E90152B1EA0E3005CCBCB /* dydxTradingRewardsViewPresenter.swift in Sources */,
0216441128F36FBE00C7093E /* CandleDataPoint.swift in Sources */,
6453A7D2299C563F0041A0C4 /* dydxClosePositionInputViewBuilder.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@
"destination":"dydxPresenters.dydxOrderDetailsViewBuilder",
"presentation":"half"
},

"/orders/:id":{
"destination":"dydxPresenters.dydxOrderDetailsViewBuilder",
"presentation":"half"
},
"/portfolio/cancel_pending_position/:market":{
"destination":"dydxPresenters.dydxCancelPendingIsolatedOrdersViewBuilder",
"presentation":"half"
},
"/profile/user":{
"destination":"ProfileInput.storyboard",
"presentation":"half"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// dydxCancelPendingIsolatedOrdersViewBuilder.swift
// dydxUI
//
// Created by Michael Maguire on 6/17/24.
// Copyright © 2024 dYdX Trading Inc. All rights reserved.
//
import Utilities
import dydxViews
import PlatformParticles
import RoutingKit
import ParticlesKit
import PlatformUI
import dydxStateManager
import Combine

public class dydxCancelPendingIsolatedOrdersViewBuilder: NSObject, ObjectBuilderProtocol {
public func build<T>() -> T? {
let presenter = dydxCancelPendingIsolatedOrdersViewBuilderPresenter()
let view = presenter.viewModel?.createView() ?? PlatformViewModel().createView()
return dydxCancelPendingIsolatedOrdersViewBuilderController(presenter: presenter, view: view, configuration: .default) as? T
}
}

private class dydxCancelPendingIsolatedOrdersViewBuilderController: HostingViewController<PlatformView, dydxCancelPendingIsolatedOrdersViewModel> {
override public func arrive(to request: RoutingRequest?, animated: Bool) -> Bool {
if let marketId = request?.params?["market"] as? String,
request?.path == "/portfolio/cancel_pending_position",
let presenter = presenter as? dydxCancelPendingIsolatedOrdersViewBuilderPresenterProtocol {
presenter.marketId = marketId
return true
}
return false
}
}

private protocol dydxCancelPendingIsolatedOrdersViewBuilderPresenterProtocol: HostedViewPresenterProtocol {
var viewModel: dydxCancelPendingIsolatedOrdersViewModel? { get }
var marketId: String? { get set }
}

private class dydxCancelPendingIsolatedOrdersViewBuilderPresenter: HostedViewPresenter<dydxCancelPendingIsolatedOrdersViewModel>, dydxCancelPendingIsolatedOrdersViewBuilderPresenterProtocol {
fileprivate var marketId: String?

override init() {
super.init()

self.viewModel = .init(marketLogoUrl: nil, marketName: "", marketId: "", orderCount: 0, cancelAction: {})
}

override func start() {
super.start()

Publishers.CombineLatest(
AbacusStateManager.shared.state.configsAndAssetMap,
AbacusStateManager.shared.state.selectedSubaccountOrders
)
.receive(on: RunLoop.main)
.sink { [weak self] configsAndAssetMap, orders in
guard let self = self,
let marketId = self.marketId,
let asset = configsAndAssetMap[marketId]?.asset
else { return }
let pendingOrders = orders.filter { $0.marketId == marketId && $0.status == .open }
self.viewModel?.marketLogoUrl = URL(string: asset.resources?.imageUrl ?? "")
self.viewModel?.marketName = asset.name ?? "--"
self.viewModel?.marketId = asset.id
self.viewModel?.orderCount = pendingOrders.count
self.viewModel?.failureCount = self.viewModel?.failureCount
self.viewModel?.cancelAction = { [weak self] in
self?.tryCancelOrders(orderIds: orders.map(\.id))
}
}
.store(in: &subscriptions)
}

private func tryCancelOrders(orderIds: [String]) {
viewModel?.state = viewModel?.failureCount == nil ? .submitting : .resubmitting
Task { [weak self] in
guard let self = self else { return }

// Create an array to hold the results of the cancellations
var results: [Result<AbacusStateManager.SubmissionStatus, Error>] = []

// Use a TaskGroup to kick off multiple calls and wait for all to finish
await withTaskGroup(of: Result<AbacusStateManager.SubmissionStatus, Error>.self) { group in
for orderId in orderIds {
group.addTask {
do {
let status = try await AbacusStateManager.shared.cancelOrder(orderId: orderId)
return .success(status)
} catch {
return .failure(error)
}
}
}

// Collect the results of all tasks
for await result in group {
results.append(result)
}
}

// Count the number of failed cancellations
let failureCount = results.filter { result in
if case .failure = result {
return true
}
return false
}.count

await updateState(failureCount: failureCount)
}
}

@MainActor
private func updateState(failureCount: Int) {
self.viewModel?.failureCount = failureCount

if failureCount > 0 {
self.viewModel?.state = .failed
} else {
Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true, completion: nil)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ protocol dydxMarketPositionViewPresenterProtocol: HostedViewPresenterProtocol {

class dydxMarketPositionViewPresenter: HostedViewPresenter<dydxMarketPositionViewModel>, dydxMarketPositionViewPresenterProtocol {
@Published var position: SubaccountPosition?
@Published var pendingPosition: SubaccountPendingPosition?

init(viewModel: dydxMarketPositionViewModel?) {
super.init()
Expand All @@ -38,6 +39,22 @@ class dydxMarketPositionViewPresenter: HostedViewPresenter<dydxMarketPositionVie
override func start() {
super.start()

Publishers
.CombineLatest3($pendingPosition,
AbacusStateManager.shared.state.marketMap,
AbacusStateManager.shared.state.assetMap)
.sink { [weak self] pendingPosition, marketMap, assetMap in
if let pendingPosition {
self?.viewModel?.pendingPosition = dydxPortfolioPositionsViewPresenter.createPendingPositionsViewModelItem(
pendingPosition: pendingPosition,
marketMap: marketMap,
assetMap: assetMap)
} else {
self?.viewModel?.pendingPosition = nil
}
}
.store(in: &subscriptions)

Publishers
.CombineLatest4($position.compactMap { $0 }.removeDuplicates(),
AbacusStateManager.shared.state.selectedSubaccountTriggerOrders,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,15 @@ private class dydxMarketInfoViewPresenter: HostedViewPresenter<dydxMarketInfoVie
.store(in: &subscriptions)

Publishers
.CombineLatest(AbacusStateManager.shared.state.selectedSubaccountPositions,
.CombineLatest3(AbacusStateManager.shared.state.selectedSubaccountPositions,
AbacusStateManager.shared.state.selectedSubaccountPendingPositions,
$marketId
.compactMap { $0 }
.removeDuplicates())
.sink { [weak self] subaccountPositions, marketId in
let position = subaccountPositions.first { (subaccountPosition: SubaccountPosition) in
subaccountPosition.id == marketId
}
self?.updatePositionSection(position: position)
.sink { [weak self] subaccountPositions, subaccountPendingPositions, marketId in
let position = subaccountPositions.first { $0.id == marketId }
let pendingPosition = subaccountPendingPositions.first { $0.marketId == marketId }
self?.updatePositionSection(position: position, pendingPosition: pendingPosition)
}
.store(in: &subscriptions)
}
Expand All @@ -170,13 +170,22 @@ private class dydxMarketInfoViewPresenter: HostedViewPresenter<dydxMarketInfoVie
// AbacusStateManager.shared.setMarket(market: nil)
}

private func updatePositionSection(position: SubaccountPosition?) {
if let position = position, position.side.current != PositionSide.none, let viewModel = viewModel {
private func updatePositionSection(position: SubaccountPosition?, pendingPosition: SubaccountPendingPosition?) {
if let position, position.side.current != PositionSide.none, let viewModel = viewModel {
viewModel.showPositionSection = true
fillsPresenter.filterByMarketId = position.id
fundingPresenter.filterByMarketId = position.id
ordersPresenter.filterByMarketId = position.id
positionPresenter.position = position
positionPresenter.pendingPosition = nil
resetPresentersForVisibilityChange()
} else if let pendingPosition, pendingPosition.orderCount > 0, let viewModel = viewModel {
viewModel.showPositionSection = true
fillsPresenter.filterByMarketId = pendingPosition.marketId
fundingPresenter.filterByMarketId = pendingPosition.marketId
ordersPresenter.filterByMarketId = pendingPosition.marketId
positionPresenter.position = nil
positionPresenter.pendingPosition = pendingPosition
resetPresentersForVisibilityChange()
} else {
viewModel?.showPositionSection = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,17 @@ class dydxPortfolioOrdersViewPresenter: HostedViewPresenter<dydxPortfolioOrdersV
.store(in: &subscriptions)

Publishers
.CombineLatest(AbacusStateManager.shared.state.selectedSubaccountOrders,
AbacusStateManager.shared.state.configsAndAssetMap)
.sink { [weak self] positions, configsAndAssetMap in
self?.updateOrders(orders: positions, configsAndAssetMap: configsAndAssetMap)
.CombineLatest3(
$filterByMarketId.removeDuplicates(),
AbacusStateManager.shared.state.selectedSubaccountOrders,
AbacusStateManager.shared.state.configsAndAssetMap)
.sink { [weak self] filterByMarketId, positions, configsAndAssetMap in
self?.updateOrders(filterByMarketId: filterByMarketId, orders: positions, configsAndAssetMap: configsAndAssetMap)
}
.store(in: &subscriptions)
}

private func updateOrders(orders: [SubaccountOrder], configsAndAssetMap: [String: MarketConfigsAndAsset]) {
private func updateOrders(filterByMarketId: String?, orders: [SubaccountOrder], configsAndAssetMap: [String: MarketConfigsAndAsset]) {
let items: [dydxPortfolioOrderItemViewModel] = orders.compactMap { order -> dydxPortfolioOrderItemViewModel? in
if let filterByMarketId = filterByMarketId, filterByMarketId != order.marketId {
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter<dydxPortfolioPosi
) -> dydxPortfolioPendingPositionsItemViewModel? {

guard let market = marketMap[pendingPosition.marketId],
let configs = market.configs,
let asset = assetMap[pendingPosition.assetId],
let margin = pendingPosition.equity?.current?.doubleValue,
margin != 0,
Expand All @@ -101,14 +100,17 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter<dydxPortfolioPosi
}

let viewOrdersAction: () -> Void = {
Router.shared?.navigate(to: RoutingRequest(path: "/market",
params: ["market": market.id,
"currentSection": "positions"]),
let routingRequest = RoutingRequest(
path: "/market",
params: ["market": market.id,
"currentSection": "orders"])
Router.shared?.navigate(to: routingRequest,
animated: true,
completion: nil)
}

let cancelOrdersAction: () -> Void = {
Router.shared?.navigate(to: RoutingRequest(path: "/trade/markets", params: ["market": market.id]), animated: true, completion: nil)
Router.shared?.navigate(to: RoutingRequest(path: "/portfolio/cancel_pending_position/\(market.id)"), animated: true, completion: nil)
}

return dydxPortfolioPendingPositionsItemViewModel(marketLogoUrl: URL(string: asset.resources?.imageUrl ?? ""),
Expand Down
Loading

0 comments on commit 171d219

Please sign in to comment.