Skip to content

Refresh duck.ai when subscription state changes #6395

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

Open
wants to merge 1 commit into
base: feature/cristian/subscriptions/show_duck_ai_pro_settings
Choose a base branch
from
Open
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
Expand Up @@ -70,6 +70,7 @@ import com.duckduckgo.duckchat.impl.ui.filechooser.capture.launcher.UploadFromEx
import com.duckduckgo.duckchat.impl.ui.filechooser.capture.launcher.UploadFromExternalMediaAppLauncher.MediaCaptureResult.NoMediaCaptured
import com.duckduckgo.js.messaging.api.JsMessageCallback
import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.navigation.api.GlobalActivityStarter
import com.duckduckgo.navigation.api.getActivityParams
import com.duckduckgo.subscriptions.api.SUBSCRIPTIONS_FEATURE_NAME
Expand All @@ -80,6 +81,8 @@ import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
Expand All @@ -92,6 +95,8 @@ internal data class DuckChatWebViewActivityWithParams(
@ContributeToActivityStarter(DuckChatWebViewActivityWithParams::class)
open class DuckChatWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationDialogListener {

private val viewModel: DuckChatWebViewActivityViewModel by bindViewModel()

@Inject
lateinit var webViewClient: DuckChatWebViewClient

Expand Down Expand Up @@ -284,6 +289,21 @@ open class DuckChatWebViewActivity : DuckDuckGoActivity(), DownloadConfirmationD
}
pendingUploadTask = null
}

// Observe ViewModel commands
viewModel.commands
.onEach { command ->
when (command) {
is DuckChatWebViewActivityViewModel.Command.SendSubscriptionAuthUpdateEvent -> {
val authUpdateEvent = SubscriptionEventData(
featureName = SUBSCRIPTIONS_FEATURE_NAME,
subscriptionName = "authUpdate",
params = org.json.JSONObject(),
)
contentScopeScripts.sendSubscriptionEvent(authUpdateEvent)
}
}
}.launchIn(lifecycleScope)
}

