-
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 all 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,9 @@ final class ModalViewModel: ObservableObject { | |
} | ||
|
||
var filteredWallets: [Listing] { | ||
if searchTerm.isEmpty { return sortByRecent(wallets) } | ||
|
||
return sortByRecent( | ||
wallets.filter { $0.name.lowercased().contains(searchTerm.lowercased()) } | ||
) | ||
wallets | ||
.sortByRecent() | ||
.filter(searchTerm: searchTerm) | ||
} | ||
|
||
private var disposeBag = Set<AnyCancellable>() | ||
|
@@ -65,11 +64,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) | ||
|
@@ -82,7 +83,7 @@ final class ModalViewModel: ObservableObject { | |
|
||
interactor.sessionRejectionPublisher | ||
.receive(on: DispatchQueue.main) | ||
.sink { (proposal, reason) in | ||
.sink { _, reason in | ||
|
||
print(reason) | ||
self.toast = Toast(style: .error, message: reason.message) | ||
|
@@ -124,15 +125,17 @@ final class ModalViewModel: ObservableObject { | |
|
||
func onBackButton() { | ||
guard destinationStack.count != 1 else { return } | ||
_ = destinationStack.popLast() | ||
|
||
withAnimation { | ||
_ = destinationStack.popLast() | ||
} | ||
|
||
if destinationStack.last?.hasSearch == false { | ||
searchTerm = "" | ||
} | ||
} | ||
|
||
func onCopyButton() { | ||
|
||
guard let uri else { | ||
toast = Toast(style: .error, message: "No uri found") | ||
return | ||
|
@@ -164,79 +167,103 @@ 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() | ||
} | ||
loadRecentWallets() | ||
checkWhetherInstalled(wallets: wallets) | ||
|
||
self.wallets = wallets | ||
.sortByOrder() | ||
.sortByInstalled() | ||
} catch { | ||
toast = Toast(style: .error, message: error.localizedDescription) | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Recent Wallets | ||
// MARK: - Sorting and filtering | ||
|
||
private extension ModalViewModel { | ||
|
||
func sortByRecent(_ input: [Listing]) -> [Listing] { | ||
input.sorted { lhs, rhs in | ||
guard let lhsLastTimeUsed = lhs.lastTimeUsed else { | ||
private extension Array where Element: Listing { | ||
func sortByOrder() -> [Listing] { | ||
sorted { | ||
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 | ||
|
||
guard let lastTimeUsed = wallet.lastTimeUsed else { | ||
return | ||
func sortByInstalled() -> [Listing] { | ||
sorted { lhs, rhs in | ||
if lhs.installed, !rhs.installed { | ||
return true | ||
} | ||
|
||
// Consider Recent only for 3 days | ||
if abs(lastTimeUsed.timeIntervalSinceNow) > (24 * 60 * 60 * 3) { | ||
return | ||
if !lhs.installed, rhs.installed { | ||
return false | ||
} | ||
|
||
setLastTimeUsed(wallet.id, date: lastTimeUsed) | ||
return false | ||
} | ||
} | ||
|
||
func saveRecentWallets() { | ||
RecentWalletsStorage().recentWallets = Array(wallets.filter { | ||
$0.lastTimeUsed != nil | ||
}.prefix(5)) | ||
func sortByRecent() -> [Listing] { | ||
sorted { lhs, rhs in | ||
guard let lhsLastTimeUsed = lhs.lastTimeUsed else { | ||
return false | ||
} | ||
|
||
guard let rhsLastTimeUsed = rhs.lastTimeUsed else { | ||
return true | ||
} | ||
|
||
return lhsLastTimeUsed > rhsLastTimeUsed | ||
} | ||
} | ||
|
||
func setLastTimeUsed(_ walletId: String, date: Date = Date()) { | ||
guard let index = wallets.firstIndex(where: { | ||
$0.id == walletId | ||
}) else { | ||
func filter(searchTerm: String) -> [Listing] { | ||
if searchTerm.isEmpty { return self } | ||
|
||
return filter { | ||
$0.name.lowercased().contains(searchTerm.lowercased()) | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Recent & Installed Wallets | ||
|
||
private extension ModalViewModel { | ||
func checkWhetherInstalled(wallets: [Listing]) { | ||
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 | ||
} | ||
} | ||
|
||
|
@@ -248,7 +275,6 @@ protocol WalletDeeplinkHandler { | |
} | ||
|
||
extension ModalViewModel: WalletDeeplinkHandler { | ||
|
||
func openAppstore(wallet: Listing) { | ||
guard | ||
let storeLinkString = wallet.app.ios, | ||
|
@@ -277,7 +303,7 @@ extension ModalViewModel: WalletDeeplinkHandler { | |
if !success { | ||
self.toast = Toast(style: .error, message: DeeplinkErrors.failedToOpen.localizedDescription) | ||
} | ||
} | ||
} | ||
} else { | ||
throw DeeplinkErrors.noWalletLinkFound | ||
} | ||
|
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