Skip to content

Commit

Permalink
add in-line alert UI
Browse files Browse the repository at this point in the history
  • Loading branch information
mike-dydx committed Apr 10, 2024
1 parent 8623b2c commit 1c3c3c0
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 6 deletions.
4 changes: 4 additions & 0 deletions PlatformUI/PlatformUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
02F38BF22A9AAE3700969E06 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F38BF12A9AAE3700969E06 /* CircularProgressView.swift */; };
1C811E336064517E256D1290 /* Pods_iOS_PlatformUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6793D8074DF2E7FE4592138 /* Pods_iOS_PlatformUITests.framework */; };
27044F882BBB2ADF004C750D /* Text+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27044F872BBB2ADF004C750D /* Text+Ext.swift */; };
277E7AC62BBF3BE8009F95DE /* InlineAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277E7AC52BBF3BE8009F95DE /* InlineAlert.swift */; };
27E6A7322AB8D5F600026CB5 /* SwipeActionsViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E6A7312AB8D5F600026CB5 /* SwipeActionsViewModifier.swift */; };
6488BBDC296F6AEA0096502F /* TabItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6488BBDB296F6AEA0096502F /* TabItemViewModel.swift */; };
64A4DC5F29677BCB008D8E20 /* PlatformOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A4DC5E29677BCB008D8E20 /* PlatformOutput.swift */; };
Expand Down Expand Up @@ -134,6 +135,7 @@
02F16FE128B53A200085DC58 /* SampleStyleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleStyleLabel.swift; sourceTree = "<group>"; };
02F38BF12A9AAE3700969E06 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
27044F872BBB2ADF004C750D /* Text+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+Ext.swift"; sourceTree = "<group>"; };
277E7AC52BBF3BE8009F95DE /* InlineAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAlert.swift; sourceTree = "<group>"; };
27E6A7312AB8D5F600026CB5 /* SwipeActionsViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionsViewModifier.swift; sourceTree = "<group>"; };
366BD14FE1ED4F2AF21D924E /* Pods-iOS-PlatformUI.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-PlatformUI.release.xcconfig"; path = "Target Support Files/Pods-iOS-PlatformUI/Pods-iOS-PlatformUI.release.xcconfig"; sourceTree = "<group>"; };
418D5C02425B3C680BF32DA4 /* Pods_iOS_PlatformUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_PlatformUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -205,6 +207,7 @@
children = (
024B794F28B6D95D00F7C386 /* SignedAmount.swift */,
027A7B44291E090F00DF402D /* ColoredText.swift */,
277E7AC52BBF3BE8009F95DE /* InlineAlert.swift */,
);
path = Labels;
sourceTree = "<group>";
Expand Down Expand Up @@ -654,6 +657,7 @@
023788F528B9924D00F212E1 /* PlatformButton.swift in Sources */,
02E2C9C128A2C22B00F7C3BE /* SampleThemeLabel.swift in Sources */,
0243A73729BB8DBB00A083FE /* PlatformListViewModel.swift in Sources */,
277E7AC62BBF3BE8009F95DE /* InlineAlert.swift in Sources */,
02F16FDB28B491EE0085DC58 /* PlatformUI.swift in Sources */,
64A4DC5F29677BCB008D8E20 /* PlatformOutput.swift in Sources */,
0243A73E29BE2D7C00A083FE /* Divider.swift in Sources */,
Expand Down
112 changes: 112 additions & 0 deletions PlatformUI/PlatformUI/Components/Labels/InlineAlert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// InlineAlert.swift
// dydxUI
//
// Created by Michael Maguire on 4/4/24.
// Copyright © 2024 dYdX Trading Inc. All rights reserved.
//

import SwiftUI

public class InlineAlertViewModel: PlatformViewModel {

@Published public var config: Config

public init(_ config: Config) {
self.config = config
}

public static var previewValue: InlineAlertViewModel = {
let vm = InlineAlertViewModel(Config(title: "Title", body: "Body", level: .error))
return vm
}()

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 config = self.config

return HStack(spacing: 0) {
config.level.tabColor.color
.frame(width: 6)
HStack(spacing: 0) {
VStack(alignment: .leading) {
Text(config.title)
.themeColor(foreground: .textPrimary)
.themeFont(fontType: .plus, fontSize: .medium)
Text(config.body)
.themeColor(foreground: .textPrimary)
.themeFont(fontType: .base, fontSize: .small)
}
Spacer()
}
.padding(.all, 10)
.themeColor(background: config.level.backgroundColor)
}

.fixedSize(horizontal: false, vertical: true)
.clipShape(.rect(cornerRadius: 6))
.wrappedInAnyView()
}
}
}

public extension InlineAlertViewModel {
struct Config {
public var title: String
public var body: String
public var level: Level

public init(title: String, body: String, level: Level) {
self.title = title
self.body = body
self.level = level
}
}
}

