diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 4329a43bd3..31540e6271 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2976,6 +2976,10 @@ D31C4761238BF176008CB818 /* MnemonicDerivation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31C4759238BF175008CB818 /* MnemonicDerivation.swift */; }; D31C4763238BF176008CB818 /* FeeRateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31C475A238BF175008CB818 /* FeeRateState.swift */; }; D31C4764238BF176008CB818 /* FeeRateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31C475A238BF175008CB818 /* FeeRateState.swift */; }; + D3384D092BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */; }; + D3384D0A2BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */; }; + D3384D0C2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */; }; + D3384D0D2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */; }; D339A93D29126D0F00B895BE /* HsCryptoKit in Frameworks */ = {isa = PBXBuildFile; productRef = D339A93C29126D0F00B895BE /* HsCryptoKit */; }; D339A93F29126D2A00B895BE /* HsCryptoKit in Frameworks */ = {isa = PBXBuildFile; productRef = D339A93E29126D2A00B895BE /* HsCryptoKit */; }; D3402AEE2BF5D58B003BF6F8 /* WatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */; }; @@ -4901,6 +4905,8 @@ D3285F5120BD158F00644076 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D3373D9420BEC7B30082BC4A /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; D3373DB120C52F640082BC4A /* LaunchScreen.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = ""; }; + D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistSignalsView.swift; sourceTree = ""; }; + D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistSignalBadge.swift; sourceTree = ""; }; D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistViewModel.swift; sourceTree = ""; }; D3402AF02BF5D59D003BF6F8 /* WatchlistModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistModifier.swift; sourceTree = ""; }; D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistManager.swift; sourceTree = ""; }; @@ -9329,6 +9335,8 @@ D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */, D3833AD62BEE1A7900ACECFB /* MarketWatchlistView.swift */, D3833AD92BEE1A8300ACECFB /* MarketWatchlistViewModel.swift */, + D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */, + D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */, ); path = Watchlist; sourceTree = ""; @@ -9851,6 +9859,7 @@ 11B358B0576F63BE43947DD5 /* Account.swift in Sources */, 11B35BEB439509EACB41AB06 /* AccountType.swift in Sources */, 11B35F663F7E12BFDDE3C88B /* AccountManager.swift in Sources */, + D3384D0D2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */, 11B353AA4AFFB020A68E09B6 /* AccountFactory.swift in Sources */, 3C7B9BAF807355796DCA80C4 /* WelcomeScreenViewController.swift in Sources */, 11B35BCD6D0462E31D7EBA06 /* BackupManager.swift in Sources */, @@ -11205,6 +11214,7 @@ 11B3523804E0F4F1DA8A1D9E /* BaseCurrencySettingsViewModel.swift in Sources */, 11B35F18FEEEAA9EC6043CA6 /* BaseCurrencySettingsView.swift in Sources */, 11B35E749106C1ABD9335778 /* TransactionFilterModule.swift in Sources */, + D3384D0A2BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */, 11B3591E867F4E701F5458F9 /* TransactionFilterView.swift in Sources */, 11B352E8348A715EB537F643 /* TransactionFilterViewModel.swift in Sources */, 11B35AAD64D68265B2128C25 /* TransactionBlockchainSelectView.swift in Sources */, @@ -11414,6 +11424,7 @@ 11B35068E05BC58C6C9A93D7 /* AccountManager.swift in Sources */, 11B35C8621E221DA1F157A5B /* AccountFactory.swift in Sources */, 3C7B9F51D15FBB02710E5EEB /* WelcomeScreenViewController.swift in Sources */, + D3384D0C2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */, 11B35BC6DFCA197FA842873B /* BackupManager.swift in Sources */, 11B35590A4DA4BCFB3D38DDF /* AppManager.swift in Sources */, D31C4761238BF176008CB818 /* MnemonicDerivation.swift in Sources */, @@ -12768,6 +12779,7 @@ ABC9AE558CE5912B5C15B6EB /* ActionSheetControllerNew.swift in Sources */, 11B3537EE13B3EFB2E979821 /* BadgeViewNew.swift in Sources */, 11B35955EE2F47EFAFCBCE9F /* BaseCurrencySettingsViewModel.swift in Sources */, + D3384D092BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */, 11B3565D4E4EAD663143ED9B /* BaseCurrencySettingsView.swift in Sources */, 11B353BAEF83867422611E7B /* TransactionFilterModule.swift in Sources */, 11B35B09AADB1FBF7DDE765C /* TransactionFilterView.swift in Sources */, diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png index 991b559865..0844165b52 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@2x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png index ca3f994bd7..c24f8f6f45 100644 Binary files a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/check_2_24.imageset/check-2@3x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewModel.swift index 9fa74eb7a0..8d57080b72 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketAdvancedSearch/MarketAdvancedSearchViewModel.swift @@ -485,7 +485,7 @@ extension TechnicalAdvice.Advice { var searchTitle: String { switch self { - case .oversold, .overbought: return "market.advanced_search.technical_advice.risk_trade".localized + case .oversold, .overbought: return "market.advanced_search.technical_advice.risky".localized case .strongBuy: return "market.advanced_search.technical_advice.strong_buy".localized case .buy: return "market.advanced_search.technical_advice.buy".localized case .neutral: return "market.advanced_search.technical_advice.neutral".localized diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalBadge.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalBadge.swift new file mode 100644 index 0000000000..02383b2c78 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalBadge.swift @@ -0,0 +1,37 @@ +import MarketKit +import SwiftUI + +struct MarketWatchlistSignalBadge: View { + let signal: TechnicalAdvice.Advice + + var body: some View { + Text(signal.searchTitle) + .font(.themeMicroSB) + .foregroundColor(foregroundColor) + .padding(.horizontal, .margin6) + .padding(.vertical, .margin2) + .background(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).fill(backgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous)) + } + + private var foregroundColor: Color { + switch signal { + case .neutral: return .themeBran + case .buy: return .themeRemus + case .sell: return .themeLucian + case .strongBuy, .strongSell: return .themeTyler + case .overbought, .oversold: return .themeJacob + } + } + + private var backgroundColor: Color { + switch signal { + case .neutral: return .themeSteel20 + case .buy: return .themeGreen.opacity(0.2) + case .sell: return .themeRed.opacity(0.2) + case .strongBuy: return .themeRemus + case .strongSell: return .themeLucian + case .overbought, .oversold: return .themeYellow20 + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalsView.swift new file mode 100644 index 0000000000..c67da0898c --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistSignalsView.swift @@ -0,0 +1,121 @@ +import MarketKit +import SwiftUI + +struct MarketWatchlistSignalsView: View { + @ObservedObject var viewModel: MarketWatchlistViewModel + @Binding var isPresented: Bool + + @State private var maxBadgeWidth: CGFloat = .zero + @State private var doNotShowAgain = false + + var body: some View { + ThemeNavigationView { + ThemeView { + BottomGradientWrapper { + ScrollView { + VStack(spacing: .margin16) { + Text("market.watchlist.signals.description".localized) + .themeSubhead2() + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: .margin8, trailing: .margin16)) + + ListSection { + row(signal: .strongBuy) + row(signal: .buy) + row(signal: .neutral) + row(signal: .sell) + row(signal: .strongSell) + row(signal: .overbought) + } + .themeListStyle(.bordered) + .onPreferenceChange(MaxWidthPreferenceKey.self) { + maxBadgeWidth = $0 + } + + HighlightedTextView(text: "market.watchlist.signals.warning".localized, style: .warning) + + ListSection { + ClickableRow(action: { + doNotShowAgain.toggle() + }) { + if doNotShowAgain { + ZStack { + Circle().fill(Color.themeJacob) + Image("check_2_24").themeIcon(color: .themeDark) + } + .frame(width: .iconSize24, height: .iconSize24) + } else { + Circle() + .fill(Color.themeSteel20) + .frame(width: .iconSize24, height: .iconSize24) + } + + Text("market.watchlist.signals.dont_show_again".localized).themeSubhead2(color: .themeLeah) + } + } + .themeListStyle(.lawrence) + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } bottomContent: { + Button(action: { + viewModel.showSignals = true + + if doNotShowAgain { + viewModel.signalsApproved = true + } + + isPresented = false + }) { + Text("market.watchlist.signals.turn_on".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + } + } + .navigationTitle("market.watchlist.signals".localized) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.cancel".localized) { + isPresented = false + } + } + } + } + } + + @ViewBuilder private func row(signal: TechnicalAdvice.Advice) -> some View { + ListRow { + MarketWatchlistSignalBadge(signal: signal) + .background( + GeometryReader { geometry in + Color.clear.preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width) + } + .scaledToFill() + ) + .frame(width: maxBadgeWidth) + + Text(description(signal: signal)).themeSubhead2(color: .themeLeah) + } + } + + private func description(signal: TechnicalAdvice.Advice) -> String { + switch signal { + case .neutral: return "market.watchlist.signals.neutral.description".localized + case .buy: return "market.watchlist.signals.buy.description".localized + case .sell: return "market.watchlist.signals.sell.description".localized + case .strongBuy: return "market.watchlist.signals.strong_buy.description".localized + case .strongSell: return "market.watchlist.signals.strong_sell.description".localized + case .overbought, .oversold: return "market.watchlist.signals.risky.description".localized + } + } + + private struct MaxWidthPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + let nextValue = nextValue() + guard nextValue > value else { return } + value = nextValue + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift index b24b41cf9c..1440e11852 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift @@ -8,6 +8,7 @@ struct MarketWatchlistView: View { @State private var sortBySelectorPresented = false @State private var timePeriodSelectorPresented = false @State private var presentedFullCoin: FullCoin? + @State private var signalsPresented = false @State private var editMode: EditMode = .inactive @@ -111,11 +112,20 @@ struct MarketWatchlistView: View { viewModel.timePeriod = WatchlistTimePeriod.allCases[index] } ) + .sheet(isPresented: $signalsPresented) { + MarketWatchlistSignalsView(viewModel: viewModel, isPresented: $signalsPresented) + } } @ViewBuilder private func signalsButton() -> some View { Button(action: { - viewModel.showSignals.toggle() + if viewModel.showSignals { + viewModel.showSignals = false + } else if viewModel.signalsApproved { + viewModel.showSignals = true + } else { + signalsPresented = true + } }) { Text("market.watchlist.signals".localized) } @@ -191,17 +201,11 @@ struct MarketWatchlistView: View { VStack(spacing: 1) { HStack(spacing: .margin8) { - HStack(spacing: .margin12) { + HStack(spacing: .margin8) { Text(code).textBody() if let signal { - Text(signal.searchTitle) - .font(.themeMicroSB) - .foregroundColor(foregroundColor(signal: signal)) - .padding(.horizontal, .margin6) - .padding(.vertical, .margin2) - .background(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous).fill(backgroundColor(signal: signal))) - .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8, style: .continuous)) + MarketWatchlistSignalBadge(signal: signal) } } @@ -224,25 +228,4 @@ struct MarketWatchlistView: View { } } } - - private func foregroundColor(signal: TechnicalAdvice.Advice) -> Color { - switch signal { - case .neutral: return .themeBran - case .buy: return .themeRemus - case .sell: return .themeLucian - case .strongBuy, .strongSell: return .themeTyler - case .overbought, .oversold: return .themeJacob - } - } - - private func backgroundColor(signal: TechnicalAdvice.Advice) -> Color { - switch signal { - case .neutral: return .themeSteel20 - case .buy: return .themeGreen.opacity(0.2) - case .sell: return .themeRed.opacity(0.2) - case .strongBuy: return .themeRemus - case .strongSell: return .themeLucian - case .overbought, .oversold: return .themeYellow20 - } - } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistViewModel.swift index 510d97b0fb..23cad81d82 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistViewModel.swift @@ -4,9 +4,12 @@ import HsExtensions import MarketKit class MarketWatchlistViewModel: ObservableObject { + private let keySignalsApproved = "market-watchlist-signals-approved" + private let marketKit = App.shared.marketKit private let currencyManager = App.shared.currencyManager private let watchlistManager = App.shared.watchlistManager + private let userDefaultsStorage = App.shared.userDefaultsStorage private var cancellables = Set() private var tasks = Set() @@ -42,10 +45,17 @@ class MarketWatchlistViewModel: ObservableObject { } } + var signalsApproved: Bool { + didSet { + userDefaultsStorage.set(value: true, for: keySignalsApproved) + } + } + init() { sortBy = watchlistManager.sortBy timePeriod = watchlistManager.timePeriod showSignals = watchlistManager.showSignals + signalsApproved = userDefaultsStorage.value(for: keySignalsApproved) ?? false } private func syncCoinUids() { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift index 4f1ce48adc..bd9c51dc85 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/BlockchainSettings/BlockchainSettingsView.swift @@ -41,7 +41,7 @@ struct BlockchainSettingsView: View { } } .sheet(item: $evmSheetBlockchain) { blockchain in - EvmNetworkView(blockchain: blockchain) + EvmNetworkView(blockchain: blockchain).ignoresSafeArea() } } } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 979b719f66..f00da80854 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -797,6 +797,16 @@ "market_watchlist.empty.caption" = "Your watchlist is empty."; "market.watchlist.signals" = "Signals"; "market.watchlist.empty" = "Your watchlist is empty"; +"market.watchlist.signals.description" = "Signals are based on the Bollinger Bands + RSI strategy to determine trading signals. All calculations are candlesticks and provide based on daily advice for a moderately long term."; +"market.watchlist.signals.strong_buy.description" = "High confidence in asset price growth."; +"market.watchlist.signals.buy.description" = "Indicates likely price increase in near future."; +"market.watchlist.signals.neutral.description" = "No clear trend, market is in equilibrium."; +"market.watchlist.signals.sell.description" = "Likely price decrease, considers current market conditions."; +"market.watchlist.signals.strong_sell.description" = "High probability of price decrease."; +"market.watchlist.signals.risky.description" = "Elevated risk level, requires cautious approach."; +"market.watchlist.signals.warning" = "Always remember to apply risk management, and note that this is not financial advice."; +"market.watchlist.signals.dont_show_again" = "Don't show it again"; +"market.watchlist.signals.turn_on" = "Turn On"; "market.advanced_search.title" = "Filters"; "market.advanced_search.show_results" = "Show Results"; @@ -824,7 +834,7 @@ "market.advanced_search.price_period" = "Price Period"; "market.advanced_search.price_change" = "Price Change"; -"market.advanced_search.technical_advice.risk_trade" = "Risk To Trade"; +"market.advanced_search.technical_advice.risky" = "Risky"; "market.advanced_search.technical_advice.strong_buy" = "Strong Buy"; "market.advanced_search.technical_advice.buy" = "Buy"; "market.advanced_search.technical_advice.neutral" = "Neutral";