diff --git a/CHANGELOG.md b/CHANGELOG.md index 692945245..ac2cba541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixes + +- Clear consumed Payment Token (HumanVerification token). + ## [7.1.11] ### Changes diff --git a/auth/presentation/api/auth-presentation.api b/auth/presentation/api/auth-presentation.api index 7157e8d6c..63bb93165 100644 --- a/auth/presentation/api/auth-presentation.api +++ b/auth/presentation/api/auth-presentation.api @@ -2152,11 +2152,11 @@ public final class me/proton/core/auth/presentation/viewmodel/signup/RecoverySMS } public final class me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel_Factory : dagger/internal/Factory { - public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V - public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/auth/presentation/viewmodel/signup/SignupViewModel_Factory; + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/auth/presentation/viewmodel/signup/SignupViewModel_Factory; public synthetic fun get ()Ljava/lang/Object; public fun get ()Lme/proton/core/auth/presentation/viewmodel/signup/SignupViewModel; - public static fun newInstance (Lme/proton/core/auth/domain/usecase/signup/PerformCreateUser;Lme/proton/core/auth/domain/usecase/signup/PerformCreateExternalEmailUser;Lme/proton/core/crypto/common/keystore/KeyStoreCrypto;Lme/proton/core/plan/presentation/PlansOrchestrator;Lme/proton/core/payment/presentation/PaymentsOrchestrator;Lme/proton/core/network/domain/client/ClientIdProvider;Lme/proton/core/humanverification/domain/HumanVerificationManager;Lme/proton/core/auth/domain/usecase/PerformLogin;Lme/proton/core/challenge/domain/ChallengeManager;Lme/proton/core/auth/domain/usecase/signup/SignupChallengeConfig;Lme/proton/core/humanverification/presentation/HumanVerificationOrchestrator;Landroidx/lifecycle/SavedStateHandle;)Lme/proton/core/auth/presentation/viewmodel/signup/SignupViewModel; + public static fun newInstance (Lme/proton/core/auth/domain/usecase/signup/PerformCreateUser;Lme/proton/core/auth/domain/usecase/signup/PerformCreateExternalEmailUser;Lme/proton/core/crypto/common/keystore/KeyStoreCrypto;Lme/proton/core/plan/presentation/PlansOrchestrator;Lme/proton/core/payment/presentation/PaymentsOrchestrator;Lme/proton/core/humanverification/domain/HumanVerificationManager;Lme/proton/core/auth/domain/usecase/PerformLogin;Lme/proton/core/challenge/domain/ChallengeManager;Lme/proton/core/auth/domain/usecase/signup/SignupChallengeConfig;Lme/proton/core/humanverification/presentation/HumanVerificationOrchestrator;Landroidx/lifecycle/SavedStateHandle;)Lme/proton/core/auth/presentation/viewmodel/signup/SignupViewModel; } public final class me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel_HiltModules { diff --git a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/ui/signup/SignupActivity.kt b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/ui/signup/SignupActivity.kt index 78d4aff8a..49e1eaa2f 100644 --- a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/ui/signup/SignupActivity.kt +++ b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/ui/signup/SignupActivity.kt @@ -37,6 +37,7 @@ import me.proton.core.auth.presentation.R import me.proton.core.auth.presentation.databinding.ActivitySignupBinding import me.proton.core.auth.presentation.entity.signup.SignUpInput import me.proton.core.auth.presentation.entity.signup.SignUpResult +import me.proton.core.auth.presentation.entity.signup.SubscriptionDetails import me.proton.core.auth.presentation.ui.AuthActivity import me.proton.core.auth.presentation.ui.removeCreatingUser import me.proton.core.auth.presentation.ui.showCongrats @@ -122,10 +123,16 @@ class SignupActivity : AuthActivity(ActivitySignupBinding KEY_PLAN_SELECTED, this ) { _, bundle -> val plan = bundle.getParcelable(BUNDLE_KEY_PLAN) - val billing = bundle.getParcelable(BUNDLE_KEY_BILLING_DETAILS) + val billingResult = bundle.getParcelable(BUNDLE_KEY_BILLING_DETAILS) if (plan != null) { + signUpViewModel.subscriptionDetails = SubscriptionDetails( + billingResult = billingResult, + planName = plan.planName, + planDisplayName = plan.planDisplayName, + cycle = plan.cycle.toSubscriptionCycle() + ) + signUpViewModel.startCreateUserWorkflow() supportFragmentManager.showCreatingUser() - onPlanSelected(plan, billing) } else { supportFragmentManager.removePlansSignup() signUpViewModel.onPlanChooserCancel() @@ -138,7 +145,6 @@ class SignupActivity : AuthActivity(ActivitySignupBinding is PostLoginAccountSetup.Result.Error.UnlockPrimaryKeyError -> onUnlockUserError(result.error) is PostLoginAccountSetup.Result.Error.UserCheckError -> onLoginError(result.error.localizedMessage) is PostLoginAccountSetup.Result.UserUnlocked -> onLoginSuccess(result.userId) - is PostLoginAccountSetup.Result.Need.ChangePassword, is PostLoginAccountSetup.Result.Need.ChooseUsername, is PostLoginAccountSetup.Result.Need.SecondFactor, @@ -146,15 +152,6 @@ class SignupActivity : AuthActivity(ActivitySignupBinding }.exhaustive } - private fun onPlanSelected(plan: SelectedPlan, billingResult: BillingResult?) { - if (billingResult == null) { - signUpViewModel.startCreateUserWorkflow() - } else { - val cycle = plan.cycle.toSubscriptionCycle() - signUpViewModel.startCreatePaidUserWorkflow(plan.planName, plan.planDisplayName, cycle, billingResult) - } - } - private fun onSignUpSuccess(loginUsername: String, encryptedPassword: EncryptedString) { with(supportFragmentManager) { for (i in 0..backStackEntryCount) { diff --git a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt index 6436bd7b7..6bb67f9ac 100644 --- a/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt +++ b/auth/presentation/src/main/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel.kt @@ -49,10 +49,7 @@ import me.proton.core.crypto.common.keystore.encrypt import me.proton.core.humanverification.domain.HumanVerificationManager import me.proton.core.humanverification.presentation.HumanVerificationOrchestrator import me.proton.core.humanverification.presentation.onHumanVerificationFailed -import me.proton.core.network.domain.client.ClientIdProvider -import me.proton.core.payment.domain.entity.SubscriptionCycle import me.proton.core.payment.presentation.PaymentsOrchestrator -import me.proton.core.payment.presentation.entity.BillingResult import me.proton.core.plan.presentation.PlansOrchestrator import me.proton.core.presentation.savedstate.flowState import me.proton.core.presentation.savedstate.state @@ -68,7 +65,6 @@ internal class SignupViewModel @Inject constructor( private val keyStoreCrypto: KeyStoreCrypto, private val plansOrchestrator: PlansOrchestrator, private val paymentsOrchestrator: PaymentsOrchestrator, - private val clientIdProvider: ClientIdProvider, private val humanVerificationManager: HumanVerificationManager, private val performLogin: PerformLogin, private val challengeManager: ChallengeManager, @@ -200,33 +196,6 @@ internal class SignupViewModel @Inject constructor( }.exhaustive } - fun startCreatePaidUserWorkflow( - planName: String, - planDisplayName: String, - cycle: SubscriptionCycle, - billingResult: BillingResult - ) = viewModelScope.launch { - val clientId = requireNotNull(clientIdProvider.getClientId(sessionId = null)) - subscriptionDetails = SubscriptionDetails( - billingResult = billingResult, - planName = planName, - planDisplayName = planDisplayName, - cycle = cycle - ) - if (billingResult.paySuccess) { - viewModelScope.launch { - // update subscription details - humanVerificationManager.addDetails( - details = BillingResult.paymentDetails( - clientId = clientId, - token = billingResult.token!! - ) - ) - } - startCreateUserWorkflow() - } - } - override fun register(context: FragmentActivity) { super.register(context) plansOrchestrator.register(context) diff --git a/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt b/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt index 80b5f342f..509bc4c42 100644 --- a/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt +++ b/auth/presentation/src/test/kotlin/me/proton/core/auth/presentation/viewmodel/signup/SignupViewModelTest.kt @@ -119,7 +119,6 @@ class SignupViewModelTest : ArchTest, CoroutinesTest { keyStoreCrypto, plansOrchestrator, paymentsOrchestrator, - clientIdProvider, humanVerificationManager, performLogin, challengeManager, diff --git a/human-verification/data/api/human-verification-data.api b/human-verification/data/api/human-verification-data.api index e3232ce60..0f4c16108 100644 --- a/human-verification/data/api/human-verification-data.api +++ b/human-verification/data/api/human-verification-data.api @@ -17,6 +17,7 @@ public final class me/proton/core/humanverification/data/HumanVerificationManage public fun (Lme/proton/core/network/domain/humanverification/HumanVerificationProvider;Lme/proton/core/network/domain/humanverification/HumanVerificationListener;Lme/proton/core/humanverification/domain/repository/HumanVerificationRepository;)V public fun addDetails (Lme/proton/core/network/domain/humanverification/HumanVerificationDetails;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun awaitHumanVerificationProcessFinished (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun clearDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun getHumanVerificationDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun handleHumanVerificationFailed (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun handleHumanVerificationSuccess (Lme/proton/core/network/domain/client/ClientId;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -187,6 +188,7 @@ public final class me/proton/core/humanverification/data/entity/HumanVerificatio public final class me/proton/core/humanverification/data/repository/HumanVerificationRepositoryImpl : me/proton/core/humanverification/domain/repository/HumanVerificationRepository { public fun (Lme/proton/core/humanverification/data/db/HumanVerificationDatabase;Lme/proton/core/crypto/common/keystore/KeyStoreCrypto;)V + public fun deleteHumanVerificationDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun getAllHumanVerificationDetails (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun getHumanVerificationDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun insertHumanVerificationDetails (Lme/proton/core/network/domain/humanverification/HumanVerificationDetails;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/HumanVerificationManagerImpl.kt b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/HumanVerificationManagerImpl.kt index 7cd6a25a1..700eb6755 100644 --- a/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/HumanVerificationManagerImpl.kt +++ b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/HumanVerificationManagerImpl.kt @@ -22,11 +22,11 @@ import kotlinx.coroutines.flow.Flow import me.proton.core.humanverification.domain.HumanVerificationManager import me.proton.core.humanverification.domain.HumanVerificationWorkflowHandler import me.proton.core.humanverification.domain.repository.HumanVerificationRepository -import me.proton.core.network.domain.humanverification.HumanVerificationDetails -import me.proton.core.network.domain.humanverification.HumanVerificationState import me.proton.core.network.domain.client.ClientId +import me.proton.core.network.domain.humanverification.HumanVerificationDetails import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider +import me.proton.core.network.domain.humanverification.HumanVerificationState class HumanVerificationManagerImpl( private val humanVerificationProvider: HumanVerificationProvider, @@ -43,6 +43,10 @@ class HumanVerificationManagerImpl( humanVerificationRepository.insertHumanVerificationDetails(details = details) } + override suspend fun clearDetails(clientId: ClientId) { + humanVerificationRepository.deleteHumanVerificationDetails(clientId) + } + override suspend fun handleHumanVerificationSuccess(clientId: ClientId, tokenType: String, tokenCode: String) { humanVerificationRepository.updateHumanVerificationState( clientId = clientId, diff --git a/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/repository/HumanVerificationRepositoryImpl.kt b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/repository/HumanVerificationRepositoryImpl.kt index 90bd58237..f243fa761 100644 --- a/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/repository/HumanVerificationRepositoryImpl.kt +++ b/human-verification/data/src/main/kotlin/me/proton/core/humanverification/data/repository/HumanVerificationRepositoryImpl.kt @@ -75,6 +75,9 @@ class HumanVerificationRepositoryImpl( getHumanVerificationDetails(clientId)?.let { tryEmitStateChanged(it) } } + override suspend fun deleteHumanVerificationDetails(clientId: ClientId) = + humanVerificationDetailsDao.deleteByClientId(clientId.id) + override suspend fun updateHumanVerificationState( clientId: ClientId, state: HumanVerificationState, diff --git a/human-verification/domain/api/human-verification-domain.api b/human-verification/domain/api/human-verification-domain.api index 03c7fa79f..ffd9972e0 100644 --- a/human-verification/domain/api/human-verification-domain.api +++ b/human-verification/domain/api/human-verification-domain.api @@ -1,5 +1,6 @@ public abstract interface class me/proton/core/humanverification/domain/HumanVerificationManager : me/proton/core/network/domain/humanverification/HumanVerificationListener, me/proton/core/network/domain/humanverification/HumanVerificationProvider { public abstract fun addDetails (Lme/proton/core/network/domain/humanverification/HumanVerificationDetails;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun clearDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun onHumanVerificationStateChanged (Z)Lkotlinx/coroutines/flow/Flow; } @@ -34,6 +35,7 @@ public final class me/proton/core/humanverification/domain/entity/TokenType$Comp } public abstract interface class me/proton/core/humanverification/domain/repository/HumanVerificationRepository { + public abstract fun deleteHumanVerificationDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getAllHumanVerificationDetails (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getHumanVerificationDetails (Lme/proton/core/network/domain/client/ClientId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun insertHumanVerificationDetails (Lme/proton/core/network/domain/humanverification/HumanVerificationDetails;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/HumanVerificationManager.kt b/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/HumanVerificationManager.kt index 63ac7d548..a3dd66830 100644 --- a/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/HumanVerificationManager.kt +++ b/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/HumanVerificationManager.kt @@ -20,6 +20,7 @@ package me.proton.core.humanverification.domain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter +import me.proton.core.network.domain.client.ClientId import me.proton.core.network.domain.humanverification.HumanVerificationDetails import me.proton.core.network.domain.humanverification.HumanVerificationListener import me.proton.core.network.domain.humanverification.HumanVerificationProvider @@ -34,7 +35,15 @@ interface HumanVerificationManager : HumanVerificationProvider, HumanVerificatio */ fun onHumanVerificationStateChanged(initialState: Boolean = false): Flow + /** + * Add (insert or update) a [HumanVerificationDetails]. + */ suspend fun addDetails(details: HumanVerificationDetails) + + /** + * Clear all [HumanVerificationDetails], by [clientId]. + */ + suspend fun clearDetails(clientId: ClientId) } /** diff --git a/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/repository/HumanVerificationRepository.kt b/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/repository/HumanVerificationRepository.kt index 2bd8f4ea2..534b0a2ae 100644 --- a/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/repository/HumanVerificationRepository.kt +++ b/human-verification/domain/src/main/kotlin/me/proton/core/humanverification/domain/repository/HumanVerificationRepository.kt @@ -26,7 +26,7 @@ import me.proton.core.network.domain.humanverification.HumanVerificationState interface HumanVerificationRepository { /** - * Get [HumanVerificationDetails], if exist, by sessionId. + * Get [HumanVerificationDetails], if exist, by [clientId]. */ suspend fun getHumanVerificationDetails(clientId: ClientId): HumanVerificationDetails? @@ -36,10 +36,15 @@ interface HumanVerificationRepository { suspend fun getAllHumanVerificationDetails(): Flow> /** - * Set [HumanVerificationDetails], by clientId. Basically it inserts new record in the DB. + * Insert [HumanVerificationDetails], by [HumanVerificationDetails.clientId]. */ suspend fun insertHumanVerificationDetails(details: HumanVerificationDetails) + /** + * Delete [HumanVerificationDetails], by [clientId]. + */ + suspend fun deleteHumanVerificationDetails(clientId: ClientId) + /** * Sets new state for a human verification flow, along with the token type and token code if needed. */ diff --git a/payment/domain/api/payment-domain.api b/payment/domain/api/payment-domain.api index 2d1910808..44e270caf 100644 --- a/payment/domain/api/payment-domain.api +++ b/payment/domain/api/payment-domain.api @@ -385,7 +385,7 @@ public final class me/proton/core/payment/domain/usecase/GetPaymentTokenStatus { } public final class me/proton/core/payment/domain/usecase/PerformSubscribe { - public fun (Lme/proton/core/payment/domain/repository/PaymentsRepository;)V + public fun (Lme/proton/core/payment/domain/repository/PaymentsRepository;Lme/proton/core/humanverification/domain/HumanVerificationManager;Lme/proton/core/network/domain/client/ClientIdProvider;)V public final fun invoke (Lme/proton/core/domain/entity/UserId;JLme/proton/core/payment/domain/entity/Currency;Lme/proton/core/payment/domain/entity/SubscriptionCycle;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun invoke$default (Lme/proton/core/payment/domain/usecase/PerformSubscribe;Lme/proton/core/domain/entity/UserId;JLme/proton/core/payment/domain/entity/Currency;Lme/proton/core/payment/domain/entity/SubscriptionCycle;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } diff --git a/payment/domain/build.gradle.kts b/payment/domain/build.gradle.kts index 5a2ee1cdd..053edc4a4 100644 --- a/payment/domain/build.gradle.kts +++ b/payment/domain/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { project(Module.networkDomain), // Features + project(Module.humanVerificationDomain), project(Module.accountDomain), project(Module.featureFlagDomain), diff --git a/payment/domain/src/main/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribe.kt b/payment/domain/src/main/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribe.kt index 3da16dee7..9b341e6d3 100644 --- a/payment/domain/src/main/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribe.kt +++ b/payment/domain/src/main/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribe.kt @@ -19,6 +19,8 @@ package me.proton.core.payment.domain.usecase import me.proton.core.domain.entity.UserId +import me.proton.core.humanverification.domain.HumanVerificationManager +import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.payment.domain.MAX_PLAN_QUANTITY import me.proton.core.payment.domain.entity.Currency import me.proton.core.payment.domain.entity.PaymentBody @@ -33,7 +35,9 @@ import javax.inject.Inject * Headers with token and token type "payment". */ class PerformSubscribe @Inject constructor( - private val paymentsRepository: PaymentsRepository + private val paymentsRepository: PaymentsRepository, + private val humanVerificationManager: HumanVerificationManager, + private val clientIdProvider: ClientIdProvider, ) { /** * @param codes optional an array of [String] coupon or gift codes used for discounts. @@ -61,6 +65,13 @@ class PerformSubscribe @Inject constructor( codes = codes, plans = planNames.map { it to MAX_PLAN_QUANTITY }.toMap(), cycle = cycle - ) + ).also { + if (paymentToken != null) { + // Clear any previous payment token (unauthenticated session cookie HV details). + // HV payment token is previously added by BillingCommonViewModel. + val clientId = requireNotNull(clientIdProvider.getClientId(sessionId = null)) + humanVerificationManager.clearDetails(clientId) + } + } } } diff --git a/payment/domain/src/test/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribeTest.kt b/payment/domain/src/test/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribeTest.kt index f2aa2b834..88f081175 100644 --- a/payment/domain/src/test/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribeTest.kt +++ b/payment/domain/src/test/kotlin/me/proton/core/payment/domain/usecase/PerformSubscribeTest.kt @@ -23,8 +23,10 @@ import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.test.runBlockingTest import me.proton.core.domain.entity.UserId +import me.proton.core.humanverification.domain.HumanVerificationManager import me.proton.core.network.domain.ApiException import me.proton.core.network.domain.ApiResult +import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.payment.domain.entity.Currency import me.proton.core.payment.domain.entity.PaymentBody import me.proton.core.payment.domain.entity.Subscription @@ -39,6 +41,8 @@ import kotlin.test.assertNotNull class PerformSubscribeTest { // region mocks private val repository = mockk(relaxed = true) + private val humanVerificationManager = mockk(relaxed = true) + private val clientIdProvider = mockk(relaxed = true) // endregion // region test data @@ -63,7 +67,7 @@ class PerformSubscribeTest { @Before fun beforeEveryTest() { - useCase = PerformSubscribe(repository) + useCase = PerformSubscribe(repository, humanVerificationManager, clientIdProvider) coEvery { repository.createOrUpdateSubscription( testUserId, @@ -205,4 +209,34 @@ class PerformSubscribeTest { assertNotNull(throwable) assertEquals("Test error", throwable.message) } + + @Test + fun `payment token is cleared on successful subscription`() = runBlockingTest { + useCase.invoke( + userId = testUserId, + amount = 48, + currency = Currency.CHF, + cycle = SubscriptionCycle.YEARLY, + planNames = listOf(testPlanName), + codes = null, + paymentToken = "token" + ) + + coVerify(atLeast = 1) { humanVerificationManager.clearDetails(any()) } + } + + @Test + fun `null payment token is not cleared on successful subscription`() = runBlockingTest { + useCase.invoke( + userId = testUserId, + amount = 0, + currency = Currency.CHF, + cycle = SubscriptionCycle.YEARLY, + planNames = listOf(testPlanName), + codes = null, + paymentToken = null + ) + + coVerify(exactly = 0) { humanVerificationManager.clearDetails(any()) } + } } diff --git a/payment/presentation/api/payment-presentation.api b/payment/presentation/api/payment-presentation.api index 7b2830f10..555945e08 100644 --- a/payment/presentation/api/payment-presentation.api +++ b/payment/presentation/api/payment-presentation.api @@ -490,7 +490,7 @@ public final class me/proton/core/payment/presentation/ui/UtilsKt { public final class me/proton/core/payment/presentation/viewmodel/BillingCommonViewModel : me/proton/core/presentation/viewmodel/ProtonViewModel { public static final field Companion Lme/proton/core/payment/presentation/viewmodel/BillingCommonViewModel$Companion; - public fun (Lme/proton/core/payment/domain/usecase/ValidateSubscriptionPlan;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewCreditCard;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewPayPal;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithExistingPaymentMethod;Lme/proton/core/payment/domain/usecase/PerformSubscribe;Lme/proton/core/country/domain/usecase/GetCountry;)V + public fun (Lme/proton/core/payment/domain/usecase/ValidateSubscriptionPlan;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewCreditCard;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewPayPal;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithExistingPaymentMethod;Lme/proton/core/payment/domain/usecase/PerformSubscribe;Lme/proton/core/country/domain/usecase/GetCountry;Lme/proton/core/humanverification/domain/HumanVerificationManager;Lme/proton/core/network/domain/client/ClientIdProvider;)V public final fun getPlansValidationState ()Lkotlinx/coroutines/flow/StateFlow; public final fun getSubscriptionResult ()Lkotlinx/coroutines/flow/StateFlow; public final fun onThreeDSTokenApproved (Lme/proton/core/domain/entity/UserId;Ljava/util/List;Ljava/util/List;JLme/proton/core/payment/domain/entity/Currency;Lme/proton/core/payment/domain/entity/SubscriptionCycle;Ljava/lang/String;)Lkotlinx/coroutines/Job; @@ -649,11 +649,11 @@ public final class me/proton/core/payment/presentation/viewmodel/BillingCommonVi } public final class me/proton/core/payment/presentation/viewmodel/BillingCommonViewModel_Factory : dagger/internal/Factory { - public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V - public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/payment/presentation/viewmodel/BillingCommonViewModel_Factory; + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lme/proton/core/payment/presentation/viewmodel/BillingCommonViewModel_Factory; public synthetic fun get ()Ljava/lang/Object; public fun get ()Lme/proton/core/payment/presentation/viewmodel/BillingCommonViewModel; - public static fun newInstance (Lme/proton/core/payment/domain/usecase/ValidateSubscriptionPlan;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewCreditCard;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewPayPal;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithExistingPaymentMethod;Lme/proton/core/payment/domain/usecase/PerformSubscribe;Lme/proton/core/country/domain/usecase/GetCountry;)Lme/proton/core/payment/presentation/viewmodel/BillingCommonViewModel; + public static fun newInstance (Lme/proton/core/payment/domain/usecase/ValidateSubscriptionPlan;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewCreditCard;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithNewPayPal;Lme/proton/core/payment/domain/usecase/CreatePaymentTokenWithExistingPaymentMethod;Lme/proton/core/payment/domain/usecase/PerformSubscribe;Lme/proton/core/country/domain/usecase/GetCountry;Lme/proton/core/humanverification/domain/HumanVerificationManager;Lme/proton/core/network/domain/client/ClientIdProvider;)Lme/proton/core/payment/presentation/viewmodel/BillingCommonViewModel; } public final class me/proton/core/payment/presentation/viewmodel/BillingViewModel : me/proton/core/presentation/viewmodel/ProtonViewModel { diff --git a/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/PaymentsOrchestrator.kt b/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/PaymentsOrchestrator.kt index b229620da..21302b843 100644 --- a/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/PaymentsOrchestrator.kt +++ b/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/PaymentsOrchestrator.kt @@ -29,6 +29,7 @@ import me.proton.core.payment.presentation.ui.StartBilling import me.proton.core.payment.presentation.ui.StartPaymentOptions import javax.inject.Inject +@Suppress("UseIfInsteadOfWhen") class PaymentsOrchestrator @Inject constructor() { // region result launchers @@ -38,6 +39,9 @@ class PaymentsOrchestrator @Inject constructor() { private var onPaymentResultListener: (result: BillingResult?) -> Unit = {} + private fun checkRegistered(launcher: ActivityResultLauncher?) = + checkNotNull(launcher) { "You must call PaymentsOrchestrator.register(context) before starting workflow!" } + // region public api fun register(caller: ActivityResultCaller) { billingLauncher = registerBillingResult(caller) @@ -60,16 +64,15 @@ class PaymentsOrchestrator @Inject constructor() { selectedPlan: PlanShortDetails, codes: List? = null ) { - userId?.let { - // start the payment options chooser screen - paymentOptionsLauncher?.launch( - PaymentOptionsInput(it.id, selectedPlan, codes) - ) ?: throw IllegalStateException("You must call register(context) before any start workflow function!") - } ?: run { - // directly start the billing screen - billingLauncher?.launch( + when (userId) { + // Directly start the billing screen. + null -> checkRegistered(billingLauncher).launch( BillingInput(null, emptyList(), selectedPlan, codes, null) - ) ?: throw IllegalStateException("You must call register before any start workflow function!") + ) + // Start the payment options chooser screen. + else -> checkRegistered(paymentOptionsLauncher).launch( + PaymentOptionsInput(userId.id, selectedPlan, codes) + ) } } // endregion diff --git a/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/BillingCommonViewModel.kt b/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/BillingCommonViewModel.kt index 1e624d602..36af9680b 100644 --- a/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/BillingCommonViewModel.kt +++ b/payment/presentation/src/main/kotlin/me/proton/core/payment/presentation/viewmodel/BillingCommonViewModel.kt @@ -27,6 +27,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.proton.core.country.domain.usecase.GetCountry import me.proton.core.domain.entity.UserId +import me.proton.core.humanverification.domain.HumanVerificationManager +import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.payment.domain.entity.Card import me.proton.core.payment.domain.entity.Currency import me.proton.core.payment.domain.entity.PaymentToken @@ -40,6 +42,7 @@ import me.proton.core.payment.domain.usecase.CreatePaymentTokenWithNewCreditCard import me.proton.core.payment.domain.usecase.CreatePaymentTokenWithNewPayPal import me.proton.core.payment.domain.usecase.PerformSubscribe import me.proton.core.payment.domain.usecase.ValidateSubscriptionPlan +import me.proton.core.payment.presentation.entity.BillingResult import me.proton.core.payment.presentation.entity.CurrentSubscribedPlanDetails import me.proton.core.plan.domain.entity.MASK_MAIL import me.proton.core.plan.domain.entity.MASK_VPN @@ -61,7 +64,9 @@ class BillingCommonViewModel @Inject constructor( private val createPaymentTokenWithNewPayPal: CreatePaymentTokenWithNewPayPal, private val createPaymentTokenWithExistingPaymentMethod: CreatePaymentTokenWithExistingPaymentMethod, private val performSubscribe: PerformSubscribe, - private val getCountry: GetCountry + private val getCountry: GetCountry, + private val humanVerificationManager: HumanVerificationManager, + private val clientIdProvider: ClientIdProvider, ) : ProtonViewModel() { private val _subscriptionState = MutableStateFlow(State.Idle) @@ -247,7 +252,11 @@ class BillingCommonViewModel @Inject constructor( token: String ): State = if (userId == null) { - // subscription should be created by the sign up module. return payment info (needed for Human Ver headers). + // Token will be used during sign up (create user), as part of HumanVerification headers. + // Subscription will be performed during login, just after create user. + // Token will be cleared by PerformSubscribe. + val clientId = requireNotNull(clientIdProvider.getClientId(sessionId = null)) + humanVerificationManager.addDetails(BillingResult.paymentDetails(clientId = clientId, token = token)) State.Success.SignUpTokenReady(amount, currency, cycle, token) } else { State.Success.SubscriptionCreated( diff --git a/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/BillingViewModelTest.kt b/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/BillingViewModelTest.kt index cfcc41f0e..ed00d7d33 100644 --- a/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/BillingViewModelTest.kt +++ b/payment/presentation/src/test/kotlin/me/proton/core/payment/presentation/viewmodel/BillingViewModelTest.kt @@ -25,8 +25,10 @@ import io.mockk.mockk import me.proton.core.country.domain.entity.Country import me.proton.core.country.domain.usecase.GetCountry import me.proton.core.domain.entity.UserId +import me.proton.core.humanverification.domain.HumanVerificationManager import me.proton.core.network.domain.ApiException import me.proton.core.network.domain.ApiResult +import me.proton.core.network.domain.client.ClientIdProvider import me.proton.core.payment.domain.entity.Card import me.proton.core.payment.domain.entity.Currency import me.proton.core.payment.domain.entity.PaymentToken @@ -57,6 +59,8 @@ class BillingViewModelTest : ArchTest, CoroutinesTest { private val createPaymentTokenWithNewPayPal = mockk() private val performSubscribe = mockk() private val getCountryCode = mockk() + private val humanVerificationManager = mockk(relaxed = true) + private val clientIdProvider = mockk(relaxed = true) // endregion // region test data @@ -88,7 +92,9 @@ class BillingViewModelTest : ArchTest, CoroutinesTest { createPaymentTokenWithNewPayPal, createPaymentTokenWithExistingPaymentMethod, performSubscribe, - getCountryCode + getCountryCode, + humanVerificationManager, + clientIdProvider ) } @@ -522,6 +528,7 @@ class BillingViewModelTest : ArchTest, CoroutinesTest { // THEN coVerify(exactly = 1) { createPaymentToken.invoke(null, 2, testCurrency, paymentType) } + coVerify(exactly = 1) { humanVerificationManager.addDetails(any()) } coVerify(exactly = 0) { performSubscribe.invoke(any(), any(), any(), any(), any(), any(), any()) } assertIs(awaitItem()) assertIs(awaitItem())