diff --git a/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj b/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj index ff6b180ee..5441f53b0 100644 --- a/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj +++ b/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj @@ -74,6 +74,7 @@ 0276FA992A0DB8FD000BDF0B /* Model+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0276FA982A0DB8FD000BDF0B /* Model+Ext.swift */; }; 0279656A29D795E8004DEB20 /* tabs_v4.json in Resources */ = {isa = PBXBuildFile; fileRef = 0279655B29D795E7004DEB20 /* tabs_v4.json */; }; 0279656C29D795E8004DEB20 /* routing_swiftui.json in Resources */ = {isa = PBXBuildFile; fileRef = 0279656929D795E7004DEB20 /* routing_swiftui.json */; }; + 0279DE482BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE472BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift */; }; 027CA87229EDFC990069781A /* dydxTransferInputCtaButtonViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027CA87129EDFC990069781A /* dydxTransferInputCtaButtonViewPresenter.swift */; }; 027CB28729EEFF910069781A /* dydxTransferStatusViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027CB28629EEFF910069781A /* dydxTransferStatusViewBuilder.swift */; }; 027E1EF829CA27CD0098666F /* dydxSettingsLandingViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027E1EF729CA27CD0098666F /* dydxSettingsLandingViewBuilder.swift */; }; @@ -431,6 +432,7 @@ 0276FA982A0DB8FD000BDF0B /* Model+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Model+Ext.swift"; sourceTree = ""; }; 0279655B29D795E7004DEB20 /* tabs_v4.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tabs_v4.json; sourceTree = ""; }; 0279656929D795E7004DEB20 /* routing_swiftui.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = routing_swiftui.json; sourceTree = ""; }; + 0279DE472BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTargetLeverageCtaButtonViewPresenter.swift; sourceTree = ""; }; 027CA87129EDFC990069781A /* dydxTransferInputCtaButtonViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTransferInputCtaButtonViewPresenter.swift; sourceTree = ""; }; 027CB28629EEFF910069781A /* dydxTransferStatusViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTransferStatusViewBuilder.swift; sourceTree = ""; }; 027E1EF729CA27CD0098666F /* dydxSettingsLandingViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSettingsLandingViewBuilder.swift; sourceTree = ""; }; @@ -749,6 +751,7 @@ 023AB3C22BEAD554005230B2 /* Margin */ = { isa = PBXGroup; children = ( + 0279DE462BEBE75D00F9ECF8 /* Components */, 023AB3C32BEAD56A005230B2 /* dydxMarginModeViewBuilder.swift */, 023AB3C72BEAD5F3005230B2 /* dydxTargetLeverageViewBuilder.swift */, ); @@ -1028,6 +1031,14 @@ path = Components; sourceTree = ""; }; + 0279DE462BEBE75D00F9ECF8 /* Components */ = { + isa = PBXGroup; + children = ( + 0279DE472BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift */, + ); + path = Components; + sourceTree = ""; + }; 027CB26A29EEFF5A0069781A /* TransferStatus */ = { isa = PBXGroup; children = ( @@ -1898,6 +1909,7 @@ 02E90C5A29D62719004E2311 /* dydxFeatureFlagsViewBuilder.swift in Sources */, 6453AB26299D98110041A0C4 /* dydxClosePositionInputEditPresenter.swift in Sources */, 0243A76129BE572C00A083FE /* dydxCancelOrderActionBuilder.swift in Sources */, + 0279DE482BEBE76900F9ECF8 /* dydxTargetLeverageCtaButtonViewPresenter.swift in Sources */, 023AB3B22BEACE14005230B2 /* dydxTradeInputMarginViewPresenter.swift in Sources */, 026388D82BB34B7A006DD6E8 /* OnboardingAnalytics.swift in Sources */, 64A4DB9929664818008D8E20 /* dydxTradeReceiptPresenter.swift in Sources */, diff --git a/dydx/dydxPresenters/dydxPresenters/_Features/routing_swiftui.json b/dydx/dydxPresenters/dydxPresenters/_Features/routing_swiftui.json index 567d8994b..3bfbe6ba1 100644 --- a/dydx/dydxPresenters/dydxPresenters/_Features/routing_swiftui.json +++ b/dydx/dydxPresenters/dydxPresenters/_Features/routing_swiftui.json @@ -313,7 +313,7 @@ }, "/trade/target_leverage":{ "destination":"dydxPresenters.dydxTargetLeverageViewBuilder", - "presentation":"half" + "presentation":"prompt" }, "/transfer":{ "destination":"dydxPresenters.dydxTransferViewBuilder", diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/Components/dydxTargetLeverageCtaButtonViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/Components/dydxTargetLeverageCtaButtonViewPresenter.swift new file mode 100644 index 000000000..e338b58c6 --- /dev/null +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/Components/dydxTargetLeverageCtaButtonViewPresenter.swift @@ -0,0 +1,25 @@ +// +// dydxTargetLeverageCtaButtonViewPresenter.swift +// dydxPresenters +// +// Created by Rui Huang on 08/05/2024. +// + +import Utilities +import dydxViews +import PlatformParticles +import RoutingKit +import ParticlesKit +import PlatformUI + +protocol dydxTargetLeverageCtaButtonViewPresenterProtocol: HostedViewPresenterProtocol { + var viewModel: dydxTargetLeverageCtaButtonViewModel? { get } +} + +class dydxTargetLeverageCtaButtonViewPresenter: HostedViewPresenter, dydxTargetLeverageCtaButtonViewPresenterProtocol { + override init() { + super.init() + + viewModel = dydxTargetLeverageCtaButtonViewModel() + } +} diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxTargetLeverageViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxTargetLeverageViewBuilder.swift index 266f294a4..672f9d0df 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxTargetLeverageViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Trade/Margin/dydxTargetLeverageViewBuilder.swift @@ -11,12 +11,15 @@ import PlatformParticles import RoutingKit import ParticlesKit import PlatformUI +import Abacus +import dydxStateManager +import dydxFormatter public class dydxTargetLeverageViewBuilder: NSObject, ObjectBuilderProtocol { public func build() -> T? { let presenter = dydxTargetLeverageViewPresenter() let view = presenter.viewModel?.createView() ?? PlatformViewModel().createView() - return dydxTargetLeverageViewController(presenter: presenter, view: view, configuration: .ignoreSafeArea) as? T + return dydxTargetLeverageViewController(presenter: presenter, view: view, configuration: .default) as? T } } @@ -34,10 +37,39 @@ private protocol dydxTargetLeverageViewPresenterProtocol: HostedViewPresenterPro } private class dydxTargetLeverageViewPresenter: HostedViewPresenter, dydxTargetLeverageViewPresenterProtocol { + private let ctaButtonPresenter = dydxTargetLeverageCtaButtonViewPresenter() + + private lazy var childPresenters: [HostedViewPresenterProtocol] = [ + ctaButtonPresenter + ] + override init() { super.init() viewModel = dydxTargetLeverageViewModel() viewModel?.description = DataLocalizer.localize(path: "APP.TRADE.ADJUST_TARGET_LEVERAGE_DESCRIPTION") + + viewModel?.leverageOptions = [ + dydxTargetLeverageViewModel.LeverageTextAndValue(text: "1x", value: 1.0), + dydxTargetLeverageViewModel.LeverageTextAndValue(text: "2x", value: 2.0), + dydxTargetLeverageViewModel.LeverageTextAndValue(text: "5x", value: 5.0), + dydxTargetLeverageViewModel.LeverageTextAndValue(text: "10.0", value: 10.0), + dydxTargetLeverageViewModel.LeverageTextAndValue(text: "Max", value: 20.0) + ] + + attachChildren(workers: childPresenters) + } + + override func start() { + super.start() + + // TODO: Fix... tradeInput?.targetLeverage is nil for now + + AbacusStateManager.shared.state.tradeInput + .sink { [weak self] tradeInput in + let value = dydxFormatter.shared.localFormatted(number: tradeInput?.targetLeverage ?? 1, digits: 1) + self?.viewModel?.leverageInput?.value = value + } + .store(in: &subscriptions) } } diff --git a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj index 6c29b9928..825364474 100644 --- a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj +++ b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj @@ -99,6 +99,7 @@ 0277A21E28BD75E8005A51F8 /* ThemeLight.json in Resources */ = {isa = PBXBuildFile; fileRef = 024B7B5228B7F89500F7C386 /* ThemeLight.json */; }; 0277A21F28BD75F9005A51F8 /* dydxStyle.json in Resources */ = {isa = PBXBuildFile; fileRef = 024B7B5328B7F89500F7C386 /* dydxStyle.json */; }; 0277A22528BD7B14005A51F8 /* WalletConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0277A22428BD7B14005A51F8 /* WalletConnectionView.swift */; }; + 0279DE452BEBE75100F9ECF8 /* dydxTargetLeverageCtaButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0279DE442BEBE75100F9ECF8 /* dydxTargetLeverageCtaButtonView.swift */; }; 027C379F29AEC11000381B00 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 027C379E29AEC11000381B00 /* Introspect */; }; 027CB28529EEFF760069781A /* dydxTransferStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027CB28429EEFF760069781A /* dydxTransferStatusView.swift */; }; 027F3EF72AB93ADC00602E5B /* dydxProfileBalancesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027F3EF62AB93ADC00602E5B /* dydxProfileBalancesViewModel.swift */; }; @@ -460,6 +461,7 @@ 0276FA7B2A0DB515000BDF0B /* OrderStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStatus.swift; sourceTree = ""; }; 02770BAB2ABE11D4004BBFE5 /* dydxReceiptRewardsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxReceiptRewardsView.swift; sourceTree = ""; }; 0277A22428BD7B14005A51F8 /* WalletConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionView.swift; sourceTree = ""; }; + 0279DE442BEBE75100F9ECF8 /* dydxTargetLeverageCtaButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTargetLeverageCtaButtonView.swift; sourceTree = ""; }; 027CB28429EEFF760069781A /* dydxTransferStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTransferStatusView.swift; sourceTree = ""; }; 027F3EF62AB93ADC00602E5B /* dydxProfileBalancesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxProfileBalancesViewModel.swift; sourceTree = ""; }; 0280B39629CB63C70017D64A /* dydxOnboardWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxOnboardWelcomeView.swift; sourceTree = ""; }; @@ -778,6 +780,7 @@ 023AB3C12BEAD544005230B2 /* Margin */ = { isa = PBXGroup; children = ( + 0279DE412BEBE56C00F9ECF8 /* Components */, 023AB3B32BEAD53D005230B2 /* dydxMarginModeView.swift */, 023AB3C52BEAD5E9005230B2 /* dydxTargetLeverageView.swift */, ); @@ -1156,6 +1159,14 @@ path = History; sourceTree = ""; }; + 0279DE412BEBE56C00F9ECF8 /* Components */ = { + isa = PBXGroup; + children = ( + 0279DE442BEBE75100F9ECF8 /* dydxTargetLeverageCtaButtonView.swift */, + ); + path = Components; + sourceTree = ""; + }; 027CB27729EEFF670069781A /* TransferStatus */ = { isa = PBXGroup; children = ( @@ -1971,6 +1982,7 @@ 024FEB642ACB75E10087A55E /* dydxFeesStuctureView.swift in Sources */, 277E918B2B27762F005CCBCB /* dydxRewardsLaunchIncentivesView.swift in Sources */, 0268BBF92A8BE08C00D0C59B /* dydxTransferOutView.swift in Sources */, + 0279DE452BEBE75100F9ECF8 /* dydxTargetLeverageCtaButtonView.swift in Sources */, 0208627A28F4D95F00C9D3A0 /* dydxMarketInfoPagingView.swift in Sources */, 027F3EF72AB93ADC00602E5B /* dydxProfileBalancesViewModel.swift in Sources */, 024B44F52983E38D00E35D54 /* dydxTradeStatusLogoView.swift in Sources */, diff --git a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxTargetLeverageCtaButtonView.swift b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxTargetLeverageCtaButtonView.swift new file mode 100644 index 000000000..c09ed97ba --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/Components/dydxTargetLeverageCtaButtonView.swift @@ -0,0 +1,100 @@ +// +// dydxTargetLeverageCtaButtonView.swift +// dydxUI +// +// Created by Rui Huang on 08/05/2024. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import SwiftUI +import PlatformUI +import Utilities + +public class dydxTargetLeverageCtaButtonViewModel: PlatformViewModel { + public enum State { + case enabled(String? = nil) + case disabled(String? = nil) + case thinking + } + + @Published public var ctaAction: (() -> Void)? + @Published public var ctaButtonState: State = .disabled() + + public init() { } + + public static var previewValue: dydxTargetLeverageCtaButtonViewModel { + let vm = dydxTargetLeverageCtaButtonViewModel() + vm.ctaButtonState = .enabled("OK") + 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) } + + return AnyView( + self.createCtaButton(parentStyle: style) + ) + } + } + + private func createCtaButton(parentStyle style: ThemeStyle) -> some View { + let buttonTitle: String + let state: PlatformButtonState + switch ctaButtonState { + case .enabled(let title): + buttonTitle = title ?? DataLocalizer.localize(path: "APP.TRADE.CONFIRM_LEVERAGE") + state = .primary + case .disabled(let title): + buttonTitle = title ?? DataLocalizer.localize(path: "APP.TRADE.ADJUST_LEVERAGE") + state = .disabled + case .thinking: + buttonTitle = DataLocalizer.localize(path: "APP.V4.CALCULATING") + state = .disabled + } + + let buttonContent = + Text(buttonTitle) + .wrappedViewModel + + return PlatformButtonViewModel(content: buttonContent, + state: state) { [weak self] in + PlatformView.hideKeyboard() + self?.ctaAction?() + } + .createView(parentStyle: style) + .animation(.easeInOut(duration: 0.1)) + } +} + +#if DEBUG +struct dydxTargetLeverageCtaButtonView_Previews_Dark: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyDarkTheme() + ThemeSettings.applyStyles() + return dydxTargetLeverageCtaButtonViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} + +struct dydxTargetLeverageCtaButtonView_Previews_Light: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyLightTheme() + ThemeSettings.applyStyles() + return dydxTargetLeverageCtaButtonViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} +#endif diff --git a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxTargetLeverageView.swift b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxTargetLeverageView.swift index efbb4f999..3ddd88f0e 100644 --- a/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxTargetLeverageView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Trade/Margin/dydxTargetLeverageView.swift @@ -11,7 +11,25 @@ import PlatformUI import Utilities public class dydxTargetLeverageViewModel: PlatformViewModel { + public struct LeverageTextAndValue { + let text: String + let value: Double + + public init(text: String, value: Double) { + self.text = text + self.value = value + } + } + @Published public var description: String? + @Published public var leverageOptions: [LeverageTextAndValue] = [] + @Published public var selectedOptionIndex: Int? + @Published public var optionSelectedAction: ((LeverageTextAndValue) -> Void)? + @Published public var leverageInput: PlatformTextInputViewModel? = + PlatformTextInputViewModel(label: DataLocalizer.localize(path: "APP.TRADE.TARGET_LEVERAGE"), + placeHolder: "0.0", + inputType: PlatformTextInputViewModel.InputType.decimalDigits) + @Published public var ctaButton: dydxTargetLeverageCtaButtonViewModel? = dydxTargetLeverageCtaButtonViewModel() public init() { } @@ -22,7 +40,7 @@ public class dydxTargetLeverageViewModel: PlatformViewModel { } public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { - PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] _ in + PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in guard let self = self else { return AnyView(PlatformView.nilView) } let view = VStack(alignment: .leading, spacing: 20) { @@ -37,16 +55,57 @@ public class dydxTargetLeverageViewModel: PlatformViewModel { .leftAligned() .themeFont(fontSize: .medium) + self.leverageInput? + .createView(parentStyle: style) + .makeInput() + + self.createOptionsGroup(parentStyle: style) + Spacer() + + self.ctaButton?.createView(parentStyle: style) } .padding(.horizontal) .themeColor(background: .layer3) .makeSheet(sheetStyle: .fitSize) - // make it visible under the tabbar - return AnyView(view.ignoresSafeArea(edges: [.bottom])) + return AnyView(view) } } + + private func createOptionsGroup(parentStyle: ThemeStyle) -> some View { + let items = self.leverageOptions.compactMap { + Text($0.text) + .themeFont(fontType: .plus, fontSize: .small) + .themeColor(foreground: .textTertiary) + .padding(8) + .frame(minWidth: 60) + .themeColor(background: .layer5) + .border(borderWidth: 1, cornerRadius: 8, borderColor: ThemeColor.SemanticColor.layer5.color) + .wrappedViewModel + } + let selectedItems = self.leverageOptions.compactMap { + Text($0.text) + .themeFont(fontType: .plus, fontSize: .small) + .themeColor(foreground: .textPrimary) + .padding(8) + .frame(minWidth: 60) + .themeColor(background: .layer1) + .border(borderWidth: 1, cornerRadius: 8, borderColor: ThemeColor.SemanticColor.layer5.color) + .wrappedViewModel + } + + return + ScrollView(.horizontal, showsIndicators: false) { + TabGroupModel(items: items, + selectedItems: selectedItems, + currentSelection: self.selectedOptionIndex, + onSelectionChanged: { index in + self.optionSelectedAction?(self.leverageOptions[index]) + }) + .createView(parentStyle: parentStyle) + } + } } #if DEBUG