/* * This file is part of the Flowee project * Copyright (C) 2022-2026 Tom Zander * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick import QtQuick.Layouts import QtQuick.Controls.Basic as QQC2 import "../Flowee" as Flowee import Flowee.org.pay Page { id: root headerText: singleAccountSetup ? qsTr("Wallet") : qsTr("Wallets") property QtObject newAccountAction: QQC2.Action { text: qsTr("Add Wallet") onTriggered: thePile.push("./NewAccount.qml") } menuItems: [ newAccountAction ] function indexOfCurrentAccount() { var list = tabBar.model var cur = portfolio.current for (let i = 0; i < list.length; i = i + 1) { if (list[i] === cur) return i } return 0 } // this is a special interpretation of the property-name in the context // of these pages where (unlike in the rest of the app) we take archived // and private wallets into account. property bool singleAccountSetup: portfolio.rawAccounts.length === 1 property QtObject backupModule: ModuleManager.moduleInfo("backupSyncModule") Flickable { id: scroller contentHeight: options.height anchors.fill: parent clip: true Column { id: options spacing: 6 width: parent.width SelectDefaultConfigButton { visible: !singleAccountSetup } Item { width: parent.width height: privateButton.height visible: !singleAccountSetup Image { source: "qrc:/private" + (Pay.useDarkSkin ? "-light.svg" : ".svg") width: 20 height: 20 smooth: true opacity: enabled ? 1 : 0.7 anchors.verticalCenter: parent.verticalCenter } TextButton { id: privateButton width: parent.width - 26 x: 26 text: Pay.privateMode ? qsTr("Exit Private Mode") : qsTr("Enter Private Mode") subtext: Pay.privateMode ? qsTr("Reveals wallets marked private") : qsTr("Hides wallets marked private") onClicked: { Pay.privateMode = !Pay.privateMode thePile.pop() } } } Item { id: placeholder // the list of wallets will be placed on top of this. width: parent.width height: walletList.height } property QtObject account: portfolio.current PageTitledBox { width: parent.width title: qsTr("Wallet Name") EditableLabel { text: options.account.name onEdited: options.account.name = text width: parent.width } } PageTitledBox { title: qsTr("Information") width: parent.width Flowee.AccountTypeLabel { width: parent.width account: options.account font.pixelSize: root.font.pixelSize * 0.9 color: palette.brightText } Flowee.Label { id: syncLabel width: parent.width color: palette.brightText font.pixelSize: root.font.pixelSize * 0.9 property string time: "" text: { if (options.account.isArchived) return qsTr("Offline") + " (" + options.account.timeBehind + ")" var height = options.account.lastBlockSynched if (height < 1) return "" let targetHeight = Pay.chainHeight if (targetHeight - height > 30) return "

" + options.account.timeBehind + " (" + height + " / " + Pay.chainHeight + ")

" let title = qsTr("Sync Status") var time = syncLabel.time if (time === "") return "

" + title + ": " + height + " / " + targetHeight + "

" return "

" + title + ": " + time + " (" + height + " / " + Pay.chainHeight + ")

" } wrapMode: Text.WrapAtWordBoundaryOrAnywhere Timer { // the lastBlockSynchedTime does not change, // but since we render it as '12 minutes ago' // we need to actually re-interpret that // ever so often to keep the relative time. running: !options.account.isArchived interval: 30000 // 30 sec repeat: true triggeredOnStart: true onTriggered: syncLabel.time = Pay.formatDateTime(options.account.lastBlockSynchedTime) } } Flowee.CheckBox { Layout.fillWidth: true visible: !singleAccountSetup enabled: !options.account.isArchived checked: options.account.isPrivate text: qsTr("Hide in private mode") onClicked: options.account.isPrivate = checked } TextButton { text: qsTr("Addresses and keys") subtext: qsTr("Find private keys and addresses") enabled: !options.account.needsPinToOpen || options.account.isDecrypted pageButton: true onClicked: thePile.push(backupDetails) } TextButton { text: qsTr("xpub") pageButton: true subtext: enabled ? qsTr("To connect this wallet") : qsTr("Unavailable for this wallet") enabled: options.account.isHDWallet && (!options.account.needsPinToOpen || options.account.isDecrypted) onClicked: thePile.push(xpubComponent) Component { id: xpubComponent Page { id: detailsPage headerText: qsTr("Your XPub details") ColumnLayout { width: parent.width spacing: 20 Flowee.Label { id: titleText text: qsTr("Be careful who you share the xpub with!") Layout.fillWidth: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere } Flowee.QRWidget { id: seedQr qrSize: 250 textVisible: false useRawString: true qrText: options.account.xpub Layout.alignment: Qt.AlignHCenter } Flowee.Label { text: options.account.xpub Layout.fillWidth: true wrapMode: Text.WrapAnywhere font.pixelSize: titleText.font.pixelSize * 0.9 horizontalAlignment: Qt.AlignHCenter } } } } } } PageTitledBox { title: qsTr("Configuration") width: parent.width InstaPayConfigButton { enabled: !options.account.isArchived account: options.account } Item { width: parent.width height: 50 Image { source: "qrc:/cloud-storage" + (Pay.useDarkSkin ? "-light.svg" : ".svg") width: 35 height: 25 smooth: true x: 5 y: 16 } TextButton { x: 50 width: parent.width - 50 visible: backupModule != null enabled: options.account.isDecrypted && options.account.isHDWallet text: qsTr("Save comments to cloud") subtext: enabled ? qsTr("Improves wallet imports") : qsTr("Unavailable for this wallet") pageButton: true onClicked: thePile.push(backupModule.sectionUrl("walletOptions"), { "pageData": options.account }) currentValue: options.account.cloudStorageEnabled } } } PageTitledBox { title: qsTr("Backup") width: parent.width Item { id: nftOptionPanel visible: backupModule.nfcAvailable enabled: options.account.isDecrypted && options.account.isHDWallet width: parent.width height: 50 Image { id: theLogo source: "qrc:/backup-sync/nfc-tag.svg" smooth: true width: 50 height: 50 } TextButton { x: 56 width: parent.width - x anchors.verticalCenter: parent.verticalCenter text: qsTr("Make NFC backup") subtext: qsTr("Create a physical backup to a tag") pageButton: true onClicked: thePile.push(backupModule.sectionUrl("storeOnNfc"), { "pageData": options.account }) buttonId: 92387 // same as the next, as one backup is enough. } } TextButton { width: parent.width enabled: options.account.isDecrypted text: qsTr("Show seed for paper backup") subtext: qsTr("The details needed to restore this wallet") pageButton: true onClicked: thePile.push(options.account.isHDWallet ? hdBackupDetails : backupDetails) buttonId: 92387 Component { id: hdBackupDetails Page { id: detailsPage headerText: qsTr("Backup Details") Item { // non-layoutable items. Flowee.Popup { id: qrPopup width: 270 height: 270 x: (root.width - width) / 2 y: 100 modal: true closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent background: Rectangle { color: palette.light border.color: palette.midlight border.width: 1 radius: 5 } Flowee.QRWidget { id: seedQr qrSize: 250 textVisible: false useRawString: true anchors.centerIn: parent } } } ColumnLayout { width: parent.width spacing: 10 PageTitledBox { title: qsTr("Wallet seed-phrase") Item { implicitHeight: mnemonicLabel.implicitHeight width: parent.width Flowee.LabelWithClipboard { id: mnemonicLabel text: options.account.mnemonic width: parent.width - 36 wrapMode: Text.WrapAtWordBoundaryOrAnywhere font.wordSpacing: 1 } Image { width: 20 height: 20 anchors.right: parent.right source: "qrc:/qr-code" + (Pay.useDarkSkin ? "-light.svg" : ".svg") MouseArea { anchors.fill: parent onClicked: { seedQr.qrText = options.account.mnemonic qrPopup.open() } } } } Flowee.Label { text: qsTr("Password") font.bold: true visible: options.account.mnemonicPwd !== "" } Flowee.LabelWithClipboard { text: options.account.mnemonicPwd visible: text !== "" } } PageTitledBox { title: qsTr("Seed format") visible: options.account.isElectrumMnemonic Flowee.Label { text: "Electrum" font.italic: true } } PageTitledBox { title: qsTr("Start Date", "date of wallet creation") Flowee.LabelWithClipboard { text: { var startDate = options.account.firstMinedTransaction if (!isNaN(startDate.getTime())) // date is valid return startDate.toLocaleDateString() return Pay.timeOfBlockHeight(options.account.accountStartBlockHeight).toLocaleDateString() } } } PageTitledBox { title: qsTr("Derivation Path") Flowee.LabelWithClipboard { text: options.account.hdDerivationPath } } Flowee.Label { Layout.fillWidth: true text: qsTr("Please save the seed-phrase on paper, in the right order, with the derivation path. This seed will allow you to recover your wallet in case you lose your mobile.") textFormat: Text.StyledText font.italic: true wrapMode: Text.WrapAtWordBoundaryOrAnywhere } Flowee.Label { Layout.fillWidth: true text: qsTr("Important: Never share your seed-phrase with others!") font.italic: true textFormat: Text.StyledText wrapMode: Text.WrapAtWordBoundaryOrAnywhere } } } } Component { id: backupDetails Page { headerText: qsTr("Wallet keys") property QtObject showIndexAction: QQC2.Action { text: qsTr("Show Index", "toggle to show numbers") checkable: true checked: listView.showHdIndex onTriggered: listView.showHdIndex = checked } menuItems: { if (options.account.isHDWallet) return [showIndexAction] return [] } PageTitledBox { width: parent.width id: optionsBox Flowee.CheckBox { text: qsTr("Change Addresses") visible: options.account.isHDWallet onClicked: options.account.secrets.showChangeChain = checked toolTipText: qsTr("Switches between addresses others can pay you on, and addresses the wallet uses to send change back to yourself.") } Flowee.CheckBox { text: qsTr("Used Addresses") visible: !options.account.isSingleAddressAccount onClicked: options.account.secrets.showUsedAddresses = checked toolTipText: qsTr("Switches between unused and used Bitcoin addresses") } } PageTitledBox { // since the non-HD wallet type has only the 'wallet-keys' as a backup page, // for such wallets we additionally add the starting date here. id: startLabel width: parent.width visible: !options.account.isHDWallet height: visible ? implicitHeight : 0 anchors.top: optionsBox.bottom title: qsTr("Start Date", "date of wallet creation") Flowee.LabelWithClipboard { text: { var startDate = options.account.firstMinedTransaction if (!isNaN(startDate.getTime())) // date is valid return startDate.toLocaleDateString() return Pay.timeOfBlockHeight(options.account.accountStartBlockHeight).toLocaleDateString() } } } Item { // this is a horrible hack... // First, ListViews almost always require clipping on, // otherwise list-items can overlap the rest of your view. // But if I enable clipping I no longer get the nice // width-filling backgrounds... // Sooo. I need a clipping item that is full width (negative // left and right margin) id: clipItem anchors { top: startLabel.bottom topMargin: 10 bottom: parent.bottom left: parent.left right: parent.right leftMargin: -10 rightMargin: -10 } clip: true Flowee.WalletSecretsView { id: listView anchors { fill: parent leftMargin: 10 rightMargin: 10 } account: options.account showHdIndex: false } } } } } } Item { width: 1; height: 10 } // spacer. Make the button not to close to the clickable checkbox for fatfingered people. Rectangle { id: archiveButton height: archiveButtonText.height + 20 color: Pay.useDarkSkin ? "#b39554" : "#e5be6b" width: archiveButtonText.width + 50 visible: !singleAccountSetup radius: 3 Image { source: "qrc:/archived.svg" x: 6 anchors.verticalCenter: parent.verticalCenter width: 32 height: 32 smooth: true } Flowee.Label { id: archiveButtonText text: options.account.isArchived ? qsTr("Unarchive Wallet") : qsTr("Archive Wallet") x: 35 anchors.verticalCenter: parent.verticalCenter color: "black" } MouseArea { anchors.fill: parent cursorShape: Qt.ArrowCursor onClicked: options.account.isArchived = !options.account.isArchived } } Item { width: 1; height: 30 } // spacer. Rectangle { id: removeWallet height: archiveButtonText.height + 20 color: mainWindow.errorRedBg width: removeWalletLabel.width + 30 visible: options.account.isArchived radius: 3 QQC2.Label { id: removeWalletLabel text: qsTr("Remove Wallet") anchors.centerIn: parent color: "#fcfcfc" } MouseArea { anchors.fill: parent cursorShape: Qt.ArrowCursor onClicked: { errorDialog.visible = true errorDialog.forceActiveFocus() } } } Item { width: 1; height: 20 } // spacer. } } MouseArea { width: parent.width + 20 height: Math.min(180, walletList.contentHeight) x: -10 y: Math.max(0, placeholder.y - scroller.contentY) Flickable { id: walletList width: parent.width height: parent.height contentHeight: column.height contentWidth: width clip: true boundsBehavior: height <= contentHeight ? Flickable.StopAtBounds : Flickable.DragAndOvershootBounds Column { id: column x: 10 Repeater { model: portfolio.rawAccounts Rectangle { width: root.width - 20 height: ali.height + 6 color: palette.light Rectangle { anchors.fill: parent anchors.bottomMargin: 6 anchors.leftMargin: -3 anchors.rightMargin: -3 color: ((index % 2) === 0) ? palette.base : palette.alternateBase radius: 6 border.width: !singleAccountSetup && options.account === modelData ? 1.3 : 0 border.color: palette.highlight } Column { width: 20 anchors.verticalCenter: ali.verticalCenter Flowee.Label { anchors.horizontalCenter: parent.horizontalCenter visible: modelData.isPrimaryAccount text: "✷" } Image { id: archivedLabel source: "qrc:/archived.svg" width: 20 height: 20 visible: modelData.isArchived anchors.horizontalCenter: parent.horizontalCenter smooth: true } Image { source: "qrc:/private" + (Pay.useDarkSkin ? "-light.svg" : ".svg") width: 16 height: 16 smooth: true anchors.horizontalCenter: parent.horizontalCenter visible: !archivedLabel.visible && modelData.isPrivate } Image { source: "qrc:/cloud-storage" + (Pay.useDarkSkin ? "-light.svg" : ".svg") width: 16 height: 13 visible: !archivedLabel.visible && modelData.cloudStorageEnabled smooth: true anchors.horizontalCenter: parent.horizontalCenter } } AccountListItem { id: ali x: 22 height: implicitHeight width: parent.width - 24 account: modelData } MouseArea { anchors.fill: parent onClicked: options.account = modelData } } } } } } Item { Flowee.Dialog { id: errorDialog standardButtons: QQC2.DialogButtonBox.Ok + QQC2.DialogButtonBox.Cancel title: qsTr("Really Delete?") text: qsTr("Removing wallet \"%1\" can not be undone.", "argument is the wallet name").arg(root.account.name) onAccepted: options.account.removeAccount() } } }