-
Notifications
You must be signed in to change notification settings - Fork 180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WCM] Detect installed wallets #991
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,15 @@ | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>LSApplicationQueriesSchemes</key> | ||
<array> | ||
<string>metamask</string> | ||
<string>trust</string> | ||
<string>safe</string> | ||
<string>zerion</string> | ||
<string>rainbow</string> | ||
<string>spot</string> | ||
</array> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just food for thought: It is possible to modify
If you want to support this list dynamically (explorer api what not) it is also possible to request them while building, smth like:
For developers, it will be enough to add the script above to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably a great way how to deal with it forward, but for now, we are sticking to simple solution and mentioning it in docs that in order to detect installed wallets they need to populate these |
||
<key>CFBundleVersion</key> | ||
<string>7</string> | ||
<key>CFBundleShortVersionString</key> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ final class ModalViewModel: ObservableObject { | |
var isShown: Binding<Bool> | ||
let interactor: ModalSheetInteractor | ||
let uiApplicationWrapper: UIApplicationWrapper | ||
let recentWalletStorage: RecentWalletsStorage | ||
|
||
@Published private(set) var destinationStack: [Destination] = [.welcome] | ||
@Published private(set) var uri: String? | ||
|
@@ -52,11 +53,11 @@ final class ModalViewModel: ObservableObject { | |
} | ||
|
||
var filteredWallets: [Listing] { | ||
if searchTerm.isEmpty { return sortByRecent(wallets) } | ||
|
||
return sortByRecent( | ||
wallets.filter { $0.name.lowercased().contains(searchTerm.lowercased()) } | ||
) | ||
wallets | ||
.sortByOrder() | ||
.sortByInstalled() | ||
.sortByRecent() | ||
.filter(searchTerm: searchTerm) | ||
} | ||
|
||
private var disposeBag = Set<AnyCancellable>() | ||
|
@@ -65,11 +66,13 @@ final class ModalViewModel: ObservableObject { | |
init( | ||
isShown: Binding<Bool>, | ||
interactor: ModalSheetInteractor, | ||
uiApplicationWrapper: UIApplicationWrapper = .live | ||
uiApplicationWrapper: UIApplicationWrapper = .live, | ||
recentWalletStorage: RecentWalletsStorage = RecentWalletsStorage() | ||
) { | ||
self.isShown = isShown | ||
self.interactor = interactor | ||
self.uiApplicationWrapper = uiApplicationWrapper | ||
self.recentWalletStorage = recentWalletStorage | ||
|
||
interactor.sessionSettlePublisher | ||
.receive(on: DispatchQueue.main) | ||
|
@@ -124,7 +127,10 @@ final class ModalViewModel: ObservableObject { | |
|
||
func onBackButton() { | ||
guard destinationStack.count != 1 else { return } | ||
_ = destinationStack.popLast() | ||
|
||
withAnimation { | ||
_ = destinationStack.popLast() | ||
} | ||
|
||
if destinationStack.last?.hasSearch == false { | ||
searchTerm = "" | ||
|
@@ -164,79 +170,104 @@ final class ModalViewModel: ObservableObject { | |
// Small deliberate delay to ensure animations execute properly | ||
try await Task.sleep(nanoseconds: 500_000_000) | ||
|
||
withAnimation { | ||
self.wallets = wallets.sorted { | ||
guard let lhs = $0.order else { | ||
return false | ||
} | ||
|
||
guard let rhs = $1.order else { | ||
return true | ||
} | ||
|
||
return lhs < rhs | ||
} | ||
|
||
loadRecentWallets() | ||
} | ||
self.wallets = wallets | ||
|
||
checkInstalledWallets() | ||
loadRecentWallets() | ||
} catch { | ||
toast = Toast(style: .error, message: error.localizedDescription) | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Recent Wallets | ||
// MARK: - Sorting and filtering | ||
|
||
private extension ModalViewModel { | ||
private extension Array where Element: Listing { | ||
|
||
func sortByRecent(_ input: [Listing]) -> [Listing] { | ||
input.sorted { lhs, rhs in | ||
guard let lhsLastTimeUsed = lhs.lastTimeUsed else { | ||
func sortByOrder() -> [Listing] { | ||
self.sorted { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as I understand every sort iterations: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, since it is in a single computed property like this, which does not sort in place but returns a new ordered sequence, it should trigger a single UI update once it orders everything. var filteredWallets: [Listing] {
wallets
.sortByOrder()
.sortByInstalled()
.sortByRecent()
.filter(searchTerm: searchTerm)
} One concern might be that we are doing unnecessary iterations since |
||
guard let lhs = $0.order else { | ||
return false | ||
} | ||
|
||
guard let rhsLastTimeUsed = rhs.lastTimeUsed else { | ||
guard let rhs = $1.order else { | ||
return true | ||
} | ||
|
||
return lhsLastTimeUsed > rhsLastTimeUsed | ||
return lhs < rhs | ||
} | ||
} | ||
|
||
func loadRecentWallets() { | ||
RecentWalletsStorage().recentWallets.forEach { wallet in | ||
func sortByInstalled() -> [Listing] { | ||
self.sorted { lhs, rhs in | ||
if lhs.installed, !rhs.installed { | ||
return true | ||
} | ||
|
||
guard let lastTimeUsed = wallet.lastTimeUsed else { | ||
return | ||
if !lhs.installed, rhs.installed { | ||
return false | ||
} | ||
|
||
// Consider Recent only for 3 days | ||
if abs(lastTimeUsed.timeIntervalSinceNow) > (24 * 60 * 60 * 3) { | ||
return | ||
return false | ||
} | ||
} | ||
|
||
func sortByRecent() -> [Listing] { | ||
self.sorted { lhs, rhs in | ||
guard let lhsLastTimeUsed = lhs.lastTimeUsed else { | ||
return false | ||
} | ||
|
||
setLastTimeUsed(wallet.id, date: lastTimeUsed) | ||
guard let rhsLastTimeUsed = rhs.lastTimeUsed else { | ||
return true | ||
} | ||
|
||
return lhsLastTimeUsed > rhsLastTimeUsed | ||
} | ||
} | ||
|
||
func saveRecentWallets() { | ||
RecentWalletsStorage().recentWallets = Array(wallets.filter { | ||
$0.lastTimeUsed != nil | ||
}.prefix(5)) | ||
func filter(searchTerm: String) -> [Listing] { | ||
if searchTerm.isEmpty { return self } | ||
|
||
return self.filter { | ||
$0.name.lowercased().contains(searchTerm.lowercased()) | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Recent & Installed Wallets | ||
|
||
private extension ModalViewModel { | ||
|
||
func setLastTimeUsed(_ walletId: String, date: Date = Date()) { | ||
guard let index = wallets.firstIndex(where: { | ||
$0.id == walletId | ||
}) else { | ||
func checkInstalledWallets() { | ||
|
||
guard let schemes = Bundle.main.object(forInfoDictionaryKey: "LSApplicationQueriesSchemes") as? [String] else { | ||
return | ||
} | ||
|
||
var copy = wallets[index] | ||
copy.lastTimeUsed = date | ||
wallets[index] = copy | ||
|
||
saveRecentWallets() | ||
wallets.forEach { | ||
if | ||
let walletScheme = $0.mobile.native, | ||
!walletScheme.isEmpty, | ||
schemes.contains(walletScheme.replacingOccurrences(of: "://", with: "")) | ||
{ | ||
$0.installed = uiApplicationWrapper.canOpenURL(URL(string: walletScheme)!) | ||
} | ||
} | ||
} | ||
|
||
func loadRecentWallets() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel that You can create a separate object, which will be responsible for storing and handling recent objects limited by capacity, and add some tests to make sure this always works as expected. But it's not mandatory, just another food for thought 😁 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved some of the logic to the |
||
recentWalletStorage.recentWallets.forEach { wallet in | ||
guard let lastTimeUsed = wallet.lastTimeUsed else { return } | ||
setLastTimeUsed(wallet.id, date: lastTimeUsed) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we already have wallet instance here, why we need to search wallet by id in setLastTimeUsed again? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The wallet instance here is the one loaded from UserDefaults, while the one I'm matching in |
||
} | ||
} | ||
|
||
func setLastTimeUsed(_ id: String, date: Date = Date()) { | ||
wallets.first { | ||
$0.id == id | ||
}?.lastTimeUsed = date | ||
recentWalletStorage.recentWallets = wallets | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,23 +9,44 @@ final class RecentWalletsStorage { | |
|
||
var recentWallets: [Listing] { | ||
get { | ||
guard | ||
let data = defaults.data(forKey: "recentWallets"), | ||
let wallets = try? JSONDecoder().decode([Listing].self, from: data) | ||
else { | ||
return [] | ||
} | ||
|
||
return wallets | ||
loadRecentWallets() | ||
} | ||
set { | ||
guard | ||
let walletsData = try? JSONEncoder().encode(newValue) | ||
else { | ||
return | ||
saveRecentWallets(newValue) | ||
} | ||
} | ||
|
||
func loadRecentWallets() -> [Listing] { | ||
guard | ||
let data = defaults.data(forKey: "recentWallets"), | ||
let wallets = try? JSONDecoder().decode([Listing].self, from: data) | ||
else { | ||
return [] | ||
} | ||
|
||
return wallets.filter { listing in | ||
guard let lastTimeUsed = listing.lastTimeUsed else { | ||
assertionFailure("Shouldn't happen we stored wallet without `lastTimeUsed`") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain why we are storing without lastTimeUsed please? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should never happen that we store it without |
||
return false | ||
} | ||
|
||
defaults.set(walletsData, forKey: "recentWallets") | ||
// Consider Recent only for 3 days | ||
return abs(lastTimeUsed.timeIntervalSinceNow) > (24 * 60 * 60 * 3) | ||
} | ||
} | ||
|
||
func saveRecentWallets(_ listings: [Listing]) { | ||
|
||
let subset = Array(listings.filter { | ||
$0.lastTimeUsed != nil | ||
}.prefix(5)) | ||
|
||
guard | ||
let walletsData = try? JSONEncoder().encode(subset) | ||
else { | ||
return | ||
} | ||
|
||
defaults.set(walletsData, forKey: "recentWallets") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ignaciosantise how did you solve this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dapps will have to modify their info.plist/android manifest manually to support the feature: https://docs.walletconnect.com/2.0/advanced/walletconnectmodal/about#for-ios