Skip to content

Commit fb1b276

Browse files
authored
Implement new widget prompt in onboarding (#6256)
Task/Issue URL:https://app.asana.com/1/137249556945/project/1200581511062568/task/1210403485354664?focus=true ### Description Added an experimental home screen widget bottom sheet dialog to promote adding the DuckDuckGo search widget to the user's home screen. The dialog includes a visual preview of the widget, explanatory text, and two action buttons: "Add Widget" and "Not Now". ### Steps to test this PR _Home Screen Widget Promotion_ - [x] Install from this branch. - [x] Apply the patch attached to the Asana task. - [x] Go through the Onboarding until you reach the Widget Promo step. - [x] Verify the experimental home screen widget bottom sheet appears when triggered. - [x] Confirm the "Add Widget" button dismisses the dialog and launches the widget addition flow. - [x] Verify the "Not Now" button properly dismisses the dialog. - [x] Check that the dialog shows a preview image of the widget and explanatory text. ### UI changes | Light | Dark | | ------ | ----- | |![Screenshot_20250607_115414](https://github.com/user-attachments/assets/cede9a93-3436-4720-aaf9-3a59c590a2a9)|![Screenshot_20250607_120058](https://github.com/user-attachments/assets/12f099c8-86ed-4ff1-a484-a01145b3ad6d)|
1 parent 6a27ab2 commit fb1b276

File tree

7 files changed

+731
-0
lines changed

7 files changed

+731
-0
lines changed

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames
125125
import com.duckduckgo.app.browser.customtabs.CustomTabViewModel.Companion.CUSTOM_TAB_NAME_PREFIX
126126
import com.duckduckgo.app.browser.databinding.FragmentBrowserTabBinding
127127
import com.duckduckgo.app.browser.databinding.HttpAuthenticationBinding
128+
import com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment.ExperimentalHomeScreenWidgetBottomSheetDialog
128129
import com.duckduckgo.app.browser.downloader.BlobConverterInjector
129130
import com.duckduckgo.app.browser.favicon.FaviconManager
130131
import com.duckduckgo.app.browser.filechooser.FileChooserIntentBuilder
@@ -581,6 +582,7 @@ class BrowserTabFragment :
581582

582583
private lateinit var popupMenu: BrowserPopupMenu
583584
private lateinit var ctaBottomSheet: PromoBottomSheetDialog
585+
private lateinit var experimentalBottomSheet: ExperimentalHomeScreenWidgetBottomSheetDialog
584586

585587
private lateinit var autoCompleteSuggestionsAdapter: BrowserAutoCompleteSuggestionsAdapter
586588

@@ -4394,6 +4396,45 @@ class BrowserTabFragment :
43944396
)
43954397
}
43964398

