Skip to content

Commit

Permalink
[desktop promo] iOS Password Promo view with variants support
Browse files Browse the repository at this point in the history
iOS promo on desktop. This is the view (bubble) that will be created
contextually when a user saves a password and is deemed elligible for
the promo. There are two variants of the view, one with a QR code and
one with a button. A follow-up CL will include the triggering logic and
button action. Note: there is still a TODO for the QR code URL that I
will clean up before merging.

Bug: 1435035
Change-Id: Iff88158e14930818bda5a4faa4efbb9c3a3ca21d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4568451
Reviewed-by: Allen Bauer <kylixrd@chromium.org>
Reviewed-by: Marc Treib <treib@chromium.org>
Reviewed-by: Dana Fried <dfried@chromium.org>
Commit-Queue: Nicolas MacBeth <nicolasmacbeth@google.com>
Cr-Commit-Position: refs/heads/main@{#1153593}
  • Loading branch information
Nicolas-MacBeth authored and Chromium LUCI CQ committed Jun 6, 2023
1 parent 152f1a2 commit a4f04f8
Show file tree
Hide file tree
Showing 13 changed files with 449 additions and 0 deletions.
37 changes: 37 additions & 0 deletions chrome/app/generated_resources.grd
Original file line number Diff line number Diff line change
Expand Up @@ -8196,6 +8196,43 @@ Keep your key file in a safe place. You will need it to create new versions of y
</if>
</if>

<!--iOS password promo strings-->
<if expr="_google_chrome">
<if expr="not is_android">
<if expr="use_titlecase">
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_TITLE" desc="In Title Case: The title for the iOS promo bubble letting users know their password is saved.">
Your Password Is Saved
</message>
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_FOOTER_TITLE" desc="In Title Case: The title for the footer of the iOS promo bubble suggesting to users that they can use their passwords on their iOS devices.">
Use Your Passwords on Your iOS Devices
</message>
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_BUTTON" desc="In Title Case: The text inside the button inviting the user to get started.">
Get Started
</message>
</if>
<if expr="not use_titlecase">
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_TITLE" desc="The title for the iOS promo bubble letting users know their password is saved.">
Your password is saved
</message>
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_FOOTER_TITLE" desc="The title for the footer of the iOS promo bubble suggesting to users that they can use their passwords on their iOS devices.">
Use your passwords on your iOS devices
</message>
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_BUTTON" desc="The text inside the button inviting the user to get started.">
Get started
</message>
</if>
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_SUBTITLE" desc="The subtitle for the iOS promo bubble which lets the user know they can access their saved password on the Google Password Manager.">
You can access it on Google Password Manager.
</message>
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_FOOTER_DESCRIPTION_GENERIC" desc="The description for the iOS promo bubble footer which lets the user know how to use their saved passwords on their phone: download Chrome for iOS and sync their account.">
To use your saved passwords on your phone, download Chrome for iOS and sync your account.
</message>
<message name="IDS_IOS_PASSWORD_PROMO_BUBBLE_FOOTER_DESCRIPTION_QR" desc="The description for the iOS promo bubble footer which lets the user know how to use their saved passwords on their phone: follow the QR code, download Chrome for iOS and sync their account.">
To use your saved passwords on your phone, follow the QR code, download Chrome for iOS and sync your account.
</message>
</if>
</if>

<!--Accessible name/action strings-->
<message name="IDS_ACCESSIBLE_INCOGNITO_WINDOW_TITLE_FORMAT" desc="The format for the accessible title of an Incognito window">
<ph name="WINDOW_TITLE">$1</ph> (Incognito)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
53267bca8ec78a177694c438a5738b24c73443c4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
649d97c62a056c3f9dd2b5508e9ea83378cc4df2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
d7b12fa0f544d615d843dc92de334214e946033d
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b6afce069f65f1c64ff44a432011346f99732e16
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bf730fa73a0a4b3dc81c563351ce507957a26682
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
89a9b77f4771f3d31a798ee7597bab7b36799be4
5 changes: 5 additions & 0 deletions chrome/app/theme/theme_resources.grd
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@
<structure type="chrome_scaled_image" name="IDR_TAILORED_SECURITY_UNCONSENTED" file="common/tailored_security_unconsented.png" />
<structure type="chrome_scaled_image" name="IDR_TAILORED_SECURITY_UNCONSENTED_UPDATED" file="common/safer_with_google_shield.png" />
</if>
<if expr="_google_chrome">
<if expr="not is_android">
<structure type="chrome_scaled_image" name="IDR_SUCCESS_GREEN_CHECKMARK" file="google_chrome/success_green_checkmark.png" />
</if>
</if>
</structures>
</release>
</grit>
7 changes: 7 additions & 0 deletions chrome/browser/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -5688,6 +5688,13 @@ static_library("ui") {

allow_circular_includes_from += [ "//chrome/browser/ui/views" ]

if (is_chrome_branded && !is_android) {
sources += [
"views/promos/ios_promo_password_bubble.cc",
"views/promos/ios_promo_password_bubble.h",
]
}

if (is_linux || is_chromeos_lacros) {
sources += [
"views/chrome_views_delegate_linux.cc",
Expand Down
291 changes: 291 additions & 0 deletions chrome/browser/ui/views/promos/ios_promo_password_bubble.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/views/promos/ios_promo_password_bubble.h"
#include <memory>
#include "base/functional/bind.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "chrome/services/qrcode_generator/public/cpp/qrcode_generator_service.h"
#include "content/public/browser/page_navigator.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

namespace constants {
// Margin for QR code image view.
constexpr int kQrCodeMargin = 20;

// Size of QR code image view.
constexpr int kQrCodeImageSize = 100;

// URL used for the QR code within the promo
const char kQRCodeURL[] =
"https://itunes.apple.com/app/apple-store/"
"id535886823?pt=9008&ct=saved-passwords-ios-promo-direct-qr&mt=8";

// URL used for the new tab opened by clicking the "Get Started" button.
const char kGetStartedButtonURL[] = "https://google.com/chrome/chrome-for-ios";
} // namespace constants

DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(IOSPromoPasswordBubble, kQRCodeView);

// Pointer to BubbleDialogDelegate instance.
views::BubbleDialogDelegate* ios_promo_password_delegate_ = nullptr;

class IOSPromoPasswordBubbleDelegate : public ui::DialogModelDelegate {
public:
explicit IOSPromoPasswordBubbleDelegate(Browser* browser)
: browser_(browser) {
qr_code_service_ = nullptr;
}

// Returns a new QR code generator service if one does not yet exist.
qrcode_generator::QRImageGenerator& GetQRCodeGenerator() {
if (!qr_code_service_) {
qr_code_service_ = std::make_unique<qrcode_generator::QRImageGenerator>();
}
return *qr_code_service_;
}

// Handler for when the window closes.
void OnWindowClosing() {
ios_promo_password_delegate_ = nullptr;
qr_code_service_ = nullptr;
}

// Callback passed to QR code generation for populating the QR code image in
// the UI.
void OnQrCodeGenerated(
const qrcode_generator::mojom::GenerateQRCodeResponsePtr response) {
DCHECK(response->error_code ==
qrcode_generator::mojom::QRCodeGeneratorError::NONE);

auto qr_code_views = views::ElementTrackerViews::GetInstance()
->GetAllMatchingViewsInAnyContext(
IOSPromoPasswordBubble::kQRCodeView);

// There should only be one promo at a time.
DCHECK(qr_code_views.size() == 1);

views::ImageView* image_view =
views::AsViewClass<views::ImageView>(qr_code_views.front());

image_view->SetImage(gfx::ImageSkia::CreateFrom1xBitmap(response->bitmap));
}

// Callback for when the "Get started" button is clicked.
void OnGetStartedButtonClicked() {
browser_->OpenURL(content::OpenURLParams(
GURL(constants::kGetStartedButtonURL), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false));

ios_promo_password_delegate_->GetWidget()->Close();
}

private:
// Pointer to the QR code generator service.
std::unique_ptr<qrcode_generator::QRImageGenerator> qr_code_service_;

// Pointer to the current Browser;
std::unique_ptr<Browser> browser_;
};

// CreateFooter creates the view that is inserted as footer to the bubble.
std::unique_ptr<views::View> CreateFooter(
IOSPromoPasswordBubble::PromoVariant variant,
IOSPromoPasswordBubbleDelegate* bubble_delegate) {
views::LayoutProvider* provider = views::LayoutProvider::Get();

auto footer_title_container =
views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kHorizontal)
.AddChild(views::Builder<views::ImageView>()
.SetImage(ui::ResourceBundle::GetSharedInstance()
.GetImageSkiaNamed(IDR_PRODUCT_LOGO_32))
.SetImageSize(gfx::Size(20, 20)))
.AddChild(views::Builder<views::Label>()
.SetText(l10n_util::GetStringUTF16(
IDS_IOS_PASSWORD_PROMO_BUBBLE_FOOTER_TITLE))
.SetTextStyle(views::style::STYLE_PRIMARY)
.SetMultiLine(true)
.SetHorizontalAlignment(
gfx::HorizontalAlignment::ALIGN_TO_HEAD))
.SetBetweenChildSpacing(provider->GetDistanceMetric(
views::DistanceMetric::
DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING))
.SetMainAxisAlignment(views::BoxLayout::MainAxisAlignment::kStart);

auto footer_view =
views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kVertical)
.SetMainAxisAlignment(views::BoxLayout::MainAxisAlignment::kStart)
.SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStretch)
.SetBetweenChildSpacing(provider->GetDistanceMetric(
views::DistanceMetric::
DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_TEXT));

if (variant ==
IOSPromoPasswordBubble::PromoVariant::GET_STARTED_BUTTON_VARIANT) {
auto footer_description =
views::Builder<views::Label>()
.SetText(l10n_util::GetStringUTF16(
IDS_IOS_PASSWORD_PROMO_BUBBLE_FOOTER_DESCRIPTION_GENERIC))
.SetTextContext(ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL)
.SetTextStyle(views::style::STYLE_SECONDARY)
.SetMultiLine(true)
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_TO_HEAD);

auto button_callback = base::BindRepeating(
&IOSPromoPasswordBubbleDelegate::OnGetStartedButtonClicked,
base::Unretained(bubble_delegate));
auto footer_button_container =
views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kHorizontal)
.SetMainAxisAlignment(views::BoxLayout::MainAxisAlignment::kEnd)
.AddChild(views::Builder<views::MdTextButton>()
.SetText(l10n_util::GetStringUTF16(
IDS_IOS_PASSWORD_PROMO_BUBBLE_BUTTON))
.SetIsDefault(true)
.SetCallback(button_callback));

return std::move(footer_view.AddChild(footer_title_container)
.AddChild(footer_description)
.AddChild(footer_button_container))
.Build();

} else if (variant == IOSPromoPasswordBubble::PromoVariant::QR_CODE_VARIANT) {
qrcode_generator::mojom::GenerateQRCodeRequestPtr request =
qrcode_generator::mojom::GenerateQRCodeRequest::New();
request->data = std::string(constants::kQRCodeURL);
request->center_image = qrcode_generator::mojom::CenterImage::CHROME_DINO;

request->render_module_style =
qrcode_generator::mojom::ModuleStyle::CIRCLES;
request->render_locator_style =
qrcode_generator::mojom::LocatorStyle::ROUNDED;

auto callback =
base::BindOnce(&IOSPromoPasswordBubbleDelegate::OnQrCodeGenerated,
base::Unretained(bubble_delegate));
bubble_delegate->GetQRCodeGenerator().GenerateQRCode(std::move(request),
std::move(callback));
views::Label* footer_qr_description_view_ptr = nullptr;
auto footer_content_container =
views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kHorizontal)
.SetMainAxisAlignment(views::BoxLayout::MainAxisAlignment::kStart)
.SetCrossAxisAlignment(
views::BoxLayout::CrossAxisAlignment::kCenter)
.AddChild(
views::Builder<views::Label>()
.SetText(l10n_util::GetStringUTF16(
IDS_IOS_PASSWORD_PROMO_BUBBLE_FOOTER_DESCRIPTION_QR))
.SetTextContext(
ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL)
.SetTextStyle(views::style::STYLE_SECONDARY)
.SetMultiLine(true)
.SetHorizontalAlignment(
gfx::HorizontalAlignment::ALIGN_TO_HEAD)
.CopyAddressTo(&footer_qr_description_view_ptr))
.AddChild(
views::Builder<views::ImageView>()
.SetHorizontalAlignment(
views::ImageView::Alignment::kCenter)
.SetHorizontalAlignment(
views::ImageView::Alignment::kCenter)
.SetImageSize(gfx::Size(constants::kQrCodeImageSize,
constants::kQrCodeImageSize))
.SetPreferredSize(gfx::Size(constants::kQrCodeImageSize,
constants::kQrCodeImageSize) +
gfx::Size(constants::kQrCodeMargin,
constants::kQrCodeMargin))
.SetProperty(views::kElementIdentifierKey,
IOSPromoPasswordBubble::kQRCodeView)
.SetVisible(true)
.SetBackground(views::CreateSolidBackground(SK_ColorWHITE)))
.AfterBuild(base::BindOnce(
[](views::Label* footer_description_view_ptr,
views::BoxLayoutView* view) {
view->SetFlexForView(footer_description_view_ptr, 1);
},
footer_qr_description_view_ptr));

