feat(WalletConnect): Fine-tune connect dApp modal

Changes:
1. Align dialog with Figma design
2. Add new components for round image with badge and connection status tag
3. Add tests
4. Dapps service will now receive wallet `RootStore` as input and reuse existing models with account balance and other necessary info for account selection and chain selection
5. Minor updates in stores
6. Minor updates in WC toast messages to display app domain instead of app url
This commit is contained in:
Alex Jbanca 2024-06-29 23:24:05 +03:00 committed by Alex Jbanca
parent b08eb128ad
commit 2f050a025f
29 changed files with 1001 additions and 449 deletions

View File

@ -27,42 +27,17 @@ import shared.stores 1.0
Item {
id: root
function openModal() {
modal.openWithFilter([1, 42161], JSON.parse(`{
"metadata": {
"description": "React App for WalletConnect",
"icons": [
"https://avatars.githubusercontent.com/u/37784886"
],
"name": "React App",
"url": "https://react-app.walletconnect.com",
"verifyUrl": "https://verify.walletconnect.com"
},
"publicKey": "300a6a1df4cb0cd73eb652f11845f35a318541eb18ab369860be85c0c2ada54a"
}`))
if (pairedCheckbox.checked) {
pairedResultTimer.restart()
}
}
// qml Splitter
SplitView {
anchors.fill: parent
ColumnLayout {
PopupBackground {
SplitView.fillWidth: true
Component.onCompleted: root.openModal()
StatusButton {
id: openButton
Layout.alignment: Qt.AlignHCenter
Layout.margins: 20
text: "Open ConnectDAppModal"
onClicked: root.openModal()
Button {
text: "Open"
onClicked: modal.open()
anchors.centerIn: parent
}
ConnectDAppModal {
@ -70,6 +45,11 @@ Item {
anchors.centerIn: parent
modal: false
closePolicy: Popup.NoAutoClose
visible: true
spacing: 8
accounts: WalletAccountsModel {}
@ -78,9 +58,12 @@ Item {
sourceModel: NetworksModel.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: false; }
}
}
ColumnLayout {}
dAppUrl: dAppUrlField.text
dAppName: dAppNameField.text
dAppIconUrl: hasIconCheckbox.checked ? "https://avatars.githubusercontent.com/u/37784886" : ""
connectionStatus: pairedCheckbox.checked ? pairedStatusCheckbox.checked ? connectionSuccessfulStatus : connectionFailedStatus : notConnectedStatus
}
}
ColumnLayout {
@ -100,6 +83,29 @@ Item {
checked: true
}
CheckBox {
id: hasIconCheckbox
text: "Has Icon"
checked: true
}
Label {
text: "DappName"
}
TextField {
id: dAppNameField
text: "React App"
}
Label {
text: "DApp URL"
}
TextField {
id: dAppUrlField
text: "https://react-app.walletconnect.com"
}
Item { Layout.fillHeight: true }
}
}
@ -111,13 +117,7 @@ Item {
running: false
repeat: false
onTriggered: {
if (pairedCheckbox.checked) {
if (pairedStatusCheckbox.checked) {
modal.pairSuccessful(null)
} else {
modal.pairFailed(null, "Pairing failed")
}
}
}
}
}

View File

@ -0,0 +1,25 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import shared.popups.walletconnect 1.0
Item {
id: root
ConnectionStatusTag {
id: connectionTag
anchors.centerIn: parent
success: checkbox.checked
}
CheckBox {
id: checkbox
anchors.bottom: connectionTag.top
anchors.horizontalCenter: connectionTag.horizontalCenter
text: "success"
checked: false
}
}
// category: Components
// https://www.figma.com/design/HrmZp1y4S77QJezRFRl6ku/dApp-Interactions---Milestone-1?node-id=481-165960&t=oshb3aHNPCiUcQdH-0

View File

@ -18,6 +18,7 @@ import Models 1.0
import Storybook 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import AppLayouts.Wallet.services.dapps 1.0
import SortFilterProxyModel 0.2
@ -314,13 +315,18 @@ Item {
}
}
walletStore: WalletStore {
property var flatNetworks: SortFilterProxyModel {
walletRootStore: WalletStores.RootStore {
property string selectedAddress: ""
property var filteredFlatModel: SortFilterProxyModel {
sourceModel: NetworksModel.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; }
}
property var accounts: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel
readonly property ListModel ownAccounts: accounts
readonly property ListModel nonWatchAccounts: accounts
function getNetworkShortNames(chainIds) {
return "eth:oeth:arb"
}
}
onDisplayToastMessage: (message, isErr) => {

View File

@ -0,0 +1,52 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import shared.popups.walletconnect 1.0
SplitView {
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
RoundImageWithBadge {
id: roundImageWithBadge
width: parent.width
height: width
imageUrl: addressField.text
badgeIcon: badgeField.text
fallbackIcon: fallbackIconField.text
}
}
Pane {
id: controlsPane
SplitView.fillHeight: true
SplitView.preferredWidth: 300
ColumnLayout {
Label { text: "Image url" }
TextField {
id: addressField
text: "https://picsum.photos/200/200"
Layout.fillWidth: true
}
Label { text: "Badge name" }
TextField {
id: badgeField
text: "walletConnect"
Layout.fillWidth: true
}
Label { text: "Fallback icon name" }
TextField {
id: fallbackIconField
text: "dapp"
Layout.fillWidth: true
}
}
}
}
// category: Components
// https://www.figma.com/design/HrmZp1y4S77QJezRFRl6ku/dApp-Interactions---Milestone-1?node-id=481-160233&t=xyix3QX5I3jxrDir-0

View File

@ -0,0 +1,319 @@
import QtQuick 2.15
import QtTest 1.15
import shared.popups.walletconnect 1.0
import Models 1.0
Item {
id: root
width: 600
height: 800
Component {
id: accountsModelComponent
WalletAccountsModel {}
}
Component {
id: componentUnderTest
ConnectDAppModal {
id: controlUnderTest
modal: false
anchors.centerIn: parent
accounts: WalletAccountsModel {}
flatNetworks: NetworksModel.flatNetworks
}
}
TestCase {
id: testConnectDappModal
name: "ConnectDappModalTest"
when: windowShown
SignalSpy {
id: connectSignalSpy
target: testConnectDappModal.dappModal
signalName: "connect"
}
SignalSpy {
id: declineSignalSpy
target: testConnectDappModal.dappModal
signalName: "decline"
}
SignalSpy {
id: disconnectSignalSpy
target: testConnectDappModal.dappModal
signalName: "disconnect"
}
property ConnectDAppModal dappModal: null
function cleanup() {
connectSignalSpy.clear()
declineSignalSpy.clear()
disconnectSignalSpy.clear()
}
function test_initialState() {
dappModal = createTemporaryObject(componentUnderTest, root, {visible: true})
verify(dappModal.visible, "ConnectDAppModal should be visible")
verify(dappModal.accounts, "ConnectDAppModal should have accounts")
verify(dappModal.flatNetworks, "ConnectDAppModal should have networks")
compare(dappModal.width, 480)
compare(dappModal.height, 633)
compare(dappModal.dAppName, "")
compare(dappModal.dAppUrl, "")
compare(dappModal.dAppIconUrl, "")
compare(dappModal.connectionStatus, dappModal.notConnectedStatus)
}
function test_notConnectedState() {
dappModal = createTemporaryObject(componentUnderTest, root, {visible: true})
compare(dappModal.connectionStatus, dappModal.notConnectedStatus)
// Reject button should be enabled
const rejectButton = findChild(dappModal, "rejectButton")
verify(rejectButton, "Reject button should be present")
compare(rejectButton.text, "Reject")
compare(rejectButton.enabled, true)
mouseClick(rejectButton)
compare(declineSignalSpy.count, 1)
// Connect button should be enabled
const connectButton = findChild(dappModal, "primaryActionButton")
verify(connectButton, "Connect button should be present")
compare(connectButton.text, "Connect")
compare(connectButton.enabled, true)
mouseClick(connectButton)
compare(connectSignalSpy.count, 1)
// Disconnect button should be disabled
const disconnectButton = findChild(dappModal, "disconnectButton")
verify(disconnectButton, "Disconnect button should be present")
compare(disconnectButton.text, "Disconnect")
compare(disconnectButton.visible, false)
mouseClick(disconnectButton)
compare(disconnectSignalSpy.count, 0)
// Account selector should be enabled and user should be able to select an account
const accountSelector = findChild(dappModal, "accountSelector")
verify(accountSelector, "Account selector should be present")
compare(accountSelector.enabled, true)
compare(accountSelector.currentIndex, 0)
mouseClick(accountSelector)
compare(accountSelector.popup.visible, true)
waitForItemPolished(accountSelector.popup.contentItem)
const accountsList = findChild(accountSelector, "accountSelectorList")
verify(accountsList, "Accounts list should be present")
const selectAddress = accountsList.itemAtIndex(1).address
mouseClick(accountsList.itemAtIndex(1))
compare(dappModal.selectedAccountAddress, accountSelector.currentAccountAddress)
compare(dappModal.selectedAccountAddress, selectAddress)
// Chain selector is enabled, all common chains preselected
const chainSelector = findChild(dappModal, "networkFilter")
verify(chainSelector, "Chain selector should be present")
compare(chainSelector.enabled, true)
compare(chainSelector.selection.length, NetworksModel.flatNetworks.count)
compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count)
// User should be able to deselect a chain
mouseClick(chainSelector)
waitForItemPolished(chainSelector)
const networkSelectorList = findChild(chainSelector, "networkSelectorList")
verify(networkSelectorList, "Network selector list should be present")
mouseClick(networkSelectorList.itemAtIndex(0))
compare(chainSelector.selection.length, NetworksModel.flatNetworks.count - 1)
compare(chainSelector.selection[0], NetworksModel.flatNetworks.get(1).chainId)
compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count - 1)
compare(dappModal.selectedChains[0], NetworksModel.flatNetworks.get(1).chainId)
}
function test_connectedState() {
dappModal = createTemporaryObject(componentUnderTest, root, {visible: true})
dappModal.pairSuccessful()
compare(dappModal.connectionStatus, dappModal.connectionSuccessfulStatus)
// Reject button should not be visible
const rejectButton = findChild(dappModal, "rejectButton")
verify(rejectButton, "Reject button should be present")
compare(rejectButton.visible, false)
mouseClick(rejectButton)
compare(declineSignalSpy.count, 0)
// Close button should be enabled
const closeButton = findChild(dappModal, "primaryActionButton")
verify(closeButton, "Close button should be present")
compare(closeButton.text, "Close")
compare(closeButton.enabled, true)
compare(closeButton.visible, true)
mouseClick(closeButton)
compare(dappModal.opened, false)
dappModal.open()
// Disconnect button should be enabled
const disconnectButton = findChild(dappModal, "disconnectButton")
verify(disconnectButton, "Disconnect button should be present")
compare(disconnectButton.text, "Disconnect")
compare(disconnectButton.visible, true)
compare(disconnectButton.enabled, true)
mouseClick(disconnectButton)
compare(disconnectSignalSpy.count, 1)
// Account selector should be disabled
const accountSelector = findChild(dappModal, "accountSelector")
verify(accountSelector, "Account selector should be present")
compare(accountSelector.currentIndex, 0)
mouseClick(accountSelector)
compare(accountSelector.popup.visible, false)
// Chain selector is disabled
const chainSelector = findChild(dappModal, "networkFilter")
verify(chainSelector, "Chain selector should be present")
compare(chainSelector.selection.length, NetworksModel.flatNetworks.count)
// User should not be able to deselect a chain
mouseClick(chainSelector)
waitForItemPolished(chainSelector)
const networkSelectorList = findChild(chainSelector, "networkSelectorList")
verify(networkSelectorList, "Network selector list should be present")
mouseClick(networkSelectorList.itemAtIndex(0))
compare(chainSelector.selection.length, NetworksModel.flatNetworks.count)
compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count)
const connectionTag = findChild(dappModal, "connectionStatusTag")
compare(connectionTag.visible, true)
compare(connectionTag.success, true)
}
function test_connectionFailedState() {
dappModal = createTemporaryObject(componentUnderTest, root, {visible: true})
dappModal.pairFailed()
compare(dappModal.connectionStatus, dappModal.connectionFailedStatus)
// Reject button should not be visible
const rejectButton = findChild(dappModal, "rejectButton")
verify(rejectButton, "Reject button should be present")
compare(rejectButton.visible, false)
mouseClick(rejectButton)
compare(declineSignalSpy.count, 0)
// Close button should be enabled
const closeButton = findChild(dappModal, "primaryActionButton")
verify(closeButton, "Close button should be present")
compare(closeButton.text, "Close")
compare(closeButton.enabled, true)
compare(closeButton.visible, true)
mouseClick(closeButton)
compare(dappModal.opened, false)
dappModal.open()
// Disconnect button should not be visible
const disconnectButton = findChild(dappModal, "disconnectButton")
verify(disconnectButton, "Disconnect button should be present")
compare(disconnectButton.text, "Disconnect")
compare(disconnectButton.visible, false)
mouseClick(disconnectButton)
compare(disconnectSignalSpy.count, 0)
// Account selector should be disabled
const accountSelector = findChild(dappModal, "accountSelector")
verify(accountSelector, "Account selector should be present")
compare(accountSelector.currentIndex, 0)
mouseClick(accountSelector)
compare(accountSelector.popup.visible, false)
// Chain selector is disabled
const chainSelector = findChild(dappModal, "networkFilter")
verify(chainSelector, "Chain selector should be present")
compare(chainSelector.selection.length, NetworksModel.flatNetworks.count)
// User should not be able to deselect a chain
mouseClick(chainSelector)
waitForItemPolished(chainSelector)
const networkSelectorList = findChild(chainSelector, "networkSelectorList")
verify(networkSelectorList, "Network selector list should be present")
mouseClick(networkSelectorList.itemAtIndex(0))
compare(chainSelector.selection.length, NetworksModel.flatNetworks.count)
compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count)
const connectionTag = findChild(dappModal, "connectionStatusTag")
compare(connectionTag.visible, true)
compare(connectionTag.success, false)
}
function test_selectingAccount() {
dappModal = createTemporaryObject(componentUnderTest, root, {visible: true, dAppChains: [1, 11155111]})
const accountSelector = findChild(dappModal, "accountSelector")
verify(accountSelector, "Account selector should be present")
compare(accountSelector.currentIndex, 0)
mouseClick(accountSelector)
compare(accountSelector.popup.visible, true)
waitForItemPolished(accountSelector.popup.contentItem)
const accountsList = findChild(accountSelector, "accountSelectorList")
verify(accountsList, "Accounts list should be present")
compare(accountsList.count, dappModal.accounts.count)
const selectAddress = accountsList.itemAtIndex(1).address
mouseClick(accountsList.itemAtIndex(1))
compare(dappModal.selectedAccountAddress, accountSelector.currentAccountAddress)
compare(dappModal.selectedAccountAddress, selectAddress)
const preselectedAddress = accountSelector.currentAccountAddress
mouseClick(accountSelector)
compare(accountSelector.popup.visible, true)
waitForItemPolished(accountSelector.popup.contentItem)
const selectAddress1 = accountsList.itemAtIndex(0).address
mouseClick(accountsList.itemAtIndex(0))
compare(dappModal.selectedAccountAddress, accountSelector.currentAccountAddress)
compare(dappModal.selectedAccountAddress, selectAddress1)
// Use preselected address
dappModal.selectedAccountAddress = preselectedAddress
compare(accountSelector.currentAccountAddress, preselectedAddress)
}
function test_chainSelection() {
dappModal = createTemporaryObject(componentUnderTest, root, {visible: true})
const chainSelector = findChild(dappModal, "networkFilter")
verify(chainSelector, "Chain selector should be present")
// All selected
compare(chainSelector.selection.length, NetworksModel.flatNetworks.count)
compare(chainSelector.selection[0], NetworksModel.flatNetworks.get(0).chainId)
compare(chainSelector.selection[1], NetworksModel.flatNetworks.get(1).chainId)
// User should be able to deselect a chain
mouseClick(chainSelector)
waitForItemPolished(chainSelector)
const networkSelectorList = findChild(chainSelector, "networkSelectorList")
verify(networkSelectorList, "Network selector list should be present")
mouseClick(networkSelectorList.itemAtIndex(0))
compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count - 1)
compare(dappModal.selectedChains[0], NetworksModel.flatNetworks.get(1).chainId)
waitForItemPolished(networkSelectorList)
mouseClick(networkSelectorList.itemAtIndex(1))
compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count - 2)
compare(dappModal.selectedChains[0], NetworksModel.flatNetworks.get(2).chainId)
const connectButton = findChild(dappModal, "primaryActionButton")
verify(!!connectButton, "Connect button should be present")
compare(connectButton.visible, true)
compare(connectButton.enabled, true)
}
}
}

