diff --git a/interface/resources/icons/tablet-icons/wallet-a.svg b/interface/resources/icons/tablet-icons/wallet-a.svg new file mode 100644 index 0000000000..4a0bab4b33 --- /dev/null +++ b/interface/resources/icons/tablet-icons/wallet-a.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + HFC + diff --git a/interface/resources/icons/tablet-icons/wallet-i.svg b/interface/resources/icons/tablet-icons/wallet-i.svg new file mode 100644 index 0000000000..2a16ecf973 --- /dev/null +++ b/interface/resources/icons/tablet-icons/wallet-i.svg @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + HFC + diff --git a/interface/resources/qml/hifi/commerce/Checkout.qml b/interface/resources/qml/hifi/commerce/Checkout.qml index 55bd6cb4c6..d9b0072917 100644 --- a/interface/resources/qml/hifi/commerce/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/Checkout.qml @@ -17,6 +17,7 @@ import QtQuick.Controls 1.4 import "../../styles-uit" import "../../controls-uit" as HifiControlsUit import "../../controls" as HifiControls +import "./wallet" as HifiWallet // references XXX from root context @@ -26,32 +27,33 @@ Rectangle { id: checkoutRoot; property bool inventoryReceived: false; property bool balanceReceived: false; - property string itemId: ""; + property string itemId: ""; property string itemHref: ""; property int balanceAfterPurchase: 0; property bool alreadyOwned: false; + property int itemPriceFull: 0; // Style color: hifi.colors.baseGray; Hifi.QmlCommerce { id: commerce; onBuyResult: { - if (result.status !== 'success') { - buyButton.text = result.message; - buyButton.enabled = false; - } else { - if (urlHandler.canHandleUrl(itemHref)) { - urlHandler.handleUrl(itemHref); - } - sendToScript({method: 'checkout_buySuccess', itemId: itemId}); + if (result.status !== 'success') { + buyButton.text = result.message; + buyButton.enabled = false; + } else { + if (urlHandler.canHandleUrl(itemHref)) { + urlHandler.handleUrl(itemHref); } + sendToScript({method: 'checkout_buySuccess', itemId: itemId}); + } } onBalanceResult: { if (result.status !== 'success') { console.log("Failed to get balance", result.message); } else { balanceReceived = true; - hfcBalanceText.text = result.data.balance; - balanceAfterPurchase = result.data.balance - parseInt(itemPriceText.text, 10); + hfcBalanceText.text = parseFloat(result.data.balance/100).toFixed(2); + balanceAfterPurchase = parseFloat(result.data.balance/100) - parseFloat(checkoutRoot.itemPriceFull/100).toFixed(2); } } onInventoryResult: { @@ -67,14 +69,6 @@ Rectangle { } } } - onSecurityImageResult: { - securityImage.source = securityImageSelection.getImagePathFromImageID(imageID); - } - } - - SecurityImageSelection { - id: securityImageSelection; - referrerURL: checkoutRoot.itemHref; } // @@ -89,20 +83,6 @@ Rectangle { anchors.left: parent.left; anchors.top: parent.top; - // Security Image - Image { - id: securityImage; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - height: parent.height - 5; - width: height; - anchors.verticalCenter: parent.verticalCenter; - fillMode: Image.PreserveAspectFit; - mipmap: true; - } - // Title Bar text RalewaySemiBold { id: titleBarText; @@ -111,7 +91,7 @@ Rectangle { size: hifi.fontSizes.overlayTitle; // Anchors anchors.top: parent.top; - anchors.left: securityImage.right; + anchors.left: parent.left; anchors.leftMargin: 16; anchors.bottom: parent.bottom; width: paintedWidth; @@ -132,7 +112,7 @@ Rectangle { // // TITLE BAR END // - + // // ITEM DESCRIPTION START // @@ -147,7 +127,7 @@ Rectangle { // Item Name text Item { - id: itemNameContainer; + id: itemNameContainer; // Anchors anchors.top: parent.top; anchors.topMargin: 4; @@ -188,11 +168,11 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } } - - + + // Item Author text Item { - id: itemAuthorContainer; + id: itemAuthorContainer; // Anchors anchors.top: itemNameContainer.bottom; anchors.topMargin: 4; @@ -233,10 +213,10 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } } - + // HFC Balance text Item { - id: hfcBalanceContainer; + id: hfcBalanceContainer; // Anchors anchors.top: itemAuthorContainer.bottom; anchors.topMargin: 16; @@ -278,10 +258,10 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } } - + // Item Price text Item { - id: itemPriceContainer; + id: itemPriceContainer; // Anchors anchors.top: hfcBalanceContainer.bottom; anchors.topMargin: 4; @@ -322,10 +302,10 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } } - + // HFC "Balance After Purchase" text Item { - id: hfcBalanceAfterPurchaseContainer; + id: hfcBalanceAfterPurchaseContainer; // Anchors anchors.top: itemPriceContainer.bottom; anchors.topMargin: 4; @@ -372,7 +352,7 @@ Rectangle { // ITEM DESCRIPTION END // - + // // ACTION BUTTONS START // @@ -420,7 +400,7 @@ Rectangle { text: (inventoryReceived && balanceReceived) ? (alreadyOwned ? "Already Owned: Get Item" : "Buy") : "--"; onClicked: { if (!alreadyOwned) { - commerce.buy(itemId, parseInt(itemPriceText.text)); + commerce.buy(itemId, parseFloat(itemPriceText.text*100)); } else { if (urlHandler.canHandleUrl(itemHref)) { urlHandler.handleUrl(itemHref); @@ -456,11 +436,11 @@ Rectangle { itemId = message.params.itemId; itemNameText.text = message.params.itemName; itemAuthorText.text = message.params.itemAuthor; - itemPriceText.text = message.params.itemPrice; + checkoutRoot.itemPriceFull = message.params.itemPrice; + itemPriceText.text = parseFloat(checkoutRoot.itemPriceFull/100).toFixed(2); itemHref = message.params.itemHref; commerce.balance(); commerce.inventory(); - commerce.getSecurityImage(); break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); diff --git a/interface/resources/qml/hifi/commerce/Inventory.qml b/interface/resources/qml/hifi/commerce/Inventory.qml index 8f22e7de0f..20458f9f16 100644 --- a/interface/resources/qml/hifi/commerce/Inventory.qml +++ b/interface/resources/qml/hifi/commerce/Inventory.qml @@ -17,6 +17,7 @@ import QtQuick.Controls 1.4 import "../../styles-uit" import "../../controls-uit" as HifiControlsUit import "../../controls" as HifiControls +import "./wallet" as HifiWallet // references XXX from root context @@ -33,7 +34,7 @@ Rectangle { if (result.status !== 'success') { console.log("Failed to get balance", result.message); } else { - hfcBalanceText.text = result.data.balance; + hfcBalanceText.text = parseFloat(result.data.balance/100).toFixed(2); } } onInventoryResult: { @@ -43,14 +44,6 @@ Rectangle { inventoryContentsList.model = result.data.assets; } } - onSecurityImageResult: { - securityImage.source = securityImageSelection.getImagePathFromImageID(imageID); - } - } - - SecurityImageSelection { - id: securityImageSelection; - referrerURL: inventoryRoot.referrerURL; } // @@ -65,20 +58,6 @@ Rectangle { anchors.left: parent.left; anchors.top: parent.top; - // Security Image - Image { - id: securityImage; - // Anchors - anchors.top: parent.top; - anchors.left: parent.left; - anchors.leftMargin: 16; - height: parent.height - 5; - width: height; - anchors.verticalCenter: parent.verticalCenter; - fillMode: Image.PreserveAspectFit; - mipmap: true; - } - // Title Bar text RalewaySemiBold { id: titleBarText; @@ -87,7 +66,7 @@ Rectangle { size: hifi.fontSizes.overlayTitle; // Anchors anchors.top: parent.top; - anchors.left: securityImage.right; + anchors.left: parent.left; anchors.leftMargin: 16; anchors.bottom: parent.bottom; width: paintedWidth; @@ -98,25 +77,6 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } - // "Change Security Image" button - HifiControlsUit.Button { - id: changeSecurityImageButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: 200; - text: "Change Security Image" - onClicked: { - securityImageSelection.isManuallyChangingSecurityImage = true; - securityImageSelection.visible = true; - } - } - // Separator HifiControlsUit.Separator { anchors.left: parent.left; @@ -132,7 +92,7 @@ Rectangle { // HFC BALANCE START // Item { - id: hfcBalanceContainer; + id: hfcBalanceContainer; // Size width: inventoryRoot.width; height: childrenRect.height + 20; @@ -177,7 +137,7 @@ Rectangle { // // HFC BALANCE END // - + // // INVENTORY CONTENTS START // @@ -192,7 +152,7 @@ Rectangle { anchors.topMargin: 8; anchors.bottom: actionButtonsContainer.top; anchors.bottomMargin: 8; - + RalewaySemiBold { id: inventoryContentsLabel; text: "Inventory:"; @@ -249,7 +209,7 @@ Rectangle { // // INVENTORY CONTENTS END // - + // // ACTION BUTTONS START // @@ -307,7 +267,6 @@ Rectangle { referrerURL = message.referrerURL; commerce.balance(); commerce.inventory(); - commerce.getSecurityImage(); break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); diff --git a/interface/resources/qml/hifi/commerce/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/SecurityImageSelection.qml deleted file mode 100644 index 7775f1ff9c..0000000000 --- a/interface/resources/qml/hifi/commerce/SecurityImageSelection.qml +++ /dev/null @@ -1,271 +0,0 @@ -// -// SecurityImageSelection.qml -// qml/hifi/commerce -// -// SecurityImageSelection -// -// Created by Zach Fox on 2017-08-15 -// Copyright 2017 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 QtQuick.Controls 1.4 -import "../../styles-uit" -import "../../controls-uit" as HifiControlsUit -import "../../controls" as HifiControls - -// references XXX from root context - -Rectangle { - HifiConstants { id: hifi; } - - id: securityImageSelectionRoot; - property string referrerURL: ""; - property bool isManuallyChangingSecurityImage: false; - anchors.fill: parent; - // Style - color: hifi.colors.baseGray; - z:999; // On top of everything else - visible: false; - - Hifi.QmlCommerce { - id: commerce; - onSecurityImageResult: { - if (!isManuallyChangingSecurityImage) { - securityImageSelectionRoot.visible = (imageID == 0); - } - if (imageID > 0) { - for (var itr = 0; itr < gridModel.count; itr++) { - var thisValue = gridModel.get(itr).securityImageEnumValue; - if (thisValue === imageID) { - securityImageGrid.currentIndex = itr; - break; - } - } - } - } - } - - Component.onCompleted: { - commerce.getSecurityImage(); - } - - // - // TITLE BAR START - // - Item { - id: titleBarContainer; - // Size - width: securityImageSelectionRoot.width; - height: 30; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - // Title Bar text - RalewaySemiBold { - id: titleBarText; - text: "Select a Security Image"; - // Text size - size: hifi.fontSizes.overlayTitle; - // Anchors - anchors.fill: parent; - anchors.leftMargin: 16; - // Style - color: hifi.colors.lightGrayText; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Separator - HifiControlsUit.Separator { - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; - } - } - // - // TITLE BAR END - // - - // - // EXPLANATION START - // - Item { - id: explanationContainer; - // Size - width: securityImageSelectionRoot.width; - height: 85; - // Anchors - anchors.top: titleBarContainer.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - - RalewayRegular { - id: explanationText; - text: "This image will be displayed on secure Inventory and Marketplace Checkout dialogs.
If you don't see your selected image on these dialogs, do not use them!
"; - // Text size - size: 16; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 4; - anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; - // Style - color: hifi.colors.lightGrayText; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHLeft; - verticalAlignment: Text.AlignVCenter; - } - - // Separator - HifiControlsUit.Separator { - anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; - } - } - // - // EXPLANATION END - // - - // - // SECURITY IMAGE GRID START - // - Item { - id: securityImageGridContainer; - // Anchors - anchors.left: parent.left; - anchors.leftMargin: 8; - anchors.right: parent.right; - anchors.rightMargin: 8; - anchors.top: explanationContainer.bottom; - anchors.topMargin: 8; - anchors.bottom: actionButtonsContainer.top; - anchors.bottomMargin: 8; - - SecurityImageModel { - id: gridModel; - } - - GridView { - id: securityImageGrid; - clip: true; - // Anchors - anchors.fill: parent; - currentIndex: -1; - cellWidth: width / 2; - cellHeight: height / 3; - model: gridModel; - delegate: Item { - width: securityImageGrid.cellWidth; - height: securityImageGrid.cellHeight; - Item { - anchors.fill: parent; - Image { - width: parent.width - 8; - height: parent.height - 8; - source: sourcePath; - anchors.horizontalCenter: parent.horizontalCenter; - anchors.verticalCenter: parent.verticalCenter; - fillMode: Image.PreserveAspectFit; - mipmap: true; - } - } - MouseArea { - anchors.fill: parent; - onClicked: { - securityImageGrid.currentIndex = index; - } - } - } - highlight: Rectangle { - width: securityImageGrid.cellWidth; - height: securityImageGrid.cellHeight; - color: hifi.colors.blueHighlight; - } - } - } - // - // SECURITY IMAGE GRID END - // - - - // - // ACTION BUTTONS START - // - Item { - id: actionButtonsContainer; - // Size - width: securityImageSelectionRoot.width; - height: 40; - // Anchors - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; - - // "Cancel" button - HifiControlsUit.Button { - id: cancelButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: "Cancel" - onClicked: { - if (!securityImageSelectionRoot.isManuallyChangingSecurityImage) { - sendToScript({method: 'securityImageSelection_cancelClicked', referrerURL: securityImageSelectionRoot.referrerURL}); - } else { - securityImageSelectionRoot.visible = false; - } - } - } - - // "Confirm" button - HifiControlsUit.Button { - id: confirmButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: "Confirm"; - onClicked: { - securityImageSelectionRoot.isManuallyChangingSecurityImage = false; - commerce.chooseSecurityImage(gridModel.get(securityImageGrid.currentIndex).securityImageEnumValue); - } - } - } - // - // ACTION BUTTONS END - // - - // - // FUNCTION DEFINITIONS START - // - signal sendToScript(var message); - - function getImagePathFromImageID(imageID) { - return (imageID ? gridModel.get(imageID - 1).sourcePath : ""); - } - // - // FUNCTION DEFINITIONS END - // -} diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml new file mode 100644 index 0000000000..2252cbfb59 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -0,0 +1,73 @@ +// +// SendMoney.qml +// qml/hifi/commerce/wallet +// +// SendMoney +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + + Hifi.QmlCommerce { + id: commerce; + } + + // "Unavailable" + RalewayRegular { + text: "Help me!"; + // Anchors + anchors.fill: parent; + // Text size + size: 24; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + default: + console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + } + } + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/NotSetUp.qml b/interface/resources/qml/hifi/commerce/wallet/NotSetUp.qml new file mode 100644 index 0000000000..3efb592ba1 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/NotSetUp.qml @@ -0,0 +1,127 @@ +// +// NotSetUp.qml +// qml/hifi/commerce/wallet +// +// NotSetUp +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + Hifi.QmlCommerce { + id: commerce; + } + + // + // TAB CONTENTS START + // + + // Text below title bar + RalewaySemiBold { + id: notSetUpText; + text: "Your Wallet Account Has Not Been Set Up"; + // Text size + size: 22; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 100; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // Explanitory text + RalewayRegular { + text: "To buy and sell items in High Fidelity Coin (HFC), you first need " + + "to set up your wallet.
You do not need to submit a credit card or personal information to set up your wallet."; + // Text size + size: 18; + // Anchors + anchors.top: notSetUpText.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: 100; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // "Set Up" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 150; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width/2; + height: 50; + text: "Set Up My Wallet"; + onClicked: { + sendSignalToWallet({method: 'setUpClicked'}); + } + } + + + // + // TAB CONTENTS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + default: + console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + } + } + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml new file mode 100644 index 0000000000..89ef851b06 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -0,0 +1,232 @@ +// +// PassphraseSelection.qml +// qml/hifi/commerce/wallet +// +// PassphraseSelection +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + + // 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; + } + + Hifi.QmlCommerce { + id: commerce; + onSecurityImageResult: { + passphrasePageSecurityImage.source = ""; + passphrasePageSecurityImage.source = "image://security/securityImage"; + } + + onPassphraseSetupStatusResult: { + sendMessageToLightbox({method: 'statusResult', status: passphraseIsSetup}); + } + } + + onVisibleChanged: { + if (visible) { + passphraseField.focus = true; + } + } + + SecurityImageModel { + id: gridModel; + } + + HifiControlsUit.TextField { + id: passphraseField; + anchors.top: parent.top; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.leftMargin: 16; + width: 280; + height: 50; + echoMode: TextInput.Password; + placeholderText: "passphrase"; + + onVisibleChanged: { + if (visible) { + text = ""; + } + } + } + HifiControlsUit.TextField { + id: passphraseFieldAgain; + anchors.top: passphraseField.bottom; + anchors.topMargin: 10; + anchors.left: passphraseField.left; + anchors.right: passphraseField.right; + height: 50; + echoMode: TextInput.Password; + placeholderText: "re-enter passphrase"; + + onVisibleChanged: { + if (visible) { + text = ""; + } + } + } + + // Security Image + Item { + id: securityImageContainer; + // Anchors + anchors.top: passphraseField.top; + anchors.left: passphraseField.right; + anchors.leftMargin: 12; + anchors.right: parent.right; + Image { + id: passphrasePageSecurityImage; + anchors.top: parent.top; + anchors.horizontalCenter: parent.horizontalCenter; + height: 75; + width: height; + fillMode: Image.PreserveAspectFit; + mipmap: true; + source: "image://security/securityImage"; + cache: false; + onVisibleChanged: { + commerce.getSecurityImage(); + } + } + // "Security picture" text below pic + RalewayRegular { + text: "security picture"; + // Text size + size: 12; + // Anchors + anchors.top: passphrasePageSecurityImage.bottom; + anchors.topMargin: 4; + anchors.left: securityImageContainer.left; + anchors.right: securityImageContainer.right; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + + // Error text below TextFields + RalewaySemiBold { + id: errorText; + text: ""; + // Text size + size: 16; + // Anchors + anchors.top: passphraseFieldAgain.bottom; + anchors.topMargin: 0; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 30; + // Style + color: hifi.colors.redHighlight; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Text below TextFields + RalewaySemiBold { + id: passwordReqs; + text: "Passphrase must be at least 4 characters"; + // Text size + size: 16; + // Anchors + anchors.top: passphraseFieldAgain.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 30; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Show passphrase text + HifiControlsUit.CheckBox { + id: showPassphrase; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.top: passwordReqs.bottom; + anchors.topMargin: 16; + height: 30; + text: "Show passphrase as plain text"; + boxSize: 24; + onClicked: { + passphraseField.echoMode = checked ? TextInput.Normal : TextInput.Password; + passphraseFieldAgain.echoMode = checked ? TextInput.Normal : TextInput.Password; + } + } + + // Text below checkbox + RalewayRegular { + text: "Your passphrase is used to encrypt your private keys. Please write it down. If it is lost, you will not be able to recover it."; + // Text size + size: 16; + // Anchors + anchors.top: showPassphrase.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + + function validateAndSubmitPassphrase() { + if (passphraseField.text.length < 4) { + setErrorText("Passphrase too short."); + return false; + } else if (passphraseField.text !== passphraseFieldAgain.text) { + setErrorText("Passphrases don't match."); + return false; + } else { + setErrorText(""); + commerce.setPassphrase(passphraseField.text); + return true; + } + } + + function setErrorText(text) { + errorText.text = text; + } + + signal sendMessageToLightbox(var msg); +} diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelectionLightbox.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelectionLightbox.qml new file mode 100644 index 0000000000..862d1894db --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelectionLightbox.qml @@ -0,0 +1,175 @@ +// +// PassphraseSelectionLightbox.qml +// qml/hifi/commerce/wallet +// +// PassphraseSelectionLightbox +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: root; + // Style + color: hifi.colors.baseGray; + + onVisibleChanged: { + if (visible) { + root.resetSubmitButton(); + } + } + + Connections { + target: passphraseSelection; + onSendMessageToLightbox: { + if (msg.method === 'statusResult') { + if (msg.status) { + // Success submitting new passphrase + root.resetSubmitButton(); + root.visible = false; + } else { + // Error submitting new passphrase + root.resetSubmitButton(); + passphraseSelection.setErrorText("Backend error"); + } + } + } + } + + // + // SECURE PASSPHRASE SELECTION START + // + Item { + id: choosePassphraseContainer; + // Anchors + anchors.fill: parent; + + Item { + id: passphraseTitle; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + text: "CHANGE PASSPHRASE"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + + // Text below title bar + RalewaySemiBold { + id: passphraseTitleHelper; + text: "Choose a Secure Passphrase"; + // Text size + size: 24; + // Anchors + anchors.top: passphraseTitle.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + PassphraseSelection { + id: passphraseSelection; + anchors.top: passphraseTitleHelper.bottom; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: passphraseNavBar.top; + } + + // Navigation Bar + Item { + id: passphraseNavBar; + // Size + width: parent.width; + height: 100; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + + // "Cancel" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: 100; + text: "Cancel" + onClicked: { + root.visible = false; + } + } + + // "Submit" button + HifiControlsUit.Button { + id: passphraseSubmitButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 100; + text: "Submit"; + onClicked: { + if (passphraseSelection.validateAndSubmitPassphrase()) { + passphraseSubmitButton.text = "Submitting..."; + passphraseSubmitButton.enabled = false; + } + } + } + } + } + // + // SECURE PASSPHRASE SELECTION END + // + + function resetSubmitButton() { + passphraseSubmitButton.enabled = true; + passphraseSubmitButton.text = "Submit"; + } +} diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml new file mode 100644 index 0000000000..3f3d00b401 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -0,0 +1,322 @@ +// +// Security.qml +// qml/hifi/commerce/wallet +// +// Security +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + + Hifi.QmlCommerce { + id: commerce; + + onSecurityImageResult: { + if (exists) { // "If security image is set up" + var path = "image://security/securityImage"; + topSecurityImage.source = ""; + topSecurityImage.source = path; + changeSecurityImageImage.source = ""; + changeSecurityImageImage.source = path; + changePassphraseImage.source = ""; + changePassphraseImage.source = path; + } + } + + onKeyFilePathResult: { + if (path !== "") { + keyFilePath.text = path; + } + } + } + + SecurityImageModel { + id: securityImageModel; + } + + // Username Text + RalewayRegular { + id: usernameText; + text: Account.username; + // Text size + size: 24; + // Style + color: hifi.colors.faintGray; + elide: Text.ElideRight; + // Anchors + anchors.top: securityImageContainer.top; + anchors.bottom: securityImageContainer.bottom; + anchors.left: parent.left; + anchors.right: securityImageContainer.left; + } + + // Security Image + Item { + id: securityImageContainer; + // Anchors + anchors.top: parent.top; + anchors.right: parent.right; + width: 75; + height: childrenRect.height; + + onVisibleChanged: { + if (visible) { + commerce.getSecurityImage(); + } + } + + Image { + id: topSecurityImage; + // Anchors + anchors.top: parent.top; + anchors.horizontalCenter: parent.horizontalCenter; + height: parent.width - 10; + width: height; + fillMode: Image.PreserveAspectFit; + mipmap: true; + source: "image://security/securityImage"; + cache: false; + } + // "Security picture" text below pic + RalewayRegular { + text: "security picture"; + // Text size + size: 12; + // Anchors + anchors.top: topSecurityImage.bottom; + anchors.topMargin: 4; + anchors.left: securityImageContainer.left; + anchors.right: securityImageContainer.right; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + + Item { + id: securityContainer; + anchors.top: securityImageContainer.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + RalewayRegular { + id: securityText; + text: "Security"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.faintGray; + } + + Item { + id: changePassphraseContainer; + anchors.top: securityText.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.right: parent.right; + height: 75; + + Image { + id: changePassphraseImage; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + height: parent.height; + width: height; + source: "image://security/securityImage"; + fillMode: Image.PreserveAspectFit; + mipmap: true; + cache: false; + } + // "Change Passphrase" button + HifiControlsUit.Button { + id: changePassphraseButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: changePassphraseImage.right; + anchors.leftMargin: 16; + width: 250; + height: 50; + text: "Change My Passphrase"; + onClicked: { + sendSignalToWallet({method: 'walletSecurity_changePassphrase'}); + } + } + } + + Item { + id: changeSecurityImageContainer; + anchors.top: changePassphraseContainer.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.right: parent.right; + height: 75; + + Image { + id: changeSecurityImageImage; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + height: parent.height; + width: height; + fillMode: Image.PreserveAspectFit; + mipmap: true; + cache: false; + source: "image://security/securityImage"; + } + // "Change Security Image" button + HifiControlsUit.Button { + id: changeSecurityImageButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.verticalCenter: parent.verticalCenter; + anchors.left: changeSecurityImageImage.right; + anchors.leftMargin: 16; + width: 250; + height: 50; + text: "Change My Security Image"; + onClicked: { + sendSignalToWallet({method: 'walletSecurity_changeSecurityImage'}); + } + } + } + } + + Item { + id: yourPrivateKeysContainer; + anchors.top: securityContainer.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + RalewaySemiBold { + id: yourPrivateKeysText; + text: "Your Private Keys"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.faintGray; + } + + // Text below "your private keys" + RalewayRegular { + id: explanitoryText; + text: "Your money and purchases are secured with private keys that only you " + + "have access to. If they are lost, you will not be able to access your money or purchases. " + + "To safeguard your private keys, back up this file regularly:"; + // Text size + size: 18; + // Anchors + anchors.top: yourPrivateKeysText.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + HifiControlsUit.TextField { + id: keyFilePath; + anchors.top: explanitoryText.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.right: clipboardButton.left; + height: 40; + readOnly: true; + + onVisibleChanged: { + if (visible) { + commerce.getKeyFilePath(); + } + } + } + HifiControlsUit.Button { + id: clipboardButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.top: keyFilePath.top; + anchors.bottom: keyFilePath.bottom; + width: height; + HiFiGlyphs { + text: hifi.glyphs.question; + // Size + size: parent.height*1.3; + // Anchors + anchors.fill: parent; + // Style + horizontalAlignment: Text.AlignHCenter; + color: enabled ? hifi.colors.white : hifi.colors.faintGray; + } + + onClicked: { + Window.copyToClipboard(keyFilePath.text); + } + } + } + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + default: + console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + } + } + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/SecurityImageModel.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml similarity index 85% rename from interface/resources/qml/hifi/commerce/SecurityImageModel.qml rename to interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml index 2fbf28683f..b49f16857b 100644 --- a/interface/resources/qml/hifi/commerce/SecurityImageModel.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageModel.qml @@ -4,7 +4,7 @@ // // SecurityImageModel // -// Created by Zach Fox on 2017-08-15 +// Created by Zach Fox on 2017-08-17 // Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -39,4 +39,8 @@ ListModel { sourcePath: "images/06gingerbread.jpg" securityImageEnumValue: 6; } + + function getImagePathFromImageID(imageID) { + return (imageID ? root.get(imageID - 1).sourcePath : ""); + } } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml new file mode 100644 index 0000000000..7ab52b7551 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -0,0 +1,98 @@ +// +// SecurityImageSelection.qml +// qml/hifi/commerce/wallet +// +// SecurityImageSelection +// +// Created by Zach Fox on 2017-08-17 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + + Hifi.QmlCommerce { + id: commerce; + onSecurityImageResult: { + } + } + + onVisibleChanged: { + if (visible) { + commerce.getSecurityImage(); + } + } + + SecurityImageModel { + id: gridModel; + } + + GridView { + id: securityImageGrid; + clip: true; + // Anchors + anchors.fill: parent; + currentIndex: -1; + cellWidth: width / 3; + cellHeight: height / 2; + model: gridModel; + delegate: Item { + width: securityImageGrid.cellWidth; + height: securityImageGrid.cellHeight; + Item { + anchors.fill: parent; + Image { + width: parent.width - 8; + height: parent.height - 8; + source: sourcePath; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.verticalCenter: parent.verticalCenter; + fillMode: Image.PreserveAspectFit; + mipmap: true; + } + } + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + onClicked: { + securityImageGrid.currentIndex = index; + } + } + } + highlight: Rectangle { + width: securityImageGrid.cellWidth; + height: securityImageGrid.cellHeight; + color: hifi.colors.blueHighlight; + } + } + + // + // FUNCTION DEFINITIONS START + // + signal sendToScript(var message); + + function getImagePathFromImageID(imageID) { + return (imageID ? gridModel.getImagePathFromImageID(imageID) : ""); + } + + function getSelectedImageIndex() { + return gridModel.get(securityImageGrid.currentIndex).securityImageEnumValue; + } + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelectionLightbox.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelectionLightbox.qml new file mode 100644 index 0000000000..d4b0b82ed3 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelectionLightbox.qml @@ -0,0 +1,200 @@ +// +// SecurityImageSelectionLightbox.qml +// qml/hifi/commerce/wallet +// +// SecurityImageSelectionLightbox +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: root; + property bool justSubmitted: false; + // Style + color: hifi.colors.baseGray; + + onVisibleChanged: { + if (visible) { + root.resetSubmitButton(); + } + } + + Hifi.QmlCommerce { + id: commerce; + + onSecurityImageResult: { + if (exists) { // Success submitting new security image + if (root.justSubmitted) { + root.resetSubmitButton(); + root.visible = false; + root.justSubmitted = false; + } + } else if (root.justSubmitted) { + // Error submitting new security image. + root.resetSubmitButton(); + root.justSubmitted = false; + } + } + } + + // + // SECURITY IMAGE SELECTION START + // + Item { + id: securityImageContainer; + // Anchors + anchors.fill: parent; + + Item { + id: securityImageTitle; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + text: "CHANGE SECURITY IMAGE"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + + // Text below title bar + RalewaySemiBold { + id: securityImageTitleHelper; + text: "Choose a Security Picture:"; + // Text size + size: 24; + // Anchors + anchors.top: securityImageTitle.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + height: 50; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + SecurityImageSelection { + id: securityImageSelection; + // Anchors + anchors.top: securityImageTitleHelper.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 280; + } + + // Text below security images + RalewayRegular { + text: "Your security picture shows you that the service asking for your passphrase is authorized. You can change your secure picture at any time."; + // Text size + size: 18; + // Anchors + anchors.top: securityImageSelection.bottom; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Navigation Bar + Item { + id: securityImageNavBar; + // Size + width: parent.width; + height: 100; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + + // "Cancel" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: 100; + text: "Cancel" + onClicked: { + root.visible = false; + } + } + + // "Submit" button + HifiControlsUit.Button { + id: securityImageSubmitButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 100; + text: "Submit"; + onClicked: { + root.justSubmitted = true; + securityImageSubmitButton.text = "Submitting..."; + securityImageSubmitButton.enabled = false; + var securityImagePath = securityImageSelection.getImagePathFromImageID(securityImageSelection.getSelectedImageIndex()) + commerce.chooseSecurityImage(securityImagePath); + } + } + } + } + // + // SECURITY IMAGE SELECTION END + // + + function resetSubmitButton() { + securityImageSubmitButton.enabled = true; + securityImageSubmitButton.text = "Submit"; + } +} diff --git a/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml b/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml new file mode 100644 index 0000000000..75334b1686 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml @@ -0,0 +1,73 @@ +// +// SendMoney.qml +// qml/hifi/commerce/wallet +// +// SendMoney +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + + Hifi.QmlCommerce { + id: commerce; + } + + // "Unavailable" + RalewayRegular { + text: "You currently cannot send money to other High Fidelity users."; + // Anchors + anchors.fill: parent; + // Text size + size: 24; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + default: + console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + } + } + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml new file mode 100644 index 0000000000..ac5d851bbc --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -0,0 +1,479 @@ +// +// Wallet.qml +// qml/hifi/commerce/wallet +// +// Wallet +// +// Created by Zach Fox on 2017-08-17 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: root; + + property string activeView: "walletHome"; + + // Style + color: hifi.colors.baseGray; + Hifi.QmlCommerce { + id: commerce; + + onSecurityImageResult: { + if (!exists) { // "If security image is not set up" + if (root.activeView !== "notSetUp") { + root.activeView = "notSetUp"; + } + } + } + + onKeyFilePathResult: { + if (path === "" && root.activeView !== "notSetUp") { + root.activeView = "notSetUp"; + } + } + } + + SecurityImageModel { + id: securityImageModel; + } + + Connections { + target: walletSetupLightbox; + onSendSignalToWallet: { + if (msg.method === 'walletSetup_cancelClicked') { + walletSetupLightbox.visible = false; + } else if (msg.method === 'walletSetup_finished') { + root.activeView = "walletHome"; + } else { + sendToScript(msg); + } + } + } + Connections { + target: notSetUp; + onSendSignalToWallet: { + if (msg.method === 'setUpClicked') { + walletSetupLightbox.visible = true; + } + } + } + + Rectangle { + id: walletSetupLightboxContainer; + visible: walletSetupLightbox.visible || passphraseSelectionLightbox.visible || securityImageSelectionLightbox.visible; + z: 998; + anchors.fill: parent; + color: "black"; + opacity: 0.5; + } + WalletSetupLightbox { + id: walletSetupLightbox; + visible: false; + z: 999; + anchors.centerIn: walletSetupLightboxContainer; + width: walletSetupLightboxContainer.width - 50; + height: walletSetupLightboxContainer.height - 50; + } + PassphraseSelectionLightbox { + id: passphraseSelectionLightbox; + visible: false; + z: 999; + anchors.centerIn: walletSetupLightboxContainer; + width: walletSetupLightboxContainer.width - 50; + height: walletSetupLightboxContainer.height - 50; + } + SecurityImageSelectionLightbox { + id: securityImageSelectionLightbox; + visible: false; + z: 999; + anchors.centerIn: walletSetupLightboxContainer; + width: walletSetupLightboxContainer.width - 50; + height: walletSetupLightboxContainer.height - 50; + } + + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + id: titleBarText; + text: "WALLET"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + // + // TAB CONTENTS START + // + NotSetUp { + id: notSetUp; + visible: root.activeView === "notSetUp"; + anchors.top: titleBarContainer.bottom; + anchors.bottom: tabButtonsContainer.top; + anchors.left: parent.left; + anchors.right: parent.right; + } + + WalletHome { + id: walletHome; + visible: root.activeView === "walletHome"; + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 16; + anchors.bottom: tabButtonsContainer.top; + anchors.bottomMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + } + + SendMoney { + id: sendMoney; + visible: root.activeView === "sendMoney"; + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 16; + anchors.bottom: tabButtonsContainer.top; + anchors.bottomMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + } + + Security { + id: security; + visible: root.activeView === "security"; + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 16; + anchors.bottom: tabButtonsContainer.top; + anchors.bottomMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + } + Connections { + target: security; + onSendSignalToWallet: { + if (msg.method === 'walletSecurity_changePassphrase') { + passphraseSelectionLightbox.visible = true; + } else if (msg.method === 'walletSecurity_changeSecurityImage') { + securityImageSelectionLightbox.visible = true; + } + } + } + + Help { + id: help; + visible: root.activeView === "help"; + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 16; + anchors.bottom: tabButtonsContainer.top; + anchors.bottomMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + } + + + // + // TAB CONTENTS END + // + + // + // TAB BUTTONS START + // + Item { + id: tabButtonsContainer; + property int numTabs: 5; + // Size + width: root.width; + height: 80; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + } + + // "WALLET HOME" tab button + Rectangle { + id: walletHomeButtonContainer; + visible: !notSetUp.visible; + color: root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: parent.width / tabButtonsContainer.numTabs; + + RalewaySemiBold { + text: "WALLET HOME"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 4; + anchors.rightMargin: 4; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + MouseArea { + enabled: !walletSetupLightboxContainer.visible; + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + root.activeView = "walletHome"; + tabButtonsContainer.resetTabButtonColors(); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = root.activeView === "walletHome" ? hifi.colors.blueAccent : hifi.colors.black; + } + + onVisibleChanged: { + if (visible) { + commerce.getSecurityImage(); + commerce.balance(); + } + } + } + + // "SEND MONEY" tab button + Rectangle { + id: sendMoneyButtonContainer; + visible: !notSetUp.visible; + color: hifi.colors.black; + anchors.top: parent.top; + anchors.left: walletHomeButtonContainer.right; + anchors.bottom: parent.bottom; + width: parent.width / tabButtonsContainer.numTabs; + + RalewaySemiBold { + text: "SEND MONEY"; + // Text size + size: 14; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 4; + anchors.rightMargin: 4; + // Style + color: hifi.colors.lightGray50; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + + // "EXCHANGE MONEY" tab button + Rectangle { + id: exchangeMoneyButtonContainer; + visible: !notSetUp.visible; + color: hifi.colors.black; + anchors.top: parent.top; + anchors.left: sendMoneyButtonContainer.right; + anchors.bottom: parent.bottom; + width: parent.width / tabButtonsContainer.numTabs; + + RalewaySemiBold { + text: "EXCHANGE MONEY"; + // Text size + size: 14; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 4; + anchors.rightMargin: 4; + // Style + color: hifi.colors.lightGray50; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + + // "SECURITY" tab button + Rectangle { + id: securityButtonContainer; + visible: !notSetUp.visible; + color: root.activeView === "security" ? hifi.colors.blueAccent : hifi.colors.black; + anchors.top: parent.top; + anchors.left: exchangeMoneyButtonContainer.right; + anchors.bottom: parent.bottom; + width: parent.width / tabButtonsContainer.numTabs; + + RalewaySemiBold { + text: "SECURITY"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 4; + anchors.rightMargin: 4; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + MouseArea { + enabled: !walletSetupLightboxContainer.visible; + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + root.activeView = "security"; + tabButtonsContainer.resetTabButtonColors(); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = root.activeView === "security" ? hifi.colors.blueAccent : hifi.colors.black; + } + + onVisibleChanged: { + if (visible) { + commerce.getSecurityImage(); + commerce.getKeyFilePath(); + } + } + } + + // "HELP" tab button + Rectangle { + id: helpButtonContainer; + visible: !notSetUp.visible; + color: root.activeView === "help" ? hifi.colors.blueAccent : hifi.colors.black; + anchors.top: parent.top; + anchors.left: securityButtonContainer.right; + anchors.bottom: parent.bottom; + width: parent.width / tabButtonsContainer.numTabs; + + RalewaySemiBold { + text: "HELP"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 4; + anchors.rightMargin: 4; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + MouseArea { + enabled: !walletSetupLightboxContainer.visible; + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + root.activeView = "help"; + tabButtonsContainer.resetTabButtonColors(); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = root.activeView === "help" ? hifi.colors.blueAccent : hifi.colors.black; + } + } + + function resetTabButtonColors() { + walletHomeButtonContainer.color = hifi.colors.black; + sendMoneyButtonContainer.color = hifi.colors.black; + securityButtonContainer.color = hifi.colors.black; + helpButtonContainer.color = hifi.colors.black; + if (root.activeView === "walletHome") { + walletHomeButtonContainer.color = hifi.colors.blueAccent; + } else if (root.activeView === "sendMoney") { + sendMoneyButtonContainer.color = hifi.colors.blueAccent; + } else if (root.activeView === "security") { + securityButtonContainer.color = hifi.colors.blueAccent; + } else if (root.activeView === "help") { + helpButtonContainer.color = hifi.colors.blueAccent; + } + } + } + // + // TAB BUTTONS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + default: + console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml new file mode 100644 index 0000000000..88f939d393 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -0,0 +1,357 @@ +// +// WalletHome.qml +// qml/hifi/commerce/wallet +// +// WalletHome +// +// Created by Zach Fox on 2017-08-18 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Item { + HifiConstants { id: hifi; } + + id: root; + + Hifi.QmlCommerce { + id: commerce; + + onSecurityImageResult: { + if (exists) { + // just set the source again (to be sure the change was noticed) + securityImage.source = ""; + securityImage.source = "image://security/securityImage"; + } + } + + onBalanceResult : { + balanceText.text = parseFloat(result.data.balance/100).toFixed(2); + } + } + + SecurityImageModel { + id: securityImageModel; + } + + Connections { + target: GlobalServices + onMyUsernameChanged: { + usernameText.text = Account.username; + } + } + + // Username Text + RalewayRegular { + id: usernameText; + text: Account.username; + // Text size + size: 24; + // Style + color: hifi.colors.faintGray; + elide: Text.ElideRight; + // Anchors + anchors.top: securityImageContainer.top; + anchors.bottom: securityImageContainer.bottom; + anchors.left: parent.left; + anchors.right: hfcBalanceContainer.left; + } + + // HFC Balance Container + Item { + id: hfcBalanceContainer; + // Anchors + anchors.top: securityImageContainer.top; + anchors.right: securityImageContainer.left; + anchors.rightMargin: 16; + width: 175; + height: 60; + Rectangle { + id: hfcBalanceField; + anchors.right: parent.right; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + height: parent.height - 15; + + // "HFC" balance label + RalewayRegular { + id: balanceLabel; + text: "HFC"; + // Text size + size: 20; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: hfcBalanceField.right; + anchors.rightMargin: 4; + width: paintedWidth; + // Style + color: hifi.colors.darkGray; + // Alignment + horizontalAlignment: Text.AlignRight; + verticalAlignment: Text.AlignVCenter; + + onVisibleChanged: { + if (visible) { + commerce.balance(); + } + } + } + + // Balance Text + FiraSansRegular { + id: balanceText; + text: "--"; + // Text size + size: 28; + // Anchors + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: balanceLabel.left; + anchors.rightMargin: 4; + // Style + color: hifi.colors.darkGray; + // Alignment + horizontalAlignment: Text.AlignRight; + verticalAlignment: Text.AlignVCenter; + } + } + // "balance" text above field + RalewayRegular { + text: "balance"; + // Text size + size: 12; + // Anchors + anchors.top: parent.top; + anchors.bottom: hfcBalanceField.top; + anchors.bottomMargin: 4; + anchors.left: hfcBalanceField.left; + anchors.right: hfcBalanceField.right; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + } + + // Security Image + Item { + id: securityImageContainer; + // Anchors + anchors.top: parent.top; + anchors.right: parent.right; + width: 75; + height: childrenRect.height; + + onVisibleChanged: { + if (visible) { + commerce.getSecurityImage(); + } + } + + Image { + id: securityImage; + // Anchors + anchors.top: parent.top; + anchors.horizontalCenter: parent.horizontalCenter; + height: parent.width - 10; + width: height; + fillMode: Image.PreserveAspectFit; + mipmap: true; + cache: false; + source: "image://security/securityImage"; + } + // "Security picture" text below pic + RalewayRegular { + text: "security picture"; + // Text size + size: 12; + // Anchors + anchors.top: securityImage.bottom; + anchors.topMargin: 4; + anchors.left: securityImageContainer.left; + anchors.right: securityImageContainer.right; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + + // Recent Activity + Item { + id: recentActivityContainer; + anchors.top: securityImageContainer.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: homeMessage.visible ? homeMessage.top : root.bottom; + anchors.bottomMargin: 10; + + RalewayRegular { + id: recentActivityText; + text: "Recent Activity"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.faintGray; + } + + Rectangle { + id: transactionHistory; + anchors.top: recentActivityText.bottom; + anchors.topMargin: 4; + anchors.bottom: toggleFullHistoryButton.top; + anchors.bottomMargin: 8; + anchors.left: parent.left; + anchors.right: parent.right; + + // some placeholder stuff + RalewayRegular { + text: homeMessage.visible ? "you CANNOT scroll through this." : "you CAN scroll through this"; + // Text size + size: 16; + // Anchors + anchors.fill: parent; + // Style + color: hifi.colors.darkGray; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + + HifiControlsUit.Button { + id: toggleFullHistoryButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + width: 250; + height: 40; + text: homeMessage.visible ? "See Full Transaction History" : "Collapse Transaction History"; + onClicked: { + if (homeMessage.visible) { + homeMessage.visible = false; + } else { + homeMessage.visible = true; + } + } + } + } + + // Item for "messages" - like "Welcome" + Item { + id: homeMessage; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: childrenRect.height; + + RalewayRegular { + id: messageText; + text: "Welcome! Let's get you some spending money.