data class FileChooserRequestedParams(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.di.scopes.ActivityScope
import com.duckduckgo.subscriptions.api.Subscriptions
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.logcat

@ContributesViewModel(ActivityScope::class)
class DuckChatWebViewActivityViewModel @Inject constructor(
private val subscriptions: Subscriptions,
) : ViewModel() {

private val commandChannel = Channel<Command>(capacity = 1, onBufferOverflow = DROP_OLDEST)
val commands = commandChannel.receiveAsFlow()

sealed class Command {
data object SendSubscriptionAuthUpdateEvent : Command()
}

init {
observeSubscriptionChanges()
}

private fun observeSubscriptionChanges() {
subscriptions.getSubscriptionStatusFlow()
.distinctUntilChanged()
.onEach { _ ->
logcat { "CRIS: SUBSCRIPTION STATUS CHANGED" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a debugging leftover.

commandChannel.trySend(Command.SendSubscriptionAuthUpdateEvent)
}.launchIn(viewModelScope)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import android.webkit.WebView
import androidx.annotation.AnyThread
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.di.AppCoroutineScope
Expand All @@ -48,6 +49,7 @@ import com.duckduckgo.common.ui.view.makeSnackbarWithNoBottomInset
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.common.utils.ConflatedJob
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.FragmentViewModelFactory
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.downloads.api.DOWNLOAD_SNACKBAR_DELAY
import com.duckduckgo.downloads.api.DOWNLOAD_SNACKBAR_LENGTH
Expand All @@ -72,6 +74,7 @@ import com.duckduckgo.duckchat.impl.ui.filechooser.capture.launcher.UploadFromEx
import com.duckduckgo.duckchat.impl.ui.filechooser.capture.launcher.UploadFromExternalMediaAppLauncher.MediaCaptureResult.NoMediaCaptured
import com.duckduckgo.js.messaging.api.JsMessageCallback
import com.duckduckgo.js.messaging.api.JsMessaging
import com.duckduckgo.js.messaging.api.SubscriptionEventData
import com.duckduckgo.subscriptions.api.SUBSCRIPTIONS_FEATURE_NAME
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
Expand All @@ -80,13 +83,22 @@ import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject

@InjectWith(FragmentScope::class)
open class DuckChatWebViewFragment : DuckDuckGoFragment(R.layout.activity_duck_chat_webview), DownloadConfirmationDialogListener {

@Inject
lateinit var viewModelFactory: FragmentViewModelFactory

private val viewModel: DuckChatWebViewViewModel by lazy {
ViewModelProvider(this, viewModelFactory)[DuckChatWebViewViewModel::class.java]
}

@Inject
lateinit var webViewClient: DuckChatWebViewClient

Expand Down Expand Up @@ -279,6 +291,21 @@ open class DuckChatWebViewFragment : DuckDuckGoFragment(R.layout.activity_duck_c
}
pendingUploadTask = null
}

// Observe ViewModel commands
viewModel.commands
.onEach { command ->
when (command) {
is DuckChatWebViewViewModel.Command.SendSubscriptionAuthUpdateEvent -> {
val authUpdateEvent = SubscriptionEventData(
featureName = SUBSCRIPTIONS_FEATURE_NAME,
subscriptionName = "authUpdate",
params = JSONObject(),
)
contentScopeScripts.sendSubscriptionEvent(authUpdateEvent)
}
}
}.launchIn(lifecycleScope)
}

data class FileChooserRequestedParams(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.subscriptions.api.Subscriptions
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.logcat

@ContributesViewModel(FragmentScope::class)
class DuckChatWebViewViewModel @Inject constructor(
private val subscriptions: Subscriptions,
) : ViewModel() {

private val commandChannel = Channel<Command>(capacity = 1, onBufferOverflow = DROP_OLDEST)
val commands = commandChannel.receiveAsFlow()

sealed class Command {
data object SendSubscriptionAuthUpdateEvent : Command()
}

init {
observeSubscriptionChanges()
}

private fun observeSubscriptionChanges() {
subscriptions.getSubscriptionStatusFlow()
.distinctUntilChanged()
.onEach { _ ->
logcat { "CRIS: SUBSCRIPTION STATUS CHANGED" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a debugging leftover.

commandChannel.trySend(Command.SendSubscriptionAuthUpdateEvent)
}.launchIn(viewModelScope)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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.ui

import androidx.test.ext.junit.runners.AndroidJUnit4
import app.cash.turbine.test
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.duckchat.impl.ui.DuckChatWebViewActivityViewModel.Command
import com.duckduckgo.subscriptions.api.SubscriptionStatus
import com.duckduckgo.subscriptions.api.SubscriptionStatus.AUTO_RENEWABLE
import com.duckduckgo.subscriptions.api.SubscriptionStatus.EXPIRED
import com.duckduckgo.subscriptions.api.SubscriptionStatus.INACTIVE
import com.duckduckgo.subscriptions.api.SubscriptionStatus.UNKNOWN
import com.duckduckgo.subscriptions.api.Subscriptions
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class DuckChatWebViewActivityViewModelTest {

@get:Rule
val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()

private val subscriptions: Subscriptions = mock()
private val subscriptionStatusFlow = MutableSharedFlow<SubscriptionStatus>()

private lateinit var viewModel: DuckChatWebViewActivityViewModel

@Before
fun setup() {
whenever(subscriptions.getSubscriptionStatusFlow()).thenReturn(subscriptionStatusFlow)
viewModel = DuckChatWebViewActivityViewModel(subscriptions)
}

@Test
fun whenSubscriptionStatusChangesToActiveThenSendSubscriptionAuthUpdateEventCommand() = runTest {
viewModel.commands.test {
subscriptionStatusFlow.emit(AUTO_RENEWABLE)

val command = awaitItem()
assertTrue(command is Command.SendSubscriptionAuthUpdateEvent)
}
}

@Test
fun whenSubscriptionStatusChangesToInactiveThenSendSubscriptionAuthUpdateEventCommand() = runTest {
viewModel.commands.test {
subscriptionStatusFlow.emit(INACTIVE)

val command = awaitItem()
assertTrue(command is Command.SendSubscriptionAuthUpdateEvent)
}
}

@Test
fun whenSubscriptionStatusChangesToExpiredThenSendSubscriptionAuthUpdateEventCommand() = runTest {
viewModel.commands.test {
subscriptionStatusFlow.emit(EXPIRED)

val command = awaitItem()
assertTrue(command is Command.SendSubscriptionAuthUpdateEvent)
}
}

@Test
fun whenSubscriptionStatusChangesToUnknownThenSendSubscriptionAuthUpdateEventCommand() = runTest {
viewModel.commands.test {
subscriptionStatusFlow.emit(UNKNOWN)

val command = awaitItem()
assertTrue(command is Command.SendSubscriptionAuthUpdateEvent)
}
}

@Test
fun whenSubscriptionStatusChangesTwiceToSameValueThenOnlyOneCommandSent() = runTest {
viewModel.commands.test {
// Emit the same status twice
subscriptionStatusFlow.emit(AUTO_RENEWABLE)
subscriptionStatusFlow.emit(AUTO_RENEWABLE)

// Should only receive one command due to distinctUntilChanged
val command = awaitItem()
assertTrue(command is Command.SendSubscriptionAuthUpdateEvent)
expectNoEvents()
}
}

@Test
fun whenSubscriptionStatusChangesTwiceToDifferentValuesThenTwoCommandsSent() = runTest {
viewModel.commands.test {
subscriptionStatusFlow.emit(AUTO_RENEWABLE)
subscriptionStatusFlow.emit(EXPIRED)

val firstCommand = awaitItem()
assertTrue(firstCommand is Command.SendSubscriptionAuthUpdateEvent)

val secondCommand = awaitItem()
assertTrue(secondCommand is Command.SendSubscriptionAuthUpdateEvent)
}
}

@Test
fun whenMultipleSubscriptionStatusChangesOccurThenCorrespondingCommandsSent() = runTest {
viewModel.commands.test {
subscriptionStatusFlow.emit(UNKNOWN)
subscriptionStatusFlow.emit(INACTIVE)
subscriptionStatusFlow.emit(AUTO_RENEWABLE)
subscriptionStatusFlow.emit(EXPIRED)

repeat(4) {
val command = awaitItem()
assertTrue(command is Command.SendSubscriptionAuthUpdateEvent)
}
}
}
}
Loading
Loading