249 lines
10 KiB
Kotlin
249 lines
10 KiB
Kotlin
/*
|
|
* Copyright (c) 2023 Proton AG
|
|
* This file is part of Proton AG and ProtonCore.
|
|
*
|
|
* ProtonCore is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* ProtonCore is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package me.proton.core.auth.domain.usecase
|
|
|
|
import me.proton.core.account.domain.entity.AccountType
|
|
import me.proton.core.accountmanager.domain.AccountWorkflowHandler
|
|
import me.proton.core.accountmanager.domain.SessionManager
|
|
import me.proton.core.auth.domain.LogTag
|
|
import me.proton.core.auth.domain.entity.BillingDetails
|
|
import me.proton.core.crypto.common.keystore.EncryptedString
|
|
import me.proton.core.domain.entity.UserId
|
|
import me.proton.core.payment.domain.MAX_PLAN_QUANTITY
|
|
import me.proton.core.payment.domain.entity.PaymentTokenEntity
|
|
import me.proton.core.payment.domain.entity.PurchaseState
|
|
import me.proton.core.payment.domain.entity.SubscriptionCycle
|
|
import me.proton.core.payment.domain.extension.getPurchaseOrNull
|
|
import me.proton.core.payment.domain.repository.PurchaseRepository
|
|
import me.proton.core.plan.domain.entity.SubscriptionManagement
|
|
import me.proton.core.plan.domain.repository.PlansRepository
|
|
import me.proton.core.plan.domain.usecase.PerformSubscribe
|
|
import me.proton.core.user.domain.UserManager
|
|
import me.proton.core.user.domain.entity.User
|
|
import me.proton.core.util.kotlin.CoreLogger
|
|
import javax.inject.Inject
|
|
|
|
/**
|
|
* Performs the account check after logging in to determine what actions are needed.
|
|
*/
|
|
class PostLoginAccountSetup @Inject constructor(
|
|
private val accountWorkflow: AccountWorkflowHandler,
|
|
private val performSubscribe: PerformSubscribe,
|
|
private val purchaseRepository: PurchaseRepository,
|
|
private val planRepository: PlansRepository,
|
|
private val setupAccountCheck: SetupAccountCheck,
|
|
private val setupExternalAddressKeys: SetupExternalAddressKeys,
|
|
private val setupInternalAddress: SetupInternalAddress,
|
|
private val setupPrimaryKeys: SetupPrimaryKeys,
|
|
private val unlockUserPrimaryKey: UnlockUserPrimaryKey,
|
|
private val userCheck: UserCheck,
|
|
private val userManager: UserManager,
|
|
private val sessionManager: SessionManager,
|
|
) {
|
|
sealed class Result {
|
|
sealed class Error : Result() {
|
|
data class UnlockPrimaryKeyError(val error: UserManager.UnlockResult.Error) : Error()
|
|
data class UserCheckError(val error: UserCheckResult.Error) : Error()
|
|
}
|
|
|
|
sealed class Need : Result() {
|
|
data class SecondFactor(val userId: UserId) : Need()
|
|
data class TwoPassMode(val userId: UserId) : Need()
|
|
data class ChangePassword(val userId: UserId) : Need()
|
|
data class ChooseUsername(val userId: UserId) : Need()
|
|
}
|
|
|
|
data class UserUnlocked(val userId: UserId) : Result()
|
|
}
|
|
|
|
sealed class UserCheckResult {
|
|
object Success : UserCheckResult()
|
|
data class Error(val localizedMessage: String, val action: UserCheckAction? = null) : UserCheckResult()
|
|
}
|
|
|
|
interface UserCheck {
|
|
/**
|
|
* Check if [User] match criteria to continue the setup account process.
|
|
*/
|
|
suspend operator fun invoke(user: User): UserCheckResult
|
|
}
|
|
|
|
suspend operator fun invoke(
|
|
userId: UserId,
|
|
encryptedPassword: EncryptedString,
|
|
requiredAccountType: AccountType,
|
|
isSecondFactorNeeded: Boolean,
|
|
isTwoPassModeNeeded: Boolean,
|
|
temporaryPassword: Boolean,
|
|
onSetupSuccess: (suspend () -> Unit)? = null,
|
|
billingDetails: BillingDetails? = null,
|
|
internalAddressDomain: String? = null
|
|
): Result {
|
|
// Flows not using PurchaseStateHandler pass billingDetails.
|
|
subscribeAnyPendingBilling(billingDetails, userId)
|
|
// Flows using PurchaseStateHandler drop a Purchase off.
|
|
subscribeAnyPendingPurchase(userId)
|
|
|
|
// If SecondFactorNeeded, we cannot proceed without.
|
|
if (isSecondFactorNeeded) {
|
|
return Result.Need.SecondFactor(userId)
|
|
}
|
|
|
|
return when (setupAccountCheck(userId, isTwoPassModeNeeded, requiredAccountType, temporaryPassword)) {
|
|
is SetupAccountCheck.Result.TwoPassNeeded -> {
|
|
accountWorkflow.handleTwoPassModeNeeded(userId)
|
|
Result.Need.TwoPassMode(userId)
|
|
}
|
|
is SetupAccountCheck.Result.ChangePasswordNeeded -> {
|
|
accountWorkflow.handleAccountDisabled(userId)
|
|
Result.Need.ChangePassword(userId)
|
|
}
|
|
is SetupAccountCheck.Result.ChooseUsernameNeeded -> {
|
|
accountWorkflow.handleCreateAddressNeeded(userId)
|
|
Result.Need.ChooseUsername(userId)
|
|
}
|
|
is SetupAccountCheck.Result.SetupPrimaryKeysNeeded -> {
|
|
setupPrimaryKeys.invoke(
|
|
userId,
|
|
encryptedPassword,
|
|
requiredAccountType,
|
|
internalAddressDomain
|
|
)
|
|
unlockUserPrimaryKey(
|
|
userId,
|
|
encryptedPassword,
|
|
onSetupSuccess
|
|
)
|
|
}
|
|
is SetupAccountCheck.Result.SetupExternalAddressKeysNeeded -> {
|
|
unlockUserPrimaryKey(
|
|
userId,
|
|
encryptedPassword,
|
|
onSetupSuccess
|
|
) {
|
|
setupExternalAddressKeys.invoke(userId)
|
|
}
|
|
}
|
|
is SetupAccountCheck.Result.SetupInternalAddressNeeded -> {
|
|
unlockUserPrimaryKey(
|
|
userId,
|
|
encryptedPassword,
|
|
onSetupSuccess
|
|
) {
|
|
setupInternalAddress.invoke(userId, internalAddressDomain)
|
|
}
|
|
}
|
|
is SetupAccountCheck.Result.NoSetupNeeded -> {
|
|
unlockUserPrimaryKey(
|
|
userId,
|
|
encryptedPassword,
|
|
onSetupSuccess
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private suspend fun subscribeAnyPendingBilling(
|
|
billingDetails: BillingDetails?,
|
|
userId: UserId
|
|
) {
|
|
if (billingDetails != null) runCatching {
|
|
performSubscribe(
|
|
userId = userId,
|
|
amount = billingDetails.amount,
|
|
currency = billingDetails.currency,
|
|
cycle = billingDetails.cycle,
|
|
planNames = listOf(billingDetails.planName),
|
|
paymentToken = billingDetails.token,
|
|
subscriptionManagement = billingDetails.subscriptionManagement
|
|
)
|
|
}.onFailure {
|
|
CoreLogger.e(LogTag.PERFORM_SUBSCRIBE, it)
|
|
}
|
|
}
|
|
|
|
private suspend fun subscribeAnyPendingPurchase(userId: UserId) {
|
|
val purchase = purchaseRepository.getPurchaseOrNull(PurchaseState.Purchased)
|
|
if (purchase != null) runCatching {
|
|
planRepository.createOrUpdateSubscription(
|
|
sessionUserId = userId,
|
|
amount = purchase.paymentAmount,
|
|
currency = purchase.paymentCurrency,
|
|
payment = PaymentTokenEntity(requireNotNull(purchase.paymentToken)),
|
|
codes = null,
|
|
plans = listOf(purchase.planName).associateWith { MAX_PLAN_QUANTITY },
|
|
cycle = SubscriptionCycle.map[purchase.planCycle] ?: SubscriptionCycle.OTHER,
|
|
subscriptionManagement = SubscriptionManagement.GOOGLE_MANAGED
|
|
)
|
|
}.onSuccess {
|
|
purchaseRepository.upsertPurchase(purchase.copy(purchaseState = PurchaseState.Subscribed))
|
|
}.onFailure {
|
|
CoreLogger.e(LogTag.PERFORM_SUBSCRIBE, it)
|
|
purchaseRepository.upsertPurchase(
|
|
purchase.copy(
|
|
purchaseFailure = it.localizedMessage,
|
|
purchaseState = PurchaseState.Failed
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private suspend fun unlockUserPrimaryKey(
|
|
userId: UserId,
|
|
password: EncryptedString,
|
|
onSetupSuccess: (suspend () -> Unit)?,
|
|
onUnlockSuccess: (suspend () -> Unit)? = null,
|
|
): Result {
|
|
return when (val result = unlockUserPrimaryKey.invoke(userId, password)) {
|
|
is UserManager.UnlockResult.Success -> {
|
|
// Invoke unlock success action.
|
|
onUnlockSuccess?.invoke()
|
|
// Refresh scopes.
|
|
sessionManager.refreshScopes(checkNotNull(sessionManager.getSessionId(userId)))
|
|
// First get the User to invoke UserCheck.
|
|
val user = userManager.getUser(userId, refresh = true)
|
|
when (val userCheckResult = userCheck.invoke(user)) {
|
|
is UserCheckResult.Error -> {
|
|
// Disable account and prevent login.
|
|
accountWorkflow.handleAccountDisabled(userId)
|
|
return Result.Error.UserCheckError(userCheckResult)
|
|
}
|
|
is UserCheckResult.Success -> {
|
|
// Invoke setup success action.
|
|
onSetupSuccess?.invoke()
|
|
// Last step, change account state to Ready.
|
|
accountWorkflow.handleAccountReady(userId)
|
|
Result.UserUnlocked(userId)
|
|
}
|
|
}
|
|
}
|
|
is UserManager.UnlockResult.Error.NoPrimaryKey,
|
|
is UserManager.UnlockResult.Error.NoKeySaltsForPrimaryKey -> {
|
|
// Unrecoverable -> Disable account.
|
|
accountWorkflow.handleUnlockFailed(userId)
|
|
Result.Error.UnlockPrimaryKeyError(result as UserManager.UnlockResult.Error)
|
|
}
|
|
is UserManager.UnlockResult.Error.PrimaryKeyInvalidPassphrase -> {
|
|
// Recoverable -> Let the User retry.
|
|
Result.Error.UnlockPrimaryKeyError(result)
|
|
}
|
|
}
|
|
}
|
|
}
|