Skip to content

Duck ai subscriber settings #6363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
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"
android:fillColor="#DDD"/>
<path
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">
<aapt:attr name="android:fillColor">
<gradient
android:startX="12"
android:startY="22"
android:endX="12"
android:endY="2"
android:type="linear">
<item android:offset="0" android:color="#FF888888"/>
<item android:offset="1" android:color="#FFAAAAAA"/>
</gradient>
</aapt:attr>
</path>
</vector>
16 changes: 16 additions & 0 deletions common/common-ui/src/main/res/drawable/ic_new_pill.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
<!--
~ Copyright (c) 2025 DuckDuckGo
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="16dp"
Expand Down
4 changes: 4 additions & 0 deletions duckchat/duckchat-impl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@
<activity
android:name="com.duckduckgo.duckchat.impl.inputscreen.ui.InputScreenActivity"
android:exported="false" />
<activity
android:name=".subscription.DuckAiPaidSettingsActivity"
android:exported="false"
android:label="@string/duck_ai_paid_settings_title"/>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_HISTO
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_MOST_RECENT_HISTORY_CHAT
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_NEW_TAB_MENU
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_OPEN_TAB_SWITCHER_FAB
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_SETTINGS_OPENED
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_BUTTON_OPEN
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_SETTING_OFF
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_SEARCHBAR_SETTING_ON
Expand Down Expand Up @@ -114,6 +116,8 @@ enum class DuckChatPixelName(override val pixelName: String) : Pixel.PixelName {
DUCK_CHAT_OPEN_HISTORY("aichat_open_history"),
DUCK_CHAT_OPEN_MOST_RECENT_HISTORY_CHAT("aichat_open_most_recent_history_chat"),
DUCK_CHAT_SEND_PROMPT_ONGOING_CHAT("aichat_sent_prompt_ongoing_chat"),
DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED("m_privacy-pro_settings_paid-ai-chat_click"),
DUCK_CHAT_PAID_SETTINGS_OPENED("m_privacy-pro_settings_paid-ai-chat_impression"),
}

object DuckChatPixelParameters {
Expand Down Expand Up @@ -148,6 +152,8 @@ class DuckChatParamRemovalPlugin @Inject constructor() : PixelParamRemovalPlugin
DUCK_CHAT_OPEN_HISTORY.pixelName to PixelParameter.removeAtb(),
DUCK_CHAT_OPEN_MOST_RECENT_HISTORY_CHAT.pixelName to PixelParameter.removeAtb(),
DUCK_CHAT_SEND_PROMPT_ONGOING_CHAT.pixelName to PixelParameter.removeAtb(),
DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED.pixelName to PixelParameter.removeAtb(),
DUCK_CHAT_PAID_SETTINGS_OPENED.pixelName to PixelParameter.removeAtb(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.duckchat.impl.subscription

import android.app.ActivityOptions
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.view.getColorFromAttr
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.extensions.html
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.impl.R.string
import com.duckduckgo.duckchat.impl.databinding.ActivityDuckAiPaidSettingsBinding
import com.duckduckgo.duckchat.impl.subscription.DuckAiPaidSettingsViewModel.Command
import com.duckduckgo.duckchat.impl.subscription.DuckAiPaidSettingsViewModel.Command.LaunchLearnMoreWebPage
import com.duckduckgo.duckchat.impl.subscription.DuckAiPaidSettingsViewModel.Command.OpenDuckAi
import com.duckduckgo.mobile.android.R
import com.duckduckgo.navigation.api.GlobalActivityStarter
import javax.inject.Inject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

object DuckAiPaidSettingsNoParams : GlobalActivityStarter.ActivityParams

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(DuckAiPaidSettingsNoParams::class)
class DuckAiPaidSettingsActivity : DuckDuckGoActivity() {

@Inject lateinit var globalActivityStarter: GlobalActivityStarter

@Inject lateinit var duckChat: DuckChat

private val viewModel: DuckAiPaidSettingsViewModel by bindViewModel()
private val binding: ActivityDuckAiPaidSettingsBinding by viewBinding()

private val toolbar
get() = binding.includeToolbar.toolbar

private val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
viewModel.onLearnMoreSelected()
}

override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = getColorFromAttr(R.attr.daxColorAccentBlue)
ds.isUnderlineText = false
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(binding.root)
setupToolbar(toolbar)

configureUiEventHandlers()
configureClickableLink()
observeViewModel()
}

private fun configureUiEventHandlers() {
binding.duckAiPaidSettingsOpenDuckAi.setOnClickListener {
viewModel.onOpenDuckAiSelected()
}
}

private fun observeViewModel() {
viewModel.commands
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { processCommand(it) }
.launchIn(lifecycleScope)
}

private fun processCommand(command: Command) {
when (command) {
is LaunchLearnMoreWebPage -> {
val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
globalActivityStarter.start(this, WebViewActivityWithParams(command.url, getString(command.titleId)), options)
}

OpenDuckAi -> {
duckChat.openDuckChat()
}
}
}

private fun configureClickableLink() {
val htmlText = getString(
string.duck_ai_paid_settings_page_description,
).html(this)
val spannableString = SpannableStringBuilder(htmlText)
val urlSpans = htmlText.getSpans(0, htmlText.length, URLSpan::class.java)
urlSpans?.forEach {
spannableString.apply {
insert(spannableString.getSpanStart(it), "\n")
setSpan(
clickableSpan,
spannableString.getSpanStart(it),
spannableString.getSpanEnd(it),
spannableString.getSpanFlags(it),
)
removeSpan(it)
trim()
}
}
binding.duckAiPaidSettingsDescription.apply {
text = spannableString
movementMethod = LinkMovementMethod.getInstance()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.duckchat.impl.subscription

import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.duckchat.impl.R
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName.DUCK_CHAT_PAID_SETTINGS_OPENED
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

@ContributesViewModel(ActivityScope::class)
class DuckAiPaidSettingsViewModel @Inject constructor(
private val pixel: Pixel,
private val dispatchers: DispatcherProvider,
) : ViewModel() {

sealed class Command {
data object OpenDuckAi : Command()
data class LaunchLearnMoreWebPage(
val url: String = "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/",
@StringRes val titleId: Int = R.string.duck_ai_paid_settings_learn_more_title,
) : Command()
}

private val _commands = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
val commands = _commands.receiveAsFlow()

init {
pixel.fire(DUCK_CHAT_PAID_SETTINGS_OPENED)
}

fun onLearnMoreSelected() {
viewModelScope.launch {
_commands.send(Command.LaunchLearnMoreWebPage())
}
}

fun onOpenDuckAiSelected() {
viewModelScope.launch {
_commands.send(Command.OpenDuckAi)
pixel.fire(DUCK_CHAT_PAID_OPEN_DUCK_AI_CLICKED)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ class DuckAiPlusSettingsView @JvmOverloads constructor(
isClickable = false
setStatus(isOn = false)
setClickListener(null)
// TODO: we need a disabled state icon
setLeadingIconResource(com.duckduckgo.mobile.android.R.drawable.ic_ai_chat_color_24)
setLeadingIconResource(com.duckduckgo.mobile.android.R.drawable.ic_ai_chat_grayscale_color_24)
}
SettingState.Hidden -> isGone = true
}
Expand All @@ -116,7 +115,7 @@ class DuckAiPlusSettingsView @JvmOverloads constructor(
private fun processCommands(command: Command) {
when (command) {
is OpenDuckAiPlusSettings -> {
// TODO: navigate to Duck Ai Plus settings
globalActivityStarter.start(context, DuckAiPaidSettingsNoParams)
}
}
}
Expand Down
Loading
Loading