Skip to content

Commit

Permalink
Second iteration for settings (#3956)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1205820670085181/f 

### Description
See task
### Steps to test this PR
See task
  • Loading branch information
marcosholgado authored Dec 13, 2023
1 parent 3434b08 commit 5f8f3a3
Show file tree
Hide file tree
Showing 16 changed files with 531 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -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")
Expand All @@ -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()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Command>(1, BufferOverflow.DROP_OLDEST)
internal fun commands(): Flow<Command> = 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) {
Expand All @@ -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 <T : ViewModel> create(modelClass: Class<T>): 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
}
Loading

0 comments on commit 5f8f3a3

Please sign in to comment.