return std::move(footer_view.AddChild(footer_title_container)
.AddChild(footer_content_container))
.Build();
} else {
NOTREACHED_NORETURN();
}
}

// static
void IOSPromoPasswordBubble::ShowBubble(views::View* anchor_view,
PageActionIconView* highlighted_button,
PromoVariant variant,
Browser* browser) {
if (ios_promo_password_delegate_) {
return;
}

auto bubble_delegate_unique =
std::make_unique<IOSPromoPasswordBubbleDelegate>(browser);
IOSPromoPasswordBubbleDelegate* bubble_delegate =
bubble_delegate_unique.get();

auto dialog_model_builder =
ui::DialogModel::Builder(std::move(bubble_delegate_unique));

dialog_model_builder.SetDialogDestroyingCallback(
base::BindOnce(&IOSPromoPasswordBubbleDelegate::OnWindowClosing,
base::Unretained(bubble_delegate)));

ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
auto banner_image = ui::ImageModel::FromImageSkia(
*bundle.GetImageSkiaNamed(IDR_SUCCESS_GREEN_CHECKMARK));
dialog_model_builder.SetBannerImage(banner_image);

dialog_model_builder.SetTitle(
l10n_util::GetStringUTF16(IDS_IOS_PASSWORD_PROMO_BUBBLE_TITLE));

auto subtitle = std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_IOS_PASSWORD_PROMO_BUBBLE_SUBTITLE),
ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL,
views::style::STYLE_SECONDARY);
subtitle->SetMultiLine(true);
subtitle->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_TO_HEAD);

dialog_model_builder.AddCustomField(
std::make_unique<views::BubbleDialogModelHost::CustomView>(
std::move(subtitle), views::BubbleDialogModelHost::FieldType::kText));

auto promo_bubble = std::make_unique<views::BubbleDialogModelHost>(
dialog_model_builder.Build(), anchor_view,
views::BubbleBorder::TOP_RIGHT);

ios_promo_password_delegate_ = promo_bubble.get();

promo_bubble->SetHighlightedButton(highlighted_button);
promo_bubble->SetFootnoteView(CreateFooter(variant, bubble_delegate));

views::Widget* const widget =
views::BubbleDialogDelegate::CreateBubble(std::move(promo_bubble));
widget->Show();
}

// static
void IOSPromoPasswordBubble::Hide() {
if (ios_promo_password_delegate_) {
ios_promo_password_delegate_->GetWidget()->Close();
}
}

0 comments on commit a4f04f8

Please sign in to comment.