Skip to content

Commit f4f1ba0

Browse files
committed
support post subscription duckai card click
1 parent 1a42fd5 commit f4f1ba0

File tree

9 files changed

+158
-0
lines changed

9 files changed

+158
-0
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,22 @@ interface PrivacyProFeature {
167167
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
168168
fun privacyProFreeTrial(): Toggle
169169

170+
/**
171+
* Enables/Disables duckAi for subscribers (advanced models)
172+
* This flag is used to hide the feature in the native client and FE.
173+
* It will be used for the feature rollout and kill-switch if necessary.
174+
*/
170175
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
171176
fun duckAiPlus(): Toggle
177+
178+
/**
179+
* When enabled, we signal FE v2 is available.
180+
* When enabled, FE requests token using getAuthAccessToken
181+
* When disabled, FE requests token using getAccessToken
182+
* Android supports v2, but messaging is new. Use this flag as kill-switch if necessary.
183+
*/
184+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
185+
fun subscriptionMessagingV2(): Toggle
172186
}
173187

174188
@ContributesBinding(AppScope::class)

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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.duckduckgo.js.messaging.api.SubscriptionEventData
3535
import com.duckduckgo.subscriptions.impl.AccessTokenResult
3636
import com.duckduckgo.subscriptions.impl.AuthTokenResult
3737
import com.duckduckgo.subscriptions.impl.JSONObjectAdapter
38+
import com.duckduckgo.subscriptions.impl.PrivacyProFeature
3839
import com.duckduckgo.subscriptions.impl.SubscriptionsChecker
3940
import com.duckduckgo.subscriptions.impl.SubscriptionsManager
4041
import com.duckduckgo.subscriptions.impl.pixels.SubscriptionPixelSender
@@ -57,6 +58,7 @@ class SubscriptionMessagingInterface @Inject constructor(
5758
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
5859
pixelSender: SubscriptionPixelSender,
5960
subscriptionsChecker: SubscriptionsChecker,
61+
private val privacyProFeature: PrivacyProFeature,
6062
) : JsMessaging {
6163
private val moshi = Moshi.Builder().add(JSONObjectAdapter()).build()
6264

@@ -69,6 +71,8 @@ class SubscriptionMessagingInterface @Inject constructor(
6971
SetSubscriptionMessage(subscriptionsManager, appCoroutineScope, dispatcherProvider, pixelSender, subscriptionsChecker),
7072
InformationalEventsMessage(subscriptionsManager, appCoroutineScope, pixelSender),
7173
GetAccessTokenMessage(subscriptionsManager),
74+
GetAuthAccessTokenMessage(subscriptionsManager),
75+
GetFeatureConfigMessage(privacyProFeature),
7276
)
7377

7478
@JavascriptInterface
@@ -311,4 +315,76 @@ class SubscriptionMessagingInterface @Inject constructor(
311315
override val featureName: String = "useSubscription"
312316
override val methods: List<String> = listOf("getAccessToken")
313317
}
318+
319+
private inner class GetAuthAccessTokenMessage(
320+
private val subscriptionsManager: SubscriptionsManager,
321+
) : JsMessageHandler {
322+
323+
override fun process(
324+
jsMessage: JsMessage,
325+
secret: String,
326+
jsMessageCallback: JsMessageCallback?,
327+
) {
328+
val jsMessageId = jsMessage.id ?: return
329+
330+
val pat: AccessTokenResult = runBlocking {
331+
subscriptionsManager.getAccessToken()
332+
}
333+
334+
val resultJson = when (pat) {
335+
is AccessTokenResult.Success -> JSONObject().apply {
336+
put("accessToken", pat.accessToken)
337+
}
338+
339+
is AccessTokenResult.Failure -> JSONObject()
340+
}
341+
342+
val response = JsRequestResponse.Success(
343+
context = jsMessage.context,
344+
featureName = featureName,
345+
method = jsMessage.method,
346+
id = jsMessageId,
347+
result = resultJson,
348+
)
349+
350+
jsMessageHelper.sendJsResponse(response, callbackName, secret, webView)
351+
}
352+
353+
override val allowedDomains: List<String> = emptyList()
354+
override val featureName: String = "useSubscription"
355+
override val methods: List<String> = listOf("getAuthAccessToken")
356+
}
357+
358+
private inner class GetFeatureConfigMessage(
359+
private val privacyProFeature: PrivacyProFeature,
360+
) : JsMessageHandler {
361+
override fun process(
362+
jsMessage: JsMessage,
363+
secret: String,
364+
jsMessageCallback: JsMessageCallback?,
365+
) {
366+
val jsMessageId = jsMessage.id ?: return
367+
368+
val useAuthV2Messaging = privacyProFeature.subscriptionMessagingV2().isEnabled()
369+
val duckAiSubscriberModelsEnabled = privacyProFeature.duckAiPlus().isEnabled()
370+
val resultJson = JSONObject().apply {
371+
put("useSubscriptionsAuthV2", useAuthV2Messaging)
372+
put("usePaidDuckAi", duckAiSubscriberModelsEnabled)
373+
}
374+
375+
val response = JsRequestResponse.Success(
376+
context = jsMessage.context,
377+
featureName = featureName,
378+
method = jsMessage.method,
379+
id = jsMessageId,
380+
result = resultJson,
381+
)
382+
383+
jsMessageHelper.sendJsResponse(response, callbackName, secret, webView)
384+
}
385+
386+
override val allowedDomains: List<String> = emptyList()
387+
override val featureName: String = "useSubscription"
388+
override val methods: List<String> = listOf("getFeatureConfig")
389+
}
314390
}

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/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)