/* 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 icon; Fn update; }; Icon CreateInteractiveLottieIcon( not_null container, Lottie::IconDescriptor &&descriptor, style::margins padding) { auto object = object_ptr(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 AddLinkButton( not_null content, not_null input) { const auto button = Ui::CreateChild( 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 { public: using TypedAbstractStep::TypedAbstractStep; [[nodiscard]] rpl::producer title() override; void setupContent(); protected: [[nodiscard]] rpl::producer> removeTypes() override; private: void setupRecoverButton( not_null container, not_null button, not_null info, Fn recoverCallback); rpl::variable> _removesFromStack; rpl::lifetime _requestLifetime; }; rpl::producer> Input::removeTypes() { return _removesFromStack.value(); } rpl::producer Input::title() { return tr::lng_settings_cloud_password_password_title(); } void Input::setupContent() { const auto content = Ui::CreateChild(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{ 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( 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 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( 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 container, not_null button, not_null info, Fn recoverCallback) { struct Status { enum class SuggestAction { Recover, Reset, CancelReset, }; SuggestAction suggest = SuggestAction::Recover; TimeId left = 0; }; struct State { base::Timer timer; rpl::variable status; }; const auto state = container->lifetime().make_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 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