" + + "Now that your account is all set up, click the button below to request your starter money. " + + "A robot will promptly review your request and put money into your account."; + // Text size + size: 16; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: 130; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + Item { + id: homeMessageButtons; + anchors.top: messageText.bottom; + anchors.topMargin: 4; + anchors.left: parent.left; + anchors.right: parent.right; + height: 40; + HifiControlsUit.Button { + id: noThanksButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + width: 100; + text: "No Thanks" + onClicked: { + messageText.text = "Okay...weird. Who doesn't like free money? If you change your mind, too bad. Sorry." + homeMessageButtons.visible = false; + } + } + HifiControlsUit.Button { + id: freeMoneyButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + width: 210; + text: "Free Money Please" + onClicked: { + messageText.text = "Go, MoneyRobots, Go!" + homeMessageButtons.visible = false; + } + } + } + } + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + default: + console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); + } + } + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetupLightbox.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetupLightbox.qml new file mode 100644 index 0000000000..474322062e --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetupLightbox.qml @@ -0,0 +1,633 @@ +// +// WalletSetupLightbox.qml +// qml/hifi/commerce/wallet +// +// WalletSetupLightbox +// +// Created by Zach Fox on 2017-08-17 +// Copyright 2017 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 QtQuick.Controls 1.4 +import "../../../styles-uit" +import "../../../controls-uit" as HifiControlsUit +import "../../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: root; + property string lastPage: "login"; + // Style + color: hifi.colors.baseGray; + + Hifi.QmlCommerce { + id: commerce; + + onLoginStatusResult: { + if (isLoggedIn) { + securityImageContainer.visible = true; + } else { + loginPageContainer.visible = true; + } + } + + onSecurityImageResult: { + if (!exists && root.lastPage === "securityImage") { + // ERROR! Invalid security image. + securityImageContainer.visible = true; + choosePassphraseContainer.visible = false; + } + } + + onPassphraseSetupStatusResult: { + securityImageContainer.visible = false; + if (passphraseIsSetup) { + privateKeysReadyContainer.visible = true; + } else { + choosePassphraseContainer.visible = true; + } + } + + onKeyFilePathResult: { + if (path !== "") { + keyFilePath.text = path; + } + } + } + + // + // LOGIN PAGE START + // + Item { + id: loginPageContainer; + visible: false; + // Anchors + anchors.fill: parent; + + Component.onCompleted: { + commerce.getLoginStatus(); + } + + Item { + id: loginTitle; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + text: "WALLET SETUP - LOGIN"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + + // Text below title bar + RalewaySemiBold { + id: loginTitleHelper; + text: "Please Log In to High Fidelity"; + // Text size + size: 24; + // Anchors + anchors.top: loginTitle.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Text below helper text + RalewaySemiBold { + id: loginDetailText; + text: "To set up your wallet, you must first log in to High Fidelity."; + // Text size + size: 18; + // Anchors + anchors.top: loginTitleHelper.bottom; + anchors.topMargin: 25; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Cancel" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: loginDetailText.bottom; + anchors.topMargin: 25; + anchors.left: parent.left; + anchors.leftMargin: 16; + width: 150; + height: 50; + text: "Log In" + onClicked: { + sendSignalToWallet({method: 'walletSetup_loginClicked'}); + } + } + + // Navigation Bar + Item { + // Size + width: parent.width; + height: 100; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + + // "Cancel" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: 100; + text: "Cancel" + onClicked: { + sendSignalToWallet({method: 'walletSetup_cancelClicked'}); + } + } + } + } + // + // LOGIN PAGE END + // + + // + // SECURITY IMAGE SELECTION START + // + Item { + id: securityImageContainer; + visible: false; + // Anchors + anchors.fill: parent; + + onVisibleChanged: { + if (visible) { + commerce.getSecurityImage(); + } + } + + Item { + id: securityImageTitle; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + text: "WALLET SETUP - STEP 1 OF 3"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + + // Text below title bar + RalewaySemiBold { + id: securityImageTitleHelper; + text: "Choose a Security Picture:"; + // Text size + size: 24; + // Anchors + anchors.top: securityImageTitle.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + height: 50; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + SecurityImageSelection { + id: securityImageSelection; + // Anchors + anchors.top: securityImageTitleHelper.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 280; + } + + // Text below security images + RalewayRegular { + text: "Your security picture shows you that the service asking for your passphrase is authorized. You can change your secure picture at any time."; + // Text size + size: 18; + // Anchors + anchors.top: securityImageSelection.bottom; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Navigation Bar + Item { + // Size + width: parent.width; + height: 100; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + + // "Cancel" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: 100; + text: "Cancel" + onClicked: { + sendSignalToWallet({method: 'walletSetup_cancelClicked'}); + } + } + + // "Next" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 100; + text: "Next"; + onClicked: { + root.lastPage = "securityImage"; + var securityImagePath = securityImageSelection.getImagePathFromImageID(securityImageSelection.getSelectedImageIndex()) + commerce.chooseSecurityImage(securityImagePath); + securityImageContainer.visible = false; + choosePassphraseContainer.visible = true; + } + } + } + } + // + // SECURITY IMAGE SELECTION END + // + + // + // SECURE PASSPHRASE SELECTION START + // + Item { + id: choosePassphraseContainer; + visible: false; + // Anchors + anchors.fill: parent; + + onVisibleChanged: { + if (visible) { + commerce.getPassphraseSetupStatus(); + } + } + + Item { + id: passphraseTitle; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + text: "WALLET SETUP - STEP 2 OF 3"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + + // Text below title bar + RalewaySemiBold { + id: passphraseTitleHelper; + text: "Choose a Secure Passphrase"; + // Text size + size: 24; + // Anchors + anchors.top: passphraseTitle.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + PassphraseSelection { + id: passphraseSelection; + anchors.top: passphraseTitleHelper.bottom; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: passphraseNavBar.top; + } + + // Navigation Bar + Item { + id: passphraseNavBar; + // Size + width: parent.width; + height: 100; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + + // "Back" button + HifiControlsUit.Button { + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: 100; + text: "Back" + onClicked: { + root.lastPage = "choosePassphrase"; + choosePassphraseContainer.visible = false; + securityImageContainer.visible = true; + } + } + + // "Next" button + HifiControlsUit.Button { + id: passphrasePageNextButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 100; + text: "Next"; + onClicked: { + if (passphraseSelection.validateAndSubmitPassphrase()) { + root.lastPage = "passphrase"; + choosePassphraseContainer.visible = false; + privateKeysReadyContainer.visible = true; + commerce.balance(); // Do this here so that keys are generated. Order might change as backend changes? + } + } + } + } + } + // + // SECURE PASSPHRASE SELECTION END + // + + // + // PRIVATE KEYS READY START + // + Item { + id: privateKeysReadyContainer; + visible: false; + // Anchors + anchors.fill: parent; + + Item { + id: keysReadyTitle; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + text: "WALLET SETUP - STEP 3 OF 3"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + + // Text below title bar + RalewaySemiBold { + id: keysReadyTitleHelper; + text: "Your Private Keys are Ready"; + // Text size + size: 24; + // Anchors + anchors.top: keysReadyTitle.bottom; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: 50; + // Style + color: hifi.colors.faintGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Text below checkbox + RalewayRegular { + id: explanationText; + text: "Your money and purchases are secured with private keys that only you have access to. " + + "If they are lost, you will not be able to access your money or purchases.

