2022-12-16 19:57:43 +01:00
/*
* This file is part of the Flowee project
2026-01-14 17:05:33 +01:00
* Copyright (C) 2022-2026 Tom Zander <tom@flowee.org>
2022-12-16 19:57:43 +01:00
*
* 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 <http://www.gnu.org/licenses/>.
*/
import QtQuick
2023-05-16 18:57:23 +02:00
import QtQuick . Layouts
2025-06-18 17:48:29 +02:00
import QtQuick . Controls . Basic as QQC2
2023-05-16 18:57:23 +02:00
import "../Flowee" as Flowee
2025-10-31 17:24:01 +01:00
import Flowee . org . pay
2022-12-16 19:57:43 +01:00
Page {
id: root
2023-05-18 17:06:33 +02:00
headerText: singleAccountSetup ? qsTr ( "Wallet" ) : qsTr ( "Wallets" )
2023-05-16 18:57:23 +02:00
property QtObject newAccountAction: QQC2 . Action {
text: qsTr ( "Add Wallet" )
onTriggered: thePile . push ( "./NewAccount.qml" )
}
menuItems: [ newAccountAction ]
2022-12-16 19:57:43 +01:00
2022-12-16 20:19:23 +01:00
function indexOfCurrentAccount ( ) {
2026-03-14 21:14:42 +01:00
var list = tabBar . model
2022-12-16 20:19:23 +01:00
var cur = portfolio . current
for ( let i = 0 ; i < list . length ; i = i + 1 ) {
2026-03-14 21:14:42 +01:00
if ( list [ i ] === cur ) return i
2022-12-16 20:19:23 +01:00
}
2026-03-14 21:14:42 +01:00
return 0
2022-12-16 20:19:23 +01:00
}
2023-05-18 17:06:33 +02:00
// 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
2026-03-14 21:14:42 +01:00
property QtObject backupModule: ModuleManager . moduleInfo ( "backupSyncModule" )
2023-05-18 17:06:33 +02:00
2025-10-31 17:24:01 +01:00
Flickable {
id: scroller
contentHeight: options . height
anchors.fill: parent
clip: true
Column {
id: options
spacing: 6
width: parent . width
SelectDefaultConfigButton {
visible: ! singleAccountSetup
2023-05-16 22:00:47 +02:00
}
2025-10-31 17:24:01 +01:00
Item {
width: parent . width
height: privateButton . height
visible: ! singleAccountSetup
Image {
2026-03-14 21:14:42 +01:00
source: "qrc:/private" + ( Pay . useDarkSkin ? "-light.svg" : ".svg" )
2025-10-31 17:24:01 +01:00
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
2026-03-14 21:14:42 +01:00
thePile . pop ( )
2025-10-31 17:24:01 +01:00
}
}
}
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 )
2026-03-14 21:14:42 +01:00
return qsTr ( "Offline" ) + " (" + options . account . timeBehind + ")"
2025-10-31 17:24:01 +01:00
var height = options . account . lastBlockSynched
if ( height < 1 )
return ""
2026-05-05 16:28:33 +02:00
let targetHeight = Pay . chainHeight
if ( targetHeight - height > 30 )
return "<p>" + options . account . timeBehind + " ("
+ height + " / " + Pay . chainHeight + ")</p>"
2026-03-14 21:14:42 +01:00
let title = qsTr ( "Sync Status" )
var time = syncLabel . time
2026-05-05 16:28:33 +02:00
if ( time === "" )
return "<p>" + title + ": " + height + " / " + targetHeight + "</p>"
return "<p>" + title + ": " + time + " ("
+ height + " / " + Pay . chainHeight + ")</p>"
2025-10-31 17:24:01 +01:00
}
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
2026-03-14 21:14:42 +01:00
onTriggered: syncLabel . time = Pay . formatDateTime ( options . account . lastBlockSynchedTime )
2025-10-31 17:24:01 +01:00
}
}
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
2026-03-14 21:14:42 +01:00
onClicked: thePile . push ( backupDetails )
2025-10-31 17:24:01 +01:00
}
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 )
2026-03-14 21:14:42 +01:00
onClicked: thePile . push ( xpubComponent )
2025-10-31 17:24:01 +01:00
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 {
2026-03-14 21:14:42 +01:00
source: "qrc:/cloud-storage" + ( Pay . useDarkSkin ? "-light.svg" : ".svg" )
2025-10-31 17:24:01 +01:00
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
2026-03-14 21:14:42 +01:00
onClicked: thePile . push ( options . account . isHDWallet ? hdBackupDetails : backupDetails )
2025-10-31 17:24:01 +01:00
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
2026-03-14 21:14:42 +01:00
source: "qrc:/qr-code" + ( Pay . useDarkSkin ? "-light.svg" : ".svg" )
2025-10-31 17:24:01 +01:00
MouseArea {
anchors.fill: parent
onClicked: {
seedQr . qrText = options . account . mnemonic
2026-03-14 21:14:42 +01:00
qrPopup . open ( )
2025-10-31 17:24:01 +01:00
}
}
}
}
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 {
2026-01-14 17:05:33 +01:00
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 ( )
}
}
2025-10-31 17:24:01 +01:00
}
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 ( "<b>Important</b>: 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 )
2026-03-14 21:14:42 +01:00
return [ showIndexAction ]
return [ ]
2025-10-31 17:24:01 +01:00
}
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 {
2026-03-14 21:14:42 +01:00
text: qsTr ( "Used Addresses" )
2025-10-31 17:24:01 +01:00
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,
2026-01-14 17:05:33 +01:00
// for such wallets we additionally add the starting date here.
id: startLabel
2025-10-31 17:24:01 +01:00
width: parent . width
visible: ! options . account . isHDWallet
height: visible ? implicitHeight : 0
anchors.top: optionsBox . bottom
2026-01-14 17:05:33 +01:00
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 ( )
}
}
2025-10-31 17:24:01 +01:00
}
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 {
2026-01-14 17:05:33 +01:00
top: startLabel . bottom
2025-10-31 17:24:01 +01:00
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
2026-03-14 21:14:42 +01:00
text: qsTr ( "Remove Wallet" )
2025-10-31 17:24:01 +01:00
anchors.centerIn: parent
color: "#fcfcfc"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt . ArrowCursor
onClicked: {
errorDialog . visible = true
errorDialog . forceActiveFocus ( )
}
}
}
Item { width: 1 ; height: 20 } // spacer.
2023-05-16 18:57:23 +02:00
}
}
2025-10-31 17:24:01 +01:00
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
2022-12-16 19:57:43 +01:00
2025-10-31 17:24:01 +01:00
Repeater {
model: portfolio . rawAccounts
Rectangle {
width: root . width - 20
height: ali . height + 6
color: palette . light
2022-12-16 19:57:43 +01:00
2025-10-31 17:24:01 +01:00
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
2026-03-14 21:14:42 +01:00
text: "✷"
2025-10-31 17:24:01 +01:00
}
Image {
id: archivedLabel
source: "qrc:/archived.svg"
width: 20
height: 20
visible: modelData . isArchived
anchors.horizontalCenter: parent . horizontalCenter
smooth: true
}
Image {
2026-03-14 21:14:42 +01:00
source: "qrc:/private" + ( Pay . useDarkSkin ? "-light.svg" : ".svg" )
2025-10-31 17:24:01 +01:00
width: 16
height: 16
smooth: true
anchors.horizontalCenter: parent . horizontalCenter
visible: ! archivedLabel . visible && modelData . isPrivate
}
Image {
2026-03-14 21:14:42 +01:00
source: "qrc:/cloud-storage" + ( Pay . useDarkSkin ? "-light.svg" : ".svg" )
2025-10-31 17:24:01 +01:00
width: 16
height: 13
visible: ! archivedLabel . visible && modelData . cloudStorageEnabled
smooth: true
anchors.horizontalCenter: parent . horizontalCenter
}
}
2023-02-22 21:45:33 +01:00
2025-10-31 17:24:01 +01:00
AccountListItem {
id: ali
x: 22
height: implicitHeight
width: parent . width - 24
account: modelData
}
MouseArea {
anchors.fill: parent
onClicked: options . account = modelData
}
}
2022-12-16 19:57:43 +01:00
}
}
}
}
2025-11-02 21:04:02 +01:00
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 )
2026-03-14 21:14:42 +01:00
onAccepted: options . account . removeAccount ( )
2025-11-02 21:04:02 +01:00
}
}
2022-12-16 19:57:43 +01:00
}