ZaStoGram_desktop/Telegram/SourceFiles/settings/sections/settings_local_passcode.cpp

1704 lines
50 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/sections/settings_local_passcode.h"
#include "base/platform/base_platform_last_input.h"
#include "base/platform/base_platform_info.h"
#include "base/openssl_help.h"
#include "base/system_unlock.h"
#include "base/timer.h"
#include "boxes/auto_lock_box.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/session/session_show.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "settings/cloud_password/settings_cloud_password_common.h"
#include "settings/cloud_password/settings_cloud_password_step.h"
#include "settings/settings_builder.h"
#include "settings/settings_common.h"
#include "storage/storage_domain.h"
#include "ui/vertical_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/password_input.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/passcode_strength_meter.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "wallet/wallet_key_protection.h"
#include "wallet/wallet_session.h"
#include "wallet/wallet_vault.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_passcode_strength_meter.h"
#include "styles/style_settings.h"
#include <QtWidgets/QApplication>
namespace Settings {
namespace {
using namespace Builder;
constexpr auto kPasscodeSectionTimeout = 60 * crl::time(1000);
constexpr auto kPasscodeCountdownTick = crl::time(1000);
// The inactivity window of a section holding the passcode bytes, shown as
// m:ss in its title bar. It filters events on qApp, because a filter on the
// section alone never sees its children's events, and watched is exactly the
// widget under a mouse press or the focus widget of a key press, so the
// owner's inside() decides whether the interaction belongs to it. The owner
// pauses it while its own flow is in flight and restarts it when that flow
// ends without leaving. tick() calls expired() last: that callback may
// destroy the owner and this object with it.
class PasscodeCountdown final : public QObject {
public:
PasscodeCountdown(Fn<bool(QWidget*)> inside, Fn<void()> expired);
~PasscodeCountdown();
void restart();
void pause();
[[nodiscard]] rpl::producer<QString> text() const;
private:
bool eventFilter(QObject *watched, QEvent *e) override;
void tick();
base::Timer _timer;
crl::time _expiresAt = 0;
bool _paused = true;
rpl::variable<QString> _text;
Fn<bool(QWidget*)> _inside;
Fn<void()> _expired;
};
[[nodiscard]] QString FormatCountdown(crl::time seconds) {
return u"%1:%2"_q
.arg(seconds / 60)
.arg(seconds % 60, 2, 10, QChar('0'));
}
PasscodeCountdown::PasscodeCountdown(
Fn<bool(QWidget*)> inside,
Fn<void()> expired)
: _timer([=] { tick(); })
, _inside(std::move(inside))
, _expired(std::move(expired)) {
qApp->installEventFilter(this);
}
PasscodeCountdown::~PasscodeCountdown() {
qApp->removeEventFilter(this);
}
void PasscodeCountdown::restart() {
_paused = false;
_expiresAt = crl::now() + kPasscodeSectionTimeout;
_text = FormatCountdown(kPasscodeSectionTimeout / 1000);
_timer.callEach(kPasscodeCountdownTick);
}
void PasscodeCountdown::pause() {
_paused = true;
_timer.cancel();
}
rpl::producer<QString> PasscodeCountdown::text() const {
return _text.value();
}
bool PasscodeCountdown::eventFilter(QObject *watched, QEvent *e) {
const auto type = e->type();
if (!_paused
&& (type == QEvent::MouseButtonPress || type == QEvent::KeyPress)
&& _inside(qobject_cast<QWidget*>(watched))) {
restart();
}
return false;
}
void PasscodeCountdown::tick() {
const auto remaining = _expiresAt - crl::now();
if (remaining > 0) {
_text = FormatCountdown((remaining + 999) / 1000);
return;
}
_text = FormatCountdown(0);
pause();
_expired();
}
// A section's own writes - the change, the lock toggle, the removal and the
// ones the wallet boxes it opens make - fire this from inside the writer
// while busy() is set, so the filter keeps it to changes made elsewhere.
// Those make the retained bytes stale: a changed passcode is asked for again,
// a removed one leaves the passcode area, because a Check for a passcode that
// no longer exists is wrong. The section's forgetAndCheck() tells the two
// apart, for this signal and for every mint that fails on stale bytes.
// Deferred out of the writer, the way KeyProtectionBox closes on the same
// signal.
void SubscribeToExternalPasscodeChanges(
not_null<Ui::RpWidget*> section,
not_null<Window::SessionController*> controller,
Fn<bool()> busy,
Fn<void()> stale) {
controller->session().domain().local().localPasscodeChanged(
) | rpl::filter([=] {
return !busy();
}) | rpl::on_next([=] {
crl::on_main(section, stale);
}, section->lifetime());
}
// A section that survives its own Back is the bottom of its settings layer -
// for example after the passcode deep link opened Check in a fresh layer and
// Manage dropped it - where Back has nowhere to go, so leaving the passcode
// area means closing the layer. Otherwise the fire ends in
// showBackFromStack(), which deletes the Info widget the section lives in -
// object_ptr::destroy() is a plain delete - so the section is gone the
// moment it returns. The event stream copies its data pointer before
// delivering, which is why the fire itself survives and only the lines after
// it need the guard. Closing the layer destroys the section as well, so the
// caller touches nothing of it after this.
void LeavePasscodeArea(
not_null<Ui::RpWidget*> section,
not_null<Window::SessionController*> controller,
Fn<void()> showBack) {
const auto weak = base::make_weak(section);
const auto weakController = base::make_weak(controller);
showBack();
if (weak && weakController) {
weakController->hideSpecialLayer();
}
}
[[nodiscard]] Storage::SetPasscodeResult SetPasscode(
not_null<Window::SessionController*> controller,
const QString &pass,
Storage::PasscodeVerification verification) {
cSetPasscodeBadTries(0);
const auto result = controller->session().domain().local().setPasscode(
pass.toUtf8(),
verification);
if (result == Storage::SetPasscodeResult::Success) {
Core::App().localPasscodeChanged();
}
return result;
}
[[nodiscard]] Storage::SetPasscodeResult SetPasscode(
not_null<Window::SessionController*> controller,
Storage::PasscodeDerivation derived,
Storage::PasscodeVerification verification) {
cSetPasscodeBadTries(0);
const auto result = controller->session().domain().local().setPasscode(
std::move(derived),
verification);
if (result == Storage::SetPasscodeResult::Success) {
Core::App().localPasscodeChanged();
}
return result;
}
[[nodiscard]] Storage::SetPasscodeResult RemovePasscode(
not_null<Window::SessionController*> controller,
Storage::PasscodeVerification verification) {
if (!Core::App().domain().local().hasPasscode()) {
return Storage::SetPasscodeResult::Success;
}
const auto result = SetPasscode(controller, QString(), verification);
if (result == Storage::SetPasscodeResult::Success) {
Core::App().settings().setSystemUnlockEnabled(false);
Core::App().saveSettingsDelayed();
}
return result;
}
// The passcode bytes the Check section accepted are consumed by the Manage
// and Change sections, which are different section objects created later by
// the navigation stack, and each keeps its own copy for its lifetime. They
// travel in the std::any the Info controller keeps for the whole settings
// stack, the same channel CloudPassword::StepData uses, but only for the one
// synchronous navigation hop that hands them over, in either direction: the
// sender writes the carrier immediately before the hop, the receiving section
// moves the bytes out of the std::any into a member of its own and resets the
// any, so nothing lingers there, and a section that closes instead of hopping
// takes its bytes straight back. std::any needs a copyable payload and
// SecureBytes is move-only, so the carrier is a shared_ptr whose buffer the
// move has already cleansed by the time the reset destroys it. So the bytes
// live exactly as long as the section holding them - leaving by hand, by
// removeFromStack or by the countdown destroys that section and cleanses
// them - and a Manage section reached straight from settings search finds
// nothing and asks for the passcode.
struct PasscodeHandOver {
std::shared_ptr<Wallet::SecureBytes> bytes;
};
[[nodiscard]] Wallet::SecureBytes TakePasscode(std::any *stepData) {
if (!stepData || !stepData->has_value()) {
return {};
}
const auto carrier = std::any_cast<PasscodeHandOver>(stepData);
if (!carrier || !carrier->bytes) {
return {};
}
auto result = std::move(*carrier->bytes);
*stepData = std::any();
return result;
}
void WritePasscode(std::any *stepData, Wallet::SecureBytes bytes) {
if (stepData) {
*stepData = PasscodeHandOver{
std::make_shared<Wallet::SecureBytes>(std::move(bytes)),
};
}
}
[[nodiscard]] QByteArray Utf8Copy(const Wallet::SecureBytes &bytes) {
return QByteArray(
reinterpret_cast<const char*>(bytes.span().data()),
bytes.size());
}
} // namespace
namespace details {
class LocalPasscodeEnter : public AbstractSection {
public:
enum class EnterType {
Create,
Check,
Change,
};
LocalPasscodeEnter(
QWidget *parent,
not_null<Window::SessionController*> controller);
~LocalPasscodeEnter();
void showFinished() override;
void setInnerFocus() override;
[[nodiscard]] rpl::producer<Type> sectionShowOther() override;
[[nodiscard]] rpl::producer<> sectionShowBack() override;
[[nodiscard]] rpl::producer<std::vector<Type>> removeFromStack() override;
void checkBeforeClose(Fn<void()> close) override;
[[nodiscard]] rpl::producer<QString> title() override;
[[nodiscard]] rpl::producer<QString> titleBadge() override;
void setStepDataReference(std::any &data) override;
protected:
void setupContent();
[[nodiscard]] virtual EnterType enterType() const = 0;
private:
void forgetAndCheck();
void leaveArea();
rpl::event_stream<> _showFinished;
rpl::event_stream<> _setInnerFocus;
rpl::event_stream<Type> _showOther;
rpl::event_stream<> _showBack;
rpl::event_stream<std::vector<Type>> _removeFromStack;
std::any *_stepData = nullptr;
Wallet::SecureBytes _passcode;
std::unique_ptr<PasscodeCountdown> _countdown;
bool _systemUnlockWithBiometric = false;
bool _deriving = false;
};
LocalPasscodeEnter::LocalPasscodeEnter(
QWidget *parent,
not_null<Window::SessionController*> controller)
: AbstractSection(parent, controller) {
}
rpl::producer<QString> LocalPasscodeEnter::title() {
return tr::lng_settings_passcode_title();
}
rpl::producer<QString> LocalPasscodeEnter::titleBadge() {
if (!_countdown) {
return nullptr;
}
return _countdown->text();
}
void LocalPasscodeEnter::setStepDataReference(std::any &data) {
// TypedLocalPasscodeEnter builds the content from its constructor, so
// this runs after setupContent(), still inside the navigation that
// creates the section, where a nested navigation is unsafe: a Change
// section that received no bytes goes to Check one turn later. Check
// and Create discard whatever carrier arrived.
_stepData = &data;
auto bytes = TakePasscode(_stepData);
if (enterType() != EnterType::Change) {
return;
}
_passcode = std::move(bytes);
if (_passcode.empty()) {
crl::on_main(this, [=] {
_showOther.fire(LocalPasscodeCheckId());
});
} else {
_countdown->restart();
}
}
void LocalPasscodeEnter::forgetAndCheck() {
if (!controller()->session().domain().local().hasPasscode()) {
leaveArea();
return;
}
_passcode.clear();
if (_countdown) {
_countdown->pause();
}
_showOther.fire(LocalPasscodeCheckId());
}
void LocalPasscodeEnter::leaveArea() {
_passcode.clear();
if (_countdown) {
_countdown->pause();
}
_removeFromStack.fire({ LocalPasscodeManageId() });
LeavePasscodeArea(this, controller(), [=] { _showBack.fire({}); });
}
void LocalPasscodeEnter::checkBeforeClose(Fn<void()> close) {
if (enterType() != EnterType::Change
|| _passcode.empty()
|| !_stepData) {
close();
return;
}
// Back rebuilds Manage from its memento through the same step-data hop
// that brought the bytes here, so the rebuilt Manage stays instead of
// asking for the passcode again. The same hook serves Close, where the
// bytes go nowhere: a Close that leaves this section alive takes them
// straight back out of the any, an instant one destroys the controller
// and the carrier together.
WritePasscode(_stepData, std::move(_passcode));
const auto weak = base::make_weak(this);
close();
if (weak) {
_passcode = TakePasscode(_stepData);
}
}
void LocalPasscodeEnter::setupContent() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
base::SystemUnlockStatus(
true
) | rpl::on_next([=](base::SystemUnlockAvailability status) {
_systemUnlockWithBiometric = status.available
&& status.withBiometrics;
}, lifetime());
const auto isCreate = (enterType() == EnterType::Create);
const auto isCheck = (enterType() == EnterType::Check);
const auto isChange = (enterType() == EnterType::Change);
const auto walletDependent = isChange
&& (Wallet::LiveKeyProtection() == Wallet::VaultKind::Passcode);
auto icon = CreateLottieIcon(
content,
{
.name = u"local_passcode_enter"_q,
.sizeOverride = st::normalBoxLottieSize,
},
st::settingLocalPasscodeIconPadding);
content->add(std::move(icon.widget));
_showFinished.events(
) | rpl::on_next([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
}, content->lifetime());
if (isChange) {
_countdown = std::make_unique<PasscodeCountdown>(
[=](QWidget *widget) {
return widget && isAncestorOf(widget);
},
[=] { leaveArea(); });
SubscribeToExternalPasscodeChanges(
this,
controller(),
[=] { return _deriving; },
[=] { forgetAndCheck(); });
}
Ui::AddSkip(content);
content->add(
object_ptr<Ui::FlatLabel>(
content,
isCreate
? tr::lng_passcode_create_title()
: isCheck
? tr::lng_passcode_check_title()
: tr::lng_passcode_change_title(),
st::changePhoneTitle),
st::changePhoneTitlePadding,
style::al_top);
const auto addDescription = [&](rpl::producer<QString> &&text) {
const auto &st = st::settingLocalPasscodeDescription;
content->add(
object_ptr<Ui::FlatLabel>(content, std::move(text), st),
st::changePhoneDescriptionPadding,
style::al_top
)->setTryMakeSimilarLines(true);
};
if (isCreate
|| controller()->session().domain().local().appLockEnabled()) {
addDescription(tr::lng_passcode_about1());
Ui::AddSkip(content);
addDescription(tr::lng_passcode_about2());
} else {
addDescription(tr::lng_settings_passcode_wallet_about());
}
Ui::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);
const auto newPasscode = CloudPassword::AddPasswordField(
content,
isCreate
? tr::lng_passcode_enter_first()
: tr::lng_passcode_enter(),
QString());
const auto meter = walletDependent
? [&] {
auto object = object_ptr<Ui::PasscodeStrengthMeter>(
content,
st::defaultPasscodeStrengthMeter);
object->setNaturalWidth(st::settingLocalPasscodeInputField.width);
return content->add(
std::move(object),
st::settingLocalPasscodeMeterPadding,
style::al_top);
}()
: nullptr;
const auto reenterPasscode = isCheck
? (Ui::PasswordInput*)(nullptr)
: CloudPassword::AddPasswordField(
content,
tr::lng_passcode_confirm_new(),
QString()).get();
const auto error = CloudPassword::AddError(
content,
isCheck ? newPasscode.get() : reenterPasscode);
error->setText(tr::lng_language_name(tr::now));
if (meter) {
connect(newPasscode, &Ui::MaskedInputField::changed, [=] {
meter->showCandidate(newPasscode->text());
error->hide();
});
}
const auto button = content->add(
object_ptr<Ui::RoundButton>(
content,
(isCreate
? tr::lng_passcode_create_button()
: isCheck
? tr::lng_passcode_check_button()
: tr::lng_passcode_change_button()),
st::changePhoneButton),
st::settingLocalPasscodeButtonPadding,
style::al_top);
// A derivation in flight disables both fields and the button and dims the
// button's label in its style's own secondary colour. The button is made
// transparent to the mouse as well, but the Enter path calls clicked()
// directly, so the click handler itself refuses while _deriving is set.
// _deriving also guards the localPasscodeChanged() subscription above,
// so it stays set through the section's own write and is lifted only on
// the branches that stay on this section; the countdown pauses with it.
const auto weak = base::make_weak(this);
const auto weakController = base::make_weak(controller());
const auto setDeriving = [=](bool deriving) {
_deriving = deriving;
if (_countdown) {
if (deriving) {
_countdown->pause();
} else {
_countdown->restart();
}
}
newPasscode->setDisabled(deriving);
if (!weak || !weakController) {
return;
}
if (reenterPasscode) {
reenterPasscode->setDisabled(deriving);
if (!weak || !weakController) {
return;
}
}
button->setDisabled(deriving);
if (!weak || !weakController) {
return;
}
button->setAttribute(Qt::WA_TransparentForMouseEvents, deriving);
button->setTextFgOverride(deriving
? std::make_optional(st::changePhoneButton.numbersTextFg->c)
: std::nullopt);
if (!deriving) {
newPasscode->setFocus();
}
};
const auto showFieldError = [=](const QString &text) {
newPasscode->setFocus();
if (!weak || !weakController) {
return;
}
newPasscode->showError();
if (!weak || !weakController) {
return;
}
error->show();
if (weak && weakController) {
error->setText(text);
}
};
// The retained bytes open key_data and the shared Passcode wrap on the
// worker. The current live key map is rotated only after those inputs
// are revalidated, before either checked write starts. A passcode-only
// change under another wallet policy leaves the keyring untouched.
const auto applyChange = [=](const QString &newText) {
auto newUtf8 = newText.toUtf8();
const auto cleanse = gsl::finally([&] {
if (!newUtf8.isEmpty()) {
OPENSSL_cleanse(newUtf8.data(), newUtf8.size());
}
});
using Result = Wallet::LocalPasscodeChangeResult;
Wallet::ChangeLocalPasscode(this, _passcode, newUtf8, [=](
Result result) {
const auto controller = weakController.get();
if (!weak || !controller) {
return;
}
switch (result) {
case Result::VaultFailed:
setDeriving(false);
if (weak && weakController) {
showFieldError(tr::lng_wallet_protection_error(tr::now));
}
return;
case Result::Stale:
forgetAndCheck();
return;
case Result::PasscodeFailed:
setDeriving(false);
if (weak && weakController) {
showFieldError(Lang::Hard::SecureSaveError());
}
return;
case Result::CommitFailed:
controller->showToast(
tr::lng_wallet_protection_passcode_changed_error(tr::now));
if (!weak || !weakController) {
return;
}
[[fallthrough]];
case Result::Done: {
auto changed = newText.toUtf8();
const auto cleanse = gsl::finally([&] {
if (!changed.isEmpty()) {
OPENSSL_cleanse(changed.data(), changed.size());
}
});
WritePasscode(_stepData, Wallet::SecureBytes(changed));
_showBack.fire({});
return;
}
}
});
};
const auto deriveAndSave = [=](const QString &newText) {
auto utf8 = newText.toUtf8();
const auto cleanse = gsl::finally([&] {
if (!utf8.isEmpty()) {
OPENSSL_cleanse(utf8.data(), utf8.size());
}
});
const auto &local = controller()->session().domain().local();
Storage::DeriveOnWorker(
local.prepareNewWrap(utf8),
crl::guard(this, [=](Storage::PasscodeDerivation &&derived) {
const auto result = SetPasscode(
controller(),
std::move(derived),
Storage::PasscodeVerification());
if (!weak || !weakController) {
return;
} else if (result != Storage::SetPasscodeResult::Success) {
setDeriving(false);
showFieldError(Lang::Hard::SecureSaveError());
return;
}
if (Platform::IsWindows() || _systemUnlockWithBiometric) {
Core::App().settings().setSystemUnlockEnabled(true);
Core::App().saveSettingsDelayed();
}
auto created = newText.toUtf8();
const auto cleanse = gsl::finally([&] {
if (!created.isEmpty()) {
OPENSSL_cleanse(created.data(), created.size());
}
});
WritePasscode(_stepData, Wallet::SecureBytes(created));
_showOther.fire(LocalPasscodeManageId());
}));
};
const auto checkAndChange = [=](const QString &newText) {
auto utf8 = newText.toUtf8();
const auto cleanse = gsl::finally([&] {
if (!utf8.isEmpty()) {
OPENSSL_cleanse(utf8.data(), utf8.size());
}
});
const auto &local = controller()->session().domain().local();
Storage::DeriveOnWorker(
local.prepareOpen(utf8),
crl::guard(this, [=](Storage::PasscodeDerivation &&derived) {
const auto &domain = controller()->session().domain();
if (domain.local().checkPasscode(std::move(derived))) {
setDeriving(false);
newPasscode->setFocus();
newPasscode->showError();
newPasscode->selectAll();
error->show();
error->setText(tr::lng_passcode_is_same(tr::now));
return;
}
applyChange(newText);
}));
};
const auto checkAndOpenManage = [=](const QString &newText) {
setDeriving(true);
auto utf8 = newText.toUtf8();
const auto cleanse = gsl::finally([&] {
if (!utf8.isEmpty()) {
OPENSSL_cleanse(utf8.data(), utf8.size());
}
});
const auto &local = controller()->session().domain().local();
Storage::DeriveOnWorker(
local.prepareOpen(utf8),
crl::guard(this, [=](Storage::PasscodeDerivation &&derived) {
const auto &local = controller()->session().domain().local();
if (!local.checkPasscode(std::move(derived))) {
setDeriving(false);
cSetPasscodeBadTries(cPasscodeBadTries() + 1);
cSetPasscodeLastTry(crl::now());
newPasscode->selectAll();
newPasscode->setFocus();
newPasscode->showError();
error->show();
error->setText(tr::lng_passcode_wrong(tr::now));
return;
}
cSetPasscodeBadTries(0);
auto accepted = newText.toUtf8();
const auto cleanse = gsl::finally([&] {
if (!accepted.isEmpty()) {
OPENSSL_cleanse(accepted.data(), accepted.size());
}
});
WritePasscode(_stepData, Wallet::SecureBytes(accepted));
_showOther.fire(LocalPasscodeManageId());
}));
};
button->setClickedCallback([=] {
if (_deriving) {
return;
}
const auto newText = newPasscode->text();
const auto reenterText = reenterPasscode
? reenterPasscode->text()
: QString();
if (isCreate || isChange) {
if (newText.isEmpty()) {
newPasscode->setFocus();
newPasscode->showError();
} else if (reenterText.isEmpty()) {
reenterPasscode->setFocus();
reenterPasscode->showError();
} else if (newText != reenterText) {
reenterPasscode->setFocus();
reenterPasscode->showError();
reenterPasscode->selectAll();
error->show();
error->setText(tr::lng_passcode_differ(tr::now));
} else {
setDeriving(true);
if (isCreate) {
deriveAndSave(newText);
} else {
checkAndChange(newText);
}
}
} else if (isCheck) {
if (!passcodeCanTry()) {
newPasscode->setFocus();
newPasscode->showError();
error->show();
error->setText(tr::lng_flood_error(tr::now));
return;
}
checkAndOpenManage(newText);
}
});
const auto submit = [=] {
if (!reenterPasscode || reenterPasscode->hasFocus()) {
button->clicked({}, Qt::LeftButton);
} else {
reenterPasscode->setFocus();
}
};
connect(newPasscode, &Ui::MaskedInputField::submitted, submit);
if (reenterPasscode) {
connect(reenterPasscode, &Ui::MaskedInputField::submitted, submit);
}
_setInnerFocus.events(
) | rpl::on_next([=] {
if (newPasscode->text().isEmpty()) {
newPasscode->setFocus();
} else if (reenterPasscode && reenterPasscode->text().isEmpty()) {
reenterPasscode->setFocus();
} else {
newPasscode->setFocus();
}
}, content->lifetime());
Ui::ResizeFitChild(this, content);
}
void LocalPasscodeEnter::showFinished() {
_showFinished.fire({});
}
void LocalPasscodeEnter::setInnerFocus() {
_setInnerFocus.fire({});
}
rpl::producer<Type> LocalPasscodeEnter::sectionShowOther() {
return _showOther.events();
}
rpl::producer<> LocalPasscodeEnter::sectionShowBack() {
return _showBack.events();
}
LocalPasscodeEnter::~LocalPasscodeEnter() = default;
} // namespace details
class LocalPasscodeCreate;
class LocalPasscodeCheck;
class LocalPasscodeChange;
template <typename SectionType>
class TypedLocalPasscodeEnter : public details::LocalPasscodeEnter {
public:
TypedLocalPasscodeEnter(
QWidget *parent,
not_null<Window::SessionController*> controller)
: details::LocalPasscodeEnter(parent, controller) {
setupContent();
}
[[nodiscard]] static Type Id() {
return SectionFactory<SectionType>::Instance();
}
[[nodiscard]] Type id() const final override {
return Id();
}
protected:
[[nodiscard]] EnterType enterType() const final override {
if constexpr (std::is_same_v<SectionType, LocalPasscodeCreate>) {
return EnterType::Create;
}
if constexpr (std::is_same_v<SectionType, LocalPasscodeCheck>) {
return EnterType::Check;
}
if constexpr (std::is_same_v<SectionType, LocalPasscodeChange>) {
return EnterType::Change;
}
return EnterType::Create;
}
};
class LocalPasscodeCreate final
: public TypedLocalPasscodeEnter<LocalPasscodeCreate> {
public:
using TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;
};
class LocalPasscodeCheck final
: public TypedLocalPasscodeEnter<LocalPasscodeCheck> {
public:
using TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;
};
class LocalPasscodeChange final
: public TypedLocalPasscodeEnter<LocalPasscodeChange> {
public:
using TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;
};
namespace details {
// Check drops the stale passcode sections beneath it, so that Back from it
// lands on what was below the passcode area and never on a Manage or Change
// rebuilt without bytes, which would only send it back to Check again.
rpl::producer<std::vector<Type>> LocalPasscodeEnter::removeFromStack() {
switch (enterType()) {
case EnterType::Check:
return rpl::single(std::vector<Type>{
LocalPasscodeManageId(),
LocalPasscodeChange::Id(),
LocalPasscodeCreateId(),
});
case EnterType::Change:
return _removeFromStack.events();
case EnterType::Create:
return nullptr;
}
Unexpected("Enter type in LocalPasscodeEnter::removeFromStack.");
}
} // namespace details
namespace {
enum class UnlockType {
None,
Default,
Biometrics,
Companion,
};
class LocalPasscodeManage;
void BuildManageContent(
SectionBuilder &builder,
LocalPasscodeManage *section);
class LocalPasscodeManage : public Section<LocalPasscodeManage> {
public:
LocalPasscodeManage(
QWidget *parent,
not_null<Window::SessionController*> controller);
~LocalPasscodeManage();
[[nodiscard]] rpl::producer<QString> title() override;
[[nodiscard]] rpl::producer<QString> titleBadge() override;
void showFinished() override;
[[nodiscard]] rpl::producer<> sectionShowBack() override;
[[nodiscard]] rpl::producer<std::vector<Type>> removeFromStack() override;
[[nodiscard]] base::weak_qptr<Ui::RpWidget> createPinnedToBottom(
not_null<Ui::RpWidget*> parent) override;
void setStepDataReference(std::any &data) override;
void setBusy(bool busy);
[[nodiscard]] rpl::producer<bool> busyValue() const;
void touched();
void mintVerification(Fn<void(Storage::PasscodeVerification)> done);
void writeRefused();
private:
void setupContent();
void forgetAndCheck();
void leaveArea();
void disable();
void checkVaultAndRemove();
void showRemoval(Wallet::SecureBytes verified);
void confirmDisable();
void disableAfterRemoval(Wallet::KeyProtectionResult result);
void removeVerifiedAndLeave();
rpl::variable<bool> _isBottomFillerShown;
rpl::event_stream<> _showBack;
QPointer<Ui::RpWidget> _disableButton;
QPointer<Ui::RpWidget> _pinnedBottom;
std::any *_stepData = nullptr;
Wallet::SecureBytes _passcode;
std::unique_ptr<PasscodeCountdown> _countdown;
rpl::variable<bool> _busy = false;
};
LocalPasscodeManage::LocalPasscodeManage(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent, controller) {
// The pinned bottom content with the Disable button is parented to the
// Info widget, not to this section, so it is asked for by itself.
_countdown = std::make_unique<PasscodeCountdown>(
[=](QWidget *widget) {
return widget
&& (isAncestorOf(widget)
|| (_pinnedBottom && _pinnedBottom->isAncestorOf(widget)));
},
[=] { leaveArea(); });
setupContent();
}
rpl::producer<QString> LocalPasscodeManage::title() {
return tr::lng_settings_passcode_title();
}
rpl::producer<QString> LocalPasscodeManage::titleBadge() {
return _countdown->text();
}
void LocalPasscodeManage::setStepDataReference(std::any &data) {
// createPinnedToBottom() runs before this, so the disable button reads
// _passcode when it is pressed, which is always later than this call.
// A Manage without bytes - reached from settings search, or rebuilt from
// a memento the bytes did not follow - asks for the passcode instead of
// showing buttons that bounce, and offers to create one when there is no
// passcode to ask for. That happens one turn later, because this still
// runs inside the navigation that creates the section.
_stepData = &data;
_passcode = TakePasscode(_stepData);
if (!_passcode.empty()) {
_countdown->restart();
return;
}
crl::on_main(this, [=] {
const auto &local = controller()->session().domain().local();
showOther(local.hasPasscode()
? LocalPasscodeCheckId()
: LocalPasscodeCreateId());
});
}
rpl::producer<std::vector<Type>> LocalPasscodeManage::removeFromStack() {
return rpl::single(std::vector<Type>{
LocalPasscodeManage::Id(),
LocalPasscodeCreate::Id(),
LocalPasscodeCheck::Id(),
LocalPasscodeChange::Id(),
});
}
void LocalPasscodeManage::setBusy(bool busy) {
_busy = busy;
if (busy) {
_countdown->pause();
} else {
_countdown->restart();
}
if (_disableButton) {
_disableButton->setDisabled(busy);
}
}
rpl::producer<bool> LocalPasscodeManage::busyValue() const {
return _busy.value();
}
void LocalPasscodeManage::touched() {
if (!_busy.current()) {
_countdown->restart();
}
}
void LocalPasscodeManage::mintVerification(
Fn<void(Storage::PasscodeVerification)> done) {
Wallet::MintVerificationOnWorker(this, _passcode, [=](
std::optional<Storage::PasscodeVerification> verification) {
if (!verification) {
forgetAndCheck();
} else {
done(*verification);
}
});
}
void LocalPasscodeManage::writeRefused() {
LOG(("App Error: key_data refused a passcode verification minted in the "
"same callback."));
forgetAndCheck();
}
void LocalPasscodeManage::forgetAndCheck() {
if (!controller()->session().domain().local().hasPasscode()) {
leaveArea();
return;
}
_passcode.clear();
_countdown->pause();
showOther(LocalPasscodeCheckId());
}
void LocalPasscodeManage::leaveArea() {
_passcode.clear();
_countdown->pause();
LeavePasscodeArea(this, controller(), [=] { _showBack.fire({}); });
}
void LocalPasscodeManage::setupContent() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
SubscribeToExternalPasscodeChanges(
this,
controller(),
[=] { return _busy.current(); },
[=] { forgetAndCheck(); });
const SectionBuildMethod buildMethod = [this](
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
Fn<void(Type)> showOther,
rpl::producer<> showFinished) {
auto &lifetime = container->lifetime();
const auto highlights = lifetime.make_state<HighlightRegistry>();
const auto isPaused = Window::PausedIn(
controller,
Window::GifPauseReason::Layer);
// The hop destroys this section, so Change receives the bytes
// themselves and hands them back the same way to the Manage rebuilt
// from the memento, which takes them in setStepDataReference().
auto passOther = crl::guard(this, [=, this](Type type) {
if (_busy.current()) {
return;
} else if (type == LocalPasscodeChange::Id()) {
WritePasscode(_stepData, std::move(_passcode));
}
showOther(type);
});
auto builder = SectionBuilder(WidgetContext{
.container = container,
.controller = controller,
.showOther = std::move(passOther),
.isPaused = isPaused,
.highlights = highlights,
});
BuildManageContent(builder, this);
std::move(showFinished) | rpl::on_next([=] {
for (const auto &[id, entry] : *highlights) {
if (entry.widget) {
controller->checkHighlightControl(
id,
entry.widget,
base::duplicate(entry.args));
}
}
}, lifetime);
};
build(content, buildMethod);
Ui::ResizeFitChild(this, content);
}
void LocalPasscodeManage::disable() {
setBusy(true);
if (Wallet::LiveKeyProtection() == Wallet::VaultKind::Passcode) {
checkVaultAndRemove();
} else {
confirmDisable();
}
}
// The retained bytes are revalidated on the worker before the chooser
// receives them. It opens the shared Passcode wrap only when the chosen
// policy needs to replace it. The final key_data removal derives and spends
// a fresh verification after that one keyring write has completed.
void LocalPasscodeManage::checkVaultAndRemove() {
auto utf8 = Utf8Copy(_passcode);
const auto cleanse = gsl::finally([&] {
if (!utf8.isEmpty()) {
OPENSSL_cleanse(utf8.data(), utf8.size());
}
});
const auto &local = controller()->session().domain().local();
Storage::DeriveOnWorker(
local.prepareOpen(utf8),
crl::guard(this, [=](Storage::PasscodeDerivation &&derived) {
const auto &local = controller()->session().domain().local();
if (!local.checkPasscode(std::move(derived))) {
forgetAndCheck();
} else {
showRemoval(_passcode.copy());
}
}));
}
void LocalPasscodeManage::showRemoval(Wallet::SecureBytes verified) {
if (Wallet::LiveKeyProtection() != Wallet::VaultKind::Passcode) {
confirmDisable();
return;
}
auto accounts = std::vector<base::weak_ptr<Main::Account>>();
auto &domain = controller()->session().domain();
auto &runtime = domain.walletKeyring();
const auto live = runtime.liveKeyring(runtime.reading().keyring);
for (const auto &[index, account] : domain.accounts()) {
const auto session = account->maybeSession();
if (session && ranges::any_of(live.entries, [&](const auto &entry) {
return entry.accountId == session->uniqueId();
})) {
accounts.push_back(base::make_weak(account.get()));
}
}
const auto weak = base::make_weak(this);
const auto weakController = base::make_weak(controller());
const auto show = Main::MakeSessionShow(
controller()->uiShow(),
&controller()->session());
Wallet::ShowKeyProtectionBox(show, Wallet::KeyProtectionArgs{
.mode = Wallet::KeyProtectionMode::Removal,
.accounts = std::move(accounts),
.done = [=](Wallet::KeyProtectionResult result) {
if (weak && weakController) {
weak->disableAfterRemoval(std::move(result));
}
},
}, std::move(verified));
}
void LocalPasscodeManage::disableAfterRemoval(
Wallet::KeyProtectionResult result) {
if (result.cancelled) {
setBusy(false);
return;
} else if (result.failed) {
controller()->showToast(tr::lng_wallet_protection_error(tr::now));
setBusy(false);
return;
} else if (result.kind == Wallet::VaultKind::Passcode) {
controller()->showToast(
tr::lng_settings_passcode_lock_off_wallet(tr::now));
setBusy(false);
return;
}
removeVerifiedAndLeave();
}
void LocalPasscodeManage::removeVerifiedAndLeave() {
if (!controller()->session().domain().local().hasPasscode()) {
leaveArea();
return;
}
const auto weak = base::make_weak(this);
const auto weakController = base::make_weak(controller());
mintVerification([=](Storage::PasscodeVerification verification) {
if (Wallet::LiveKeyProtection() == Wallet::VaultKind::Passcode) {
weakController->showToast(
tr::lng_wallet_protection_error(tr::now));
setBusy(false);
return;
}
const auto result = RemovePasscode(controller(), verification);
if (!weak || !weakController) {
return;
}
if (result == Storage::SetPasscodeResult::NeedsVerification) {
writeRefused();
return;
} else if (result != Storage::SetPasscodeResult::Success) {
weakController->showToast(Lang::Hard::SecureSaveError());
setBusy(false);
return;
}
leaveArea();
});
}
void LocalPasscodeManage::confirmDisable() {
const auto weak = base::make_weak(this);
const auto weakController = base::make_weak(controller());
// While the launch lock is on, the passcode is the only thing
// covering an open vault's key at rest, so removing it says so -
// the wording the app-lock toggle already shows, composed with
// today's confirmation instead of given a key of its own. A vault
// that holds no key has nothing to cover, and nothing is said.
const auto warned = (Wallet::LiveKeyProtection() == Wallet::VaultKind::Open)
&& controller()->session().domain().local().appLockEnabled();
auto text = [&]() -> rpl::producer<QString> {
if (!warned) {
return tr::lng_settings_passcode_disable_sure();
}
return rpl::combine(
tr::lng_settings_passcode_disable_sure(),
tr::lng_wallet_protection_open_warning_nolock()
) | rpl::map([](const QString &sure, const QString &warning) {
return sure + u"\n\n"_q + warning;
});
}();
controller()->show(Ui::MakeConfirmBox({
.text = std::move(text),
.confirmed = [=](Fn<void()> close) {
if (!weak || !weakController) {
close();
return;
}
const auto [needsRemoval, needsWarning] = [&] {
const auto policy = Wallet::LiveKeyProtection();
const auto &local = weakController->session().domain().local();
return std::pair(
policy == Wallet::VaultKind::Passcode,
policy == Wallet::VaultKind::Open && local.appLockEnabled());
}();
close();
if (!weak || !weakController) {
return;
} else if (needsRemoval) {
weak->checkVaultAndRemove();
return;
} else if (needsWarning && !warned) {
weak->confirmDisable();
return;
}
weak->removeVerifiedAndLeave();
},
.cancelled = [=](Fn<void()> close) {
close();
if (weak) {
weak->setBusy(false);
}
},
.confirmText = tr::lng_settings_auto_night_disable(),
.confirmStyle = &st::attentionBoxButton,
}));
}
base::weak_qptr<Ui::RpWidget> LocalPasscodeManage::createPinnedToBottom(
not_null<Ui::RpWidget*> parent) {
auto bottomButton = CloudPassword::CreateBottomDisableButton(
parent,
geometryValue(),
tr::lng_settings_passcode_disable(),
[=] {
if (!_busy.current()) {
disable();
}
});
_isBottomFillerShown = base::take(bottomButton.isBottomFillerShown);
_disableButton = bottomButton.button.get();
_pinnedBottom = bottomButton.content.get();
return bottomButton.content;
}
void LocalPasscodeManage::showFinished() {
Section<LocalPasscodeManage>::showFinished();
controller()->checkHighlightControl(
u"passcode/disable"_q,
_disableButton);
}
rpl::producer<> LocalPasscodeManage::sectionShowBack() {
return _showBack.events();
}
LocalPasscodeManage::~LocalPasscodeManage() = default;
void BuildManageContent(
SectionBuilder &builder,
LocalPasscodeManage *section) {
const auto controller = builder.controller();
if (!controller) {
return;
}
const auto container = builder.container();
struct State {
rpl::event_stream<> autoLockBoxClosing;
rpl::event_stream<bool> appLockToggles;
rpl::variable<bool> appLockOn;
rpl::variable<bool> walletDependent;
};
const auto state = container->lifetime().make_state<State>();
const auto &local = controller->session().domain().local();
// The app lock state follows the store and not the row, because the
// removal box of the disable flow turns the lock off by itself, from an
// object that cannot reach this state. appLockToggles carries only the
// reverts a refused or dismissed toggle needs, and it reaches the row
// alone: rpl::variable drops a value equal to the one it already holds,
// and a revert is exactly the case where the store never moved.
// Both the initial toggle and its committed-change refresh use verified
// launch protection, so planted bytes cannot make the row say off.
state->appLockOn = rpl::single(
local.appLockEnabled()
) | rpl::then(local.localPasscodeChanged() | rpl::map([] {
return Core::App().domain().local().appLockEnabled();
}));
state->walletDependent = rpl::single(
rpl::empty
) | rpl::then(rpl::merge(
local.localPasscodeChanged(),
controller->session().wallet().keyProtectionUpdates())
) | rpl::map([] {
return (Wallet::LiveKeyProtection() == Wallet::VaultKind::Passcode);
});
builder.addSkip();
builder.addButton({
.id = u"passcode/change"_q,
.title = tr::lng_passcode_change(),
.icon = { &st::menuIconLock },
.onClick = [=] {
builder.showOther()(LocalPasscodeChange::Id());
},
.keywords = { u"password"_q, u"code"_q },
});
const auto lockApp = builder.addButton({
.id = u"passcode/lock-app"_q,
.title = tr::lng_settings_passcode_lock_app(),
.icon = { &st::menuIconPermissions },
.toggled = rpl::merge(
state->appLockOn.value(),
state->appLockToggles.events()),
.keywords = { u"lock"_q, u"launch"_q, u"startup"_q },
.shown = state->walletDependent.value(),
});
if (lockApp) {
const auto weak = base::make_weak(container);
section->busyValue(
) | rpl::on_next([=](bool busy) {
lockApp->setDisabled(busy);
}, lockApp->lifetime());
// Turning the lock on installs nothing weaker and needs no proof, so
// it is one synchronous write. Turning it off installs an open wrap,
// which key_data lets through only with a proof minted from the
// retained bytes, in the same callback as the write. Both run with
// the section busy: the write fires localPasscodeChanged() and the
// section's subscription must see it as its own. A refused or failed
// flip reverts the row and lifts the busy state; a refused proof
// means the bytes went stale and asks for the passcode again.
const auto lockOn = [=] {
section->setBusy(true);
const auto result = Core::App().domain().local().setAppLockEnabled(
true,
Storage::PasscodeVerification());
if (result != Storage::SetPasscodeResult::Success) {
state->appLockToggles.fire_copy(false);
controller->showToast(Lang::Hard::SecureSaveError());
section->setBusy(false);
return;
}
Core::App().localPasscodeChanged();
if (weak) {
section->setBusy(false);
}
};
const auto lockOff = [=](bool walletDependent) {
section->setBusy(true);
section->mintVerification([=](
Storage::PasscodeVerification verification) {
const auto &domain = Core::App().domain();
const auto result = domain.local().setAppLockEnabled(
false,
verification);
if (result == Storage::SetPasscodeResult::NeedsVerification) {
section->writeRefused();
return;
} else if (result != Storage::SetPasscodeResult::Success) {
state->appLockToggles.fire_copy(true);
controller->showToast(Lang::Hard::SecureSaveError());
section->setBusy(false);
return;
}
Core::App().localPasscodeChanged();
if (walletDependent) {
controller->showToast(Ui::Toast::Config{
.text = tr::lng_settings_passcode_lock_off_wallet(
tr::now,
Ui::Text::WithEntities),
.duration = 2 * Ui::Toast::kDefaultDuration,
});
}
if (weak) {
section->setBusy(false);
}
});
};
lockApp->toggledChanges(
) | rpl::filter([=](bool value) {
// Compare verified locking so planted bytes cannot suppress
// a change the user requested.
return value != Core::App().domain().local().appLockEnabled();
}) | rpl::on_next([=](bool value) {
if (value) {
lockOn();
} else {
// The row is shown only while the wallet policy is Passcode.
lockOff(Wallet::LiveKeyProtection()
== Wallet::VaultKind::Passcode);
}
}, lockApp->lifetime());
}
// The wallet note belongs to the lock toggle above it, so it stays under
// that toggle and the launch-lock rows open below it. Its section is
// closed by a skip only when those rows follow: the disable button brings
// its own skip.
builder.scope([&] {
builder.addSkip();
builder.addDividerText(tr::lng_settings_passcode_wallet_about());
}, state->walletDependent.value());
builder.scope([&] {
builder.addSkip();
}, rpl::combine(
state->walletDependent.value(),
state->appLockOn.value(),
rpl::mappers::_1 && rpl::mappers::_2));
builder.scope([&] {
auto autolockLabel = state->autoLockBoxClosing.events_starting_with(
{}
) | rpl::map([] {
const auto autolock = Core::App().settings().autoLock();
const auto hours = autolock / 3600;
const auto minutes = (autolock - (hours * 3600)) / 60;
return (hours && minutes)
? tr::lng_passcode_autolock_hours_minutes(
tr::now,
lt_hours_count,
QString::number(hours),
lt_minutes_count,
QString::number(minutes))
: minutes
? tr::lng_minutes(tr::now, lt_count, minutes)
: tr::lng_hours(tr::now, lt_count, hours);
});
const auto autoLockButton = builder.addButton({
.id = u"passcode/auto-lock"_q,
.title = base::Platform::LastUserInputTimeSupported()
? tr::lng_passcode_autolock_away()
: tr::lng_passcode_autolock_inactive(),
.icon = { &st::menuIconTimer },
.label = std::move(autolockLabel),
.keywords = { u"timeout"_q, u"lock"_q, u"time"_q },
});
if (autoLockButton) {
const auto weak = base::make_weak(container);
autoLockButton->addClickHandler([=] {
const auto box = controller->show(Box<AutoLockBox>());
box->boxClosing(
) | rpl::on_next([=] {
if (weak) {
state->autoLockBoxClosing.fire({});
section->touched();
}
}, box->lifetime());
});
}
builder.addSkip();
using Divider = CloudPassword::OneEdgeBoxContentDivider;
builder.add([](const WidgetContext &ctx) {
const auto divider = Ui::CreateChild<Divider>(ctx.container.get());
divider->lower();
const auto about = ctx.container->add(
object_ptr<Ui::PaddingWrap<>>(
ctx.container,
object_ptr<Ui::FlatLabel>(
ctx.container,
rpl::combine(
tr::lng_passcode_about1(),
tr::lng_passcode_about3()
) | rpl::map([](const QString &s1, const QString &s2) {
return s1 + "\n\n" + s2;
}),
st::boxDividerLabel),
st::defaultBoxDividerLabelPadding));
about->geometryValue(
) | rpl::on_next([=](const QRect &r) {
divider->setGeometry(r);
}, divider->lifetime());
return SectionBuilder::WidgetToAdd{};
});
builder.add([=](const WidgetContext &ctx) {
const auto systemUnlockWrap = ctx.container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
ctx.container,
object_ptr<Ui::VerticalLayout>(ctx.container))
)->setDuration(0);
const auto systemUnlockContent = systemUnlockWrap->entity();
const auto unlockType = systemUnlockContent->lifetime().make_state<
rpl::variable<UnlockType>
>(base::SystemUnlockStatus(
true
) | rpl::map([](base::SystemUnlockAvailability status) {
return status.withBiometrics
? UnlockType::Biometrics
: status.withCompanion
? UnlockType::Companion
: status.available
? UnlockType::Default
: UnlockType::None;
}));
const auto highlights = ctx.highlights;
unlockType->value(
) | rpl::on_next([=](UnlockType type) {
while (systemUnlockContent->count()) {
delete systemUnlockContent->widgetAt(0);
}
Ui::AddSkip(systemUnlockContent);
const auto biometricsButton = AddButtonWithIcon(
systemUnlockContent,
(Platform::IsWindows()
? tr::lng_settings_use_winhello()
: (type == UnlockType::Biometrics)
? tr::lng_settings_use_touchid()
: (type == UnlockType::Companion)
? tr::lng_settings_use_applewatch()
: tr::lng_settings_use_systempwd()),
st::settingsButton,
{ Platform::IsWindows()
? &st::menuIconWinHello
: (type == UnlockType::Biometrics)
? &st::menuIconTouchID
: (type == UnlockType::Companion)
? &st::menuIconAppleWatch
: &st::menuIconSystemPwd });
biometricsButton->toggleOn(
rpl::single(Core::App().settings().systemUnlockEnabled())
)->toggledChanges(
) | rpl::filter([=](bool value) {
return value
!= Core::App().settings().systemUnlockEnabled();
}) | rpl::on_next([=](bool value) {
Core::App().settings().setSystemUnlockEnabled(value);
Core::App().saveSettingsDelayed();
section->touched();
}, systemUnlockContent->lifetime());
if (highlights) {
highlights->push_back({
u"passcode/biometrics"_q,
{ biometricsButton.get() },
});
}
Ui::AddSkip(systemUnlockContent);
Ui::AddDividerText(
systemUnlockContent,
(Platform::IsWindows()
? tr::lng_settings_use_winhello_about()
: (type == UnlockType::Biometrics)
? tr::lng_settings_use_touchid_about()
: (type == UnlockType::Companion)
? tr::lng_settings_use_applewatch_about()
: tr::lng_settings_use_systempwd_about()));
}, systemUnlockContent->lifetime());
systemUnlockWrap->toggleOn(unlockType->value(
) | rpl::map(rpl::mappers::_1 != UnlockType::None));
return SectionBuilder::WidgetToAdd{};
}, [] {
return SearchEntry{
.id = u"passcode/biometrics"_q,
.title = Platform::IsWindows()
? tr::lng_settings_use_winhello(tr::now)
: tr::lng_settings_use_touchid(tr::now),
.keywords = { u"biometrics"_q, u"touchid"_q, u"faceid"_q,
u"winhello"_q, u"fingerprint"_q },
};
});
}, state->appLockOn.value());
builder.add(nullptr, [] {
return SearchEntry{
.id = u"passcode/disable"_q,
.title = tr::lng_settings_passcode_disable(tr::now),
.keywords = { u"disable"_q, u"remove"_q, u"turn off"_q },
};
});
}
const auto kMeta = BuildHelper({
.id = LocalPasscodeManage::Id(),
.parentId = nullptr,
.title = &tr::lng_settings_passcode_title,
.icon = &st::menuIconLock,
}, [](SectionBuilder &builder) {
BuildManageContent(builder, nullptr);
});
} // namespace
Type LocalPasscodeCreateId() {
return LocalPasscodeCreate::Id();
}
Type LocalPasscodeCheckId() {
return LocalPasscodeCheck::Id();
}
Type LocalPasscodeManageId() {
return LocalPasscodeManage::Id();
}
} // namespace Settings