From 5f8f3a347550a881f8be9b0c3634527ba234d73a Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 13 Dec 2023 10:44:31 +0000 Subject: [PATCH] Second iteration for settings (#3956) Task/Issue URL: https://app.asana.com/0/0/1205820670085181/f ### Description See task ### Steps to test this PR See task --- .../settings/SubsSettingsPlugin.kt | 2 +- .../settings/plugins/SubsSettingsPlugins.kt | 21 +++- ...ProSettingBuyView.kt => ItrSettingView.kt} | 46 ++++--- ...BuyViewModel.kt => ItrSettingViewModel.kt} | 41 +++++-- .../impl/settings/views/PirSettingView.kt | 116 ++++++++++++++++++ .../settings/views/PirSettingViewModel.kt | 87 +++++++++++++ .../impl/settings/views/ProSettingView.kt | 49 ++++++-- .../settings/views/ProSettingViewModel.kt | 8 +- ...settings_buy.xml => view_itr_settings.xml} | 8 +- .../src/main/res/layout/view_pir_settings.xml | 25 ++++ .../src/main/res/layout/view_settings.xml | 36 +++++- .../src/main/res/values/donottranslate.xml | 9 ++ .../settings/views/ItrSettingViewModelTest.kt | 60 +++++++++ .../settings/views/PirSettingViewModelTest.kt | 60 +++++++++ .../views/ProSettingBuyViewModelTest.kt | 32 ----- .../settings/views/ProSettingViewModelTest.kt | 10 ++ 16 files changed, 531 insertions(+), 79 deletions(-) rename subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/{ProSettingBuyView.kt => ItrSettingView.kt} (64%) rename subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/{ProSettingBuyViewModel.kt => ItrSettingViewModel.kt} (51%) create mode 100644 subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt create mode 100644 subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt rename subscriptions/subscriptions-impl/src/main/res/layout/{view_settings_buy.xml => view_itr_settings.xml} (82%) create mode 100644 subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml create mode 100644 subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt create mode 100644 subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt delete mode 100644 subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyViewModelTest.kt diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/SubsSettingsPlugin.kt b/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/SubsSettingsPlugin.kt index cbd23ed9558b..ab55ff663b8a 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/SubsSettingsPlugin.kt +++ b/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/SubsSettingsPlugin.kt @@ -25,7 +25,7 @@ import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject @ContributesMultibinding(ActivityScope::class) -@PositionKey(150) +@PositionKey(200) class ProSettingsNetP @Inject constructor() : ProSettingsPlugin { override fun getView(context: Context): View { return ProSettingNetPView(context) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt index fc4f0331daab..029ab77f0357 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/plugins/SubsSettingsPlugins.kt @@ -23,7 +23,8 @@ import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.settings.api.PositionKey import com.duckduckgo.settings.api.ProSettingsPlugin import com.duckduckgo.subscriptions.impl.R -import com.duckduckgo.subscriptions.impl.settings.views.ProSettingBuyView +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingView +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingView import com.duckduckgo.subscriptions.impl.settings.views.ProSettingView import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject @@ -39,17 +40,25 @@ class ProSettingsTitle @Inject constructor() : ProSettingsPlugin { } @ContributesMultibinding(scope = ActivityScope::class) -@PositionKey(200) -class ProSettingBuy @Inject constructor() : ProSettingsPlugin { +@PositionKey(500) +class ProSettings @Inject constructor() : ProSettingsPlugin { override fun getView(context: Context): View { - return ProSettingBuyView(context) + return ProSettingView(context) } } @ContributesMultibinding(scope = ActivityScope::class) @PositionKey(300) -class ProSettings @Inject constructor() : ProSettingsPlugin { +class PIRSettings @Inject constructor() : ProSettingsPlugin { override fun getView(context: Context): View { - return ProSettingView(context) + return PirSettingView(context) + } +} + +@ContributesMultibinding(scope = ActivityScope::class) +@PositionKey(400) +class ITRSettings @Inject constructor() : ProSettingsPlugin { + override fun getView(context: Context): View { + return ItrSettingView(context) } } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingView.kt similarity index 64% rename from subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyView.kt rename to subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingView.kt index fdf99bdeef7c..ad26cd9cf60e 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyView.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingView.kt @@ -20,19 +20,22 @@ import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout +import android.widget.Toast import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.navigation.api.GlobalActivityStarter -import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BUY_URL -import com.duckduckgo.subscriptions.impl.databinding.ViewSettingsBuyBinding -import com.duckduckgo.subscriptions.impl.settings.views.ProSettingBuyViewModel.Command -import com.duckduckgo.subscriptions.impl.settings.views.ProSettingBuyViewModel.Command.OpenBuyScreen -import com.duckduckgo.subscriptions.impl.settings.views.ProSettingBuyViewModel.Factory -import com.duckduckgo.subscriptions.impl.ui.SubscriptionsWebViewActivityWithParams +import com.duckduckgo.subscriptions.impl.databinding.ViewItrSettingsBinding +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Command +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Command.OpenItr +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Factory +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.ViewState import dagger.android.support.AndroidSupportInjection import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,7 +46,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @InjectWith(ViewScope::class) -class ProSettingBuyView @JvmOverloads constructor( +class ItrSettingView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0, @@ -57,10 +60,10 @@ class ProSettingBuyView @JvmOverloads constructor( private var coroutineScope: CoroutineScope? = null - private val binding: ViewSettingsBuyBinding by viewBinding() + private val binding: ViewItrSettingsBinding by viewBinding() - private val viewModel: ProSettingBuyViewModel by lazy { - ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[ProSettingBuyViewModel::class.java] + private val viewModel: ItrSettingViewModel by lazy { + ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[ItrSettingViewModel::class.java] } private var job: ConflatedJob = ConflatedJob() @@ -69,8 +72,10 @@ class ProSettingBuyView @JvmOverloads constructor( AndroidSupportInjection.inject(this) super.onAttachedToWindow() - binding.buy.setClickListener { - viewModel.onBuyClicked() + ViewTreeLifecycleOwner.get(this)?.lifecycle?.addObserver(viewModel) + + binding.itrSettings.setClickListener { + viewModel.onItr() } @SuppressLint("NoHardcodedCoroutineDispatcher") @@ -79,19 +84,32 @@ class ProSettingBuyView @JvmOverloads constructor( job += viewModel.commands() .onEach { processCommands(it) } .launchIn(coroutineScope!!) + + viewModel.viewState + .onEach { renderView(it) } + .launchIn(coroutineScope!!) } override fun onDetachedFromWindow() { super.onDetachedFromWindow() + ViewTreeLifecycleOwner.get(this)?.lifecycle?.removeObserver(viewModel) coroutineScope?.cancel() job.cancel() coroutineScope = null } + private fun renderView(viewState: ViewState) { + if (viewState.hasSubscription) { + binding.itrSettings.show() + } else { + binding.itrSettings.gone() + } + } + private fun processCommands(command: Command) { when (command) { - is OpenBuyScreen -> { - globalActivityStarter.start(context, SubscriptionsWebViewActivityWithParams(url = BUY_URL, "Buy Subscription")) + is OpenItr -> { + Toast.makeText(context, "Open ITR", Toast.LENGTH_SHORT).show() } } } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt similarity index 51% rename from subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyViewModel.kt rename to subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt index 9f6f232adad1..4becd1b71b84 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModel.kt @@ -16,28 +16,52 @@ package com.duckduckgo.subscriptions.impl.settings.views +import android.annotation.SuppressLint +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.duckduckgo.subscriptions.impl.settings.views.ProSettingBuyViewModel.Command.OpenBuyScreen +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Command.OpenItr import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -class ProSettingBuyViewModel : ViewModel() { +@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle +class ItrSettingViewModel( + private val subscriptionsManager: SubscriptionsManager, + private val dispatcherProvider: DispatcherProvider, +) : ViewModel(), DefaultLifecycleObserver { sealed class Command { - object OpenBuyScreen : Command() + data object OpenItr : Command() } private val command = Channel(1, BufferOverflow.DROP_OLDEST) internal fun commands(): Flow = command.receiveAsFlow() + data class ViewState(val hasSubscription: Boolean = false) - fun onBuyClicked() { - sendCommand(OpenBuyScreen) + private val _viewState = MutableStateFlow(ViewState()) + val viewState = _viewState.asStateFlow() + + fun onItr() { + sendCommand(OpenItr) + } + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + viewModelScope.launch(dispatcherProvider.io()) { + subscriptionsManager.hasSubscription.collect { + _viewState.emit(viewState.value.copy(hasSubscription = it)) + } + } } private fun sendCommand(newCommand: Command) { @@ -47,11 +71,14 @@ class ProSettingBuyViewModel : ViewModel() { } @Suppress("UNCHECKED_CAST") - class Factory @Inject constructor() : ViewModelProvider.NewInstanceFactory() { + class Factory @Inject constructor( + private val subscriptionsManager: SubscriptionsManager, + private val dispatcherProvider: DispatcherProvider, + ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { return with(modelClass) { when { - isAssignableFrom(ProSettingBuyViewModel::class.java) -> ProSettingBuyViewModel() + isAssignableFrom(ItrSettingViewModel::class.java) -> ItrSettingViewModel(subscriptionsManager, dispatcherProvider) else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt new file mode 100644 index 000000000000..71b3a73da36c --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingView.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 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.subscriptions.impl.settings.views + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.show +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.subscriptions.impl.databinding.ViewPirSettingsBinding +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command.OpenPir +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Factory +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.ViewState +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@InjectWith(ViewScope::class) +class PirSettingView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + @Inject + lateinit var viewModelFactory: Factory + + @Inject + lateinit var globalActivityStarter: GlobalActivityStarter + + private var coroutineScope: CoroutineScope? = null + + private val binding: ViewPirSettingsBinding by viewBinding() + + private val viewModel: PirSettingViewModel by lazy { + ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[PirSettingViewModel::class.java] + } + + private var job: ConflatedJob = ConflatedJob() + + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + + ViewTreeLifecycleOwner.get(this)?.lifecycle?.addObserver(viewModel) + + binding.pirSettings.setClickListener { + viewModel.onPir() + } + + @SuppressLint("NoHardcodedCoroutineDispatcher") + coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + job += viewModel.commands() + .onEach { processCommands(it) } + .launchIn(coroutineScope!!) + + viewModel.viewState + .onEach { renderView(it) } + .launchIn(coroutineScope!!) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + ViewTreeLifecycleOwner.get(this)?.lifecycle?.removeObserver(viewModel) + coroutineScope?.cancel() + job.cancel() + coroutineScope = null + } + + private fun renderView(viewState: ViewState) { + if (viewState.hasSubscription) { + binding.pirSettings.show() + } else { + binding.pirSettings.gone() + } + } + + private fun processCommands(command: Command) { + when (command) { + is OpenPir -> { + Toast.makeText(context, "Open PIR", Toast.LENGTH_SHORT).show() + } + } + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt new file mode 100644 index 000000000000..9447329f2600 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 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.subscriptions.impl.settings.views + +import android.annotation.SuppressLint +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command.OpenPir +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle +class PirSettingViewModel( + private val subscriptionsManager: SubscriptionsManager, + private val dispatcherProvider: DispatcherProvider, +) : ViewModel(), DefaultLifecycleObserver { + + sealed class Command { + data object OpenPir : Command() + } + + private val command = Channel(1, BufferOverflow.DROP_OLDEST) + internal fun commands(): Flow = command.receiveAsFlow() + data class ViewState(val hasSubscription: Boolean = false) + + private val _viewState = MutableStateFlow(ViewState()) + val viewState = _viewState.asStateFlow() + + fun onPir() { + sendCommand(OpenPir) + } + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + viewModelScope.launch(dispatcherProvider.io()) { + subscriptionsManager.hasSubscription.collect { + _viewState.emit(viewState.value.copy(hasSubscription = it)) + } + } + } + + private fun sendCommand(newCommand: Command) { + viewModelScope.launch { + command.send(newCommand) + } + } + + @Suppress("UNCHECKED_CAST") + class Factory @Inject constructor( + private val subscriptionsManager: SubscriptionsManager, + private val dispatcherProvider: DispatcherProvider, + ) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return with(modelClass) { + when { + isAssignableFrom(PirSettingViewModel::class.java) -> PirSettingViewModel(subscriptionsManager, dispatcherProvider) + else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + } + } as T + } + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt index a4aa93b71633..e19fe6b1afcb 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingView.kt @@ -19,22 +19,30 @@ package com.duckduckgo.subscriptions.impl.settings.views import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet +import android.view.MotionEvent import android.widget.FrameLayout +import android.widget.LinearLayout import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.common.utils.extensions.html import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.subscriptions.impl.R +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants import com.duckduckgo.subscriptions.impl.databinding.ViewSettingsBinding import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command +import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenBuyScreen import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenSettings import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Factory import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.ViewState import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsActivity.Companion.SubscriptionsSettingsScreenWithEmptyParams +import com.duckduckgo.subscriptions.impl.ui.SubscriptionsWebViewActivityWithParams import dagger.android.support.AndroidSupportInjection import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -73,10 +81,6 @@ class ProSettingView @JvmOverloads constructor( ViewTreeLifecycleOwner.get(this)?.lifecycle?.addObserver(viewModel) - binding.settings.setClickListener { - viewModel.onSettings() - } - @SuppressLint("NoHardcodedCoroutineDispatcher") coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) @@ -97,13 +101,29 @@ class ProSettingView @JvmOverloads constructor( coroutineScope = null } + @SuppressLint("ClickableViewAccessibility") private fun renderView(viewState: ViewState) { + binding.subscriptionSetting.setOnClickListener(null) + binding.subscriptionSetting.setOnTouchListener(null) + binding.subscriptionBuy.setOnClickListener(null) + binding.subscriptionBuy.setOnTouchListener(null) + if (viewState.hasSubscription) { - binding.settings.setPrimaryText(context.getString(R.string.subscriptionSetting)) - binding.settings.setSecondaryText("") + binding.subscriptionBuy.gone() + binding.subscribeSecondary.gone() + binding.subscriptionSetting.show() + binding.settingContainer.setOnClickListener { + viewModel.onSettings() + } } else { - binding.settings.setPrimaryText(context.getString(R.string.subscriptionSettingSubscribe)) - binding.settings.setSecondaryText(context.getString(R.string.subscriptionSettingSubscribeSubtitle)) + val htmlText = context.getString(R.string.subscriptionSettingFeaturesList).html(context) + binding.subscribeSecondary.show() + binding.subscribeSecondary.text = htmlText + binding.subscriptionBuy.show() + binding.subscriptionSetting.gone() + binding.settingContainer.setOnClickListener { + viewModel.onBuy() + } } } @@ -112,6 +132,19 @@ class ProSettingView @JvmOverloads constructor( is OpenSettings -> { globalActivityStarter.start(context, SubscriptionsSettingsScreenWithEmptyParams) } + is OpenBuyScreen -> { + globalActivityStarter.start(context, SubscriptionsWebViewActivityWithParams(url = SubscriptionsConstants.BUY_URL, "")) + } } } } + +class SubscriptionSettingLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr) { + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } +} diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt index 6e052069f266..b147c551ba85 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModel.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenBuyScreen import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenSettings import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow @@ -41,7 +42,8 @@ class ProSettingViewModel( ) : ViewModel(), DefaultLifecycleObserver { sealed class Command { - object OpenSettings : Command() + data object OpenSettings : Command() + data object OpenBuyScreen : Command() } private val command = Channel(1, BufferOverflow.DROP_OLDEST) @@ -55,6 +57,10 @@ class ProSettingViewModel( sendCommand(OpenSettings) } + fun onBuy() { + sendCommand(OpenBuyScreen) + } + override fun onResume(owner: LifecycleOwner) { super.onResume(owner) viewModelScope.launch(dispatcherProvider.io()) { diff --git a/subscriptions/subscriptions-impl/src/main/res/layout/view_settings_buy.xml b/subscriptions/subscriptions-impl/src/main/res/layout/view_itr_settings.xml similarity index 82% rename from subscriptions/subscriptions-impl/src/main/res/layout/view_settings_buy.xml rename to subscriptions/subscriptions-impl/src/main/res/layout/view_itr_settings.xml index 156e39f391db..18102d72e6a4 100644 --- a/subscriptions/subscriptions-impl/src/main/res/layout/view_settings_buy.xml +++ b/subscriptions/subscriptions-impl/src/main/res/layout/view_itr_settings.xml @@ -14,12 +14,12 @@ ~ limitations under the License. --> - \ No newline at end of file + app:secondaryText="@string/itrSettingSubtitle" /> \ No newline at end of file diff --git a/subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml b/subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml new file mode 100644 index 000000000000..fde1b482bad8 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/res/layout/view_pir_settings.xml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/subscriptions/subscriptions-impl/src/main/res/layout/view_settings.xml b/subscriptions/subscriptions-impl/src/main/res/layout/view_settings.xml index 169f289979fe..6a8dfb7bb109 100644 --- a/subscriptions/subscriptions-impl/src/main/res/layout/view_settings.xml +++ b/subscriptions/subscriptions-impl/src/main/res/layout/view_settings.xml @@ -14,12 +14,36 @@ ~ limitations under the License. --> - \ No newline at end of file + android:layout_height="wrap_content"> + + + + + + + + \ No newline at end of file diff --git a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml index a63a0428c2c0..31b5d5b083cc 100644 --- a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml +++ b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml @@ -60,6 +60,7 @@ Subscription Settings Subscribe To Privacy Pro More seamless privacy with three new protections, including: +
  •  VPN (Virtual Private Network)
  •  Personal Information Removal
  •  Identity Theft Restoration

  • Learn More]]>
    You\'re all set. @@ -67,4 +68,12 @@ Your already had a subscription and we\'ve recovered that for you. Something went wrong :( We couldn\'t find the subscription. Please try again. + + + Personal Information Removal + Remove your info from sites that sell it + + + Identity Theft Restoration + If your identity is stolen, we\'ll help restore it \ No newline at end of file diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt new file mode 100644 index 000000000000..5b847843c0fa --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ItrSettingViewModelTest.kt @@ -0,0 +1,60 @@ +package com.duckduckgo.subscriptions.impl.settings.views + +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.settings.views.ItrSettingViewModel.Command.OpenItr +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +class ItrSettingViewModelTest { + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val subscriptionsManager: SubscriptionsManager = mock() + private lateinit var viewModel: ItrSettingViewModel + + @Before + fun before() { + viewModel = ItrSettingViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider) + } + + @Test + fun whenOnItrThenCommandSent() = runTest { + viewModel.commands().test { + viewModel.onItr() + assertTrue(awaitItem() is OpenItr) + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenOnResumeIfSubscriptionEmitViewState() = runTest { + whenever(subscriptionsManager.hasSubscription).thenReturn(flowOf(true)) + + viewModel.onResume(mock()) + viewModel.viewState.test { + assertTrue(awaitItem().hasSubscription) + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenOnResumeIfNotSubscriptionEmitViewState() = runTest { + whenever(subscriptionsManager.hasSubscription).thenReturn(flowOf(false)) + + viewModel.onResume(mock()) + viewModel.viewState.test { + assertFalse(awaitItem().hasSubscription) + cancelAndConsumeRemainingEvents() + } + } +} diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt new file mode 100644 index 000000000000..183bd0b8051b --- /dev/null +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/PirSettingViewModelTest.kt @@ -0,0 +1,60 @@ +package com.duckduckgo.subscriptions.impl.settings.views + +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.settings.views.PirSettingViewModel.Command.OpenPir +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +class PirSettingViewModelTest { + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val subscriptionsManager: SubscriptionsManager = mock() + private lateinit var viewModel: PirSettingViewModel + + @Before + fun before() { + viewModel = PirSettingViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider) + } + + @Test + fun whenOnPirThenCommandSent() = runTest { + viewModel.commands().test { + viewModel.onPir() + assertTrue(awaitItem() is OpenPir) + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenOnResumeIfSubscriptionEmitViewState() = runTest { + whenever(subscriptionsManager.hasSubscription).thenReturn(flowOf(true)) + + viewModel.onResume(mock()) + viewModel.viewState.test { + assertTrue(awaitItem().hasSubscription) + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun whenOnResumeIfNotSubscriptionEmitViewState() = runTest { + whenever(subscriptionsManager.hasSubscription).thenReturn(flowOf(false)) + + viewModel.onResume(mock()) + viewModel.viewState.test { + assertFalse(awaitItem().hasSubscription) + cancelAndConsumeRemainingEvents() + } + } +} diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyViewModelTest.kt deleted file mode 100644 index 8a0462ce9567..000000000000 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingBuyViewModelTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.duckduckgo.subscriptions.impl.settings.views - -import app.cash.turbine.test -import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.subscriptions.impl.settings.views.ProSettingBuyViewModel.Command.OpenBuyScreen -import kotlinx.coroutines.test.runTest -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class ProSettingBuyViewModelTest { - - @get:Rule - val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() - - private lateinit var viewModel: ProSettingBuyViewModel - - @Before - fun before() { - viewModel = ProSettingBuyViewModel() - } - - @Test - fun whenOnBuyThenCommandSent() = runTest { - viewModel.commands().test { - viewModel.onBuyClicked() - assertTrue(awaitItem() is OpenBuyScreen) - cancelAndConsumeRemainingEvents() - } - } -} diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt index 02010555740e..a8420686d2e5 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/settings/views/ProSettingViewModelTest.kt @@ -3,6 +3,7 @@ package com.duckduckgo.subscriptions.impl.settings.views import app.cash.turbine.test import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.subscriptions.impl.SubscriptionsManager +import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenBuyScreen import com.duckduckgo.subscriptions.impl.settings.views.ProSettingViewModel.Command.OpenSettings import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -34,6 +35,15 @@ class ProSettingViewModelTest { } } + @Test + fun whenOnBuyThenCommandSent() = runTest { + viewModel.commands().test { + viewModel.onBuy() + assertTrue(awaitItem() is OpenBuyScreen) + cancelAndConsumeRemainingEvents() + } + } + @Test fun whenOnResumeIfSubscriptionEmitViewState() = runTest { whenever(subscriptionsManager.hasSubscription).thenReturn(flowOf(true))