diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 08895ecaa1..2e7ff39ed6 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -40,7 +40,6 @@ ModalWindow { Loader { id: bodyLoader - anchors.fill: parent source: loginDialog.isSteamRunning() ? "LoginDialog/SignInBody.qml" : "LoginDialog/LinkAccountBody.qml" } } diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index f1b36ff6a7..e06ce239ab 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -18,8 +18,8 @@ import "../styles-uit" Item { id: completeProfileBody clip: true - width: pane.width - height: pane.height + width: root.pane.width + height: root.pane.height QtObject { id: d @@ -33,8 +33,8 @@ Item { termsContainer.contentWidth)) var targetHeight = 5 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height + termsContainer.height - root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) - root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) } } diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index fbcb3d5e37..e27635dbbd 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -19,11 +19,13 @@ import "../styles-uit" Item { id: linkAccountBody clip: true - width: root.pane.width height: root.pane.height + width: root.pane.width + property bool failAfterSignUp: false function login() { mainTextContainer.visible = false + toggleLoading(true) loginDialog.login(usernameField.text, passwordField.text) } @@ -51,12 +53,40 @@ Item { targetHeight += hifi.dimensions.contentSpacing.y + additionalInformation.height } - root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); - root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); + parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y); } } + function toggleLoading(isLoading) { + linkAccountSpinner.visible = isLoading + form.visible = !isLoading + + if (loginDialog.isSteamRunning()) { + additionalInformation.visible = !isLoading + } + + leftButton.visible = !isLoading + buttons.visible = !isLoading + } + + BusyIndicator { + id: linkAccountSpinner + + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + running: true + + width: 48 + height: 48 + } + ShortcutText { id: mainTextContainer anchors { @@ -96,7 +126,7 @@ Item { } width: 350 - label: "User Name or Email" + label: "Username or Email" } ShortcutText { @@ -108,6 +138,7 @@ Item { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent onLinkActivated: loginDialog.openUrl(link) } @@ -135,6 +166,7 @@ Item { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent onLinkActivated: loginDialog.openUrl(link) } @@ -173,6 +205,31 @@ Item { } } + Row { + id: leftButton + anchors { + left: parent.left + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Sign Up") + visible: !loginDialog.isSteamRunning() + + onClicked: { + bodyLoader.setSource("SignUpBody.qml") + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + } + Row { id: buttons anchors { @@ -209,6 +266,11 @@ Item { keyboardEnabled = HMD.active; d.resize(); + if (failAfterSignUp) { + mainTextContainer.text = "Account created successfully." + mainTextContainer.visible = true + } + usernameField.forceActiveFocus(); } @@ -228,6 +290,7 @@ Item { onHandleLoginFailed: { console.log("Login Failed") mainTextContainer.visible = true + toggleLoading(false) } onHandleLinkCompleted: { console.log("Link Succeeded") @@ -238,7 +301,7 @@ Item { } onHandleLinkFailed: { console.log("Link Failed") - + toggleLoading(false) } } diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml index 7bbba683c3..167ed1640a 100644 --- a/interface/resources/qml/LoginDialog/SignInBody.qml +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -18,8 +18,8 @@ import "../styles-uit" Item { id: signInBody clip: true - width: pane.width - height: pane.height + width: root.pane.width + height: root.pane.height property bool required: false @@ -43,8 +43,8 @@ Item { var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height - root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) - root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) } } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml new file mode 100644 index 0000000000..c0ff2d77cb --- /dev/null +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -0,0 +1,296 @@ +// +// SignUpBody.qml +// +// Created by Stephen Birarda on 7 Dec 2016 +// Copyright 2016 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 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: signupBody + clip: true + height: root.pane.height + width: root.pane.width + + function signup() { + mainTextContainer.visible = false + toggleLoading(true) + loginDialog.signup(emailField.text, usernameField.text, passwordField.text) + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, form.contentWidth); + var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + + 4 * hifi.dimensions.contentSpacing.y + form.height + + hifi.dimensions.contentSpacing.y + buttons.height; + + parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)); + parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0); + } + } + + function toggleLoading(isLoading) { + linkAccountSpinner.visible = isLoading + form.visible = !isLoading + + leftButton.visible = !isLoading + buttons.visible = !isLoading + } + + BusyIndicator { + id: linkAccountSpinner + + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + running: true + + width: 48 + height: 48 + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + + text: qsTr("There was an unknown error while creating your account.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + horizontalAlignment: Text.AlignLeft + } + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: emailField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Email" + } + } + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Username" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: qsTr("No spaces / special chars.") + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.blueAccent + } + } + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: qsTr("At least 6 characters") + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.blueAccent + } + } + + } + + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: leftButton + anchors { + left: parent.left + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Existing User") + + onClicked: { + bodyLoader.setSource("LinkAccountBody.qml") + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + } + + Row { + id: buttons + anchors { + right: parent.right + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Sign Up") + color: hifi.buttons.blue + + onClicked: signupBody.signup() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: root.destroy() + } + } + + Component.onCompleted: { + root.title = qsTr("Create an Account") + root.iconText = "<" + keyboardEnabled = HMD.active; + d.resize(); + + emailField.forceActiveFocus(); + } + + Connections { + target: loginDialog + onHandleSignupCompleted: { + console.log("Sign Up Succeeded"); + + // now that we have an account, login with that username and password + loginDialog.login(usernameField.text, passwordField.text) + } + onHandleSignupFailed: { + console.log("Sign Up Failed") + toggleLoading(false) + + mainTextContainer.text = errorString + mainTextContainer.visible = true + + d.resize(); + } + onHandleLoginCompleted: { + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack": false }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginFailed: { + // we failed to login, show the LoginDialog so the user will try again + bodyLoader.setSource("LinkAccountBody.qml", { "failAfterSignUp": true }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + signupBody.signup() + break + } + } +} diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index eafe9e4b47..18c831b3a9 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -47,11 +47,9 @@ Item { hifi.dimensions.contentSpacing.y + textField.height + hifi.dimensions.contentSpacing.y + buttons.height - root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) - root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y) - - height = root.height } } diff --git a/interface/resources/qml/LoginDialog/WelcomeBody.qml b/interface/resources/qml/LoginDialog/WelcomeBody.qml index 5fed9addf8..eb91956532 100644 --- a/interface/resources/qml/LoginDialog/WelcomeBody.qml +++ b/interface/resources/qml/LoginDialog/WelcomeBody.qml @@ -17,8 +17,8 @@ import "../styles-uit" Item { id: welcomeBody clip: true - width: pane.width - height: pane.height + width: root.pane.width + height: root.pane.height property bool welcomeBack: false @@ -39,8 +39,8 @@ Item { var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height - root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) - root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + parent.width = root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + parent.height = root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) } } diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 5a08096257..e2a368db2d 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -119,7 +119,7 @@ namespace MenuOption { const QString LoadScript = "Open and Run Script File..."; const QString LoadScriptURL = "Open and Run Script from URL..."; const QString LodTools = "LOD Tools"; - const QString Login = "Login"; + const QString Login = "Login / Sign Up"; const QString Log = "Log"; const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index fef0a12813..d27c3e907a 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -11,9 +11,10 @@ #include "LoginDialog.h" -#include -#include -#include +#include +#include +#include +#include #include #include @@ -47,7 +48,7 @@ void LoginDialog::toggleAction() { connection = connect(loginAction, &QAction::triggered, accountManager.data(), &AccountManager::logout); } else { // change the menu item to login - loginAction->setText("Login"); + loginAction->setText("Login / Sign Up"); connection = connect(loginAction, &QAction::triggered, [] { LoginDialog::show(); }); @@ -153,3 +154,83 @@ void LoginDialog::createFailed(QNetworkReply& reply) { emit handleCreateFailed(reply.errorString()); } +void LoginDialog::signup(const QString& email, const QString& username, const QString& password) { + + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "signupCompleted"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "signupFailed"; + + QJsonObject payload; + + QJsonObject userObject; + userObject.insert("email", email); + userObject.insert("username", username); + userObject.insert("password", password); + + payload.insert("user", userObject); + + static const QString API_SIGNUP_PATH = "api/v1/users"; + + qDebug() << "Sending a request to create an account for" << username; + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(API_SIGNUP_PATH, AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(payload).toJson()); +} + +void LoginDialog::signupCompleted(QNetworkReply& reply) { + emit handleSignupCompleted(); +} + +QString errorStringFromAPIObject(const QJsonValue& apiObject) { + if (apiObject.isArray()) { + return apiObject.toArray()[0].toString(); + } else if (apiObject.isString()) { + return apiObject.toString(); + } else { + return "is invalid"; + } +} + +void LoginDialog::signupFailed(QNetworkReply& reply) { + + // parse the returned JSON to see what the problem was + auto jsonResponse = QJsonDocument::fromJson(reply.readAll()); + + static const QString RESPONSE_DATA_KEY = "data"; + + auto dataJsonValue = jsonResponse.object()[RESPONSE_DATA_KEY]; + + if (dataJsonValue.isObject()) { + auto dataObject = dataJsonValue.toObject(); + + static const QString EMAIL_DATA_KEY = "email"; + static const QString USERNAME_DATA_KEY = "username"; + static const QString PASSWORD_DATA_KEY = "password"; + + QStringList errorStringList; + + if (dataObject.contains(EMAIL_DATA_KEY)) { + errorStringList.append(QString("Email %1.").arg(errorStringFromAPIObject(dataObject[EMAIL_DATA_KEY]))); + } + + if (dataObject.contains(USERNAME_DATA_KEY)) { + errorStringList.append(QString("Username %1.").arg(errorStringFromAPIObject(dataObject[USERNAME_DATA_KEY]))); + } + + if (dataObject.contains(PASSWORD_DATA_KEY)) { + errorStringList.append(QString("Password %1.").arg(errorStringFromAPIObject(dataObject[PASSWORD_DATA_KEY]))); + } + + emit handleSignupFailed(errorStringList.join('\n')); + } else { + static const QString DEFAULT_SIGN_UP_FAILURE_MESSAGE = "There was an unknown error while creating your account. Please try again later."; + emit handleSignupFailed(DEFAULT_SIGN_UP_FAILURE_MESSAGE); + } + + +} + diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 8b6dc40302..ce6075793b 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -36,6 +36,9 @@ signals: void handleCreateCompleted(); void handleCreateFailed(QString error); + + void handleSignupCompleted(); + void handleSignupFailed(QString errorString); public slots: void linkCompleted(QNetworkReply& reply); @@ -43,6 +46,9 @@ public slots: void createCompleted(QNetworkReply& reply); void createFailed(QNetworkReply& reply); + + void signupCompleted(QNetworkReply& reply); + void signupFailed(QNetworkReply& reply); protected slots: Q_INVOKABLE bool isSteamRunning() const; @@ -51,6 +57,8 @@ protected slots: Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void linkSteam(); Q_INVOKABLE void createAccountFromStream(QString username = QString()); + + Q_INVOKABLE void signup(const QString& email, const QString& username, const QString& password); Q_INVOKABLE void openUrl(const QString& url) const;