diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index d88ded6a15..372fb3c774 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -59,17 +59,17 @@ Rectangle { if (root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } - } else if (walletStatus === 1) { + } else if ((walletStatus === 1) || (walletStatus === 2) || (walletStatus === 3)) { if (root.activeView !== "notSetUp") { root.activeView = "notSetUp"; notSetUpTimer.start(); } - } else if (walletStatus === 2) { + } else if (walletStatus === 4) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; UserActivityLogger.commercePassphraseEntry("marketplace checkout"); } - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { authSuccessStep(); } else { console.log("ERROR in Checkout.qml: Unknown wallet status: " + walletStatus); diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml index b24af716ad..eeda3afc71 100644 --- a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -25,10 +25,13 @@ Rectangle { property string titleText; property string bodyImageSource; property string bodyText; + property string button1color: hifi.buttons.noneBorderlessGray; property string button1text; property string button1method; + property string button2color: hifi.buttons.noneBorderless; property string button2text; property string button2method; + property string buttonLayout: "leftright"; readonly property string securityPicBodyText: "When you see your Security Pic, your actions and data are securely making use of your " + "Wallet's private keys.

You can change your Security Pic in your Wallet."; @@ -39,6 +42,12 @@ Rectangle { color: Qt.rgba(0, 0, 0, 0.5); z: 999; + onVisibleChanged: { + if (!visible) { + resetLightbox(); + } + } + // This object is always used in a popup. // This MouseArea is used to prevent a user from being // able to click on a button/mouseArea underneath the popup. @@ -112,18 +121,21 @@ Rectangle { anchors.topMargin: 30; anchors.left: parent.left; anchors.right: parent.right; - height: 70; + height: root.buttonLayout === "leftright" ? 70 : 150; // Button 1 HifiControlsUit.Button { - color: hifi.buttons.noneBorderlessGray; + id: button1; + color: root.button1color; colorScheme: hifi.colorSchemes.light; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 20; + anchors.top: root.buttonLayout === "leftright" ? parent.top : parent.top; anchors.left: parent.left; anchors.leftMargin: 10; - width: root.button2text ? parent.width/2 - anchors.leftMargin*2 : parent.width - anchors.leftMargin * 2; + anchors.right: root.buttonLayout === "leftright" ? undefined : parent.right; + anchors.rightMargin: root.buttonLayout === "leftright" ? undefined : 10; + width: root.buttonLayout === "leftright" ? (root.button2text ? parent.width/2 - anchors.leftMargin*2 : parent.width - anchors.leftMargin * 2) : + (undefined); + height: 50; text: root.button1text; onClicked: { eval(button1method); @@ -132,15 +144,18 @@ Rectangle { // Button 2 HifiControlsUit.Button { + id: button2; visible: root.button2text; - color: hifi.buttons.noneBorderless; + color: root.button2color; colorScheme: hifi.colorSchemes.light; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 20; + anchors.top: root.buttonLayout === "leftright" ? parent.top : button1.bottom; + anchors.topMargin: root.buttonLayout === "leftright" ? undefined : 20; + anchors.left: root.buttonLayout === "leftright" ? undefined : parent.left; + anchors.leftMargin: root.buttonLayout === "leftright" ? undefined : 10; anchors.right: parent.right; anchors.rightMargin: 10; - width: parent.width/2 - anchors.rightMargin*2; + width: root.buttonLayout === "leftright" ? parent.width/2 - anchors.rightMargin*2 : undefined; + height: 50; text: root.button2text; onClicked: { eval(button2method); @@ -153,6 +168,19 @@ Rectangle { // FUNCTION DEFINITIONS START // signal sendToParent(var msg); + + function resetLightbox() { + root.titleText = ""; + root.bodyImageSource = ""; + root.bodyText = ""; + root.button1color = hifi.buttons.noneBorderlessGray; + root.button1text = ""; + root.button1method = ""; + root.button2color = hifi.buttons.noneBorderless; + root.button2text = ""; + root.button2method = ""; + root.buttonLayout = "leftright"; + } // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index a4b0f57e8f..eb8159a4e7 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -37,9 +37,9 @@ Item { onWalletStatusResult: { if (walletStatus === 0) { sendToParent({method: "needsLogIn"}); - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { Commerce.getSecurityImage(); - } else if (walletStatus > 3) { + } else if (walletStatus > 5) { console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus); } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 3612de7323..c505baebf4 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -47,17 +47,17 @@ Rectangle { if (root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } - } else if (walletStatus === 1) { + } else if ((walletStatus === 1) || (walletStatus === 2) || (walletStatus === 3)) { if (root.activeView !== "notSetUp") { root.activeView = "notSetUp"; notSetUpTimer.start(); } - } else if (walletStatus === 2) { + } else if (walletStatus === 4) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; UserActivityLogger.commercePassphraseEntry("marketplace purchases"); } - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { root.activeView = "firstUseTutorial"; } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ae42b8e3e1..952390a7a4 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -47,20 +47,22 @@ Rectangle { } } else if (walletStatus === 1) { if (root.activeView !== "walletSetup") { - root.activeView = "walletSetup"; - Commerce.resetLocalWalletOnly(); - var timestamp = new Date(); - walletSetup.startingTimestamp = timestamp; - walletSetup.setupAttemptID = generateUUID(); - UserActivityLogger.commerceWalletSetupStarted(timestamp, setupAttemptID, walletSetup.setupFlowVersion, walletSetup.referrer ? walletSetup.referrer : "wallet app", - (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '')); + walletResetSetup(); } } else if (walletStatus === 2) { + if (root.activeView != "preexisting") { + root.activeView = "preexisting"; + } + } else if (walletStatus === 3) { + if (root.activeView != "conflicting") { + root.activeView = "conflicting"; + } + } else if (walletStatus === 4) { if (root.activeView !== "passphraseModal") { root.activeView = "passphraseModal"; UserActivityLogger.commercePassphraseEntry("wallet app"); } - } else if (walletStatus === 3) { + } else if (walletStatus === 5) { if (root.activeView !== "walletSetup") { root.activeView = "walletHome"; Commerce.getSecurityImage(); @@ -169,6 +171,25 @@ Rectangle { // TITLE BAR END // + WalletChoice { + id: walletChoice; + proceedFunction: function (isReset) { + console.log(isReset ? "Reset wallet." : "Trying again with new wallet."); + Commerce.setSoftReset(); + if (isReset) { + walletResetSetup(); + } else { + var msg = { referrer: walletChoice.referrer } + followReferrer(msg); + } + } + copyFunction: Commerce.copyKeyFileFrom; + z: 997; + visible: (root.activeView === "preexisting") || (root.activeView === "conflicting"); + activeView: root.activeView; + anchors.fill: parent; + } + WalletSetup { id: walletSetup; visible: root.activeView === "walletSetup"; @@ -178,14 +199,7 @@ Rectangle { Connections { onSendSignalToWallet: { if (msg.method === 'walletSetup_finished') { - if (msg.referrer === '' || msg.referrer === 'marketplace cta') { - root.activeView = "initialize"; - Commerce.getWalletStatus(); - } else if (msg.referrer === 'purchases') { - sendToScript({method: 'goToPurchases'}); - } else { - sendToScript({method: 'goToMarketplaceItemPage', itemId: msg.referrer}); - } + followReferrer(msg); } else if (msg.method === 'walletSetup_raiseKeyboard') { root.keyboardRaised = true; root.isPassword = msg.isPasswordField; @@ -738,6 +752,7 @@ Rectangle { switch (message.method) { case 'updateWalletReferrer': walletSetup.referrer = message.referrer; + walletChoice.referrer = message.referrer; break; case 'inspectionCertificate_resetCert': // NOP @@ -768,6 +783,28 @@ Rectangle { return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } + + function walletResetSetup() { + root.activeView = "walletSetup"; + var timestamp = new Date(); + walletSetup.startingTimestamp = timestamp; + walletSetup.setupAttemptID = generateUUID(); + UserActivityLogger.commerceWalletSetupStarted(timestamp, walletSetup.setupAttemptID, walletSetup.setupFlowVersion, walletSetup.referrer ? walletSetup.referrer : "wallet app", + (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '')); + } + + function followReferrer(msg) { + if (msg.referrer === '' || msg.referrer === 'marketplace cta') { + root.activeView = "initialize"; + Commerce.getWalletStatus(); + } else if (msg.referrer === 'purchases') { + sendToScript({method: 'goToPurchases'}); + } else if (msg.referrer === 'marketplace cta' || msg.referrer === 'mainPage') { + sendToScript({method: 'goToMarketplaceMainPage', itemId: msg.referrer}); + } else { + sendToScript({method: 'goToMarketplaceItemPage', itemId: msg.referrer}); + } + } // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml b/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml new file mode 100644 index 0000000000..457db55fd1 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/WalletChoice.qml @@ -0,0 +1,297 @@ +// +// WalletChoice.qml +// qml/hifi/commerce/wallet +// +// WalletChoice +// +// Created by Howard Stearns +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../common" as HifiCommerceCommon +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit + + +Item { + HifiConstants { id: hifi; } + + id: root; + property string activeView: "conflict"; + property var proceedFunction: nil; + property var copyFunction: nil; + property string referrer: ""; + + Image { + anchors.fill: parent; + source: "images/wallet-bg.jpg"; + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup; + visible: false; + anchors.fill: parent; + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + anchors.right: parent.right; + + // Wallet icon + HiFiGlyphs { + id: walletIcon; + text: hifi.glyphs.wallet; + // Size + size: parent.height * 0.8; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.verticalCenter: parent.verticalCenter; + // Style + color: hifi.colors.blueHighlight; + } + + // Title Bar text + RalewayRegular { + id: titleBarText; + text: "Wallet Setup"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: walletIcon.right; + anchors.leftMargin: 8; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.white; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + // + // TITLE BAR END + // + + // + // MAIN PAGE START + // + Item { + id: preexistingContainer; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + + HiFiGlyphs { + id: bigKeyIcon; + text: hifi.glyphs.walletKey; + // Size + size: 180; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 40; + anchors.horizontalCenter: parent.horizontalCenter; + // Style + color: hifi.colors.white; + } + + RalewayRegular { + id: text01; + text: root.activeView === "preexisting" ? + "Where are your private keys?" : + "Hmm, your keys are different" + // Text size + size: 26; + // Anchors + anchors.top: bigKeyIcon.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + width: paintedWidth; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + RalewayRegular { + id: text02; + text: root.activeView === "preexisting" ? + "Our records indicate that you created a wallet, but the private keys are not in the folder where we checked." : + "Our records indicate that you created a wallet with different keys than the keys you're providing." + // Text size + size: 18; + // Anchors + anchors.top: text01.bottom; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 65; + anchors.right: parent.right; + anchors.rightMargin: 65; + height: paintedHeight; + width: paintedWidth; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // "Locate" button + HifiControlsUit.Button { + id: locateButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: text02.bottom; + anchors.topMargin: 40; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: root.activeView === "preexisting" ? + "LOCATE MY KEYS" : + "LOCATE OTHER KEYS" + onClicked: { + walletChooser(); + } + } + + // "Create New" OR "Continue" button + HifiControlsUit.Button { + id: button02; + color: hifi.buttons.none; + colorScheme: hifi.colorSchemes.dark; + anchors.top: locateButton.bottom; + anchors.topMargin: 20; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: root.activeView === "preexisting" ? + "CREATE NEW WALLET" : + "CONTINUE WITH THESE KEYS" + onClicked: { + lightboxPopup.titleText = "Are you sure?"; + lightboxPopup.bodyText = "Taking this step will abandon your old wallet and you will no " + + "longer be able to access your money and your past purchases.

" + + "This step should only be used if you cannot find your keys.

" + + "This step cannot be undone."; + lightboxPopup.button1color = hifi.buttons.red; + lightboxPopup.button1text = "YES, CREATE NEW WALLET"; + lightboxPopup.button1method = "root.visible = false;proceed(true);"; + lightboxPopup.button2text = "CANCEL"; + lightboxPopup.button2method = "root.visible = false;" + lightboxPopup.buttonLayout = "topbottom"; + lightboxPopup.visible = true; + } + } + + // "What's This?" link + RalewayRegular { + id: whatsThisLink; + text: 'What\'s this?'; + // Anchors + anchors.bottom: parent.bottom; + anchors.bottomMargin: 48; + anchors.horizontalCenter: parent.horizontalCenter; + width: paintedWidth; + height: paintedHeight; + // Text size + size: 18; + // Style + color: hifi.colors.white; + + MouseArea { + anchors.fill: parent; + + onClicked: { + if (root.activeView === "preexisting") { + lightboxPopup.titleText = "Your wallet's private keys are not in the folder we expected"; + lightboxPopup.bodyText = "We see that you have created a wallet but the private keys " + + "for it seem to have been moved to a different folder.

" + + "To tell us where the keys are, click 'Locate My Keys'.

" + + "If you'd prefer to create a new wallet (not recommended - you will lose your money and past " + + "purchases), click 'Create New Wallet'."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } else { + lightboxPopup.titleText = "You may have set up more than one wallet"; + lightboxPopup.bodyText = "We see that the private keys stored on your computer are different " + + "from the ones you used last time. This may mean that you set up more than one wallet. " + + "If you would like to use these keys, click 'Continue With These Keys'.

" + + "If you would prefer to use another wallet, click 'Locate Other Keys' to show us where " + + "you've stored the private keys for that wallet."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = "root.visible = false;" + lightboxPopup.visible = true; + } + } + } + } + } + // + // MAIN PAGE END + // + + // + // FUNCTION DEFINITIONS START + // + function onFileOpenChanged(filename) { + // disconnect the event, otherwise the requests will stack up + try { // Not all calls to onFileOpenChanged() connect an event. + Window.browseChanged.disconnect(onFileOpenChanged); + } catch (e) { + console.log('WalletChoice.qml ignoring', e); + } + if (filename) { + if (copyFunction && copyFunction(filename)) { + proceed(false); + } else { + console.log("WalletChoice.qml copyFunction", copyFunction, "failed."); + } + } // Else we're still at WalletChoice + } + function walletChooser() { + Window.browseChanged.connect(onFileOpenChanged); + Window.browseAsync("Locate your .hifikey file", "", "*.hifikey"); + } + function proceed(isReset) { + if (!proceedFunction) { + console.log("Provide a function of no arguments to WalletChoice.qml."); + } else { + proceedFunction(isReset); + } + } + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 83c1a2035d..27660b5e9e 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -310,7 +310,7 @@ Item { height: parent.height; HifiControlsUit.Separator { - colorScheme: 1; + colorScheme: 1; anchors.left: parent.left; anchors.right: parent.right; anchors.top: parent.top; @@ -318,20 +318,42 @@ Item { RalewayRegular { id: noActivityText; - text: "The Wallet app is in closed Beta.

To request entry and receive free HFC, please contact " + - "info@highfidelity.com with your High Fidelity account username and the email address registered to that account."; - // Text size - size: 24; - // Style - color: hifi.colors.blueAccent; - anchors.left: parent.left; - anchors.leftMargin: 12; - anchors.right: parent.right; - anchors.rightMargin: 12; - anchors.verticalCenter: parent.verticalCenter; - height: paintedHeight; - wrapMode: Text.WordWrap; - horizontalAlignment: Text.AlignHCenter; + text: "Congrats! Your wallet is all set!

" + + "Where's my HFC?
" + + "High Fidelity commerce is in open beta right now. Want more HFC? Get it by meeting with a banker at " + + "BankOfHighFidelity!" + // Text size + size: 22; + // Style + color: hifi.colors.blueAccent; + anchors.top: parent.top; + anchors.topMargin: 36; + anchors.left: parent.left; + anchors.leftMargin: 12; + anchors.right: parent.right; + anchors.rightMargin: 12; + height: paintedHeight; + wrapMode: Text.WordWrap; + horizontalAlignment: Text.AlignHCenter; + + onLinkActivated: { + sendSignalToWallet({ method: "transactionHistory_goToBank" }); + } + } + + HifiControlsUit.Button { + id: bankButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: noActivityText.bottom; + anchors.topMargin: 30; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: "VISIT BANK OF HIGH FIDELITY"; + onClicked: { + sendSignalToWallet({ method: "transactionHistory_goToBank" }); + } } } diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index dff441f840..712c505e8a 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -22,6 +22,8 @@ // inventory answers {status: 'success', data: {assets: [{id: "guid", title: "name", preview: "url"}....]}} // balance answers {status: 'success', data: {balance: integer}} // buy and receive_at answer {status: 'success'} +// account synthesizes a result {status: 'success', data: {keyStatus: "preexisting"|"conflicting"|"ok"}} + QJsonObject Ledger::apiResponse(const QString& label, QNetworkReply& reply) { QByteArray response = reply.readAll(); @@ -99,7 +101,7 @@ void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, cons signedSend("transaction", transactionString, hfc_key, "buy", "buySuccess", "buyFailure", controlled_failure); } -bool Ledger::receiveAt(const QString& hfc_key, const QString& old_key) { +bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key) { auto accountManager = DependencyManager::get(); if (!accountManager->isLoggedIn()) { qCWarning(commerce) << "Cannot set receiveAt when not logged in."; @@ -108,7 +110,7 @@ bool Ledger::receiveAt(const QString& hfc_key, const QString& old_key) { return false; // We know right away that we will fail, so tell the caller. } - signedSend("public_key", hfc_key.toUtf8(), old_key, "receive_at", "receiveAtSuccess", "receiveAtFailure"); + signedSend("public_key", hfc_key.toUtf8(), signing_key, "receive_at", "receiveAtSuccess", "receiveAtFailure"); return true; // Note that there may still be an asynchronous signal of failure that callers might be interested in. } @@ -179,7 +181,7 @@ QString transactionString(const QJsonObject& valueObject) { } else { result += valueObject["message"].toString(); } - + // no matter what we append a smaller date to the bottom of this... result += QString("
%1").arg(createdAt.toLocalTime().toString(Qt::DefaultLocaleShortDate)); return result; @@ -246,18 +248,33 @@ void Ledger::accountSuccess(QNetworkReply& reply) { auto iv = QByteArray::fromBase64(data["iv"].toString().toUtf8()); auto ckey = QByteArray::fromBase64(data["ckey"].toString().toUtf8()); QString remotePublicKey = data["public_key"].toString(); + bool isOverride = wallet->wasSoftReset(); wallet->setSalt(salt); wallet->setIv(iv); wallet->setCKey(ckey); + QString keyStatus = "ok"; QStringList localPublicKeys = wallet->listPublicKeys(); - if (remotePublicKey.isEmpty() && !localPublicKeys.isEmpty()) { - receiveAt(localPublicKeys.first(), ""); + if (remotePublicKey.isEmpty() || isOverride) { + if (!localPublicKeys.isEmpty()) { + QString key = localPublicKeys.first(); + receiveAt(key, key); + } + } else { + if (localPublicKeys.isEmpty()) { + keyStatus = "preexisting"; + } else if (localPublicKeys.first() != remotePublicKey) { + keyStatus = "conflicting"; + } } // none of the hfc account info should be emitted - emit accountResult(QJsonObject{ {"status", "success"} }); + QJsonObject json; + QJsonObject responseData{ { "status", "success"} }; + json["keyStatus"] = keyStatus; + responseData["data"] = json; + emit accountResult(responseData); } void Ledger::accountFailure(QNetworkReply& reply) { diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index ac9fe950d9..703ebda2dc 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -26,7 +26,7 @@ class Ledger : public QObject, public Dependency { public: void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); - bool receiveAt(const QString& hfc_key, const QString& old_key); + bool receiveAt(const QString& hfc_key, const QString& signing_key); void balance(const QStringList& keys); void inventory(const QStringList& keys); void history(const QStringList& keys, const int& pageNumber); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index e7d62930cf..557193c074 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -62,6 +62,11 @@ void QmlCommerce::getKeyFilePathIfExists() { emit keyFilePathIfExistsResult(wallet->getKeyFilePath()); } +bool QmlCommerce::copyKeyFileFrom(const QString& pathname) { + auto wallet = DependencyManager::get(); + return wallet->copyKeyFileFrom(pathname); +} + void QmlCommerce::getWalletAuthenticatedStatus() { auto wallet = DependencyManager::get(); emit walletAuthenticatedStatusResult(wallet->walletIsAuthenticatedWithPassphrase()); @@ -128,6 +133,11 @@ void QmlCommerce::changePassphrase(const QString& oldPassphrase, const QString& } } +void QmlCommerce::setSoftReset() { + auto wallet = DependencyManager::get(); + wallet->setSoftReset(); +} + void QmlCommerce::setPassphrase(const QString& passphrase) { auto wallet = DependencyManager::get(); wallet->setPassphrase(passphrase); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 09eb7137af..6a4eaa2be2 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -61,10 +61,12 @@ protected: Q_INVOKABLE void getKeyFilePathIfExists(); Q_INVOKABLE void getSecurityImage(); Q_INVOKABLE void getWalletAuthenticatedStatus(); + Q_INVOKABLE bool copyKeyFileFrom(const QString& pathname); Q_INVOKABLE void chooseSecurityImage(const QString& imageFile); Q_INVOKABLE void setPassphrase(const QString& passphrase); Q_INVOKABLE void changePassphrase(const QString& oldPassphrase, const QString& newPassphrase); + Q_INVOKABLE void setSoftReset(); Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 9599af827f..fad82115d6 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -59,6 +59,23 @@ QString keyFilePath() { auto accountManager = DependencyManager::get(); return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE)); } +bool Wallet::copyKeyFileFrom(const QString& pathname) { + QString existing = getKeyFilePath(); + qCDebug(commerce) << "Old keyfile" << existing; + if (!existing.isEmpty()) { + QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1, + QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", "")); + qCDebug(commerce) << "Renaming old keyfile to" << backup; + if (!QFile::rename(existing, backup)) { + qCCritical(commerce) << "Unable to backup" << existing << "to" << backup; + return false; + } + } + QString destination = keyFilePath(); + bool result = QFile::copy(pathname, destination); + qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result; + return result; +} // use the cached _passphrase if it exists, otherwise we need to prompt int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { @@ -300,17 +317,24 @@ Wallet::Wallet() { packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnershipPacket"); packetReceiver.registerListener(PacketType::ChallengeOwnershipRequest, this, "handleChallengeOwnershipPacket"); - connect(ledger.data(), &Ledger::accountResult, this, [&]() { + connect(ledger.data(), &Ledger::accountResult, this, [&](QJsonObject result) { auto wallet = DependencyManager::get(); auto walletScriptingInterface = DependencyManager::get(); uint status; + QString keyStatus = result.contains("data") ? result["data"].toObject()["keyStatus"].toString() : ""; if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) { - status = (uint)WalletStatus::WALLET_STATUS_NOT_SET_UP; + if (keyStatus == "preexisting") { + status = (uint) WalletStatus::WALLET_STATUS_PREEXISTING; + } else{ + status = (uint) WalletStatus::WALLET_STATUS_NOT_SET_UP; + } } else if (!wallet->walletIsAuthenticatedWithPassphrase()) { - status = (uint)WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; + status = (uint) WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; + } else if (keyStatus == "conflicting") { + status = (uint) WalletStatus::WALLET_STATUS_CONFLICTING; } else { - status = (uint)WalletStatus::WALLET_STATUS_READY; + status = (uint) WalletStatus::WALLET_STATUS_READY; } walletScriptingInterface->setWalletStatus(status); @@ -524,17 +548,17 @@ bool Wallet::generateKeyPair() { // TODO: redo this soon -- need error checking and so on writeSecurityImage(_securityImage, keyFilePath()); - QString oldKey = _publicKeys.count() == 0 ? "" : _publicKeys.last(); QString key = keyPair.first->toBase64(); _publicKeys.push_back(key); qCDebug(commerce) << "public key:" << key; + _isOverridingServer = false; // It's arguable whether we want to change the receiveAt every time, but: // 1. It's certainly needed the first time, when createIfNeeded answers true. // 2. It is maximally private, and we can step back from that later if desired. // 3. It maximally exercises all the machinery, so we are most likely to surface issues now. auto ledger = DependencyManager::get(); - return ledger->receiveAt(key, oldKey); + return ledger->receiveAt(key, key); } QStringList Wallet::listPublicKeys() { diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index fe3a9f1d5f..d771f404e5 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -35,6 +35,7 @@ public: void chooseSecurityImage(const QString& imageFile); bool getSecurityImage(); QString getKeyFilePath(); + bool copyKeyFileFrom(const QString& pathname); void setSalt(const QByteArray& salt) { _salt = salt; } QByteArray getSalt() { return _salt; } @@ -48,11 +49,15 @@ public: bool getPassphraseIsCached() { return !(_passphrase->isEmpty()); } bool walletIsAuthenticatedWithPassphrase(); bool changePassphrase(const QString& newPassphrase); + void setSoftReset() { _isOverridingServer = true; } + bool wasSoftReset() { bool was = _isOverridingServer; _isOverridingServer = false; return was; } void getWalletStatus(); enum WalletStatus { WALLET_STATUS_NOT_LOGGED_IN = 0, WALLET_STATUS_NOT_SET_UP, + WALLET_STATUS_PREEXISTING, + WALLET_STATUS_CONFLICTING, WALLET_STATUS_NOT_AUTHENTICATED, WALLET_STATUS_READY }; @@ -73,6 +78,7 @@ private: QByteArray _iv; QByteArray _ckey; QString* _passphrase { new QString("") }; + bool _isOverridingServer { false }; bool writeWallet(const QString& newPassphrase = QString("")); void updateImageProvider(); diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index d1a2ff3943..b324bf39c4 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -233,6 +234,7 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("Controller", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Pointers", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Web3DOverlay", this); + _webSurface->getSurfaceContext()->setContextProperty("Window", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 8cf5b72b9a..26ffb08796 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -655,6 +655,9 @@ case 'goToPurchases': tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); break; + case 'goToMarketplaceMainPage': + tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + break; case 'goToMarketplaceItemPage': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); break; @@ -688,6 +691,13 @@ updateSendMoneyParticleEffect(); sendMoneyParticleEffectUpdateTimer = Script.setInterval(updateSendMoneyParticleEffect, SEND_MONEY_PARTICLE_TIMER_UPDATE); break; + case 'transactionHistory_goToBank': + if (Account.metaverseServerURL.indexOf("staging") >= 0) { + Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging. + } else { + Window.location = "hifi://BankOfHighFidelity"; + } + break; default: print('Unrecognized message from QML:', JSON.stringify(message)); }