4399+
private fun showExperimentalHomeWidget(
4400+
configuration: HomePanelCta,
4401+
) {
4402+
hideDaxCta()
4403+
4404+
if (!::experimentalBottomSheet.isInitialized) {
4405+
experimentalBottomSheet = ExperimentalHomeScreenWidgetBottomSheetDialog(
4406+
context = requireContext(),
4407+
isLightModeEnabled = appTheme.isLightModeEnabled(),
4408+
)
4409+
experimentalBottomSheet.eventListener = object : ExperimentalHomeScreenWidgetBottomSheetDialog.EventListener {
4410+
override fun onShown() {
4411+
viewModel.onExperimentalHomeScreenWidgetBottomSheetDialogShown()
4412+
viewModel.onCtaShown()
4413+
}
4414+
4415+
override fun onCanceled() {
4416+
viewModel.onExperimentalHomeScreenWidgetBottomSheetDialogCancelled()
4417+
viewModel.onUserClickCtaSecondaryButton(configuration)
4418+
}
4419+
4420+
override fun onAddWidgetButtonClicked() {
4421+
viewModel.onExperimentalHomeScreenWidgetBottomSheetDialogAddWidgetClicked()
4422+
viewModel.onUserClickCtaOkButton(configuration)
4423+
}
4424+
4425+
override fun onNotNowButtonClicked() {
4426+
viewModel.onExperimentalHomeScreenWidgetBottomSheetDialogNotNowClicked()
4427+
viewModel.onUserClickCtaSecondaryButton(configuration)
4428+
}
4429+
}
4430+
experimentalBottomSheet.show()
4431+
} else {
4432+
if (!experimentalBottomSheet.isShowing) {
4433+
experimentalBottomSheet.show()
4434+
}
4435+
}
4436+
}
4437+
43974438
private fun showHomeCta(
43984439
configuration: HomePanelCta,
43994440
) {

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4186,6 +4186,23 @@ class BrowserTabViewModel @Inject constructor(
41864186
lastSubmittedUserQuery = query
41874187
}
41884188

4189+
fun onExperimentalHomeScreenWidgetBottomSheetDialogShown() {
4190+
logcat { "Experimental Home Screen Widget bottom sheet shown" }
4191+
}
4192+
4193+
fun onExperimentalHomeScreenWidgetBottomSheetDialogCancelled() {
4194+
logcat { "Experimental Home Screen Widget bottom sheet dismissed" }
4195+
}
4196+
4197+
fun onExperimentalHomeScreenWidgetBottomSheetDialogAddWidgetClicked() {
4198+
logcat { "Add Widget clicked on the Experimental Home Screen Widget bottom sheet" }
4199+
command.value = LaunchAddWidget
4200+
}
4201+
4202+
fun onExperimentalHomeScreenWidgetBottomSheetDialogNotNowClicked() {
4203+
logcat { "Not Now clicked on the Experimental Home Screen Widget bottom sheet" }
4204+
}
4205+
41894206
companion object {
41904207
private const val FIXED_PROGRESS = 50
41914208

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.defaultbrowsing.prompts.ui.experiment
18+
19+
import android.annotation.SuppressLint
20+
import android.content.Context
21+
import android.content.DialogInterface
22+
import android.view.LayoutInflater
23+
import android.widget.FrameLayout
24+
import com.duckduckgo.app.browser.databinding.BottomSheetExperimentHomeScreenWidgetBinding
25+
import com.google.android.material.R
26+
import com.google.android.material.bottomsheet.BottomSheetBehavior
27+
import com.google.android.material.bottomsheet.BottomSheetDialog
28+
import com.google.android.material.shape.CornerFamily
29+
import com.google.android.material.shape.MaterialShapeDrawable
30+
31+
@SuppressLint("NoBottomSheetDialog")
32+
class ExperimentalHomeScreenWidgetBottomSheetDialog(
33+
private val context: Context,
34+
isLightModeEnabled: Boolean,
35+
) : BottomSheetDialog(context) {
36+
37+
private val binding: BottomSheetExperimentHomeScreenWidgetBinding =
38+
BottomSheetExperimentHomeScreenWidgetBinding.inflate(LayoutInflater.from(context))
39+
40+
var eventListener: EventListener? = null
41+
42+
init {
43+
setContentView(binding.root)
44+
// We need the dialog to always be expanded and not draggable because the content takes up a lot of vertical space and requires a scroll view,
45+
// especially in landscape aspect-ratios. If the dialog started as collapsed, the drag would interfere with internal scroll.
46+
this.behavior.state = BottomSheetBehavior.STATE_EXPANDED
47+
this.behavior.isDraggable = false
48+
49+
setOnShowListener { dialogInterface ->
50+
setRoundCorners(dialogInterface)
51+
eventListener?.onShown()
52+
}
53+
setOnCancelListener {
54+
eventListener?.onCanceled()
55+
dismiss()
56+
}
57+
binding.experimentHomeScreenWidgetBottomSheetDialogImage.setImageResource(
58+
if (isLightModeEnabled) {
59+
com.duckduckgo.app.browser.R.drawable.experiment_widget_promo_light
60+
} else {
61+
com.duckduckgo.app.browser.R.drawable.experiment_widget_promo_dark
62+
},
63+
)
64+
binding.experimentHomeScreenWidgetBottomSheetDialogPrimaryButton.setOnClickListener {
65+
eventListener?.onAddWidgetButtonClicked()
66+
dismiss()
67+
}
68+
binding.experimentHomeScreenWidgetBottomSheetDialogGhostButton.setOnClickListener {
69+
eventListener?.onNotNowButtonClicked()
70+
dismiss()
71+
}
72+
}
73+
74+
/**
75+
* By default, when bottom sheet dialog is expanded, the corners become squared.
76+
* This function ensures that the bottom sheet dialog will have rounded corners even when in an expanded state.
77+
*/
78+
private fun setRoundCorners(dialogInterface: DialogInterface) {
79+
val bottomSheetDialog = dialogInterface as BottomSheetDialog
80+
val bottomSheet = bottomSheetDialog.findViewById<FrameLayout>(R.id.design_bottom_sheet)
81+
82+
val shapeDrawable = MaterialShapeDrawable.createWithElevationOverlay(context)
83+
shapeDrawable.shapeAppearanceModel = shapeDrawable.shapeAppearanceModel
84+
.toBuilder()
85+
.setTopLeftCorner(CornerFamily.ROUNDED, context.resources.getDimension(com.duckduckgo.mobile.android.R.dimen.dialogBorderRadius))
86+
.setTopRightCorner(CornerFamily.ROUNDED, context.resources.getDimension(com.duckduckgo.mobile.android.R.dimen.dialogBorderRadius))
87+
.build()
88+
bottomSheet?.background = shapeDrawable
89+
}
90+
91+
interface EventListener {
92+
fun onShown()
93+
fun onCanceled()
94+
fun onAddWidgetButtonClicked()
95+
fun onNotNowButtonClicked()
96+
}
97+
}

0 commit comments

Comments
 (0)