From 59606b8b880477456ddc0dbc480b6f0b3ea21537 Mon Sep 17 00:00:00 2001 From: zenparsing Date: Thu, 3 Oct 2024 13:08:51 -0400 Subject: [PATCH] [Rewards 3.0] Add adaptive captcha modal --- browser/ui/BUILD.gn | 2 +- .../brave_tooltip_popup_handler.cc | 7 +- .../brave_rewards/rewards_page_data_source.cc | 17 +++ .../brave_rewards/rewards_page_handler.cc | 78 +++++++++-- .../brave_rewards/rewards_page_handler.h | 10 +- .../common/mojom/rewards_page.mojom | 36 ++++- .../components/account_balance.tsx | 26 ++++ .../resources/rewards_page/components/app.tsx | 18 ++- .../components/captcha_modal.style.ts | 51 +++++++ .../rewards_page/components/captcha_modal.tsx | 131 ++++++++++++++++++ .../contribute/contribute_modal.style.ts | 7 +- .../components/contribute/payment_form.tsx | 6 +- .../contribute/transfer_error.style.ts | 7 +- .../home/ads_history_modal.style.ts | 14 ++ .../components/home/payout_account_card.tsx | 10 +- .../rewards_page/components/modal.style.ts | 6 +- .../self_custody_invite_modal.style.ts | 9 ++ .../components/self_custody_invite_modal.tsx | 6 +- .../resources/rewards_page/lib/app_model.ts | 14 +- .../rewards_page/lib/locale_strings.ts | 5 + .../resources/rewards_page/lib/webui_model.ts | 42 ++++-- .../rewards_page/stories/storybook_model.ts | 11 +- 22 files changed, 459 insertions(+), 54 deletions(-) create mode 100644 components/brave_rewards/resources/rewards_page/components/account_balance.tsx create mode 100644 components/brave_rewards/resources/rewards_page/components/captcha_modal.style.ts create mode 100644 components/brave_rewards/resources/rewards_page/components/captcha_modal.tsx diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index a839f4f6c796..1fa757d73a6e 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -751,6 +751,7 @@ source_set("ui") { "//brave/common", "//brave/components/ai_chat/core/common/buildflags", "//brave/components/ai_rewriter/common/buildflags", + "//brave/components/brave_adaptive_captcha", "//brave/components/brave_adblock_ui:generated_resources", "//brave/components/brave_adblock_ui/adblock_internals:generated_resources", "//brave/components/brave_ads/browser", @@ -1030,7 +1031,6 @@ source_set("ui") { "//brave/browser/brave_wallet", "//brave/browser/resources/settings:resources", "//brave/common/importer", - "//brave/components/brave_adaptive_captcha", "//brave/components/brave_new_tab_ui:generated_resources", "//brave/components/brave_new_tab_ui:mojom", "//brave/components/brave_news/browser", diff --git a/browser/ui/views/brave_tooltips/brave_tooltip_popup_handler.cc b/browser/ui/views/brave_tooltips/brave_tooltip_popup_handler.cc index bda93fcff119..5f048976de9e 100644 --- a/browser/ui/views/brave_tooltips/brave_tooltip_popup_handler.cc +++ b/browser/ui/views/brave_tooltips/brave_tooltip_popup_handler.cc @@ -31,9 +31,10 @@ void BraveTooltipPopupHandler::Show(Profile* profile, DCHECK(tooltip); const std::string tooltip_id = tooltip->id(); - DCHECK(!tooltip_popups_[tooltip_id]); - tooltip_popups_[tooltip_id] = - new brave_tooltips::BraveTooltipPopup(profile, std::move(tooltip)); + if (!tooltip_popups_[tooltip_id]) { + tooltip_popups_[tooltip_id] = + new brave_tooltips::BraveTooltipPopup(profile, std::move(tooltip)); + } } // static diff --git a/browser/ui/webui/brave_rewards/rewards_page_data_source.cc b/browser/ui/webui/brave_rewards/rewards_page_data_source.cc index 2f79d0ced7ef..e8c4f3b00681 100644 --- a/browser/ui/webui/brave_rewards/rewards_page_data_source.cc +++ b/browser/ui/webui/brave_rewards/rewards_page_data_source.cc @@ -8,6 +8,7 @@ #include #include "base/feature_list.h" +#include "brave/components/brave_adaptive_captcha/server_util.h" #include "brave/components/brave_rewards/common/features.h" #include "brave/components/brave_rewards/resources/grit/brave_rewards_resources.h" #include "brave/components/brave_rewards/resources/grit/rewards_page_generated_map.h" @@ -138,6 +139,13 @@ static constexpr webui::LocalizedString kStrings[] = { {"benefitsStoreText", IDS_REWARDS_BENEFITS_STORE_TEXT}, {"benefitsTitle", IDS_REWARDS_BENEFITS_TITLE}, {"cancelButtonLabel", IDS_REWARDS_PANEL_CANCEL}, + {"captchaMaxAttemptsExceededText", + IDS_REWARDS_CAPTCHA_MAX_ATTEMPTS_EXCEEDED_TEXT}, + {"captchaMaxAttemptsExceededTitle", + IDS_REWARDS_CAPTCHA_MAX_ATTEMPTS_EXCEEDED_TITLE}, + {"captchaSolvedText", IDS_REWARDS_CAPTCHA_SOLVED_TEXT}, + {"captchaSolvedTitle", IDS_REWARDS_CAPTCHA_SOLVED_TITLE}, + {"captchaSupportButtonLabel", IDS_REWARDS_CAPTCHA_CONTACT_SUPPORT}, {"closeButtonLabel", IDS_BRAVE_REWARDS_ONBOARDING_CLOSE}, {"connectAccountSubtext", IDS_REWARDS_CONNECT_ACCOUNT_SUBTEXT}, {"connectAccountText", IDS_REWARDS_CONNECT_ACCOUNT_TEXT_2}, @@ -268,6 +276,15 @@ void CreateAndAddRewardsPageDataSource(content::WebUI& web_ui, source, base::make_span(kRewardsPageGenerated, kRewardsPageGeneratedSize), IDR_NEW_BRAVE_REWARDS_PAGE_HTML); + // Adaptive captcha challenges are displayed in an iframe on the Rewards + // panel. In order to display these challenges we need to specify in CSP that + // frames can be loaded from the adaptive captcha server URL. + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ChildSrc, + "frame-src 'self' " + + brave_adaptive_captcha::ServerUtil::GetInstance()->GetServerUrl("/") + + ";"); + // Override img-src to allow chrome://rewards-image support. source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::ImgSrc, diff --git a/browser/ui/webui/brave_rewards/rewards_page_handler.cc b/browser/ui/webui/brave_rewards/rewards_page_handler.cc index 5d378f818916..d001750613ff 100644 --- a/browser/ui/webui/brave_rewards/rewards_page_handler.cc +++ b/browser/ui/webui/brave_rewards/rewards_page_handler.cc @@ -15,8 +15,10 @@ #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/scoped_observation.h" +#include "brave/browser/brave_adaptive_captcha/brave_adaptive_captcha_service_factory.h" #include "brave/browser/brave_ads/ads_service_factory.h" #include "brave/browser/brave_rewards/rewards_service_factory.h" +#include "brave/components/brave_adaptive_captcha/brave_adaptive_captcha_service.h" #include "brave/components/brave_ads/browser/ads_service.h" #include "brave/components/brave_ads/core/public/ads_util.h" #include "brave/components/brave_ads/core/public/history/ad_history_feature.h" @@ -43,6 +45,8 @@ namespace brave_rewards { namespace { +using brave_adaptive_captcha::BraveAdaptiveCaptchaServiceFactory; + static constexpr auto kPluralStrings = base::MakeFixedFlatMap( {{"unconnectedAdsViewedText", @@ -198,6 +202,8 @@ RewardsPageHandler::RewardsPageHandler( bubble_delegate_(std::move(bubble_delegate)), rewards_service_(RewardsServiceFactory::GetForProfile(profile)), ads_service_(brave_ads::AdsServiceFactory::GetForProfile(profile)), + captcha_service_( + BraveAdaptiveCaptchaServiceFactory::GetForProfile(profile)), prefs_(profile->GetPrefs()) { CHECK(rewards_service_); CHECK(ads_service_); @@ -469,7 +475,38 @@ void RewardsPageHandler::GetAdsSettings(GetAdsSettingsCallback callback) { } void RewardsPageHandler::GetAdsStatement(GetAdsStatementCallback callback) { - ads_service_->GetStatementOfAccounts(std::move(callback)); + auto on_statement = [](decltype(callback) callback, + brave_ads::mojom::StatementInfoPtr info) { + if (!info) { + std::move(callback).Run(nullptr); + return; + } + + auto statement = mojom::AdsStatement::New(); + + statement->min_earnings_previous_month = info->min_earnings_previous_month; + statement->max_earnings_previous_month = info->max_earnings_previous_month; + statement->min_earnings_this_month = info->min_earnings_this_month; + statement->max_earnings_this_month = info->max_earnings_this_month; + statement->next_payment_date = info->next_payment_date; + statement->ads_received_this_month = info->ads_received_this_month; + statement->ad_type_summary_this_month = mojom::AdTypeSummary::New(); + + auto& summary = statement->ad_type_summary_this_month; + auto& ad_type_map = info->ads_summary_this_month; + + using AdType = brave_ads::mojom::AdType; + + summary->notification_ads = ad_type_map[AdType::kNotificationAd]; + summary->new_tab_page_ads = ad_type_map[AdType::kNewTabPageAd]; + summary->inline_content_ads = ad_type_map[AdType::kInlineContentAd]; + summary->search_result_ads = ad_type_map[AdType::kSearchResultAd]; + + std::move(callback).Run(std::move(statement)); + }; + + ads_service_->GetStatementOfAccounts( + base::BindOnce(on_statement, std::move(callback))); } void RewardsPageHandler::GetAdsHistory(GetAdsHistoryCallback callback) { @@ -557,10 +594,8 @@ void RewardsPageHandler::ToggleAdLike(const std::string& history_item, // reactions to use `mojom::ReactionInfo` instead of `AdHistoryItemInfo`. const brave_ads::AdHistoryItemInfo ad_history_item = brave_ads::AdHistoryItemFromValue(*dict); - brave_ads::mojom::ReactionInfoPtr mojom_reaction = - brave_ads::CreateReaction(ad_history_item); - ads_service_->ToggleLikeAd(std::move(mojom_reaction), + ads_service_->ToggleLikeAd(brave_ads::CreateReaction(ad_history_item), base::IgnoreArgs(std::move(callback))); } @@ -576,10 +611,8 @@ void RewardsPageHandler::ToggleAdDislike(const std::string& history_item, // reactions to use `mojom::ReactionInfo` instead of `AdHistoryItemInfo`. const brave_ads::AdHistoryItemInfo ad_history_item = brave_ads::AdHistoryItemFromValue(*dict); - brave_ads::mojom::ReactionInfoPtr mojom_reaction = - brave_ads::CreateReaction(ad_history_item); - ads_service_->ToggleDislikeAd(std::move(mojom_reaction), + ads_service_->ToggleDislikeAd(brave_ads::CreateReaction(ad_history_item), base::IgnoreArgs(std::move(callback))); } @@ -596,11 +629,10 @@ void RewardsPageHandler::ToggleAdInappropriate( // reactions to use `mojom::ReactionInfo` instead of `AdHistoryItemInfo`. const brave_ads::AdHistoryItemInfo ad_history_item = brave_ads::AdHistoryItemFromValue(*dict); - brave_ads::mojom::ReactionInfoPtr mojom_reaction = - brave_ads::CreateReaction(ad_history_item); ads_service_->ToggleMarkAdAsInappropriate( - std::move(mojom_reaction), base::IgnoreArgs(std::move(callback))); + brave_ads::CreateReaction(ad_history_item), + base::IgnoreArgs(std::move(callback))); } void RewardsPageHandler::EnableRewards(const std::string& country_code, @@ -629,6 +661,32 @@ void RewardsPageHandler::SendContribution(const std::string& creator_id, std::move(callback)); } +void RewardsPageHandler::GetCaptchaInfo(GetCaptchaInfoCallback callback) { + if (!captcha_service_) { + std::move(callback).Run(nullptr); + return; + } + + auto info = mojom::CaptchaInfo::New(); + captcha_service_->GetScheduledCaptchaInfo(&info->url, + &info->max_attempts_exceeded); + + if (info->url.empty()) { + info = nullptr; + } + + std::move(callback).Run(std::move(info)); +} + +void RewardsPageHandler::OnCaptchaResult(bool success, + OnCaptchaResultCallback callback) { + if (captcha_service_) { + captcha_service_->UpdateScheduledCaptchaResult(success); + } + ads_service_->NotifyDidSolveAdaptiveCaptcha(); + std::move(callback).Run(); +} + void RewardsPageHandler::ResetRewards(ResetRewardsCallback callback) { rewards_service_->CompleteReset(std::move(callback)); } diff --git a/browser/ui/webui/brave_rewards/rewards_page_handler.h b/browser/ui/webui/brave_rewards/rewards_page_handler.h index 9912973a64e9..fa7663e89748 100644 --- a/browser/ui/webui/brave_rewards/rewards_page_handler.h +++ b/browser/ui/webui/brave_rewards/rewards_page_handler.h @@ -19,6 +19,10 @@ class PrefService; class Profile; +namespace brave_adaptive_captcha { +class BraveAdaptiveCaptchaService; +} + namespace brave_ads { class AdsService; } @@ -93,7 +97,7 @@ class RewardsPageHandler : public mojom::RewardsPageHandler { void GetAdsSettings(GetAdsSettingsCallback callback) override; void GetAdsStatement(GetAdsStatementCallback callback) override; void GetAdsHistory(GetAdsHistoryCallback callback) override; - void SetAdTypeEnabled(brave_ads::mojom::AdType mojom_ad_type, + void SetAdTypeEnabled(brave_ads::mojom::AdType ad_type, bool enabled, SetAdTypeEnabledCallback callback) override; void SetNotificationAdsPerHour( @@ -120,6 +124,8 @@ class RewardsPageHandler : public mojom::RewardsPageHandler { double amount, bool recurring, SendContributionCallback callback) override; + void GetCaptchaInfo(GetCaptchaInfoCallback callback) override; + void OnCaptchaResult(bool success, OnCaptchaResultCallback callback) override; void ResetRewards(ResetRewardsCallback callback) override; private: @@ -133,6 +139,8 @@ class RewardsPageHandler : public mojom::RewardsPageHandler { std::unique_ptr update_observer_; raw_ptr rewards_service_ = nullptr; raw_ptr ads_service_ = nullptr; + raw_ptr + captcha_service_ = nullptr; raw_ptr prefs_ = nullptr; }; diff --git a/components/brave_rewards/common/mojom/rewards_page.mojom b/components/brave_rewards/common/mojom/rewards_page.mojom index 34e0c8327dbb..40953ba52be1 100644 --- a/components/brave_rewards/common/mojom/rewards_page.mojom +++ b/components/brave_rewards/common/mojom/rewards_page.mojom @@ -7,6 +7,7 @@ module brave_rewards.mojom; import "brave/components/brave_ads/core/mojom/brave_ads.mojom"; import "brave/components/brave_rewards/common/mojom/rewards.mojom"; +import "mojo/public/mojom/base/time.mojom"; // Initializes messaging between the browser and the page. interface RewardsPageHandlerFactory { @@ -51,6 +52,32 @@ struct AdsSettings { array available_subdivisions; }; +struct AdTypeSummary { + int32 notification_ads; + int32 new_tab_page_ads; + int32 inline_content_ads; + int32 search_result_ads; +}; + +// We introduce a new struct instead of using `brave_ads.mojom.StatementInfo` +// directly, because the "ad type summary" map does not currently generate an +// appropriate TypeScript type, and the compiler can fail to detect breaking +// changes to that structure. Instead, we use named fields on `AdTypeSummary`. +struct AdsStatement { + double min_earnings_previous_month; + double max_earnings_previous_month; + double min_earnings_this_month; + double max_earnings_this_month; + mojo_base.mojom.Time next_payment_date; + int32 ads_received_this_month; + AdTypeSummary ad_type_summary_this_month; +}; + +struct CaptchaInfo { + string url; + bool max_attempts_exceeded; +}; + // Browser-side handler for requests from WebUI page. interface RewardsPageHandler { // Notifies the browser that that Rewards page has rendered and is ready to be @@ -123,7 +150,7 @@ interface RewardsPageHandler { GetAdsSettings() => (AdsSettings settings); // Returns information about the current state of ad views. - GetAdsStatement() => (brave_ads.mojom.StatementInfo? statement); + GetAdsStatement() => (AdsStatement? statement); // Returns Ads history for the current user. GetAdsHistory() => (string history); @@ -157,6 +184,13 @@ interface RewardsPageHandler { SendContribution(string creator_id, double amount, bool recurring) => (bool contribution_sent); + // Returns the current adaptive captcha info for the current user. + GetCaptchaInfo() => (CaptchaInfo? captchaInfo); + + // Called when the user completes (with success or failure) an adaptive + // captcha attempt. + OnCaptchaResult(bool success) => (); + // Clears all Rewards-related user state. ResetRewards() => (bool success); }; diff --git a/components/brave_rewards/resources/rewards_page/components/account_balance.tsx b/components/brave_rewards/resources/rewards_page/components/account_balance.tsx new file mode 100644 index 000000000000..f875a429e5be --- /dev/null +++ b/components/brave_rewards/resources/rewards_page/components/account_balance.tsx @@ -0,0 +1,26 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { Optional } from '../../shared/lib/optional' + +const balanceFormatter = new Intl.NumberFormat(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 4 +}) + +interface Props { + balance: Optional +} + +export function AccountBalance(props: Props) { + if (!props.balance.hasValue()) { + return <> + } + return <> + {balanceFormatter.format(props.balance.valueOr(0)) + ' BAT'} + +} diff --git a/components/brave_rewards/resources/rewards_page/components/app.tsx b/components/brave_rewards/resources/rewards_page/components/app.tsx index 135a5ea4f972..9a553b8a9408 100644 --- a/components/brave_rewards/resources/rewards_page/components/app.tsx +++ b/components/brave_rewards/resources/rewards_page/components/app.tsx @@ -17,6 +17,7 @@ import { Onboarding } from './onboarding/onboarding' import { OnboardingSuccessModal } from './onboarding/onboarding_success_modal' import { ConnectAccount } from './connect_account' import { AuthorizationModal } from './authorization_modal' +import { CaptchaModal } from './captcha_modal' import { ContributeModal } from './contribute/contribute_modal' import { ResetModal } from './reset_modal' import { TosUpdateModal } from './tos_update_modal' @@ -35,18 +36,21 @@ export function App() { loading, embedder, paymentId, - tosUpdateRequired + tosUpdateRequired, + captchaInfo, ] = useAppState((state) => [ state.loading, state.embedder, state.paymentId, - state.tosUpdateRequired + state.tosUpdateRequired, + state.captchaInfo ]) const viewType = useBreakpoint() const [showResetModal, setShowResetModal] = React.useState(false) const [showContributeModal, setShowContributeModal] = React.useState(false) + const [hideCaptcha, setHideCaptcha] = React.useState(false) const [showOnboardingSuccess, setShowOnboardingSuccess] = React.useState(false) @@ -127,6 +131,16 @@ export function App() { return setShowContributeModal(false)} /> } + if (captchaInfo && !hideCaptcha) { + return ( + model.onCaptchaResult(success)} + onClose={() => setHideCaptcha(true)} + /> + ) + } + if (tosUpdateRequired) { return ( void + onClose: () => void +} + +export function CaptchaModal(props: Props) { + const tabOpener = React.useContext(TabOpenerContext) + const { getString } = useLocaleContext() + const iframeRef = React.useRef(null) + const [captchaSolved, setCaptchaSolved] = React.useState(false) + + React.useEffect(() => { + const listener = (event: MessageEvent) => { + // Sandboxed iframes which lack the 'allow-same-origin' header have "null" + // rather than a valid origin. + if (event.origin !== 'null') { + return + } + + if (!iframeRef.current) { + return + } + + const { contentWindow } = iframeRef.current + if (!event.source || event.source !== contentWindow || !event.data) { + return + } + + switch (event.data) { + case 'captchaSuccess': + props.onCaptchaResult(true) + setCaptchaSolved(true) + break + case 'captchaFailure': + case 'error': + props.onCaptchaResult(false) + break + } + } + + window.addEventListener('message', listener) + return () => { window.removeEventListener('message', listener) } + }, [props.onCaptchaResult]) + + function renderContent() { + if (captchaSolved) { + return ( +
+
+

{getString('captchaSolvedTitle')}

+

{getString('captchaSolvedText')}

+ +
+ ) + } + + if (props.captchaInfo.maxAttemptsExceeded) { + return ( +
+
+

{getString('captchaMaxAttemptsExceededTitle')}

+

{getString('captchaMaxAttemptsExceededText')}

+ +
+ ) + } + + return ( +