Skip to content

Commit fcfc3f7

Browse files
authored
Custom Duck.ai Onboarding - initial screen copy alternatives (#8696)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1214496516674987?focus=true ### Description Provides alternative copies for initial, reinstaller, and skip onboarding screens. ### Steps to test this PR - [ ] Remove the `Download/DuckDuckGo` directory. - [ ] Clean install the app. - [ ] Verify you see the mainline copy on the screen, and only a single action button. - [ ] Finish onboarding. - [ ] Clear storage and reopen the app. - [ ] Verify you see the mainline reinstaller copy on the screen, and two action buttons (one to skip). - [ ] Click the "I've been here before" button. - [ ] Verify you see the mainline skip copy on the screen. - [ ] Click "Show tutorial". - [ ] Verify the title on the browser comparison screen says "Protections activated!" - [ ] Apply this diff: ```diff diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BrandDesignUpdatePageViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BrandDesignUpdatePageViewModel.kt index 4ea86fa..7ac1698fb4 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BrandDesignUpdatePageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BrandDesignUpdatePageViewModel.kt @@ -139,7 +139,7 @@ class BrandDesignUpdatePageViewModel @Inject constructor( val inputScreenPreviewIsSearchSelected: Boolean = false, val hideSetDefaultBrowserRow: Boolean = false, val hideAddWidgetRow: Boolean = false, - val isCustomAiOnboardingCopyEnabled: Boolean = false, + val isCustomAiOnboardingCopyEnabled: Boolean = true, ) { val maxPageCount = 3 } ``` - [ ] Remove the `Download/DuckDuckGo` directory. - [ ] Clean install the app. - [ ] Verify you see the alternative copy on the screen, and only a single action button. - [ ] Finish onboarding. - [ ] Clear storage and reopen the app. - [ ] Verify you see the alternative reinstaller copy on the screen, and two action buttons (one to skip). - [ ] Click the "I've been here before" button. - [ ] Verify you see the alternative skip copy on the screen. - [ ] Click "Show tutorial". - [ ] Verify the title on the browser comparison screen says "Want to make DuckDuckGo your default browser?" ### UI changes | Before | After | | ------ | ----- | <img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/a5e6a1cd-ff4d-4bd3-b173-82e89bb0fcbd" /><img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/1c947cd7-fcdc-4083-923f-ea7d4a58ceb1" /><img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/ce70b5f6-ab8a-417f-8bc5-cf61d5d022a6" /><img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/e9665354-4be7-40a6-a726-6eef3b6951a6" />|<img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/18528e9d-3da0-4688-a6de-637555c5b3e6" /><img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/55fb59c4-eb05-4f8c-8113-1aebe5f252cd" /><img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/b223aeab-f07d-4508-a399-2077de03026f" /><img width="480" height="1071" alt="image" src="https://github.com/user-attachments/assets/80a77856-b360-4e6f-865d-8db3595aae8e" />| <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > UI copy and presentation only behind a default-off flag; no auth, data, or navigation logic changes beyond string selection. > > **Overview** > Adds an optional **Duck.ai–focused copy path** for brand-design pre-onboarding, driven by `isCustomAiOnboardingCopyEnabled` on `BrandDesignUpdatePageViewModel.ViewState` (defaults off; PR tests by flipping it to `true`). > > When enabled, **welcome** (`INITIAL` / reinstall) uses a single custom body line and hides the second paragraph; **skip onboarding** uses alternate body text with an inline chat icon via `appendIconToText`, a **Start AI Chat** primary CTA, and the same layout rules as sync-restore for hiding `bodyText2`. **Browser comparison chart** title switches from “Protections activated!” to “Want to make DuckDuckGo your default browser?” via `ComparisonChartConfig.Browser(isCustomAiCopy)`. > > `ComparisonChartConfig` is refactored from companion `Default` / `Ai` values to a **sealed** hierarchy (`Browser` + `Ai` object). Four new non-translated strings live in `donottranslate.xml`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 87fdb1c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 2d81e97 commit fcfc3f7

4 files changed

Lines changed: 77 additions & 41 deletions

File tree

app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BrandDesignUpdatePageViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class BrandDesignUpdatePageViewModel @Inject constructor(
144144
val hideSetDefaultBrowserRow: Boolean = false,
145145
val hideAddWidgetRow: Boolean = false,
146146
val isDuckAiIntroAnimationEnabled: Boolean = false,
147+
val isCustomAiOnboardingCopyEnabled: Boolean = false,
147148
) {
148149
val maxPageCount = 3
149150
}

app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BrandDesignUpdateWelcomePage.kt

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import com.duckduckgo.appbuildconfig.api.AppBuildConfig
8989
import com.duckduckgo.common.ui.store.AppTheme
9090
import com.duckduckgo.common.ui.view.TypeAnimationTextView
9191
import com.duckduckgo.common.ui.view.addBottomShadow
92+
import com.duckduckgo.common.ui.view.appendIconToText
9293
import com.duckduckgo.common.ui.view.text.DaxTextView
9394
import com.duckduckgo.common.ui.view.toPx
9495
import com.duckduckgo.common.ui.viewbinding.viewBinding
@@ -535,6 +536,7 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
535536
inputScreenSelected = state.inputScreenSelected,
536537
maxPageCount = state.maxPageCount,
537538
comparisonChartConfig = state.currentComparisonChartConfig(),
539+
isCustomAiCopy = state.isCustomAiOnboardingCopyEnabled,
538540
)
539541
}
540542
}
@@ -547,6 +549,7 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
547549
inputScreenSelected = state.inputScreenSelected,
548550
maxPageCount = state.maxPageCount,
549551
comparisonChartConfig = state.currentComparisonChartConfig(),
552+
isCustomAiCopy = state.isCustomAiOnboardingCopyEnabled,
550553
)
551554
}
552555
}
@@ -739,6 +742,7 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
739742
inputScreenSelected: Boolean,
740743
maxPageCount: Int,
741744
comparisonChartConfig: ComparisonChartConfig,
745+
isCustomAiCopy: Boolean,
742746
) {
743747
context?.let {
744748
isAnimating = true
@@ -769,10 +773,13 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
769773
getString(R.string.syncRestoreDialogBrandDesignBody1).preventWidows().html(requireContext())
770774
binding.daxDialogCta.primaryCta.text = getString(R.string.syncRestoreDialogPrimaryCta)
771775
binding.daxDialogCta.secondaryCta.text = getString(R.string.syncRestoreDialogSecondaryCta)
776+
} else if (isCustomAiCopy) {
777+
binding.daxDialogCta.welcomeContent.bodyText1.text =
778+
getString(R.string.preOnboardingWelcomeDialogBodyCustomAi).preventWidows().html(requireContext())
772779
}
773-
// SYNC_RESTORE shows no second body line; INITIAL/INITIAL_REINSTALL_USER do.
780+
// SYNC_RESTORE shows no second body line; custom-AI copy is a single sentence and also hides it; INITIAL/INITIAL_REINSTALL_USER otherwise show both.
774781
// Set isVisible explicitly so a prior dialog that hid bodyText2 doesn't leak into this one.
775-
binding.daxDialogCta.welcomeContent.bodyText2.isVisible = !isSyncRestore
782+
binding.daxDialogCta.welcomeContent.bodyText2.isVisible = !isSyncRestore && !isCustomAiCopy
776783

777784
val showWalkingDax = applyWalkingDaxLayout()
778785
binding.daxDialogCta.cardView.setArrowDepthFraction(if (showWalkingDax) 1f else 0f)
@@ -793,7 +800,7 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
793800
ObjectAnimator.ofFloat(binding.daxDialogCta.primaryCta, View.ALPHA, 1f)
794801
.setDuration(DIALOG_CONTENT_FADE_IN_DURATION),
795802
)
796-
if (!isSyncRestore) {
803+
if (!isSyncRestore && !isCustomAiCopy) {
797804
animators += ObjectAnimator.ofFloat(binding.daxDialogCta.welcomeContent.bodyText2, View.ALPHA, 1f)
798805
.setDuration(DIALOG_CONTENT_FADE_IN_DURATION)
799806
}
@@ -1040,14 +1047,23 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
10401047
binding.daxDialogCta.welcomeContent.hiddenTitleText.text =
10411048
getString(R.string.preOnboardingDaxDialog3Title)
10421049
binding.daxDialogCta.welcomeContent.bodyText1.text =
1043-
getString(R.string.preOnboardingDaxDialog3Text).preventWidows().html(requireContext())
1050+
if (isCustomAiCopy) {
1051+
requireContext().appendIconToText(
1052+
getString(R.string.preOnboardingDaxDialog3TextCustomAi).preventWidows(),
1053+
CommonR.drawable.ic_ai_chat_16,
1054+
)
1055+
} else {
1056+
getString(R.string.preOnboardingDaxDialog3Text).preventWidows().html(requireContext())
1057+
}
10441058
binding.daxDialogCta.welcomeContent.bodyText2.isGone = true
10451059

10461060
binding.daxDialogCta.welcomeContent.titleText.cancelAnimation()
10471061
binding.daxDialogCta.welcomeContent.titleText.text = ""
10481062
binding.daxDialogCta.welcomeContent.titleText.alpha = 1f
10491063

1050-
binding.daxDialogCta.primaryCta.text = getString(R.string.preOnboardingDaxDialog3Button)
1064+
binding.daxDialogCta.primaryCta.text = getString(
1065+
if (isCustomAiCopy) R.string.preOnboardingDaxDialog3ButtonCustomAi else R.string.preOnboardingDaxDialog3Button,
1066+
)
10511067
binding.daxDialogCta.secondaryCta.text = getString(R.string.preOnboardingDaxDialog3SecondaryButton)
10521068

10531069
binding.daxDialogCta.welcomeContent.titleText.startOnboardingTypingAnimation(
@@ -1384,6 +1400,7 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
13841400
inputScreenSelected: Boolean,
13851401
maxPageCount: Int,
13861402
comparisonChartConfig: ComparisonChartConfig,
1403+
isCustomAiCopy: Boolean,
13871404
) {
13881405
snapToIntroEndState()
13891406

@@ -1427,10 +1444,13 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
14271444
getString(R.string.syncRestoreDialogBrandDesignBody1).preventWidows().html(requireContext())
14281445
binding.daxDialogCta.primaryCta.text = getString(R.string.syncRestoreDialogPrimaryCta)
14291446
binding.daxDialogCta.secondaryCta.text = getString(R.string.syncRestoreDialogSecondaryCta)
1447+
} else if (isCustomAiCopy) {
1448+
binding.daxDialogCta.welcomeContent.bodyText1.text =
1449+
getString(R.string.preOnboardingWelcomeDialogBodyCustomAi).preventWidows().html(requireContext())
14301450
}
1431-
// SYNC_RESTORE shows no second body line; INITIAL/INITIAL_REINSTALL_USER do.
1451+
// SYNC_RESTORE shows no second body line; custom-AI copy is a single sentence and also hides it; INITIAL/INITIAL_REINSTALL_USER otherwise show both.
14321452
// Set isVisible explicitly so a prior dialog that hid bodyText2 doesn't leak into this one.
1433-
binding.daxDialogCta.welcomeContent.bodyText2.isVisible = !isSyncRestore
1453+
binding.daxDialogCta.welcomeContent.bodyText2.isVisible = !isSyncRestore && !isCustomAiCopy
14341454
binding.daxDialogCta.welcomeContent.bodyText1.alpha = 1f
14351455
binding.daxDialogCta.welcomeContent.bodyText2.alpha = 1f
14361456
binding.daxDialogCta.primaryCta.alpha = 1f
@@ -1515,9 +1535,18 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
15151535
binding.daxDialogCta.welcomeContent.root.isVisible = true
15161536
binding.daxDialogCta.welcomeContent.hiddenTitleText.text = getString(R.string.preOnboardingDaxDialog3Title)
15171537
binding.daxDialogCta.welcomeContent.bodyText1.text =
1518-
getString(R.string.preOnboardingDaxDialog3Text).preventWidows().html(requireContext())
1538+
if (isCustomAiCopy) {
1539+
requireContext().appendIconToText(
1540+
getString(R.string.preOnboardingDaxDialog3TextCustomAi).preventWidows(),
1541+
CommonR.drawable.ic_ai_chat_16,
1542+
)
1543+
} else {
1544+
getString(R.string.preOnboardingDaxDialog3Text).preventWidows().html(requireContext())
1545+
}
15191546
binding.daxDialogCta.welcomeContent.bodyText2.isGone = true
1520-
binding.daxDialogCta.primaryCta.text = getString(R.string.preOnboardingDaxDialog3Button)
1547+
binding.daxDialogCta.primaryCta.text = getString(
1548+
if (isCustomAiCopy) R.string.preOnboardingDaxDialog3ButtonCustomAi else R.string.preOnboardingDaxDialog3Button,
1549+
)
15211550
binding.daxDialogCta.secondaryCta.text = getString(R.string.preOnboardingDaxDialog3SecondaryButton)
15221551
binding.daxDialogCta.secondaryCta.visibility = View.INVISIBLE
15231552

@@ -2406,7 +2435,7 @@ class BrandDesignUpdateWelcomePage : OnboardingPageFragment(R.layout.content_onb
24062435

24072436
private fun BrandDesignUpdatePageViewModel.ViewState.currentComparisonChartConfig(): ComparisonChartConfig = when (this.currentDialog) {
24082437
AI_COMPARISON_CHART -> ComparisonChartConfig.Ai
2409-
else -> ComparisonChartConfig.Default
2438+
else -> ComparisonChartConfig.Browser(isCustomAiCopy = this.isCustomAiOnboardingCopyEnabled)
24102439
}
24112440

24122441
private fun populateComparisonChart(config: ComparisonChartConfig) {

app/src/main/java/com/duckduckgo/app/onboarding/ui/page/ComparisonChartConfig.kt

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import androidx.annotation.StringRes
2121
import com.duckduckgo.app.browser.R
2222
import com.duckduckgo.mobile.android.R as CommonR
2323

24-
data class ComparisonChartConfig(
24+
sealed class ComparisonChartConfig(
2525
@StringRes val titleRes: Int,
2626
@StringRes val primaryCtaTextRes: Int,
2727
@DrawableRes val headerLeftIconRes: Int,
@@ -34,34 +34,36 @@ data class ComparisonChartConfig(
3434
@StringRes val textRes: Int,
3535
)
3636

37-
companion object {
38-
val Default = ComparisonChartConfig(
39-
titleRes = R.string.preOnboardingDaxDialog2Title,
40-
primaryCtaTextRes = R.string.preOnboardingDaxDialog2Button,
41-
headerLeftIconRes = CommonR.drawable.ic_chrome,
42-
headerLeftIconSizeDp = 32f, // the target size is 31.5dp but the icon already has some padding
43-
headerLeftLabelRes = null,
44-
rows = listOf(
45-
Row(CommonR.drawable.ic_vpn_color_24_rebrand, R.string.preOnboardingComparisonChartItem1),
46-
Row(CommonR.drawable.ic_duck_ai_color_24_rebrand, R.string.preOnboardingComparisonChartDuckAi),
47-
Row(CommonR.drawable.ic_shield_color_24_rebrand, R.string.preOnboardingComparisonChartItem2),
48-
Row(CommonR.drawable.ic_cookies_color_24_rebrand, R.string.preOnboardingComparisonChartItem3),
49-
Row(CommonR.drawable.ic_profile_blocker_color_24_rebrand, R.string.preOnboardingComparisonChartItem4),
50-
),
51-
)
37+
data class Browser(private val isCustomAiCopy: Boolean) : ComparisonChartConfig(
38+
titleRes = if (isCustomAiCopy) {
39+
R.string.preOnboardingDaxDialog2TitleCustomAi
40+
} else {
41+
R.string.preOnboardingDaxDialog2Title
42+
},
43+
primaryCtaTextRes = R.string.preOnboardingDaxDialog2Button,
44+
headerLeftIconRes = CommonR.drawable.ic_chrome,
45+
headerLeftIconSizeDp = 32f, // the target size is 31.5dp but the icon already has some padding
46+
headerLeftLabelRes = null,
47+
rows = listOf(
48+
Row(CommonR.drawable.ic_vpn_color_24_rebrand, R.string.preOnboardingComparisonChartItem1),
49+
Row(CommonR.drawable.ic_duck_ai_color_24_rebrand, R.string.preOnboardingComparisonChartDuckAi),
50+
Row(CommonR.drawable.ic_shield_color_24_rebrand, R.string.preOnboardingComparisonChartItem2),
51+
Row(CommonR.drawable.ic_cookies_color_24_rebrand, R.string.preOnboardingComparisonChartItem3),
52+
Row(CommonR.drawable.ic_profile_blocker_color_24_rebrand, R.string.preOnboardingComparisonChartItem4),
53+
),
54+
)
5255

53-
val Ai = ComparisonChartConfig(
54-
titleRes = R.string.preOnboardingDaxDialogAiTitle,
55-
primaryCtaTextRes = R.string.preOnboardingAiComparisonChartButton,
56-
headerLeftIconRes = CommonR.drawable.ic_ai_general_16,
57-
headerLeftIconSizeDp = 18f,
58-
headerLeftLabelRes = R.string.preOnboardingAiComparisonChartPopularAis,
59-
rows = listOf(
60-
Row(CommonR.drawable.ic_shield_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem1),
61-
Row(CommonR.drawable.ic_duck_ai_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem2),
62-
Row(CommonR.drawable.ic_lock_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem3),
63-
Row(CommonR.drawable.ic_ai_general_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem4),
64-
),
65-
)
66-
}
56+
data object Ai : ComparisonChartConfig(
57+
titleRes = R.string.preOnboardingDaxDialogAiTitle,
58+
primaryCtaTextRes = R.string.preOnboardingAiComparisonChartButton,
59+
headerLeftIconRes = CommonR.drawable.ic_ai_general_16,
60+
headerLeftIconSizeDp = 18f,
61+
headerLeftLabelRes = R.string.preOnboardingAiComparisonChartPopularAis,
62+
rows = listOf(
63+
Row(CommonR.drawable.ic_shield_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem1),
64+
Row(CommonR.drawable.ic_duck_ai_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem2),
65+
Row(CommonR.drawable.ic_lock_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem3),
66+
Row(CommonR.drawable.ic_ai_general_color_24_rebrand, R.string.preOnboardingAiComparisonChartItem4),
67+
),
68+
)
6769
}

app/src/main/res/values/donottranslate.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,9 @@
144144
<!-- In-context onboarding. -->
145145
<string name="onboardingEndCustomAiFlowDaxDialogDescription"><![CDATA[Start a private AI chat with Duck.ai or toggle to Search for protected browsing.<br/><br/>You can use Duck.ai from anywhere you see the chat icon]]></string>
146146
<string name="onboardingPrivacyProCustomAiFlowDaxDialogDescription">We also offer a paid subscription featuring advanced AI models with higher chat limits from GPT, Claude, and Llama, a secure VPN, and more.</string>
147-
147+
<!-- Pre-onboarding: initial screens -->
148+
<string name="preOnboardingWelcomeDialogBodyCustomAi">Ready to chat privately with ChatGPT, Claude, and other AIs for free, in a browser that actively protects you?</string>
149+
<string name="preOnboardingDaxDialog2TitleCustomAi">Want to make DuckDuckGo your default browser?</string>
150+
<string name="preOnboardingDaxDialog3TextCustomAi">Remember: you can use Duck.ai from anywhere you see the chat icon</string>
151+
<string name="preOnboardingDaxDialog3ButtonCustomAi">Start AI Chat</string>
148152
</resources>

0 commit comments

Comments
 (0)