diff --git a/libnymea-app-core/connection/awsclient.cpp b/libnymea-app-core/connection/awsclient.cpp index 27b38c49..10e41f95 100644 --- a/libnymea-app-core/connection/awsclient.cpp +++ b/libnymea-app-core/connection/awsclient.cpp @@ -74,9 +74,23 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent), config.apiEndpoint = "testapi-cloud.guh.io"; m_configs.append(config); + // Marantec environment + config.clientId = "7rf6da8pcqi1qi8tp1evf933h2"; + config.poolId = "eu-west-1_d4DdcqKJ8"; + config.identityPoolId = "eu-west-1:d32f6d94-caae-4f08-a193-f9fba8652646"; + // Generating certificates is not supported for the Marantec environment + config.certificateEndpoint = ""; + config.certificateApiKey = ""; + config.certificateVendorId = ""; + config.mqttEndpoint = "a27q7a2x15m8h3.iot.eu-west-1.amazonaws.com"; + config.region = "eu-west-1"; + config.apiEndpoint = "api-cloud.guh.io"; + m_configs.append(config); + QSettings settings; settings.beginGroup("cloud"); m_username = settings.value("username").toString(); + m_userId = settings.value("userId").toString(); m_password = settings.value("password").toString(); m_accessToken = settings.value("accessToken").toByteArray(); m_accessTokenExpiry = settings.value("accessTokenExpiry").toDateTime(); @@ -94,7 +108,7 @@ AWSClient::AWSClient(QObject *parent) : QObject(parent), bool AWSClient::isLoggedIn() const { - return !m_username.isEmpty() && !m_password.isEmpty(); + return !m_userId.isEmpty() && !m_username.isEmpty() && !m_password.isEmpty(); } QString AWSClient::username() const @@ -181,18 +195,30 @@ void AWSClient::login(const QString &username, const QString &password) m_idToken = authenticationResult.value("IdToken").toByteArray(); m_refreshToken = authenticationResult.value("RefreshToken").toByteArray(); + qDebug() << "AWS ID token" << m_idToken; + QList jwtParts = m_idToken.split('.'); + if (jwtParts.count() != 3) { + qWarning() << "JWT token doesn't have 3 parts"; + return; + } +// qDebug() << "decoded header:" << QByteArray::fromBase64(jwtParts.at(0)); +// qDebug() << "decoded payload:" << QByteArray::fromBase64(jwtParts.at(1)); + QJsonDocument tokenPayloadJsonDoc = QJsonDocument::fromJson(QByteArray::fromBase64(jwtParts.at(1))); + m_userId = tokenPayloadJsonDoc.toVariant().toMap().value("cognito:username").toByteArray(); + QSettings settings; settings.remove("cloud"); settings.beginGroup("cloud"); settings.setValue("username", m_username); + settings.setValue("userId", m_userId); settings.setValue("password", m_password); settings.setValue("accessToken", m_accessToken); settings.setValue("accessTokenExpiry", m_accessTokenExpiry); settings.setValue("idToken", m_idToken); settings.setValue("refreshToken", m_refreshToken); - qDebug() << "AWS login successful";// << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented)); + qDebug() << "AWS login successful. Userid:" << m_userId;// << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented)); emit isLoggedInChanged(); qDebug() << "Getting cognito ID"; @@ -202,8 +228,10 @@ void AWSClient::login(const QString &username, const QString &password) void AWSClient::logout() { + m_userId.clear(); m_username.clear(); m_password.clear(); + m_devices->clear(); QSettings settings; settings.remove("cloud"); emit isLoggedInChanged(); @@ -441,7 +469,7 @@ void AWSClient::deleteAccount() } qDebug() << "Deleting account"; - QUrl url(QString("https://%1/users/profiles/%2").arg(m_configs.at(m_usedConfigIndex).apiEndpoint).arg(m_username)); + QUrl url(QString("https://%1/users/profiles/%2").arg(m_configs.at(m_usedConfigIndex).apiEndpoint).arg(m_userId)); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("x-api-idToken", m_idToken); @@ -458,14 +486,17 @@ void AWSClient::deleteAccount() QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { qWarning() << "Error deleting cloud user account:" << reply->error() << reply->errorString() << qUtf8Printable(data); + emit deleteAccountResult(LoginErrorUnknownError); return; } QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qWarning() << "Failed to parse JSON from server" << error.errorString() << qUtf8Printable(data); + emit deleteAccountResult(LoginErrorUnknownError); return; } + emit deleteAccountResult(LoginErrorNoError); logout(); qDebug() << "Account deleted" << data; }); @@ -489,9 +520,11 @@ void AWSClient::unpairDevice(const QString &boxId) request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("x-api-idToken", m_idToken); + m_devices->setBusy(true); QNetworkReply *reply = m_nam->deleteResource(request); connect(reply, &QNetworkReply::finished, this, [this, reply, boxId]() { reply->deleteLater(); + m_devices->setBusy(false); QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { qWarning() << "Error unpairing cloud device:" << reply->error() << reply->errorString() << qUtf8Printable(data); @@ -787,9 +820,11 @@ void AWSClient::fetchDevices() request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("x-api-idToken", m_idToken); + m_devices->setBusy(true); QNetworkReply *reply = m_nam->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); + m_devices->setBusy(false); QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { qWarning() << "Error fetching cloud devices:" << reply->error() << reply->errorString() << qUtf8Printable(data); @@ -931,6 +966,19 @@ QHash AWSDevices::roleNames() const return roles; } +bool AWSDevices::busy() const +{ + return m_busy; +} + +void AWSDevices::setBusy(bool busy) +{ + if (m_busy != busy) { + m_busy = busy; + emit busyChanged(); + } +} + AWSDevice *AWSDevices::getDevice(const QString &uuid) const { for (int i = 0; i < m_list.count(); i++) { @@ -977,6 +1025,16 @@ void AWSDevices::remove(const QString &uuid) emit countChanged(); } +void AWSDevices::clear() +{ + beginResetModel(); + while (m_list.count() > 0) { + m_list.takeFirst()->deleteLater(); + } + endResetModel(); + emit countChanged(); +} + AWSDevice::AWSDevice(const QString &id, const QString &name, bool online, QObject *parent): QObject (parent), m_id(id), diff --git a/libnymea-app-core/connection/awsclient.h b/libnymea-app-core/connection/awsclient.h index 37d29680..ba86becf 100644 --- a/libnymea-app-core/connection/awsclient.h +++ b/libnymea-app-core/connection/awsclient.h @@ -33,6 +33,7 @@ private: class AWSDevices: public QAbstractListModel { Q_OBJECT Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: enum Roles { RoleName, @@ -43,14 +44,19 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; + bool busy() const; + void setBusy(bool busy); Q_INVOKABLE AWSDevice* getDevice(const QString &uuid) const; Q_INVOKABLE AWSDevice* get(int index) const; void insert(AWSDevice *device); void remove(const QString &uuid); + void clear(); signals: void countChanged(); + void busyChanged(); private: QList m_list; + bool m_busy = false; }; class AWSConfiguration { @@ -127,6 +133,7 @@ signals: void confirmationResult(LoginError error); void forgotPasswordResult(LoginError error); void confirmForgotPasswordResult(LoginError error); + void deleteAccountResult(LoginError error); void isLoggedInChanged(); void confirmationPendingChanged(); diff --git a/libnymea-app-core/engine.cpp b/libnymea-app-core/engine.cpp index 8c41b279..315c40f9 100644 --- a/libnymea-app-core/engine.cpp +++ b/libnymea-app-core/engine.cpp @@ -127,14 +127,14 @@ Engine::Engine(QObject *parent) : connect(m_aws, &AWSClient::devicesFetched, this, [this]() { if (m_jsonRpcClient->connected() && m_jsonRpcClient->cloudConnectionState() == JsonRpcClient::CloudConnectionStateConnected) { if (m_aws->awsDevices()->getDevice(m_jsonRpcClient->serverUuid()) == nullptr) { - m_jsonRpcClient->setupRemoteAccess(m_aws->idToken(), m_aws->cognitoIdentityId()); + m_jsonRpcClient->setupRemoteAccess(m_aws->idToken(), m_aws->userId()); } } }); connect(m_jsonRpcClient, &JsonRpcClient::connectedChanged, this, [this]() { if (m_jsonRpcClient->connected() && m_jsonRpcClient->cloudConnectionState() == JsonRpcClient::CloudConnectionStateConnected) { if (m_aws->awsDevices()->getDevice(m_jsonRpcClient->serverUuid()) == nullptr) { - m_jsonRpcClient->setupRemoteAccess(m_aws->idToken(), m_aws->cognitoIdentityId()); + m_jsonRpcClient->setupRemoteAccess(m_aws->idToken(), m_aws->userId()); } } }); diff --git a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp index b6609d31..31a293d8 100644 --- a/libnymea-app-core/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app-core/jsonrpc/jsonrpcclient.cpp @@ -96,7 +96,7 @@ void JsonRpcClient::setNotificationsEnabledResponse(const QVariantMap ¶ms) void JsonRpcClient::notificationReceived(const QVariantMap &data) { - //JsonRpcClient: Notification received QMap(("id", QVariant(double, 2))("notification", QVariant(QString, "JSONRPC.PushButtonAuthFinished"))("params", QVariant(QVariantMap, QMap(("success", QVariant(bool, true))("token", QVariant(QString, "FJPaAJ8FEtrqcC+/s0s/lAcDubz0OyEtwbRsyFIWM9c="))("transactionId", QVariant(double, 2)))))) + qDebug() << "Notification received:" << data; if (data.value("notification").toString() == "JSONRPC.PushButtonAuthFinished") { qDebug() << "Push button auth finished."; if (data.value("params").toMap().value("transactionId").toInt() != m_pendingPushButtonTransaction) { diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 30a45675..433648a9 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -18,7 +18,7 @@ Page { ListElement { iconSource: "../images/share.svg"; text: qsTr("Configure things"); page: "EditDevicesPage.qml" } ListElement { iconSource: "../images/magic.svg"; text: qsTr("Magic"); page: "MagicPage.qml" } ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "appsettings/AppSettingsPage.qml" } - ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.qml" } + ListElement { iconSource: "../images/settings.svg"; text: qsTr("Box settings"); page: "SettingsPage.qml" } } onClicked: { @@ -173,39 +173,38 @@ Page { } } - TabBar { - id: tabBar - Layout.fillWidth: true - Material.elevation: 3 - currentIndex: settings.currentMainViewIndex - position: TabBar.Footer - Layout.preferredHeight: 70 + (app.landscape ? - ((systemProductType === "ios" && Screen.height === 375) ? -10 : -20) : - (systemProductType === "ios" && Screen.height === 812) ? 14 : 0) + } + footer: TabBar { + id: tabBar + Material.elevation: 3 + currentIndex: settings.currentMainViewIndex + position: TabBar.Footer + implicitHeight: 70 + (app.landscape ? + ((systemProductType === "ios" && Screen.height === 375) ? -10 : -20) : + (systemProductType === "ios" && Screen.height === 812) ? 14 : 0) - // FIXME: All this can go away when we require Controls 2.3 (Qt 5.10) or greater as TabBar got a major rework there. - // Ideally we'd just list the 3 items and set visible to false if the server version isn't good enough but TabBar - // has troubles dealing with that. For now, let's manually fill it and use a timer to initialize the currentIndex. - Component.onCompleted: { - var pi = 0; - if (Engine.jsonRpcClient.ensureServerVersion(1.6)) { - tabEntryComponent.createObject(tabBar, {text: qsTr("Favorites"), iconSource: "../images/starred.svg", pageIndex: pi++}) - } - tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++}) - tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++}) - initTimer.start() + // FIXME: All this can go away when we require Controls 2.3 (Qt 5.10) or greater as TabBar got a major rework there. + // Ideally we'd just list the 3 items and set visible to false if the server version isn't good enough but TabBar + // has troubles dealing with that. For now, let's manually fill it and use a timer to initialize the currentIndex. + Component.onCompleted: { + var pi = 0; + if (Engine.jsonRpcClient.ensureServerVersion(1.6)) { + tabEntryComponent.createObject(tabBar, {text: qsTr("Favorites"), iconSource: "../images/starred.svg", pageIndex: pi++}) } - Timer { id: initTimer; interval: 1; repeat: false; onTriggered: tabBar.currentIndex = Qt.binding(function() {return settings.currentMainViewIndex;})} + tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++}) + tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++}) + initTimer.start() + } + Timer { id: initTimer; interval: 1; repeat: false; onTriggered: tabBar.currentIndex = Qt.binding(function() {return settings.currentMainViewIndex;})} - Component { - id: tabEntryComponent - MainPageTabButton { - property int pageIndex: 0 + Component { + id: tabEntryComponent + MainPageTabButton { + property int pageIndex: 0 // height: tabBar.height - onClicked: settings.currentMainViewIndex = pageIndex - alignment: app.landscape ? Qt.Horizontal : Qt.Vertical - } + onClicked: settings.currentMainViewIndex = pageIndex + alignment: app.landscape ? Qt.Horizontal : Qt.Vertical } } } diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index 8af363b6..fd040ce1 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -8,7 +8,7 @@ import "components" Page { id: root header: GuhHeader { - text: qsTr("System settings") + text: qsTr("Box settings") backButtonVisible: true onBackPressed: pageStack.pop() } diff --git a/nymea-app/ui/appsettings/AppSettingsPage.qml b/nymea-app/ui/appsettings/AppSettingsPage.qml index 47816393..450581d1 100644 --- a/nymea-app/ui/appsettings/AppSettingsPage.qml +++ b/nymea-app/ui/appsettings/AppSettingsPage.qml @@ -124,7 +124,7 @@ Page { } MeaListItemDelegate { Layout.fillWidth: true - visible: settings.showHiddenOption + visible: settings.showHiddenOptions text: qsTr("Developer options") iconName: "../images/configure.svg" onClicked: pageStack.push(Qt.resolvedUrl("DeveloperOptionsPage.qml")) diff --git a/nymea-app/ui/appsettings/CloudLoginPage.qml b/nymea-app/ui/appsettings/CloudLoginPage.qml index b7360e14..2bb7c431 100644 --- a/nymea-app/ui/appsettings/CloudLoginPage.qml +++ b/nymea-app/ui/appsettings/CloudLoginPage.qml @@ -25,10 +25,20 @@ Page { Engine.awsClient.fetchDevices(); } } + onDeleteAccountResult: { + busyOverlay.shown = false; + if (error !== AWSClient.LoginErrorNoError) { + var errorDialog = Qt.createComponent(Qt.resolvedUrl("../components/ErrorDialog.qml")); + var text = qsTr("Sorry, an error happened removing the account. Please try again later."); + var popup = errorDialog.createObject(app, {text: text}) + popup.open() + return; + } + } } ColumnLayout { - anchors { left: parent.left; top: parent.top; right: parent.right } + anchors.fill: parent visible: Engine.awsClient.isLoggedIn Label { Layout.fillWidth: true @@ -64,6 +74,7 @@ Page { Layout.fillWidth: true Layout.fillHeight: true model: Engine.awsClient.awsDevices + clip: true delegate: MeaListItemDelegate { width: parent.width text: model.name @@ -77,6 +88,11 @@ Page { Engine.awsClient.unpairDevice(model.id); } } + + BusyIndicator { + anchors.centerIn: parent + visible: Engine.awsClient.awsDevices.busy + } } } @@ -84,28 +100,29 @@ Page { id: logoutDialog title: qsTr("Goodbye") // Deleting user profile not working in cloud yet -// text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1 boxes remotely any more. However, you can come back any time, we'll keep your user account. If you whish to completely delete your account and all the data associated with it, check the box below before hitting ok.").arg(app.systemName) - text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1 boxes remotely any more. However, you can come back any time.").arg(app.systemName) + text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1 boxes remotely any more. However, you can come back any time, we'll keep your user account. If you whish to completely delete your account and all the data associated with it, check the box below before hitting ok.").arg(app.systemName) +// text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1 boxes remotely any more. However, you can come back any time.").arg(app.systemName) headerIcon: "../images/dialog-warning-symbolic.svg" standardButtons: Dialog.Cancel | Dialog.Ok -// RowLayout { -// CheckBox { -// id: deleteCheckbox -// } -// Label { -// Layout.fillWidth: true -// wrapMode: Text.WordWrap -// text: qsTr("Delete my account") -// } -// } + RowLayout { + CheckBox { + id: deleteCheckbox + } + Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: qsTr("Delete my account") + } + } onAccepted: { -// if (deleteCheckbox.checked) { -// Engine.awsClient.deleteAccount() -// } else { + if (deleteCheckbox.checked) { + busyOverlay.shown = true; + Engine.awsClient.deleteAccount() + } else { Engine.awsClient.logout() -// } + } } } diff --git a/nymea-app/ui/appsettings/DeveloperOptionsPage.qml b/nymea-app/ui/appsettings/DeveloperOptionsPage.qml index d2b5dcb9..dd2c174b 100644 --- a/nymea-app/ui/appsettings/DeveloperOptionsPage.qml +++ b/nymea-app/ui/appsettings/DeveloperOptionsPage.qml @@ -25,7 +25,7 @@ Page { ComboBox { currentIndex: app.settings.cloudEnvironment - model: [qsTr("Community"), qsTr("Testing")] + model: [qsTr("Community"), qsTr("Testing"), qsTr("Marantec")] onActivated: { app.settings.cloudEnvironment = index; } diff --git a/nymea-app/ui/system/CloudSettingsPage.qml b/nymea-app/ui/system/CloudSettingsPage.qml index f9f34930..6ef3f701 100644 --- a/nymea-app/ui/system/CloudSettingsPage.qml +++ b/nymea-app/ui/system/CloudSettingsPage.qml @@ -18,8 +18,12 @@ Page { Connections { target: Engine.jsonRpcClient onCloudConnectionStateChanged: { + print("cloud connection state changed", Engine.jsonRpcClient.cloudConnectionState) if (Engine.jsonRpcClient.cloudConnectionState == JsonRpcClient.CloudConnectionStateConnected) { d.deploymentStarted = false; + if (Engine.awsClient.awsDevices.getDevice(Engine.jsonRpcClient.serverUuid) === null) { + Engine.jsonRpcClient.setupRemoteAccess(Engine.awsClient.idToken, Engine.awsClient.userId) + } } } } @@ -37,6 +41,11 @@ Page { wrapMode: Text.WordWrap } +// Button { +// text: "pair" +// onClicked: Engine.jsonRpcClient.setupRemoteAccess(Engine.awsClient.idToken, Engine.awsClient.userId) +// } + SwitchDelegate { Layout.fillWidth: true text: qsTr("Cloud connection enabled")