Improve first-run UX
- Replace LoginPage with step-by-step approach to support different login flows - Implement login using SSO
This commit is contained in:
@@ -20,6 +20,7 @@ add_executable(neochat
|
||||
chatdocumenthandler.cpp
|
||||
devicesmodel.cpp
|
||||
filetypesingleton.cpp
|
||||
login.cpp
|
||||
../res.qrc
|
||||
)
|
||||
|
||||
|
||||
@@ -91,50 +91,6 @@ inline QString accessTokenFileName(const AccountSettings &account)
|
||||
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/' + fileName;
|
||||
}
|
||||
|
||||
void Controller::loginWithCredentials(const QString &serverAddr, const QString &user, const QString &pass, QString deviceName)
|
||||
{
|
||||
if (user.isEmpty() || pass.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deviceName.isEmpty()) {
|
||||
deviceName = "NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture();
|
||||
}
|
||||
|
||||
auto conn = new Connection(this);
|
||||
const QUrl serverUrl = QUrl::fromUserInput(serverAddr);
|
||||
// we are using a fake mixd since resolveServer just set the homeserver url :sigh:
|
||||
conn->resolveServer("@username:" + serverUrl.host() + ":" + QString::number(serverUrl.port(443)));
|
||||
|
||||
connect(conn, &Connection::loginFlowsChanged, this, [this, user, conn, pass, deviceName]() {
|
||||
conn->loginWithPassword(user, pass, deviceName, "");
|
||||
connect(conn, &Connection::connected, this, [this, conn, deviceName] {
|
||||
AccountSettings account(conn->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(conn->homeserver());
|
||||
account.setDeviceId(conn->deviceId());
|
||||
account.setDeviceName(deviceName);
|
||||
if (!saveAccessTokenToKeyChain(account, conn->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
addConnection(conn);
|
||||
setActiveConnection(conn);
|
||||
});
|
||||
connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) {
|
||||
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
connect(conn, &Connection::loginError, [=](QString error, const QString &) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed"), std::move(error));
|
||||
});
|
||||
});
|
||||
|
||||
connect(conn, &Connection::resolveError, this, [=](QString error) {
|
||||
Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
}
|
||||
|
||||
void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName)
|
||||
{
|
||||
if (user.isEmpty() || token.isEmpty()) {
|
||||
@@ -551,4 +507,5 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om
|
||||
QJsonObject _data;
|
||||
addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth);
|
||||
setRequestData(std::move(_data));
|
||||
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ public:
|
||||
void addConnection(Connection *c);
|
||||
void dropConnection(Connection *c);
|
||||
|
||||
Q_INVOKABLE void loginWithCredentials(const QString &, const QString &, const QString &, QString);
|
||||
Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &);
|
||||
|
||||
Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString ¤tPassword, const QString &newPassword);
|
||||
@@ -61,6 +60,9 @@ public:
|
||||
void setAboutData(const KAboutData &aboutData);
|
||||
[[nodiscard]] KAboutData aboutData() const;
|
||||
|
||||
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
|
||||
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
|
||||
|
||||
enum PasswordStatus {
|
||||
Success,
|
||||
Wrong,
|
||||
@@ -79,8 +81,6 @@ private:
|
||||
static QByteArray loadAccessTokenFromFile(const AccountSettings &account);
|
||||
QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account);
|
||||
|
||||
bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken);
|
||||
bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken);
|
||||
void loadSettings();
|
||||
void saveSettings() const;
|
||||
|
||||
@@ -110,6 +110,7 @@ Q_SIGNALS:
|
||||
void showWindow();
|
||||
void openRoom(NeoChatRoom *room);
|
||||
void userConsentRequired(QUrl url);
|
||||
void testConnectionResult(const QString &connection, bool usable);
|
||||
|
||||
public Q_SLOTS:
|
||||
void logout(Quotient::Connection *conn, bool serverSideLogout);
|
||||
|
||||
206
src/login.cpp
Normal file
206
src/login.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "login.h"
|
||||
#include "connection.h"
|
||||
#include "controller.h"
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
Login::Login(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
void Login::init()
|
||||
{
|
||||
m_homeserverReachable = false;
|
||||
m_connection = nullptr;
|
||||
m_matrixId = QString();
|
||||
m_password = QString();
|
||||
m_deviceName = QString();
|
||||
m_supportsSso = false;
|
||||
m_supportsPassword = false;
|
||||
m_ssoUrl = QUrl();
|
||||
|
||||
connect(this, &Login::matrixIdChanged, this, [=](){
|
||||
setHomeserverReachable(false);
|
||||
|
||||
if (m_connection) {
|
||||
delete m_connection;
|
||||
m_connection = nullptr;
|
||||
}
|
||||
|
||||
if(m_matrixId == "@") {
|
||||
return;
|
||||
}
|
||||
|
||||
m_testing = true;
|
||||
Q_EMIT testingChanged();
|
||||
m_connection = new Connection(this);
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
connect(m_connection, &Connection::loginFlowsChanged, this, [=](){
|
||||
setHomeserverReachable(true);
|
||||
m_testing = false;
|
||||
Q_EMIT testingChanged();
|
||||
m_supportsSso = m_connection->supportsSso();
|
||||
m_supportsPassword = m_connection->supportsPasswordAuth();
|
||||
Q_EMIT loginFlowsChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Login::setHomeserverReachable(bool reachable)
|
||||
{
|
||||
m_homeserverReachable = reachable;
|
||||
Q_EMIT homeserverReachableChanged();
|
||||
}
|
||||
|
||||
bool Login::homeserverReachable() const
|
||||
{
|
||||
return m_homeserverReachable;
|
||||
}
|
||||
|
||||
QString Login::matrixId() const
|
||||
{
|
||||
return m_matrixId;
|
||||
}
|
||||
|
||||
void Login::setMatrixId(const QString &matrixId)
|
||||
{
|
||||
m_matrixId = matrixId;
|
||||
if(!m_matrixId.startsWith('@')) {
|
||||
m_matrixId.prepend('@');
|
||||
}
|
||||
Q_EMIT matrixIdChanged();
|
||||
}
|
||||
|
||||
QString Login::password() const
|
||||
{
|
||||
return m_password;
|
||||
}
|
||||
|
||||
void Login::setPassword(const QString &password)
|
||||
{
|
||||
m_password = password;
|
||||
Q_EMIT passwordChanged();
|
||||
}
|
||||
|
||||
QString Login::deviceName() const
|
||||
{
|
||||
return m_deviceName;
|
||||
}
|
||||
|
||||
void Login::setDeviceName(const QString &deviceName)
|
||||
{
|
||||
m_deviceName = deviceName;
|
||||
Q_EMIT deviceNameChanged();
|
||||
}
|
||||
|
||||
void Login::login()
|
||||
{
|
||||
m_isLoggingIn = true;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
|
||||
setDeviceName("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture());
|
||||
|
||||
m_connection = new Connection(this);
|
||||
m_connection->resolveServer(m_matrixId);
|
||||
|
||||
connect(m_connection, &Connection::loginFlowsChanged, this, [=]() {
|
||||
m_connection->loginWithPassword(m_matrixId, m_password, m_deviceName, QString());
|
||||
connect(m_connection, &Connection::connected, this, [=] {
|
||||
Q_EMIT connected();
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
AccountSettings account(m_connection->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(m_connection->homeserver());
|
||||
account.setDeviceId(m_connection->deviceId());
|
||||
account.setDeviceName(m_deviceName);
|
||||
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
Controller::instance().addConnection(m_connection);
|
||||
Controller::instance().setActiveConnection(m_connection);
|
||||
});
|
||||
connect(m_connection, &Connection::networkError, [=](QString error, const QString &, int, int) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
connect(m_connection, &Connection::loginError, [=](QString error, const QString &) {
|
||||
Q_EMIT errorOccured(i18n("Login Failed: %1", error));
|
||||
m_isLoggingIn = false;
|
||||
Q_EMIT isLoggingInChanged();
|
||||
});
|
||||
});
|
||||
|
||||
connect(m_connection, &Connection::resolveError, this, [=](QString error) {
|
||||
Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error));
|
||||
});
|
||||
|
||||
connect(m_connection, &Connection::syncDone, this, [=]() {
|
||||
Q_EMIT initialSyncFinished();
|
||||
disconnect(m_connection, &Connection::syncDone, this, nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
bool Login::supportsPassword() const
|
||||
{
|
||||
return m_supportsPassword;
|
||||
}
|
||||
|
||||
bool Login::supportsSso() const
|
||||
{
|
||||
return m_supportsSso;
|
||||
}
|
||||
|
||||
QUrl Login::ssoUrl() const
|
||||
{
|
||||
return m_ssoUrl;
|
||||
}
|
||||
|
||||
void Login::loginWithSso()
|
||||
{
|
||||
SsoSession *session = m_connection->prepareForSso("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture());
|
||||
m_ssoUrl = session->ssoUrl();
|
||||
Q_EMIT ssoUrlChanged();
|
||||
connect(m_connection, &Connection::connected, [=](){
|
||||
Q_EMIT connected();
|
||||
AccountSettings account(m_connection->userId());
|
||||
account.setKeepLoggedIn(true);
|
||||
account.clearAccessToken(); // Drop the legacy - just in case
|
||||
account.setHomeserver(m_connection->homeserver());
|
||||
account.setDeviceId(m_connection->deviceId());
|
||||
account.setDeviceName(m_deviceName);
|
||||
if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) {
|
||||
qWarning() << "Couldn't save access token";
|
||||
}
|
||||
account.sync();
|
||||
Controller::instance().addConnection(m_connection);
|
||||
Controller::instance().setActiveConnection(m_connection);
|
||||
});
|
||||
connect(m_connection, &Connection::syncDone, this, [=]() {
|
||||
Q_EMIT initialSyncFinished();
|
||||
disconnect(m_connection, &Connection::syncDone, this, nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
bool Login::testing() const
|
||||
{
|
||||
return m_testing;
|
||||
}
|
||||
|
||||
bool Login::isLoggingIn() const
|
||||
{
|
||||
return m_isLoggingIn;
|
||||
}
|
||||
85
src/login.h
Normal file
85
src/login.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "csapi/wellknown.h"
|
||||
#include "connection.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class Login : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool homeserverReachable READ homeserverReachable NOTIFY homeserverReachableChanged)
|
||||
Q_PROPERTY(bool testing READ testing NOTIFY testingChanged)
|
||||
Q_PROPERTY(QString matrixId READ matrixId WRITE setMatrixId NOTIFY matrixIdChanged)
|
||||
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
|
||||
Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged)
|
||||
Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false)
|
||||
Q_PROPERTY(bool supportsPassword READ supportsPassword NOTIFY loginFlowsChanged STORED false)
|
||||
Q_PROPERTY(QUrl ssoUrl READ ssoUrl NOTIFY ssoUrlChanged)
|
||||
Q_PROPERTY(bool isLoggingIn READ isLoggingIn NOTIFY isLoggingInChanged)
|
||||
|
||||
public:
|
||||
explicit Login(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void init();
|
||||
|
||||
bool homeserverReachable() const;
|
||||
|
||||
QString matrixId() const;
|
||||
void setMatrixId(const QString &matrixId);
|
||||
|
||||
QString password() const;
|
||||
void setPassword(const QString &password);
|
||||
|
||||
QString deviceName() const;
|
||||
void setDeviceName(const QString &deviceName);
|
||||
|
||||
bool supportsPassword() const;
|
||||
bool supportsSso() const;
|
||||
|
||||
bool testing() const;
|
||||
|
||||
QUrl ssoUrl() const;
|
||||
|
||||
bool isLoggingIn() const;
|
||||
|
||||
Q_INVOKABLE void login();
|
||||
Q_INVOKABLE void loginWithSso();
|
||||
|
||||
Q_SIGNALS:
|
||||
void homeserverReachableChanged();
|
||||
void testHomeserverFinished();
|
||||
void matrixIdChanged();
|
||||
void passwordChanged();
|
||||
void deviceNameChanged();
|
||||
void initialSyncFinished();
|
||||
void loginFlowsChanged();
|
||||
void ssoUrlChanged();
|
||||
void connected();
|
||||
void errorOccured(QString message);
|
||||
void testingChanged();
|
||||
void isLoggingInChanged();
|
||||
|
||||
private:
|
||||
void setHomeserverReachable(bool reachable);
|
||||
|
||||
bool m_homeserverReachable;
|
||||
QString m_matrixId;
|
||||
QString m_password;
|
||||
QString m_deviceName;
|
||||
bool m_supportsSso = false;
|
||||
bool m_supportsPassword = false;
|
||||
Connection *m_connection = nullptr;
|
||||
QUrl m_ssoUrl;
|
||||
bool m_testing;
|
||||
bool m_isLoggingIn = false;
|
||||
};
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "devicesmodel.h"
|
||||
#include "emojimodel.h"
|
||||
#include "filetypesingleton.h"
|
||||
#include "login.h"
|
||||
#include "matriximageprovider.h"
|
||||
#include "messageeventmodel.h"
|
||||
#include "messagefiltermodel.h"
|
||||
@@ -98,10 +99,13 @@ int main(int argc, char *argv[])
|
||||
auto config = NeoChatConfig::self();
|
||||
FileTypeSingleton fileTypeSingleton;
|
||||
|
||||
Login *login = new Login();
|
||||
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton);
|
||||
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login);
|
||||
qmlRegisterType<AccountListModel>("org.kde.neochat", 1, 0, "AccountListModel");
|
||||
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
|
||||
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
|
||||
|
||||
Reference in New Issue
Block a user