Skip to content

Commit 81fe788

Browse files
committed
Duck ai subscriber settings (#6363)
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1210450968200755?focus=true ### Description Adds duck.ai subscriber settings ### Steps to test this PR _Feature 1_ - [x] Apply staging patch (any patch) - [x] Purchase a subscription - [x] In settings, subscription section, Duck.ai is listed - [x] Click on duck.ai - [x] Ensure you navigate to duck.ai setting screen - [x] Ensure pixel `m_privacy-pro_settings_paid-ai-chat_impression` sent - [x] click on learn more - [x] Ensure it navigates (url will fix later) - [x] go back - [x] Click on Open Duck.ai - [x] Ensure opens Duck.ai - [x] ensure pixel `m_privacy-pro_settings_paid-ai-chat_click` sent - [x] Go to settings and cancel your subscription - [x] Wait until expires - [x] When expired, ensure is listed in disabled state - [x] Clicking on the option does not open the screen ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent fbf374c commit 81fe788

File tree

14 files changed

+626
-61
lines changed

14 files changed

+626
-61
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:aapt="http://schemas.android.com/aapt"
3+
android:width="24dp"
4+
android:height="24dp"
5+
android:viewportWidth="24"
6+
android:viewportHeight="24">
7+
<path
8+
android:pathData="M3.762,20.236c-0.659,0.765 -0.015,1.921 0.98,1.746 3.373,-0.594 8.771,-1.652 11.027,-2.695C19.424,17.95 22,14.732 22,10.973 22,6.017 17.523,2 12,2S2,6.017 2,10.973c0,2.422 1.07,4.62 2.81,6.235 0.466,0.433 0.557,1.164 0.141,1.647z"
9+
android:fillColor="#DDD"/>
10+
<path
11+
android:pathData="M12,2c5.523,0 10,4.017 10,8.973 0,3.759 -2.576,6.979 -6.231,8.314 -2.256,1.043 -7.654,2.102 -11.028,2.695 -0.994,0.175 -1.638,-0.98 -0.98,-1.746l1.19,-1.381c0.415,-0.483 0.325,-1.214 -0.141,-1.647C3.07,15.594 2,13.395 2,10.973 2,6.017 6.477,2 12,2m0,1.25c-4.963,0 -8.75,3.582 -8.75,7.723l0.001,0.095c0.028,2.003 0.92,3.843 2.408,5.224 0.887,0.823 1.155,2.314 0.24,3.378l-0.851,0.988c1.591,-0.285 3.528,-0.658 5.348,-1.074 2.058,-0.47 3.853,-0.972 4.848,-1.432l0.047,-0.022 0.049,-0.018c3.244,-1.185 5.41,-3.988 5.41,-7.14 0,-4.14 -3.787,-7.722 -8.75,-7.722m-0.468,3.052c0.122,-0.487 0.814,-0.487 0.936,0l0.264,1.06a4.01,4.01 0,0 0,2.921 2.92l1.06,0.266c0.487,0.122 0.487,0.813 0,0.934l-1.06,0.265a4.02,4.02 0,0 0,-2.92 2.922l-0.265,1.06c-0.122,0.486 -0.814,0.486 -0.936,0l-0.264,-1.06a4.02,4.02 0,0 0,-2.922 -2.922l-1.06,-0.265c-0.486,-0.121 -0.486,-0.812 0,-0.934l1.06,-0.266a4.01,4.01 0,0 0,2.922 -2.92z">
12+
<aapt:attr name="android:fillColor">
13+
<gradient
14+
android:startX="12"
15+
android:startY="22"
16+
android:endX="12"
17+
android:endY="2"
18+
android:type="linear">
19+
<item android:offset="0" android:color="#FF888888"/>
20+
<item android:offset="1" android:color="#FFAAAAAA"/>
21+
</gradient>
22+
</aapt:attr>
23+
</path>
24+
</vector>

common/common-ui/src/main/res/drawable/ic_new_pill.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
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+
117
<vector xmlns:android="http://schemas.android.com/apk/res/android"
218
android:width="30dp"
319
android:height="16dp"

duckchat/duckchat-impl/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,9 @@
3030
<activity
3131
android:name="com.duckduckgo.duckchat.impl.inputscreen.ui.InputScreenActivity"
3232
android:exported="false" />
33+
<activity
34+
android:name=".subscription.DuckAiPaidSettingsActivity"
35+
android:exported="false"
36+
android:label="@string/duck_ai_paid_settings_title"/>
3337
</application>
3438
</manifest>

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/pixel/DuckChatPixels.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_HISTO
4040
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_MOST_RECENT_HISTORY_CHAT
4141
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_NEW_TAB_MENU
4242
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_TAB_SWITCHER_FAB
43+
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED
44+
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_SETTINGS_OPENED
4345
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_BUTTON_OPEN
4446
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_SETTING_OFF
4547
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_SETTING_ON
@@ -114,6 +116,8 @@ enum class DuckChatPixelName(override val pixelName: String) : Pixel.PixelName {
114116
DUCK_CHAT_OPEN_HISTORY("aichat_open_history"),
115117
DUCK_CHAT_OPEN_MOST_RECENT_HISTORY_CHAT("aichat_open_most_recent_history_chat"),
116118
DUCK_CHAT_SEND_PROMPT_ONGOING_CHAT("aichat_sent_prompt_ongoing_chat"),
119+
DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED("m_privacy-pro_settings_paid-ai-chat_click"),
120+
DUCK_CHAT_PAID_SETTINGS_OPENED("m_privacy-pro_settings_paid-ai-chat_impression"),
117121
}
118122

119123
object DuckChatPixelParameters {
@@ -148,6 +152,8 @@ class DuckChatParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugin
148152
DUCK_CHAT_OPEN_HISTORY.pixelName to PixelParameter.removeAtb(),
149153
DUCK_CHAT_OPEN_MOST_RECENT_HISTORY_CHAT.pixelName to PixelParameter.removeAtb(),
150154
DUCK_CHAT_SEND_PROMPT_ONGOING_CHAT.pixelName to PixelParameter.removeAtb(),
155+
DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED.pixelName to PixelParameter.removeAtb(),
156+
DUCK_CHAT_PAID_SETTINGS_OPENED.pixelName to PixelParameter.removeAtb(),
151157
)
152158
}
153159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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.duckchat.impl.subscription
18+
19+
import android.app.ActivityOptions
20+
import android.os.Bundle
21+
import android.text.SpannableStringBuilder
22+
import android.text.TextPaint
23+
import android.text.method.LinkMovementMethod
24+
import android.text.style.ClickableSpan
25+
import android.text.style.URLSpan
26+
import android.view.View
27+
import androidx.lifecycle.Lifecycle
28+
import androidx.lifecycle.flowWithLifecycle
29+
import androidx.lifecycle.lifecycleScope
30+
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
31+
import com.duckduckgo.anvil.annotations.InjectWith
32+
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
33+
import com.duckduckgo.common.ui.DuckDuckGoActivity
34+
import com.duckduckgo.common.ui.view.getColorFromAttr
35+
import com.duckduckgo.common.ui.viewbinding.viewBinding
36+
import com.duckduckgo.common.utils.extensions.html
37+
import com.duckduckgo.di.scopes.ActivityScope
38+
import com.duckduckgo.duckchat.api.DuckChat
39+
import com.duckduckgo.duckchat.impl.R.string
40+
import com.duckduckgo.duckchat.impl.databinding.ActivityDuckAiPaidSettingsBinding
41+
import com.duckduckgo.duckchat.impl.subscription.DuckAiPaidSettingsViewModel.Command
42+
import com.duckduckgo.duckchat.impl.subscription.DuckAiPaidSettingsViewModel.Command.LaunchLearnMoreWebPage
43+
import com.duckduckgo.duckchat.impl.subscription.DuckAiPaidSettingsViewModel.Command.OpenDuckAi
44+
import com.duckduckgo.mobile.android.R
45+
import com.duckduckgo.navigation.api.GlobalActivityStarter
46+
import javax.inject.Inject
47+
import kotlinx.coroutines.flow.launchIn
48+
import kotlinx.coroutines.flow.onEach
49+
50+
object DuckAiPaidSettingsNoParams : GlobalActivityStarter.ActivityParams
51+
52+
@InjectWith(ActivityScope::class)
53+
@ContributeToActivityStarter(DuckAiPaidSettingsNoParams::class)
54+
class DuckAiPaidSettingsActivity : DuckDuckGoActivity() {
55+
56+
@Inject lateinit var globalActivityStarter: GlobalActivityStarter
57+
58+
@Inject lateinit var duckChat: DuckChat
59+
60+
private val viewModel: DuckAiPaidSettingsViewModel by bindViewModel()
61+
private val binding: ActivityDuckAiPaidSettingsBinding by viewBinding()
62+
63+
private val toolbar
64+
get() = binding.includeToolbar.toolbar
65+
66+
private val clickableSpan = object : ClickableSpan() {
67+
override fun onClick(widget: View) {
68+
viewModel.onLearnMoreSelected()
69+
}
70+
71+
override fun updateDrawState(ds: TextPaint) {
72+
super.updateDrawState(ds)
73+
ds.color = getColorFromAttr(R.attr.daxColorAccentBlue)
74+
ds.isUnderlineText = false
75+
}
76+
}
77+
78+
override fun onCreate(savedInstanceState: Bundle?) {
79+
super.onCreate(savedInstanceState)
80+
81+
setContentView(binding.root)
82+
setupToolbar(toolbar)
83+
84+
configureUiEventHandlers()
85+
configureClickableLink()
86+
observeViewModel()
87+
}
88+
89+
private fun configureUiEventHandlers() {
90+
binding.duckAiPaidSettingsOpenDuckAi.setOnClickListener {
91+
viewModel.onOpenDuckAiSelected()
92+
}
93+
}
94+
95+
private fun observeViewModel() {
96+
viewModel.commands
97+
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
98+
.onEach { processCommand(it) }
99+
.launchIn(lifecycleScope)
100+
}
101+
102+
private fun processCommand(command: Command) {
103+
when (command) {
104+
is LaunchLearnMoreWebPage -> {
105+
val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
106+
globalActivityStarter.start(this, WebViewActivityWithParams(command.url, getString(command.titleId)), options)
107+
}
108+
109+
OpenDuckAi -> {
110+
duckChat.openDuckChat()
111+
}
112+
}
113+
}
114+
115+
private fun configureClickableLink() {
116+
val htmlText = getString(
117+
string.duck_ai_paid_settings_page_description,
118+
).html(this)
119+
val spannableString = SpannableStringBuilder(htmlText)
120+
val urlSpans = htmlText.getSpans(0, htmlText.length, URLSpan::class.java)
121+
urlSpans?.forEach {
122+
spannableString.apply {
123+
insert(spannableString.getSpanStart(it), "\n")
124+
setSpan(
125+
clickableSpan,
126+
spannableString.getSpanStart(it),
127+
spannableString.getSpanEnd(it),
128+
spannableString.getSpanFlags(it),
129+
)
130+
removeSpan(it)
131+
trim()
132+
}
133+
}
134+
binding.duckAiPaidSettingsDescription.apply {
135+
text = spannableString
136+
movementMethod = LinkMovementMethod.getInstance()
137+
}
138+
}
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.duckchat.impl.subscription
18+
19+
import androidx.annotation.StringRes
20+
import androidx.lifecycle.ViewModel
21+
import androidx.lifecycle.viewModelScope
22+
import com.duckduckgo.anvil.annotations.ContributesViewModel
23+
import com.duckduckgo.app.statistics.pixels.Pixel
24+
import com.duckduckgo.common.utils.DispatcherProvider
25+
import com.duckduckgo.di.scopes.ActivityScope
26+
import com.duckduckgo.duckchat.impl.R
27+
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED
28+
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_SETTINGS_OPENED
29+
import javax.inject.Inject
30+
import kotlinx.coroutines.channels.BufferOverflow
31+
import kotlinx.coroutines.channels.Channel
32+
import kotlinx.coroutines.flow.receiveAsFlow
33+
import kotlinx.coroutines.launch
34+
35+
@ContributesViewModel(ActivityScope::class)
36+
class DuckAiPaidSettingsViewModel @Inject constructor(
37+
private val pixel: Pixel,
38+
private val dispatchers: DispatcherProvider,
39+
) : ViewModel() {
40+
41+
sealed class Command {
42+
data object OpenDuckAi : Command()
43+
data class LaunchLearnMoreWebPage(
44+
val url: String = "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/",
45+
@StringRes val titleId: Int = R.string.duck_ai_paid_settings_learn_more_title,
46+
) : Command()
47+
}
48+
49+
private val _commands = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
50+
val commands = _commands.receiveAsFlow()
51+
52+
init {
53+
pixel.fire(DUCK_CHAT_PAID_SETTINGS_OPENED)
54+
}
55+
56+
fun onLearnMoreSelected() {
57+
viewModelScope.launch {
58+
_commands.send(Command.LaunchLearnMoreWebPage())
59+
}
60+
}
61+
62+
fun onOpenDuckAiSelected() {
63+
viewModelScope.launch {
64+
_commands.send(Command.OpenDuckAi)
65+
pixel.fire(DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED)
66+
}
67+
}
68+
}

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/subscription/DuckAiPlusSettingsView.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ class DuckAiPlusSettingsView @JvmOverloads constructor(
105105
isClickable = false
106106
setStatus(isOn = false)
107107
setClickListener(null)
108-
// TODO: we need a disabled state icon
109-
setLeadingIconResource(com.duckduckgo.mobile.android.R.drawable.ic_ai_chat_color_24)
108+
setLeadingIconResource(com.duckduckgo.mobile.android.R.drawable.ic_ai_chat_grayscale_color_24)
110109
}
111110
SettingState.Hidden -> isGone = true
112111
}
@@ -116,7 +115,7 @@ class DuckAiPlusSettingsView @JvmOverloads constructor(
116115
private fun processCommands(command: Command) {
117116
when (command) {
118117
is OpenDuckAiPlusSettings -> {
119-
// TODO: navigate to Duck Ai Plus settings
118+
globalActivityStarter.start(context, DuckAiPaidSettingsNoParams)
120119
}
121120
}
122121
}

0 commit comments

Comments
 (0)