From 33da15daba6471f26b57f4a678d30184e583054e Mon Sep 17 00:00:00 2001 From: Kateryna Kostiuk Date: Mon, 24 Feb 2025 09:46:30 -0500 Subject: [PATCH] account: implement import-from-device using new API - Implements new APIs - Implements import-from-device mechanism (creation wizard) - Minor refactoring of accountmodel and accountadapter Gitlab: #1695 Change-Id: Ib3c6301b82b19a25320dd703f2f7e941f8048a8e --- .gitignore | 1 + daemon | 2 +- src/app/accountadapter.cpp | 91 +++- src/app/accountadapter.h | 10 + src/app/avatarimageprovider.h | 11 + src/app/commoncomponents/Avatar.qml | 5 +- src/app/net/jami/Constants/JamiStrings.qml | 26 +- src/app/qmlregister.cpp | 9 +- src/app/qrimageprovider.h | 1 - .../components/LinkDeviceDialog.qml | 20 - src/app/utils.cpp | 37 +- src/app/utils.h | 3 + src/app/wizardview/WizardView.qml | 4 +- .../components/ImportFromDevicePage.qml | 479 ++++++++++++------ src/app/wizardviewstepmodel.cpp | 27 +- src/app/wizardviewstepmodel.h | 25 +- src/libclient/accountmodel.cpp | 139 +++-- src/libclient/api/account.h | 62 ++- src/libclient/api/accountmodel.h | 91 +++- src/libclient/callbackshandler.cpp | 14 +- src/libclient/callbackshandler.h | 26 +- src/libclient/namedirectory.cpp | 15 - src/libclient/namedirectory.h | 14 +- src/libclient/private/namedirectory_p.h | 1 - .../qtwrapper/configurationmanager_wrap.h | 54 +- 25 files changed, 820 insertions(+), 347 deletions(-) diff --git a/.gitignore b/.gitignore index 3f839901..55f90aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ doc/Doxyfile !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +*.code-workspace ### VisualStudioCode Patch ### # Ignore all local history of files diff --git a/daemon b/daemon index 597cde8d..86d3bb66 160000 --- a/daemon +++ b/daemon @@ -1 +1 @@ -Subproject commit 597cde8d30814b5078e2ac8c8a0953dd471ec716 +Subproject commit 86d3bb664489077107e68b838e419f4cd6459859 diff --git a/src/app/accountadapter.cpp b/src/app/accountadapter.cpp index 5d4daffa..13b2a301 100644 --- a/src/app/accountadapter.cpp +++ b/src/app/accountadapter.cpp @@ -22,8 +22,11 @@ #include "systemtray.h" #include "lrcinstance.h" #include "accountlistmodel.h" +#include "wizardviewstepmodel.h" +#include "global.h" +#include "api/account.h" -#include +#include AccountAdapter::AccountAdapter(AppSettingsManager* settingsManager, SystemTray* systemTray, @@ -111,7 +114,10 @@ AccountAdapter::createJamiAccount(const QVariantMap& settings) &lrcInstance_->accountModel(), &lrc::api::AccountModel::accountAdded, [this, registeredName, settings](const QString& accountId) { - lrcInstance_->accountModel().setAvatar(accountId, settings["avatar"].toString(), true,1); + lrcInstance_->accountModel().setAvatar(accountId, + settings["avatar"].toString(), + true, + 1); Utils::oneShotConnect(&lrcInstance_->accountModel(), &lrc::api::AccountModel::accountDetailsChanged, [this](const QString& accountId) { @@ -159,8 +165,9 @@ AccountAdapter::createJamiAccount(const QVariantMap& settings) connectFailure(); - auto futureResult = QtConcurrent::run([this, settings] { + QThreadPool::globalInstance()->start([this, settings] { lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::JAMI, + {}, settings["alias"].toString(), settings["archivePath"].toString(), settings["password"].toString(), @@ -206,14 +213,14 @@ AccountAdapter::createSIPAccount(const QVariantMap& settings) connectFailure(); - auto futureResult = QtConcurrent::run([this, settings] { + QThreadPool::globalInstance()->start([this, settings] { lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::SIP, + {}, settings["alias"].toString(), settings["archivePath"].toString(), "", "", - settings["username"].toString(), - {}); + settings["username"].toString()); }); } @@ -250,7 +257,7 @@ AccountAdapter::createJAMSAccount(const QVariantMap& settings) connectFailure(); - auto futureResult = QtConcurrent::run([this, settings] { + QThreadPool::globalInstance()->start([this, settings] { lrcInstance_->accountModel().connectToAccountManager(settings["username"].toString(), settings["password"].toString(), settings["manager"].toString()); @@ -293,7 +300,7 @@ AccountAdapter::setCurrAccDisplayName(const QString& text) void AccountAdapter::setCurrentAccountAvatarFile(const QString& source) { - auto futureResult = QtConcurrent::run([this, source]() { + QThreadPool::globalInstance()->start([this, source]() { QPixmap image; if (!image.load(source)) { qWarning() << "Not a valid image file"; @@ -308,7 +315,7 @@ AccountAdapter::setCurrentAccountAvatarFile(const QString& source) void AccountAdapter::setCurrentAccountAvatarBase64(const QString& data) { - auto futureResult = QtConcurrent::run([this, data]() { + QThreadPool::globalInstance()->start([this, data]() { auto accountId = lrcInstance_->get_currentAccountId(); lrcInstance_->accountModel().setAvatar(accountId, data, true, 1); }); @@ -339,9 +346,73 @@ AccountAdapter::exportToFile(const QString& accountId, void AccountAdapter::setArchivePasswordAsync(const QString& accountID, const QString& password) { - auto futureResult = QtConcurrent::run([this, accountID, password] { + QThreadPool::globalInstance()->start([this, accountID, password] { auto config = lrcInstance_->accountModel().getAccountConfig(accountID); config.archivePassword = password; lrcInstance_->accountModel().setAccountConfig(accountID, config); }); } + +void +AccountAdapter::startImportAccount() +{ + auto wizardModel = qApp->property("WizardViewStepModel").value(); + wizardModel->set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT); + wizardModel->set_deviceLinkDetails({}); + + // This will create an account with the ARCHIVE_URL configured to start the import process. + importAccountId_ = lrcInstance_->accountModel().createDeviceImportAccount(); +} + +void +AccountAdapter::provideAccountAuthentication(const QString& password) +{ + if (importAccountId_.isEmpty()) { + qWarning() << "No import account to provide password to"; + return; + } + + auto wizardModel = qApp->property("WizardViewStepModel").value(); + wizardModel->set_deviceAuthState(lrc::api::account::DeviceAuthState::IN_PROGRESS); + + Utils::oneShotConnect( + &lrcInstance_->accountModel(), + &lrc::api::AccountModel::accountAdded, + [this](const QString& accountId) { + Q_EMIT lrcInstance_->accountListChanged(); + Q_EMIT accountAdded(accountId, + lrcInstance_->accountModel().getAccountList().indexOf(accountId)); + }, + this, + &AccountAdapter::accountCreationFailed); + + connectFailure(); + + QThreadPool::globalInstance()->start([this, password] { + lrcInstance_->accountModel().provideAccountAuthentication(importAccountId_, password); + }); +} + +QString +AccountAdapter::getImportErrorMessage(QVariantMap details) +{ + QString errorString = details.value("error").toString(); + if (!errorString.isEmpty() && errorString != "none") { + auto error = lrc::api::account::mapLinkDeviceError(errorString.toStdString()); + return lrc::api::account::getLinkDeviceString(error); + } + + return ""; +} + +void +AccountAdapter::cancelImportAccount() +{ + auto wizardModel = qApp->property("WizardViewStepModel").value(); + wizardModel->set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT); + wizardModel->set_deviceLinkDetails({}); + + // Remove the account if it was created + lrcInstance_->accountModel().removeAccount(importAccountId_); + importAccountId_.clear(); +} diff --git a/src/app/accountadapter.h b/src/app/accountadapter.h index bd2f7675..16845379 100644 --- a/src/app/accountadapter.h +++ b/src/app/accountadapter.h @@ -81,6 +81,13 @@ public: const bool& state); Q_INVOKABLE QStringList getDefaultModerators(const QString& accountId); + // New import account / link device functions + // import: (note: Listen for: DeviceAuthStateChanged) + Q_INVOKABLE void startImportAccount(); + Q_INVOKABLE void provideAccountAuthentication(const QString& password = {}); + Q_INVOKABLE QString getImportErrorMessage(QVariantMap details); + Q_INVOKABLE void cancelImportAccount(); + Q_SIGNALS: // Trigger other components to reconnect account related signals. void accountStatusChanged(QString accountId); @@ -98,6 +105,9 @@ private: QMetaObject::Connection registeredNameSavedConnection_; + // The account ID of the last used import account. + QString importAccountId_; + AppSettingsManager* settingsManager_; SystemTray* systemTray_; }; diff --git a/src/app/avatarimageprovider.h b/src/app/avatarimageprovider.h index 36e21787..8e0f5d00 100644 --- a/src/app/avatarimageprovider.h +++ b/src/app/avatarimageprovider.h @@ -22,6 +22,7 @@ #include "lrcinstance.h" #include +#include class AsyncAvatarImageResponseRunnable : public AsyncImageResponseRunnable { @@ -69,6 +70,16 @@ public: image = Utils::accountPhoto(lrcInstance_, imageId, requestedSize_); } else if (type == "contact") { image = Utils::contactPhoto(lrcInstance_, imageId, requestedSize_); + } else if (type == "temporaryAccount") { + // Check if imageId is a SHA-1 hash (jamiId or registered name) + static const QRegularExpression sha1Pattern("^[0-9a-fA-F]{40}$"); + if (sha1Pattern.match(imageId).hasMatch()) { + // If we only have a jamiId use default avatar + image = Utils::fallbackAvatar("jami:" + imageId, QString(), requestedSize_); + } else { + // For registered usernames, use fallbackAvatar avatar with the name + image = Utils::fallbackAvatar(QString(), imageId, requestedSize_); + } } else { qWarning() << Q_FUNC_INFO << "Missing valid prefix in the image url"; return; diff --git a/src/app/commoncomponents/Avatar.qml b/src/app/commoncomponents/Avatar.qml index 89728ded..270100d8 100644 --- a/src/app/commoncomponents/Avatar.qml +++ b/src/app/commoncomponents/Avatar.qml @@ -28,7 +28,8 @@ Item { enum Mode { Account, Contact, - Conversation + Conversation, + TemporaryAccount } property int mode: Avatar.Mode.Account property alias sourceSize: image.sourceSize @@ -45,6 +46,8 @@ Item { return 'contact'; case Avatar.Mode.Conversation: return 'conversation'; + case Avatar.Mode.TemporaryAccount: + return 'temporaryAccount'; } } diff --git a/src/app/net/jami/Constants/JamiStrings.qml b/src/app/net/jami/Constants/JamiStrings.qml index f9ab4902..d848e849 100644 --- a/src/app/net/jami/Constants/JamiStrings.qml +++ b/src/app/net/jami/Constants/JamiStrings.qml @@ -70,6 +70,21 @@ Item { property string transferThisCall: qsTr("Transfer this call") property string transferTo: qsTr("Transfer to") + // Device import/linking + property string scanToImportAccount: qsTr("Scan this QR code on your other device to proceed with importing your account.") + property string waitingForToken: qsTr("Please wait…") + property string scanQRCode: qsTr("Scan QR code") + property string connectingToDevice: qsTr("Action required.\nPlease confirm account on your old device.") + property string confirmAccountImport: qsTr("Authenticating device") + property string transferringAccount: qsTr("Transferring account…") + property string cantScanQRCode: qsTr("If you are unable to scan the QR code, enter this token on your other device to proceed.") + property string optionConfirm: qsTr("Confirm") + property string optionTryAgain: qsTr("Try again") + property string importFailed: qsTr("Import failed") + property string importFromAnotherAccount: qsTr("Import from another account") + property string connectToAccount: qsTr("Connect to account") + property string authenticationError: qsTr("An authentication error occurred. Please check credentials and try again.") + // AccountMigrationDialog property string authenticationRequired: qsTr("Authentication required") property string migrationReason: qsTr("Your session has expired or been revoked on this device. Please enter your password.") @@ -579,19 +594,8 @@ Item { // ImportFromDevicePage property string importButton: qsTr("Import") property string pin: qsTr("Enter the PIN code") - property string importFromDeviceDescription: qsTr("A PIN code is required to use an existing Jami account on this device.") - property string importStep1: qsTr("Step 1") - property string importStep2: qsTr("Step 2") - property string importStep3: qsTr("Step 3") - property string importStep4: qsTr("Step 4") - property string importStep1Desc: qsTr("Open the manage account tab in the settings of the previous device.") - property string importStep2Desc: qsTr("Select the account to link.") - property string importStep3Desc: qsTr("Select “Link new device.”") - property string importStep4Desc: qsTr("The PIN code will expire in 10 minutes.") - property string importPasswordDesc: qsTr("Fill if the account is password-encrypted.") // LinkDevicesDialog - property string pinTimerInfos: qsTr("The PIN code and the account password should be entered in the device within 10 minutes.") property string close: qsTr("Close") property string enterAccountPassword: qsTr("Enter account password") property string enterPasswordPinCode: qsTr("This account is password encrypted, enter the password to generate a PIN code.") diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp index 19357e4e..de94474e 100644 --- a/src/app/qmlregister.cpp +++ b/src/app/qmlregister.cpp @@ -179,6 +179,12 @@ registerTypes(QQmlEngine* engine, QQmlEngine::setObjectOwnership(pluginStoreListModel, QQmlEngine::CppOwnership); REG_QML_SINGLETON(REG_MODEL, "PluginStoreListModel", CREATE(pluginStoreListModel)); + // WizardViewStepModel + auto wizardViewStepModel = new WizardViewStepModel(lrcInstance, settingsManager, app); + qApp->setProperty("WizardViewStepModel", QVariant::fromValue(wizardViewStepModel)); + QQmlEngine::setObjectOwnership(wizardViewStepModel, QQmlEngine::CppOwnership); + REG_QML_SINGLETON(REG_MODEL, "WizardViewStepModel", CREATE(wizardViewStepModel)); + // Register app-level objects that are used by QML created objects. // These MUST be set prior to loading the initial QML file, in order to // be available to the QML adapter class factory creation methods. @@ -205,7 +211,6 @@ registerTypes(QQmlEngine* engine, QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, TipsModel); QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices); QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate); - QML_REGISTERSINGLETON_TYPE(NS_MODELS, WizardViewStepModel); QML_REGISTERSINGLETON_TYPE(NS_HELPERS, ImageDownloader); // TODO: remove these @@ -263,12 +268,12 @@ registerTypes(QQmlEngine* engine, // Enums QML_REGISTERUNCREATABLE(NS_ENUMS, Settings) QML_REGISTERUNCREATABLE(NS_ENUMS, NetworkManager) - QML_REGISTERUNCREATABLE(NS_ENUMS, WizardViewStepModel) QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceItemListModel) QML_REGISTERUNCREATABLE(NS_ENUMS, ModeratorListModel) QML_REGISTERUNCREATABLE(NS_ENUMS, VideoInputDeviceModel) QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatResolutionModel) QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatFpsModel) + QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceAuthStateEnum) engine->addImageProvider(QLatin1String("qrImage"), new QrImageProvider(lrcInstance)); engine->addImageProvider(QLatin1String("avatarimage"), new AvatarImageProvider(lrcInstance)); diff --git a/src/app/qrimageprovider.h b/src/app/qrimageprovider.h index 72c9ab8c..c86172ea 100644 --- a/src/app/qrimageprovider.h +++ b/src/app/qrimageprovider.h @@ -18,7 +18,6 @@ #pragma once #include "quickimageproviderbase.h" -#include "accountlistmodel.h" #include #include diff --git a/src/app/settingsview/components/LinkDeviceDialog.qml b/src/app/settingsview/components/LinkDeviceDialog.qml index 813a2335..7148d502 100644 --- a/src/app/settingsview/components/LinkDeviceDialog.qml +++ b/src/app/settingsview/components/LinkDeviceDialog.qml @@ -42,7 +42,6 @@ BaseModalDialog { } stackedWidget.currentIndex = exportingSpinnerPage.pageIndex; spinnerMovie.playing = true; - timerForExport.restart(); } function setExportPage(status, pin) { @@ -69,25 +68,6 @@ BaseModalDialog { stackedWidget.height = exportingLayout.implicitHeight; } - Timer { - id: timerForExport - - repeat: false - interval: 200 - - onTriggered: { - AccountAdapter.model.exportOnRing(LRCInstance.currentAccountId, passwordEdit.dynamicText); - } - } - - Connections { - target: NameDirectory - - function onExportOnRingEnded(status, pin) { - stackedWidget.setExportPage(status, pin); - } - } - onVisibleChanged: { if (visible) { if (CurrentAccount.hasArchivePassword) { diff --git a/src/app/utils.cpp b/src/app/utils.cpp index ad4f500c..52cdb786 100644 --- a/src/app/utils.cpp +++ b/src/app/utils.cpp @@ -165,8 +165,7 @@ Utils::CreateStartupLink(const std::wstring& wstrAppName) #endif if (desktopPath.isEmpty() || !(QFile::exists(desktopPath))) { - qDebug() << "Error while attempting to locate .desktop file at" - << desktopPath; + qDebug() << "Error while attempting to locate .desktop file at" << desktopPath; return false; } @@ -193,8 +192,7 @@ Utils::CreateStartupLink(const std::wstring& wstrAppName) if (QDir().mkdir(autoStartDir)) { qDebug() << "Created autostart directory:" << autoStartDir; } else { - qWarning() << "Error while creating autostart directory:" - << autoStartDir; + qWarning() << "Error while creating autostart directory:" << autoStartDir; return false; } } @@ -283,7 +281,8 @@ Utils::CheckStartupLink(const std::wstring& wstrAppName) #else Q_UNUSED(wstrAppName) return ( - !QStandardPaths::locate(QStandardPaths::ConfigLocation, "autostart/net.jami.Jami.desktop").isEmpty()); + !QStandardPaths::locate(QStandardPaths::ConfigLocation, "autostart/net.jami.Jami.desktop") + .isEmpty()); #endif } @@ -616,14 +615,16 @@ Utils::getProjectCredits() return {}; } QTextStream in(&projectCreditsFile); - return in.readAll().arg( - QObject::tr("We would like to thank our contributors, whose efforts over many years have made this software what it is."), - QObject::tr("Developers"), - QObject::tr("Media"), - QObject::tr("Community Management"), - QObject::tr("Special thanks to"), - QObject::tr("This is a list of people who have made a significant investment of time, with useful results, into Jami. Any such contributors who want to be added to the list should contact us.") - ); + return in.readAll().arg(QObject::tr("We would like to thank our contributors, whose efforts " + "over many years have made this software what it is."), + QObject::tr("Developers"), + QObject::tr("Media"), + QObject::tr("Community Management"), + QObject::tr("Special thanks to"), + QObject::tr( + "This is a list of people who have made a significant investment " + "of time, with useful results, into Jami. Any such contributors " + "who want to be added to the list should contact us.")); } inline QString @@ -951,3 +952,13 @@ Utils::getTempSwarmAvatarPath() return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QDir::separator() + "tmpSwarmImage"; } + +QVariantMap +Utils::mapStringStringToVariantMap(const MapStringString& map) +{ + QVariantMap variantMap; + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + variantMap.insert(it.key(), it.value()); + } + return variantMap; +} diff --git a/src/app/utils.h b/src/app/utils.h index ec0e6bc2..7124a1fd 100644 --- a/src/app/utils.h +++ b/src/app/utils.h @@ -120,4 +120,7 @@ QString generateUid(); QString humanFileSize(qint64 fileSize); QString getDebugFilePath(); +// Convert a MapStringString to a QVariantMap +QVariantMap mapStringStringToVariantMap(const MapStringString& map); + } // namespace Utils diff --git a/src/app/wizardview/WizardView.qml b/src/app/wizardview/WizardView.qml index 151c198d..13da78fd 100644 --- a/src/app/wizardview/WizardView.qml +++ b/src/app/wizardview/WizardView.qml @@ -56,9 +56,11 @@ BaseView { case WizardViewStepModel.AccountCreationOption.CreateJamiAccount: case WizardViewStepModel.AccountCreationOption.CreateRendezVous: case WizardViewStepModel.AccountCreationOption.ImportFromBackup: - case WizardViewStepModel.AccountCreationOption.ImportFromDevice: AccountAdapter.createJamiAccount(WizardViewStepModel.accountCreationInfo); break; + case WizardViewStepModel.AccountCreationOption.ImportFromDevice: + AccountAdapter.startImportAccount(); + break; case WizardViewStepModel.AccountCreationOption.ConnectToAccountManager: AccountAdapter.createJAMSAccount(WizardViewStepModel.accountCreationInfo); break; diff --git a/src/app/wizardview/components/ImportFromDevicePage.qml b/src/app/wizardview/components/ImportFromDevicePage.qml index 4d59ffc8..b308ddf4 100644 --- a/src/app/wizardview/components/ImportFromDevicePage.qml +++ b/src/app/wizardview/components/ImportFromDevicePage.qml @@ -17,9 +17,13 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls +import QtQuick.Dialogs +import net.jami.Adapters 1.1 import net.jami.Models 1.1 import net.jami.Constants 1.1 +import net.jami.Enums 1.1 import "../../commoncomponents" +import "../../mainview/components" Rectangle { id: root @@ -27,30 +31,99 @@ Rectangle { property string errorText: "" property int preferredHeight: importFromDevicePageColumnLayout.implicitHeight + 2 * JamiTheme.preferredMarginSize - signal showThisPage + // The token is used to generate the QR code and is also provided to the user as a backup if the QR + // code cannot be scanned. It is a URI using the scheme "jami-auth". + readonly property string tokenUri: WizardViewStepModel.deviceLinkDetails["token"] || "" - function initializeOnShowUp() { - clearAllTextFields(); + property string jamiId: "" + + function isPasswordWrong() { + return WizardViewStepModel.deviceLinkDetails["auth_error"] !== undefined && + WizardViewStepModel.deviceLinkDetails["auth_error"] !== "" && + WizardViewStepModel.deviceLinkDetails["auth_error"] !== "none" } + function requiresPassword() { + return WizardViewStepModel.deviceLinkDetails["auth_scheme"] === "password" + } + + function requiresConfirmationBeforeClosing() { + const state = WizardViewStepModel.deviceAuthState + return state !== DeviceAuthStateEnum.INIT && + state !== DeviceAuthStateEnum.DONE + } + + function isLoadingState() { + const state = WizardViewStepModel.deviceAuthState + return state === DeviceAuthStateEnum.INIT || + state === DeviceAuthStateEnum.CONNECTING || + state === DeviceAuthStateEnum.IN_PROGRESS + } + + signal showThisPage + function clearAllTextFields() { - connectBtn.spinnerTriggered = false; + errorText = ""; } function errorOccurred(errorMessage) { errorText = errorMessage; - connectBtn.spinnerTriggered = false; + } + + MessageDialog { + id: confirmCloseDialog + + text: JamiStrings.linkDeviceCloseWarningTitle + informativeText: JamiStrings.linkDeviceCloseWarningMessage + buttons: MessageDialog.Ok | MessageDialog.Cancel + + onButtonClicked: function(button) { + if (button === MessageDialog.Ok) { + AccountAdapter.cancelImportAccount(); + WizardViewStepModel.previousStep(); + } + } } Connections { target: WizardViewStepModel function onMainStepChanged() { - if (WizardViewStepModel.mainStep === WizardViewStepModel.MainSteps.AccountCreation && WizardViewStepModel.accountCreationOption === WizardViewStepModel.AccountCreationOption.ImportFromDevice) { + if (WizardViewStepModel.mainStep === WizardViewStepModel.MainSteps.DeviceAuthorization) { clearAllTextFields(); root.showThisPage(); } } + + function onDeviceAuthStateChanged() { + switch (WizardViewStepModel.deviceAuthState) { + case DeviceAuthStateEnum.TOKEN_AVAILABLE: + // Token is available and displayed as QR code + clearAllTextFields(); + break; + case DeviceAuthStateEnum.CONNECTING: + // P2P connection being established + clearAllTextFields(); + break; + case DeviceAuthStateEnum.AUTHENTICATING: + jamiId = WizardViewStepModel.deviceLinkDetails["peer_id"] || ""; + if (jamiId.length > 0) { + NameDirectory.lookupAddress(CurrentAccount.id, jamiId) + } + break; + case DeviceAuthStateEnum.IN_PROGRESS: + // Account archive is being transferred + clearAllTextFields(); + break; + case DeviceAuthStateEnum.DONE: + // Final state - check for specific errors + const error = AccountAdapter.getImportErrorMessage(WizardViewStepModel.deviceLinkDetails); + if (error.length > 0) { + errorOccurred(error) + } + break; + } + } } color: JamiTheme.secondaryBackgroundColor @@ -65,184 +138,276 @@ Rectangle { width: Math.max(508, root.width - 100) Text { - - text: JamiStrings.importAccountFromAnotherDevice + text: JamiStrings.importFromAnotherAccount Layout.alignment: Qt.AlignCenter Layout.topMargin: JamiTheme.preferredMarginSize Layout.preferredWidth: Math.min(360, root.width - JamiTheme.preferredMarginSize * 2) horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - color: JamiTheme.textColor + color: JamiTheme.textColor font.pixelSize: JamiTheme.wizardViewTitleFontPixelSize wrapMode: Text.WordWrap } Text { - - text: JamiStrings.importFromDeviceDescription - Layout.preferredWidth: Math.min(360, root.width - JamiTheme.preferredMarginSize * 2) - Layout.topMargin: JamiTheme.wizardViewDescriptionMarginSize - Layout.alignment: Qt.AlignCenter - font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize - font.weight: Font.Medium - color: JamiTheme.textColor - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - lineHeight: JamiTheme.wizardViewTextLineHeight - } - - Flow { - spacing: 30 Layout.alignment: Qt.AlignHCenter - Layout.topMargin: JamiTheme.wizardViewBlocMarginSize - Layout.preferredWidth: Math.min(step1.width * 2 + spacing, root.width - JamiTheme.preferredMarginSize * 2) - - InfoBox { - id: step1 - icoSource: JamiResources.settings_24dp_svg - title: JamiStrings.importStep1 - description: JamiStrings.importStep1Desc - icoColor: JamiTheme.buttonTintedBlue - } - - InfoBox { - id: step2 - icoSource: JamiResources.person_24dp_svg - title: JamiStrings.importStep2 - description: JamiStrings.importStep2Desc - icoColor: JamiTheme.buttonTintedBlue - } - - InfoBox { - id: step3 - icoSource: JamiResources.finger_select_svg - title: JamiStrings.importStep3 - description: JamiStrings.importStep3Desc - icoColor: JamiTheme.buttonTintedBlue - } - - InfoBox { - id: step4 - icoSource: JamiResources.time_clock_svg - title: JamiStrings.importStep4 - description: JamiStrings.importStep4Desc - icoColor: JamiTheme.buttonTintedBlue - } - } - - ModalTextEdit { - id: pinFromDevice - - objectName: "pinFromDevice" - - Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: Math.min(410, root.width - JamiTheme.preferredMarginSize * 2) - Layout.topMargin: JamiTheme.wizardViewBlocMarginSize - - focus: visible - - placeholderText: JamiStrings.pin - staticText: "" - - KeyNavigation.up: backButton - KeyNavigation.down: passwordFromDevice - KeyNavigation.tab: KeyNavigation.down - - onAccepted: passwordFromDevice.forceActiveFocus() - } - - Text { - - Layout.alignment: Qt.AlignCenter - Layout.topMargin: JamiTheme.wizardViewBlocMarginSize - - color: JamiTheme.textColor - wrapMode: Text.WordWrap - text: JamiStrings.importPasswordDesc + Layout.maximumWidth: parent.width + horizontalAlignment: Text.AlignHCenter font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize - font.weight: Font.Medium + lineHeight: JamiTheme.wizardViewTextLineHeight + text: { + switch (WizardViewStepModel.deviceAuthState) { + case DeviceAuthStateEnum.INIT: + return JamiStrings.waitingForToken; + case DeviceAuthStateEnum.TOKEN_AVAILABLE: + return JamiStrings.scanToImportAccount; + case DeviceAuthStateEnum.CONNECTING: + return JamiStrings.connectingToDevice; + case DeviceAuthStateEnum.AUTHENTICATING: + return JamiStrings.confirmAccountImport; + case DeviceAuthStateEnum.IN_PROGRESS: + return JamiStrings.transferringAccount; + case DeviceAuthStateEnum.DONE: + return errorText.length > 0 ? JamiStrings.importFailed : ""; + default: + return ""; + } + } + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + color: JamiTheme.textColor } - PasswordTextEdit { - id: passwordFromDevice + // Confirmation form + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: Math.min(parent.width - 40, 400) + visible: WizardViewStepModel.deviceAuthState === DeviceAuthStateEnum.AUTHENTICATING + spacing: JamiTheme.wizardViewPageLayoutSpacing - objectName: "passwordFromDevice" - Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: Math.min(410, root.width - JamiTheme.preferredMarginSize * 2) - Layout.topMargin: JamiTheme.wizardViewMarginSize - - placeholderText: JamiStrings.enterPassword - - KeyNavigation.up: pinFromDevice - KeyNavigation.down: { - if (connectBtn.enabled) - return connectBtn; - else if (connectBtn.spinnerTriggered) - return passwordFromDevice; - return backButton; - } - KeyNavigation.tab: KeyNavigation.down - - onAccepted: pinFromDevice.forceActiveFocus() - } - - SpinnerButton { - id: connectBtn - - TextMetrics { - id: textSize - font.weight: Font.Bold - font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize - text: connectBtn.normalText + Text { + Layout.fillWidth: true + font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize + lineHeight: JamiTheme.wizardViewTextLineHeight + text: JamiStrings.connectToAccount + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + horizontalAlignment: Text.AlignHCenter + color: JamiTheme.textColor + font.bold: true } - objectName: "importFromDevicePageConnectBtn" + // Account Widget (avatar + username + ID) + Rectangle { + id: accountContainer + Layout.alignment: Qt.AlignHCenter + implicitWidth: accountLayout.implicitWidth + 40 + implicitHeight: accountLayout.implicitHeight + 40 + radius: 8 + color: JamiTheme.primaryBackgroundColor + border.width: 1 + border.color: JamiTheme.tabbarBorderColor - Layout.alignment: Qt.AlignCenter - Layout.topMargin: JamiTheme.wizardViewBlocMarginSize - Layout.bottomMargin: errorLabel.visible ? 0 : JamiTheme.wizardViewPageBackButtonMargins + RowLayout { + id: accountLayout + anchors { + centerIn: parent + } + spacing: 20 - preferredWidth: textSize.width + 2 * JamiTheme.buttontextWizzardPadding + 1 - primary: true + Avatar { + id: accountAvatar + showPresenceIndicator: false + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + mode: Avatar.Mode.TemporaryAccount + imageId: name.text || jamiId + } - spinnerTriggeredtext: JamiStrings.generatingAccount - normalText: JamiStrings.importButton + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + spacing: 4 - enabled: pinFromDevice.dynamicText.length !== 0 && !spinnerTriggered + Text { + id: name + visible: text !== undefined && text !== "" - KeyNavigation.tab: backButton - KeyNavigation.up: passwordFromDevice - KeyNavigation.down: backButton + Connections { + id: registeredNameFoundConnection + target: NameDirectory + enabled: jamiId.length > 0 - onClicked: { - spinnerTriggered = true; - WizardViewStepModel.accountCreationInfo = JamiQmlUtils.setUpAccountCreationInputPara({ - "archivePin": pinFromDevice.dynamicText, - "password": passwordFromDevice.dynamicText - }); - WizardViewStepModel.nextStep(); + function onRegisteredNameFound(status, address, registeredName, requestedName) { + if (address === jamiId && status === NameDirectory.LookupStatus.SUCCESS) { + name.text = registeredName; + } + } + } + } + Text { + id: userId + text: jamiId + } + } + } + } + + // Password + PasswordTextEdit { + id: passwordField + + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + Layout.topMargin: 10 + Layout.bottomMargin: 10 + visible: requiresPassword() + placeholderText: JamiStrings.enterPassword + echoMode: TextInput.Password + + onAccepted: confirmButton.clicked() + } + + Text { + id: passwordErrorField + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width - 40 + visible: isPasswordWrong() + text: JamiStrings.authenticationError + font.pointSize: JamiTheme.tinyFontSize + color: JamiTheme.redColor + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 16 + Layout.margins: 10 + + MaterialButton { + id: confirmButton + text: JamiStrings.optionConfirm + primary: true + enabled: !passwordField.visible || passwordField.dynamicText.length > 0 + onClicked: { + AccountAdapter.provideAccountAuthentication(passwordField.visible ? passwordField.dynamicText : ""); + } + } } } - Label { - id: errorLabel + // Show busy indicator when waiting for token + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + visible: isLoadingState() + Layout.preferredWidth: 50 + Layout.preferredHeight: 50 + running: visible + } - Layout.alignment: Qt.AlignCenter - Layout.bottomMargin: JamiTheme.wizardViewPageBackButtonMargins + // QR Code container with frame + Rectangle { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: qrLoader.Layout.preferredWidth + 40 + Layout.preferredHeight: qrLoader.Layout.preferredHeight + 40 + visible: WizardViewStepModel.deviceAuthState === DeviceAuthStateEnum.TOKEN_AVAILABLE + color: JamiTheme.primaryBackgroundColor + radius: 8 + border.width: 1 + border.color: JamiTheme.tabbarBorderColor - visible: errorText.length !== 0 + Loader { + id: qrLoader + anchors.centerIn: parent + active: WizardViewStepModel.deviceAuthState === DeviceAuthStateEnum.TOKEN_AVAILABLE + Layout.preferredWidth: Math.min(parent.parent.width - 60, 250) + Layout.preferredHeight: Layout.preferredWidth - text: errorText + sourceComponent: Image { + width: qrLoader.Layout.preferredWidth + height: qrLoader.Layout.preferredHeight + smooth: false + fillMode: Image.PreserveAspectFit + source: "image://qrImage/raw_" + tokenUri + } + } + } - font.pixelSize: JamiTheme.textEditError - color: JamiTheme.redColor + // Token URI backup text + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + visible: tokenUri !== "" + spacing: 8 + + Text { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.parent.width - 40 + horizontalAlignment: Text.AlignHCenter + text: JamiStrings.cantScanQRCode + font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize + lineHeight: JamiTheme.wizardViewTextLineHeight + color: JamiTheme.textColor + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + + TextArea { + id: tokenUriTextArea + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.parent.width - 40 + text: tokenUri + font.pointSize: JamiTheme.wizardViewDescriptionFontPixelSize + horizontalAlignment: Text.AlignHCenter + readOnly: true + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + selectByMouse: true + background: Rectangle { + color: JamiTheme.primaryBackgroundColor + radius: 5 + border.width: 1 + border.color: JamiTheme.tabbarBorderColor + } + } + } + + // Error view + ColumnLayout { + id: errorColumn + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width - 40 + visible: errorText !== "" + spacing: 16 + + Text { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width + text: errorText + color: JamiTheme.textColor + font.pointSize: JamiTheme.mediumFontSize + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + MaterialButton { + Layout.alignment: Qt.AlignHCenter + text: JamiStrings.optionTryAgain + toolTipText: JamiStrings.optionTryAgain + primary: true + onClicked: { + AccountAdapter.cancelImportAccount(); + WizardViewStepModel.previousStep(); + } + } } } - BackButton { + // Back button + JamiPushButton { id: backButton + QWKSetParentHitTestVisible { + } objectName: "importFromDevicePageBackButton" @@ -250,12 +415,18 @@ Rectangle { anchors.top: parent.top anchors.margins: JamiTheme.wizardViewPageBackButtonMargins - visible: !connectBtn.spinnerTriggered + preferredSize: 36 + imageContainerWidth: 20 + source: JamiResources.ic_arrow_back_24dp_svg - KeyNavigation.tab: pinFromDevice - KeyNavigation.up: connectBtn.enabled ? connectBtn : passwordFromDevice - KeyNavigation.down: pinFromDevice + visible: WizardViewStepModel.deviceAuthState !== DeviceAuthStateEnum.IN_PROGRESS - onClicked: WizardViewStepModel.previousStep() + onClicked: { + if (requiresConfirmationBeforeClosing()) { + confirmCloseDialog.open(); + } else { + WizardViewStepModel.previousStep(); + } + } } } diff --git a/src/app/wizardviewstepmodel.cpp b/src/app/wizardviewstepmodel.cpp index 06567c02..6b19cd52 100644 --- a/src/app/wizardviewstepmodel.cpp +++ b/src/app/wizardviewstepmodel.cpp @@ -19,6 +19,7 @@ #include "appsettingsmanager.h" #include "lrcinstance.h" +#include "global.h" #include "api/accountmodel.h" @@ -46,17 +47,31 @@ WizardViewStepModel::WizardViewStepModel(LRCInstance* lrcInstance, Q_EMIT accountIsReady(accountId); }); + + // Connect to account model signals to track import progress + connect(&lrcInstance_->accountModel(), + &AccountModel::deviceAuthStateChanged, + this, + [this](const QString& accountID, int state, const MapStringString& details) { + set_deviceLinkDetails(Utils::mapStringStringToVariantMap(details)); + set_deviceAuthState(static_cast(state)); + }); } void WizardViewStepModel::startAccountCreationFlow(AccountCreationOption accountCreationOption) { + using namespace lrc::api::account; set_accountCreationOption(accountCreationOption); - if (accountCreationOption == AccountCreationOption::CreateJamiAccount - || accountCreationOption == AccountCreationOption::CreateRendezVous) + if (accountCreationOption == AccountCreationOption::ImportFromDevice) { + set_mainStep(MainSteps::DeviceAuthorization); + Q_EMIT createAccountRequested(accountCreationOption); + } else if (accountCreationOption == AccountCreationOption::CreateJamiAccount + || accountCreationOption == AccountCreationOption::CreateRendezVous) { set_mainStep(MainSteps::NameRegistration); - else + } else { set_mainStep(MainSteps::AccountCreation); + } } void @@ -80,6 +95,10 @@ WizardViewStepModel::previousStep() reset(); break; } + case MainSteps::DeviceAuthorization: { + reset(); + break; + } } } @@ -88,4 +107,6 @@ WizardViewStepModel::reset() { set_accountCreationOption(AccountCreationOption::None); set_mainStep(MainSteps::Initial); + set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT); + set_deviceLinkDetails({}); } diff --git a/src/app/wizardviewstepmodel.h b/src/app/wizardviewstepmodel.h index ebf4ff0c..084c4704 100644 --- a/src/app/wizardviewstepmodel.h +++ b/src/app/wizardviewstepmodel.h @@ -18,6 +18,7 @@ #pragma once #include "qtutils.h" +#include "api/account.h" // Include for DeviceAuthState #include #include @@ -29,6 +30,21 @@ class AccountAdapter; class LRCInstance; class AppSettingsManager; +class DeviceAuthStateEnum : public QObject +{ + Q_OBJECT +public: + enum State { + INIT = static_cast(lrc::api::account::DeviceAuthState::INIT), + TOKEN_AVAILABLE = static_cast(lrc::api::account::DeviceAuthState::TOKEN_AVAILABLE), + CONNECTING = static_cast(lrc::api::account::DeviceAuthState::CONNECTING), + AUTHENTICATING = static_cast(lrc::api::account::DeviceAuthState::AUTHENTICATING), + IN_PROGRESS = static_cast(lrc::api::account::DeviceAuthState::IN_PROGRESS), + DONE = static_cast(lrc::api::account::DeviceAuthState::DONE) + }; + Q_ENUM(State) +}; + class WizardViewStepModel : public QObject { Q_OBJECT @@ -37,9 +53,10 @@ class WizardViewStepModel : public QObject public: enum class MainSteps { - Initial, // Initial welcome step. - AccountCreation, // General account creation step. - NameRegistration, // Name registration step : CreateJamiAccount, CreateRendezVous + Initial, // Initial welcome step. + AccountCreation, // General account creation step. + NameRegistration, // Name registration step : CreateJamiAccount, CreateRendezVous + DeviceAuthorization // Add new step for device authorization. }; Q_ENUM(MainSteps) @@ -57,6 +74,8 @@ public: QML_PROPERTY(MainSteps, mainStep) QML_PROPERTY(AccountCreationOption, accountCreationOption) QML_PROPERTY(QVariantMap, accountCreationInfo) + QML_PROPERTY(lrc::api::account::DeviceAuthState, deviceAuthState) + QML_PROPERTY(QVariantMap, deviceLinkDetails) public: static WizardViewStepModel* create(QQmlEngine*, QJSEngine*) diff --git a/src/libclient/accountmodel.cpp b/src/libclient/accountmodel.cpp index a2826579..3d0fdd51 100644 --- a/src/libclient/accountmodel.cpp +++ b/src/libclient/accountmodel.cpp @@ -117,12 +117,14 @@ public Q_SLOTS: void slotAccountStatusChanged(const QString& accountID, const api::account::Status status); /** - * Emit exportOnRingEnded. + * Emit deviceAuthStateChanged. * @param accountId - * @param status - * @param pin + * @param state + * @param details map */ - void slotExportOnRingEnded(const QString& accountID, int status, const QString& pin); + void slotDeviceAuthStateChanged(const QString& accountID, + int state, + const MapStringString& details); /** * @param accountId @@ -282,11 +284,12 @@ AccountModel::setAlias(const QString& accountId, const QString& alias, bool save accountInfo.profileInfo.alias = alias; if (save) - ConfigurationManager::instance().updateProfile(accountId, - alias, - "", - "", - 5);// flag out of range to avoid updating avatar + ConfigurationManager::instance() + .updateProfile(accountId, + alias, + "", + "", + 5); // flag out of range to avoid updating avatar Q_EMIT profileUpdated(accountId); } @@ -323,9 +326,30 @@ AccountModel::exportToFile(const QString& accountId, } bool -AccountModel::exportOnRing(const QString& accountId, const QString& password) const +AccountModel::provideAccountAuthentication(const QString& accountId, + const QString& credentialsFromUser) const { - return ConfigurationManager::instance().exportOnRing(accountId, password); + return ConfigurationManager::instance().provideAccountAuthentication(accountId, + credentialsFromUser, + "password"); +} + +int32_t +AccountModel::addDevice(const QString& accountId, const QString& token) const +{ + return ConfigurationManager::instance().addDevice(accountId, token); +} + +bool +AccountModel::confirmAddDevice(const QString& accountId, uint32_t operationId) const +{ + return ConfigurationManager::instance().confirmAddDevice(accountId, operationId); +} + +bool +AccountModel::cancelAddDevice(const QString& accountId, uint32_t operationId) const +{ + return ConfigurationManager::instance().cancelAddDevice(accountId, operationId); } void @@ -403,9 +427,9 @@ AccountModelPimpl::AccountModelPimpl(AccountModel& linked, this, &AccountModelPimpl::slotVolatileAccountDetailsChanged); connect(&callbacksHandler, - &CallbacksHandler::exportOnRingEnded, - this, - &AccountModelPimpl::slotExportOnRingEnded); + &CallbacksHandler::deviceAuthStateChanged, + &linked, + &AccountModel::deviceAuthStateChanged); connect(&callbacksHandler, &CallbacksHandler::nameRegistrationEnded, this, @@ -594,23 +618,13 @@ AccountModelPimpl::slotVolatileAccountDetailsChanged(const QString& accountId, } void -AccountModelPimpl::slotExportOnRingEnded(const QString& accountID, int status, const QString& pin) +AccountModelPimpl::slotDeviceAuthStateChanged(const QString& accountId, + int state, + const MapStringString& details) { - account::ExportOnRingStatus convertedStatus = account::ExportOnRingStatus::INVALID; - switch (status) { - case 0: - convertedStatus = account::ExportOnRingStatus::SUCCESS; - break; - case 1: - convertedStatus = account::ExportOnRingStatus::WRONG_PASSWORD; - break; - case 2: - convertedStatus = account::ExportOnRingStatus::NETWORK_ERROR; - break; - default: - break; - } - Q_EMIT linked.exportOnRingEnded(accountID, convertedStatus, pin); + // implement business logic here + // can be bypassed with a signal to signal + Q_EMIT linked.deviceAuthStateChanged(accountId, state, details); } void @@ -673,7 +687,11 @@ AccountModelPimpl::slotRegisteredNameFound(const QString& accountId, default: break; } - Q_EMIT linked.registeredNameFound(accountId, requestedName, convertedStatus, address, registeredName); + Q_EMIT linked.registeredNameFound(accountId, + requestedName, + convertedStatus, + address, + registeredName); } void @@ -1041,32 +1059,47 @@ account::ConfProperties_t::toDetails() const QString AccountModel::createNewAccount(profile::Type type, + const MapStringString& config, const QString& displayName, const QString& archivePath, const QString& password, const QString& pin, - const QString& uri, - const MapStringString& config) + const QString& uri) { + // Get the template for the account type to prefill the details MapStringString details = type == profile::Type::SIP ? ConfigurationManager::instance().getAccountTemplate("SIP") : ConfigurationManager::instance().getAccountTemplate("RING"); - using namespace libjami::Account; - details[ConfProperties::TYPE] = type == profile::Type::SIP ? "SIP" : "RING"; - details[ConfProperties::DISPLAYNAME] = displayName; - details[ConfProperties::ALIAS] = displayName; - details[ConfProperties::UPNP_ENABLED] = "true"; - details[ConfProperties::ARCHIVE_PASSWORD] = password; - details[ConfProperties::ARCHIVE_PIN] = pin; - details[ConfProperties::ARCHIVE_PATH] = archivePath; - if (type == profile::Type::SIP) - details[ConfProperties::USERNAME] = uri; + + // Add the supplied config to the details if (!config.isEmpty()) { for (MapStringString::const_iterator it = config.begin(); it != config.end(); it++) { details[it.key()] = it.value(); } } + using namespace libjami::Account; + + // Add the rest of the details if we are not creating an ephemeral account for linking + // in which case the ARCHIVE_URL was set to "jami-auth" or the MANAGER_URI was set to + // the account manager URI in the case of a remote account manager connection + if (details[ConfProperties::ARCHIVE_URL].isEmpty() + && details[ConfProperties::MANAGER_URI].isEmpty()) { + details[ConfProperties::TYPE] = type == profile::Type::SIP ? "SIP" : "RING"; + details[ConfProperties::DISPLAYNAME] = displayName; + details[ConfProperties::ALIAS] = displayName; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = password; + details[ConfProperties::ARCHIVE_PIN] = pin; + details[ConfProperties::ARCHIVE_PATH] = archivePath; + + // Override the username with the provided URI if it's a SIP account + if (type == profile::Type::SIP) { + details[ConfProperties::USERNAME] = uri; + } + } + + // Actually add the account and return the account ID QString accountId = ConfigurationManager::instance().addAccount(details); return accountId; } @@ -1077,20 +1110,24 @@ AccountModel::connectToAccountManager(const QString& username, const QString& serverUri, const MapStringString& config) { - MapStringString details = ConfigurationManager::instance().getAccountTemplate("RING"); + MapStringString details = config; using namespace libjami::Account; details[ConfProperties::TYPE] = "RING"; details[ConfProperties::MANAGER_URI] = serverUri; details[ConfProperties::MANAGER_USERNAME] = username; details[ConfProperties::ARCHIVE_PASSWORD] = password; - if (!config.isEmpty()) { - for (MapStringString::const_iterator it = config.begin(); it != config.end(); it++) { - details[it.key()] = it.value(); - } - } + return createNewAccount(profile::Type::JAMI, details); +} - QString accountId = ConfigurationManager::instance().addAccount(details); - return accountId; +QString +AccountModel::createDeviceImportAccount() +{ + // auto details = ConfigurationManager::instance().getAccountTemplate("RING"); + MapStringString details; + using namespace libjami::Account; + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::ARCHIVE_URL] = "jami-auth"; + return createNewAccount(profile::Type::JAMI, details); } void diff --git a/src/libclient/api/account.h b/src/libclient/api/account.h index c2504c17..43092143 100644 --- a/src/libclient/api/account.h +++ b/src/libclient/api/account.h @@ -188,9 +188,65 @@ struct ConfProperties_t MapStringString toDetails() const; }; -// Possible account export status -enum class ExportOnRingStatus { SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2, INVALID }; -Q_ENUM_NS(ExportOnRingStatus) +// The following statuses are used to track the status of +// device-linking and account-import +enum class DeviceAuthState { + INIT = 0, + TOKEN_AVAILABLE = 1, + CONNECTING = 2, + AUTHENTICATING = 3, + IN_PROGRESS = 4, + DONE = 5 +}; +Q_ENUM_NS(DeviceAuthState) + +enum class DeviceLinkError { + WRONG_PASSWORD, // auth_error, invalid_credentials + NETWORK, // network + TIMEOUT, // timeout + STATE, // state + CANCELED, // canceled + UNKNOWN // fallback +}; + +Q_ENUM_NS(DeviceLinkError) + +inline DeviceLinkError +mapLinkDeviceError(const std::string& error) +{ + if (error == "auth_error" || error == "invalid_credentials") + return DeviceLinkError::WRONG_PASSWORD; + if (error == "network") + return DeviceLinkError::NETWORK; + if (error == "timeout") + return DeviceLinkError::TIMEOUT; + if (error == "state") + return DeviceLinkError::STATE; + if (error == "canceled") + return DeviceLinkError::CANCELED; + return DeviceLinkError::UNKNOWN; +} + +inline QString +getLinkDeviceString(DeviceLinkError error) +{ + switch (error) { + case DeviceLinkError::WRONG_PASSWORD: + return QObject::tr( + "An authentication error occurred.\nPlease check credentials and try again."); + case DeviceLinkError::NETWORK: + return QObject::tr("A network error occurred.\nPlease verify your connection."); + case DeviceLinkError::TIMEOUT: + return QObject::tr("The operation has timed out.\nPlease try again."); + case DeviceLinkError::STATE: + return QObject::tr("An error occurred while exporting the account.\nPlease try again."); + case DeviceLinkError::CANCELED: + return QObject::tr("Operation was canceled."); + case DeviceLinkError::UNKNOWN: + default: + return QObject::tr("An unexpected error occurred.\nPlease try again."); + } +} enum class RegisterNameStatus { SUCCESS = 0, diff --git a/src/libclient/api/accountmodel.h b/src/libclient/api/accountmodel.h index 25573cdf..6b55a85c 100644 --- a/src/libclient/api/accountmodel.h +++ b/src/libclient/api/accountmodel.h @@ -112,13 +112,37 @@ public: Q_INVOKABLE bool exportToFile(const QString& accountId, const QString& path, const QString& password = {}) const; + /** - * Call exportOnRing from the daemon + * Provide authentication for an account * @param accountId - * @param password + * @param credentialsFromUser + * @return if the authentication is successful + */ + Q_INVOKABLE bool provideAccountAuthentication(const QString& accountId, + const QString& credentialsFromUser) const; + + /** + * @param accountId + * @param uri * @return if the export is initialized */ - Q_INVOKABLE bool exportOnRing(const QString& accountId, const QString& password) const; + Q_INVOKABLE int32_t addDevice(const QString& accountId, const QString& token) const; + + /** + * Confirm the addition of a device + * @param accountId + * @param operationId + */ + Q_INVOKABLE bool confirmAddDevice(const QString& accountId, uint32_t operationId) const; + + /** + * Cancel the addition of a device + * @param accountId + * @param operationId + */ + Q_INVOKABLE bool cancelAddDevice(const QString& accountId, uint32_t operationId) const; + /** * Call removeAccount from the daemon * @param accountId to remove @@ -141,7 +165,7 @@ public: * @param avatar * @throws out_of_range exception if account is not found */ - void setAvatar(const QString& accountId, const QString& avatar, bool save = true, int flag =0); + void setAvatar(const QString& accountId, const QString& avatar, bool save = true, int flag = 0); /** * Change the alias of an account * @param accountId @@ -159,18 +183,7 @@ public: Q_INVOKABLE bool registerName(const QString& accountId, const QString& password, const QString& username); - /** - * Connect to JAMS to retrieve the account - * @param username - * @param password - * @param serverUri - * @param config - * @return the account id - */ - static QString connectToAccountManager(const QString& username, - const QString& password, - const QString& serverUri, - const MapStringString& config = MapStringString()); + /** * Create a new Ring or SIP account * @param type determine if the new account will be a Ring account or a SIP one @@ -184,12 +197,32 @@ public: * @return the created account */ static QString createNewAccount(profile::Type type, + const MapStringString& config = MapStringString(), const QString& displayName = "", const QString& archivePath = "", const QString& password = "", const QString& pin = "", - const QString& uri = "", - const MapStringString& config = MapStringString()); + const QString& uri = ""); + + /** + * Connect to JAMS to retrieve the account + * @param username + * @param password + * @param serverUri + * @param config + * @return the account id + */ + static QString connectToAccountManager(const QString& username, + const QString& password, + const QString& serverUri, + const MapStringString& config = MapStringString()); + + /** + * Create a simple ephemeral account from a device import + * @return the account id of the created account + */ + static QString createDeviceImportAccount(); + /** * Set an account to the first position */ @@ -296,14 +329,24 @@ Q_SIGNALS: void profileUpdated(const QString& accountID); /** - * Connect this signal to know when an account is exported on the DHT + * Device authentication state has changed * @param accountID - * @param status - * @param pin + * @param state + * @param details map */ - void exportOnRingEnded(const QString& accountID, - account::ExportOnRingStatus status, - const QString& pin); + void deviceAuthStateChanged(const QString& accountID, int state, const MapStringString& details); + + /** + * Add device state has changed + * @param accountID + * @param operationId + * @param state + * @param details map + */ + void addDeviceStateChanged(const QString& accountID, + uint32_t operationId, + int state, + const MapStringString& details); /** * Name registration has ended diff --git a/src/libclient/callbackshandler.cpp b/src/libclient/callbackshandler.cpp index 3452f281..308e234e 100644 --- a/src/libclient/callbackshandler.cpp +++ b/src/libclient/callbackshandler.cpp @@ -242,9 +242,9 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent) Qt::QueuedConnection); connect(&ConfigurationManager::instance(), - &ConfigurationManagerInterface::exportOnRingEnded, + &ConfigurationManagerInterface::deviceAuthStateChanged, this, - &CallbacksHandler::slotExportOnRingEnded, + &CallbacksHandler::slotDeviceAuthStateChanged, Qt::QueuedConnection); connect(&ConfigurationManager::instance(), @@ -546,7 +546,9 @@ CallbacksHandler::slotIncomingMessage(const QString& accountId, } void -CallbacksHandler::slotConferenceCreated(const QString& accountId, const QString& convId, const QString& callId) +CallbacksHandler::slotConferenceCreated(const QString& accountId, + const QString& convId, + const QString& callId) { Q_EMIT conferenceCreated(accountId, convId, callId); } @@ -678,9 +680,11 @@ CallbacksHandler::slotDeviceRevokationEnded(const QString& accountId, } void -CallbacksHandler::slotExportOnRingEnded(const QString& accountId, int status, const QString& pin) +CallbacksHandler::slotDeviceAuthStateChanged(const QString& accountId, + int state, + const MapStringString& details) { - Q_EMIT exportOnRingEnded(accountId, status, pin); + Q_EMIT deviceAuthStateChanged(accountId, state, details); } void diff --git a/src/libclient/callbackshandler.h b/src/libclient/callbackshandler.h index 9f0f5478..428cf54c 100644 --- a/src/libclient/callbackshandler.h +++ b/src/libclient/callbackshandler.h @@ -171,7 +171,9 @@ Q_SIGNALS: * Connect this signal to know when a new conference is created * @param callId of the conference */ - void conferenceCreated(const QString& accountId, const QString& conversationId, const QString& callId); + void conferenceCreated(const QString& accountId, + const QString& conversationId, + const QString& callId); void conferenceChanged(const QString& accountId, const QString& confId, const QString& state); /** * Connect this signal to know when a conference is removed @@ -235,12 +237,12 @@ Q_SIGNALS: const QString& userPhoto); /** - * Emit exportOnRingEnded + * Device authentication state has changed * @param accountId - * @param status SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2 - * @param pin + * @param state + * @param details map */ - void exportOnRingEnded(const QString& accountId, int status, const QString& pin); + void deviceAuthStateChanged(const QString& accountId, int state, const MapStringString& details); /** * Name registration has ended @@ -504,7 +506,9 @@ private Q_SLOTS: * @param callId of the conference * @param conversationId of the conference */ - void slotConferenceCreated(const QString& accountId, const QString& conversationId, const QString& callId); + void slotConferenceCreated(const QString& accountId, + const QString& conversationId, + const QString& callId); /** * Emit conferenceRemove * @param accountId @@ -574,12 +578,14 @@ private Q_SLOTS: const QString& userPhoto); /** - * Emit exportOnRingEnded + * Device authentication state has changed * @param accountId - * @param status SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2 - * @param pin + * @param state + * @param details map */ - void slotExportOnRingEnded(const QString& accountId, int status, const QString& pin); + void slotDeviceAuthStateChanged(const QString& accountId, + int state, + const MapStringString& details); /** * Emit nameRegistrationEnded diff --git a/src/libclient/namedirectory.cpp b/src/libclient/namedirectory.cpp index 15fb0d58..3068a202 100644 --- a/src/libclient/namedirectory.cpp +++ b/src/libclient/namedirectory.cpp @@ -36,11 +36,6 @@ NameDirectoryPrivate::NameDirectoryPrivate(NameDirectory* q) this, &NameDirectoryPrivate::slotRegisteredNameFound, Qt::QueuedConnection); - connect(&configurationManager, - &ConfigurationManagerInterface::exportOnRingEnded, - this, - &NameDirectoryPrivate::slotExportOnRingEnded, - Qt::QueuedConnection); } NameDirectory::NameDirectory() @@ -100,16 +95,6 @@ NameDirectoryPrivate::slotRegisteredNameFound(const QString& accountId, requestedName); } -// Export account has ended with pin generated -void -NameDirectoryPrivate::slotExportOnRingEnded(const QString& accountId, int status, const QString& pin) -{ - LC_DBG << "Export on ring ended for account: " << accountId << "status: " << status - << "PIN: " << pin; - - Q_EMIT q_ptr->exportOnRingEnded(static_cast(status), pin); -} - // Lookup a name bool NameDirectory::lookupName(const QString& accountId, diff --git a/src/libclient/namedirectory.h b/src/libclient/namedirectory.h index 008dc197..9211c80f 100644 --- a/src/libclient/namedirectory.h +++ b/src/libclient/namedirectory.h @@ -40,15 +40,16 @@ public: enum class LookupStatus { SUCCESS = 0, INVALID_NAME = 1, NOT_FOUND = 2, ERROR = 3 }; Q_ENUM(LookupStatus) - enum class ExportOnRingStatus { SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2, INVALID }; - Q_ENUM(ExportOnRingStatus) - // Singleton static NameDirectory& instance(); // Lookup - Q_INVOKABLE bool lookupName(const QString& accountId, const QString& name, const QString& nameServiceURL = "") const; - Q_INVOKABLE bool lookupAddress(const QString& accountId, const QString& address, const QString& nameServiceURL = "") const; + Q_INVOKABLE bool lookupName(const QString& accountId, + const QString& name, + const QString& nameServiceURL = "") const; + Q_INVOKABLE bool lookupAddress(const QString& accountId, + const QString& address, + const QString& nameServiceURL = "") const; private: // Constructors & Destructors @@ -70,8 +71,5 @@ Q_SIGNALS: const QString& address, const QString& registeredName, const QString& requestedName); - - // Export account has ended with pin generated - void exportOnRingEnded(NameDirectory::ExportOnRingStatus status, const QString& pin); }; Q_DECLARE_METATYPE(NameDirectory*) diff --git a/src/libclient/private/namedirectory_p.h b/src/libclient/private/namedirectory_p.h index 89e49cd2..ebc52043 100644 --- a/src/libclient/private/namedirectory_p.h +++ b/src/libclient/private/namedirectory_p.h @@ -37,5 +37,4 @@ public Q_SLOTS: int status, const QString& address, const QString& registeredName); - void slotExportOnRingEnded(const QString& accountId, int status, const QString& pin); }; diff --git a/src/libclient/qtwrapper/configurationmanager_wrap.h b/src/libclient/qtwrapper/configurationmanager_wrap.h index 467fed9e..b34918f8 100644 --- a/src/libclient/qtwrapper/configurationmanager_wrap.h +++ b/src/libclient/qtwrapper/configurationmanager_wrap.h @@ -150,11 +150,23 @@ public: QString(displayName.c_str()), QString(userPhoto.c_str())); }), - exportable_callback( - [this](const std::string& accountId, int status, const std::string& pin) { - Q_EMIT this->exportOnRingEnded(QString(accountId.c_str()), - status, - QString(pin.c_str())); + exportable_callback( + [this](const std::string& accountId, + uint32_t operationId, + int state, + const std::map& details) { + Q_EMIT this->addDeviceStateChanged(QString(accountId.c_str()), + operationId, + state, + convertMap(details)); + }), + exportable_callback( + [this](const std::string& accountId, + int state, + const std::map& details) { + Q_EMIT this->deviceAuthStateChanged(QString(accountId.c_str()), + state, + convertMap(details)); }), exportable_callback( [this](const std::string& accountId, int status, const std::string& name) { @@ -431,9 +443,28 @@ public Q_SLOTS: // METHODS path.toStdString()); } - bool exportOnRing(const QString& accountId, const QString& password) + bool provideAccountAuthentication(const QString& accountId, + const QString& credentialsFromUser, + const QString scheme = "password") { - return libjami::exportOnRing(accountId.toStdString(), password.toStdString()); + return libjami::provideAccountAuthentication(accountId.toStdString(), + credentialsFromUser.toStdString(), + scheme.toStdString()); + } + + int32_t addDevice(const QString& accountId, const QString& token) + { + return libjami::addDevice(accountId.toStdString(), token.toStdString()); + } + + bool confirmAddDevice(const QString& accountId, uint32_t operationId) + { + return libjami::confirmAddDevice(accountId.toStdString(), operationId); + } + + bool cancelAddDevice(const QString& accountId, uint32_t operationId) + { + return libjami::cancelAddDevice(accountId.toStdString(), operationId); } bool exportToFile(const QString& accountId, @@ -498,8 +529,7 @@ public Q_SLOTS: // METHODS displayName.toStdString(), avatarPath.toStdString(), fileType.toStdString(), - flag - ); + flag); } QStringList getAccountList() @@ -1197,7 +1227,11 @@ Q_SIGNALS: // SIGNALS const QString& certId, const QString& status); void knownDevicesChanged(const QString& accountId, const MapStringString& devices); - void exportOnRingEnded(const QString& accountId, int status, const QString& pin); + void addDeviceStateChanged(const QString& accountId, + uint32_t operationId, + int state, + const MapStringString& details); + void deviceAuthStateChanged(const QString& accountId, int state, const MapStringString& details); void incomingAccountMessage(const QString& accountId, const QString& from, const QString msgId,