Skip to content

Commit bb2959d

Browse files
committed
support post subscription duckai card click
1 parent 9a53001 commit bb2959d

File tree

10 files changed

+114
-2
lines changed

10 files changed

+114
-2
lines changed

subscriptions/subscriptions-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ dependencies {
6060
implementation project(':survey-api')
6161
implementation project(':vpn-api')
6262
implementation project(':content-scope-scripts-api')
63+
implementation project(':duckchat-api')
6364

6465
implementation AndroidX.appCompat
6566
implementation KotlinX.coroutines.core

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/RealSubscriptions.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ interface PrivacyProFeature {
168168
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
169169
fun privacyProFreeTrial(): Toggle
170170

171+
/**
172+
* Enables/Disables duckAi for subscribers (advanced models)
173+
* This flag is used to hide the feature in the native client and FE.
174+
* It will be used for the feature rollout and kill-switch if necessary.
175+
*/
171176
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
172177
fun duckAiPlus(): Toggle
173178

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ object SubscriptionsConstants {
4545
const val ITR = "Identity Theft Restoration"
4646
const val ROW_ITR = "Global Identity Theft Restoration"
4747
const val PIR = "Data Broker Protection"
48+
const val DUCK_AI = "Duck.ai"
4849

4950
// Platform
5051
const val PLATFORM = "android"

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,10 @@ class SubscriptionMessagingInterface @Inject constructor(
368368
if (privacyProFeature.enableNewSubscriptionMessages().isEnabled().not()) return
369369

370370
val authV2Enabled = privacyProFeature.enableSubscriptionFlowsV2().isEnabled()
371+
val duckAiSubscriberModelsEnabled = privacyProFeature.duckAiPlus().isEnabled()
371372
val resultJson = JSONObject().apply {
372373
put("useSubscriptionsAuthV2", authV2Enabled)
374+
put("usePaidDuckAi", duckAiSubscriberModelsEnabled)
373375
}
374376

375377
val response = JsRequestResponse.Success(

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ enum class SubscriptionPixel(
141141
type = Unique(),
142142
includedParameters = setOf(ATB, APP_VERSION),
143143
),
144+
ONBOARDING_DUCK_AI_CLICK(
145+
baseName = "m_privacy-pro_welcome_paid-ai-chat_click",
146+
type = Unique(),
147+
includedParameters = setOf(ATB, APP_VERSION),
148+
),
144149
SUBSCRIPTION_SETTINGS_SHOWN(
145150
baseName = "m_privacy-pro_settings_screen_impression",
146151
type = Count,

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/pixels/SubscriptionPixelSender.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_RESTORE_
3939
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SCREEN_SHOWN
4040
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.OFFER_SUBSCRIBE_CLICK
4141
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_ADD_DEVICE_CLICK
42+
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_DUCK_AI_CLICK
4243
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_IDTR_CLICK
4344
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_PIR_CLICK
4445
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixel.ONBOARDING_VPN_CLICK
@@ -89,6 +90,7 @@ interface SubscriptionPixelSender {
8990
fun reportOnboardingVpnClick()
9091
fun reportOnboardingPirClick()
9192
fun reportOnboardingIdtrClick()
93+
fun reportOnboardingDuckAiClick()
9294
fun reportSubscriptionSettingsShown()
9395
fun reportAppSettingsPirClick()
9496
fun reportAppSettingsIdtrClick()
@@ -191,6 +193,9 @@ class SubscriptionPixelSenderImpl @Inject constructor(
191193
override fun reportOnboardingIdtrClick() =
192194
fire(ONBOARDING_IDTR_CLICK)
193195

196+
override fun reportOnboardingDuckAiClick() =
197+
fire(ONBOARDING_DUCK_AI_CLICK)
198+
194199
override fun reportSubscriptionSettingsShown() =
195200
fire(SUBSCRIPTION_SETTINGS_SHOWN)
196201

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
3232
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
3333
import com.duckduckgo.subscriptions.impl.SubscriptionOffer
3434
import com.duckduckgo.subscriptions.impl.SubscriptionsChecker
35+
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.DUCK_AI
3536
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.ITR
3637
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_ITR
3738
import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.LEGACY_FE_NETP
@@ -192,13 +193,15 @@ class SubscriptionWebViewViewModel @Inject constructor(
192193
NETP, LEGACY_FE_NETP -> networkProtectionAccessState.getScreenForCurrentState()?.let { GoToNetP(it) }
193194
ITR, LEGACY_FE_ITR, ROW_ITR -> GoToITR
194195
PIR, LEGACY_FE_PIR -> GoToPIR
196+
DUCK_AI -> GoToDuckAI
195197
else -> null
196198
}
197199
if (hasPurchasedSubscription()) {
198200
when (commandToSend) {
199201
GoToITR -> pixelSender.reportOnboardingIdtrClick()
200202
is GoToNetP -> pixelSender.reportOnboardingVpnClick()
201203
GoToPIR -> pixelSender.reportOnboardingPirClick()
204+
GoToDuckAI -> pixelSender.reportOnboardingDuckAiClick()
202205
else -> {} // no-op
203206
}
204207
}
@@ -427,6 +430,7 @@ class SubscriptionWebViewViewModel @Inject constructor(
427430
data object GoToITR : Command()
428431
data object GoToPIR : Command()
429432
data class GoToNetP(val activityParams: ActivityParams) : Command()
433+
data object GoToDuckAI : Command()
430434
data object Reload : Command()
431435
}
432436

subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionsWebViewActivity.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import com.duckduckgo.downloads.api.DownloadStateListener
5858
import com.duckduckgo.downloads.api.DownloadsFileActions
5959
import com.duckduckgo.downloads.api.FileDownloader
6060
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
61+
import com.duckduckgo.duckchat.api.DuckChat
6162
import com.duckduckgo.js.messaging.api.JsCallbackData
6263
import com.duckduckgo.js.messaging.api.JsMessageCallback
6364
import com.duckduckgo.js.messaging.api.JsMessaging
@@ -78,6 +79,7 @@ import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionActivity.Companio
7879
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command
7980
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettings
8081
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.BackToSettingsActivateSuccess
82+
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToDuckAI
8183
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToITR
8284
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToNetP
8385
import com.duckduckgo.subscriptions.impl.ui.SubscriptionWebViewViewModel.Command.GoToPIR
@@ -161,6 +163,9 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
161163
@Inject
162164
lateinit var pixelSender: SubscriptionPixelSender
163165

166+
@Inject
167+
lateinit var duckChat: DuckChat
168+
164169
private val viewModel: SubscriptionWebViewViewModel by bindViewModel()
165170

166171
private val binding: ActivitySubscriptionsWebviewBinding by viewBinding()
@@ -422,6 +427,7 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
422427
is GoToITR -> goToITR()
423428
is GoToPIR -> goToPIR()
424429
is GoToNetP -> goToNetP(command.activityParams)
430+
is GoToDuckAI -> goToDuckAI()
425431
Reload -> binding.webview.reload()
426432
}
427433
}
@@ -447,6 +453,10 @@ class SubscriptionsWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
447453
globalActivityStarter.start(this, params)
448454
}
449455

456+
private fun goToDuckAI() {
457+
duckChat.openDuckChat()
458+
}
459+
450460
private fun renderPurchaseState(purchaseState: PurchaseStateView) {
451461
when (purchaseState) {
452462
is PurchaseStateView.InProgress, PurchaseStateView.Inactive -> {

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterfaceTest.kt

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,13 +790,14 @@ class SubscriptionMessagingInterfaceTest {
790790
givenInterfaceIsRegistered()
791791
givenSubscriptionMessaging(enabled = true)
792792
givenAuthV2(enabled = true)
793+
givenDuckAiPlus(enabled = true)
793794

794795
val expected = JsRequestResponse.Success(
795796
context = "subscriptionPages",
796797
featureName = "useSubscription",
797798
method = "getFeatureConfig",
798799
id = "myId",
799-
result = JSONObject("""{"useSubscriptionsAuthV2":true}"""),
800+
result = JSONObject("""{"useSubscriptionsAuthV2":true,"usePaidDuckAi":true}"""),
800801
)
801802

802803
val message = """
@@ -818,13 +819,43 @@ class SubscriptionMessagingInterfaceTest {
818819
givenInterfaceIsRegistered()
819820
givenSubscriptionMessaging(enabled = true)
820821
givenAuthV2(enabled = false)
822+
givenDuckAiPlus(enabled = true)
821823

822824
val expected = JsRequestResponse.Success(
823825
context = "subscriptionPages",
824826
featureName = "useSubscription",
825827
method = "getFeatureConfig",
826828
id = "myId",
827-
result = JSONObject("""{"useSubscriptionsAuthV2":false}"""),
829+
result = JSONObject("""{"useSubscriptionsAuthV2":false,"usePaidDuckAi":true}"""),
830+
)
831+
832+
val message = """
833+
{"context":"subscriptionPages","featureName":"useSubscription","method":"getFeatureConfig","id":"myId","params":{}}
834+
""".trimIndent()
835+
836+
messagingInterface.process(message, "duckduckgo-android-messaging-secret")
837+
838+
val captor = argumentCaptor<JsRequestResponse>()
839+
verify(jsMessageHelper).sendJsResponse(captor.capture(), eq(CALLBACK_NAME), eq(SECRET), eq(webView))
840+
val jsMessage = captor.firstValue
841+
842+
assertTrue(jsMessage is JsRequestResponse.Success)
843+
checkEquals(expected, jsMessage)
844+
}
845+
846+
@Test
847+
fun `when process and get feature config and messaging enabled but duck ai plus disabled then return response with duck ai false`() = runTest {
848+
givenInterfaceIsRegistered()
849+
givenSubscriptionMessaging(enabled = true)
850+
givenAuthV2(enabled = true)
851+
givenDuckAiPlus(enabled = false)
852+
853+
val expected = JsRequestResponse.Success(
854+
context = "subscriptionPages",
855+
featureName = "useSubscription",
856+
method = "getFeatureConfig",
857+
id = "myId",
858+
result = JSONObject("""{"useSubscriptionsAuthV2":true,"usePaidDuckAi":false}"""),
828859
)
829860

830861
val message = """
@@ -920,6 +951,12 @@ class SubscriptionMessagingInterfaceTest {
920951
whenever(privacyProFeature.enableSubscriptionFlowsV2()).thenReturn(v2SubscriptionFlow)
921952
}
922953

954+
private fun givenDuckAiPlus(enabled: Boolean) {
955+
val duckAiPlusToggle = mock<com.duckduckgo.feature.toggles.api.Toggle>()
956+
whenever(duckAiPlusToggle.isEnabled()).thenReturn(enabled)
957+
whenever(privacyProFeature.duckAiPlus()).thenReturn(duckAiPlusToggle)
958+
}
959+
923960
private fun checkEquals(expected: JsRequestResponse, actual: JsRequestResponse) {
924961
if (expected is JsRequestResponse.Success && actual is JsRequestResponse.Success) {
925962
assertEquals(expected.id, actual.id)

subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionWebViewViewModelTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,20 @@ class SubscriptionWebViewViewModelTest {
487487
}
488488
}
489489

490+
@Test
491+
fun whenFeatureSelectedAndFeatureIsDuckAiThenCommandSent() = runTest {
492+
givenSubscriptionStatus(EXPIRED)
493+
viewModel.commands().test {
494+
viewModel.processJsCallbackMessage(
495+
"test",
496+
"featureSelected",
497+
null,
498+
JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
499+
)
500+
assertTrue(awaitItem() is Command.GoToDuckAI)
501+
}
502+
}
503+
490504
@Test
491505
fun whenSubscriptionSelectedThenPixelIsSent() = runTest {
492506
viewModel.processJsCallbackMessage(
@@ -622,6 +636,34 @@ class SubscriptionWebViewViewModelTest {
622636
verifyNoInteractions(pixelSender)
623637
}
624638

639+
@Test
640+
fun whenFeatureSelectedAndFeatureIsDuckAiAndInPurchaseFlowThenPixelIsSent() = runTest {
641+
givenSubscriptionStatus(AUTO_RENEWABLE)
642+
whenever(subscriptionsManager.currentPurchaseState).thenReturn(flowOf(CurrentPurchase.Success))
643+
viewModel.start()
644+
645+
viewModel.processJsCallbackMessage(
646+
featureName = "test",
647+
method = "featureSelected",
648+
id = null,
649+
data = JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
650+
)
651+
verify(pixelSender).reportOnboardingDuckAiClick()
652+
}
653+
654+
@Test
655+
fun whenFeatureSelectedAndFeatureIsDuckAiAndNotInPurchaseFlowThenPixelIsNotSent() = runTest {
656+
givenSubscriptionStatus(AUTO_RENEWABLE)
657+
658+
viewModel.processJsCallbackMessage(
659+
featureName = "test",
660+
method = "featureSelected",
661+
id = null,
662+
data = JSONObject("""{"feature":"${SubscriptionsConstants.DUCK_AI}"}"""),
663+
)
664+
verifyNoInteractions(pixelSender)
665+
}
666+
625667
@Test
626668
fun whenSubscriptionsWelcomeFaqClickedAndInPurchaseFlowThenPixelIsSent() = runTest {
627669
givenSubscriptionStatus(AUTO_RENEWABLE)

0 commit comments

Comments
 (0)