Clear consumed Payment Token (HumanVerification token).

This commit is contained in:
Neil Marietta
2022-04-13 11:40:30 +02:00
parent e640aee52b
commit 5c4faa2c26
19 changed files with 130 additions and 71 deletions
+4
View File
@@ -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
+3 -3
View File
@@ -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 <init> (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 <init> (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 {
@@ -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>(ActivitySignupBinding
KEY_PLAN_SELECTED, this
) { _, bundle ->
val plan = bundle.getParcelable<SelectedPlan>(BUNDLE_KEY_PLAN)
val billing = bundle.getParcelable<BillingResult>(BUNDLE_KEY_BILLING_DETAILS)
val billingResult = bundle.getParcelable<BillingResult>(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>(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>(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) {
@@ -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)
@@ -119,7 +119,6 @@ class SignupViewModelTest : ArchTest, CoroutinesTest {
keyStoreCrypto,
plansOrchestrator,
paymentsOrchestrator,
clientIdProvider,
humanVerificationManager,
performLogin,
challengeManager,
@@ -17,6 +17,7 @@ public final class me/proton/core/humanverification/data/HumanVerificationManage
public fun <init> (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 <init> (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;
@@ -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,
@@ -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,
@@ -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;
@@ -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<HumanVerificationDetails>
/**
* Add (insert or update) a [HumanVerificationDetails].
*/
suspend fun addDetails(details: HumanVerificationDetails)
/**
* Clear all [HumanVerificationDetails], by [clientId].
*/
suspend fun clearDetails(clientId: ClientId)
}
/**
@@ -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<List<HumanVerificationDetails>>
/**
* 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.
*/
+1 -1
View File
@@ -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 <init> (Lme/proton/core/payment/domain/repository/PaymentsRepository;)V
public fun <init> (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;
}
+1
View File
@@ -37,6 +37,7 @@ dependencies {
project(Module.networkDomain),
// Features
project(Module.humanVerificationDomain),
project(Module.accountDomain),
project(Module.featureFlagDomain),
@@ -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)
}
}
}
}
@@ -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<PaymentsRepository>(relaxed = true)
private val humanVerificationManager = mockk<HumanVerificationManager>(relaxed = true)
private val clientIdProvider = mockk<ClientIdProvider>(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()) }
}
}
@@ -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 <init> (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 <init> (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 <init> (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 <init> (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 {
@@ -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 <T> checkRegistered(launcher: ActivityResultLauncher<T>?) =
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<String>? = 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
@@ -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>(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(
@@ -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<CreatePaymentTokenWithNewPayPal>()
private val performSubscribe = mockk<PerformSubscribe>()
private val getCountryCode = mockk<GetCountry>()
private val humanVerificationManager = mockk<HumanVerificationManager>(relaxed = true)
private val clientIdProvider = mockk<ClientIdProvider>(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<BillingCommonViewModel.State.Idle>(awaitItem())
assertIs<BillingCommonViewModel.State.Processing>(awaitItem())