" + + "To protect your privacy, High Fidelity has no access to your private keys and cannot " + + "recover them for any reason.

To safeguard your private keys, backup this file on a regular basis:
"; + // Text size + size: 16; + // Anchors + anchors.top: keysReadyTitleHelper.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: paintedHeight; + // Style + color: hifi.colors.faintGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + HifiControlsUit.TextField { + id: keyFilePath; + anchors.top: explanationText.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: clipboardButton.left; + height: 40; + readOnly: true; + + onVisibleChanged: { + if (visible) { + commerce.getKeyFilePath(); + } + } + } + HifiControlsUit.Button { + id: clipboardButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.rightMargin: 16; + anchors.top: keyFilePath.top; + anchors.bottom: keyFilePath.bottom; + width: height; + HiFiGlyphs { + text: hifi.glyphs.question; + // Size + size: parent.height*1.3; + // Anchors + anchors.fill: parent; + // Style + horizontalAlignment: Text.AlignHCenter; + color: enabled ? hifi.colors.white : hifi.colors.faintGray; + } + + onClicked: { + Window.copyToClipboard(keyFilePath.text); + } + } + + // Navigation Bar + Item { + // Size + width: parent.width; + height: 100; + // Anchors: + anchors.left: parent.left; + anchors.bottom: parent.bottom; + // "Next" button + HifiControlsUit.Button { + id: keysReadyPageNextButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 100; + text: "Finish"; + onClicked: { + root.visible = false; + sendSignalToWallet({method: 'walletSetup_finished'}); + } + } + } + } + // + // PRIVATE KEYS READY END + // + + // + // FUNCTION DEFINITIONS START + // + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/images/01cat.jpg b/interface/resources/qml/hifi/commerce/wallet/images/01cat.jpg new file mode 100644 index 0000000000..6e7897cb82 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/01cat.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/02car.jpg b/interface/resources/qml/hifi/commerce/wallet/images/02car.jpg new file mode 100644 index 0000000000..5dd8091e57 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/02car.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/03dog.jpg b/interface/resources/qml/hifi/commerce/wallet/images/03dog.jpg new file mode 100644 index 0000000000..4a85b80c0c Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/03dog.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/04stars.jpg b/interface/resources/qml/hifi/commerce/wallet/images/04stars.jpg new file mode 100644 index 0000000000..8f2bf62f83 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/04stars.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/05plane.jpg b/interface/resources/qml/hifi/commerce/wallet/images/05plane.jpg new file mode 100644 index 0000000000..6504459d8b Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/05plane.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/images/06gingerbread.jpg b/interface/resources/qml/hifi/commerce/wallet/images/06gingerbread.jpg new file mode 100644 index 0000000000..54c37faa2f Binary files /dev/null and b/interface/resources/qml/hifi/commerce/wallet/images/06gingerbread.jpg differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d3b547a54c..affbf83081 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -183,6 +183,7 @@ #include "ui/UpdateDialog.h" #include "ui/overlays/Overlays.h" #include "ui/DomainConnectionModel.h" +#include "ui/ImageProvider.h" #include "Util.h" #include "InterfaceParentFinder.h" #include "ui/OctreeStatsProvider.h" @@ -2165,6 +2166,9 @@ void Application::initializeUi() { qApp->quit(); }); + // register the pixmap image provider (used only for security image, for now) + engine->addImageProvider(ImageProvider::PROVIDER_NAME, new ImageProvider()); + setupPreferences(); // For some reason there is already an "Application" object in the QML context, @@ -3757,7 +3761,7 @@ bool Application::shouldPaint() { (float)paintDelaySamples / paintDelayUsecs << "us"; } #endif - + // Throttle if requested if (displayPlugin->isThrottled() && (_lastTimeUpdated.elapsed() < THROTTLED_SIM_FRAME_PERIOD_MS)) { return false; @@ -6297,7 +6301,7 @@ bool Application::askToReplaceDomainContent(const QString& url) { OffscreenUi::warning("Unable to replace content", "You do not have permissions to replace domain content", QMessageBox::Ok, QMessageBox::Ok); } - QJsonObject messageProperties = { + QJsonObject messageProperties = { { "status", methodDetails }, { "content_set_url", url } }; @@ -6380,7 +6384,7 @@ void Application::showAssetServerWidget(QString filePath) { void Application::addAssetToWorldFromURL(QString url) { qInfo(interfaceapp) << "Download model and add to world from" << url; - + QString filename; if (url.contains("filename")) { filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index b3d6a5e67a..f1e9fa139e 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -14,6 +14,7 @@ #include "DependencyManager.h" #include "Ledger.h" #include "Wallet.h" +#include HIFI_QML_DEF(QmlCommerce) @@ -24,6 +25,7 @@ QmlCommerce::QmlCommerce(QQuickItem* parent) : OffscreenQmlDialog(parent) { connect(ledger.data(), &Ledger::balanceResult, this, &QmlCommerce::balanceResult); connect(ledger.data(), &Ledger::inventoryResult, this, &QmlCommerce::inventoryResult); connect(wallet.data(), &Wallet::securityImageResult, this, &QmlCommerce::securityImageResult); + connect(wallet.data(), &Wallet::keyFilePathResult, this, &QmlCommerce::keyFilePathResult); } void QmlCommerce::buy(const QString& assetId, int cost, const QString& buyerUsername) { @@ -50,11 +52,24 @@ void QmlCommerce::inventory() { ledger->inventory(wallet->listPublicKeys()); } -void QmlCommerce::chooseSecurityImage(uint imageID) { +void QmlCommerce::chooseSecurityImage(const QString& imageFile) { auto wallet = DependencyManager::get(); - wallet->chooseSecurityImage(imageID); + wallet->chooseSecurityImage(imageFile); } void QmlCommerce::getSecurityImage() { auto wallet = DependencyManager::get(); wallet->getSecurityImage(); -} \ No newline at end of file +} +void QmlCommerce::getLoginStatus() { + emit loginStatusResult(DependencyManager::get()->isLoggedIn()); +} +void QmlCommerce::setPassphrase(const QString& passphrase) { + emit passphraseSetupStatusResult(true); +} +void QmlCommerce::getPassphraseSetupStatus() { + emit passphraseSetupStatusResult(false); +} +void QmlCommerce::getKeyFilePath() { + auto wallet = DependencyManager::get(); + wallet->getKeyFilePath(); +} diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 4112ab7177..d7a892cf1f 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -18,6 +18,8 @@ #include #include +#include + class QmlCommerce : public OffscreenQmlDialog { Q_OBJECT HIFI_QML_DECL @@ -27,18 +29,25 @@ public: signals: void buyResult(QJsonObject result); - // Balance and Inventory are NOT properties, because QML can't change them (without risk of failure), and + // Balance and Inventory are NOT properties, because QML can't change them (without risk of failure), and // because we can't scalably know of out-of-band changes (e.g., another machine interacting with the block chain). void balanceResult(QJsonObject result); void inventoryResult(QJsonObject result); - void securityImageResult(uint imageID); + void securityImageResult(bool exists); + void loginStatusResult(bool isLoggedIn); + void passphraseSetupStatusResult(bool passphraseIsSetup); + void keyFilePathResult(const QString& path); protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const QString& buyerUsername = ""); Q_INVOKABLE void balance(); Q_INVOKABLE void inventory(); - Q_INVOKABLE void chooseSecurityImage(uint imageID); + Q_INVOKABLE void chooseSecurityImage(const QString& imageFile); Q_INVOKABLE void getSecurityImage(); + Q_INVOKABLE void getLoginStatus(); + Q_INVOKABLE void setPassphrase(const QString& passphrase); + Q_INVOKABLE void getPassphraseSetupStatus(); + Q_INVOKABLE void getKeyFilePath(); }; #endif // hifi_QmlCommerce_h diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 05ccea3a59..5fefd0c43b 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -12,10 +12,15 @@ #include "CommerceLogging.h" #include "Ledger.h" #include "Wallet.h" +#include "Application.h" +#include "ui/ImageProvider.h" #include +#include +#include #include +#include #include #include @@ -23,8 +28,10 @@ #include #include #include +#include static const char* KEY_FILE = "hifikey"; +static const char* IMAGE_FILE = "hifi_image"; // eventually this will live in keyfile void initialize() { static bool initialized = false; @@ -40,18 +47,30 @@ QString keyFilePath() { return PathUtils::getAppDataFilePath(KEY_FILE); } -// for now the callback function just returns the same string. Later we can hook -// this to the gui (some thought required) +QString imageFilePath() { + return PathUtils::getAppDataFilePath(IMAGE_FILE); +} + +// use the cached _passphrase if it exists, otherwise we need to prompt int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { // just return a hardcoded pwd for now - static const char* pwd = "pwd"; - strcpy(password, pwd); - return static_cast(strlen(pwd)); + auto passphrase = DependencyManager::get()->getPassphrase(); + if (passphrase) { + strcpy(password, passphrase->toLocal8Bit().constData()); + return static_cast(passphrase->size()); + } else { + // ok gotta bring up modal dialog... But right now lets just + // just keep it empty + return 0; + } } // BEGIN copied code - this will be removed/changed at some point soon // copied (without emits for various signals) from libraries/networking/src/RSAKeypairGenerator.cpp. // We will have a different implementation in practice, but this gives us a start for now +// +// NOTE: we don't really use the private keys returned - we can see how this evolves, but probably +// we should just return a list of public keys? QPair generateRSAKeypair() { RSA* keyPair = RSA_new(); @@ -106,9 +125,11 @@ QPair generateRSAKeypair() { // now lets persist them to files - // TODO: figure out a scheme for multiple keys, etc... + // FIXME: for now I'm appending to the file if it exists. As long as we always put + // the keys in the same order, this works fine. TODO: verify this will skip over + // anything else (like an embedded image) FILE* fp; - if ((fp = fopen(keyFilePath().toStdString().c_str(), "wt"))) { + if ((fp = fopen(keyFilePath().toStdString().c_str(), "at"))) { if (!PEM_write_RSAPublicKey(fp, keyPair)) { fclose(fp); qCDebug(commerce) << "failed to write public key"; @@ -125,7 +146,9 @@ QPair generateRSAKeypair() { RSA_free(keyPair); - // prepare the return values + // prepare the return values. TODO: Fix this - we probably don't really even want the + // private key at all (better to read it when we need it?). Or maybe we do, when we have + // multiple keys? retval.first = new QByteArray(reinterpret_cast(publicKeyDER), publicKeyLength ), retval.second = new QByteArray(reinterpret_cast(privateKeyDER), privateKeyLength ); @@ -192,6 +215,126 @@ RSA* readPrivateKey(const char* filename) { return key; } +static const unsigned char IVEC[16] = "IAmAnIVecYay123"; + +void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) { + // first ivec + memcpy(ivec, IVEC, 16); + auto hash = QCryptographicHash::hash(salt, QCryptographicHash::Md5); + memcpy(ckey, hash.data(), 16); +} + +void Wallet::setPassphrase(const QString& passphrase) { + if (_passphrase) { + delete _passphrase; + } + _passphrase = new QString(passphrase); +} + +// encrypt some stuff +bool Wallet::encryptFile(const QString& inputFilePath, const QString& outputFilePath) { + // aes requires a couple 128-bit keys (ckey and ivec). For now, I'll just + // use the md5 of the salt as the ckey (md5 is 128-bit), and ivec will be + // a constant. We can review this later - there are ways to generate keys + // from a password that may be better. + unsigned char ivec[16]; + unsigned char ckey[16]; + + initializeAESKeys(ivec, ckey, _salt); + + int tempSize, outSize; + + // read entire unencrypted file into memory + QFile inputFile(inputFilePath); + if (!inputFile.exists()) { + qCDebug(commerce) << "cannot encrypt" << inputFilePath << "file doesn't exist"; + return false; + } + inputFile.open(QIODevice::ReadOnly); + QByteArray inputFileBuffer = inputFile.readAll(); + inputFile.close(); + + // reserve enough capacity for encrypted bytes + unsigned char* outputFileBuffer = new unsigned char[inputFileBuffer.size() + AES_BLOCK_SIZE]; + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + + // TODO: add error handling!!! + if (!EVP_EncryptInit_ex(ctx, EVP_des_ede3_cbc(), NULL, ckey, ivec)) { + qCDebug(commerce) << "encrypt init failure"; + delete[] outputFileBuffer; + return false; + } + if (!EVP_EncryptUpdate(ctx, outputFileBuffer, &tempSize, (unsigned char*)inputFileBuffer.data(), inputFileBuffer.size())) { + qCDebug(commerce) << "encrypt update failure"; + delete[] outputFileBuffer; + return false; + } + outSize = tempSize; + if (!EVP_EncryptFinal_ex(ctx, outputFileBuffer + outSize, &tempSize)) { + qCDebug(commerce) << "encrypt final failure"; + delete[] outputFileBuffer; + return false; + } + + outSize += tempSize; + EVP_CIPHER_CTX_free(ctx); + qCDebug(commerce) << "encrypted buffer size" << outSize; + QByteArray output((const char*)outputFileBuffer, outSize); + QFile outputFile(outputFilePath); + outputFile.open(QIODevice::WriteOnly); + outputFile.write(output); + outputFile.close(); + + delete[] outputFileBuffer; + return true; +} + +bool Wallet::decryptFile(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferSize) { + unsigned char ivec[16]; + unsigned char ckey[16]; + initializeAESKeys(ivec, ckey, _salt); + + // read encrypted file + QFile inputFile(inputFilePath); + if (!inputFile.exists()) { + qCDebug(commerce) << "cannot decrypt file" << inputFilePath << "it doesn't exist"; + return false; + } + inputFile.open(QIODevice::ReadOnly); + QByteArray encryptedBuffer = inputFile.readAll(); + inputFile.close(); + + // setup decrypted buffer + unsigned char* outputBuffer = new unsigned char[encryptedBuffer.size()]; + int tempSize; + + // TODO: add error handling + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!EVP_DecryptInit_ex(ctx, EVP_des_ede3_cbc(), NULL, ckey, ivec)) { + qCDebug(commerce) << "decrypt init failure"; + delete[] outputBuffer; + return false; + } + if (!EVP_DecryptUpdate(ctx, outputBuffer, &tempSize, (unsigned char*)encryptedBuffer.data(), encryptedBuffer.size())) { + qCDebug(commerce) << "decrypt update failure"; + delete[] outputBuffer; + return false; + } + *outputBufferSize = tempSize; + if (!EVP_DecryptFinal_ex(ctx, outputBuffer + tempSize, &tempSize)) { + qCDebug(commerce) << "decrypt final failure"; + delete[] outputBuffer; + return false; + } + EVP_CIPHER_CTX_free(ctx); + *outputBufferSize += tempSize; + *outputBufferPtr = outputBuffer; + qCDebug(commerce) << "decrypted buffer size" << *outputBufferSize; + delete[] outputBuffer; + return true; +} + bool Wallet::createIfNeeded() { if (_publicKeys.count() > 0) return false; @@ -205,7 +348,7 @@ bool Wallet::createIfNeeded() { qCDebug(commerce) << "read private key"; RSA_free(key); // K -- add the public key since we have a legit private key associated with it - _publicKeys.push_back(publicKey.toBase64()); + _publicKeys.push_back(QUrl::toPercentEncoding(publicKey.toBase64())); return false; } } @@ -228,6 +371,7 @@ bool Wallet::generateKeyPair() { auto ledger = DependencyManager::get(); return ledger->receiveAt(key, oldKey); } + QStringList Wallet::listPublicKeys() { qCInfo(commerce) << "Enumerating public keys."; createIfNeeded(); @@ -268,10 +412,65 @@ QString Wallet::signWithKey(const QByteArray& text, const QString& key) { } -void Wallet::chooseSecurityImage(uint imageID) { - _chosenSecurityImage = (SecurityImage)imageID; - emit securityImageResult(imageID); +void Wallet::chooseSecurityImage(const QString& filename) { + + if (_securityImage) { + delete _securityImage; + } + // temporary... + QString path = qApp->applicationDirPath(); + path.append("/resources/qml/hifi/commerce/"); + path.append(filename); + // now create a new security image pixmap + _securityImage = new QPixmap(); + + qCDebug(commerce) << "loading data for pixmap from" << path; + _securityImage->load(path); + + // encrypt it and save. + if (encryptFile(path, imageFilePath())) { + qCDebug(commerce) << "emitting pixmap"; + + // inform the image provider + auto engine = DependencyManager::get()->getSurfaceContext()->engine(); + auto imageProvider = reinterpret_cast(engine->imageProvider(ImageProvider::PROVIDER_NAME)); + imageProvider->setSecurityImage(_securityImage); + + emit securityImageResult(true); + } else { + qCDebug(commerce) << "failed to encrypt security image"; + emit securityImageResult(false); + } } + void Wallet::getSecurityImage() { - emit securityImageResult(_chosenSecurityImage); + unsigned char* data; + int dataLen; + + // if already decrypted, don't do it again + if (_securityImage) { + emit securityImageResult(true); + return; + } + + // decrypt and return + if (decryptFile(imageFilePath(), &data, &dataLen)) { + // create the pixmap + _securityImage = new QPixmap(); + _securityImage->loadFromData(data, dataLen, "jpg"); + qCDebug(commerce) << "created pixmap from encrypted file"; + + // inform the image provider + auto engine = DependencyManager::get()->getSurfaceContext()->engine(); + auto imageProvider = reinterpret_cast(engine->imageProvider(ImageProvider::PROVIDER_NAME)); + imageProvider->setSecurityImage(_securityImage); + + emit securityImageResult(true); + } else { + qCDebug(commerce) << "failed to decrypt security image (maybe none saved yet?)"; + emit securityImageResult(false); + } +} +void Wallet::getKeyFilePath() { + emit keyFilePathResult(keyFilePath()); } diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index a1c7c7752b..4c0d7190bb 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -16,6 +16,8 @@ #include +#include + class Wallet : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY @@ -26,16 +28,21 @@ public: bool generateKeyPair(); QStringList listPublicKeys(); QString signWithKey(const QByteArray& text, const QString& key); - void chooseSecurityImage(uint imageID); + void chooseSecurityImage(const QString& imageFile); void getSecurityImage(); + void getKeyFilePath(); + + void setSalt(const QByteArray& salt) { _salt = salt; } + QByteArray getSalt() { return _salt; } + + void setPassphrase(const QString& passphrase); + QString* getPassphrase() { return _passphrase; } signals: - void securityImageResult(uint imageID); + void securityImageResult(bool exists) ; + void keyFilePathResult(const QString& path); protected: - // ALWAYS add SecurityImage enum values to the END of the enum. - // They must be in the same order as the images are listed in - // SecurityImageSelection.qml enum SecurityImage { NONE = 0, Cat, @@ -48,7 +55,12 @@ protected: private: QStringList _publicKeys{}; - SecurityImage _chosenSecurityImage = SecurityImage::NONE; + QPixmap* _securityImage { nullptr }; + QByteArray _salt {"iamsalt!"}; + QString* _passphrase { new QString("pwd") }; + + bool encryptFile(const QString& inputFilePath, const QString& outputFilePath); + bool decryptFile(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferLen); }; #endif // hifi_Wallet_h diff --git a/interface/src/ui/ImageProvider.cpp b/interface/src/ui/ImageProvider.cpp new file mode 100644 index 0000000000..4925cdf1e9 --- /dev/null +++ b/interface/src/ui/ImageProvider.cpp @@ -0,0 +1,29 @@ +// +// ImageProvider.cpp +// interface/src/ui +// +// Created by David Kelly on 8/23/2017. +// Copyright 2017 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 +// + +#include "ImageProvider.h" +#include + +const QString ImageProvider::PROVIDER_NAME = "security"; + +QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) { + + // adjust the internal pixmap to have the requested size + if (id == "securityImage" && _securityImage) { + *size = _securityImage->size(); + if (requestedSize.width() > 0 && requestedSize.height() > 0) { + return _securityImage->scaled(requestedSize.width(), requestedSize.height(), Qt::KeepAspectRatio); + } else { + return _securityImage->copy(); + } + } + return QPixmap(); +} diff --git a/interface/src/ui/ImageProvider.h b/interface/src/ui/ImageProvider.h new file mode 100644 index 0000000000..c0b620585a --- /dev/null +++ b/interface/src/ui/ImageProvider.h @@ -0,0 +1,33 @@ +// +// ImageProvider.h +// +// Created by David Kelly on 2017/08/23 +// Copyright 2017 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 +// + +#pragma once +#ifndef hifi_ImageProvider_h +#define hifi_ImageProvider_h + +#include + +class ImageProvider: public QQuickImageProvider { +public: + static const QString PROVIDER_NAME; + + ImageProvider() : QQuickImageProvider(QQuickImageProvider::Pixmap) {} + + QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; + + void setSecurityImage(QPixmap* pixmap) { _securityImage = pixmap; } + +protected: + QPixmap* _securityImage { nullptr }; + +}; + +#endif //hifi_ImageProvider_h + diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2270118861..2285337f41 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -28,7 +28,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/notifications.js", "system/dialTone.js", "system/firstPersonHMD.js", - "system/tablet-ui/tabletUI.js" + "system/tablet-ui/tabletUI.js", + "system/commerce/wallet.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js new file mode 100644 index 0000000000..14954e5df6 --- /dev/null +++ b/scripts/system/commerce/wallet.js @@ -0,0 +1,152 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// wallet.js +// +// Created by Zach Fox on 2017-08-17 +// Copyright 2017 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 +// + +/*global XXX */ + +(function () { // BEGIN LOCAL_SCOPE + + + // Function Name: onButtonClicked() + // + // Description: + // -Fired when the app button is pressed. + // + // Relevant Variables: + // -WALLET_QML_SOURCE: The path to the Wallet QML + // -onWalletScreen: true/false depending on whether we're looking at the app. + var WALLET_QML_SOURCE = Script.resourcesPath() + "qml/hifi/commerce/wallet/Wallet.qml"; + var onWalletScreen = false; + function onButtonClicked() { + if (!tablet) { + print("Warning in buttonClicked(): 'tablet' undefined!"); + return; + } + if (onWalletScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + tablet.loadQMLSource(WALLET_QML_SOURCE); + } + } + + // Function Name: sendToQml() + // + // Description: + // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to + // the QML in the format "{method, params}", like json-rpc. See also fromQml(). + function sendToQml(message) { + tablet.sendToQml(message); + } + + // Function Name: fromQml() + // + // Description: + // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the QML + // in the format "{method, params}", like json-rpc. See also sendToQml(). + function fromQml(message) { + switch (message.method) { + case 'walletSetup_cancelClicked': + tablet.gotoHomeScreen(); + break; + case 'walletSetup_loginClicked': + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { + Menu.triggerOption("Login / Sign Up"); + tablet.gotoHomeScreen(); + } else { + tablet.loadQMLOnTop("../../../dialogs/TabletLoginDialog.qml"); + } + break; + default: + print('Unrecognized message from QML:', JSON.stringify(message)); + } + } + + // Function Name: wireEventBridge() + // + // Description: + // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or + // disable to event bridge. + // + // Relevant Variables: + // -hasEventBridge: true/false depending on whether we've already connected the event bridge. + var hasEventBridge = false; + function wireEventBridge(on) { + if (!tablet) { + print("Warning in wireEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } + + // Function Name: onTabletScreenChanged() + // + // Description: + // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string + // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. + function onTabletScreenChanged(type, url) { + onWalletScreen = (type === "QML" && url === WALLET_QML_SOURCE); + wireEventBridge(onWalletScreen); + // Change button to active when window is first openend, false otherwise. + if (button) { + button.editProperties({ isActive: onWalletScreen }); + } + } + + // + // Manage the connection between the button and the window. + // + var button; + var buttonName = "WALLET"; + var tablet = null; + var walletEnabled = Settings.getValue("inspectionMode", false); + function startup() { + if (walletEnabled) { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/wallet-i.svg", + activeIcon: "icons/tablet-icons/wallet-a.svg" + }); + button.clicked.connect(onButtonClicked); + tablet.screenChanged.connect(onTabletScreenChanged); + } + } + function shutdown() { + button.clicked.disconnect(onButtonClicked); + tablet.removeButton(button); + if (tablet) { + tablet.screenChanged.disconnect(onTabletScreenChanged); + if (onWalletScreen) { + tablet.gotoHomeScreen(); + } + } + } + + // + // Run the functions. + // + startup(); + Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 77be746bf4..2889a1514a 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -115,7 +115,7 @@ itemId: id, itemName: name, itemAuthor: author, - itemPrice: price ? parseInt(price, 10) : Math.round(Math.random() * 50), + itemPrice: price ? parseInt(price, 10) : 0, itemHref: href })); }