Merge pull request #71 from zeriontech/feature/better-account-selection

Better account selection
This commit is contained in:
Ivan Grachev 2022-08-23 15:27:59 +03:00 committed by GitHub
commit 2eb2e88830
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 710 additions and 116 deletions

View File

@ -55,13 +55,17 @@ struct SafariRequest {
self.name = name
self.host = host
if let favicon = json["favicon"] as? String {
if favicon.first == "/" {
if let favicon = json["favicon"] as? String, !favicon.isEmpty {
if favicon.hasPrefix("//") {
self.favicon = "https:" + favicon
} else if favicon.first == "/" {
self.favicon = "https://" + host + favicon
} else if favicon.first == "." {
self.favicon = "https://" + host + favicon.dropFirst()
} else if favicon.hasPrefix("http") {
self.favicon = favicon
} else {
self.favicon = nil
self.favicon = "https://" + host + "/" + favicon
}
} else {
self.favicon = nil
@ -88,7 +92,7 @@ struct SafariRequest {
if let request = Near(name: name, json: jsonBody) {
body = .near(request)
}
case .unknown:
case .unknown, .multiple:
if let request = Unknown(name: name, json: jsonBody) {
body = .unknown(request)
}

View File

@ -11,11 +11,47 @@ extension SafariRequest {
case switchAccount
}
struct ProviderConfiguration {
let provider: Web3Provider
let address: String
}
let method: Method
let providerConfigurations: [ProviderConfiguration]
init?(name: String, json: [String: Any]) {
guard let method = Method(rawValue: name) else { return nil }
self.method = method
var configurations = [ProviderConfiguration]()
let jsonDecoder = JSONDecoder()
if let latestConfigurations = json["latestConfigurations"] as? [[String: Any]] {
for configuration in latestConfigurations {
guard let providerString = configuration["provider"] as? String,
let provider = Web3Provider(rawValue: providerString),
let data = try? JSONSerialization.data(withJSONObject: configuration)
else { continue }
switch provider {
case .ethereum:
guard let response = try? jsonDecoder.decode(ResponseToExtension.Ethereum.self, from: data),
let address = response.results?.first else { continue }
configurations.append(ProviderConfiguration(provider: provider, address: address))
case .solana:
guard let response = try? jsonDecoder.decode(ResponseToExtension.Solana.self, from: data),
let address = response.publicKey else { continue }
configurations.append(ProviderConfiguration(provider: provider, address: address))
case .near:
guard let response = try? jsonDecoder.decode(ResponseToExtension.Near.self, from: data),
let address = response.account else { continue }
configurations.append(ProviderConfiguration(provider: provider, address: address))
case .tezos, .unknown, .multiple:
continue
}
}
}
self.providerConfigurations = configurations
}
var responseUpdatesStoredConfiguration: Bool {

View File

@ -0,0 +1,14 @@
// Copyright © 2022 Tokenary. All rights reserved.
import Foundation
extension ResponseToExtension {
struct Multiple {
let bodies: [Body]
let providersToDisconnect: [Web3Provider]
}
}

View File

@ -12,6 +12,7 @@ struct ResponseToExtension {
case solana(Solana)
case tezos(Tezos)
case near(Near)
case multiple(Multiple)
var json: [String: Any] {
let data: Data?
@ -26,9 +27,17 @@ struct ResponseToExtension {
data = try? jsonEncoder.encode(body)
case .tezos(let body):
data = try? jsonEncoder.encode(body)
case .multiple(let body):
let dict: [String: Any] = [
"bodies": body.bodies.map { $0.json },
"providersToDisconnect": body.providersToDisconnect.map { $0.rawValue },
"provider": provider.rawValue
]
return dict
}
if let data = data, let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
if let data = data, var dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
dict["provider"] = provider.rawValue
return dict
} else {
return [:]
@ -45,17 +54,16 @@ struct ResponseToExtension {
return .near
case .tezos:
return .tezos
case .multiple:
return .multiple
}
}
}
init(for request: SafariRequest, body: Body? = nil, error: String? = nil) {
self.id = request.id
let provider = (body?.provider ?? request.provider).rawValue
var json: [String: Any] = [
"id": request.id,
"provider": provider,
"name": request.name
]
@ -63,15 +71,15 @@ struct ResponseToExtension {
json["error"] = error
}
var bodyJSON = body?.json ?? [:]
let bodyJSON = body?.json ?? [:]
json.merge(bodyJSON) { (current, _) in current }
if request.body.value.responseUpdatesStoredConfiguration {
if !bodyJSON.isEmpty {
bodyJSON["provider"] = provider
if request.body.value.responseUpdatesStoredConfiguration, error == nil {
if let bodies = bodyJSON["bodies"] {
json["configurationToStore"] = bodies
} else {
json["configurationToStore"] = bodyJSON
}
json["configurationToStore"] = bodyJSON
}
self.json = json

View File

@ -3,5 +3,5 @@
import Foundation
enum Web3Provider: String, Codable {
case ethereum, solana, tezos, near, unknown
case ethereum, solana, tezos, near, unknown, multiple
}

View File

@ -2,11 +2,14 @@
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.subject === "message-to-wallet") {
browser.runtime.sendNativeMessage("mac.tokenary.io", request.message, function(response) {
sendResponse(response);
didCompleteRequest(request.message.id, sender.tab.id);
storeConfigurationIfNeeded(request.host, response);
});
if ("name" in request.message, request.message.name == "switchAccount") {
getLatestConfiguration(request.host, function(currentConfiguration) {
request.message.body = currentConfiguration;
sendNativeMessage(request, sender, sendResponse);
});
} else {
sendNativeMessage(request, sender, sendResponse);
}
} else if (request.subject === "getResponse") {
browser.runtime.sendNativeMessage("mac.tokenary.io", request, function(response) {
sendResponse(response);
@ -21,6 +24,14 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
var latestConfigurations = {};
var didReadLatestConfigurations = false;
function sendNativeMessage(request, sender, sendResponse) {
browser.runtime.sendNativeMessage("mac.tokenary.io", request.message, function(response) {
sendResponse(response);
didCompleteRequest(request.message.id, sender.tab.id);
storeConfigurationIfNeeded(request.host, response);
});
}
function respondWithLatestConfiguration(host, sendResponse) {
var response = {};
const latest = latestConfigurations[host];
@ -38,7 +49,10 @@ function respondWithLatestConfiguration(host, sendResponse) {
function storeLatestConfiguration(host, configuration) {
var latestArray = [];
if ("provider" in configuration) {
if (Array.isArray(configuration)) {
latestArray = configuration;
} else if ("provider" in configuration) {
const latest = latestConfigurations[host];
if (Array.isArray(latest)) {
@ -97,13 +111,22 @@ function storeConfigurationIfNeeded(host, response) {
}
}
function justShowApp() {
const id = genId();
const showAppMessage = {name: "justShowApp", id: id, provider: "unknown", body: {}, host: ""};
browser.runtime.sendNativeMessage("mac.tokenary.io", showAppMessage);
}
browser.browserAction.onClicked.addListener(function(tab) {
const message = {didTapExtensionButton: true};
browser.tabs.sendMessage(tab.id, message);
browser.tabs.sendMessage(tab.id, message, function(pong) {
if (pong != true) {
justShowApp();
}
});
if (tab.url == "" && tab.pendingUrl == "") {
const id = genId();
const showAppMessage = {name: "justShowApp", id: id, provider: "unknown", body: {}, host: ""};
browser.runtime.sendNativeMessage("mac.tokenary.io", showAppMessage);
justShowApp();
}
});

View File

@ -113,13 +113,13 @@ function sendMessageToNativeApp(message) {
function didTapExtensionButton() {
const id = genId();
const message = {name: "switchAccount", id: id, provider: "unknown", body: {}};
// TODO: pass current network id for ethereum. or maybe just pass latestConfiguration here as well
sendMessageToNativeApp(message);
}
// Receive from background
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if ("didTapExtensionButton" in request) {
sendResponse(true);
didTapExtensionButton();
}
});
@ -132,10 +132,18 @@ window.addEventListener("message", function(event) {
});
var getFavicon = function() {
if (document.favicon) {
return document.favicon;
}
var nodeList = document.getElementsByTagName("link");
for (var i = 0; i < nodeList.length; i++) {
if ((nodeList[i].getAttribute("rel") == "icon") || (nodeList[i].getAttribute("rel") == "shortcut icon")) {
return nodeList[i].getAttribute("href");
if ((nodeList[i].getAttribute("rel") == "apple-touch-icon") || (nodeList[i].getAttribute("rel") == "icon") || (nodeList[i].getAttribute("rel") == "shortcut icon")) {
const favicon = nodeList[i].getAttribute("href");
if (!favicon.endsWith("svg")) {
document.favicon = favicon;
return favicon;
}
}
}
return "";

File diff suppressed because one or more lines are too long

View File

@ -53,6 +53,12 @@ class TokenaryEthereum extends EventEmitter {
setTimeout( function() { window.ethereum.emit("_initialized"); }, 1);
}
externalDisconnect() {
this.setAddress("");
window.ethereum.emit("disconnect");
window.ethereum.emit("accountsChanged", []);
}
setAddress(address) {
const lowerAddress = (address || "").toLowerCase();
this.address = lowerAddress;

View File

@ -69,6 +69,30 @@ function deliverResponseToSpecificProvider(id, response, provider) {
break;
case "near":
window.near.processTokenaryResponse(id, response);
break;
case "multiple":
response.bodies.forEach((body) => {
body.id = id;
body.name = response.name;
deliverResponseToSpecificProvider(id, body, body.provider);
});
response.providersToDisconnect.forEach((provider) => {
switch (provider) {
case "ethereum":
window.ethereum.externalDisconnect();
break;
case "solana":
window.solana.externalDisconnect();
break;
case "near":
window.near.externalDisconnect();
break;
default:
break;
}
});
break;
default:
// pass unknown provider message to all providers

View File

@ -31,7 +31,13 @@ class TokenaryNear extends EventEmitter {
return this.accountId;
}
externalDisconnect() {
this.accountId = null;
this.emit("signOut");
}
signOut() {
this.accountId = null;
this.emit("signOut");
return new Promise((resolve, reject) => {
resolve(true);

View File

@ -67,9 +67,14 @@ class TokenarySolana extends EventEmitter {
return this.request(payload);
}
externalDisconnect() {
this.disconnect();
}
disconnect() {
// TODO: implement
// support also via request "disconnect" method
this.isConnected = false;
this.publicKey = null;
this.emit("disconnect");
}
signTransaction(transaction) {
@ -108,6 +113,10 @@ class TokenarySolana extends EventEmitter {
}
request(payload) {
if (payload.method == "disconnect") {
return this.disconnect();
}
this.idMapping.tryFixId(payload);
return new Promise((resolve, reject) => {
if (!payload.id) {

View File

@ -9,7 +9,7 @@ extension CoinType {
case .solana:
return "Solana"
case .ethereum:
return "Ethereum"
return "Ethereum & L2s"
case .near:
return "Near"
default:
@ -43,4 +43,19 @@ extension CoinType {
}
}
static func correspondingToWeb3Provider(_ web3Provider: Web3Provider) -> CoinType? {
switch web3Provider {
case .ethereum:
return .ethereum
case .solana:
return .solana
case .tezos:
return .tezos
case .near:
return .near
case .unknown, .multiple:
return nil
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright © 2022 Tokenary. All rights reserved.
import Foundation
import WalletCore
struct AccountSelectionConfiguration {
let peer: PeerMeta?
let coinType: CoinType?
var selectedAccounts: Set<SpecificWalletAccount>
let initiallyConnectedProviders: Set<Web3Provider>
let completion: ((EthereumChain?, [SpecificWalletAccount]?) -> Void)
}

View File

@ -14,7 +14,9 @@ enum DappRequestAction {
struct SelectAccountAction {
let provider: Web3Provider
let completion: (EthereumChain?, TokenaryWallet?, Account?) -> Void
let initiallyConnectedProviders: Set<Web3Provider>
let preselectedAccounts: [SpecificWalletAccount]
let completion: (EthereumChain?, [SpecificWalletAccount]?) -> Void
}
struct SignMessageAction {

View File

@ -23,3 +23,11 @@ struct PeerMeta {
}
}
extension SafariRequest {
var peerMeta: PeerMeta {
return PeerMeta(title: host, iconURLString: favicon)
}
}

View File

@ -0,0 +1,9 @@
// Copyright © 2022 Tokenary. All rights reserved.
import Foundation
import WalletCore
struct SpecificWalletAccount: Hashable {
let walletId: String
let account: Account
}

View File

@ -32,21 +32,44 @@ struct DappRequestProcessor {
ExtensionBridge.respond(response: ResponseToExtension(for: request))
return .justShowApp
case .switchAccount:
let action = SelectAccountAction(provider: .unknown) { chain, _, account in
if let chain = chain, let account = account {
switch account.coin {
case .ethereum:
let responseBody = ResponseToExtension.Ethereum(results: [account.address], chainId: chain.hexStringId, rpcURL: chain.nodeURLString)
respond(to: request, body: .ethereum(responseBody), completion: completion)
case .solana:
let responseBody = ResponseToExtension.Solana(publicKey: account.address)
respond(to: request, body: .solana(responseBody), completion: completion)
case .near:
let responseBody = ResponseToExtension.Near(account: account.address)
respond(to: request, body: .near(responseBody), completion: completion)
default:
fatalError("Can't select that coin")
let preselectedAccounts = body.providerConfigurations.compactMap { (configuration) -> SpecificWalletAccount? in
guard let coin = CoinType.correspondingToWeb3Provider(configuration.provider) else { return nil }
return walletsManager.getSpecificAccount(coin: coin, address: configuration.address)
}
let initiallyConnectedProviders = Set(body.providerConfigurations.map { $0.provider })
let action = SelectAccountAction(provider: .unknown,
initiallyConnectedProviders: initiallyConnectedProviders,
preselectedAccounts: preselectedAccounts) { chain, specificWalletAccounts in
if let chain = chain, let specificWalletAccounts = specificWalletAccounts {
var specificProviderBodies = [ResponseToExtension.Body]()
for specificWalletAccount in specificWalletAccounts {
let account = specificWalletAccount.account
switch account.coin {
case .ethereum:
let responseBody = ResponseToExtension.Ethereum(results: [account.address], chainId: chain.hexStringId, rpcURL: chain.nodeURLString)
specificProviderBodies.append(.ethereum(responseBody))
case .solana:
let responseBody = ResponseToExtension.Solana(publicKey: account.address)
specificProviderBodies.append(.solana(responseBody))
case .near:
let responseBody = ResponseToExtension.Near(account: account.address)
specificProviderBodies.append(.near(responseBody))
default:
fatalError("Can't select that coin")
}
}
let providersToDisconnect = initiallyConnectedProviders.filter { provider in
if let coin = CoinType.correspondingToWeb3Provider(provider),
specificWalletAccounts.contains(where: { $0.account.coin == coin }) {
return false
} else {
return true
}
}
let body = ResponseToExtension.Multiple(bodies: specificProviderBodies, providersToDisconnect: Array(providersToDisconnect))
respond(to: request, body: .multiple(body), completion: completion)
} else {
respond(to: request, error: Strings.canceled, completion: completion)
}
@ -57,15 +80,16 @@ struct DappRequestProcessor {
}
private static func process(request: SafariRequest, nearRequest body: SafariRequest.Near, completion: @escaping () -> Void) -> DappRequestAction {
let peerMeta = PeerMeta(title: request.host, iconURLString: request.favicon)
let peerMeta = request.peerMeta
lazy var account = getAccount(coin: .near, address: body.account)
lazy var privateKey = getPrivateKey(coin: .near, address: body.account)
switch body.method {
case .signIn:
let action = SelectAccountAction(provider: .near) { _, _, account in
if let account = account, account.coin == .near {
let responseBody = ResponseToExtension.Near(account: account.address)
let suggestedAccounts = walletsManager.suggestedAccounts(coin: .near)
let action = SelectAccountAction(provider: .near, initiallyConnectedProviders: Set(), preselectedAccounts: suggestedAccounts) { _, specificWalletAccounts in
if let specificWalletAccount = specificWalletAccounts?.first, specificWalletAccount.account.coin == .near {
let responseBody = ResponseToExtension.Near(account: specificWalletAccount.account.address)
respond(to: request, body: .near(responseBody), completion: completion)
} else {
respond(to: request, error: Strings.canceled, completion: completion)
@ -108,15 +132,16 @@ struct DappRequestProcessor {
}
private static func process(request: SafariRequest, solanaRequest body: SafariRequest.Solana, completion: @escaping () -> Void) -> DappRequestAction {
let peerMeta = PeerMeta(title: request.host, iconURLString: request.favicon)
let peerMeta = request.peerMeta
lazy var account = getAccount(coin: .solana, address: body.publicKey)
lazy var privateKey = getPrivateKey(coin: .solana, address: body.publicKey)
switch body.method {
case .connect:
let action = SelectAccountAction(provider: .solana) { _, _, account in
if let account = account, account.coin == .solana {
let responseBody = ResponseToExtension.Solana(publicKey: account.address)
let suggestedAccounts = walletsManager.suggestedAccounts(coin: .solana)
let action = SelectAccountAction(provider: .solana, initiallyConnectedProviders: Set(), preselectedAccounts: suggestedAccounts) { _, specificWalletAccounts in
if let specificWalletAccount = specificWalletAccounts?.first, specificWalletAccount.account.coin == .solana {
let responseBody = ResponseToExtension.Solana(publicKey: specificWalletAccount.account.address)
respond(to: request, body: .solana(responseBody), completion: completion)
} else {
respond(to: request, error: Strings.canceled, completion: completion)
@ -189,14 +214,15 @@ struct DappRequestProcessor {
}
private static func process(request: SafariRequest, ethereumRequest: SafariRequest.Ethereum, completion: @escaping () -> Void) -> DappRequestAction {
let peerMeta = PeerMeta(title: request.host, iconURLString: request.favicon)
let peerMeta = request.peerMeta
lazy var account = getAccount(coin: .ethereum, address: ethereumRequest.address)
switch ethereumRequest.method {
case .requestAccounts:
let action = SelectAccountAction(provider: .ethereum) { chain, wallet, account in
if let chain = chain, let address = wallet?.ethereumAddress, account?.coin == .ethereum {
let responseBody = ResponseToExtension.Ethereum(results: [address], chainId: chain.hexStringId, rpcURL: chain.nodeURLString)
let suggestedAccounts = walletsManager.suggestedAccounts(coin: .ethereum)
let action = SelectAccountAction(provider: .ethereum, initiallyConnectedProviders: Set(), preselectedAccounts: suggestedAccounts) { chain, specificWalletAccounts in
if let chain = chain, let specificWalletAccount = specificWalletAccounts?.first, specificWalletAccount.account.coin == .ethereum {
let responseBody = ResponseToExtension.Ethereum(results: [specificWalletAccount.account.address], chainId: chain.hexStringId, rpcURL: chain.nodeURLString)
respond(to: request, body: .ethereum(responseBody), completion: completion)
} else {
respond(to: request, error: Strings.canceled, completion: completion)

View File

@ -88,5 +88,6 @@ struct Strings {
static let data = "Data"
static let viewOnNearExplorer = "View on Near explorer"
static let sendingTransaction = "Sending a transaction"
static let disconnect = "Disconnect"
}

View File

@ -64,6 +64,7 @@ final class WalletsManager {
return wallets.first(where: { $0.id == id })
}
// TODO: deprecate
func getWallet(ethereumAddress: String) -> TokenaryWallet? {
return wallets.first(where: { $0.ethereumAddress?.lowercased() == ethereumAddress.lowercased() })
}
@ -83,6 +84,24 @@ final class WalletsManager {
}
}
func getSpecificAccount(coin: CoinType, address: String) -> SpecificWalletAccount? {
for wallet in wallets {
if let account = wallet.accounts.first(where: { $0.coin == coin && $0.address == address }) {
return SpecificWalletAccount(walletId: wallet.id, account: account)
}
}
return nil
}
func suggestedAccounts(coin: CoinType) -> [SpecificWalletAccount] {
for wallet in wallets {
if let account = wallet.accounts.first(where: { $0.coin == coin }) {
return [SpecificWalletAccount(walletId: wallet.id, account: account)]
}
}
return []
}
private func createWallet(name: String, password: String) throws -> TokenaryWallet {
let key = StoredKey(name: name, password: Data(password.utf8))
let id = makeNewWalletId()

View File

@ -78,11 +78,15 @@ class Agent: NSObject {
} else {
let accountsList = instantiate(AccountsListViewController.self)
if case let .wcSession(session) = request {
accountsList.onSelectedWallet = onSelectedWallet(session: session)
if case let .wcSession(session) = request, let completion = onSelectedWallet(session: session) {
accountsList.accountSelectionConfiguration = AccountSelectionConfiguration(peer: nil,
coinType: .ethereum,
selectedAccounts: Set(),
initiallyConnectedProviders: Set(),
completion: completion)
}
let windowController = Window.showNew(closeOthers: accountsList.onSelectedWallet == nil)
let windowController = Window.showNew(closeOthers: accountsList.accountSelectionConfiguration == nil)
windowController.contentViewController = accountsList
}
}
@ -116,7 +120,7 @@ class Agent: NSObject {
windowController.contentViewController = approveViewController
}
func getWalletSelectionCompletionIfShouldSelect() -> ((EthereumChain?, TokenaryWallet?, Account?) -> Void)? {
func getWalletSelectionCompletionIfShouldSelect() -> ((EthereumChain?, [SpecificWalletAccount]?) -> Void)? {
let session = getSessionFromPasteboard()
return onSelectedWallet(session: session)
}
@ -160,11 +164,14 @@ class Agent: NSObject {
alert.alertStyle = .warning
alert.addButton(withTitle: Strings.ok)
alert.addButton(withTitle: Strings.cancel)
if alert.runModal() == .alertFirstButtonReturn {
NSApp.terminate(nil)
}
if updateStatusBarAfterwards {
setupStatusBarItem()
DispatchQueue.main.async { [weak self] in
if alert.runModal() == .alertFirstButtonReturn {
NSApp.terminate(nil)
}
if updateStatusBarAfterwards {
self?.setupStatusBarItem()
}
}
}
@ -212,14 +219,14 @@ class Agent: NSObject {
}
}
private func onSelectedWallet(session: WCSession?) -> ((EthereumChain?, TokenaryWallet?, Account?) -> Void)? {
private func onSelectedWallet(session: WCSession?) -> ((EthereumChain?, [SpecificWalletAccount]?) -> Void)? {
guard let session = session else { return nil }
return { [weak self] chain, wallet, account in
guard let chain = chain, let wallet = wallet, account?.coin == .ethereum else {
return { [weak self] chain, specificWalletAccounts in
guard let chain = chain, let specificWalletAccount = specificWalletAccounts?.first, specificWalletAccount.account.coin == .ethereum else {
Window.closeAllAndActivateBrowser(specific: nil)
return
}
self?.connectWallet(session: session, chainId: chain.id, wallet: wallet)
self?.connectWallet(session: session, chainId: chain.id, walletId: specificWalletAccount.walletId)
}
}
@ -283,12 +290,12 @@ class Agent: NSObject {
}
}
private func connectWallet(session: WCSession, chainId: Int, wallet: TokenaryWallet) {
private func connectWallet(session: WCSession, chainId: Int, walletId: String) {
let windowController = Window.showNew(closeOthers: true)
let window = windowController.window
windowController.contentViewController = WaitingViewController.withReason(Strings.connecting)
walletConnect.connect(session: session, chainId: chainId, walletId: wallet.id) { [weak window] _ in
walletConnect.connect(session: session, chainId: chainId, walletId: walletId) { [weak window] _ in
if window?.isVisible == true {
Window.closeAllAndActivateBrowser(specific: nil)
}
@ -315,7 +322,12 @@ class Agent: NSObject {
let windowController = Window.showNew(closeOthers: closeOtherWindows)
windowNumber = windowController.window?.windowNumber
let accountsList = instantiate(AccountsListViewController.self)
accountsList.onSelectedWallet = accountAction.completion
let coinType = CoinType.correspondingToWeb3Provider(accountAction.provider)
accountsList.accountSelectionConfiguration = AccountSelectionConfiguration(peer: safariRequest.peerMeta,
coinType: coinType,
selectedAccounts: Set(accountAction.preselectedAccounts),
initiallyConnectedProviders: accountAction.initiallyConnectedProviders,
completion: accountAction.completion)
windowController.contentViewController = accountsList
case .approveMessage(let action):
let windowController = Window.showNew(closeOthers: false)

View File

@ -808,11 +808,11 @@ DQ
<objects>
<viewController storyboardIdentifier="AccountsListViewController" id="29s-Rd-OUf" customClass="AccountsListViewController" customModule="Tokenary" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="Yjc-Zm-uZY">
<rect key="frame" x="0.0" y="0.0" width="250" height="350"/>
<rect key="frame" x="0.0" y="0.0" width="250" height="412"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dkh-kG-EFj">
<rect key="frame" x="53" y="292" width="144" height="34"/>
<rect key="frame" x="53" y="354" width="144" height="34"/>
<textFieldCell key="cell" controlSize="large" enabled="NO" allowsUndo="NO" alignment="center" title="Accounts" id="9No-vQ-vBK">
<font key="font" metaFont="systemHeavy" size="29"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@ -820,7 +820,7 @@ DQ
</textFieldCell>
</textField>
<scrollView autohidesScrollers="YES" horizontalLineScroll="40" horizontalPageScroll="0.0" verticalLineScroll="40" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="7bs-Kr-ija">
<rect key="frame" x="0.0" y="0.0" width="250" height="280"/>
<rect key="frame" x="0.0" y="62" width="250" height="280"/>
<clipView key="contentView" id="RjU-hi-SHx">
<rect key="frame" x="1" y="1" width="248" height="278"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -944,6 +944,7 @@ DQ
</subviews>
</clipView>
<constraints>
<constraint firstAttribute="height" constant="280" id="wta-1a-4RU"/>
<constraint firstAttribute="width" constant="250" id="zxW-2l-wsU"/>
</constraints>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="IT1-B8-OHF">
@ -956,7 +957,7 @@ DQ
</scroller>
</scrollView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ngQ-Bn-Kwd">
<rect key="frame" x="205" y="292" width="33" height="34"/>
<rect key="frame" x="205" y="354" width="33" height="34"/>
<buttonCell key="cell" type="inline" title="+" bezelStyle="inline" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="JVh-da-a0h">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="systemBold" size="29"/>
@ -966,7 +967,7 @@ DQ
</connections>
</button>
<button hidden="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eNt-GB-Tzb">
<rect key="frame" x="17" y="294.5" width="26" height="26"/>
<rect key="frame" x="17" y="356.5" width="26" height="26"/>
<buttonCell key="cell" type="inline" bezelStyle="inline" image="globe" catalog="system" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="tZs-rG-rml">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="systemBold" size="21"/>
@ -975,32 +976,125 @@ DQ
<action selector="networkButtonTapped:" target="29s-Rd-OUf" id="JUo-sQ-bBT"/>
</connections>
</button>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="0.0" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="1000" verticalHuggingPriority="1000" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="teC-wK-5y1">
<rect key="frame" x="125" y="396" width="0.0" height="0.0"/>
<subviews>
<stackView hidden="YES" distribution="fill" orientation="horizontal" alignment="top" spacing="6" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="1000" verticalCompressionResistancePriority="1000" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="H9M-qt-ZuF">
<rect key="frame" x="0.0" y="-16" width="119" height="16"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="25J-Lu-4aq">
<rect key="frame" x="0.0" y="0.0" width="16" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="9BR-I3-Qat"/>
<constraint firstAttribute="width" constant="16" id="c28-0n-EPd"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="9eJ-C5-e6g"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Khi-cn-EGb">
<rect key="frame" x="20" y="0.0" width="101" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" title="Unknown dapp" id="CM5-MZ-VQq">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<stackView distribution="fill" orientation="horizontal" alignment="top" spacing="12" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="CWc-Db-L0d">
<rect key="frame" x="52" y="20" width="146" height="28"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="x8R-fO-ffb">
<rect key="frame" x="-6" y="-6" width="74" height="40"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" controlSize="large" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="tEk-C9-Zxh">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="didClickSecondaryButton:" target="29s-Rd-OUf" id="kaH-C7-Dx1"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sfH-TT-fxb">
<rect key="frame" x="68" y="-6" width="84" height="40"/>
<buttonCell key="cell" type="push" title="Connect" bezelStyle="rounded" alignment="center" controlSize="large" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="A0m-eU-I2u">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="didClickPrimaryButton:" target="29s-Rd-OUf" id="bQt-bZ-sLx"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="CWc-Db-L0d" secondAttribute="bottom" constant="20" id="0Ia-R2-NPx"/>
<constraint firstItem="CWc-Db-L0d" firstAttribute="width" relation="lessThanOrEqual" secondItem="Yjc-Zm-uZY" secondAttribute="width" constant="-20" id="22A-wv-4e9"/>
<constraint firstItem="ngQ-Bn-Kwd" firstAttribute="leading" secondItem="dkh-kG-EFj" secondAttribute="trailing" constant="10" id="2jH-ov-jln"/>
<constraint firstItem="7bs-Kr-ija" firstAttribute="leading" secondItem="Yjc-Zm-uZY" secondAttribute="leading" id="3n0-jB-KwV"/>
<constraint firstAttribute="trailing" secondItem="7bs-Kr-ija" secondAttribute="trailing" id="6m3-S7-WJp"/>
<constraint firstItem="teC-wK-5y1" firstAttribute="top" secondItem="Yjc-Zm-uZY" secondAttribute="top" constant="16" id="D2B-2h-top"/>
<constraint firstItem="CWc-Db-L0d" firstAttribute="centerX" secondItem="Yjc-Zm-uZY" secondAttribute="centerX" id="KMO-vQ-oM4"/>
<constraint firstAttribute="trailing" secondItem="ngQ-Bn-Kwd" secondAttribute="trailing" constant="12" id="Ln4-b5-NfT"/>
<constraint firstItem="teC-wK-5y1" firstAttribute="centerX" secondItem="Yjc-Zm-uZY" secondAttribute="centerX" id="QdA-64-M4F"/>
<constraint firstItem="ngQ-Bn-Kwd" firstAttribute="firstBaseline" secondItem="dkh-kG-EFj" secondAttribute="firstBaseline" id="RDE-f4-Heb"/>
<constraint firstItem="eNt-GB-Tzb" firstAttribute="top" secondItem="Yjc-Zm-uZY" secondAttribute="top" constant="35" id="dRD-EV-wsJ"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="teC-wK-5y1" secondAttribute="trailing" constant="16" id="c7u-4P-5OL"/>
<constraint firstAttribute="bottom" secondItem="7bs-Kr-ija" secondAttribute="bottom" constant="62" id="cFH-vX-gKa"/>
<constraint firstItem="eNt-GB-Tzb" firstAttribute="centerY" secondItem="ngQ-Bn-Kwd" secondAttribute="centerY" constant="2" id="dq4-lW-ssv"/>
<constraint firstItem="7bs-Kr-ija" firstAttribute="top" secondItem="dkh-kG-EFj" secondAttribute="bottom" constant="12" id="fXb-zL-LLv"/>
<constraint firstItem="dkh-kG-EFj" firstAttribute="top" secondItem="teC-wK-5y1" secondAttribute="bottom" constant="8" id="fig-45-iff"/>
<constraint firstItem="dkh-kG-EFj" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="eNt-GB-Tzb" secondAttribute="trailing" constant="3" id="imS-CC-tSQ"/>
<constraint firstItem="dkh-kG-EFj" firstAttribute="top" secondItem="Yjc-Zm-uZY" secondAttribute="top" constant="24" id="oaa-yR-g0U"/>
<constraint firstItem="dkh-kG-EFj" firstAttribute="centerX" secondItem="Yjc-Zm-uZY" secondAttribute="centerX" id="qoF-Dr-xWA"/>
<constraint firstAttribute="bottom" secondItem="7bs-Kr-ija" secondAttribute="bottom" id="zCE-cx-u4r"/>
<constraint firstItem="eNt-GB-Tzb" firstAttribute="leading" secondItem="Yjc-Zm-uZY" secondAttribute="leading" constant="17" id="zcY-Q0-K0f"/>
</constraints>
</view>
<connections>
<outlet property="accountsListBottomConstraint" destination="cFH-vX-gKa" id="vbK-Ea-dro"/>
<outlet property="addButton" destination="ngQ-Bn-Kwd" id="FlT-oq-C5e"/>
<outlet property="bottomButtonsStackView" destination="CWc-Db-L0d" id="RH4-Z6-Fs8"/>
<outlet property="networkButton" destination="eNt-GB-Tzb" id="cvm-05-aZj"/>
<outlet property="primaryButton" destination="sfH-TT-fxb" id="OJf-Ua-1SW"/>
<outlet property="secondaryButton" destination="x8R-fO-ffb" id="F9u-nW-BbM"/>
<outlet property="tableView" destination="glA-FK-Kdd" id="9aW-Qr-UuF"/>
<outlet property="titleLabel" destination="dkh-kG-EFj" id="xDn-bP-HWG"/>
<outlet property="titleLabelTopConstraint" destination="fig-45-iff" id="oIf-XA-wf5"/>
<outlet property="websiteLogoImageView" destination="25J-Lu-4aq" id="GrB-Xe-SUU"/>
<outlet property="websiteNameLabel" destination="Khi-cn-EGb" id="Qf9-nL-7eP"/>
<outlet property="websiteNameStackView" destination="H9M-qt-ZuF" id="598-jz-EmW"/>
</connections>
</viewController>
<customObject id="JTb-7y-Jwq" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="380" y="741"/>
<point key="canvasLocation" x="980" y="346"/>
</scene>
<!--Waiting View Controller-->
<scene sceneID="xkb-7p-mUK">
@ -1775,7 +1869,7 @@ DQ
</viewController>
<customObject id="pvA-vl-oOf" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="989" y="1177"/>
<point key="canvasLocation" x="380" y="740"/>
</scene>
</scenes>
<resources>

View File

@ -8,10 +8,9 @@ class AccountsListViewController: NSViewController {
private let agent = Agent.shared
private let walletsManager = WalletsManager.shared
private var cellModels = [CellModel]()
private var chain = EthereumChain.ethereum
private var didCallCompletion = false
var onSelectedWallet: ((EthereumChain?, TokenaryWallet?, Account?) -> Void)?
var accountSelectionConfiguration: AccountSelectionConfiguration?
var newWalletId: String?
var getBackToRect: CGRect?
@ -44,6 +43,21 @@ class AccountsListViewController: NSViewController {
}
}
@IBOutlet weak var websiteLogoImageView: NSImageView! {
didSet {
websiteLogoImageView.wantsLayer = true
websiteLogoImageView.layer?.backgroundColor = NSColor.systemGray.withAlphaComponent(0.5).cgColor
websiteLogoImageView.layer?.cornerRadius = 5
}
}
@IBOutlet weak var secondaryButton: NSButton!
@IBOutlet weak var primaryButton: NSButton!
@IBOutlet weak var bottomButtonsStackView: NSStackView!
@IBOutlet weak var accountsListBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var titleLabelTopConstraint: NSLayoutConstraint!
@IBOutlet weak var websiteNameStackView: NSStackView!
@IBOutlet weak var websiteNameLabel: NSTextField!
@IBOutlet weak var networkButton: NSButton!
@IBOutlet weak var titleLabel: NSTextField!
@IBOutlet weak var tableView: RightClickTableView! {
@ -65,9 +79,13 @@ class AccountsListViewController: NSViewController {
super.viewDidLoad()
reloadHeader()
updateBottomButtons()
updateCellModels()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(walletsChanged), name: Notification.Name.walletsChanged, object: nil)
if let preselectedAccount = accountSelectionConfiguration?.selectedAccounts.first {
scrollTo(specificWalletAccount: preselectedAccount)
}
}
override func viewDidAppear() {
@ -84,19 +102,60 @@ class AccountsListViewController: NSViewController {
Alert.showSafariPrompt()
}
private func callCompletion(wallet: TokenaryWallet?, account: Account?) {
private func callCompletion(specificWalletAccounts: [SpecificWalletAccount]?) {
if !didCallCompletion {
didCallCompletion = true
onSelectedWallet?(chain, wallet, account)
accountSelectionConfiguration?.completion(chain, specificWalletAccounts)
}
}
private func updateBottomButtons() {
if let accountSelectionConfiguration = accountSelectionConfiguration {
accountsListBottomConstraint.constant = 62
bottomButtonsStackView.isHidden = false
if !accountSelectionConfiguration.initiallyConnectedProviders.isEmpty {
primaryButton.title = Strings.ok
secondaryButton.title = Strings.disconnect
secondaryButton.keyEquivalent = ""
}
} else {
accountsListBottomConstraint.constant = 0
bottomButtonsStackView.isHidden = true
}
updatePrimaryButton()
}
private func updatePrimaryButton() {
primaryButton.isEnabled = accountSelectionConfiguration?.selectedAccounts.isEmpty == false
}
private func reloadHeader() {
let canSelectAccount = onSelectedWallet != nil && !wallets.isEmpty
let canSelectAccount = accountSelectionConfiguration != nil && !wallets.isEmpty
titleLabel.stringValue = canSelectAccount ? Strings.selectAccountTwoLines : Strings.wallets
addButton.isHidden = wallets.isEmpty
if canSelectAccount, networkButton.isHidden {
if canSelectAccount, let peer = accountSelectionConfiguration?.peer {
websiteNameLabel.stringValue = peer.name
titleLabelTopConstraint.constant = 14
websiteNameStackView.isHidden = false
if websiteLogoImageView.image == nil, let urlString = peer.iconURLString, let url = URL(string: urlString) {
websiteLogoImageView.kf.setImage(with: url) { [weak websiteLogoImageView] result in
if case .success = result {
websiteLogoImageView?.layer?.backgroundColor = NSColor.clear.cgColor
websiteLogoImageView?.layer?.cornerRadius = 0
}
}
}
} else {
titleLabelTopConstraint.constant = 8
websiteNameStackView.isHidden = true
}
let canSelectNetworkForCurrentProvider = accountSelectionConfiguration?.coinType == .ethereum || accountSelectionConfiguration?.coinType == nil
if canSelectAccount, networkButton.isHidden, canSelectNetworkForCurrentProvider {
networkButton.isHidden = false
let menu = NSMenu()
let titleItem = NSMenuItem(title: Strings.selectNetworkOptionally, action: nil, keyEquivalent: "")
@ -121,19 +180,11 @@ class AccountsListViewController: NSViewController {
menu.addItem(.separator())
menu.addItem(submenuItem)
networkButton.menu = menu
} else if !canSelectAccount, !networkButton.isHidden {
} else if !(canSelectAccount && canSelectNetworkForCurrentProvider), !networkButton.isHidden {
networkButton.isHidden = true
}
}
@objc private func didBecomeActive() {
guard view.window?.isVisible == true else { return }
if let completion = agent.getWalletSelectionCompletionIfShouldSelect() {
onSelectedWallet = completion
}
reloadHeader()
}
@IBAction func addButtonTapped(_ sender: NSButton) {
let menu = sender.menu
@ -158,6 +209,18 @@ class AccountsListViewController: NSViewController {
sender.menu?.popUp(positioning: nil, at: origin, in: view)
}
@IBAction func didClickSecondaryButton(_ sender: Any) {
if accountSelectionConfiguration?.initiallyConnectedProviders.isEmpty == false {
callCompletion(specificWalletAccounts: [])
} else {
callCompletion(specificWalletAccounts: nil)
}
}
@IBAction func didClickPrimaryButton(_ sender: Any) {
callCompletion(specificWalletAccounts: accountSelectionConfiguration?.selectedAccounts.map { $0 })
}
@objc private func didSelectChain(_ sender: AnyObject) {
guard let menuItem = sender as? NSMenuItem,
let selectedChain = EthereumChain(rawValue: menuItem.tag) else { return }
@ -217,10 +280,37 @@ class AccountsListViewController: NSViewController {
@objc private func didClickImportAccount() {
let importViewController = instantiate(ImportViewController.self)
importViewController.onSelectedWallet = onSelectedWallet
importViewController.accountSelectionConfiguration = accountSelectionConfiguration
view.window?.contentViewController = importViewController
}
private func scrollTo(specificWalletAccount: SpecificWalletAccount) {
guard let specificWalletIndex = wallets.firstIndex(where: { $0.id == specificWalletAccount.walletId }),
let specificAccountIndex = wallets[specificWalletIndex].accounts.firstIndex(where: { $0 == specificWalletAccount.account })
else { return }
let row = cellModels.firstIndex { cellModel in
switch cellModel {
case let .mnemonicAccount(walletIndex, accountIndex):
return walletIndex == specificWalletIndex && accountIndex == specificAccountIndex
case let .privateKeyAccount(walletIndex):
return walletIndex == specificWalletIndex
default:
return false
}
}
if let row = row {
tableView.scrollRowToVisible(row)
}
}
override func cancelOperation(_ sender: Any?) {
if accountSelectionConfiguration?.initiallyConnectedProviders.isEmpty == false {
callCompletion(specificWalletAccounts: nil)
}
}
private func walletForRow(_ row: Int) -> TokenaryWallet? {
guard row >= 0 else { return nil }
let item = cellModels[row]
@ -317,8 +407,10 @@ class AccountsListViewController: NSViewController {
try? walletsManager.delete(wallet: wallet)
}
@objc private func walletsChanged() {
@objc private func walletsChanged() {
validateSelectedAccounts()
reloadHeader()
updateBottomButtons()
updateCellModels()
tableView.reloadData()
}
@ -403,6 +495,38 @@ class AccountsListViewController: NSViewController {
}
}
private func validateSelectedAccounts() {
guard let specificWalletAccounts = accountSelectionConfiguration?.selectedAccounts else { return }
for specificWalletAccount in specificWalletAccounts {
if let wallet = wallets.first(where: { $0.id == specificWalletAccount.walletId }),
wallet.accounts.contains(specificWalletAccount.account) {
continue
} else {
accountSelectionConfiguration?.selectedAccounts.remove(specificWalletAccount)
}
}
}
private func didClickAccountInSelectionMode(specificWalletAccount: SpecificWalletAccount) {
let wasSelected = accountSelectionConfiguration?.selectedAccounts.contains(specificWalletAccount) == true
if !wasSelected, let toDeselect = accountSelectionConfiguration?.selectedAccounts.first(where: { $0.account.coin == specificWalletAccount.account.coin }) {
accountSelectionConfiguration?.selectedAccounts.remove(toDeselect)
}
if wasSelected {
accountSelectionConfiguration?.selectedAccounts.remove(specificWalletAccount)
} else {
accountSelectionConfiguration?.selectedAccounts.insert(specificWalletAccount)
}
updatePrimaryButton()
}
private func accountCanBeSelected(_ account: Account) -> Bool {
return accountSelectionConfiguration?.coinType == nil || accountSelectionConfiguration?.coinType == account.coin
}
}
extension AccountsListViewController: TableViewMenuSource {
@ -452,7 +576,7 @@ extension AccountsListViewController: AccountsHeaderDelegate {
guard let wallet = walletForRow(row) else { return }
let editAccountsViewController = instantiate(EditAccountsViewController.self)
editAccountsViewController.onSelectedWallet = onSelectedWallet
editAccountsViewController.accountSelectionConfiguration = accountSelectionConfiguration
editAccountsViewController.wallet = wallet
editAccountsViewController.getBackToRect = tableView.visibleRect
view.window?.contentViewController = editAccountsViewController
@ -500,12 +624,17 @@ extension AccountsListViewController: NSTableViewDelegate {
return false
}
if onSelectedWallet != nil {
callCompletion(wallet: wallet, account: account)
if accountSelectionConfiguration != nil {
if accountCanBeSelected(account) {
let specificWalletAccount = SpecificWalletAccount(walletId: wallet.id, account: account)
didClickAccountInSelectionMode(specificWalletAccount: specificWalletAccount)
tableView.reloadData()
}
return false
} else {
showMenuOnCellSelection(row: row)
return true
}
return true
}
}
@ -518,12 +647,18 @@ extension AccountsListViewController: NSTableViewDataSource {
case let .privateKeyAccount(walletIndex: walletIndex):
let wallet = wallets[walletIndex]
let rowView = tableView.makeViewOfType(AccountCellView.self, owner: self)
rowView.setup(account: wallet.accounts[0])
let account = wallet.accounts[0]
let specificWalletAccount = SpecificWalletAccount(walletId: wallet.id, account: account)
let isSelected = accountSelectionConfiguration?.selectedAccounts.contains(specificWalletAccount) == true
rowView.setup(account: account, isSelected: isSelected, isDisabled: !accountCanBeSelected(account))
return rowView
case let .mnemonicAccount(walletIndex: walletIndex, accountIndex: accountIndex):
let wallet = wallets[walletIndex]
let rowView = tableView.makeViewOfType(AccountCellView.self, owner: self)
rowView.setup(account: wallet.accounts[accountIndex])
let account = wallet.accounts[accountIndex]
let specificWalletAccount = SpecificWalletAccount(walletId: wallet.id, account: account)
let isSelected = accountSelectionConfiguration?.selectedAccounts.contains(specificWalletAccount) == true
rowView.setup(account: account, isSelected: isSelected, isDisabled: !accountCanBeSelected(account))
return rowView
case .mnemonicWalletHeader:
let rowView = tableView.makeViewOfType(AccountsHeaderRowView.self, owner: self)
@ -573,7 +708,7 @@ extension AccountsListViewController: NSMenuDelegate {
extension AccountsListViewController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
callCompletion(wallet: nil, account: nil)
callCompletion(specificWalletAccounts: nil)
}
}

View File

@ -21,6 +21,7 @@ class ApproveTransactionViewController: NSViewController {
didSet {
peerLogoImageView.wantsLayer = true
peerLogoImageView.layer?.backgroundColor = NSColor.systemGray.withAlphaComponent(0.5).cgColor
peerLogoImageView.layer?.cornerRadius = 5
}
}
@ -57,6 +58,7 @@ class ApproveTransactionViewController: NSViewController {
peerLogoImageView.kf.setImage(with: url) { [weak peerLogoImageView] result in
if case .success = result {
peerLogoImageView?.layer?.backgroundColor = NSColor.clear.cgColor
peerLogoImageView?.layer?.cornerRadius = 0
}
}
}
@ -66,6 +68,7 @@ class ApproveTransactionViewController: NSViewController {
override func viewDidAppear() {
super.viewDidAppear()
view.window?.delegate = self
view.window?.makeFirstResponder(view)
}
private func callCompletion(result: Transaction?) {

View File

@ -15,6 +15,7 @@ class ApproveViewController: NSViewController {
didSet {
peerLogoImageView.wantsLayer = true
peerLogoImageView.layer?.backgroundColor = NSColor.systemGray.withAlphaComponent(0.5).cgColor
peerLogoImageView.layer?.cornerRadius = 5
}
}
@ -50,6 +51,7 @@ class ApproveViewController: NSViewController {
peerLogoImageView.kf.setImage(with: url) { [weak peerLogoImageView] result in
if case .success = result {
peerLogoImageView?.layer?.backgroundColor = NSColor.clear.cgColor
peerLogoImageView?.layer?.cornerRadius = 0
}
}
}
@ -59,6 +61,7 @@ class ApproveViewController: NSViewController {
override func viewDidAppear() {
super.viewDidAppear()
view.window?.delegate = self
view.window?.makeFirstResponder(view)
}
func enableWaiting() {

View File

@ -7,7 +7,7 @@ class EditAccountsViewController: NSViewController {
var wallet: TokenaryWallet!
var getBackToRect: CGRect?
var onSelectedWallet: ((EthereumChain?, TokenaryWallet?, Account?) -> Void)?
var accountSelectionConfiguration: AccountSelectionConfiguration?
struct CoinDerivationCellModel {
let coinDerivation: CoinDerivation
@ -64,7 +64,7 @@ class EditAccountsViewController: NSViewController {
private func showAccountsList() {
let accountsListViewController = instantiate(AccountsListViewController.self)
accountsListViewController.onSelectedWallet = onSelectedWallet
accountsListViewController.accountSelectionConfiguration = accountSelectionConfiguration
accountsListViewController.getBackToRect = getBackToRect
view.window?.contentViewController = accountsListViewController
}

View File

@ -6,7 +6,7 @@ import WalletCore
class ImportViewController: NSViewController {
private let walletsManager = WalletsManager.shared
var onSelectedWallet: ((EthereumChain?, TokenaryWallet?, Account?) -> Void)?
var accountSelectionConfiguration: AccountSelectionConfiguration?
private var inputValidationResult = WalletsManager.InputValidationResult.invalid
@IBOutlet weak var textField: NSTextField! {
@ -62,7 +62,7 @@ class ImportViewController: NSViewController {
private func showAccountsList(newWalletId: String?) {
let accountsListViewController = instantiate(AccountsListViewController.self)
accountsListViewController.onSelectedWallet = onSelectedWallet
accountsListViewController.accountSelectionConfiguration = accountSelectionConfiguration
accountsListViewController.newWalletId = newWalletId
view.window?.contentViewController = accountsListViewController
}

View File

@ -15,9 +15,26 @@ class AccountCellView: NSTableRowView {
}
@IBOutlet weak var addressTextField: NSTextField!
func setup(account: Account) {
override func awakeFromNib() {
super.awakeFromNib()
wantsLayer = true
}
func setup(account: Account, isSelected: Bool, isDisabled: Bool) {
addressImageView.image = account.image
addressTextField.stringValue = account.croppedAddress
setSelected(isSelected)
setDisabled(isDisabled)
}
private func setDisabled(_ disabled: Bool) {
addressImageView.alphaValue = disabled ? 0.4 : 1
addressTextField.alphaValue = disabled ? 0.4 : 1
}
private func setSelected(_ selected: Bool) {
layer?.backgroundColor = (selected ? NSColor.selectedContentBackgroundColor : NSColor.clear).cgColor
addressTextField.textColor = selected ? NSColor.selectedMenuItemTextColor : NSColor.labelColor
}
func blink() {

View File

@ -71,6 +71,12 @@
2C264BE927B5AC6800234393 /* TezosResponseToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C264BE527B5AC6800234393 /* TezosResponseToExtension.swift */; };
2C264BEB27B6B50700234393 /* DappRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C264BEA27B6B50700234393 /* DappRequestProcessor.swift */; };
2C264BEC27B6B50700234393 /* DappRequestProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C264BEA27B6B50700234393 /* DappRequestProcessor.swift */; };
2C2AA1D228AD1DC100E35DBF /* SpecificWalletAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AA1D128AD1DC100E35DBF /* SpecificWalletAccount.swift */; };
2C2AA1D328AD1DC100E35DBF /* SpecificWalletAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AA1D128AD1DC100E35DBF /* SpecificWalletAccount.swift */; };
2C2AA1D528AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AA1D428AFB1AD00E35DBF /* MultipleResponseToExtension.swift */; };
2C2AA1D628AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AA1D428AFB1AD00E35DBF /* MultipleResponseToExtension.swift */; };
2C2AA1D728AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AA1D428AFB1AD00E35DBF /* MultipleResponseToExtension.swift */; };
2C2AA1D828AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2AA1D428AFB1AD00E35DBF /* MultipleResponseToExtension.swift */; };
2C3B7F022756A08600931264 /* Identifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3B7F012756A08600931264 /* Identifiers.swift */; };
2C40379428199110004C7263 /* Solana.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C40379328199110004C7263 /* Solana.swift */; };
2C40379528199110004C7263 /* Solana.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C40379328199110004C7263 /* Solana.swift */; };
@ -102,6 +108,8 @@
2C6F6D5A28273FE500D6E8FB /* EditAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6F6D5928273FE500D6E8FB /* EditAccountsViewController.swift */; };
2C6F6D5D2827434800D6E8FB /* CoinDerivationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C6F6D5B2827434800D6E8FB /* CoinDerivationTableViewCell.xib */; };
2C6F6D5E2827434800D6E8FB /* CoinDerivationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6F6D5C2827434800D6E8FB /* CoinDerivationTableViewCell.swift */; };
2C71175328AA62DE00ABBF2C /* AccountSelectionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C71175228AA62DE00ABBF2C /* AccountSelectionConfiguration.swift */; };
2C71175428AA62DE00ABBF2C /* AccountSelectionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C71175228AA62DE00ABBF2C /* AccountSelectionConfiguration.swift */; };
2C773F5E27450B97007B04E7 /* ExtensionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C773F5D27450B97007B04E7 /* ExtensionBridge.swift */; };
2C773F5F27450FBD007B04E7 /* ExtensionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C773F5D27450B97007B04E7 /* ExtensionBridge.swift */; };
2C773F62274523DC007B04E7 /* ResponseToExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C773F61274523DC007B04E7 /* ResponseToExtension.swift */; };
@ -304,6 +312,8 @@
2C264BE027B5AC6000234393 /* SolanaResponseToExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaResponseToExtension.swift; sourceTree = "<group>"; };
2C264BE527B5AC6800234393 /* TezosResponseToExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TezosResponseToExtension.swift; sourceTree = "<group>"; };
2C264BEA27B6B50700234393 /* DappRequestProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DappRequestProcessor.swift; sourceTree = "<group>"; };
2C2AA1D128AD1DC100E35DBF /* SpecificWalletAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecificWalletAccount.swift; sourceTree = "<group>"; };
2C2AA1D428AFB1AD00E35DBF /* MultipleResponseToExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleResponseToExtension.swift; sourceTree = "<group>"; };
2C3B7F012756A08600931264 /* Identifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identifiers.swift; sourceTree = "<group>"; };
2C40379328199110004C7263 /* Solana.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Solana.swift; sourceTree = "<group>"; };
2C40708E27667A6600AB3D55 /* MultilineLabelTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineLabelTableViewCell.swift; sourceTree = "<group>"; };
@ -336,6 +346,7 @@
2C6F6D5928273FE500D6E8FB /* EditAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountsViewController.swift; sourceTree = "<group>"; };
2C6F6D5B2827434800D6E8FB /* CoinDerivationTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CoinDerivationTableViewCell.xib; sourceTree = "<group>"; };
2C6F6D5C2827434800D6E8FB /* CoinDerivationTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinDerivationTableViewCell.swift; sourceTree = "<group>"; };
2C71175228AA62DE00ABBF2C /* AccountSelectionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionConfiguration.swift; sourceTree = "<group>"; };
2C74386E28297DAC00EC9304 /* near.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = near.js; path = "web3-provider/near.js"; sourceTree = "<group>"; };
2C773F5D27450B97007B04E7 /* ExtensionBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionBridge.swift; sourceTree = "<group>"; };
2C773F61274523DC007B04E7 /* ResponseToExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseToExtension.swift; sourceTree = "<group>"; };
@ -567,6 +578,7 @@
isa = PBXGroup;
children = (
2C773F61274523DC007B04E7 /* ResponseToExtension.swift */,
2C2AA1D428AFB1AD00E35DBF /* MultipleResponseToExtension.swift */,
2C264BDB27B5AC5400234393 /* EthereumResponseToExtension.swift */,
2C264BE027B5AC6000234393 /* SolanaResponseToExtension.swift */,
2C86A266282D1F220028EA11 /* NearResponseToExtension.swift */,
@ -860,7 +872,9 @@
0D059AD126C2796200EE3023 /* ApprovalSubject.swift */,
2C09FC652828331D00DE9C27 /* Image.swift */,
2C89D26727BADCA9006C0C8D /* DappRequestAction.swift */,
2C71175228AA62DE00ABBF2C /* AccountSelectionConfiguration.swift */,
0DC850E626B73A5900809E82 /* AuthenticationReason.swift */,
2C2AA1D128AD1DC100E35DBF /* SpecificWalletAccount.swift */,
);
path = Models;
sourceTree = "<group>";
@ -1319,6 +1333,7 @@
2C264BD727B5806200234393 /* Web3Provider.swift in Sources */,
2C90E61D27B2C5C100C8991E /* InternalSafariRequest.swift in Sources */,
2C86A269282D1F220028EA11 /* NearResponseToExtension.swift in Sources */,
2C2AA1D728AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */,
2C264BE327B5AC6000234393 /* SolanaResponseToExtension.swift in Sources */,
2C773F5E27450B97007B04E7 /* ExtensionBridge.swift in Sources */,
2C264BDE27B5AC5400234393 /* EthereumResponseToExtension.swift in Sources */,
@ -1338,6 +1353,7 @@
files = (
2CB4031B281C99C800BAEBEE /* AccountsHeaderRowView.swift in Sources */,
2C901C4A2689F01700D0926A /* Strings.swift in Sources */,
2C2AA1D228AD1DC100E35DBF /* SpecificWalletAccount.swift in Sources */,
2C6706A5267A6BFE006AAEF2 /* Bundle.swift in Sources */,
2CC0CDBE2692027E0072922A /* PriceService.swift in Sources */,
2C9B7735283FBA18008C191C /* UInt2x.swift in Sources */,
@ -1393,6 +1409,7 @@
2C264BE127B5AC6000234393 /* SolanaResponseToExtension.swift in Sources */,
2C264BBC27B2F25E00234393 /* SafariRequest.swift in Sources */,
2CD0B3F526A0DAA900488D92 /* NSPasteboard.swift in Sources */,
2C2AA1D528AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */,
2C264BEB27B6B50700234393 /* DappRequestProcessor.swift in Sources */,
2C264BE627B5AC6800234393 /* TezosResponseToExtension.swift in Sources */,
2C10DE4528367637001D8694 /* Near.swift in Sources */,
@ -1406,6 +1423,7 @@
2C40379428199110004C7263 /* Solana.swift in Sources */,
2C8A09DF267579EA00993638 /* AccountsListViewController.swift in Sources */,
2C917429267D2A6E00049075 /* Keychain.swift in Sources */,
2C71175328AA62DE00ABBF2C /* AccountSelectionConfiguration.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1421,6 +1439,7 @@
2CE059372763D60A0042D844 /* KeyboardObserver.swift in Sources */,
2CF255A0275A47DD00AE54B9 /* Bundle.swift in Sources */,
2CF255AC275A48CF00AE54B9 /* EthereumNetwork.swift in Sources */,
2C2AA1D328AD1DC100E35DBF /* SpecificWalletAccount.swift in Sources */,
2C40709027667A6600AB3D55 /* MultilineLabelTableViewCell.swift in Sources */,
2CF255A6275A48BB00AE54B9 /* GasService.swift in Sources */,
2C96D392276232A300687301 /* UITableView.swift in Sources */,
@ -1428,6 +1447,7 @@
2CF255B6275A746000AE54B9 /* AccountsListViewController.swift in Sources */,
2C264BCC27B2F2FF00234393 /* TezosSafariRequest.swift in Sources */,
2C96D3A42763C6A800687301 /* UIView.swift in Sources */,
2C71175428AA62DE00ABBF2C /* AccountSelectionConfiguration.swift in Sources */,
2CF25597275A46D300AE54B9 /* Defaults.swift in Sources */,
2CF255A2275A47DD00AE54B9 /* String.swift in Sources */,
2CF2559D275A479800AE54B9 /* TokenaryWallet.swift in Sources */,
@ -1485,6 +1505,7 @@
2C5FF97426C84F7B00B32ACC /* SceneDelegate.swift in Sources */,
2CF2559E275A479800AE54B9 /* WalletsManager.swift in Sources */,
2C90E62327B2ED2D00C8991E /* SafariRequest+Helpers.swift in Sources */,
2C2AA1D628AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1508,6 +1529,7 @@
2C264BD827B5806200234393 /* Web3Provider.swift in Sources */,
2C90E61E27B2C5C100C8991E /* InternalSafariRequest.swift in Sources */,
2C86A26A282D1F220028EA11 /* NearResponseToExtension.swift in Sources */,
2C2AA1D828AFB1AD00E35DBF /* MultipleResponseToExtension.swift in Sources */,
2C264BE427B5AC6000234393 /* SolanaResponseToExtension.swift in Sources */,
2CE0594327640EAB0042D844 /* ExtensionBridge.swift in Sources */,
2C264BDF27B5AC5400234393 /* EthereumResponseToExtension.swift in Sources */,

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2C19953B2674C4B900A8E370"
BuildableName = "Tokenary.app"
BlueprintName = "Tokenary"
ReferencedContainer = "container:Tokenary.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2C19953B2674C4B900A8E370"
BuildableName = "Tokenary.app"
BlueprintName = "Tokenary"
ReferencedContainer = "container:Tokenary.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2C19953B2674C4B900A8E370"
BuildableName = "Tokenary.app"
BlueprintName = "Tokenary"
ReferencedContainer = "container:Tokenary.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>