diff --git a/app/build.gradle b/app/build.gradle index 3bcd0e19896d..0aef14438cc2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -300,7 +300,6 @@ dependencies { implementation project(':web-compat-store') implementation project(':subscriptions-api') - implementation project(':network-protection-subscription-internal') implementation project(':subscriptions-impl') internalImplementation project(':subscriptions-internal') diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt index ca07a606c6c4..838ba6d33129 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt @@ -192,6 +192,7 @@ class SettingsViewModel @Inject constructor( if (isPrivacyProEnabled()) Hidden else getNetworkProtectionEntryState(this) }, isAutoconsentEnabled = autoconsent.isSettingEnabled(), + isPrivacyProEnabled = isPrivacyProEnabled() && subscriptions.isEligible(), ), ) networkProtectionState.getConnectionStateFlow() diff --git a/build.gradle b/build.gradle index 01c745914c14..9caf9fea47d7 100644 --- a/build.gradle +++ b/build.gradle @@ -123,7 +123,7 @@ subprojects { if (dependencyPath == projectPath) continue // internal modules have to use internalImplementation // when a non internal configuration is built (i.e. PlayDebug) no internal dependencies should be found - def internalExceptions = [":network-protection-subscription-internal"] + def internalExceptions = [] if (dependencyPath.endsWith('internal') && !internalExceptions.contains(dependencyPath) && !name.toLowerCase().contains("internal")) { throw new GradleException("Invalid dependency $projectPath -> $dependencyPath. " + "'internal' modules must use internalImplementation") diff --git a/network-protection/network-protection-impl/build.gradle b/network-protection/network-protection-impl/build.gradle index 5c07b8c233ed..ad83da952894 100644 --- a/network-protection/network-protection-impl/build.gradle +++ b/network-protection/network-protection-impl/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation project(':privacy-config-api') implementation project(':navigation-api') implementation project(':subscriptions-api') + implementation project(':settings-api') implementation AndroidX.appCompat implementation AndroidX.constraintLayout diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/configuration/NetpRequestInterceptor.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/configuration/NetpRequestInterceptor.kt index 92500e516974..0e0cf9df809d 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/configuration/NetpRequestInterceptor.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/configuration/NetpRequestInterceptor.kt @@ -21,7 +21,6 @@ import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.appbuildconfig.api.isInternalBuild import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.networkprotection.impl.BuildConfig -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository import com.duckduckgo.networkprotection.impl.waitlist.store.NetPWaitlistRepository import com.duckduckgo.subscriptions.api.Subscriptions import com.squareup.anvil.annotations.ContributesMultibinding @@ -39,7 +38,6 @@ import okhttp3.Response class NetpWaitlistRequestInterceptor @Inject constructor( private val netpWaitlistRepository: NetPWaitlistRepository, private val appBuildConfig: AppBuildConfig, - private val networkProtectionRepository: NetworkProtectionRepository, private val subscriptions: Subscriptions, ) : ApiInterceptorPlugin, Interceptor { @@ -65,11 +63,7 @@ class NetpWaitlistRequestInterceptor @Inject constructor( chain.proceed( newRequest.build().also { logcat { "headers: ${it.headers}" } }, - ).also { - if (runBlocking { subscriptions.isEnabled() }) { - networkProtectionRepository.vpnAccessRevoked = (it.code == 403) - } - } + ) } else { chain.proceed(newRequest.build()) } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt index e88b88fe014c..83db66fc0df5 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationBuilder.kt @@ -29,7 +29,9 @@ import com.duckduckgo.browser.api.ui.BrowserScreens.SettingsScreenNoParams import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.networkprotection.impl.R -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager +import com.duckduckgo.networkprotection.impl.subscription.isActive +import com.duckduckgo.networkprotection.impl.subscription.isExpired import com.squareup.anvil.annotations.ContributesBinding import java.text.DateFormat import java.text.SimpleDateFormat @@ -37,7 +39,7 @@ import java.util.Date import javax.inject.Inject interface NetPDisabledNotificationBuilder { - fun buildDisabledNotification(context: Context): Notification + suspend fun buildDisabledNotification(context: Context): Notification? fun buildSnoozeNotification( context: Context, @@ -55,7 +57,7 @@ interface NetPDisabledNotificationBuilder { class RealNetPDisabledNotificationBuilder @Inject constructor( private val netPNotificationActions: NetPNotificationActions, private val globalActivityStarter: GlobalActivityStarter, - private val netpRepository: NetworkProtectionRepository, + private val netpSubscriptionManager: NetpSubscriptionManager, ) : NetPDisabledNotificationBuilder { private val defaultDateTimeFormatter = SimpleDateFormat.getTimeInstance(DateFormat.SHORT) @@ -72,11 +74,14 @@ class RealNetPDisabledNotificationBuilder @Inject constructor( } } - override fun buildDisabledNotification(context: Context): Notification { - return if (netpRepository.vpnAccessRevoked) { + override suspend fun buildDisabledNotification(context: Context): Notification? { + val vpnStatus = netpSubscriptionManager.getVpnStatus() + return if (vpnStatus.isExpired()) { buildVpnAccessRevokedNotification(context) - } else { + } else if (vpnStatus.isActive()) { buildVpnDisabledNotification(context) + } else { + null } } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationScheduler.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationScheduler.kt index 8ceeb697658d..dbc40b789b84 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationScheduler.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationScheduler.kt @@ -28,7 +28,6 @@ import com.duckduckgo.mobile.android.vpn.service.VpnServiceCallbacks import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason import com.duckduckgo.networkprotection.api.NetworkProtectionState import com.duckduckgo.networkprotection.impl.settings.NetPSettingsLocalConfig -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository import com.duckduckgo.networkprotection.impl.waitlist.NetPRemoteFeature import com.squareup.anvil.annotations.ContributesMultibinding import java.util.concurrent.atomic.AtomicReference @@ -47,7 +46,6 @@ class NetPDisabledNotificationScheduler @Inject constructor( @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val netPRemoteFeature: NetPRemoteFeature, - private val networkProtectionRepository: NetworkProtectionRepository, ) : VpnServiceCallbacks { private var isNetPEnabled: AtomicReference = AtomicReference(false) @@ -80,20 +78,6 @@ class NetPDisabledNotificationScheduler @Inject constructor( } } - override fun onVpnStartFailed(coroutineScope: CoroutineScope) { - coroutineScope.launch(dispatcherProvider.io()) { - if (networkProtectionRepository.vpnAccessRevoked) { - notificationManager.checkPermissionAndNotify( - context, - NETP_REMINDER_NOTIFICATION_ID, - netPDisabledNotificationBuilder.buildVpnAccessRevokedNotification(context), - ) - // This is to clear the registered features and remove VPN - networkProtectionState.stop() - } - } - } - private suspend fun shouldShowImmediateNotification(): Boolean { // When VPN is stopped and if AppTP has been enabled AND user has been onboarded, then we show the disabled notif return isNetPEnabled.get() && networkProtectionState.isOnboarded() && netPRemoteFeature.waitlistBetaActive().isEnabled() @@ -140,11 +124,13 @@ class NetPDisabledNotificationScheduler @Inject constructor( coroutineScope.launch(dispatcherProvider.io()) { logcat { "Showing disabled notification for NetP" } if (!netPSettingsLocalConfig.vpnNotificationAlerts().isEnabled()) return@launch - notificationManager.checkPermissionAndNotify( - context, - NETP_REMINDER_NOTIFICATION_ID, - netPDisabledNotificationBuilder.buildDisabledNotification(context), - ) + netPDisabledNotificationBuilder.buildDisabledNotification(context)?.let { notification -> + notificationManager.checkPermissionAndNotify( + context, + NETP_REMINDER_NOTIFICATION_ID, + notification, + ) + } } } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/AccessRevokedDialog.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/AccessRevokedDialog.kt index 700444dbba38..8692fd9e4337 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/AccessRevokedDialog.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/AccessRevokedDialog.kt @@ -17,37 +17,76 @@ package com.duckduckgo.networkprotection.impl.revoked import android.app.Activity +import android.content.SharedPreferences +import androidx.core.content.edit import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder.EventListener import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.mobile.android.vpn.prefs.VpnSharedPreferencesProvider import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.networkprotection.impl.R import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixels -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository import com.duckduckgo.subscriptions.api.SubscriptionScreens.SubscriptionScreenNoParams import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext interface AccessRevokedDialog { - fun show(activity: Activity) + /** + * Call this method to always show the dialog + */ + fun showAlways(activity: Activity) + + /** + * Call this method to show the dialog only once. + * Use [clearIsShown] to reset that state + */ + fun showOnce(activity: Activity) + + /** + * Call this method to allow [showOnce] to show the dialog more than once + */ + fun clearIsShown() } @ContributesBinding(AppScope::class) class RealAccessRevokedDialog @Inject constructor( @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, - private val networkProtectionRepository: NetworkProtectionRepository, private val globalActivityStarter: GlobalActivityStarter, private val networkProtectionPixels: NetworkProtectionPixels, + private val vpnSharedPreferencesProvider: VpnSharedPreferencesProvider, ) : AccessRevokedDialog { + private val preferences: SharedPreferences by lazy { + vpnSharedPreferencesProvider.getSharedPreferences( + FILENAME, + multiprocess = true, + migrate = false, + ) + } + private var boundActivity: Activity? = null - override fun show(activity: Activity) { + override fun showAlways(activity: Activity) { + showInternal(activity) + } + + override fun showOnce(activity: Activity) { + coroutineScope.launch { + if (!isShown()) { + withContext(dispatcherProvider.main()) { + showInternal(activity) + } + } + } + } + + private fun showInternal(activity: Activity) { if (boundActivity == activity) return boundActivity = activity @@ -62,11 +101,14 @@ class RealAccessRevokedDialog @Inject constructor( override fun onPositiveButtonClicked() { // Commenting this for now since this is still behind the subs build globalActivityStarter.start(activity, SubscriptionScreenNoParams) - resetVpnAccessRevokedState() } - override fun onNegativeButtonClicked() { - resetVpnAccessRevokedState() + override fun onNegativeButtonClicked() {} + + override fun onDialogDismissed() { + coroutineScope.launch { + markAsShown() + } } }, ) @@ -75,9 +117,26 @@ class RealAccessRevokedDialog @Inject constructor( networkProtectionPixels.reportAccessRevokedDialogShown() } - private fun resetVpnAccessRevokedState() { + override fun clearIsShown() { coroutineScope.launch(dispatcherProvider.io()) { - networkProtectionRepository.vpnAccessRevoked = false + preferences.edit(commit = true) { + putBoolean(KEY_END_DIALOG_SHOWN, false) + } + } + } + + private suspend fun isShown(): Boolean = withContext(dispatcherProvider.io()) { + return@withContext preferences.getBoolean(KEY_END_DIALOG_SHOWN, false) + } + + private suspend fun markAsShown() = withContext(dispatcherProvider.io()) { + preferences.edit(commit = true) { + putBoolean(KEY_END_DIALOG_SHOWN, true) } } + + companion object { + private const val FILENAME = "com.duckduckgo.networkprotection.dialog.access.revoked.store.v1" + private const val KEY_END_DIALOG_SHOWN = "KEY_END_DIALOG_SHOWN" + } } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/BetaEndedDialog.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/BetaEndedDialog.kt index 8a85d9c97f16..84a5a01c16da 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/BetaEndedDialog.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/BetaEndedDialog.kt @@ -27,7 +27,6 @@ import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.mobile.android.vpn.prefs.VpnSharedPreferencesProvider import com.duckduckgo.networkprotection.impl.R.string import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixels -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -42,7 +41,6 @@ interface BetaEndedDialog { class RealBetaEndedDialog @Inject constructor( @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, - private val networkProtectionRepository: NetworkProtectionRepository, private val networkProtectionPixels: NetworkProtectionPixels, private val vpnSharedPreferencesProvider: VpnSharedPreferencesProvider, ) : BetaEndedDialog { @@ -83,7 +81,6 @@ class RealBetaEndedDialog @Inject constructor( private fun resetVpnAccessRevokedState() { coroutineScope.launch(dispatcherProvider.io()) { - networkProtectionRepository.vpnAccessRevoked = false storeBetaEndDialogShown() } } @@ -96,7 +93,7 @@ class RealBetaEndedDialog @Inject constructor( private fun hasShownBetaEndDialog(): Boolean = preferences.getBoolean(KEY_END_DIALOG_SHOWN, false) companion object { - const val FILENAME = "com.duckduckgo.networkprotection.impl.waitlist.end.store.v1" - const val KEY_END_DIALOG_SHOWN = "KEY_END_DIALOG_SHOWN" + private const val FILENAME = "com.duckduckgo.networkprotection.impl.waitlist.end.store.v1" + private const val KEY_END_DIALOG_SHOWN = "KEY_END_DIALOG_SHOWN" } } diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitor.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitor.kt index 8f090832eacc..deb64b77d9d7 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitor.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitor.kt @@ -19,46 +19,68 @@ package com.duckduckgo.networkprotection.impl.revoked import android.app.Activity import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.browser.api.ActivityLifecycleCallbacks +import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository +import com.duckduckgo.networkprotection.api.NetworkProtectionState +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager +import com.duckduckgo.networkprotection.impl.subscription.isExpired import com.duckduckgo.networkprotection.impl.waitlist.store.NetPWaitlistRepository import com.duckduckgo.subscriptions.api.Subscriptions import com.squareup.anvil.annotations.ContributesMultibinding import dagger.SingleInstanceIn import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import logcat.logcat @ContributesMultibinding(AppScope::class) @SingleInstanceIn(AppScope::class) class NetpVpnAccessRevokedDialogMonitor @Inject constructor( - private val networkProtectionRepository: NetworkProtectionRepository, + private val netpSubscriptionManager: NetpSubscriptionManager, @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, private val betaEndedDialog: BetaEndedDialog, private val accessRevokedDialog: AccessRevokedDialog, private val subscriptions: Subscriptions, private val netPWaitlistRepository: NetPWaitlistRepository, + private val networkProtectionState: NetworkProtectionState, ) : ActivityLifecycleCallbacks { + private val conflatedJob = ConflatedJob() + override fun onActivityResumed(activity: Activity) { super.onActivityResumed(activity) - coroutineScope.launch(dispatcherProvider.io()) { + conflatedJob += coroutineScope.launch(dispatcherProvider.io()) { + delay(500) // debounce fast screen state changes, eg. resume -> pause -> resume if (shouldShowDialog()) { + logcat { "VPN beta ended" } // Resetting here so we don't show this dialog anymore withContext(dispatcherProvider.main()) { betaEndedDialog.show(activity) } - } else if (networkProtectionRepository.vpnAccessRevoked) { + } else if (netpSubscriptionManager.getVpnStatus().isExpired() && networkProtectionState.isOnboarded()) { + // we don't want to show this dialog in eg. fresh installs withContext(dispatcherProvider.main()) { - accessRevokedDialog.show(activity) + accessRevokedDialog.showOnce(activity) + } + if (networkProtectionState.isEnabled()) { + networkProtectionState.stop() } + } else { + logcat { "VPN access revoke dialog clear shown state" } + accessRevokedDialog.clearIsShown() } } } + override fun onActivityPaused(activity: Activity) { + super.onActivityPaused(activity) + conflatedJob.cancel() + } + private suspend fun shouldShowDialog(): Boolean { // Show dialog only if the pro is launched, user participated in beta (authentication was set - which only happens in beta) // AND dialog hasn't been shown before to the user. diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/store/NetworkProtectionRepository.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/store/NetworkProtectionRepository.kt index 84c734cce798..49942153fe88 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/store/NetworkProtectionRepository.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/store/NetworkProtectionRepository.kt @@ -25,7 +25,6 @@ import javax.inject.Inject interface NetworkProtectionRepository { var enabledTimeInMillis: Long - var vpnAccessRevoked: Boolean } @ContributesBinding( @@ -50,14 +49,7 @@ class RealNetworkProtectionRepository @Inject constructor( networkProtectionPrefs.clear() } - override var vpnAccessRevoked: Boolean - get() = networkProtectionPrefs.getBoolean(KEY_VPN_ACCESS_REVOKED, false) - set(value) { - networkProtectionPrefs.putBoolean(KEY_VPN_ACCESS_REVOKED, value) - } - companion object { private const val KEY_WG_SERVER_ENABLE_TIME = "wg_server_enable_time" - private const val KEY_VPN_ACCESS_REVOKED = "key_vpn_access_revoked" } } diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetPWaitlistEndedChecker.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetPWaitlistEndedChecker.kt similarity index 95% rename from network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetPWaitlistEndedChecker.kt rename to network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetPWaitlistEndedChecker.kt index cd6d491bf0f7..b33ea7955e39 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetPWaitlistEndedChecker.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetPWaitlistEndedChecker.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.networkprotection.subscription +package com.duckduckgo.networkprotection.impl.subscription import android.content.SharedPreferences import androidx.core.content.edit @@ -56,7 +56,7 @@ class NetPWaitlistEndedChecker @Inject constructor( var hasEntitlement = false // I know, I don't like it either, but it seems we can't ensure a race otherwise for (retries in 1..5) { - hasEntitlement = netpSubscriptionManager.hasValidEntitlement() + hasEntitlement = netpSubscriptionManager.getVpnStatus().isActive() if (hasEntitlement) return@launch delay(200) } diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetpSubscriptionChecker.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionChecker.kt similarity index 91% rename from network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetpSubscriptionChecker.kt rename to network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionChecker.kt index be38303d4e6e..d298f2ae44ce 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetpSubscriptionChecker.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionChecker.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.networkprotection.subscription +package com.duckduckgo.networkprotection.impl.subscription import android.content.Context import androidx.work.BackoffPolicy @@ -32,7 +32,6 @@ import com.duckduckgo.di.scopes.VpnScope import com.duckduckgo.mobile.android.vpn.service.VpnServiceCallbacks import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason import com.duckduckgo.networkprotection.api.NetworkProtectionState -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository import com.duckduckgo.subscriptions.api.Subscriptions import com.squareup.anvil.annotations.ContributesMultibinding import java.util.concurrent.TimeUnit.MINUTES @@ -106,9 +105,6 @@ class NetpSubscriptionCheckWorker( @Inject lateinit var workManager: WorkManager - @Inject - lateinit var netpRepository: NetworkProtectionRepository - @Inject lateinit var subscriptions: Subscriptions @@ -116,11 +112,10 @@ class NetpSubscriptionCheckWorker( logcat { "Sub check: checking entitlement" } if (networkProtectionState.isEnabled()) { if (subscriptions.isEnabled()) { - if (netpSubscriptionManager.hasValidEntitlement()) { - netpRepository.vpnAccessRevoked = false + if (netpSubscriptionManager.getVpnStatus().isActive()) { + logcat { "Sub check: has entitlements" } } else { logcat { "Sub check: disabling" } - netpRepository.vpnAccessRevoked = true networkProtectionState.stop() } } else { diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt new file mode 100644 index 000000000000..9439808df175 --- /dev/null +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetpSubscriptionManager.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 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.networkprotection.impl.subscription + +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus +import com.duckduckgo.subscriptions.api.Product.NetP +import com.duckduckgo.subscriptions.api.SubscriptionStatus +import com.duckduckgo.subscriptions.api.Subscriptions +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +interface NetpSubscriptionManager { + suspend fun getToken(): String? + suspend fun getVpnStatus(): VpnStatus + suspend fun vpnStatus(): Flow + enum class VpnStatus { + ACTIVE, + EXPIRED, + SIGNED_OUT, + INACTIVE, + } +} + +fun VpnStatus.isActive(): Boolean { + return this == VpnStatus.ACTIVE +} + +fun VpnStatus.isExpired(): Boolean { + return this == VpnStatus.EXPIRED +} + +@ContributesBinding(AppScope::class) +class RealNetpSubscriptionManager @Inject constructor( + private val subscriptions: Subscriptions, + private val dispatcherProvider: DispatcherProvider, +) : NetpSubscriptionManager { + + override suspend fun getToken(): String? = withContext(dispatcherProvider.io()) { + subscriptions.getAccessToken() + } + + override suspend fun getVpnStatus(): VpnStatus { + val hasValidEntitlement = hasValidEntitlement() + return getVpnStatusInternal(hasValidEntitlement) + } + + override suspend fun vpnStatus(): Flow { + return hasValidEntitlementFlow().map { getVpnStatusInternal(it) } + } + + private suspend fun hasValidEntitlement(): Boolean = withContext(dispatcherProvider.io()) { + val entitlements = subscriptions.getEntitlementStatus().firstOrNull() + return@withContext (entitlements?.contains(NetP) == true) + } + + private fun hasValidEntitlementFlow(): Flow = subscriptions.getEntitlementStatus().map { it.contains(NetP) } + + private suspend fun getVpnStatusInternal(hasValidEntitlement: Boolean): VpnStatus { + val subscriptionState = subscriptions.getSubscriptionStatus() + return when (subscriptionState) { + SubscriptionStatus.INACTIVE, SubscriptionStatus.EXPIRED -> VpnStatus.EXPIRED + SubscriptionStatus.UNKNOWN -> VpnStatus.SIGNED_OUT + else -> { + if (hasValidEntitlement) { + VpnStatus.ACTIVE + } else { + VpnStatus.INACTIVE + } + } + } + } +} diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetworkProtectionAccessState.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetworkProtectionAccessState.kt similarity index 91% rename from network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetworkProtectionAccessState.kt rename to network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetworkProtectionAccessState.kt index 4bb11e28203c..d91aa4f66968 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetworkProtectionAccessState.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/NetworkProtectionAccessState.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.networkprotection.subscription +package com.duckduckgo.networkprotection.impl.subscription import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope @@ -28,7 +28,6 @@ import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitli import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.JoinedWaitlist import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.NotUnlocked import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.PendingInviteCode -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository import com.duckduckgo.networkprotection.impl.waitlist.NetworkProtectionWaitlistImpl import com.duckduckgo.networkprotection.impl.waitlist.store.NetPWaitlistRepository import com.duckduckgo.subscriptions.api.Subscriptions @@ -77,13 +76,12 @@ class NetworkProtectionAccessState @Inject constructor( private val networkProtectionState: NetworkProtectionState, private val dispatcherProvider: DispatcherProvider, private val netpSubscriptionManager: NetpSubscriptionManager, - private val networkProtectionRepository: NetworkProtectionRepository, private val subscriptions: Subscriptions, ) : NetworkProtectionWaitlist { override suspend fun getState(): NetPWaitlistState = withContext(dispatcherProvider.io()) { if (isTreated()) { - return@withContext if (!netpSubscriptionManager.hasValidEntitlement()) { + return@withContext if (!netpSubscriptionManager.getVpnStatus().isActive()) { // if entitlement check succeeded and no entitlement, reset state and hide access. handleRevokedVPNState() NotUnlocked @@ -96,8 +94,8 @@ class NetworkProtectionAccessState @Inject constructor( override suspend fun getStateFlow(): Flow = withContext(dispatcherProvider.io()) { if (isTreated()) { - netpSubscriptionManager.hasValidEntitlementFlow().map { - if (!it) { + netpSubscriptionManager.vpnStatus().map { status -> + if (!status.isActive()) { // if entitlement check succeeded and no entitlement, reset state and hide access. handleRevokedVPNState() NotUnlocked @@ -112,7 +110,6 @@ class NetworkProtectionAccessState @Inject constructor( private suspend fun handleRevokedVPNState() { if (networkProtectionState.isEnabled()) { - networkProtectionRepository.vpnAccessRevoked = true networkProtectionState.stop() } } diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPView.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt similarity index 80% rename from network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPView.kt rename to network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt index 62881f7bdc53..4c3884d85bf5 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPView.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.networkprotection.subscription.settings +package com.duckduckgo.networkprotection.impl.subscription.settings import android.annotation.SuppressLint import android.content.Context @@ -29,15 +29,15 @@ import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.navigation.api.GlobalActivityStarter -import com.duckduckgo.networkprotection.subscription.R -import com.duckduckgo.networkprotection.subscription.databinding.ViewSettingsNetpBinding -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.Command -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.Factory -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState +import com.duckduckgo.networkprotection.impl.R +import com.duckduckgo.networkprotection.impl.databinding.ViewSettingsNetpBinding +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Factory +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState import dagger.android.support.AndroidSupportInjection import javax.inject.Inject import kotlinx.coroutines.CoroutineScope diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPViewModel.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt similarity index 90% rename from network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPViewModel.kt rename to network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt index 0a15adc968c6..821627e2883b 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPViewModel.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.networkprotection.subscription.settings +package com.duckduckgo.networkprotection.impl.subscription.settings import android.annotation.SuppressLint import androidx.annotation.StringRes @@ -35,11 +35,12 @@ import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionSta import com.duckduckgo.networkprotection.api.NetworkProtectionState.ConnectionState.DISCONNECTED import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState +import com.duckduckgo.networkprotection.impl.R import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED -import com.duckduckgo.networkprotection.subscription.R -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command.OpenNetPScreen +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState import javax.inject.Inject import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -99,7 +100,7 @@ class ProSettingNetPViewModel( viewModelScope.launch { val screen = networkProtectionWaitlist.getScreenForCurrentState() screen?.let { - command.send(Command.OpenNetPScreen(screen)) + command.send(OpenNetPScreen(screen)) pixel.fire(NETP_SETTINGS_PRESSED) } ?: logcat { "Get screen for current NetP state is null" } } diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/SubsSettingsPlugin.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/SubsSettingsPlugin.kt similarity index 91% rename from network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/SubsSettingsPlugin.kt rename to network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/SubsSettingsPlugin.kt index 1579429db7bd..108c75960b3d 100644 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/settings/SubsSettingsPlugin.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/subscription/settings/SubsSettingsPlugin.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.networkprotection.subscription.settings +package com.duckduckgo.networkprotection.impl.subscription.settings import android.content.Context import android.view.View diff --git a/network-protection/network-protection-subscription-internal/src/main/res/layout/activity_netp_verify_subs.xml b/network-protection/network-protection-impl/src/main/res/layout/activity_netp_verify_subs.xml similarity index 100% rename from network-protection/network-protection-subscription-internal/src/main/res/layout/activity_netp_verify_subs.xml rename to network-protection/network-protection-impl/src/main/res/layout/activity_netp_verify_subs.xml diff --git a/network-protection/network-protection-subscription-internal/src/main/res/layout/view_settings_netp.xml b/network-protection/network-protection-impl/src/main/res/layout/view_settings_netp.xml similarity index 100% rename from network-protection/network-protection-subscription-internal/src/main/res/layout/view_settings_netp.xml rename to network-protection/network-protection-impl/src/main/res/layout/view_settings_netp.xml diff --git a/network-protection/network-protection-impl/src/main/res/values/donottranslate.xml b/network-protection/network-protection-impl/src/main/res/values/donottranslate.xml index 178a0cd47cd0..0641d6c43ca8 100644 --- a/network-protection/network-protection-impl/src/main/res/values/donottranslate.xml +++ b/network-protection/network-protection-impl/src/main/res/values/donottranslate.xml @@ -279,4 +279,17 @@ Subscribe to Privacy Pro to reconnect DuckDuckGo VPN. Subscribe Dismiss + + + Verify Subscription + Please wait while we verify your subscription… + Oops! You don’t have any valid subscription. + Oops! Your subscription is not valid for DuckDuckGo VPN. + + + VPN + Enabled + Connecting… + Disabled + Secure your network connection anytime, anywhere \ No newline at end of file diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/configuration/NetpWaitlistRequestInterceptorTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/configuration/NetpWaitlistRequestInterceptorTest.kt index fd7ed3b63417..7dc09d1b2dc1 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/configuration/NetpWaitlistRequestInterceptorTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/configuration/NetpWaitlistRequestInterceptorTest.kt @@ -6,13 +6,9 @@ import com.duckduckgo.appbuildconfig.api.BuildFlavor.INTERNAL import com.duckduckgo.appbuildconfig.api.BuildFlavor.PLAY import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.api.FakeChain -import com.duckduckgo.mobile.android.vpn.prefs.FakeVpnSharedPreferencesProvider import com.duckduckgo.networkprotection.impl.fakes.FakeNetPWaitlistDataStore -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository -import com.duckduckgo.networkprotection.impl.store.RealNetworkProtectionRepository import com.duckduckgo.networkprotection.impl.waitlist.store.NetPWaitlistRepository import com.duckduckgo.networkprotection.impl.waitlist.store.RealNetPWaitlistRepository -import com.duckduckgo.networkprotection.store.RealNetworkProtectionPrefs import com.duckduckgo.subscriptions.api.Subscriptions import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking @@ -22,7 +18,6 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -31,9 +26,6 @@ class NetpWaitlistRequestInterceptorTest { private val coroutineRule = CoroutineTestRule() private val appBuildConfig: AppBuildConfig = mock() private val subscriptions: Subscriptions = mock() - private val networkProtectionRepository: NetworkProtectionRepository = RealNetworkProtectionRepository( - RealNetworkProtectionPrefs(FakeVpnSharedPreferencesProvider()), - ) private lateinit var netPWaitlistRepository: NetPWaitlistRepository private lateinit var interceptor: NetpWaitlistRequestInterceptor @@ -54,7 +46,6 @@ class NetpWaitlistRequestInterceptorTest { interceptor = NetpWaitlistRequestInterceptor( netPWaitlistRepository, appBuildConfig, - networkProtectionRepository, subscriptions, ) } @@ -207,42 +198,4 @@ class NetpWaitlistRequestInterceptorTest { assertTrue(headers.names().contains("NetP-Debug-Code")) } } - - @Test - fun whenUrlIsNotNetpThenDoNothingWithVPNAccessRevoked() = runTest { - whenever(subscriptions.isEnabled()).thenReturn(true) - val fakeChain = FakeChain(url = "https://improving.duckduckgo.com/t/m_netp_ev_enabled_android_phone?atb=v336-7&appVersion=5.131.0&test=1") - - interceptor.intercept(fakeChain) - - assertFalse(networkProtectionRepository.vpnAccessRevoked) - } - - @Test - fun whenUrlIsNetPAndResponseCodeIs200ThenSetVPNAccessRevokedFalse() = runTest { - val fakeChain = FakeChain(url = "https://controller.netp.duckduckgo.com/servers") - whenever(appBuildConfig.flavor).thenReturn(INTERNAL) - whenever(subscriptions.isEnabled()).thenReturn(true) - whenever(subscriptions.getAccessToken()).thenReturn("token123") - - interceptor.intercept(fakeChain) - - assertFalse(networkProtectionRepository.vpnAccessRevoked) - } - - @Test - fun whenUrlIsNetPAndResponseCodeIs403ThenSetVPNAccessRevokedFalse() = runTest { - val url = "https://controller.netp.duckduckgo.com/servers" - val fakeChain = FakeChain( - url = url, - expectedResponseCode = 403, - ) - whenever(appBuildConfig.flavor).thenReturn(INTERNAL) - whenever(subscriptions.isEnabled()).thenReturn(true) - whenever(subscriptions.getAccessToken()).thenReturn("token123") - - interceptor.intercept(fakeChain) - - assertTrue(networkProtectionRepository.vpnAccessRevoked) - } } diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationSchedulerTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationSchedulerTest.kt index b8b8a33bf700..372bd204c71a 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationSchedulerTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/notification/NetPDisabledNotificationSchedulerTest.kt @@ -21,15 +21,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.Toggle.State -import com.duckduckgo.mobile.android.vpn.prefs.FakeVpnSharedPreferencesProvider import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason import com.duckduckgo.networkprotection.api.NetworkProtectionState import com.duckduckgo.networkprotection.impl.settings.FakeNetPSettingsLocalConfigFactory import com.duckduckgo.networkprotection.impl.settings.NetPSettingsLocalConfig -import com.duckduckgo.networkprotection.impl.store.RealNetworkProtectionRepository import com.duckduckgo.networkprotection.impl.waitlist.FakeNetPRemoteFeatureFactory import com.duckduckgo.networkprotection.impl.waitlist.NetPRemoteFeature -import com.duckduckgo.networkprotection.store.RealNetworkProtectionPrefs import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -75,9 +72,6 @@ class NetPDisabledNotificationSchedulerTest { TestScope(), coroutineRule.testDispatcherProvider, netPRemoteFeature, - RealNetworkProtectionRepository( - RealNetworkProtectionPrefs(FakeVpnSharedPreferencesProvider()), - ), ) } diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitorTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitorTest.kt index 130537203ae0..1a4d5fbae45e 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitorTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/revoked/NetpVpnAccessRevokedDialogMonitorTest.kt @@ -17,10 +17,16 @@ package com.duckduckgo.networkprotection.impl.revoked import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository +import com.duckduckgo.networkprotection.api.NetworkProtectionState +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.ACTIVE +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.EXPIRED +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.INACTIVE +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.SIGNED_OUT import com.duckduckgo.networkprotection.impl.waitlist.store.NetPWaitlistRepository import com.duckduckgo.subscriptions.api.Subscriptions -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Rule import org.junit.Test @@ -38,7 +44,7 @@ class NetpVpnAccessRevokedDialogMonitorTest { val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() @Mock - private lateinit var networkProtectionRepository: NetworkProtectionRepository + private lateinit var netpSubscriptionManager: NetpSubscriptionManager @Mock private lateinit var betaEndedDialog: BetaEndedDialog @@ -51,85 +57,277 @@ class NetpVpnAccessRevokedDialogMonitorTest { @Mock private lateinit var netPWaitlistRepository: NetPWaitlistRepository + + @Mock lateinit var networkProtectionState: NetworkProtectionState + private lateinit var netpVpnAccessRevokedDialogMonitor: NetpVpnAccessRevokedDialogMonitor @Before fun setUp() { MockitoAnnotations.openMocks(this) + runBlocking { whenever(networkProtectionState.isOnboarded()) }.thenReturn(true) + runBlocking { whenever(networkProtectionState.isEnabled()) }.thenReturn(false) + netpVpnAccessRevokedDialogMonitor = NetpVpnAccessRevokedDialogMonitor( - networkProtectionRepository, + netpSubscriptionManager, coroutineTestRule.testScope, coroutineTestRule.testDispatcherProvider, betaEndedDialog, accessRevokedDialog, subscriptions, netPWaitlistRepository, + networkProtectionState, ) } @Test - fun whenUserParticipatedInBetaAndPrivacyProActiveAndDialogNotShownThenShowBetaEndDialog() = runTest { - whenever(networkProtectionRepository.vpnAccessRevoked).thenReturn(true) - whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") - whenever(subscriptions.isEnabled()).thenReturn(true) + fun whenUserParticipatedInBetaAndPrivacyProActiveAndDialogNotShownThenShowBetaEndDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(INACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog).show(any()) + verify(networkProtectionState, never()).stop() + verifyNoInteractions(accessRevokedDialog) + } + } + + @Test + fun whenDialogAlreadyNotShownThenDontShowAnyDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(false) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenPrivacyProNotActiveThenDontShowAnyDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") + whenever(subscriptions.isEnabled()).thenReturn(false) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaThenDontShowAnyDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaVPNEnabledThenDontShowAnyDialog() { + coroutineTestRule.testScope.launch { + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaAndVPNInactiveThenClearShownAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(INACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaAndVPNEnabledAndInactiveThenClearShownAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(INACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaAndVPNSignedOutThenClearShownAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(SIGNED_OUT) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaAndVPNEnabledAndSignedOutThenClearShownAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(SIGNED_OUT) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaAndVPNActiveThenClearShownAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) + + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } + } + + @Test + fun whenUserNotInBetaAndVPNEnabledActiveThenClearShownAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) - netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) - verify(betaEndedDialog).show(any()) - verifyNoInteractions(accessRevokedDialog) + verify(betaEndedDialog, never()).show(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog).clearIsShown() + } } @Test - fun whenDialogAlreadyNotShownThenDontShowAnyDialog() = runTest { - whenever(networkProtectionRepository.vpnAccessRevoked).thenReturn(false) - whenever(betaEndedDialog.shouldShowDialog()).thenReturn(false) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") - whenever(subscriptions.isEnabled()).thenReturn(true) + fun whenUserNotInBetaAndVPNExpiredThenShowAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(EXPIRED) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) - netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) - verify(betaEndedDialog, never()).show(any()) - verifyNoInteractions(accessRevokedDialog) + verify(betaEndedDialog, never()).show(any()) + verify(accessRevokedDialog).showOnce(any()) + verify(networkProtectionState, never()).stop() + } } @Test - fun whenPrivacyProNotActiveThenDontShowAnyDialog() = runTest { - whenever(networkProtectionRepository.vpnAccessRevoked).thenReturn(false) - whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") - whenever(subscriptions.isEnabled()).thenReturn(false) + fun whenUserNotInBetaAndVPNEnabledExpiredThenShowAccessRevokedDialog() { + coroutineTestRule.testScope.launch { + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(EXPIRED) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) - netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) - verify(betaEndedDialog, never()).show(any()) - verifyNoInteractions(accessRevokedDialog) + verify(betaEndedDialog, never()).show(any()) + verify(accessRevokedDialog).showOnce(any()) + verify(networkProtectionState).stop() + } } @Test - fun whenUserNotInBetaThenDontShowAnyDialog() = runTest { - whenever(networkProtectionRepository.vpnAccessRevoked).thenReturn(false) - whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) - whenever(subscriptions.isEnabled()).thenReturn(true) + fun whenUserNotInBetaAndVPNExpiredAndVpnNotOnboardedThenNoDialog() { + coroutineTestRule.testScope.launch { + whenever(networkProtectionState.isOnboarded()).thenReturn(false) + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(EXPIRED) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) - netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) - verify(betaEndedDialog, never()).show(any()) - verifyNoInteractions(accessRevokedDialog) + verify(betaEndedDialog, never()).show(any()) + verify(accessRevokedDialog, never()).showOnce(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog.clearIsShown()) + } } @Test - fun whenUserNotInBetaAndVPNAccessRevokedThenShowAccessRevokedDialog() = runTest { - whenever(networkProtectionRepository.vpnAccessRevoked).thenReturn(true) - whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) - whenever(subscriptions.isEnabled()).thenReturn(true) + fun whenUserNotInBetaAndVPNEanbledAndExpiredAndVpnNotOnboardedThenNoDialog() { + coroutineTestRule.testScope.launch { + whenever(networkProtectionState.isOnboarded()).thenReturn(false) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(EXPIRED) + whenever(betaEndedDialog.shouldShowDialog()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn(null) + whenever(subscriptions.isEnabled()).thenReturn(true) - netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) + netpVpnAccessRevokedDialogMonitor.onActivityResumed(mock()) - verify(betaEndedDialog, never()).show(any()) - verify(accessRevokedDialog).show(any()) + verify(betaEndedDialog, never()).show(any()) + verify(accessRevokedDialog, never()).showOnce(any()) + verify(networkProtectionState, never()).stop() + verify(accessRevokedDialog.clearIsShown()) + } } } diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/NetworkProtectionAccessStateTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/NetworkProtectionAccessStateTest.kt new file mode 100644 index 000000000000..ec45579e2b24 --- /dev/null +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/NetworkProtectionAccessStateTest.kt @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2024 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.networkprotection.impl.subscription + +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.networkprotection.api.NetworkProtectionState +import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.InBeta +import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.NotUnlocked +import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.ACTIVE +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.EXPIRED +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.INACTIVE +import com.duckduckgo.networkprotection.impl.subscription.NetpSubscriptionManager.VpnStatus.SIGNED_OUT +import com.duckduckgo.networkprotection.impl.waitlist.store.NetPWaitlistRepository +import com.duckduckgo.subscriptions.api.Subscriptions +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +class NetworkProtectionAccessStateTest { + private lateinit var testee: NetworkProtectionAccessState + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + @Mock + private lateinit var netPWaitlistRepository: NetPWaitlistRepository + + @Mock + private lateinit var networkProtectionState: NetworkProtectionState + + @Mock + private lateinit var netpSubscriptionManager: NetpSubscriptionManager + + @Mock + private lateinit var networkProtectionRepository: NetworkProtectionRepository + + @Mock + private lateinit var subscriptions: Subscriptions + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + runBlocking { whenever(subscriptions.isEnabled()) }.thenReturn(true) + + testee = NetworkProtectionAccessState( + netPWaitlistRepository, + networkProtectionState, + coroutineTestRule.testDispatcherProvider, + netpSubscriptionManager, + subscriptions, + ) + } + + @Test + fun whenSubscriptionsDisabledThenReturnNotUnlocked() = runTest { + whenever(subscriptions.isEnabled()).thenReturn(false) + testee.getState().also { + assertEquals(NotUnlocked, it) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnInactiveAndNetpDisabledThenReturnNotUnlocked() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(INACTIVE) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + testee.getState().also { + assertEquals(NotUnlocked, it) + verifyNoInteractions(networkProtectionRepository) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnExpiredAndNetpDisabledThenReturnNotUnlocked() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(EXPIRED) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + testee.getState().also { + assertEquals(NotUnlocked, it) + verifyNoInteractions(networkProtectionRepository) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnSignedOutAndNetpDisabledThenReturnNotUnlocked() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(SIGNED_OUT) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + testee.getState().also { + assertEquals(NotUnlocked, it) + verifyNoInteractions(networkProtectionRepository) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnInactiveAndNetpEnabledThenReturnNotUnlockedAndResetVpnState() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(INACTIVE) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + testee.getState().also { + assertEquals(NotUnlocked, it) + verify(networkProtectionState).stop() + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnExpiredAndNetpEnabledThenReturnNotUnlockedAndResetVpnState() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(EXPIRED) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + testee.getState().also { + assertEquals(NotUnlocked, it) + verify(networkProtectionState).stop() + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnSignedOutAndNetpEnabledThenReturnNotUnlockedAndResetVpnState() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(SIGNED_OUT) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + testee.getState().also { + assertEquals(NotUnlocked, it) + verify(networkProtectionState).stop() + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnActiveAndHasAuthTokenAndNotAcceptedTermsReturnInBetaFalse() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(false) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") + testee.getState().also { + assertEquals(InBeta(false), it) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnActiveAndAcceptedTermsReturnInBetaTrue() = runTest { + whenever(netpSubscriptionManager.getVpnStatus()).thenReturn(ACTIVE) + whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") + testee.getState().also { + assertEquals(InBeta(true), it) + } + } + + @Test + fun whenSubscriptionsDisabledThenReturnFlowEmitsNotUnlocked() = runTest { + whenever(subscriptions.isEnabled()).thenReturn(false) + testee.getStateFlow().test { + assertEquals(NotUnlocked, expectMostRecentItem()) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnInactiveAndNetpDisabledThenReturnFlowEmitsNotUnlocked() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(INACTIVE)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + testee.getStateFlow().test { + assertEquals(NotUnlocked, expectMostRecentItem()) + verifyNoInteractions(networkProtectionRepository) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnExpiredAndNetpDisabledThenReturnFlowEmitsNotUnlocked() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(EXPIRED)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + testee.getStateFlow().test { + assertEquals(NotUnlocked, expectMostRecentItem()) + verifyNoInteractions(networkProtectionRepository) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnSingedOutAndNetpDisabledThenReturnFlowEmitsNotUnlocked() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(SIGNED_OUT)) + whenever(networkProtectionState.isEnabled()).thenReturn(false) + testee.getStateFlow().test { + assertEquals(NotUnlocked, expectMostRecentItem()) + verifyNoInteractions(networkProtectionRepository) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnInactiveAndNetpEnabledThenReturnFlowEmitNotUnlockedAndResetVpnState() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(INACTIVE)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + testee.getStateFlow().test { + assertEquals(NotUnlocked, expectMostRecentItem()) + verify(networkProtectionState).stop() + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnExpiredAndNetpEnabledThenReturnFlowEmitNotUnlockedAndResetVpnState() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(EXPIRED)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + testee.getStateFlow().test { + assertEquals(NotUnlocked, expectMostRecentItem()) + verify(networkProtectionState).stop() + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnSignedOutAndNetpEnabledThenReturnFlowEmitNotUnlockedAndResetVpnState() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(SIGNED_OUT)) + whenever(networkProtectionState.isEnabled()).thenReturn(true) + testee.getStateFlow().test { + assertEquals(NotUnlocked, expectMostRecentItem()) + verify(networkProtectionState).stop() + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnActiveAndHasAuthTokenAndNotAcceptedTermsReturnFlowEmitInBetaFalse() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(ACTIVE)) + whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(false) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") + testee.getStateFlow().test { + assertEquals(InBeta(false), expectMostRecentItem()) + } + } + + @Test + fun whenSubscriptionsEnabledAndVpnActiveAndAcceptedTermsReturnFlowEmitInBetaTrue() = runTest { + whenever(netpSubscriptionManager.vpnStatus()).thenReturn(flowOf(ACTIVE)) + whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(true) + whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") + testee.getStateFlow().test { + assertEquals(InBeta(true), expectMostRecentItem()) + } + } +} diff --git a/network-protection/network-protection-subscription-internal/src/test/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPViewModelTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt similarity index 87% rename from network-protection/network-protection-subscription-internal/src/test/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPViewModelTest.kt rename to network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt index 09842a6f3d89..fda4db72964d 100644 --- a/network-protection/network-protection-subscription-internal/src/test/java/com/duckduckgo/networkprotection/subscription/settings/ProSettingNetPViewModelTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/subscription/settings/ProSettingNetPViewModelTest.kt @@ -1,4 +1,20 @@ -package com.duckduckgo.networkprotection.subscription.settings +/* + * Copyright (c) 2024 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.networkprotection.impl.subscription.settings import app.cash.turbine.test import com.duckduckgo.app.statistics.pixels.Pixel @@ -15,12 +31,12 @@ import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitli import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.JoinedWaitlist import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.NotUnlocked import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.PendingInviteCode +import com.duckduckgo.networkprotection.impl.R import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixelNames.NETP_SETTINGS_PRESSED -import com.duckduckgo.networkprotection.subscription.R -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.Command -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending -import com.duckduckgo.networkprotection.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.Command +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Hidden +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.Pending +import com.duckduckgo.networkprotection.impl.subscription.settings.ProSettingNetPViewModel.NetPEntryState.ShowState import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.* diff --git a/network-protection/network-protection-subscription-internal/.gitignore b/network-protection/network-protection-subscription-internal/.gitignore deleted file mode 100644 index 42afabfd2abe..000000000000 --- a/network-protection/network-protection-subscription-internal/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/network-protection/network-protection-subscription-internal/build.gradle b/network-protection/network-protection-subscription-internal/build.gradle deleted file mode 100644 index a230a61b60d7..000000000000 --- a/network-protection/network-protection-subscription-internal/build.gradle +++ /dev/null @@ -1,68 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'com.squareup.anvil' -} - -apply from: "$rootProject.projectDir/gradle/android-library.gradle" - -android { - namespace 'com.duckduckgo.networkprotection.subscription' - lintOptions { - baseline file("lint-baseline.xml") - } - testOptions { - unitTests { - includeAndroidResources = true - } - } - anvil { - generateDaggerFactories = true // default is false - } -} - -dependencies { - testImplementation 'junit:junit:4.13.1' - anvil project(':anvil-compiler') - implementation project(':app-build-config-api') - implementation project(':anvil-annotations') - implementation project(':browser-api') - implementation project(':common-ui') - implementation project(':common-utils') - implementation project(':di') - implementation project(':navigation-api') - implementation project(':vpn-api') - implementation project(':network-protection-api') - implementation project(':network-protection-impl') - implementation project(':subscriptions-api') - implementation project(':settings-api') - implementation project(':statistics') - - implementation AndroidX.appCompat - implementation AndroidX.lifecycle.runtime.ktx - implementation AndroidX.lifecycle.viewModelKtx - implementation AndroidX.work.runtimeKtx - implementation Google.android.material - implementation Google.dagger - implementation KotlinX.coroutines.core - implementation Square.retrofit2.retrofit - implementation Square.retrofit2.converter.moshi - implementation "androidx.work:work-multiprocess:_" - implementation "com.squareup.logcat:logcat:_" - - // Testing dependencies - testImplementation project(':common-test') - testImplementation "org.mockito.kotlin:mockito-kotlin:_" - testImplementation Testing.junit4 - testImplementation AndroidX.archCore.testing - testImplementation AndroidX.core - testImplementation AndroidX.test.ext.junit - testImplementation "androidx.test:runner:_" - testImplementation Testing.robolectric - testImplementation 'app.cash.turbine:turbine:_' - testImplementation (KotlinX.coroutines.test) { - // https://github.com/Kotlin/kotlinx.coroutines/issues/2023 - // conflicts with mockito due to direct inclusion of byte buddy - exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" - } -} \ No newline at end of file diff --git a/network-protection/network-protection-subscription-internal/lint-baseline.xml b/network-protection/network-protection-subscription-internal/lint-baseline.xml deleted file mode 100644 index 0722790eb056..000000000000 --- a/network-protection/network-protection-subscription-internal/lint-baseline.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/network-protection/network-protection-subscription-internal/src/main/AndroidManifest.xml b/network-protection/network-protection-subscription-internal/src/main/AndroidManifest.xml deleted file mode 100644 index be1f37e1e101..000000000000 --- a/network-protection/network-protection-subscription-internal/src/main/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetpSubscriptionManager.kt b/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetpSubscriptionManager.kt deleted file mode 100644 index 67c669f78371..000000000000 --- a/network-protection/network-protection-subscription-internal/src/main/java/com/duckduckgo/networkprotection/subscription/NetpSubscriptionManager.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.networkprotection.subscription - -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.subscriptions.api.Product.NetP -import com.duckduckgo.subscriptions.api.Subscriptions -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext - -interface NetpSubscriptionManager { - suspend fun getToken(): String? - suspend fun hasValidEntitlement(): Boolean - - fun hasValidEntitlementFlow(): Flow -} - -@ContributesBinding(AppScope::class) -class RealNetpSubscriptionManager @Inject constructor( - private val subscriptions: Subscriptions, - private val dispatcherProvider: DispatcherProvider, -) : NetpSubscriptionManager { - - override suspend fun getToken(): String? = withContext(dispatcherProvider.io()) { - subscriptions.getAccessToken() - } - - override suspend fun hasValidEntitlement(): Boolean = withContext(dispatcherProvider.io()) { - val entitlements = subscriptions.getEntitlementStatus().firstOrNull() - return@withContext (entitlements?.contains(NetP) == true) - } - - override fun hasValidEntitlementFlow(): Flow = subscriptions.getEntitlementStatus().map { it.contains(NetP) } -} diff --git a/network-protection/network-protection-subscription-internal/src/main/res/values/donottranslate.xml b/network-protection/network-protection-subscription-internal/src/main/res/values/donottranslate.xml deleted file mode 100644 index 1f095431daba..000000000000 --- a/network-protection/network-protection-subscription-internal/src/main/res/values/donottranslate.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - Verify Subscription - Please wait while we verify your subscription… - Oops! You don’t have any valid subscription. - Oops! Your subscription is not valid for DuckDuckGo VPN. - - - VPN - Enabled - Connecting… - Disabled - Secure your network connection anytime, anywhere - - \ No newline at end of file diff --git a/network-protection/network-protection-subscription-internal/src/test/java/com/duckduckgo/networkprotection/subscription/NetworkProtectionAccessStateTest.kt b/network-protection/network-protection-subscription-internal/src/test/java/com/duckduckgo/networkprotection/subscription/NetworkProtectionAccessStateTest.kt deleted file mode 100644 index d13256e7f81f..000000000000 --- a/network-protection/network-protection-subscription-internal/src/test/java/com/duckduckgo/networkprotection/subscription/NetworkProtectionAccessStateTest.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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.networkprotection.subscription - -import app.cash.turbine.test -import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.networkprotection.api.NetworkProtectionState -import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.InBeta -import com.duckduckgo.networkprotection.api.NetworkProtectionWaitlist.NetPWaitlistState.NotUnlocked -import com.duckduckgo.networkprotection.impl.store.NetworkProtectionRepository -import com.duckduckgo.networkprotection.impl.waitlist.store.NetPWaitlistRepository -import com.duckduckgo.subscriptions.api.Subscriptions -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever - -@ExperimentalCoroutinesApi -class NetworkProtectionAccessStateTest { - private lateinit var testee: NetworkProtectionAccessState - - @get:Rule - val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() - - @Mock - private lateinit var netPWaitlistRepository: NetPWaitlistRepository - - @Mock - private lateinit var networkProtectionState: NetworkProtectionState - - @Mock - private lateinit var netpSubscriptionManager: NetpSubscriptionManager - - @Mock - private lateinit var networkProtectionRepository: NetworkProtectionRepository - - @Mock - private lateinit var subscriptions: Subscriptions - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - - runBlocking { whenever(subscriptions.isEnabled()) }.thenReturn(true) - - testee = NetworkProtectionAccessState( - netPWaitlistRepository, - networkProtectionState, - coroutineTestRule.testDispatcherProvider, - netpSubscriptionManager, - networkProtectionRepository, - subscriptions, - ) - } - - @Test - fun whenSubscriptionsDisabledThenReturnNotUnlocked() = runTest { - whenever(subscriptions.isEnabled()).thenReturn(false) - testee.getState().also { - assertEquals(NotUnlocked, it) - } - } - - @Test - fun whenSubscriptionsEnabledAndHasNoEntitlementAndNetpDisabledThenReturnNotUnlocked() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlement()).thenReturn(false) - whenever(networkProtectionState.isEnabled()).thenReturn(false) - testee.getState().also { - assertEquals(NotUnlocked, it) - verifyNoInteractions(networkProtectionRepository) - } - } - - @Test - fun whenSubscriptionsEnabledAndHasNoEntitlementAndNetpEnabledThenReturnNotUnlockedAndResetVpnState() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlement()).thenReturn(false) - whenever(networkProtectionState.isEnabled()).thenReturn(true) - testee.getState().also { - assertEquals(NotUnlocked, it) - verify(networkProtectionRepository).vpnAccessRevoked = true - verify(networkProtectionState).stop() - } - } - - @Test - fun whenSubscriptionsEnabledAndHasEntitlementAndHasAuthTokenAndNotAcceptedTermsReturnInBetaFalse() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlement()).thenReturn(true) - whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(false) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") - testee.getState().also { - assertEquals(InBeta(false), it) - } - } - - @Test - fun whenSubscriptionsEnabledAndHasEntitlementAndAcceptedTermsReturnInBetaTrue() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlement()).thenReturn(true) - whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(true) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") - testee.getState().also { - assertEquals(InBeta(true), it) - } - } - - @Test - fun whenSubscriptionsDisabledThenReturnFlowEmitsNotUnlocked() = runTest { - whenever(subscriptions.isEnabled()).thenReturn(false) - testee.getStateFlow().test { - assertEquals(NotUnlocked, expectMostRecentItem()) - } - } - - @Test - fun whenSubscriptionsEnabledAndHasNoEntitlementAndNetpDisabledThenReturnFlowEmitsNotUnlocked() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlementFlow()).thenReturn(flowOf(false)) - whenever(networkProtectionState.isEnabled()).thenReturn(false) - testee.getStateFlow().test { - assertEquals(NotUnlocked, expectMostRecentItem()) - verifyNoInteractions(networkProtectionRepository) - } - } - - @Test - fun whenSubscriptionsEnabledAndHasNoEntitlementAndNetpEnabledThenReturnFlowEmitNotUnlockedAndResetVpnState() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlementFlow()).thenReturn(flowOf(false)) - whenever(networkProtectionState.isEnabled()).thenReturn(true) - testee.getStateFlow().test { - assertEquals(NotUnlocked, expectMostRecentItem()) - verify(networkProtectionRepository).vpnAccessRevoked = true - verify(networkProtectionState).stop() - } - } - - @Test - fun whenSubscriptionsEnabledAndHasEntitlementAndHasAuthTokenAndNotAcceptedTermsReturnFlowEmitInBetaFalse() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlementFlow()).thenReturn(flowOf(true)) - whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(false) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") - testee.getStateFlow().test { - assertEquals(InBeta(false), expectMostRecentItem()) - } - } - - @Test - fun whenSubscriptionsEnabledAndHasEntitlementAndAcceptedTermsReturnFlowEmitInBetaTrue() = runTest { - whenever(netpSubscriptionManager.hasValidEntitlementFlow()).thenReturn(flowOf(true)) - whenever(netPWaitlistRepository.didAcceptWaitlistTerms()).thenReturn(true) - whenever(netPWaitlistRepository.getAuthenticationToken()).thenReturn("123") - testee.getStateFlow().test { - assertEquals(InBeta(true), expectMostRecentItem()) - } - } -}