581 lines
16 KiB
C++
581 lines
16 KiB
C++
/*
|
||
This file is part of Telegram Desktop,
|
||
the official desktop application for the Telegram messaging service.
|
||
|
||
For license and copyright information please follow this link:
|
||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||
*/
|
||
#include "settings/cloud_password/settings_cloud_password_input.h"
|
||
|
||
#include "api/api_cloud_password.h"
|
||
#include "base/qt_signal_producer.h"
|
||
#include "base/timer.h"
|
||
#include "base/unixtime.h"
|
||
#include "core/core_cloud_password.h"
|
||
#include "lang/lang_keys.h"
|
||
#include "lottie/lottie_icon.h"
|
||
#include "settings/cloud_password/settings_cloud_password_common.h"
|
||
#include "settings/cloud_password/settings_cloud_password_email_confirm.h"
|
||
#include "settings/cloud_password/settings_cloud_password_hint.h"
|
||
#include "settings/cloud_password/settings_cloud_password_manage.h"
|
||
#include "ui/boxes/confirm_box.h"
|
||
#include "ui/text/format_values.h"
|
||
#include "ui/widgets/buttons.h"
|
||
#include "ui/widgets/fields/password_input.h"
|
||
#include "ui/widgets/labels.h"
|
||
#include "ui/wrap/vertical_layout.h"
|
||
#include "window/window_session_controller.h"
|
||
#include "styles/style_boxes.h"
|
||
#include "styles/style_layers.h"
|
||
#include "styles/style_settings.h"
|
||
|
||
/*
|
||
Available actions for follow states.
|
||
|
||
CreatePassword:
|
||
– Continue to CreateHint.
|
||
– Back to Start.
|
||
|
||
ChangePassword:
|
||
– Continue to ChangeHint.
|
||
– Back to Manage.
|
||
|
||
CheckPassword:
|
||
– Continue to Manage.
|
||
– Recover to EmailConfirm.
|
||
– Reset and wait (+ Cancel reset).
|
||
– Reset now and Back to Settings.
|
||
– Back to Settings.
|
||
|
||
RecreateResetPassword:
|
||
– Continue to RecreateResetHint.
|
||
– Clear password and Back to Settings.
|
||
– Back to Settings.
|
||
*/
|
||
|
||
namespace Settings {
|
||
namespace CloudPassword {
|
||
namespace {
|
||
|
||
struct Icon {
|
||
not_null<Lottie::Icon*> icon;
|
||
Fn<void()> update;
|
||
};
|
||
|
||
Icon CreateInteractiveLottieIcon(
|
||
not_null<Ui::VerticalLayout*> container,
|
||
Lottie::IconDescriptor &&descriptor,
|
||
style::margins padding) {
|
||
auto object = object_ptr<Ui::RpWidget>(container);
|
||
const auto raw = object.data();
|
||
|
||
const auto width = descriptor.sizeOverride.width();
|
||
raw->resize(QRect(
|
||
QPoint(),
|
||
descriptor.sizeOverride).marginsAdded(padding).size());
|
||
|
||
auto owned = Lottie::MakeIcon(std::move(descriptor));
|
||
const auto icon = owned.get();
|
||
|
||
raw->lifetime().add([kept = std::move(owned)]{});
|
||
|
||
raw->paintRequest(
|
||
) | rpl::start_with_next([=] {
|
||
auto p = QPainter(raw);
|
||
const auto left = (raw->width() - width) / 2;
|
||
icon->paint(p, left, padding.top());
|
||
}, raw->lifetime());
|
||
|
||
container->add(std::move(object));
|
||
return { .icon = icon, .update = [=] { raw->update(); } };
|
||
}
|
||
|
||
[[nodiscard]] not_null<Ui::LinkButton*> AddLinkButton(
|
||
not_null<Ui::VerticalLayout*> content,
|
||
not_null<Ui::PasswordInput*> input) {
|
||
const auto button = Ui::CreateChild<Ui::LinkButton>(
|
||
content.get(),
|
||
QString());
|
||
|
||
rpl::merge(
|
||
content->geometryValue(),
|
||
input->geometryValue()
|
||
) | rpl::start_with_next([=] {
|
||
const auto topLeft = input->mapTo(content, input->pos());
|
||
button->moveToLeft(
|
||
input->pos().x(),
|
||
topLeft.y() + input->height() + st::passcodeTextLine);
|
||
}, button->lifetime());
|
||
return button;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
class Input : public TypedAbstractStep<Input> {
|
||
public:
|
||
using TypedAbstractStep::TypedAbstractStep;
|
||
|
||
[[nodiscard]] rpl::producer<QString> title() override;
|
||
void setupContent();
|
||
|
||
protected:
|
||
[[nodiscard]] rpl::producer<std::vector<Type>> removeTypes() override;
|
||
|
||
private:
|
||
void setupRecoverButton(
|
||
not_null<Ui::VerticalLayout*> container,
|
||
not_null<Ui::LinkButton*> button,
|
||
not_null<Ui::FlatLabel*> info,
|
||
Fn<void()> recoverCallback);
|
||
|
||
rpl::variable<std::vector<Type>> _removesFromStack;
|
||
rpl::lifetime _requestLifetime;
|
||
|
||
};
|
||
|
||
rpl::producer<std::vector<Type>> Input::removeTypes() {
|
||
return _removesFromStack.value();
|
||
}
|
||
|
||
rpl::producer<QString> Input::title() {
|
||
return tr::lng_settings_cloud_password_password_title();
|
||
}
|
||
|
||
void Input::setupContent() {
|
||
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
||
auto currentStepData = stepData();
|
||
const auto currentStepDataPassword = base::take(currentStepData.password);
|
||
const auto currentStepProcessRecover = base::take(
|
||
currentStepData.processRecover);
|
||
setStepData(currentStepData);
|
||
|
||
const auto currentState = cloudPassword().stateCurrent();
|
||
const auto hasPassword = !currentStepProcessRecover.setNewPassword
|
||
&& (currentState ? currentState->hasPassword : false);
|
||
const auto isCheck = currentStepData.currentPassword.isEmpty()
|
||
&& hasPassword
|
||
&& !currentStepProcessRecover.setNewPassword;
|
||
|
||
if (currentStepProcessRecover.setNewPassword) {
|
||
_removesFromStack = std::vector<Type>{
|
||
CloudPasswordEmailConfirmId()
|
||
};
|
||
}
|
||
|
||
const auto icon = CreateInteractiveLottieIcon(
|
||
content,
|
||
{
|
||
.name = u"cloud_password/password_input"_q,
|
||
.sizeOverride = {
|
||
st::settingsCloudPasswordIconSize,
|
||
st::settingsCloudPasswordIconSize
|
||
},
|
||
},
|
||
st::settingLocalPasscodeIconPadding);
|
||
|
||
SetupHeader(
|
||
content,
|
||
QString(),
|
||
rpl::never<>(),
|
||
isCheck
|
||
? tr::lng_settings_cloud_password_check_subtitle()
|
||
: hasPassword
|
||
? tr::lng_settings_cloud_password_manage_password_change()
|
||
: tr::lng_settings_cloud_password_password_subtitle(),
|
||
tr::lng_cloud_password_about());
|
||
|
||
AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);
|
||
|
||
const auto newInput = AddPasswordField(
|
||
content,
|
||
isCheck
|
||
? tr::lng_cloud_password_enter_old()
|
||
: tr::lng_cloud_password_enter_new(),
|
||
currentStepDataPassword);
|
||
const auto reenterInput = isCheck
|
||
? (Ui::PasswordInput*)(nullptr)
|
||
: AddPasswordField(
|
||
content,
|
||
tr::lng_cloud_password_confirm_new(),
|
||
currentStepDataPassword).get();
|
||
const auto error = AddError(content, newInput);
|
||
if (reenterInput) {
|
||
QObject::connect(reenterInput, &Ui::MaskedInputField::changed, [=] {
|
||
error->hide();
|
||
});
|
||
}
|
||
|
||
if (isCheck) {
|
||
AddSkipInsteadOfField(content);
|
||
|
||
const auto hint = currentState ? currentState->hint : QString();
|
||
const auto hintInfo = Ui::CreateChild<Ui::FlatLabel>(
|
||
error->parentWidget(),
|
||
tr::lng_signin_hint(tr::now, lt_password_hint, hint),
|
||
st::defaultFlatLabel);
|
||
hintInfo->setVisible(!hint.isEmpty());
|
||
error->geometryValue(
|
||
) | rpl::start_with_next([=](const QRect &r) {
|
||
hintInfo->setGeometry(r);
|
||
}, hintInfo->lifetime());
|
||
error->shownValue(
|
||
) | rpl::start_with_next([=](bool shown) {
|
||
if (shown) {
|
||
hintInfo->hide();
|
||
} else {
|
||
hintInfo->setVisible(!hint.isEmpty());
|
||
}
|
||
}, hintInfo->lifetime());
|
||
|
||
auto recoverCallback = [=] {
|
||
if (_requestLifetime) {
|
||
return;
|
||
}
|
||
const auto state = cloudPassword().stateCurrent();
|
||
if (!state) {
|
||
return;
|
||
}
|
||
if (state->hasRecovery) {
|
||
_requestLifetime = cloudPassword().requestPasswordRecovery(
|
||
) | rpl::start_with_next_error([=](const QString &pattern) {
|
||
_requestLifetime.destroy();
|
||
|
||
auto data = stepData();
|
||
data.processRecover = currentStepProcessRecover;
|
||
data.processRecover.emailPattern = pattern;
|
||
setStepData(std::move(data));
|
||
showOther(CloudPasswordEmailConfirmId());
|
||
}, [=](const QString &type) {
|
||
_requestLifetime.destroy();
|
||
|
||
error->show();
|
||
if (MTP::IsFloodError(type)) {
|
||
error->setText(tr::lng_flood_error(tr::now));
|
||
} else {
|
||
error->setText(Lang::Hard::ServerError());
|
||
}
|
||
});
|
||
} else {
|
||
const auto callback = [=](Fn<void()> close) {
|
||
if (_requestLifetime) {
|
||
return;
|
||
}
|
||
close();
|
||
_requestLifetime = cloudPassword().resetPassword(
|
||
) | rpl::start_with_error_done([=](const QString &type) {
|
||
_requestLifetime.destroy();
|
||
}, [=] {
|
||
_requestLifetime.destroy();
|
||
});
|
||
};
|
||
controller()->show(Ui::MakeConfirmBox({
|
||
.text = tr::lng_cloud_password_reset_no_email(),
|
||
.confirmed = callback,
|
||
.confirmText = tr::lng_cloud_password_reset_ok(),
|
||
.cancelText = tr::lng_cancel(),
|
||
.confirmStyle = &st::attentionBoxButton,
|
||
}));
|
||
}
|
||
};
|
||
|
||
const auto recover = AddLinkButton(content, newInput);
|
||
const auto resetInfo = Ui::CreateChild<Ui::FlatLabel>(
|
||
content,
|
||
QString(),
|
||
st::boxDividerLabel);
|
||
recover->geometryValue(
|
||
) | rpl::start_with_next([=](const QRect &r) {
|
||
resetInfo->moveToLeft(r.x(), r.y() + st::passcodeTextLine);
|
||
}, resetInfo->lifetime());
|
||
|
||
setupRecoverButton(
|
||
content,
|
||
recover,
|
||
resetInfo,
|
||
std::move(recoverCallback));
|
||
} else if (currentStepProcessRecover.setNewPassword && reenterInput) {
|
||
const auto skip = AddLinkButton(content, reenterInput);
|
||
skip->setText(tr::lng_settings_auto_night_disable(tr::now));
|
||
skip->setClickedCallback([=] {
|
||
if (_requestLifetime) {
|
||
return;
|
||
}
|
||
_requestLifetime = cloudPassword().recoverPassword(
|
||
currentStepProcessRecover.checkedCode,
|
||
QString(),
|
||
QString()
|
||
) | rpl::start_with_error_done([=](const QString &type) {
|
||
_requestLifetime.destroy();
|
||
|
||
error->show();
|
||
if (MTP::IsFloodError(type)) {
|
||
error->setText(tr::lng_flood_error(tr::now));
|
||
} else {
|
||
error->setText(Lang::Hard::ServerError());
|
||
}
|
||
}, [=] {
|
||
_requestLifetime.destroy();
|
||
|
||
controller()->show(
|
||
Ui::MakeInformBox(tr::lng_cloud_password_removed()));
|
||
setStepData(StepData());
|
||
showBack();
|
||
});
|
||
});
|
||
AddSkip(content);
|
||
}
|
||
|
||
if (!newInput->text().isEmpty()) {
|
||
icon.icon->jumpTo(icon.icon->framesCount() / 2, icon.update);
|
||
}
|
||
|
||
const auto checkPassword = [=](const QString &pass) {
|
||
if (_requestLifetime) {
|
||
return;
|
||
}
|
||
_requestLifetime = cloudPassword().check(
|
||
pass
|
||
) | rpl::start_with_error_done([=](const QString &type) {
|
||
_requestLifetime.destroy();
|
||
|
||
newInput->setFocus();
|
||
newInput->showError();
|
||
newInput->selectAll();
|
||
error->show();
|
||
if (MTP::IsFloodError(type)) {
|
||
error->setText(tr::lng_flood_error(tr::now));
|
||
} else if (type == u"PASSWORD_HASH_INVALID"_q
|
||
|| type == u"SRP_PASSWORD_CHANGED"_q) {
|
||
error->setText(tr::lng_cloud_password_wrong(tr::now));
|
||
} else {
|
||
error->setText(Lang::Hard::ServerError());
|
||
}
|
||
}, [=] {
|
||
_requestLifetime.destroy();
|
||
|
||
if (const auto state = cloudPassword().stateCurrent()) {
|
||
if (state->pendingResetDate > 0) {
|
||
auto lifetime = rpl::lifetime();
|
||
lifetime = cloudPassword().cancelResetPassword(
|
||
) | rpl::start_with_next([] {});
|
||
}
|
||
}
|
||
|
||
auto data = stepData();
|
||
data.currentPassword = pass;
|
||
setStepData(std::move(data));
|
||
showOther(CloudPasswordManageId());
|
||
});
|
||
};
|
||
|
||
const auto button = AddDoneButton(
|
||
content,
|
||
isCheck ? tr::lng_passcode_check_button() : tr::lng_continue());
|
||
button->setClickedCallback([=] {
|
||
const auto newText = newInput->text();
|
||
const auto reenterText = isCheck ? QString() : reenterInput->text();
|
||
if (newText.isEmpty()) {
|
||
newInput->setFocus();
|
||
newInput->showError();
|
||
} else if (reenterInput && reenterText.isEmpty()) {
|
||
reenterInput->setFocus();
|
||
reenterInput->showError();
|
||
} else if (reenterInput && (newText != reenterText)) {
|
||
reenterInput->setFocus();
|
||
reenterInput->showError();
|
||
reenterInput->selectAll();
|
||
error->show();
|
||
error->setText(tr::lng_cloud_password_differ(tr::now));
|
||
} else if (isCheck) {
|
||
checkPassword(newText);
|
||
} else {
|
||
auto data = stepData();
|
||
data.processRecover = currentStepProcessRecover;
|
||
data.password = newText;
|
||
setStepData(std::move(data));
|
||
showOther(CloudPasswordHintId());
|
||
}
|
||
});
|
||
|
||
base::qt_signal_producer(
|
||
newInput.get(),
|
||
&QLineEdit::textChanged // Covers Undo.
|
||
) | rpl::map([=] {
|
||
return newInput->text().isEmpty();
|
||
}) | rpl::distinct_until_changed(
|
||
) | rpl::start_with_next([=](bool empty) {
|
||
const auto from = icon.icon->frameIndex();
|
||
const auto to = empty ? 0 : (icon.icon->framesCount() / 2 - 1);
|
||
icon.icon->animate(icon.update, from, to);
|
||
}, content->lifetime());
|
||
|
||
const auto submit = [=] {
|
||
if (!reenterInput || reenterInput->hasFocus()) {
|
||
button->clicked({}, Qt::LeftButton);
|
||
} else {
|
||
reenterInput->setFocus();
|
||
}
|
||
};
|
||
QObject::connect(newInput, &Ui::MaskedInputField::submitted, submit);
|
||
if (reenterInput) {
|
||
using namespace Ui;
|
||
QObject::connect(reenterInput, &MaskedInputField::submitted, submit);
|
||
}
|
||
|
||
setFocusCallback([=] {
|
||
if (isCheck || newInput->text().isEmpty()) {
|
||
newInput->setFocus();
|
||
} else if (reenterInput->text().isEmpty()) {
|
||
reenterInput->setFocus();
|
||
} else {
|
||
newInput->setFocus();
|
||
}
|
||
});
|
||
|
||
Ui::ResizeFitChild(this, content);
|
||
}
|
||
|
||
void Input::setupRecoverButton(
|
||
not_null<Ui::VerticalLayout*> container,
|
||
not_null<Ui::LinkButton*> button,
|
||
not_null<Ui::FlatLabel*> info,
|
||
Fn<void()> recoverCallback) {
|
||
|
||
struct Status {
|
||
enum class SuggestAction {
|
||
Recover,
|
||
Reset,
|
||
CancelReset,
|
||
};
|
||
SuggestAction suggest = SuggestAction::Recover;
|
||
TimeId left = 0;
|
||
};
|
||
|
||
struct State {
|
||
base::Timer timer;
|
||
rpl::variable<Status> status;
|
||
};
|
||
|
||
const auto state = container->lifetime().make_state<State>();
|
||
|
||
const auto updateStatus = [=] {
|
||
const auto passwordState = cloudPassword().stateCurrent();
|
||
const auto date = passwordState ? passwordState->pendingResetDate : 0;
|
||
const auto left = (date - base::unixtime::now());
|
||
state->status = Status{
|
||
.suggest = ((left > 0)
|
||
? Status::SuggestAction::CancelReset
|
||
: date
|
||
? Status::SuggestAction::Reset
|
||
: Status::SuggestAction::Recover),
|
||
.left = left,
|
||
};
|
||
};
|
||
state->timer.setCallback(updateStatus);
|
||
updateStatus();
|
||
|
||
state->status.value(
|
||
) | rpl::start_with_next([=](const Status &status) {
|
||
switch (status.suggest) {
|
||
case Status::SuggestAction::Recover: {
|
||
info->setText(QString());
|
||
button->setText(tr::lng_signin_recover(tr::now));
|
||
} break;
|
||
case Status::SuggestAction::Reset: {
|
||
info->setText(QString());
|
||
button->setText(tr::lng_cloud_password_reset_ready(tr::now));
|
||
} break;
|
||
case Status::SuggestAction::CancelReset: {
|
||
info->setText(
|
||
tr::lng_settings_cloud_password_reset_in(
|
||
tr::now,
|
||
lt_duration,
|
||
Ui::FormatResetCloudPasswordIn(status.left)));
|
||
button->setText(
|
||
tr::lng_cloud_password_reset_cancel_title(tr::now));
|
||
} break;
|
||
}
|
||
}, container->lifetime());
|
||
|
||
cloudPassword().state(
|
||
) | rpl::start_with_next([=](const Core::CloudPasswordState &passState) {
|
||
updateStatus();
|
||
state->timer.cancel();
|
||
if (passState.pendingResetDate) {
|
||
state->timer.callEach(999);
|
||
}
|
||
}, container->lifetime());
|
||
|
||
button->setClickedCallback([=] {
|
||
const auto passState = cloudPassword().stateCurrent();
|
||
if (_requestLifetime || !passState) {
|
||
return;
|
||
}
|
||
updateStatus();
|
||
const auto suggest = state->status.current().suggest;
|
||
if (suggest == Status::SuggestAction::Recover) {
|
||
recoverCallback();
|
||
} else if (suggest == Status::SuggestAction::CancelReset) {
|
||
const auto cancel = [=](Fn<void()> close) {
|
||
if (_requestLifetime) {
|
||
return;
|
||
}
|
||
close();
|
||
_requestLifetime = cloudPassword().cancelResetPassword(
|
||
) | rpl::start_with_error_done([=](const QString &error) {
|
||
_requestLifetime.destroy();
|
||
}, [=] {
|
||
_requestLifetime.destroy();
|
||
});
|
||
};
|
||
controller()->show(Ui::MakeConfirmBox({
|
||
.text = tr::lng_cloud_password_reset_cancel_sure(),
|
||
.confirmed = cancel,
|
||
.confirmText = tr::lng_box_yes(),
|
||
.cancelText = tr::lng_box_no(),
|
||
}));
|
||
} else if (suggest == Status::SuggestAction::Reset) {
|
||
_requestLifetime = cloudPassword().resetPassword(
|
||
) | rpl::start_with_next_error_done([=](
|
||
Api::CloudPassword::ResetRetryDate retryDate) {
|
||
_requestLifetime.destroy();
|
||
const auto left = std::max(
|
||
retryDate - base::unixtime::now(),
|
||
60);
|
||
controller()->show(Ui::MakeInformBox(
|
||
tr::lng_cloud_password_reset_later(
|
||
tr::now,
|
||
lt_duration,
|
||
Ui::FormatResetCloudPasswordIn(left))));
|
||
}, [=](const QString &type) {
|
||
_requestLifetime.destroy();
|
||
}, [=] {
|
||
_requestLifetime.destroy();
|
||
|
||
cloudPassword().reload();
|
||
using PasswordState = Core::CloudPasswordState;
|
||
_requestLifetime = cloudPassword().state(
|
||
) | rpl::filter([=](const PasswordState &s) {
|
||
return !s.hasPassword;
|
||
}) | rpl::take(
|
||
1
|
||
) | rpl::start_with_next([=](const PasswordState &s) {
|
||
_requestLifetime.destroy();
|
||
controller()->show(Ui::MakeInformBox(
|
||
tr::lng_cloud_password_removed()));
|
||
setStepData(StepData());
|
||
showBack();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
} // namespace CloudPassword
|
||
|
||
Type CloudPasswordInputId() {
|
||
return CloudPassword::Input::Id();
|
||
}
|
||
|
||
} // namespace Settings
|