public extension InlineAlertViewModel {
enum Level {
case error
case warning
case success

fileprivate var tabColor: ThemeColor.SemanticColor {
switch self {
case .error:
return .colorRed
case .warning:
return .colorYellow
case .success:
return .colorGreen
}
}

fileprivate var backgroundColor: ThemeColor.SemanticColor {
switch self {
case .error:
return .colorFadedRed
case .warning:
return .colorFadedYellow
case .success:
return .colorFadedGreen
}
}
}
}

#if DEBUG
struct InlineAlert_Previews: PreviewProvider {
@StateObject static var themeSettings = ThemeSettings.shared

static var previews: some View {
Group {
InlineAlertViewModel.previewValue
.createView()
.environmentObject(themeSettings)
.previewLayout(.sizeThatFits)
}
}
}
#endif

Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ private class dydxTakeProfitStopLossViewPresenter: HostedViewPresenter<dydxTakeP
override func start() {
super.start()

clearTriggersInput()
guard let marketId = marketId else { return }
AbacusStateManager.shared.triggerOrders(input: marketId, type: .marketid)

Publishers
.CombineLatest(AbacusStateManager.shared.state.selectedSubaccountPositions,
Expand All @@ -75,17 +75,76 @@ private class dydxTakeProfitStopLossViewPresenter: HostedViewPresenter<dydxTakeP
self?.update(triggerOrdersInput: triggerOrdersInput)
}
.store(in: &subscriptions)

AbacusStateManager.shared.state.validationErrors
.compactMap { $0 }
.sink { [weak self] errors in
self?.updateValidationErrors(errors)
}
.store(in: &subscriptions)
}

private func clearTriggersInput() {
AbacusStateManager.shared.triggerOrders(input: nil, type: .marketid)
AbacusStateManager.shared.triggerOrders(input: nil, type: .size)
AbacusStateManager.shared.triggerOrders(input: nil, type: .stoplossorderid)
AbacusStateManager.shared.triggerOrders(input: nil, type: .stoplossordersize)
AbacusStateManager.shared.triggerOrders(input: nil, type: .stoplossordertype)
AbacusStateManager.shared.triggerOrders(input: nil, type: .stoplosslimitprice)
AbacusStateManager.shared.triggerOrders(input: nil, type: .stoplossprice)
AbacusStateManager.shared.triggerOrders(input: nil, type: .takeprofitorderid)
AbacusStateManager.shared.triggerOrders(input: nil, type: .takeprofitordersize)
AbacusStateManager.shared.triggerOrders(input: nil, type: .takeprofitordertype)
AbacusStateManager.shared.triggerOrders(input: nil, type: .takeprofitlimitprice)
AbacusStateManager.shared.triggerOrders(input: nil, type: .takeprofitprice)
}

private func update(market: PerpetualMarket?) {
viewModel?.oraclePrice = market?.oraclePrice?.doubleValue
}

private func updateValidationErrors(_ errors: [ValidationError]) {
// TODO: update
if errors.isEmpty {
print("mmm: no errors")
viewModel?.takeProfitStopLossInputAreaViewModel?.takeProfitAlert = nil
viewModel?.takeProfitStopLossInputAreaViewModel?.stopLossAlert = nil
} else {
for error in errors {
switch error.code {
case "TRIGGER_MUST_BELOW_INDEX_PRICE":
print("mmm: TRIGGER_MUST_BELOW_INDEX_PRICE")
default:
print("mmm: ", error.code)
}
}
}
}

private func update(triggerOrdersInput: TriggerOrdersInput?) {
viewModel?.takeProfitStopLossInputAreaViewModel?.takeProfitPriceInputViewModel?.value = dydxFormatter.shared.dollar(number: triggerOrdersInput?.takeProfitOrder?.price?.triggerPrice?.doubleValue)
viewModel?.takeProfitStopLossInputAreaViewModel?.gainInputViewModel?.value = dydxFormatter.shared.dollar(number: triggerOrdersInput?.takeProfitOrder?.price?.usdcDiff?.doubleValue)
viewModel?.takeProfitStopLossInputAreaViewModel?.stopLossPriceInputViewModel?.value = dydxFormatter.shared.dollar(number: triggerOrdersInput?.stopLossOrder?.price?.triggerPrice?.doubleValue)
viewModel?.takeProfitStopLossInputAreaViewModel?.lossInputViewModel?.value = dydxFormatter.shared.dollar(number: triggerOrdersInput?.stopLossOrder?.price?.usdcDiff?.doubleValue)
#if DEBUG
// TODO: move this to abacus, needs more validation
if let tpUsdcDiff = triggerOrdersInput?.takeProfitOrder?.price?.usdcDiff?.doubleValue, tpUsdcDiff < 0 {
viewModel?.takeProfitStopLossInputAreaViewModel?.takeProfitAlert = .init(.init(title: "test error bad take profit",
body: "test error take profit description",
level: .error))
} else {
viewModel?.takeProfitStopLossInputAreaViewModel?.takeProfitAlert = nil
}
if let slUsdcDiff = triggerOrdersInput?.stopLossOrder?.price?.usdcDiff?.doubleValue, slUsdcDiff < 0 {
viewModel?.takeProfitStopLossInputAreaViewModel?.stopLossAlert = .init(.init(title: "test error bad stop loss",
body: "test error stop loss description",
level: .error))
} else {
viewModel?.takeProfitStopLossInputAreaViewModel?.stopLossAlert = nil
}
#endif

viewModel?.takeProfitStopLossInputAreaViewModel?.takeProfitPriceInputViewModel?.value = triggerOrdersInput?.takeProfitOrder?.price?.triggerPrice?.doubleValue.round(size: 2).description
viewModel?.takeProfitStopLossInputAreaViewModel?.gainInputViewModel?.value = triggerOrdersInput?.takeProfitOrder?.price?.usdcDiff?.doubleValue.round(size: 2).description
viewModel?.takeProfitStopLossInputAreaViewModel?.stopLossPriceInputViewModel?.value = triggerOrdersInput?.stopLossOrder?.price?.triggerPrice?.doubleValue.round(size: 2).description
viewModel?.takeProfitStopLossInputAreaViewModel?.lossInputViewModel?.value = triggerOrdersInput?.stopLossOrder?.price?.usdcDiff?.doubleValue.round(size: 2).description

}

