Skip to content

Commit

Permalink
MOB-1909 - Global search receiver functionality (#454)
Browse files Browse the repository at this point in the history
* Updated receive button place. Removed current profile switcher.
Added send button

* Updated home screen title view

* Added send crypto view.
Show from the home view.
Added input field and scan qr option

* Added section with user's wallets

* Added send crypto view model
Added UI for following selection

* Refactoring

* Updated nav bar customisation

* Disable interactive dismiss when navigate

* Extracted tab selection view from explore module

* Refactoring

* Created UI for token balance

* Make tokens selectable

* Added UI for domains list in assets selection
Adjusted navigation header UI

* Created senc crypto flow actions
Handle actions in view model
Hide empty sections on receiver selection screen

* Pass actions from select receiver and asset type views

* Refactoring

* Fixed initial onAppear call in token icons view

* Created UDNumberButtonView

* Created number pad view and input interpreter.
Created tests for interpreter

* Improved number pad logic

* Added UI for input and token info

* Added converted value label and toggle

* Added confirmation button

* Refactoring

* Disable animation for primary input view

* Swap buy and send actions on home screen

* Fixed copy on select receiver screen

* Added token icon near primary input value

* Adjusted UI for nav bar and in the IP SE

* Added search by domain profiles

* Show direct wallet address result

* Fixed first responder reset issue
  • Loading branch information
Oleg-Pecheneg authored Mar 21, 2024
1 parent 071002f commit f941702
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,8 @@
C66FCD1E2845C3AE009B9525 /* DomainsCollectionMintingInProgressCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C66FCD192845C3AE009B9525 /* DomainsCollectionMintingInProgressCell.xib */; };
C66FFD052B01E2EE00988A6F /* CoreDataMessagingStorageServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C66FFD042B01E2EE00988A6F /* CoreDataMessagingStorageServiceTests.swift */; };
C670695628981C84001FD241 /* TextWarningButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C670695528981C84001FD241 /* TextWarningButton.swift */; };
C67097AC2BABD47E00BB8AB1 /* DebounceObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67097AB2BABD47E00BB8AB1 /* DebounceObject.swift */; };
C67097AD2BABD47E00BB8AB1 /* DebounceObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67097AB2BABD47E00BB8AB1 /* DebounceObject.swift */; };
C671E38728FE85C800A2B3A0 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C671E38628FE85C800A2B3A0 /* CarouselView.swift */; };
C671E38E28FE86EB00A2B3A0 /* CarouselCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C671E38C28FE86EB00A2B3A0 /* CarouselCollectionViewCell.swift */; };
C671E39228FE86EB00A2B3A0 /* CarouselCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C671E38D28FE86EB00A2B3A0 /* CarouselCollectionViewCell.xib */; };
Expand Down Expand Up @@ -3262,6 +3264,7 @@
C66FCD192845C3AE009B9525 /* DomainsCollectionMintingInProgressCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DomainsCollectionMintingInProgressCell.xib; sourceTree = "<group>"; };
C66FFD042B01E2EE00988A6F /* CoreDataMessagingStorageServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataMessagingStorageServiceTests.swift; sourceTree = "<group>"; };
C670695528981C84001FD241 /* TextWarningButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWarningButton.swift; sourceTree = "<group>"; };
C67097AB2BABD47E00BB8AB1 /* DebounceObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebounceObject.swift; sourceTree = "<group>"; };
C671E38628FE85C800A2B3A0 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; };
C671E38C28FE86EB00A2B3A0 /* CarouselCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselCollectionViewCell.swift; sourceTree = "<group>"; };
C671E38D28FE86EB00A2B3A0 /* CarouselCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CarouselCollectionViewCell.xib; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5538,7 +5541,6 @@
isa = PBXGroup;
children = (
C63AD09C2B956D2A00BF8C83 /* HomeExploreGlobalSearchResultSectionView.swift */,
C6170EBD2B79DA9A008E9C93 /* DomainSearchResultProfileRowView.swift */,
);
path = "Global search";
sourceTree = "<group>";
Expand Down Expand Up @@ -7255,6 +7257,7 @@
C60D2716295AC4AB0060635D /* ConnectedAppsBackgroundColorCache.swift */,
30472D8B2672435200B0D7A7 /* Debugger.swift */,
307852632771FB020039FF40 /* DeepLinks.swift */,
C67097AB2BABD47E00BB8AB1 /* DebounceObject.swift */,
C69F99402A9F1478004B1958 /* DomainProfileBadgeDisplayInfo.swift */,
C663D1AD2AFCA022003C54E0 /* DomainProfileLinkValidator.swift */,
C6D011C42B996A5C0008BF40 /* DomainProfileSuggestion.swift */,
Expand Down Expand Up @@ -7723,6 +7726,7 @@
C6098314282B6F7000546392 /* EmptyRootNavigationController.swift */,
C62247A4283B92C0002A0CBD /* UDNavigationController.swift */,
C62900ED2BAAC5C0008B35A2 /* BalanceTokenIconsView.swift */,
C6170EBD2B79DA9A008E9C93 /* DomainSearchResultProfileRowView.swift */,
C6B65F4D2B54DA6D006D1812 /* Home */,
C60982A7282A724400546392 /* AddWallet */,
C6386835285C0EB3000F98C4 /* AppUpdatedRequired */,
Expand Down Expand Up @@ -8874,6 +8878,7 @@
30EFBBDC27F4754E00B8D667 /* MainButton.swift in Sources */,
C6F9FBD92A25C58C00102F81 /* PushMessagingAPIService.swift in Sources */,
C629A4802A403C1C00A528A1 /* MessagingChatConversationState.swift in Sources */,
C67097AC2BABD47E00BB8AB1 /* DebounceObject.swift in Sources */,
C6C8F94C2B21853900A9834D /* AppReviewServiceProtocol.swift in Sources */,
C66FCD0D2844FDB2009B9525 /* ConfettiImageView.swift in Sources */,
C6F6AF5928A24E2C00A7B571 /* BackToSettingsNavBarPopAnimation.swift in Sources */,
Expand Down Expand Up @@ -9894,6 +9899,7 @@
C688C1AB2B84A4CF00BD233A /* UnencryptedMessageInfoPullUpView.swift in Sources */,
C6D6477F2B1EE1DF00D724AC /* ReverseResolutionIllustrationView.swift in Sources */,
C6D645AB2B1DBB3F00D724AC /* BaseListCollectionViewCell.swift in Sources */,
C67097AD2BABD47E00BB8AB1 /* DebounceObject.swift in Sources */,
C6D6462C2B1DC2BC00D724AC /* ResizableRoundedImageView.swift in Sources */,
C630E4B72B7F58BC008F3269 /* MessageInputView.swift in Sources */,
C6D646742B1ED11B00D724AC /* DomainProfileUpdateDataRequestType.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// DebounceObject.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 21.03.2024.
//

import Combine
import SwiftUI

public final class DebounceObject: ObservableObject {
@Published var text: String = ""
@Published var debouncedText: String = ""
private var bag = Set<AnyCancellable>()

public init(dueTime: TimeInterval = 0.5) {
$text
.removeDuplicates()
.debounce(for: .seconds(dueTime), scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.debouncedText = value
})
.store(in: &bag)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,18 @@ final class DomainsGlobalSearchService {
typealias SearchProfilesTask = Task<[SearchDomainProfile], Error>
private var currentTask: SearchProfilesTask?

func searchForGlobalProfilesExcludingUsers(with searchKey: String,
walletsDataService: WalletsDataServiceProtocol) async throws -> [SearchDomainProfile] {
let profiles = try await searchForGlobalProfiles(with: searchKey)
let userDomains = walletsDataService.wallets.combinedDomains()
let userDomainsNames = Set(userDomains.map({ $0.name }))
return profiles.filter({ !userDomainsNames.contains($0.name) && $0.ownerAddress != nil })
}

func searchForGlobalProfiles(with searchKey: String) async throws -> [SearchDomainProfile] {
// Cancel previous search task if it exists
currentTask?.cancel()
let searchKey = searchKey.trimmedSpaces.lowercased()

let task: SearchProfilesTask = Task.detached {
do {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,7 @@ extension String {
static let review = "REVIEW"
static let usingMax = "USING_MAX"
static let useMax = "USE_MAX"
static let results = "RESULTS"
}

enum BlockChainIcons: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,8 @@ private extension HomeExploreViewModel {
isLoadingGlobalProfiles = true
Task {
do {
let profiles = try await searchService.searchForGlobalProfiles(with: getLowercasedTrimmedSearchKey())
let userDomains = Set(self.userDomains.map({ $0.name }))
self.globalProfiles = profiles.filter({ !userDomains.contains($0.name) && $0.ownerAddress != nil })
self.globalProfiles = try await searchService.searchForGlobalProfilesExcludingUsers(with: searchKey,
walletsDataService: walletsDataService)
}
isLoadingGlobalProfiles = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,30 @@ struct SendCryptoAssetSelectReceiverView: View, ViewAnalyticsLogger {


@Environment(\.domainProfilesService) var domainProfilesService
@Environment(\.walletsDataService) var walletsDataService
@Environment(\.presentationMode) private var presentationMode

@EnvironmentObject var viewModel: SendCryptoAssetViewModel
var analyticsName: Analytics.ViewName { .sendCryptoReceiverSelection }


@State private var userWallets: [WalletEntity] = []
@State private var followingList: [DomainName] = []
@State private var globalProfiles: [SearchDomainProfile] = []
@StateObject private var debounceObject = DebounceObject()
@State private var inputText: String = ""
@State private var isLoadingGlobalProfiles = false

@State private var socialRelationshipDetailsPublisher: AnyCancellable?
private let searchService = DomainsGlobalSearchService()

var body: some View {
List {
inputFieldView()
.listRowSeparator(.hidden)
scanQRView()
.listRowSeparator(.hidden)
userWalletsSection()
.listRowSeparator(.hidden)
followingsSection()
.listRowSeparator(.hidden)
globalSearchResultSection()
}
.listStyle(.plain)
.animation(.default, value: UUID())
Expand Down Expand Up @@ -66,13 +69,17 @@ private extension SendCryptoAssetSelectReceiverView {

@ViewBuilder
func inputFieldView() -> some View {
UDTextFieldView(text: $inputText,
UDTextFieldView(text: $debounceObject.text,
placeholder: String.Constants.domainOrAddress.localized(),
hint: String.Constants.to.localized() + ":",
rightViewType: .paste,
rightViewMode: .always,
autocapitalization: .never,
autocorrectionDisabled: true)
.onChange(of: debounceObject.debouncedText) { text in
inputText = text.lowercased().trimmedSpaces
searchForGlobalProfiles()
}
}

@ViewBuilder
Expand All @@ -92,18 +99,24 @@ private extension SendCryptoAssetSelectReceiverView {
} callback: {
viewModel.handleAction(.scanQRSelected)
}
.listRowSeparator(.hidden)
}

@ViewBuilder
func userWalletsSection() -> some View {
if !userWallets.isEmpty {
Section {
ForEach(userWallets) { wallet in
selectableUserWalletView(wallet: wallet)
if !isSearchingInProgress {
ForEach(userWallets) { wallet in
selectableUserWalletView(wallet: wallet)
}
}
} header: {
sectionHeaderViewWith(title: String.Constants.yourWallets.localized())
if !isSearchingInProgress {
sectionHeaderViewWith(title: String.Constants.yourWallets.localized())
}
}
.listRowSeparator(.hidden)
}
}

Expand All @@ -118,14 +131,15 @@ private extension SendCryptoAssetSelectReceiverView {

@ViewBuilder
func followingsSection() -> some View {
if !followingList.isEmpty {
if !followingList.isEmpty, !isSearchingInProgress {
Section {
ForEach(followingList, id: \.self) { following in
selectableFollowingView(following: following)
}
} header: {
sectionHeaderViewWith(title: String.Constants.following.localized())
}
.listRowSeparator(.hidden)
}
}

Expand All @@ -138,6 +152,77 @@ private extension SendCryptoAssetSelectReceiverView {
}
}

@ViewBuilder
func globalSearchResultSection() -> some View {
if isSearchingInProgress,
!isLoadingGlobalProfiles {
globalSearchResultOrEmptyView()
.listRowSeparator(.hidden)
}
}

enum GlobalSearchResult {
case profiles([SearchDomainProfile])
case fullAddress(HexAddress)
}

func getCurrentGlobalSearchResult() -> GlobalSearchResult? {
if !globalProfiles.isEmpty {
return .profiles(globalProfiles)
} else if inputText.isValidAddress() {
return .fullAddress(inputText)
}
return nil
}

@ViewBuilder
func globalSearchResultOrEmptyView() -> some View {
if let result = getCurrentGlobalSearchResult() {
Section {
switch result {
case .profiles(let globalProfiles):
ForEach(globalProfiles, id: \.self) { profile in
selectableGlobalProfileView(profile: profile)
}
case .fullAddress(let address):
selectableGlobalAddressRowView(address)
}
} header: {
sectionHeaderViewWith(title: String.Constants.results.localized())
}
} else {
HomeExploreEmptySearchResultView()
}
}

@ViewBuilder
func selectableGlobalAddressRowView(_ address: HexAddress) -> some View {
selectableRowView({
UDListItemView(title: address.walletAddressTruncated,
titleColor: .foregroundDefault,
subtitle: nil,
subtitleStyle: .default,
value: nil,
imageType: .image(.walletExternalIcon),
imageStyle: .centred(offset: .init(8),
foreground: .foregroundDefault,
background: .backgroundMuted2,
bordered: true),
rightViewStyle: nil)
}, callback: {
viewModel.handleAction(.globalWalletAddressSelected(address))
})
}

@ViewBuilder
func selectableGlobalProfileView(profile: SearchDomainProfile) -> some View {
selectableRowView {
DomainSearchResultProfileRowView(profile: profile)
} callback: {
viewModel.handleAction(.globalProfileSelected(profile))
}
}

@ViewBuilder
func sectionHeaderViewWith(title: String) -> some View {
HStack(spacing: 8) {
Expand All @@ -161,7 +246,32 @@ private extension SendCryptoAssetSelectReceiverView {
}
}

// MARK: - Private methods
private extension SendCryptoAssetSelectReceiverView {
var isSearchingInProgress: Bool {
!inputText.isEmpty
}

func searchForGlobalProfiles() {
guard isSearchingInProgress else {
globalProfiles = []
return
}

isLoadingGlobalProfiles = true
Task {
do {
self.globalProfiles = try await searchService.searchForGlobalProfilesExcludingUsers(with: inputText,
walletsDataService: walletsDataService)
}
isLoadingGlobalProfiles = false
}
}
}

#Preview {
SendCryptoAssetSelectReceiverView()
NavigationStack {
SendCryptoAssetSelectReceiverView()
}
.environmentObject(MockEntitiesFabric.SendCrypto.mockViewModel())
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ extension SendCryptoAsset {
case scanQRSelected
case userWalletSelected(WalletEntity)
case followingDomainSelected(DomainName)
case globalProfileSelected(SearchDomainProfile)
case globalWalletAddressSelected(HexAddress)

case userTokenSelected(BalanceTokenUIDescription)
case userDomainSelected(DomainDisplayInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ final class SendCryptoAssetViewModel: ObservableObject {
navPath.append(.selectAssetToSend)
case .followingDomainSelected(let domainName):
navPath.append(.selectAssetToSend)
case .globalProfileSelected:
navPath.append(.selectAssetToSend)
case .globalWalletAddressSelected:
navPath.append(.selectAssetToSend)
case .userTokenSelected(let token):
navPath.append(.selectTokenAmountToSend(token))
case .userDomainSelected(let domain):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,19 +477,3 @@ private extension PurchaseSearchDomainsView {
PurchaseSearchDomainsView(domainSelectedCallback: { _ in })
.environment(\.purchaseDomainsService, MockFirebaseInteractionsService())
}

public final class DebounceObject: ObservableObject {
@Published var text: String = ""
@Published var debouncedText: String = ""
private var bag = Set<AnyCancellable>()

public init(dueTime: TimeInterval = 0.5) {
$text
.removeDuplicates()
.debounce(for: .seconds(dueTime), scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.debouncedText = value
})
.store(in: &bag)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1058,3 +1058,4 @@ More tabs are coming in the next updates.";
"REVIEW" = "Review";
"USING_MAX" = "Using Max";
"USE_MAX" = "Use Max";
"RESULTS" = "Results";

0 comments on commit f941702

Please sign in to comment.