/* 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_passkeys.h" #include "base/unixtime.h" #include "core/application.h" #include "data/components/passkeys.h" #include "data/data_session.h" #include "data/stickers/data_custom_emoji.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "main/main_session.h" #include "platform/platform_webauthn.h" #include "settings/cloud_password/settings_cloud_password_common.h" #include "settings/sections/settings_privacy_security.h" #include "settings/settings_builder.h" #include "settings/settings_common.h" #include "settings/settings_common_session.h" #include "ui/boxes/confirm_box.h" #include "ui/layers/generic_box.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/custom_emoji_instance.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" #include "styles/style_channel_earn.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_premium.h" #include "styles/style_settings.h" namespace Settings { namespace { using namespace Builder; class Passkeys : public Section { public: Passkeys( QWidget *parent, not_null controller); void showFinished() override; [[nodiscard]] rpl::producer title() override; const Ui::RoundRect *bottomSkipRounding() const override { return &_bottomSkipRounding; } private: void setupContent(); const not_null _container; QPointer _addButton; Ui::RoundRect _bottomSkipRounding; rpl::event_stream<> _showFinished; }; void BuildPasskeysSection( SectionBuilder &builder, QPointer *addButton, Fn)> setupList) { const auto session = builder.session(); const auto controller = builder.controller(); builder.add([=](const WidgetContext &ctx) { setupList(ctx.container); return SectionBuilder::WidgetToAdd{}; }); auto buttonShown = session->passkeys().requestList( ) | rpl::map([session] { return session->passkeys().canRegister(); }); const auto button = builder.addButton({ .id = u"passkeys/create"_q, .title = tr::lng_settings_passkeys_button(), .st = &st::settingsButtonActive, .icon = { &st::settingsIconPasskeys }, .onClick = [controller, session] { controller->show(Box(PasskeysNoneBox, session)); }, .keywords = { u"add"_q, u"register"_q, u"create"_q }, .highlight = { .rippleShape = true }, .shown = std::move(buttonShown), }); if (addButton) { *addButton = button; } builder.addSkip(); builder.add([=](const WidgetContext &ctx) { const auto label = Ui::AddDividerText( ctx.container, tr::lng_settings_passkeys_button_about( lt_link, tr::lng_channel_earn_about_link( lt_emoji, rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), tr::rich ) | rpl::map([](TextWithEntities text) { return tr::link(std::move(text), u"internal"_q); }), tr::rich )); label->setClickHandlerFilter([controller, session](const auto &...) { controller->show(Box(PasskeysNoneBox, session)); return false; }); return SectionBuilder::WidgetToAdd{}; }); } const auto kMeta = BuildHelper({ .id = Passkeys::Id(), .parentId = PrivacySecurityId(), .title = &tr::lng_settings_passkeys_title, .icon = &st::menuIconPermissions, }, [](SectionBuilder &builder) { BuildPasskeysSection(builder, nullptr, [](not_null) {}); }); Passkeys::Passkeys( QWidget *parent, not_null controller) : Section(parent, controller) , _container(Ui::CreateChild(this)) , _bottomSkipRounding(st::boxRadius, st::boxDividerBg) { setupContent(); } void Passkeys::showFinished() { Section::showFinished(); _showFinished.fire({}); if (_addButton) { controller()->checkHighlightControl( u"passkeys/create"_q, _addButton, { .rippleShape = true }); } } rpl::producer Passkeys::title() { return tr::lng_settings_passkeys_title(); } void Passkeys::setupContent() { const auto session = &controller()->session(); const auto addButton = &_addButton; CloudPassword::SetupHeader( _container, u"passkeys"_q, _showFinished.events(), rpl::single(QString()), tr::lng_settings_passkeys_about()); Ui::AddSkip(_container); const auto setupList = [=](not_null container) { const auto passkeysListContainer = container->add( object_ptr(container)); const auto &st = st::peerListBoxItem; const auto nameStyle = &st.nameStyle; const auto ctrl = controller(); const auto rebuild = [=] { while (passkeysListContainer->count()) { delete passkeysListContainer->widgetAt(0); } for (const auto &passkey : session->passkeys().list()) { const auto button = passkeysListContainer->add( object_ptr(passkeysListContainer)); button->resize(button->width(), st.height); const auto menu = Ui::CreateChild( button, st::themesMenuToggle); menu->setClickedCallback([=] { const auto popup = Ui::CreateChild( menu, st::popupMenuWithIcons); const auto handler = [=, id = passkey.id] { ctrl->show(Ui::MakeConfirmBox({ .text = rpl::combine( tr::lng_settings_passkeys_delete_sure_about(), tr::lng_settings_passkeys_delete_sure_about2() ) | rpl::map([](QString a, QString b) { return a + "\n\n" + b; }), .confirmed = [=](Fn close) { session->passkeys().deletePasskey( id, close, [](QString) {}); }, .confirmText = tr::lng_box_delete(), .confirmStyle = &st::attentionBoxButton, .title = tr::lng_settings_passkeys_delete_sure_title(), })); }; Ui::Menu::CreateAddActionCallback(popup)({ .text = tr::lng_proxy_menu_delete(tr::now), .handler = handler, .icon = &st::menuIconDeleteAttention, .isAttention = true, }); popup->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); const auto menuGlobal = menu->mapToGlobal( QPoint(menu->width(), menu->height())); popup->popup(menuGlobal); }); button->widthValue() | rpl::on_next([=](int width) { menu->moveToRight(0, (st.height - menu->height()) / 2, width); }, button->lifetime()); const auto iconSize = st::settingsIconPasskeys.width(); const auto emoji = iconSize; const auto iconLeft = st::settingsButton.iconLeft; auto emojiInstance = passkey.softwareEmojiId ? session->data().customEmojiManager().create( passkey.softwareEmojiId, [=] { button->update(); }, Data::CustomEmojiSizeTag::Large, emoji) : nullptr; const auto emojiPtr = emojiInstance.get(); button->lifetime().add([emoji = std::move(emojiInstance)] {}); const auto formatDateTime = [](TimeId timestamp) { const auto dt = base::unixtime::parse(timestamp); return tr::lng_mediaview_date_time( tr::now, lt_date, langDayOfMonthFull(dt.date()), lt_time, QLocale().toString(dt.time(), QLocale::ShortFormat)); }; const auto date = (passkey.lastUsageDate > 0) ? tr::lng_settings_passkeys_last_used( tr::now, lt_date, formatDateTime(passkey.lastUsageDate)) : tr::lng_settings_passkeys_created( tr::now, lt_date, formatDateTime(passkey.date)); const auto nameText = button->lifetime().make_state< Ui::Text::String>( *nameStyle, passkey.name.isEmpty() ? tr::lng_settings_passkey_unknown(tr::now) : passkey.name); const auto dateText = button->lifetime().make_state< Ui::Text::String>(st::defaultTextStyle, date); button->paintOn([=](QPainter &p) { const auto iconTop = (st.height - iconSize) / 2; if (emojiPtr) { emojiPtr->paint(p, { .textColor = st.nameFg->c, .now = crl::now(), .position = QPoint(iconLeft, iconTop), }); } else { const auto w = button->width(); st::settingsIconPasskeys.paint(p, iconLeft, iconTop, w); } const auto textLeft = st::settingsButton.padding.left(); const auto textWidth = button->width() - textLeft - st::settingsButton.padding.right(); p.setPen(st.nameFg); nameText->draw(p, { .position = { textLeft, st.namePosition.y() }, .outerWidth = button->width(), .availableWidth = textWidth, .elisionLines = 1, }); p.setPen(st.statusFg); dateText->draw(p, { .position = { textLeft, st.statusPosition.y() }, .outerWidth = button->width(), .availableWidth = textWidth, .elisionLines = 1, }); }); button->showChildren(); } passkeysListContainer->showChildren(); passkeysListContainer->resizeToWidth(container->width()); }; session->passkeys().requestList( ) | rpl::on_next(rebuild, container->lifetime()); rebuild(); }; const SectionBuildMethod buildMethod = [=]( not_null container, not_null controller, Fn showOther, rpl::producer<> showFinished) { const auto isPaused = Window::PausedIn( controller, Window::GifPauseReason::Layer); auto builder = SectionBuilder(WidgetContext{ .container = container, .controller = controller, .showOther = std::move(showOther), .isPaused = isPaused, }); BuildPasskeysSection(builder, addButton, setupList); }; build(_container, buildMethod); widthValue( ) | rpl::on_next([=](int width) { _container->resizeToWidth(width); }, _container->lifetime()); _container->heightValue( ) | rpl::on_next([=](int height) { resize(width(), height); }, _container->lifetime()); } } // namespace void PasskeysNoneBox( not_null box, not_null<::Main::Session*> session) { box->setWidth(st::boxWideWidth); box->setNoContentMargin(true); box->setCloseByEscape(true); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); const auto content = box->verticalLayout().get(); Ui::AddSkip(content); { const auto &size = st::settingsCloudPasswordIconSize; auto icon = CreateLottieIcon( content, { .name = u"passkeys"_q, .sizeOverride = { size, size } }, st::settingLocalPasscodeIconPadding); const auto animate = std::move(icon.animate); box->addRow(std::move(icon.widget), style::al_top); box->showFinishes() | rpl::take(1) | rpl::on_next([=] { animate(anim::repeat::once); }, content->lifetime()); } Ui::AddSkip(content); Ui::AddSkip(content); box->addRow( object_ptr( content, tr::lng_settings_passkeys_none_title(), st::boxTitle), style::al_top); Ui::AddSkip(content); box->addRow( object_ptr( content, tr::lng_settings_passkeys_none_about(), st::channelEarnLearnDescription), style::al_top); Ui::AddSkip(content); Ui::AddSkip(content); Ui::AddSkip(content); { const auto padding = QMargins( st::settingsButton.padding.left(), st::boxRowPadding.top(), st::boxRowPadding.right(), st::boxRowPadding.bottom()); const auto iconLeft = st::settingsButton.iconLeft; const auto addEntry = [&]( rpl::producer title, rpl::producer about, const style::icon &icon) { const auto top = content->add( object_ptr( content, std::move(title), st::channelEarnSemiboldLabel), padding); Ui::AddSkip(content, st::channelEarnHistoryThreeSkip); content->add( object_ptr( content, std::move(about), st::channelEarnHistoryRecipientLabel), padding); const auto left = Ui::CreateChild( box->verticalLayout().get()); left->paintRequest( ) | rpl::on_next([=] { auto p = Painter(left); icon.paint(p, 0, 0, left->width()); }, left->lifetime()); left->resize(icon.size()); top->geometryValue( ) | rpl::on_next([=](const QRect &g) { left->moveToLeft( iconLeft, g.top() + st::channelEarnHistoryThreeSkip); }, left->lifetime()); }; addEntry( tr::lng_settings_passkeys_none_info1_title(), tr::lng_settings_passkeys_none_info1_about(), st::settingsIconPasskeysAboutIcon1); Ui::AddSkip(content); Ui::AddSkip(content); addEntry( tr::lng_settings_passkeys_none_info2_title(), tr::lng_settings_passkeys_none_info2_about(), st::settingsIconPasskeysAboutIcon2); Ui::AddSkip(content); Ui::AddSkip(content); addEntry( tr::lng_settings_passkeys_none_info3_title(), tr::lng_settings_passkeys_none_info3_about(), st::settingsIconPasskeysAboutIcon3); Ui::AddSkip(content); Ui::AddSkip(content); } Ui::AddSkip(content); Ui::AddSkip(content); { const auto &st = st::premiumPreviewDoubledLimitsBox; const auto canRegister = session->passkeys().canRegister(); box->setStyle(st); auto button = object_ptr( box, canRegister ? tr::lng_settings_passkeys_none_button() : tr::lng_settings_passkeys_none_button_unsupported(), st::defaultActiveButton); const auto createButton = button.data(); button->resizeToWidth(box->width() - st.buttonPadding.left() - st.buttonPadding.left()); button->setClickedCallback([=] { session->passkeys().initRegistration([=]( const Data::Passkey::RegisterData &data) { Platform::WebAuthn::RegisterKey(data, [=]( Platform::WebAuthn::RegisterResult result) { if (!result.success) { using Error = Platform::WebAuthn::Error; if (result.error == Error::UnsignedBuild) { box->uiShow()->showToast( tr::lng_settings_passkeys_unsigned_error( tr::now)); } return; } session->passkeys().registerPasskey(result, [=] { box->closeBox(); }); }); }); }); if (!canRegister) { button->setAttribute(Qt::WA_TransparentForMouseEvents); button->setTextFgOverride( anim::with_alpha(button->st().textFg->c, 0.5)); } box->addButton(std::move(button)); box->showFinishes( ) | rpl::take(1) | rpl::on_next([=] { if (const auto window = Core::App().findWindow(box)) { window->checkHighlightControl( u"passkeys/create"_q, createButton, { .color = &st::activeButtonFg, .opacity = 0.6, .rippleShape = true, .scroll = false, }); } }, box->lifetime()); } } Type PasskeysId() { return Passkeys::Id(); } namespace Builder { SectionBuildMethod PasskeysSection = kMeta.build; } // namespace Builder } // namespace Settings