private func update(subaccountPositions: [SubaccountPosition], subaccountOrders: [SubaccountOrder]) {
Expand All @@ -106,17 +165,23 @@ private class dydxTakeProfitStopLossViewPresenter: HostedViewPresenter<dydxTakeP
viewModel?.takeProfitStopLossInputAreaViewModel?.numOpenStopLossOrders = stopLossOrders.count

if takeProfitOrders.count == 1, let order = takeProfitOrders.first {
AbacusStateManager.shared.triggerOrders(input: order.id, type: .takeprofitorderid)
AbacusStateManager.shared.triggerOrders(input: order.size.description, type: .takeprofitordersize)
AbacusStateManager.shared.triggerOrders(input: order.type.rawValue, type: .takeprofitordertype)
AbacusStateManager.shared.triggerOrders(input: order.price.description, type: .takeprofitlimitprice)
AbacusStateManager.shared.triggerOrders(input: order.triggerPrice?.stringValue, type: .takeprofitprice)
}
if stopLossOrders.count == 1, let order = stopLossOrders.first {
AbacusStateManager.shared.triggerOrders(input: order.id, type: .stoplossorderid)
AbacusStateManager.shared.triggerOrders(input: order.size.description, type: .stoplossordersize)
AbacusStateManager.shared.triggerOrders(input: order.type.rawValue, type: .stoplossordertype)
AbacusStateManager.shared.triggerOrders(input: order.price.description, type: .stoplosslimitprice)
AbacusStateManager.shared.triggerOrders(input: order.triggerPrice?.stringValue, type: .stoplossprice)
}

AbacusStateManager.shared.triggerOrders(input: position?.size?.current?.stringValue, type: .size)
AbacusStateManager.shared.triggerOrders(input: marketId, type: .marketid)

}

override init() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,8 @@ public final class AbacusState {
public var triggerOrdersInput: AnyPublisher<TriggerOrdersInput?, Never> {
statePublisher
.map(\.?.input?.triggerOrders)
.throttle(for: .milliseconds(10), scheduler: DispatchQueue.main, latest: true)
.removeDuplicates()
.throttle(for: .milliseconds(10), scheduler: DispatchQueue.main, latest: true)
.share()
.eraseToAnyPublisher()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ public class dydxTakeProfitStopLossInputAreaModel: PlatformViewModel {
@Published public var numOpenTakeProfitOrders: Int?
@Published public var takeProfitPriceInputViewModel: dydxTriggerPriceInputViewModel?
@Published public var gainInputViewModel: dydxGainLossInputViewModel?
@Published public var takeProfitAlert: InlineAlertViewModel?

@Published public var numOpenStopLossOrders: Int?
@Published public var stopLossPriceInputViewModel: dydxTriggerPriceInputViewModel?
@Published public var lossInputViewModel: dydxGainLossInputViewModel?
@Published public var stopLossAlert: InlineAlertViewModel?

@Published public var multipleOrdersExistViewModel: dydxMultipleOrdersExistViewModel?
private var hasMultipleTakeProfitOrders: Bool { (numOpenTakeProfitOrders ?? 0) > 1 }
Expand Down Expand Up @@ -141,6 +143,7 @@ public class dydxTakeProfitStopLossInputAreaModel: PlatformViewModel {
self.gainInputViewModel?.createView(parentStyle: parentStyle, styleKey: styleKey)
}
}
self.takeProfitAlert?.createView(parentStyle: parentStyle, styleKey: styleKey)
}
VStack(alignment: .leading, spacing: 16) {
self.createSectionHeader(triggerType: .stopLoss)
Expand All @@ -152,6 +155,7 @@ public class dydxTakeProfitStopLossInputAreaModel: PlatformViewModel {
self.lossInputViewModel?.createView(parentStyle: parentStyle, styleKey: styleKey)
}
}
self.stopLossAlert?.createView(parentStyle: parentStyle, styleKey: styleKey)
}
}
.wrappedInAnyView()
Expand Down

0 comments on commit 1c3c3c0

Please sign in to comment.