View File

@ -14,6 +14,7 @@ import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import AppLayouts.Profile.stores 1.0
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import shared.stores 1.0
@ -115,8 +116,9 @@ Item {
Component {
id: walletStoreComponent
WalletStore {
readonly property ListModel flatNetworks: ListModel {
WalletStores.RootStore {
property string selectedAddress: ""
readonly property ListModel filteredFlatModel: ListModel {
ListElement { chainId: 1 }
ListElement {
chainId: 2
@ -125,7 +127,7 @@ Item {
}
}
readonly property ListModel accounts: ListModel {
readonly property ListModel nonWatchAccounts: ListModel {
ListElement {address: "0x1"}
ListElement {
address: "0x2"
@ -135,9 +137,9 @@ Item {
}
ListElement { address: "0x3a" }
}
readonly property ListModel ownAccounts: accounts
function getNetworkShortNames(chainIds) {
return "eth:oeth:arb"
}
}
}
@ -167,7 +169,12 @@ Item {
verify(!!sdk)
let store = createTemporaryObject(dappsStoreComponent, root)
verify(!!store)
handler = createTemporaryObject(dappsRequestHandlerComponent, root, {sdk: sdk, store: store, walletStore: walletStore})
handler = createTemporaryObject(dappsRequestHandlerComponent, root, {
sdk: sdk,
store: store,
accountsModel: walletStore.nonWatchAccounts,
networksModel: walletStore.filteredFlatModel
})
verify(!!handler)
}
@ -176,7 +183,7 @@ Item {
}
function test_TestAuthentication() {
let td = mockSessionRequestEvent(this, handler.sdk, handler.walletStore)
let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel)
handler.authenticate(td.request)
compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser")
@ -239,7 +246,7 @@ Item {
verify(!!sdk)
let store = createTemporaryObject(dappsStoreComponent, root)
verify(!!store)
service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletStore: walletStore})
service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletRootStore: walletStore})
verify(!!service)
}
@ -251,7 +258,7 @@ Item {
function test_TestPairing() {
// All calls to SDK are expected as events to be made by the wallet connect SDK
let sdk = service.wcSDK
let walletStore = service.walletStore
let walletStore = service.walletRootStore
let store = service.store
service.pair("wc:12ab@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=12ab")
@ -262,12 +269,12 @@ Item {
var args = sdk.buildApprovedNamespacesCalls[0]
verify(!!args.supportedNamespaces, "expected supportedNamespaces to be set")
let chainsForApproval = args.supportedNamespaces.eip155.chains
let networksArray = ModelUtils.modelToArray(walletStore.flatNetworks).map(entry => entry.chainId)
let networksArray = ModelUtils.modelToArray(walletStore.filteredFlatModel).map(entry => entry.chainId)
verify(networksArray.every(chainId => chainsForApproval.some(eip155Chain => eip155Chain === `eip155:${chainId}`)),
"expect all the networks to be present")
// We test here all accounts for one chain only, we have separate tests to validate that all accounts are present
let allAccountsForApproval = args.supportedNamespaces.eip155.accounts
let accountsArray = ModelUtils.modelToArray(walletStore.accounts).map(entry => entry.address)
let accountsArray = ModelUtils.modelToArray(walletStore.nonWatchAccounts).map(entry => entry.address)
verify(accountsArray.every(address => allAccountsForApproval.some(eip155Address => eip155Address === `eip155:${networksArray[0]}:${address}`)),
"expect at least all accounts for the first chain to be present"
)
@ -276,11 +283,11 @@ Item {
sdk.buildApprovedNamespacesResult(allApprovedNamespaces, "")
compare(connectDAppSpy.count, 1, "expected a call to service.connectDApp")
let connectArgs = connectDAppSpy.signalArguments[0]
compare(connectArgs[connectDAppSpy.argPos.dappChains], networksArray, "expected all provided networks (walletStore.flatNetworks) for the dappChains")
compare(connectArgs[connectDAppSpy.argPos.dappChains], networksArray, "expected all provided networks (walletStore.filteredFlatModel) for the dappChains")
verify(!!connectArgs[connectDAppSpy.argPos.sessionProposalJson], "expected sessionProposalJson to be set")
verify(!!connectArgs[connectDAppSpy.argPos.availableNamespaces], "expected availableNamespaces to be set")
let selectedAccount = walletStore.accounts.get(1)
let selectedAccount = walletStore.nonWatchAccounts.get(1)
service.approvePairSession(connectArgs[connectDAppSpy.argPos.sessionProposalJson], connectArgs[connectDAppSpy.argPos.dappChains], selectedAccount)
compare(sdk.buildApprovedNamespacesCalls.length, 2, "expected a call to sdk.buildApprovedNamespaces")
args = sdk.buildApprovedNamespacesCalls[1]
@ -314,7 +321,7 @@ Item {
function test_SessionRequestMainFlow() {
// All calls to SDK are expected as events to be made by the wallet connect SDK
let sdk = service.wcSDK
let walletStore = service.walletStore
let walletStore = service.walletRootStore
let store = service.store
let testAddress = "0x3a"
@ -519,7 +526,7 @@ Item {
verify(!!sdk)
let store = createTemporaryObject(dappsStoreComponent, root)
verify(!!store)
let service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletStore: walletStore})
let service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletRootStore: walletStore})
verify(!!service)
controlUnderTest = createTemporaryObject(componentUnderTest, root, {wcService: service})
verify(!!controlUnderTest)
@ -581,7 +588,7 @@ Item {
waitForRendering(controlUnderTest)
let service = controlUnderTest.wcService
let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore)
let td = mockSessionRequestEvent(this, service.wcSDK, service.walletRootStore.nonWatchAccounts, service.walletRootStore.filteredFlatModel)
waitForRendering(controlUnderTest)
let popup = findChild(controlUnderTest, "dappsRequestModal")
@ -604,7 +611,7 @@ Item {
waitForRendering(controlUnderTest)
let service = controlUnderTest.wcService
let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore)
let td = mockSessionRequestEvent(this, service.wcSDK, service.walletRootStore.nonWatchAccounts, service.walletRootStore.filteredFlatModel)
waitForRendering(controlUnderTest)
let popup = findChild(controlUnderTest, "dappsRequestModal")
@ -625,9 +632,9 @@ Item {
}
}
function mockSessionRequestEvent(tc, sdk, walletStore) {
let account = walletStore.accounts.get(1)
let network = walletStore.flatNetworks.get(1)
function mockSessionRequestEvent(tc, sdk, accountsModel, networksMdodel) {
let account = accountsModel.get(1)
let network = networksMdodel.get(1)
let method = "personal_sign"
let message = "hello world"
let params = [Helpers.strToHex(message), account.address]

View File

@ -0,0 +1,5 @@
import QtQml 2.15
QtObject {
id: root
}

View File

@ -1,5 +1,6 @@
CollectiblesStore 1.0 CollectiblesStore.qml
WalletAssetsStore 1.0 WalletAssetsStore.qml
TokensStore 1.0 TokensStore.qml
ActivityFiltersStore 1.0 ActivityFiltersStore.qml
CollectiblesStore 1.0 CollectiblesStore.qml
RootStore 1.0 RootStore.qml
SwapStore 1.0 SwapStore.qml
TokensStore 1.0 TokensStore.qml
WalletAssetsStore 1.0 WalletAssetsStore.qml

View File

@ -322,6 +322,7 @@
<file>assets/img/icons/username.svg</file>
<file>assets/img/icons/view.svg</file>
<file>assets/img/icons/wallet.svg</file>
<file>assets/img/icons/walletConnect.svg</file>
<file>assets/img/icons/warning.svg</file>
<file>assets/img/icons/youtube.svg</file>
<file>assets/img/status-logo-icon.png</file>

View File

@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="22" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4.5H7.69231C6.98275 4.5 6.8299 4.50619 6.72962 4.52457C6.1166 4.63691 5.63691 5.1166 5.52457 5.72962C5.50619 5.8299 5.5 5.98275 5.5 6.69231V8.25C5.5 8.66421 5.16421 9 4.75 9C4.33579 9 4 8.66421 4 8.25V6.69231C4 6.04903 4 5.72738 4.04914 5.45923C4.27382 4.2332 5.2332 3.27382 6.45923 3.04914C6.72738 3 7.04903 3 7.69231 3H11C15.9706 3 20 7.02944 20 12C20 16.9706 15.9706 21 11 21H7.69231C7.04903 21 6.72738 21 6.45923 20.9509C5.2332 20.7262 4.27382 19.7668 4.04914 18.5408C4 18.2726 4 17.951 4 17.3077V15.75C4 15.3358 4.33579 15 4.75 15C5.16421 15 5.5 15.3358 5.5 15.75V17.3077C5.5 18.0172 5.50619 18.1701 5.52457 18.2704C5.63691 18.8834 6.1166 19.3631 6.72962 19.4754C6.8299 19.4938 6.98275 19.5 7.69231 19.5H11C15.1421 19.5 18.5 16.1421 18.5 12C18.5 7.85786 15.1421 4.5 11 4.5Z" fill="black"/>
<path d="M3 11.25C2.58579 11.25 2.25 11.5858 2.25 12C2.25 12.4142 2.58579 12.75 3 12.75H11C11.4142 12.75 11.75 12.4142 11.75 12C11.75 11.5858 11.4142 11.25 11 11.25H3Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg fill="none" height="400" viewBox="0 0 400 400" width="400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m0 0h400v400h-400z"/></clipPath><g clip-path="url(#a)"><circle cx="200" cy="200" fill="#3396ff" r="199.5" stroke="#66b1ff"/><path d="m122.519 148.965c42.791-41.729 112.171-41.729 154.962 0l5.15 5.022c2.14 2.086 2.14 5.469 0 7.555l-17.617 17.18c-1.07 1.043-2.804 1.043-3.874 0l-7.087-6.911c-29.853-29.111-78.253-29.111-108.106 0l-7.59 7.401c-1.07 1.043-2.804 1.043-3.874 0l-17.617-17.18c-2.14-2.086-2.14-5.469 0-7.555zm191.397 35.529 15.679 15.29c2.14 2.086 2.14 5.469 0 7.555l-70.7 68.944c-2.139 2.087-5.608 2.087-7.748 0l-50.178-48.931c-.535-.522-1.402-.522-1.937 0l-50.178 48.931c-2.139 2.087-5.608 2.087-7.748 0l-70.7015-68.945c-2.1396-2.086-2.1396-5.469 0-7.555l15.6795-15.29c2.1396-2.086 5.6085-2.086 7.7481 0l50.1789 48.932c.535.522 1.402.522 1.937 0l50.177-48.932c2.139-2.087 5.608-2.087 7.748 0l50.179 48.932c.535.522 1.402.522 1.937 0l50.179-48.931c2.139-2.087 5.608-2.087 7.748 0z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -43,13 +43,27 @@ QtObject {
property var originModel: accountsModule.keyPairModel
property var ownAccounts: SortFilterProxyModel {
sourceModel: root.accounts
proxyRoles: FastExpressionRole {
name: "preferredSharingChainShortNames"
expression: {
return root.networksModuleInst.getNetworkShortNames(model.preferredSharingChainIds)
proxyRoles: [
FastExpressionRole {
name: "preferredSharingChainShortNames"
expression: {
return root.networksModuleInst.getNetworkShortNames(model.preferredSharingChainIds)
}
expectedRoles: ["preferredSharingChainIds"]
},
FastExpressionRole {
name: "color"
function getColor(colorId) {
return Utils.getColorForId(colorId)
}
// Direct call for singleton function is not handled properly by
// SortFilterProxyModel that's why helper function is used instead.
expression: { return getColor(model.colorId) }
expectedRoles: ["colorId"]
}
expectedRoles: ["preferredSharingChainIds"]
}
]
filters: ValueFilter {
roleName: "walletType"
value: Constants.watchWalletType

View File

@ -2,6 +2,9 @@ import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import SortFilterProxyModel 0.2
import AppLayouts.Wallet.controls 1.0
import shared.popups.walletconnect 1.0
@ -51,19 +54,34 @@ DappsComboBox {
active: false
onLoaded: item.openWithFilter(dappChains, sessionProposal.params.proposer)
property var dappChains: []
property var sessionProposal: null
property var availableNamespaces: null
property var sessionTopic: null
readonly property var proposalMedatada: !!sessionProposal
? sessionProposal.params.proposer.metadata
: { name: "", url: "", icons: [] }
sourceComponent: ConnectDAppModal {
visible: true
onClosed: connectDappLoader.active = false
accounts: root.wcService.validAccounts
flatNetworks: root.wcService.flatNetworks
flatNetworks: SortFilterProxyModel {
sourceModel: root.wcService.flatNetworks
filters: [
FastExpressionFilter {
inverted: true
expression: connectDappLoader.dappChains.indexOf(chainId) === -1
expectedRoles: ["chainId"]
}
]
}
selectedAccountAddress: root.wcService.selectedAccountAddress
dAppUrl: proposalMedatada.url
dAppName: proposalMedatada.name
dAppIconUrl: !!proposalMedatada.icons && proposalMedatada.icons.length > 0 ? proposalMedatada.icons[0] : ""
onConnect: {
root.wcService.approvePairSession(sessionProposal, selectedChains, selectedAccount)
@ -171,9 +189,6 @@ DappsComboBox {
}
function onApproveSessionResult(session, err) {
connectDappLoader.dappChains = []
connectDappLoader.sessionProposal = null
connectDappLoader.availableNamespaces = null
connectDappLoader.sessionTopic = session.topic
let modal = connectDappLoader.item

View File

@ -14,8 +14,9 @@ QObject {
id: root
required property WalletConnectSDKBase sdk
required property var walletStore
required property DAppsStore store
required property var accountsModel
required property var networksModel
property alias requestsModel: requests
@ -189,8 +190,8 @@ QObject {
}
address = event.params.request.params[0].from
}
return ModelUtils.getFirstModelEntryIf(walletStore.ownAccounts, (account) => {
return account.address.toLowerCase() === address.toLowerCase()
return ModelUtils.getFirstModelEntryIf(root.accountsModel, (account) => {
return account.address.toLowerCase() === address.toLowerCase();
})
}
@ -200,13 +201,7 @@ QObject {
return null
}
let chainId = Helpers.chainIdFromEip155(event.params.chainId)
for (let i = 0; i < walletStore.flatNetworks.count; i++) {
let network = ModelUtils.get(walletStore.flatNetworks, i)
if (network.chainId === chainId) {
return network
}
}
return null
return ModelUtils.getByKey(root.networksModel, "chainId", chainId)
}
function extractMethodData(event, method) {

View File

@ -5,6 +5,7 @@ import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import AppLayouts.Wallet 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import AppLayouts.Profile.stores 1.0
@ -21,23 +22,20 @@ QObject {
required property WalletConnectSDKBase wcSDK
required property DAppsStore store
required property WalletStore walletStore
required property WalletStores.RootStore walletRootStore
readonly property string selectedAccountAddress: walletRootStore.selectedAddress
readonly property alias dappsModel: dappsProvider.dappsModel
readonly property alias requestHandler: requestHandler
readonly property var validAccounts: SortFilterProxyModel {
sourceModel: root.walletStore ? root.walletStore.accounts : null
filters: ValueFilter {
roleName: "walletType"
value: Constants.watchWalletType
inverted: true
}
sourceModel: root.walletRootStore.nonWatchAccounts
proxyRoles: [
FastExpressionRole {
name: "colorizedChainPrefixes"
function getChainShortNames(chainIds) {
const chainShortNames = root.walletStore.getNetworkShortNames(chainIds)
const chainShortNames = root.walletRootStore.getNetworkShortNames(chainIds)
return WalletUtils.colorizedChainPrefix(chainShortNames)
}
expression: getChainShortNames(model.preferredSharingChainIds)
@ -45,7 +43,7 @@ QObject {
}
]
}
readonly property var flatNetworks: root.walletStore ? root.walletStore.flatNetworks : null
readonly property var flatNetworks: root.walletRootStore.filteredFlatModel
function pair(uri) {
d.acceptedSessionProposal = null
@ -109,8 +107,9 @@ QObject {
}
// TODO #14754: implement custom dApp notification
let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_url), false)
const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
const app_domain = StringUtils.extractDomainFromLink(app_url)
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), false)
// Persist session
if(!store.addWalletConnectSession(JSON.stringify(session))) {
@ -124,20 +123,22 @@ QObject {
}
function onRejectSessionResult(err) {
let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
const app_domain = StringUtils.extractDomainFromLink(app_url)
if(err) {
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_url), true)
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), true)
} else {
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_url), false)
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), false)
}
}
function onSessionDelete(topic, err) {
let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
const app_domain = StringUtils.extractDomainFromLink(app_url)
if(err) {
root.displayToastMessage(qsTr("Failed to disconnect from %1").arg(app_url), true)
root.displayToastMessage(qsTr("Failed to disconnect from %1").arg(app_domain), true)
} else {
root.displayToastMessage(qsTr("Disconnected from %1").arg(app_url), false)
root.displayToastMessage(qsTr("Disconnected from %1").arg(app_domain), false)
}
}
}
@ -175,7 +176,8 @@ QObject {
sdk: root.wcSDK
store: root.store
walletStore: root.walletStore
accountsModel: root.validAccounts
networksModel: root.flatNetworks
onSessionRequest: (request) => {
root.sessionRequest(request)

View File

@ -7,6 +7,7 @@ import shared.stores 1.0 as Stores
import utils 1.0
import StatusQ 0.1
import SortFilterProxyModel 0.2
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
@ -65,7 +66,7 @@ QtObject {
property var nonWatchAccounts: SortFilterProxyModel {
sourceModel: accounts
proxyRoles: [
ExpressionRole {
FastExpressionRole {
name: "color"
function getColor(colorId) {
@ -75,6 +76,7 @@ QtObject {
// Direct call for singleton function is not handled properly by
// SortFilterProxyModel that's why helper function is used instead.
expression: { return getColor(model.colorId) }
expectedRoles: ["colorId"]
}
]
filters: ValueFilter {
@ -159,8 +161,8 @@ QtObject {
return d.chainColors[chainShortName]
}
property var flatNetworks: networksModule.flatNetworks
property SortFilterProxyModel filteredFlatModel: SortFilterProxyModel {
readonly property var flatNetworks: networksModule.flatNetworks
readonly property SortFilterProxyModel filteredFlatModel: SortFilterProxyModel {
sourceModel: root.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: root.areTestNetworksEnabled }
}

View File

@ -36,6 +36,8 @@ StatusListView {
signal toggleNetwork(int chainId, int index)
objectName: "networkSelectorList"
onSelectionChanged: {
if (!root.multiSelection && selection.length > 1) {
console.warn("Warning: Multi-selection is disabled, but multiple items are selected. Automatically selecting the last inserted item.")

View File

@ -2057,7 +2057,7 @@ Item {
store: DAppsStore {
controller: WalletStore.RootStore.walletConnectController
}
walletStore: appMain.rootStore.profileSectionStore.walletStore
walletRootStore: WalletStore.RootStore
Component.onCompleted: {
Global.walletConnectService = walletConnectService

View File

@ -48,10 +48,7 @@ StatusComboBox {
type: StatusComboBox.Type.Secondary
size: StatusComboBox.Size.Small
currentIndex: {
if (count === 0) return
return Math.max(control.indexOfValue(d.currentAccountSelection), 0)
}
currentIndex: 0
objectName: "accountSelector"
popupContentItemObjectName: "accountSelectorList"
@ -129,10 +126,10 @@ StatusComboBox {
QtObject {
id: d
property string currentAccountSelection: root.selectedAddress
property string currentAccountSelection: root.selectedAddress || root.currentValue
Binding on currentAccountSelection {
value: root.selectedAddress
value: root.selectedAddress || root.currentValue
}
}
}

View File

@ -16,6 +16,7 @@ Control {
property alias middleLabel: middleLabel
property alias asset: smartIdenticon.asset
property alias rightComponent: rightComponent.sourceComponent
property alias leftComponent: leftComponent.sourceComponent
property bool loading: false
property int secondarylabelMaxWidth: 100
@ -48,6 +49,9 @@ Control {
contentItem: RowLayout {
spacing: root.spacing
visible: !root.loading
Loader {
id: leftComponent
}
StatusSmartIdenticon {
id: smartIdenticon
Layout.maximumWidth: visible ? 16 : 0
@ -60,8 +64,10 @@ Control {
}
StatusBaseText {
id: tagPrimaryLabel
Layout.maximumWidth: root.availableWidth
font.pixelSize: Style.current.tertiaryTextFontSize
visible: text !== ""
elide: Text.ElideRight
}
StatusBaseText {
id: middleLabel

View File

@ -22,73 +22,81 @@ import shared.popups.walletconnect.controls 1.0
import AppLayouts.Wallet.controls 1.0
import utils 1.0
import shared.popups.walletconnect.private 1.0
StatusDialog {
id: root
width: 480
implicitHeight: d.connectionStatus === root.notConnectedStatus ? 633 : 681
/*
Accounts model
Expected model structure:
name [string] - account name e.g. "Piggy Bank"
address [string] - wallet account address e.g. "0x1234567890"
colorizedChainPrefixes [string] - chain prefixes with rich text colors e.g. "<font color=\"red\">eth:</font><font color=\"blue\">oeth:</font><font color=\"green\">arb:</font>"
emoji [string] - emoji for account e.g. "🐷"
colorId [string] - color id for account e.g. "1"
currencyBalance [var] - fiat currency balance
amount [number] - amount of currency e.g. 1234
symbol [string] - currency symbol e.g. "USD"
optDisplayDecimals [number] - optional number of decimals to display
stripTrailingZeroes [bool] - strip trailing zeroes
walletType [string] - wallet type e.g. Constants.watchWalletType. See `Constants` for possible values
migratedToKeycard [bool] - whether account is migrated to keycard
accountBalance [var] - account balance for a specific network
formattedBalance [string] - formatted balance e.g. "1234.56B"
balance [string] - balance e.g. "123456000000"
iconUrl [string] - icon url e.g. "network/Network=Hermez"
chainColor [string] - chain color e.g. "#FF0000"
*/
required property var accounts
/*
Networks model
Expected model structure:
chainName [string] - chain long name. e.g. "Ethereum" or "Optimism"
chainId [int] - chain unique identifier
iconUrl [string] - SVG icon name. e.g. "network/Network=Ethereum"
layer [int] - chain layer. e.g. 1 or 2
isTest [bool] - true if the chain is a testnet
*/
required property var flatNetworks
readonly property alias selectedAccount: d.selectedAccount
property alias dAppUrl: dappCard.dAppUrl
property alias dAppName: dappCard.name
property alias dAppIconUrl: dappCard.iconUrl
property alias connectionStatus: d.connectionStatus
/*
Selected account address holds the initial account address selection for the account selector.
It is used to preselect the account in the account selector.
*/
property string selectedAccountAddress: contextCard.selectedAccount.address ?? ""
readonly property alias selectedAccount: contextCard.selectedAccount
readonly property alias selectedChains: d.selectedChains
readonly property int notConnectedStatus: 0
readonly property int connectionSuccessfulStatus: 1
readonly property int connectionFailedStatus: 2
function openWithFilter(dappChains, proposer) {
d.connectionStatus = root.notConnectedStatus
d.afterTwoSecondsFromStatus = false
let m = proposer.metadata
dappCard.name = m.name
dappCard.url = m.url
if(m.icons.length > 0) {
dappCard.iconUrl = m.icons[0]
} else {
dappCard.iconUrl = ""
}
d.dappChains.clear()
for (let i = 0; i < dappChains.length; i++) {
// Convert to int
d.dappChains.append({ chainId: parseInt(dappChains[i]) })
}
root.open()
}
function pairSuccessful(session) {
d.connectionStatus = root.connectionSuccessfulStatus
closeAndRetryTimer.start()
}
function pairFailed(session, err) {
d.connectionStatus = root.connectionFailedStatus
closeAndRetryTimer.start()
}
Timer {
id: closeAndRetryTimer
interval: 2000
running: false
repeat: false
onTriggered: {
d.afterTwoSecondsFromStatus = true
}
}
signal connect()
signal decline()
signal disconnect()
width: 480
implicitHeight: !d.connectionAttempted ? 633 : 681
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
title: qsTr("Connection request")
title: d.connectionSuccessful ? qsTr("dApp connected") :
qsTr("Connection request")
padding: 20
@ -98,37 +106,40 @@ StatusDialog {
DAppCard {
id: dappCard
afterTwoSecondsFromStatus: d.afterTwoSecondsFromStatus
isConnectedSuccessfully: d.connectionStatus === root.connectionSuccessfulStatus
isConnectionFailed: d.connectionStatus === root.connectionFailedStatus
isConnectionStarted: d.connectionStatus !== root.notConnectedStatus
isConnectionFailedOrDisconnected: d.connectionStatus !== root.connectionSuccessfulStatus
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: root.availableWidth - Layout.leftMargin * 2
Layout.leftMargin: 12
Layout.rightMargin: Layout.leftMargin
Layout.topMargin: 20
Layout.topMargin: 14
Layout.bottomMargin: Layout.topMargin
}
ContextCard {
id: contextCard
Layout.maximumWidth: root.availableWidth
Layout.fillWidth: true
accountsProxy: d.accountsProxy
selectedAccount: d.selectedAccount
selectedChains: d.selectedChains
filteredChains: d.filteredChains
notConnected: d.connectionStatus === root.notConnectedStatus
selectedAccountAddress: root.selectedAccountAddress
connectionAttempted: d.connectionAttempted
accountsModel: d.accountsProxy
chainsModel: root.flatNetworks
chainSelection: d.selectedChains
onChainSelectionChanged: {
if (d.selectedChains !== chainSelection) {
d.selectedChains = chainSelection
}
}
}
PermissionsCard {
Layout.maximumWidth: root.availableWidth
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.leftMargin: 16
Layout.rightMargin: Layout.leftMargin
Layout.topMargin: 20
Layout.topMargin: 12
Layout.bottomMargin: Layout.topMargin
dappName: dappCard.name
}
}
@ -136,31 +147,39 @@ StatusDialog {
id: footer
rightButtons: ObjectModel {
StatusButton {
objectName: "rejectButton"
height: 44
text: qsTr("Decline")
text: qsTr("Reject")
visible: d.connectionStatus === root.notConnectedStatus
visible: !d.connectionAttempted
onClicked: root.decline()
}
StatusButton {
StatusFlatButton {
objectName: "disconnectButton"
height: 44
text: qsTr("Disconnect")
visible: d.connectionStatus === root.connectionSuccessfulStatus
visible: d.connectionSuccessful
type: StatusBaseButton.Type.Danger
onClicked: root.disconnect()
}
StatusButton {
objectName: "primaryActionButton"
height: 44
text: d.connectionStatus === root.notConnectedStatus
? qsTr("Connect")
: qsTr("Close")
text: d.connectionAttempted
? qsTr("Close")
: qsTr("Connect")
enabled: {
if (!d.connectionAttempted)
return root.selectedChains.length > 0
return true
}
onClicked: {
if (d.connectionStatus === root.notConnectedStatus)
if (!d.connectionAttempted)
root.connect()
else
root.close()
@ -178,27 +197,19 @@ StatusDialog {
sorters: RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder }
}
property var selectedAccount: ({})
property var selectedChains: allChainIdsAggregator.value
readonly property var filteredChains: LeftJoinModel {
leftModel: d.dappChains
rightModel: root.flatNetworks
joinRole: "chainId"
}
readonly property FunctionAggregator allChainIdsAggregator: FunctionAggregator {
model: d.filteredChains
model: root.flatNetworks
initialValue: []
roleName: "chainId"
aggregateFunction: (aggr, value) => [...aggr, value]
}
readonly property var dappChains: ListModel {}
property int connectionStatus: notConnectedStatus
property bool afterTwoSecondsFromStatus: false
property int connectionStatus: root.notConnectedStatus
readonly property bool connectionSuccessful: d.connectionStatus === root.connectionSuccessfulStatus
readonly property bool connectionFailed: d.connectionStatus === root.connectionFailedStatus
readonly property bool connectionAttempted: d.connectionStatus !== root.notConnectedStatus
}
}

View File

@ -0,0 +1,57 @@
import QtQuick 2.15
import StatusQ.Core.Theme 0.1
import shared.controls 1.0
import utils 1.0
InformationTag {
id: root
property bool success: false
tagPrimaryLabel.text: qsTr("Connected. You can now go back to the dApp.")
tagPrimaryLabel.color: Theme.palette.directColor1
tagPrimaryLabel.font.pixelSize: Style.current.additionalTextSize
backgroundColor: Theme.palette.successColor3
bgBorderColor: Theme.palette.alphaColor(Theme.palette.successColor1, 0.4)
asset.color: tagPrimaryLabel.color
verticalPadding: Style.current.halfPadding
horizontalPadding: 12
leftComponent: successBadge
states: [
State {
name: "error"
when: !root.success
PropertyChanges { target: tagPrimaryLabel; text: qsTr("Error connecting to dApp. Close and try again") }
PropertyChanges { target: tagPrimaryLabel; color: Theme.palette.dangerColor1 }
PropertyChanges { target: asset; name: "warning" }
PropertyChanges {
target: root
backgroundColor: Theme.palette.dangerColor3
bgBorderColor: Theme.palette.alphaColor(Theme.palette.dangerColor1, 0.4)
}
}
]
ColorAnimation on backgroundColor { running: root.success && root.visible; to: Theme.palette.successColor2; duration: 2000 }
ColorAnimation on backgroundColor { running: !root.success && root.visible; to: "transparent"; duration: 2000 }
ColorAnimation on bgBorderColor { running: root.success && root.visible; to: Theme.palette.successColor3; duration: 2000 }
ColorAnimation on bgBorderColor { running: !root.success && root.visible; to: Theme.palette.dangerColor2; duration: 2000 }
Component {
id: successBadge
Item {
width: visible ? 10 : 0
height: visible ? 6 : 0
visible: root.success
Rectangle {
width: 6
height: 6
radius: width / 2
color: Theme.palette.successColor1
}
}
}
}

View File

@ -0,0 +1,86 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import utils 1.0
Item {
id: root
property url imageUrl: ""
property string badgeIcon: "walletConnect"
property string fallbackIcon: "dapp"
readonly property bool iconLoaded: !mainImage.isError && !mainImage.isLoading && mainImage.image.source !== ""
implicitWidth: mainImage.implicitWidth
implicitHeight: mainImage.implicitHeight
Item {
id: imageContainer
width: parent.width
height: width
StatusRoundedImage {
id: mainImage
width: parent.width
height: width
visible: !isError && !isLoading && root.imageUrl != ""
image.source: root.imageUrl
}
Loader {
anchors.fill: mainImage
active: !mainImage.visible
sourceComponent: StatusRoundedComponent {
color: Theme.palette.primaryColor3
StatusIcon {
anchors.fill: parent
anchors.margins: Style.current.padding
color: Theme.palette.primaryColor1
icon: "dapp"
}
}
}
layer.enabled: true
layer.effect: OpacityMask {
id: mask
invert: true
maskSource: Item {
width: mask.width + 2
height: mask.height + 2
Rectangle {
x: badge.x + 1
y: badge.y + 1
width: badge.width + 2
height: badge.width + 2
radius: badge.width / 2
}
}
}
}
StatusRoundIcon {
id: badge
width: (root.width / 2) - Style.current.padding
height: width
anchors.bottom: parent.bottom
anchors.right: parent.right
asset.name: root.badgeIcon
asset.color: "transparent"
asset.width: width
asset.height: height
asset.bgWidth: width
asset.bgHeight: height
}
}

View File

@ -1,137 +0,0 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
ColumnLayout {
id: root
property alias name: appNameText.text
property alias url: appUrlText.text
property string iconUrl: ""
property bool afterTwoSecondsFromStatus
property bool isConnectedSuccessfully: false
property bool isConnectionFailed: true
property bool isConnectionStarted: false
property bool isConnectionFailedOrDisconnected: true
Rectangle {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 72
Layout.preferredHeight: Layout.preferredWidth
radius: width / 2
color: Theme.palette.primaryColor3
StatusRoundedImage {
id: iconDisplay
anchors.fill: parent
visible: !fallbackImage.visible
image.source: iconUrl
}
StatusIcon {
id: fallbackImage
anchors.centerIn: parent
width: 40
height: 40
icon: "dapp"
color: Theme.palette.primaryColor1
visible: iconDisplay.image.isLoading || iconDisplay.image.isError || !iconUrl
}
}
StatusBaseText {
id: appNameText
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 4
font.bold: true
font.pixelSize: 17
}
// TODO replace with the proper URL control
StatusLinkText {
id: appUrlText
Layout.alignment: Qt.AlignHCenter
font.pixelSize: 15
}
Rectangle {
Layout.preferredWidth: pairingStatusLayout.implicitWidth + 32
Layout.preferredHeight: pairingStatusLayout.implicitHeight + 14
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
visible: root.isConnectionStarted
color: root.isConnectedSuccessfully
? root.afterTwoSecondsFromStatus
? Theme.palette.successColor2
: Theme.palette.successColor3
: root.afterTwoSecondsFromStatus
? "transparent"
: Theme.palette.dangerColor3
border.color: root.isConnectedSuccessfully
? Theme.palette.successColor2
: Theme.palette.dangerColor2
border.width: 1
radius: height / 2
RowLayout {
id: pairingStatusLayout
anchors.centerIn: parent
spacing: 8
Rectangle {
width: 6
height: 6
radius: width / 2
visible: root.isConnectedSuccessfully
color: Theme.palette.successColor1
}
StatusIcon {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
visible: root.isConnectionFailedOrDisconnected
color: Theme.palette.dangerColor1
icon: "warning"
}
StatusBaseText {
text: {
if (root.isConnectedSuccessfully)
return qsTr("Connected. You can now go back to the dApp.")
else if (root.isConnectionFailed)
return qsTr("Error connecting to dApp. Close and try again")
return ""
}
font.pixelSize: 12
color: root.isConnectedSuccessfully ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
}
}
}

View File

@ -6,17 +6,19 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import AppLayouts.Wallet.controls 1.0
import shared.controls 1.0
Rectangle {
id: root
property var accountsProxy
property var selectedAccount
property var selectedChains
property var filteredChains
property bool notConnected: true
property string selectedAccountAddress: ""
property bool connectionAttempted: false
property var accountsModel
property var chainsModel
property alias chainSelection: networkFilter.selection
readonly property alias selectedAccount: accountsDropdown.currentAccount
implicitWidth: contextLayout.implicitWidth
implicitHeight: contextLayout.implicitHeight
@ -45,10 +47,13 @@ Rectangle {
id: accountsDropdown
Layout.preferredWidth: 204
control.enabled: root.notConnected && count > 1
model: accountsProxy
onCurrentAccountChanged: root.selectedAccount = currentAccount
Layout.preferredHeight: 38
control.horizontalPadding: 12
control.verticalPadding: 4
control.enabled: !root.connectionAttempted && count > 1
model: root.accountsModel
indicator.visible: control.enabled
selectedAddress: root.selectedAccountAddress
}
}
@ -59,7 +64,7 @@ Rectangle {
}
RowLayout {
Layout.margins: 16
Layout.margins: 15
StatusBaseText {
text: qsTr("On")
@ -69,19 +74,14 @@ Rectangle {
NetworkFilter {
id: networkFilter
objectName: "networkFilter"
Layout.preferredWidth: accountsDropdown.Layout.preferredWidth
flatNetworks: root.filteredChains
flatNetworks: root.chainsModel
showTitle: true
multiSelection: true
selectionAllowed: notConnected && root.selectedChains.length > 1
selection: root.selectedChains
onSelectionChanged: {
if (root.selectedChains !== networkFilter.selection) {
root.selectedChains = networkFilter.selection
}
}
showAllSelectedText: false
selectionAllowed: !root.connectionAttempted && root.chainsModel.ModelCount.count > 1
}
}
}

View File

@ -0,0 +1,74 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import shared.popups.walletconnect 1.0
import utils 1.0
ColumnLayout {
id: root
property alias name: appNameText.text
property url dAppUrl: ""
property url iconUrl: ""
spacing: Style.current.padding
RoundImageWithBadge {
objectName: "dappIcon"
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 72
Layout.preferredHeight: Layout.preferredWidth
imageUrl: iconUrl
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
StatusBaseText {
id: appNameText
objectName: "appNameText"
Layout.fillWidth: true
Layout.maximumWidth: root.width
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
font.bold: true
font.pixelSize: 17
}
StatusFlatButton {
id: appUrlText
objectName: "appUrlControl"
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: root.width
icon.name: "external-link"
icon.color: hovered ? Theme.palette.baseColor1 : Theme.palette.directColor1
textPosition: StatusBaseButton.TextPosition.Left
size: StatusBaseButton.Size.Tiny
textColor: Theme.palette.directColor1
hoverColor: "transparent"
spacing: 0
font.pixelSize: 15
font.weight: Font.Normal
horizontalPadding: 0
verticalPadding: 0
text: StringUtils.extractDomainFromLink(dAppUrl)
onClicked: {
Global.openLinkWithConfirmation(dAppUrl, text)
}
}
ConnectionStatusTag {
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: root.width
objectName: "connectionStatusTag"
success: d.connectionSuccessful
visible: d.connectionAttempted
}
}
}

View File

@ -5,23 +5,29 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
ColumnLayout {
id: root
spacing: 8
StatusBaseText {
text: qsTr("Uniswap Interface will be able to:")
property string dappName: ""
StatusBaseText {
objectName: "permissionsTitle"
text: qsTr("%1 will be able to:").arg(root.dappName)
Layout.preferredHeight: 18
font.pixelSize: 13
color: Theme.palette.baseColor1
}
StatusBaseText {
text: qsTr("Check your account balance and activity")
Layout.preferredHeight: 18
font.pixelSize: 13
}
StatusBaseText {
text: qsTr("Request transactions and message signing")
Layout.preferredHeight: 18
font.pixelSize: 13
}

View File

@ -0,0 +1,3 @@
ContextCard 1.0 ContextCard.qml
DAppCard 1.0 DAppCard.qml
PermissionsCard 1.0 PermissionsCard.qml

View File

@ -1,5 +1,7 @@
PairWCModal 1.0 PairWCModal.qml
DAppsListPopup 1.0 DAppsListPopup.qml
ConnectDAppModal 1.0 ConnectDAppModal.qml
ConnectionStatusTag 1.0 ConnectionStatusTag.qml
DAppRequestModal 1.0 DAppRequestModal.qml
DAppsUriCopyInstructionsPopup 1.0 DAppsUriCopyInstructionsPopup.qml
RoundImageWithBadge 1.0 RoundImageWithBadge.qml