1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-07-01 22:25:26 +02:00

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
This commit is contained in:
Kateryna Kostiuk 2025-02-24 09:46:30 -05:00 committed by Andreas Traczyk
parent 82c876c0fa
commit 33da15daba
25 changed files with 820 additions and 347 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ doc/Doxyfile
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
*.code-workspace
### VisualStudioCode Patch ### ### VisualStudioCode Patch ###
# Ignore all local history of files # Ignore all local history of files

2
daemon

@ -1 +1 @@
Subproject commit 597cde8d30814b5078e2ac8c8a0953dd471ec716 Subproject commit 86d3bb664489077107e68b838e419f4cd6459859

View file

@ -22,8 +22,11 @@
#include "systemtray.h" #include "systemtray.h"
#include "lrcinstance.h" #include "lrcinstance.h"
#include "accountlistmodel.h" #include "accountlistmodel.h"
#include "wizardviewstepmodel.h"
#include "global.h"
#include "api/account.h"
#include <QtConcurrent/QtConcurrent> #include <QThreadPool>
AccountAdapter::AccountAdapter(AppSettingsManager* settingsManager, AccountAdapter::AccountAdapter(AppSettingsManager* settingsManager,
SystemTray* systemTray, SystemTray* systemTray,
@ -111,7 +114,10 @@ AccountAdapter::createJamiAccount(const QVariantMap& settings)
&lrcInstance_->accountModel(), &lrcInstance_->accountModel(),
&lrc::api::AccountModel::accountAdded, &lrc::api::AccountModel::accountAdded,
[this, registeredName, settings](const QString& accountId) { [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(), Utils::oneShotConnect(&lrcInstance_->accountModel(),
&lrc::api::AccountModel::accountDetailsChanged, &lrc::api::AccountModel::accountDetailsChanged,
[this](const QString& accountId) { [this](const QString& accountId) {
@ -159,8 +165,9 @@ AccountAdapter::createJamiAccount(const QVariantMap& settings)
connectFailure(); connectFailure();
auto futureResult = QtConcurrent::run([this, settings] { QThreadPool::globalInstance()->start([this, settings] {
lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::JAMI, lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::JAMI,
{},
settings["alias"].toString(), settings["alias"].toString(),
settings["archivePath"].toString(), settings["archivePath"].toString(),
settings["password"].toString(), settings["password"].toString(),
@ -206,14 +213,14 @@ AccountAdapter::createSIPAccount(const QVariantMap& settings)
connectFailure(); connectFailure();
auto futureResult = QtConcurrent::run([this, settings] { QThreadPool::globalInstance()->start([this, settings] {
lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::SIP, lrcInstance_->accountModel().createNewAccount(lrc::api::profile::Type::SIP,
{},
settings["alias"].toString(), settings["alias"].toString(),
settings["archivePath"].toString(), settings["archivePath"].toString(),
"", "",
"", "",
settings["username"].toString(), settings["username"].toString());
{});
}); });
} }
@ -250,7 +257,7 @@ AccountAdapter::createJAMSAccount(const QVariantMap& settings)
connectFailure(); connectFailure();
auto futureResult = QtConcurrent::run([this, settings] { QThreadPool::globalInstance()->start([this, settings] {
lrcInstance_->accountModel().connectToAccountManager(settings["username"].toString(), lrcInstance_->accountModel().connectToAccountManager(settings["username"].toString(),
settings["password"].toString(), settings["password"].toString(),
settings["manager"].toString()); settings["manager"].toString());
@ -293,7 +300,7 @@ AccountAdapter::setCurrAccDisplayName(const QString& text)
void void
AccountAdapter::setCurrentAccountAvatarFile(const QString& source) AccountAdapter::setCurrentAccountAvatarFile(const QString& source)
{ {
auto futureResult = QtConcurrent::run([this, source]() { QThreadPool::globalInstance()->start([this, source]() {
QPixmap image; QPixmap image;
if (!image.load(source)) { if (!image.load(source)) {
qWarning() << "Not a valid image file"; qWarning() << "Not a valid image file";
@ -308,7 +315,7 @@ AccountAdapter::setCurrentAccountAvatarFile(const QString& source)
void void
AccountAdapter::setCurrentAccountAvatarBase64(const QString& data) AccountAdapter::setCurrentAccountAvatarBase64(const QString& data)
{ {
auto futureResult = QtConcurrent::run([this, data]() { QThreadPool::globalInstance()->start([this, data]() {
auto accountId = lrcInstance_->get_currentAccountId(); auto accountId = lrcInstance_->get_currentAccountId();
lrcInstance_->accountModel().setAvatar(accountId, data, true, 1); lrcInstance_->accountModel().setAvatar(accountId, data, true, 1);
}); });
@ -339,9 +346,73 @@ AccountAdapter::exportToFile(const QString& accountId,
void void
AccountAdapter::setArchivePasswordAsync(const QString& accountID, const QString& password) 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); auto config = lrcInstance_->accountModel().getAccountConfig(accountID);
config.archivePassword = password; config.archivePassword = password;
lrcInstance_->accountModel().setAccountConfig(accountID, config); lrcInstance_->accountModel().setAccountConfig(accountID, config);
}); });
} }
void
AccountAdapter::startImportAccount()
{
auto wizardModel = qApp->property("WizardViewStepModel").value<WizardViewStepModel*>();
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<WizardViewStepModel*>();
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<WizardViewStepModel*>();
wizardModel->set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT);
wizardModel->set_deviceLinkDetails({});
// Remove the account if it was created
lrcInstance_->accountModel().removeAccount(importAccountId_);
importAccountId_.clear();
}

View file

@ -81,6 +81,13 @@ public:
const bool& state); const bool& state);
Q_INVOKABLE QStringList getDefaultModerators(const QString& accountId); 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: Q_SIGNALS:
// Trigger other components to reconnect account related signals. // Trigger other components to reconnect account related signals.
void accountStatusChanged(QString accountId); void accountStatusChanged(QString accountId);
@ -98,6 +105,9 @@ private:
QMetaObject::Connection registeredNameSavedConnection_; QMetaObject::Connection registeredNameSavedConnection_;
// The account ID of the last used import account.
QString importAccountId_;
AppSettingsManager* settingsManager_; AppSettingsManager* settingsManager_;
SystemTray* systemTray_; SystemTray* systemTray_;
}; };

View file

@ -22,6 +22,7 @@
#include "lrcinstance.h" #include "lrcinstance.h"
#include <QImage> #include <QImage>
#include <QRegularExpression>
class AsyncAvatarImageResponseRunnable : public AsyncImageResponseRunnable class AsyncAvatarImageResponseRunnable : public AsyncImageResponseRunnable
{ {
@ -69,6 +70,16 @@ public:
image = Utils::accountPhoto(lrcInstance_, imageId, requestedSize_); image = Utils::accountPhoto(lrcInstance_, imageId, requestedSize_);
} else if (type == "contact") { } else if (type == "contact") {
image = Utils::contactPhoto(lrcInstance_, imageId, requestedSize_); 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 { } else {
qWarning() << Q_FUNC_INFO << "Missing valid prefix in the image url"; qWarning() << Q_FUNC_INFO << "Missing valid prefix in the image url";
return; return;

View file

@ -28,7 +28,8 @@ Item {
enum Mode { enum Mode {
Account, Account,
Contact, Contact,
Conversation Conversation,
TemporaryAccount
} }
property int mode: Avatar.Mode.Account property int mode: Avatar.Mode.Account
property alias sourceSize: image.sourceSize property alias sourceSize: image.sourceSize
@ -45,6 +46,8 @@ Item {
return 'contact'; return 'contact';
case Avatar.Mode.Conversation: case Avatar.Mode.Conversation:
return 'conversation'; return 'conversation';
case Avatar.Mode.TemporaryAccount:
return 'temporaryAccount';
} }
} }

View file

@ -70,6 +70,21 @@ Item {
property string transferThisCall: qsTr("Transfer this call") property string transferThisCall: qsTr("Transfer this call")
property string transferTo: qsTr("Transfer to") 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 // AccountMigrationDialog
property string authenticationRequired: qsTr("Authentication required") property string authenticationRequired: qsTr("Authentication required")
property string migrationReason: qsTr("Your session has expired or been revoked on this device. Please enter your password.") property string migrationReason: qsTr("Your session has expired or been revoked on this device. Please enter your password.")
@ -579,19 +594,8 @@ Item {
// ImportFromDevicePage // ImportFromDevicePage
property string importButton: qsTr("Import") property string importButton: qsTr("Import")
property string pin: qsTr("Enter the PIN code") 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 // 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 close: qsTr("Close")
property string enterAccountPassword: qsTr("Enter account password") property string enterAccountPassword: qsTr("Enter account password")
property string enterPasswordPinCode: qsTr("This account is password encrypted, enter the password to generate a PIN code.") property string enterPasswordPinCode: qsTr("This account is password encrypted, enter the password to generate a PIN code.")

View file

@ -179,6 +179,12 @@ registerTypes(QQmlEngine* engine,
QQmlEngine::setObjectOwnership(pluginStoreListModel, QQmlEngine::CppOwnership); QQmlEngine::setObjectOwnership(pluginStoreListModel, QQmlEngine::CppOwnership);
REG_QML_SINGLETON<PluginStoreListModel>(REG_MODEL, "PluginStoreListModel", CREATE(pluginStoreListModel)); REG_QML_SINGLETON<PluginStoreListModel>(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<WizardViewStepModel>(REG_MODEL, "WizardViewStepModel", CREATE(wizardViewStepModel));
// Register app-level objects that are used by QML created objects. // 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 // These MUST be set prior to loading the initial QML file, in order to
// be available to the QML adapter class factory creation methods. // 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, TipsModel);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices); QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate); QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
QML_REGISTERSINGLETON_TYPE(NS_MODELS, WizardViewStepModel);
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, ImageDownloader); QML_REGISTERSINGLETON_TYPE(NS_HELPERS, ImageDownloader);
// TODO: remove these // TODO: remove these
@ -263,12 +268,12 @@ registerTypes(QQmlEngine* engine,
// Enums // Enums
QML_REGISTERUNCREATABLE(NS_ENUMS, Settings) QML_REGISTERUNCREATABLE(NS_ENUMS, Settings)
QML_REGISTERUNCREATABLE(NS_ENUMS, NetworkManager) QML_REGISTERUNCREATABLE(NS_ENUMS, NetworkManager)
QML_REGISTERUNCREATABLE(NS_ENUMS, WizardViewStepModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceItemListModel) QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceItemListModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, ModeratorListModel) QML_REGISTERUNCREATABLE(NS_ENUMS, ModeratorListModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, VideoInputDeviceModel) QML_REGISTERUNCREATABLE(NS_ENUMS, VideoInputDeviceModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatResolutionModel) QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatResolutionModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatFpsModel) QML_REGISTERUNCREATABLE(NS_ENUMS, VideoFormatFpsModel)
QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceAuthStateEnum)
engine->addImageProvider(QLatin1String("qrImage"), new QrImageProvider(lrcInstance)); engine->addImageProvider(QLatin1String("qrImage"), new QrImageProvider(lrcInstance));
engine->addImageProvider(QLatin1String("avatarimage"), new AvatarImageProvider(lrcInstance)); engine->addImageProvider(QLatin1String("avatarimage"), new AvatarImageProvider(lrcInstance));

View file

@ -18,7 +18,6 @@
#pragma once #pragma once
#include "quickimageproviderbase.h" #include "quickimageproviderbase.h"
#include "accountlistmodel.h"
#include <QPair> #include <QPair>
#include <QString> #include <QString>

View file

@ -42,7 +42,6 @@ BaseModalDialog {
} }
stackedWidget.currentIndex = exportingSpinnerPage.pageIndex; stackedWidget.currentIndex = exportingSpinnerPage.pageIndex;
spinnerMovie.playing = true; spinnerMovie.playing = true;
timerForExport.restart();
} }
function setExportPage(status, pin) { function setExportPage(status, pin) {
@ -69,25 +68,6 @@ BaseModalDialog {
stackedWidget.height = exportingLayout.implicitHeight; 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: { onVisibleChanged: {
if (visible) { if (visible) {
if (CurrentAccount.hasArchivePassword) { if (CurrentAccount.hasArchivePassword) {

View file

@ -165,8 +165,7 @@ Utils::CreateStartupLink(const std::wstring& wstrAppName)
#endif #endif
if (desktopPath.isEmpty() || !(QFile::exists(desktopPath))) { if (desktopPath.isEmpty() || !(QFile::exists(desktopPath))) {
qDebug() << "Error while attempting to locate .desktop file at" qDebug() << "Error while attempting to locate .desktop file at" << desktopPath;
<< desktopPath;
return false; return false;
} }
@ -193,8 +192,7 @@ Utils::CreateStartupLink(const std::wstring& wstrAppName)
if (QDir().mkdir(autoStartDir)) { if (QDir().mkdir(autoStartDir)) {
qDebug() << "Created autostart directory:" << autoStartDir; qDebug() << "Created autostart directory:" << autoStartDir;
} else { } else {
qWarning() << "Error while creating autostart directory:" qWarning() << "Error while creating autostart directory:" << autoStartDir;
<< autoStartDir;
return false; return false;
} }
} }
@ -283,7 +281,8 @@ Utils::CheckStartupLink(const std::wstring& wstrAppName)
#else #else
Q_UNUSED(wstrAppName) Q_UNUSED(wstrAppName)
return ( return (
!QStandardPaths::locate(QStandardPaths::ConfigLocation, "autostart/net.jami.Jami.desktop").isEmpty()); !QStandardPaths::locate(QStandardPaths::ConfigLocation, "autostart/net.jami.Jami.desktop")
.isEmpty());
#endif #endif
} }
@ -616,14 +615,16 @@ Utils::getProjectCredits()
return {}; return {};
} }
QTextStream in(&projectCreditsFile); QTextStream in(&projectCreditsFile);
return in.readAll().arg( return in.readAll().arg(QObject::tr("We would like to thank our contributors, whose efforts "
QObject::tr("We would like to thank our contributors, whose efforts over many years have made this software what it is."), "over many years have made this software what it is."),
QObject::tr("Developers"), QObject::tr("Developers"),
QObject::tr("Media"), QObject::tr("Media"),
QObject::tr("Community Management"), QObject::tr("Community Management"),
QObject::tr("Special thanks to"), 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.") 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 inline QString
@ -951,3 +952,13 @@ Utils::getTempSwarmAvatarPath()
return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QDir::separator() return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QDir::separator()
+ "tmpSwarmImage"; + "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;
}

View file

@ -120,4 +120,7 @@ QString generateUid();
QString humanFileSize(qint64 fileSize); QString humanFileSize(qint64 fileSize);
QString getDebugFilePath(); QString getDebugFilePath();
// Convert a MapStringString to a QVariantMap
QVariantMap mapStringStringToVariantMap(const MapStringString& map);
} // namespace Utils } // namespace Utils

View file

@ -56,9 +56,11 @@ BaseView {
case WizardViewStepModel.AccountCreationOption.CreateJamiAccount: case WizardViewStepModel.AccountCreationOption.CreateJamiAccount:
case WizardViewStepModel.AccountCreationOption.CreateRendezVous: case WizardViewStepModel.AccountCreationOption.CreateRendezVous:
case WizardViewStepModel.AccountCreationOption.ImportFromBackup: case WizardViewStepModel.AccountCreationOption.ImportFromBackup:
case WizardViewStepModel.AccountCreationOption.ImportFromDevice:
AccountAdapter.createJamiAccount(WizardViewStepModel.accountCreationInfo); AccountAdapter.createJamiAccount(WizardViewStepModel.accountCreationInfo);
break; break;
case WizardViewStepModel.AccountCreationOption.ImportFromDevice:
AccountAdapter.startImportAccount();
break;
case WizardViewStepModel.AccountCreationOption.ConnectToAccountManager: case WizardViewStepModel.AccountCreationOption.ConnectToAccountManager:
AccountAdapter.createJAMSAccount(WizardViewStepModel.accountCreationInfo); AccountAdapter.createJAMSAccount(WizardViewStepModel.accountCreationInfo);
break; break;

View file

@ -17,9 +17,13 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Dialogs
import net.jami.Adapters 1.1
import net.jami.Models 1.1 import net.jami.Models 1.1
import net.jami.Constants 1.1 import net.jami.Constants 1.1
import net.jami.Enums 1.1
import "../../commoncomponents" import "../../commoncomponents"
import "../../mainview/components"
Rectangle { Rectangle {
id: root id: root
@ -27,30 +31,99 @@ Rectangle {
property string errorText: "" property string errorText: ""
property int preferredHeight: importFromDevicePageColumnLayout.implicitHeight + 2 * JamiTheme.preferredMarginSize 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() { property string jamiId: ""
clearAllTextFields();
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() { function clearAllTextFields() {
connectBtn.spinnerTriggered = false; errorText = "";
} }
function errorOccurred(errorMessage) { function errorOccurred(errorMessage) {
errorText = 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 { Connections {
target: WizardViewStepModel target: WizardViewStepModel
function onMainStepChanged() { function onMainStepChanged() {
if (WizardViewStepModel.mainStep === WizardViewStepModel.MainSteps.AccountCreation && WizardViewStepModel.accountCreationOption === WizardViewStepModel.AccountCreationOption.ImportFromDevice) { if (WizardViewStepModel.mainStep === WizardViewStepModel.MainSteps.DeviceAuthorization) {
clearAllTextFields(); clearAllTextFields();
root.showThisPage(); 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 color: JamiTheme.secondaryBackgroundColor
@ -65,184 +138,276 @@ Rectangle {
width: Math.max(508, root.width - 100) width: Math.max(508, root.width - 100)
Text { Text {
text: JamiStrings.importFromAnotherAccount
text: JamiStrings.importAccountFromAnotherDevice
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
Layout.topMargin: JamiTheme.preferredMarginSize Layout.topMargin: JamiTheme.preferredMarginSize
Layout.preferredWidth: Math.min(360, root.width - JamiTheme.preferredMarginSize * 2) Layout.preferredWidth: Math.min(360, root.width - JamiTheme.preferredMarginSize * 2)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: JamiTheme.textColor
color: JamiTheme.textColor
font.pixelSize: JamiTheme.wizardViewTitleFontPixelSize font.pixelSize: JamiTheme.wizardViewTitleFontPixelSize
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
Text { 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.alignment: Qt.AlignHCenter
Layout.topMargin: JamiTheme.wizardViewBlocMarginSize Layout.maximumWidth: parent.width
Layout.preferredWidth: Math.min(step1.width * 2 + spacing, root.width - JamiTheme.preferredMarginSize * 2) horizontalAlignment: Text.AlignHCenter
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
InfoBox { lineHeight: JamiTheme.wizardViewTextLineHeight
id: step1 text: {
icoSource: JamiResources.settings_24dp_svg switch (WizardViewStepModel.deviceAuthState) {
title: JamiStrings.importStep1 case DeviceAuthStateEnum.INIT:
description: JamiStrings.importStep1Desc return JamiStrings.waitingForToken;
icoColor: JamiTheme.buttonTintedBlue 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
} }
InfoBox { // Confirmation form
id: step2 ColumnLayout {
icoSource: JamiResources.person_24dp_svg Layout.alignment: Qt.AlignHCenter
title: JamiStrings.importStep2 Layout.maximumWidth: Math.min(parent.width - 40, 400)
description: JamiStrings.importStep2Desc visible: WizardViewStepModel.deviceAuthState === DeviceAuthStateEnum.AUTHENTICATING
icoColor: JamiTheme.buttonTintedBlue spacing: JamiTheme.wizardViewPageLayoutSpacing
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
} }
InfoBox { // Account Widget (avatar + username + ID)
id: step3 Rectangle {
icoSource: JamiResources.finger_select_svg id: accountContainer
title: JamiStrings.importStep3 Layout.alignment: Qt.AlignHCenter
description: JamiStrings.importStep3Desc implicitWidth: accountLayout.implicitWidth + 40
icoColor: JamiTheme.buttonTintedBlue implicitHeight: accountLayout.implicitHeight + 40
radius: 8
color: JamiTheme.primaryBackgroundColor
border.width: 1
border.color: JamiTheme.tabbarBorderColor
RowLayout {
id: accountLayout
anchors {
centerIn: parent
}
spacing: 20
Avatar {
id: accountAvatar
showPresenceIndicator: false
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 48
Layout.preferredHeight: 48
mode: Avatar.Mode.TemporaryAccount
imageId: name.text || jamiId
} }
InfoBox { ColumnLayout {
id: step4 Layout.fillWidth: true
icoSource: JamiResources.time_clock_svg Layout.fillHeight: true
title: JamiStrings.importStep4 Layout.alignment: Qt.AlignVCenter
description: JamiStrings.importStep4Desc spacing: 4
icoColor: JamiTheme.buttonTintedBlue
Text {
id: name
visible: text !== undefined && text !== ""
Connections {
id: registeredNameFoundConnection
target: NameDirectory
enabled: jamiId.length > 0
function onRegisteredNameFound(status, address, registeredName, requestedName) {
if (address === jamiId && status === NameDirectory.LookupStatus.SUCCESS) {
name.text = registeredName;
}
}
}
}
Text {
id: userId
text: jamiId
}
}
} }
} }
ModalTextEdit { // Password
id: pinFromDevice PasswordTextEdit {
id: passwordField
objectName: "pinFromDevice" Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.topMargin: 10
Layout.bottomMargin: 10
visible: requiresPassword()
placeholderText: JamiStrings.enterPassword
echoMode: TextInput.Password
Layout.alignment: Qt.AlignCenter onAccepted: confirmButton.clicked()
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 { Text {
id: passwordErrorField
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: JamiTheme.wizardViewBlocMarginSize Layout.maximumWidth: parent.width - 40
visible: isPasswordWrong()
color: JamiTheme.textColor text: JamiStrings.authenticationError
wrapMode: Text.WordWrap font.pointSize: JamiTheme.tinyFontSize
text: JamiStrings.importPasswordDesc
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
font.weight: Font.Medium
}
PasswordTextEdit {
id: passwordFromDevice
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
}
objectName: "importFromDevicePageConnectBtn"
Layout.alignment: Qt.AlignCenter
Layout.topMargin: JamiTheme.wizardViewBlocMarginSize
Layout.bottomMargin: errorLabel.visible ? 0 : JamiTheme.wizardViewPageBackButtonMargins
preferredWidth: textSize.width + 2 * JamiTheme.buttontextWizzardPadding + 1
primary: true
spinnerTriggeredtext: JamiStrings.generatingAccount
normalText: JamiStrings.importButton
enabled: pinFromDevice.dynamicText.length !== 0 && !spinnerTriggered
KeyNavigation.tab: backButton
KeyNavigation.up: passwordFromDevice
KeyNavigation.down: backButton
onClicked: {
spinnerTriggered = true;
WizardViewStepModel.accountCreationInfo = JamiQmlUtils.setUpAccountCreationInputPara({
"archivePin": pinFromDevice.dynamicText,
"password": passwordFromDevice.dynamicText
});
WizardViewStepModel.nextStep();
}
}
Label {
id: errorLabel
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: JamiTheme.wizardViewPageBackButtonMargins
visible: errorText.length !== 0
text: errorText
font.pixelSize: JamiTheme.textEditError
color: JamiTheme.redColor 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 : "");
}
}
} }
} }
BackButton { // Show busy indicator when waiting for token
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
visible: isLoadingState()
Layout.preferredWidth: 50
Layout.preferredHeight: 50
running: visible
}
// 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
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
sourceComponent: Image {
width: qrLoader.Layout.preferredWidth
height: qrLoader.Layout.preferredHeight
smooth: false
fillMode: Image.PreserveAspectFit
source: "image://qrImage/raw_" + tokenUri
}
}
}
// 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();
}
}
}
}
// Back button
JamiPushButton {
id: backButton id: backButton
QWKSetParentHitTestVisible {
}
objectName: "importFromDevicePageBackButton" objectName: "importFromDevicePageBackButton"
@ -250,12 +415,18 @@ Rectangle {
anchors.top: parent.top anchors.top: parent.top
anchors.margins: JamiTheme.wizardViewPageBackButtonMargins anchors.margins: JamiTheme.wizardViewPageBackButtonMargins
visible: !connectBtn.spinnerTriggered preferredSize: 36
imageContainerWidth: 20
source: JamiResources.ic_arrow_back_24dp_svg
KeyNavigation.tab: pinFromDevice visible: WizardViewStepModel.deviceAuthState !== DeviceAuthStateEnum.IN_PROGRESS
KeyNavigation.up: connectBtn.enabled ? connectBtn : passwordFromDevice
KeyNavigation.down: pinFromDevice
onClicked: WizardViewStepModel.previousStep() onClicked: {
if (requiresConfirmationBeforeClosing()) {
confirmCloseDialog.open();
} else {
WizardViewStepModel.previousStep();
}
}
} }
} }

View file

@ -19,6 +19,7 @@
#include "appsettingsmanager.h" #include "appsettingsmanager.h"
#include "lrcinstance.h" #include "lrcinstance.h"
#include "global.h"
#include "api/accountmodel.h" #include "api/accountmodel.h"
@ -46,17 +47,31 @@ WizardViewStepModel::WizardViewStepModel(LRCInstance* lrcInstance,
Q_EMIT accountIsReady(accountId); 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<lrc::api::account::DeviceAuthState>(state));
});
} }
void void
WizardViewStepModel::startAccountCreationFlow(AccountCreationOption accountCreationOption) WizardViewStepModel::startAccountCreationFlow(AccountCreationOption accountCreationOption)
{ {
using namespace lrc::api::account;
set_accountCreationOption(accountCreationOption); set_accountCreationOption(accountCreationOption);
if (accountCreationOption == AccountCreationOption::CreateJamiAccount if (accountCreationOption == AccountCreationOption::ImportFromDevice) {
|| accountCreationOption == AccountCreationOption::CreateRendezVous) set_mainStep(MainSteps::DeviceAuthorization);
Q_EMIT createAccountRequested(accountCreationOption);
} else if (accountCreationOption == AccountCreationOption::CreateJamiAccount
|| accountCreationOption == AccountCreationOption::CreateRendezVous) {
set_mainStep(MainSteps::NameRegistration); set_mainStep(MainSteps::NameRegistration);
else } else {
set_mainStep(MainSteps::AccountCreation); set_mainStep(MainSteps::AccountCreation);
}
} }
void void
@ -80,6 +95,10 @@ WizardViewStepModel::previousStep()
reset(); reset();
break; break;
} }
case MainSteps::DeviceAuthorization: {
reset();
break;
}
} }
} }
@ -88,4 +107,6 @@ WizardViewStepModel::reset()
{ {
set_accountCreationOption(AccountCreationOption::None); set_accountCreationOption(AccountCreationOption::None);
set_mainStep(MainSteps::Initial); set_mainStep(MainSteps::Initial);
set_deviceAuthState(lrc::api::account::DeviceAuthState::INIT);
set_deviceLinkDetails({});
} }

View file

@ -18,6 +18,7 @@
#pragma once #pragma once
#include "qtutils.h" #include "qtutils.h"
#include "api/account.h" // Include for DeviceAuthState
#include <QObject> #include <QObject>
#include <QVariant> #include <QVariant>
@ -29,6 +30,21 @@ class AccountAdapter;
class LRCInstance; class LRCInstance;
class AppSettingsManager; class AppSettingsManager;
class DeviceAuthStateEnum : public QObject
{
Q_OBJECT
public:
enum State {
INIT = static_cast<int>(lrc::api::account::DeviceAuthState::INIT),
TOKEN_AVAILABLE = static_cast<int>(lrc::api::account::DeviceAuthState::TOKEN_AVAILABLE),
CONNECTING = static_cast<int>(lrc::api::account::DeviceAuthState::CONNECTING),
AUTHENTICATING = static_cast<int>(lrc::api::account::DeviceAuthState::AUTHENTICATING),
IN_PROGRESS = static_cast<int>(lrc::api::account::DeviceAuthState::IN_PROGRESS),
DONE = static_cast<int>(lrc::api::account::DeviceAuthState::DONE)
};
Q_ENUM(State)
};
class WizardViewStepModel : public QObject class WizardViewStepModel : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -40,6 +56,7 @@ public:
Initial, // Initial welcome step. Initial, // Initial welcome step.
AccountCreation, // General account creation step. AccountCreation, // General account creation step.
NameRegistration, // Name registration step : CreateJamiAccount, CreateRendezVous NameRegistration, // Name registration step : CreateJamiAccount, CreateRendezVous
DeviceAuthorization // Add new step for device authorization.
}; };
Q_ENUM(MainSteps) Q_ENUM(MainSteps)
@ -57,6 +74,8 @@ public:
QML_PROPERTY(MainSteps, mainStep) QML_PROPERTY(MainSteps, mainStep)
QML_PROPERTY(AccountCreationOption, accountCreationOption) QML_PROPERTY(AccountCreationOption, accountCreationOption)
QML_PROPERTY(QVariantMap, accountCreationInfo) QML_PROPERTY(QVariantMap, accountCreationInfo)
QML_PROPERTY(lrc::api::account::DeviceAuthState, deviceAuthState)
QML_PROPERTY(QVariantMap, deviceLinkDetails)
public: public:
static WizardViewStepModel* create(QQmlEngine*, QJSEngine*) static WizardViewStepModel* create(QQmlEngine*, QJSEngine*)

View file

@ -117,12 +117,14 @@ public Q_SLOTS:
void slotAccountStatusChanged(const QString& accountID, const api::account::Status status); void slotAccountStatusChanged(const QString& accountID, const api::account::Status status);
/** /**
* Emit exportOnRingEnded. * Emit deviceAuthStateChanged.
* @param accountId * @param accountId
* @param status * @param state
* @param pin * @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 * @param accountId
@ -282,11 +284,12 @@ AccountModel::setAlias(const QString& accountId, const QString& alias, bool save
accountInfo.profileInfo.alias = alias; accountInfo.profileInfo.alias = alias;
if (save) if (save)
ConfigurationManager::instance().updateProfile(accountId, ConfigurationManager::instance()
.updateProfile(accountId,
alias, alias,
"", "",
"", "",
5);// flag out of range to avoid updating avatar 5); // flag out of range to avoid updating avatar
Q_EMIT profileUpdated(accountId); Q_EMIT profileUpdated(accountId);
} }
@ -323,9 +326,30 @@ AccountModel::exportToFile(const QString& accountId,
} }
bool 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 void
@ -403,9 +427,9 @@ AccountModelPimpl::AccountModelPimpl(AccountModel& linked,
this, this,
&AccountModelPimpl::slotVolatileAccountDetailsChanged); &AccountModelPimpl::slotVolatileAccountDetailsChanged);
connect(&callbacksHandler, connect(&callbacksHandler,
&CallbacksHandler::exportOnRingEnded, &CallbacksHandler::deviceAuthStateChanged,
this, &linked,
&AccountModelPimpl::slotExportOnRingEnded); &AccountModel::deviceAuthStateChanged);
connect(&callbacksHandler, connect(&callbacksHandler,
&CallbacksHandler::nameRegistrationEnded, &CallbacksHandler::nameRegistrationEnded,
this, this,
@ -594,23 +618,13 @@ AccountModelPimpl::slotVolatileAccountDetailsChanged(const QString& accountId,
} }
void 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; // implement business logic here
switch (status) { // can be bypassed with a signal to signal
case 0: Q_EMIT linked.deviceAuthStateChanged(accountId, state, details);
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);
} }
void void
@ -673,7 +687,11 @@ AccountModelPimpl::slotRegisteredNameFound(const QString& accountId,
default: default:
break; break;
} }
Q_EMIT linked.registeredNameFound(accountId, requestedName, convertedStatus, address, registeredName); Q_EMIT linked.registeredNameFound(accountId,
requestedName,
convertedStatus,
address,
registeredName);
} }
void void
@ -1041,17 +1059,32 @@ account::ConfProperties_t::toDetails() const
QString QString
AccountModel::createNewAccount(profile::Type type, AccountModel::createNewAccount(profile::Type type,
const MapStringString& config,
const QString& displayName, const QString& displayName,
const QString& archivePath, const QString& archivePath,
const QString& password, const QString& password,
const QString& pin, const QString& pin,
const QString& uri, const QString& uri)
const MapStringString& config)
{ {
// Get the template for the account type to prefill the details
MapStringString details = type == profile::Type::SIP MapStringString details = type == profile::Type::SIP
? ConfigurationManager::instance().getAccountTemplate("SIP") ? ConfigurationManager::instance().getAccountTemplate("SIP")
: ConfigurationManager::instance().getAccountTemplate("RING"); : ConfigurationManager::instance().getAccountTemplate("RING");
// 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; 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::TYPE] = type == profile::Type::SIP ? "SIP" : "RING";
details[ConfProperties::DISPLAYNAME] = displayName; details[ConfProperties::DISPLAYNAME] = displayName;
details[ConfProperties::ALIAS] = displayName; details[ConfProperties::ALIAS] = displayName;
@ -1059,14 +1092,14 @@ AccountModel::createNewAccount(profile::Type type,
details[ConfProperties::ARCHIVE_PASSWORD] = password; details[ConfProperties::ARCHIVE_PASSWORD] = password;
details[ConfProperties::ARCHIVE_PIN] = pin; details[ConfProperties::ARCHIVE_PIN] = pin;
details[ConfProperties::ARCHIVE_PATH] = archivePath; details[ConfProperties::ARCHIVE_PATH] = archivePath;
if (type == profile::Type::SIP)
// Override the username with the provided URI if it's a SIP account
if (type == profile::Type::SIP) {
details[ConfProperties::USERNAME] = uri; details[ConfProperties::USERNAME] = uri;
if (!config.isEmpty()) {
for (MapStringString::const_iterator it = config.begin(); it != config.end(); it++) {
details[it.key()] = it.value();
} }
} }
// Actually add the account and return the account ID
QString accountId = ConfigurationManager::instance().addAccount(details); QString accountId = ConfigurationManager::instance().addAccount(details);
return accountId; return accountId;
} }
@ -1077,20 +1110,24 @@ AccountModel::connectToAccountManager(const QString& username,
const QString& serverUri, const QString& serverUri,
const MapStringString& config) const MapStringString& config)
{ {
MapStringString details = ConfigurationManager::instance().getAccountTemplate("RING"); MapStringString details = config;
using namespace libjami::Account; using namespace libjami::Account;
details[ConfProperties::TYPE] = "RING"; details[ConfProperties::TYPE] = "RING";
details[ConfProperties::MANAGER_URI] = serverUri; details[ConfProperties::MANAGER_URI] = serverUri;
details[ConfProperties::MANAGER_USERNAME] = username; details[ConfProperties::MANAGER_USERNAME] = username;
details[ConfProperties::ARCHIVE_PASSWORD] = password; details[ConfProperties::ARCHIVE_PASSWORD] = password;
if (!config.isEmpty()) { return createNewAccount(profile::Type::JAMI, details);
for (MapStringString::const_iterator it = config.begin(); it != config.end(); it++) { }
details[it.key()] = it.value();
}
}
QString accountId = ConfigurationManager::instance().addAccount(details); QString
return accountId; 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 void

View file

@ -188,9 +188,65 @@ struct ConfProperties_t
MapStringString toDetails() const; MapStringString toDetails() const;
}; };
// Possible account export status // The following statuses are used to track the status of
enum class ExportOnRingStatus { SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2, INVALID }; // device-linking and account-import
Q_ENUM_NS(ExportOnRingStatus) 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 { enum class RegisterNameStatus {
SUCCESS = 0, SUCCESS = 0,

View file

@ -112,13 +112,37 @@ public:
Q_INVOKABLE bool exportToFile(const QString& accountId, Q_INVOKABLE bool exportToFile(const QString& accountId,
const QString& path, const QString& path,
const QString& password = {}) const; const QString& password = {}) const;
/** /**
* Call exportOnRing from the daemon * Provide authentication for an account
* @param accountId * @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 * @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 * Call removeAccount from the daemon
* @param accountId to remove * @param accountId to remove
@ -141,7 +165,7 @@ public:
* @param avatar * @param avatar
* @throws out_of_range exception if account is not found * @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 * Change the alias of an account
* @param accountId * @param accountId
@ -159,18 +183,7 @@ public:
Q_INVOKABLE bool registerName(const QString& accountId, Q_INVOKABLE bool registerName(const QString& accountId,
const QString& password, const QString& password,
const QString& username); 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 * Create a new Ring or SIP account
* @param type determine if the new account will be a Ring account or a SIP one * @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 * @return the created account
*/ */
static QString createNewAccount(profile::Type type, static QString createNewAccount(profile::Type type,
const MapStringString& config = MapStringString(),
const QString& displayName = "", const QString& displayName = "",
const QString& archivePath = "", const QString& archivePath = "",
const QString& password = "", const QString& password = "",
const QString& pin = "", const QString& pin = "",
const QString& uri = "", 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()); 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 * Set an account to the first position
*/ */
@ -296,14 +329,24 @@ Q_SIGNALS:
void profileUpdated(const QString& accountID); 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 accountID
* @param status * @param state
* @param pin * @param details map
*/ */
void exportOnRingEnded(const QString& accountID, void deviceAuthStateChanged(const QString& accountID, int state, const MapStringString& details);
account::ExportOnRingStatus status,
const QString& pin); /**
* 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 * Name registration has ended

View file

@ -242,9 +242,9 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent)
Qt::QueuedConnection); Qt::QueuedConnection);
connect(&ConfigurationManager::instance(), connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::exportOnRingEnded, &ConfigurationManagerInterface::deviceAuthStateChanged,
this, this,
&CallbacksHandler::slotExportOnRingEnded, &CallbacksHandler::slotDeviceAuthStateChanged,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(&ConfigurationManager::instance(), connect(&ConfigurationManager::instance(),
@ -546,7 +546,9 @@ CallbacksHandler::slotIncomingMessage(const QString& accountId,
} }
void 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); Q_EMIT conferenceCreated(accountId, convId, callId);
} }
@ -678,9 +680,11 @@ CallbacksHandler::slotDeviceRevokationEnded(const QString& accountId,
} }
void 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 void

View file

@ -171,7 +171,9 @@ Q_SIGNALS:
* Connect this signal to know when a new conference is created * Connect this signal to know when a new conference is created
* @param callId of the conference * @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); void conferenceChanged(const QString& accountId, const QString& confId, const QString& state);
/** /**
* Connect this signal to know when a conference is removed * Connect this signal to know when a conference is removed
@ -235,12 +237,12 @@ Q_SIGNALS:
const QString& userPhoto); const QString& userPhoto);
/** /**
* Emit exportOnRingEnded * Device authentication state has changed
* @param accountId * @param accountId
* @param status SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2 * @param state
* @param pin * @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 * Name registration has ended
@ -504,7 +506,9 @@ private Q_SLOTS:
* @param callId of the conference * @param callId of the conference
* @param conversationId 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 * Emit conferenceRemove
* @param accountId * @param accountId
@ -574,12 +578,14 @@ private Q_SLOTS:
const QString& userPhoto); const QString& userPhoto);
/** /**
* Emit exportOnRingEnded * Device authentication state has changed
* @param accountId * @param accountId
* @param status SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2 * @param state
* @param pin * @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 * Emit nameRegistrationEnded

View file

@ -36,11 +36,6 @@ NameDirectoryPrivate::NameDirectoryPrivate(NameDirectory* q)
this, this,
&NameDirectoryPrivate::slotRegisteredNameFound, &NameDirectoryPrivate::slotRegisteredNameFound,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(&configurationManager,
&ConfigurationManagerInterface::exportOnRingEnded,
this,
&NameDirectoryPrivate::slotExportOnRingEnded,
Qt::QueuedConnection);
} }
NameDirectory::NameDirectory() NameDirectory::NameDirectory()
@ -100,16 +95,6 @@ NameDirectoryPrivate::slotRegisteredNameFound(const QString& accountId,
requestedName); 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<NameDirectory::ExportOnRingStatus>(status), pin);
}
// Lookup a name // Lookup a name
bool bool
NameDirectory::lookupName(const QString& accountId, NameDirectory::lookupName(const QString& accountId,

View file

@ -40,15 +40,16 @@ public:
enum class LookupStatus { SUCCESS = 0, INVALID_NAME = 1, NOT_FOUND = 2, ERROR = 3 }; enum class LookupStatus { SUCCESS = 0, INVALID_NAME = 1, NOT_FOUND = 2, ERROR = 3 };
Q_ENUM(LookupStatus) Q_ENUM(LookupStatus)
enum class ExportOnRingStatus { SUCCESS = 0, WRONG_PASSWORD = 1, NETWORK_ERROR = 2, INVALID };
Q_ENUM(ExportOnRingStatus)
// Singleton // Singleton
static NameDirectory& instance(); static NameDirectory& instance();
// Lookup // Lookup
Q_INVOKABLE bool lookupName(const QString& accountId, const QString& name, const QString& nameServiceURL = "") const; Q_INVOKABLE bool lookupName(const QString& accountId,
Q_INVOKABLE bool lookupAddress(const QString& accountId, const QString& address, const QString& nameServiceURL = "") const; const QString& name,
const QString& nameServiceURL = "") const;
Q_INVOKABLE bool lookupAddress(const QString& accountId,
const QString& address,
const QString& nameServiceURL = "") const;
private: private:
// Constructors & Destructors // Constructors & Destructors
@ -70,8 +71,5 @@ Q_SIGNALS:
const QString& address, const QString& address,
const QString& registeredName, const QString& registeredName,
const QString& requestedName); const QString& requestedName);
// Export account has ended with pin generated
void exportOnRingEnded(NameDirectory::ExportOnRingStatus status, const QString& pin);
}; };
Q_DECLARE_METATYPE(NameDirectory*) Q_DECLARE_METATYPE(NameDirectory*)

View file

@ -37,5 +37,4 @@ public Q_SLOTS:
int status, int status,
const QString& address, const QString& address,
const QString& registeredName); const QString& registeredName);
void slotExportOnRingEnded(const QString& accountId, int status, const QString& pin);
}; };

View file

@ -150,11 +150,23 @@ public:
QString(displayName.c_str()), QString(displayName.c_str()),
QString(userPhoto.c_str())); QString(userPhoto.c_str()));
}), }),
exportable_callback<ConfigurationSignal::ExportOnRingEnded>( exportable_callback<ConfigurationSignal::AddDeviceStateChanged>(
[this](const std::string& accountId, int status, const std::string& pin) { [this](const std::string& accountId,
Q_EMIT this->exportOnRingEnded(QString(accountId.c_str()), uint32_t operationId,
status, int state,
QString(pin.c_str())); const std::map<std::string, std::string>& details) {
Q_EMIT this->addDeviceStateChanged(QString(accountId.c_str()),
operationId,
state,
convertMap(details));
}),
exportable_callback<ConfigurationSignal::DeviceAuthStateChanged>(
[this](const std::string& accountId,
int state,
const std::map<std::string, std::string>& details) {
Q_EMIT this->deviceAuthStateChanged(QString(accountId.c_str()),
state,
convertMap(details));
}), }),
exportable_callback<ConfigurationSignal::NameRegistrationEnded>( exportable_callback<ConfigurationSignal::NameRegistrationEnded>(
[this](const std::string& accountId, int status, const std::string& name) { [this](const std::string& accountId, int status, const std::string& name) {
@ -431,9 +443,28 @@ public Q_SLOTS: // METHODS
path.toStdString()); 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, bool exportToFile(const QString& accountId,
@ -498,8 +529,7 @@ public Q_SLOTS: // METHODS
displayName.toStdString(), displayName.toStdString(),
avatarPath.toStdString(), avatarPath.toStdString(),
fileType.toStdString(), fileType.toStdString(),
flag flag);
);
} }
QStringList getAccountList() QStringList getAccountList()
@ -1197,7 +1227,11 @@ Q_SIGNALS: // SIGNALS
const QString& certId, const QString& certId,
const QString& status); const QString& status);
void knownDevicesChanged(const QString& accountId, const MapStringString& devices); 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, void incomingAccountMessage(const QString& accountId,
const QString& from, const QString& from,
const QString msgId, const QString msgId,