From ac941b1b6e95a0cc51e8653571c476b0328ca648 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 13 Dec 2023 15:35:15 +0000 Subject: [PATCH] Add subs info in settings screen (#3962) Task/Issue URL: https://app.asana.com/0/0/1206024785662125/f ### Description See task ### Steps to test this PR See task --- .../impl/SubscriptionsManager.kt | 76 +++++++++++++--- .../impl/{auth => services}/AuthService.kt | 2 +- .../impl/services/SubscriptionsService.kt | 36 ++++++++ .../impl/ui/SubscriptionSettingsActivity.kt | 29 ++++++ .../impl/ui/SubscriptionSettingsViewModel.kt | 44 ++++++++- .../src/main/res/values/donottranslate.xml | 8 +- .../impl/RealSubscriptionsManagerTest.kt | 89 +++++++++++++++++-- .../impl/RealSubscriptionsTest.kt | 2 +- .../ui/RestoreSubscriptionViewModelTest.kt | 2 +- .../ui/SubscriptionSettingsViewModelTest.kt | 59 +++++++++++- 10 files changed, 323 insertions(+), 24 deletions(-) rename subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/{auth => services}/AuthService.kt (98%) create mode 100644 subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt index 0bfba7d86bcb..14392dccc66e 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt @@ -23,14 +23,21 @@ import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AutoRenewable +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.Expired +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.GracePeriod +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.Inactive +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.NotAutoRenewable +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.Unknown import com.duckduckgo.subscriptions.impl.SubscriptionsData.* -import com.duckduckgo.subscriptions.impl.auth.AuthService -import com.duckduckgo.subscriptions.impl.auth.CreateAccountResponse -import com.duckduckgo.subscriptions.impl.auth.Entitlement -import com.duckduckgo.subscriptions.impl.auth.ResponseError -import com.duckduckgo.subscriptions.impl.auth.StoreLoginBody import com.duckduckgo.subscriptions.impl.billing.BillingClientWrapper import com.duckduckgo.subscriptions.impl.billing.PurchaseState +import com.duckduckgo.subscriptions.impl.services.AuthService +import com.duckduckgo.subscriptions.impl.services.CreateAccountResponse +import com.duckduckgo.subscriptions.impl.services.Entitlement +import com.duckduckgo.subscriptions.impl.services.ResponseError +import com.duckduckgo.subscriptions.impl.services.StoreLoginBody +import com.duckduckgo.subscriptions.impl.services.SubscriptionsService import com.duckduckgo.subscriptions.store.AuthDataStore import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.Moshi @@ -68,6 +75,11 @@ interface SubscriptionsManager { */ suspend fun getSubscriptionData(): SubscriptionsData + /** + * Gets the subscription for an authenticated user + */ + suspend fun getSubscription(): Subscription + /** * Authenticates the user based on the auth token */ @@ -113,6 +125,7 @@ interface SubscriptionsManager { @ContributesBinding(AppScope::class) class RealSubscriptionsManager @Inject constructor( private val authService: AuthService, + private val subscriptionsService: SubscriptionsService, private val authDataStore: AuthDataStore, private val billingClientWrapper: BillingClientWrapper, private val emailManager: EmailManager, @@ -155,6 +168,35 @@ class RealSubscriptionsManager @Inject constructor( } } + override suspend fun getSubscription(): Subscription { + return try { + if (isUserAuthenticated()) { + val response = subscriptionsService.subscription("Bearer ${authDataStore.accessToken}") + val state = when (response.status) { + "Auto-Renewable" -> AutoRenewable + "Not Auto-Renewable" -> NotAutoRenewable + "Grace Period" -> GracePeriod + "Inactive" -> Inactive + "Expired" -> Expired + else -> Unknown + } + return Subscription.Success( + productId = response.productId, + startedAt = response.startedAt, + expiresOrRenewsAt = response.expiresOrRenewsAt, + status = state, + ) + } else { + Subscription.Failure("Subscription not found") + } + } catch (e: HttpException) { + val error = parseError(e)?.error ?: "An error happened" + Subscription.Failure(error) + } catch (e: Exception) { + Subscription.Failure(e.message ?: "An error happened") + } + } + override suspend fun signOut() { authDataStore.authToken = "" authDataStore.accessToken = "" @@ -390,11 +432,25 @@ sealed class SubscriptionsData { data class Failure(val message: String) : SubscriptionsData() } +sealed class Subscription { + data class Success(val productId: String, val startedAt: Long, val expiresOrRenewsAt: Long, val status: SubscriptionStatus) : Subscription() + data class Failure(val message: String) : Subscription() +} + +sealed class SubscriptionStatus { + data object AutoRenewable : SubscriptionStatus() + data object NotAutoRenewable : SubscriptionStatus() + data object GracePeriod : SubscriptionStatus() + data object Inactive : SubscriptionStatus() + data object Expired : SubscriptionStatus() + data object Unknown : SubscriptionStatus() +} + sealed class CurrentPurchase { - object PreFlowInProgress : CurrentPurchase() - object PreFlowFinished : CurrentPurchase() - object InProgress : CurrentPurchase() - object Success : CurrentPurchase() - object Recovered : CurrentPurchase() + data object PreFlowInProgress : CurrentPurchase() + data object PreFlowFinished : CurrentPurchase() + data object InProgress : CurrentPurchase() + data object Success : CurrentPurchase() + data object Recovered : CurrentPurchase() data class Failure(val message: String) : CurrentPurchase() } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/auth/AuthService.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/AuthService.kt similarity index 98% rename from subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/auth/AuthService.kt rename to subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/AuthService.kt index 78205b1148f0..4c83b6997941 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/auth/AuthService.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/AuthService.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.subscriptions.impl.auth +package com.duckduckgo.subscriptions.impl.services import com.duckduckgo.anvil.annotations.ContributesNonCachingServiceApi import com.duckduckgo.di.scopes.AppScope diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt new file mode 100644 index 000000000000..17cdde6b1e79 --- /dev/null +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/services/SubscriptionsService.kt @@ -0,0 +1,36 @@ +/* + * 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.services + +import com.duckduckgo.anvil.annotations.ContributesNonCachingServiceApi +import com.duckduckgo.di.scopes.AppScope +import retrofit2.http.GET +import retrofit2.http.Header + +@ContributesNonCachingServiceApi(AppScope::class) +interface SubscriptionsService { + @GET("https://subscriptions-dev.duckduckgo.com/api/subscription") + suspend fun subscription(@Header("Authorization") authorization: String?): SubscriptionResponse +} + +data class SubscriptionResponse( + val productId: String, + val startedAt: Long, + val expiresOrRenewsAt: Long, + val platform: String, + val status: String, +) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt index 7993bb028198..c7af361d6dda 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsActivity.kt @@ -31,12 +31,15 @@ import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.subscriptions.impl.R.string +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AutoRenewable import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.BASIC_SUBSCRIPTION import com.duckduckgo.subscriptions.impl.databinding.ActivitySubscriptionSettingsBinding import com.duckduckgo.subscriptions.impl.ui.AddDeviceActivity.Companion.AddDeviceScreenWithEmptyParams import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsActivity.Companion.SubscriptionsSettingsScreenWithEmptyParams import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut +import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.SubscriptionDuration.Monthly +import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.ViewState import javax.inject.Inject import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -59,11 +62,17 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() { setContentView(binding.root) setupToolbar(toolbar) + lifecycle.addObserver(viewModel) + viewModel.commands() .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .onEach { processCommand(it) } .launchIn(lifecycleScope) + viewModel.viewState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).onEach { + renderView(it) + }.launchIn(lifecycleScope) + binding.addDevice.setClickListener { globalActivityStarter.start(this, AddDeviceScreenWithEmptyParams) } @@ -101,6 +110,26 @@ class SubscriptionSettingsActivity : DuckDuckGoActivity() { } } + override fun onDestroy() { + super.onDestroy() + lifecycle.removeObserver(viewModel) + } + + private fun renderView(viewState: ViewState) { + val duration = if (viewState.duration is Monthly) { + getString(string.monthly) + } else { + getString(string.yearly) + } + + val status = when (viewState.status) { + is AutoRenewable -> getString(string.renews) + else -> getString(string.expires) + } + + binding.description.text = getString(string.subscriptionsData, duration, status, viewState.date) + } + private fun processCommand(command: Command) { when (command) { is FinishSignOut -> { diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt index 1d6448e22551..5cd4ad684a52 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModel.kt @@ -16,27 +16,62 @@ package com.duckduckgo.subscriptions.impl.ui +import android.annotation.SuppressLint +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.subscriptions.impl.Subscription +import com.duckduckgo.subscriptions.impl.SubscriptionStatus +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut +import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.SubscriptionDuration.Monthly +import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.SubscriptionDuration.Yearly +import java.text.SimpleDateFormat +import java.util.* import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST 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 @ContributesViewModel(ActivityScope::class) class SubscriptionSettingsViewModel @Inject constructor( private val subscriptionsManager: SubscriptionsManager, -) : ViewModel() { + private val dispatcherProvider: DispatcherProvider, +) : ViewModel(), DefaultLifecycleObserver { private val command = Channel(1, DROP_OLDEST) internal fun commands(): Flow = command.receiveAsFlow() + private val _viewState = MutableStateFlow(ViewState()) + val viewState = _viewState.asStateFlow() + data class ViewState( + val date: String? = null, + val duration: SubscriptionDuration? = null, + val status: SubscriptionStatus? = null, + ) + + override fun onResume(owner: LifecycleOwner) { + viewModelScope.launch(dispatcherProvider.io()) { + val subs = subscriptionsManager.getSubscription() + if (subs is Subscription.Success) { + val formatter = SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault()) + val date = formatter.format(Date(subs.expiresOrRenewsAt)) + val type = if (subs.productId == MONTHLY_PLAN) Monthly else Yearly + _viewState.emit(viewState.value.copy(date = date, duration = type, status = subs.status)) + } + } + } + fun removeFromDevice() { viewModelScope.launch { subscriptionsManager.signOut() @@ -44,7 +79,12 @@ class SubscriptionSettingsViewModel @Inject constructor( } } + sealed class SubscriptionDuration { + data object Monthly : SubscriptionDuration() + data object Yearly : SubscriptionDuration() + } + sealed class Command { - object FinishSignOut : Command() + data object FinishSignOut : Command() } } diff --git a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml index 31b5d5b083cc..4fac7af862d1 100644 --- a/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml +++ b/subscriptions/subscriptions-impl/src/main/res/values/donottranslate.xml @@ -14,7 +14,8 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + + Cancel OK @@ -55,6 +56,11 @@ Your subscription has been removed from this device Remove From This Device? You will no longer be able to access your subscription on this device. This will not cancel your subscription, and it will remain active on your other device. + Your %1$s Privacy Pro subscription %2$s on %3$s. + monthly + yearly + renews + expires Subscription Settings diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt index b70e44738f40..d675611d0746 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt @@ -7,17 +7,26 @@ import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.PurchaseHistoryRecord import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AutoRenewable +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.Expired +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.GracePeriod +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.Inactive +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.NotAutoRenewable +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.Unknown +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants.MONTHLY_PLAN import com.duckduckgo.subscriptions.impl.SubscriptionsData.Failure import com.duckduckgo.subscriptions.impl.SubscriptionsData.Success -import com.duckduckgo.subscriptions.impl.auth.AccessTokenResponse -import com.duckduckgo.subscriptions.impl.auth.AccountResponse -import com.duckduckgo.subscriptions.impl.auth.AuthService -import com.duckduckgo.subscriptions.impl.auth.CreateAccountResponse -import com.duckduckgo.subscriptions.impl.auth.Entitlement -import com.duckduckgo.subscriptions.impl.auth.StoreLoginResponse -import com.duckduckgo.subscriptions.impl.auth.ValidateTokenResponse import com.duckduckgo.subscriptions.impl.billing.BillingClientWrapper import com.duckduckgo.subscriptions.impl.billing.PurchaseState +import com.duckduckgo.subscriptions.impl.services.AccessTokenResponse +import com.duckduckgo.subscriptions.impl.services.AccountResponse +import com.duckduckgo.subscriptions.impl.services.AuthService +import com.duckduckgo.subscriptions.impl.services.CreateAccountResponse +import com.duckduckgo.subscriptions.impl.services.Entitlement +import com.duckduckgo.subscriptions.impl.services.StoreLoginResponse +import com.duckduckgo.subscriptions.impl.services.SubscriptionResponse +import com.duckduckgo.subscriptions.impl.services.SubscriptionsService +import com.duckduckgo.subscriptions.impl.services.ValidateTokenResponse import com.duckduckgo.subscriptions.store.AuthDataStore import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.TestScope @@ -45,6 +54,7 @@ class RealSubscriptionsManagerTest { val coroutineRule = CoroutineTestRule() private val authService: AuthService = mock() + private val subscriptionsService: SubscriptionsService = mock() private val authDataStore: AuthDataStore = FakeDataStore() private val emailManager: EmailManager = mock() private val billingClient: BillingClientWrapper = mock() @@ -60,6 +70,7 @@ class RealSubscriptionsManagerTest { whenever(billingBuilder.build()).thenReturn(mock()) subscriptionsManager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -387,6 +398,7 @@ class RealSubscriptionsManagerTest { val manager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -408,6 +420,7 @@ class RealSubscriptionsManagerTest { val manager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -429,6 +442,7 @@ class RealSubscriptionsManagerTest { val manager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -449,6 +463,7 @@ class RealSubscriptionsManagerTest { givenValidateTokenSucceedsWithEntitlements() val manager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -469,6 +484,7 @@ class RealSubscriptionsManagerTest { givenValidateTokenFails("failure") val manager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -493,6 +509,7 @@ class RealSubscriptionsManagerTest { val manager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -519,6 +536,7 @@ class RealSubscriptionsManagerTest { val manager = RealSubscriptionsManager( authService, + subscriptionsService, authDataStore, billingClient, emailManager, @@ -624,6 +642,63 @@ class RealSubscriptionsManagerTest { assertTrue(result is AuthToken.Failure) } + @Test + fun whenGetSubscriptionIfUserNotAuthenticatedThenReturnFailure() = runTest { + givenUserIsNotAuthenticated() + + val result = subscriptionsManager.getSubscription() + + assertTrue(result is Subscription.Failure) + } + + @Test + fun whenGetSubscriptionIfServiceFailsThenReturnFailure() = runTest { + givenUserIsAuthenticated() + givenSubscriptionFails() + val result = subscriptionsManager.getSubscription() + + assertTrue(result is Subscription.Failure) + } + + @Test + fun whenGetSubscriptionThenReturnCorrectStatus() = runTest { + givenUserIsAuthenticated() + givenSubscriptionSucceeds("Auto-Renewable") + assertTrue((subscriptionsManager.getSubscription() as Subscription.Success).status is AutoRenewable) + + givenSubscriptionSucceeds("Not Auto-Renewable") + assertTrue((subscriptionsManager.getSubscription() as Subscription.Success).status is NotAutoRenewable) + + givenSubscriptionSucceeds("Grace Period") + assertTrue((subscriptionsManager.getSubscription() as Subscription.Success).status is GracePeriod) + + givenSubscriptionSucceeds("Inactive") + assertTrue((subscriptionsManager.getSubscription() as Subscription.Success).status is Inactive) + + givenSubscriptionSucceeds("Expired") + assertTrue((subscriptionsManager.getSubscription() as Subscription.Success).status is Expired) + + givenSubscriptionSucceeds("test") + assertTrue((subscriptionsManager.getSubscription() as Subscription.Success).status is Unknown) + } + + private suspend fun givenSubscriptionFails() { + val exception = "failure".toResponseBody("text/json".toMediaTypeOrNull()) + whenever(subscriptionsService.subscription(any())).thenThrow(HttpException(Response.error(400, exception))) + } + + private suspend fun givenSubscriptionSucceeds(status: String = "Auto-Renewable") { + whenever(subscriptionsService.subscription(any())).thenReturn( + SubscriptionResponse( + productId = MONTHLY_PLAN, + startedAt = 1234, + expiresOrRenewsAt = 1234, + platform = "google", + status = status, + ), + ) + } + private fun givenUserIsNotAuthenticated() { authDataStore.accessToken = null authDataStore.authToken = null diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt index 46b42eb331c1..ad54791bdf8f 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsTest.kt @@ -18,7 +18,7 @@ package com.duckduckgo.subscriptions.impl import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.subscriptions.impl.auth.Entitlement +import com.duckduckgo.subscriptions.impl.services.Entitlement import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt index be18b255840f..a239679710dc 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/RestoreSubscriptionViewModelTest.kt @@ -5,7 +5,7 @@ import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.subscriptions.impl.RealSubscriptionsManager.Companion.SUBSCRIPTION_NOT_FOUND_ERROR import com.duckduckgo.subscriptions.impl.SubscriptionsData import com.duckduckgo.subscriptions.impl.SubscriptionsManager -import com.duckduckgo.subscriptions.impl.auth.Entitlement +import com.duckduckgo.subscriptions.impl.services.Entitlement import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.Error import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.RestoreFromEmail import com.duckduckgo.subscriptions.impl.ui.RestoreSubscriptionViewModel.Command.SubscriptionNotFound diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt index 9d42e454102c..ece21d69a455 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/ui/SubscriptionSettingsViewModelTest.kt @@ -2,14 +2,20 @@ package com.duckduckgo.subscriptions.impl.ui import app.cash.turbine.test import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.subscriptions.impl.Subscription +import com.duckduckgo.subscriptions.impl.SubscriptionStatus.AutoRenewable +import com.duckduckgo.subscriptions.impl.SubscriptionsConstants import com.duckduckgo.subscriptions.impl.SubscriptionsManager import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.Command.FinishSignOut +import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.SubscriptionDuration.Monthly +import com.duckduckgo.subscriptions.impl.ui.SubscriptionSettingsViewModel.SubscriptionDuration.Yearly 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 class SubscriptionSettingsViewModelTest { @@ -21,7 +27,7 @@ class SubscriptionSettingsViewModelTest { @Before fun before() { - viewModel = SubscriptionSettingsViewModel(subscriptionsManager) + viewModel = SubscriptionSettingsViewModel(subscriptionsManager, coroutineTestRule.testDispatcherProvider) } @Test @@ -31,4 +37,55 @@ class SubscriptionSettingsViewModelTest { assertTrue(awaitItem() is FinishSignOut) } } + + @Test + fun whenSubscriptionThenFormatDateCorrectly() = runTest { + whenever(subscriptionsManager.getSubscription()).thenReturn( + Subscription.Success( + productId = SubscriptionsConstants.MONTHLY_PLAN, + startedAt = 1234, + expiresOrRenewsAt = 1701694623000, + status = AutoRenewable, + ), + ) + + viewModel.onResume(mock()) + viewModel.viewState.test { + assertEquals("December 04, 2023", awaitItem().date) + } + } + + @Test + fun whenSubscriptionMonthlyThenReturnMonthly() = runTest { + whenever(subscriptionsManager.getSubscription()).thenReturn( + Subscription.Success( + productId = SubscriptionsConstants.MONTHLY_PLAN, + startedAt = 1234, + expiresOrRenewsAt = 1701694623000, + status = AutoRenewable, + ), + ) + + viewModel.onResume(mock()) + viewModel.viewState.test { + assertEquals(Monthly, awaitItem().duration) + } + } + + @Test + fun whenSubscriptionYearlyThenReturnYearly() = runTest { + whenever(subscriptionsManager.getSubscription()).thenReturn( + Subscription.Success( + productId = SubscriptionsConstants.YEARLY_PLAN, + startedAt = 1234, + expiresOrRenewsAt = 1701694623000, + status = AutoRenewable, + ), + ) + + viewModel.onResume(mock()) + viewModel.viewState.test { + assertEquals(Yearly, awaitItem().duration) + } + } }