feat: Added CreateAccountNeeded handling.

This commit is contained in:
Neil Marietta 2024-02-19 14:18:44 +01:00
parent b2f50c3cc0
commit fd105f362e
28 changed files with 402 additions and 75 deletions

View File

@ -5,7 +5,7 @@ public class hilt_aggregated_deps/_me_proton_core_accountmanager_data_RefreshUse
public final class me/proton/core/accountmanager/data/AccountManagerImpl : me/proton/core/accountmanager/domain/AccountManager, me/proton/core/accountmanager/domain/AccountWorkflowHandler {
public fun <init> (Lme/proton/core/domain/entity/Product;Lme/proton/core/account/domain/repository/AccountRepository;Lme/proton/core/auth/domain/repository/AuthRepository;Lme/proton/core/user/domain/UserManager;Lme/proton/core/network/domain/session/SessionListener;)V
public fun addAccount (Lme/proton/core/account/domain/entity/Account;Lme/proton/core/network/domain/session/Session;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun disableAccount (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun disableAccount (Lme/proton/core/domain/entity/UserId;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getAccount (Lme/proton/core/domain/entity/UserId;)Lkotlinx/coroutines/flow/Flow;
public fun getAccounts ()Lkotlinx/coroutines/flow/Flow;
public fun getPreviousPrimaryUserId (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

View File

@ -28,7 +28,7 @@ protonBuild {
}
protonCoverage {
branchCoveragePercentage.set(45)
branchCoveragePercentage.set(43)
lineCoveragePercentage.set(79)
}

View File

@ -74,14 +74,14 @@ class AccountManagerImpl @Inject constructor(
}
}
private suspend fun disableAccount(account: Account) {
private suspend fun disableAccount(account: Account, keepSession: Boolean) {
accountRepository.updateAccountState(account.userId, Disabled)
account.sessionId?.let { removeSession(it) }
account.sessionId?.takeUnless { keepSession }?.let { removeSession(it) }
userManager.lock(account.userId)
}
private suspend fun disableAccount(sessionId: SessionId) {
accountRepository.getAccountOrNull(sessionId)?.let { disableAccount(it) }
private suspend fun disableAccount(sessionId: SessionId, keepSession: Boolean) {
accountRepository.getAccountOrNull(sessionId)?.let { disableAccount(it, keepSession) }
}
private suspend fun clearSessionDetails(userId: UserId) {
@ -100,8 +100,8 @@ class AccountManagerImpl @Inject constructor(
}
}
override suspend fun disableAccount(userId: UserId) {
accountRepository.getAccountOrNull(userId)?.let { disableAccount(it) }
override suspend fun disableAccount(userId: UserId, keepSession: Boolean) {
accountRepository.getAccountOrNull(userId)?.let { disableAccount(it, keepSession) }
}
override fun getAccount(userId: UserId): Flow<Account?> =
@ -160,7 +160,7 @@ class AccountManagerImpl @Inject constructor(
override suspend fun handleSecondFactorFailed(sessionId: SessionId) {
accountRepository.updateSessionState(sessionId, SessionState.SecondFactorFailed)
disableAccount(sessionId)
disableAccount(sessionId, keepSession = false)
}
override suspend fun handleCreateAddressNeeded(userId: UserId) {
@ -181,6 +181,7 @@ class AccountManagerImpl @Inject constructor(
override suspend fun handleCreateAccountSuccess(userId: UserId) {
accountRepository.updateAccountState(userId, CreateAccountSuccess)
disableAccount(userId, keepSession = true)
}
override suspend fun handleCreateAccountFailed(userId: UserId) {

View File

@ -1,7 +1,8 @@
public abstract class me/proton/core/accountmanager/domain/AccountManager {
public fun <init> (Lme/proton/core/domain/entity/Product;)V
public abstract fun addAccount (Lme/proton/core/account/domain/entity/Account;Lme/proton/core/network/domain/session/Session;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun disableAccount (Lme/proton/core/domain/entity/UserId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun disableAccount (Lme/proton/core/domain/entity/UserId;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun disableAccount$default (Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/domain/entity/UserId;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public abstract fun getAccount (Lme/proton/core/domain/entity/UserId;)Lkotlinx/coroutines/flow/Flow;
public abstract fun getAccounts ()Lkotlinx/coroutines/flow/Flow;
public abstract fun getPreviousPrimaryUserId (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

View File

@ -58,7 +58,7 @@ abstract class AccountManager(
*
* Note: The [Account.state] will be set to [AccountState.Disabled].
*/
abstract suspend fun disableAccount(userId: UserId)
abstract suspend fun disableAccount(userId: UserId, keepSession: Boolean = false)
/**
* Flow of persisted [Account] on this device, by userId.

View File

@ -14,6 +14,10 @@ public final class me/proton/core/accountmanager/presentation/AccountManagerObse
public final class me/proton/core/accountmanager/presentation/AccountManagerObserverKt {
public static final fun observe (Lme/proton/core/accountmanager/domain/AccountManager;Landroidx/lifecycle/Lifecycle;Landroidx/lifecycle/Lifecycle$State;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static synthetic fun observe$default (Lme/proton/core/accountmanager/domain/AccountManager;Landroidx/lifecycle/Lifecycle;Landroidx/lifecycle/Lifecycle$State;ILjava/lang/Object;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static final fun onAccountCreateAccountFailed (Lme/proton/core/accountmanager/presentation/AccountManagerObserver;ZLkotlin/jvm/functions/Function2;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static synthetic fun onAccountCreateAccountFailed$default (Lme/proton/core/accountmanager/presentation/AccountManagerObserver;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static final fun onAccountCreateAccountNeeded (Lme/proton/core/accountmanager/presentation/AccountManagerObserver;ZLkotlin/jvm/functions/Function2;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static synthetic fun onAccountCreateAccountNeeded$default (Lme/proton/core/accountmanager/presentation/AccountManagerObserver;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static final fun onAccountCreateAddressFailed (Lme/proton/core/accountmanager/presentation/AccountManagerObserver;ZLkotlin/jvm/functions/Function2;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static synthetic fun onAccountCreateAddressFailed$default (Lme/proton/core/accountmanager/presentation/AccountManagerObserver;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;
public static final fun onAccountCreateAddressNeeded (Lme/proton/core/accountmanager/presentation/AccountManagerObserver;ZLkotlin/jvm/functions/Function2;)Lme/proton/core/accountmanager/presentation/AccountManagerObserver;

View File

@ -98,6 +98,22 @@ fun AccountManagerObserver.onAccountCreateAddressFailed(
return this
}
fun AccountManagerObserver.onAccountCreateAccountNeeded(
initialState: Boolean = true,
block: suspend (Account) -> Unit
): AccountManagerObserver {
addAccountStateListener(AccountState.CreateAccountNeeded, initialState, block)
return this
}
fun AccountManagerObserver.onAccountCreateAccountFailed(
initialState: Boolean = true,
block: suspend (Account) -> Unit
): AccountManagerObserver {
addAccountStateListener(AccountState.CreateAccountFailed, initialState, block)
return this
}
fun AccountManagerObserver.onAccountReady(
initialState: Boolean = true,
block: suspend (Account) -> Unit

View File

@ -27,7 +27,7 @@ protonBuild {
}
protonCoverage {
branchCoveragePercentage.set(64)
branchCoveragePercentage.set(63)
lineCoveragePercentage.set(92)
}

View File

@ -1,6 +1,7 @@
public final class me/proton/core/auth/domain/LogTag {
public static final field INSTANCE Lme/proton/core/auth/domain/LogTag;
public static final field INVALID_SRP_PROOF Ljava/lang/String;
public static final field PERFORM_SUBSCRIBE Ljava/lang/String;
}
public abstract class me/proton/core/auth/domain/entity/AuthInfo {
@ -317,7 +318,7 @@ public final class me/proton/core/auth/domain/usecase/PerformSecondFactor {
}
public final class me/proton/core/auth/domain/usecase/PostLoginAccountSetup {
public fun <init> (Lme/proton/core/accountmanager/domain/AccountWorkflowHandler;Lme/proton/core/plan/domain/usecase/PerformSubscribe;Lme/proton/core/auth/domain/usecase/SetupAccountCheck;Lme/proton/core/auth/domain/usecase/SetupExternalAddressKeys;Lme/proton/core/auth/domain/usecase/SetupInternalAddress;Lme/proton/core/auth/domain/usecase/SetupPrimaryKeys;Lme/proton/core/auth/domain/usecase/UnlockUserPrimaryKey;Lme/proton/core/auth/domain/usecase/PostLoginAccountSetup$UserCheck;Lme/proton/core/user/domain/UserManager;Lme/proton/core/accountmanager/domain/SessionManager;)V
public fun <init> (Lme/proton/core/accountmanager/domain/AccountWorkflowHandler;Lme/proton/core/plan/domain/usecase/PerformSubscribe;Lme/proton/core/payment/domain/repository/PurchaseRepository;Lme/proton/core/plan/domain/repository/PlansRepository;Lme/proton/core/auth/domain/usecase/SetupAccountCheck;Lme/proton/core/auth/domain/usecase/SetupExternalAddressKeys;Lme/proton/core/auth/domain/usecase/SetupInternalAddress;Lme/proton/core/auth/domain/usecase/SetupPrimaryKeys;Lme/proton/core/auth/domain/usecase/UnlockUserPrimaryKey;Lme/proton/core/auth/domain/usecase/PostLoginAccountSetup$UserCheck;Lme/proton/core/user/domain/UserManager;Lme/proton/core/accountmanager/domain/SessionManager;)V
public final fun invoke (Lme/proton/core/domain/entity/UserId;Ljava/lang/String;Lme/proton/core/account/domain/entity/AccountType;ZZZLkotlin/jvm/functions/Function1;Lme/proton/core/auth/domain/entity/BillingDetails;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun invoke$default (Lme/proton/core/auth/domain/usecase/PostLoginAccountSetup;Lme/proton/core/domain/entity/UserId;Ljava/lang/String;Lme/proton/core/account/domain/entity/AccountType;ZZZLkotlin/jvm/functions/Function1;Lme/proton/core/auth/domain/entity/BillingDetails;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}
@ -553,6 +554,11 @@ public final class me/proton/core/auth/domain/usecase/signup/PerformCreateUser {
public final fun invoke (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lme/proton/core/user/domain/entity/CreateUserType;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class me/proton/core/auth/domain/usecase/signup/SetCreateAccountSuccess {
public fun <init> (Lme/proton/core/accountmanager/domain/AccountManager;Lme/proton/core/accountmanager/domain/AccountWorkflowHandler;)V
public final fun invoke (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class me/proton/core/auth/domain/usecase/signup/SignupChallengeConfig : me/proton/core/challenge/domain/ChallengeConfig {
public static final field Companion Lme/proton/core/auth/domain/usecase/signup/SignupChallengeConfig$Companion;
public static final field SIGN_UP_FRAME_RECOVERY Ljava/lang/String;

View File

@ -30,7 +30,7 @@ protonBuild {
protonCoverage {
branchCoveragePercentage.set(62)
lineCoveragePercentage.set(86)
lineCoveragePercentage.set(85)
}
publishOption.shouldBePublishedAsLib = true

View File

@ -20,4 +20,5 @@ package me.proton.core.auth.domain
object LogTag {
const val INVALID_SRP_PROOF = "core.auth.domain.srp.invalid.server.proof"
const val PERFORM_SUBSCRIBE = "core.auth.domain.perform.subscribe"
}

View File

@ -19,14 +19,24 @@
package me.proton.core.auth.domain.usecase
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.accountmanager.domain.SessionManager
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
/**
@ -35,6 +45,8 @@ import javax.inject.Inject
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,
@ -83,21 +95,10 @@ class PostLoginAccountSetup @Inject constructor(
billingDetails: BillingDetails? = null,
internalAddressDomain: String? = null
): Result {
// Subscribe to any pending subscription/billing.
// TODO: Add If any Purchase in Purchased state for this userId -> use it.
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
)
}
}
// 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) {
@ -158,6 +159,51 @@ class PostLoginAccountSetup @Inject constructor(
}
}
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,

View File

@ -0,0 +1,21 @@
package me.proton.core.auth.domain.usecase.signup
import kotlinx.coroutines.flow.first
import me.proton.core.account.domain.entity.AccountState
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.accountmanager.domain.AccountWorkflowHandler
import me.proton.core.accountmanager.domain.getAccounts
import me.proton.core.util.kotlin.annotation.ExcludeFromCoverage
import javax.inject.Inject
@ExcludeFromCoverage
class SetCreateAccountSuccess @Inject constructor(
private val accountManager: AccountManager,
private val accountWorkflowHandler: AccountWorkflowHandler
) {
suspend operator fun invoke() {
accountManager.getAccounts(AccountState.CreateAccountNeeded).first().forEach {
accountWorkflowHandler.handleCreateAccountSuccess(it.userId)
}
}
}

View File

@ -26,16 +26,23 @@ import io.mockk.mockk
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.accountmanager.domain.SessionManager
import me.proton.core.accountmanager.domain.AccountWorkflowHandler
import me.proton.core.accountmanager.domain.SessionManager
import me.proton.core.auth.domain.entity.BillingDetails
import me.proton.core.auth.domain.entity.SessionInfo
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.session.SessionId
import me.proton.core.payment.domain.entity.Currency
import me.proton.core.payment.domain.entity.PaymentTokenEntity
import me.proton.core.payment.domain.entity.ProtonPaymentToken
import me.proton.core.payment.domain.entity.Purchase
import me.proton.core.payment.domain.entity.PurchaseState
import me.proton.core.payment.domain.entity.SubscriptionCycle
import me.proton.core.payment.domain.repository.PlanQuantity
import me.proton.core.payment.domain.repository.PurchaseRepository
import me.proton.core.payment.domain.usecase.PaymentProvider
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
@ -47,6 +54,8 @@ import kotlin.test.assertTrue
class PostLoginAccountSetupTest {
private lateinit var accountWorkflowHandler: AccountWorkflowHandler
private lateinit var performSubscribe: PerformSubscribe
private lateinit var purchaseRepository: PurchaseRepository
private lateinit var planRepository: PlansRepository
private lateinit var setupAccountCheck: SetupAccountCheck
private lateinit var setupInternalAddress: SetupInternalAddress
private lateinit var setupExternalAddressKeys: SetupExternalAddressKeys
@ -69,6 +78,7 @@ class PostLoginAccountSetupTest {
fun setUp() {
accountWorkflowHandler = mockk()
performSubscribe = mockk()
planRepository = mockk(relaxed = true)
setupAccountCheck = mockk()
setupExternalAddressKeys = mockk()
setupInternalAddress = mockk()
@ -89,9 +99,15 @@ class PostLoginAccountSetupTest {
coEvery { refreshScopes(any()) } returns Unit
}
purchaseRepository = mockk(relaxed = true) {
coEvery { getPurchases() } returns emptyList()
}
tested = PostLoginAccountSetup(
accountWorkflowHandler,
performSubscribe,
purchaseRepository,
planRepository,
setupAccountCheck,
setupExternalAddressKeys,
setupInternalAddress,
@ -245,6 +261,74 @@ class PostLoginAccountSetupTest {
assertEquals(ProtonPaymentToken("test-token"), paymentToken.captured)
}
@Test
fun `subscription setup with purchase`() = runTest {
val sessionInfo = mockSessionInfo()
val pendingUserId = UserId("pendingUserId")
val pendingSessionId = SessionId("pendingSessionId")
val purchase = Purchase(
sessionId = pendingSessionId,
planName = "test-plan-name",
planCycle = 12,
purchaseState = PurchaseState.Purchased,
purchaseFailure = null,
paymentProvider = PaymentProvider.GoogleInAppPurchase,
paymentOrderId = "orderId",
paymentToken = ProtonPaymentToken("test-token"),
paymentCurrency = Currency.EUR,
paymentAmount = 99
)
coEvery { purchaseRepository.getPurchases() } returns listOf(purchase)
coEvery { sessionManager.getUserId(pendingSessionId) } returns pendingUserId
coJustRun { accountWorkflowHandler.handleAccountReady(any()) }
coJustRun { accountWorkflowHandler.handleCreateAccountSuccess(any()) }
coEvery { setupAccountCheck.invoke(any(), any(), any(), any()) } returns SetupAccountCheck.Result.NoSetupNeeded
coEvery { unlockUserPrimaryKey.invoke(any(), any()) } returns UserManager.UnlockResult.Success
val result = tested.invoke(
sessionInfo.userId,
testEncryptedPassword,
testAccountType,
isSecondFactorNeeded = sessionInfo.isSecondFactorNeeded,
isTwoPassModeNeeded = sessionInfo.isTwoPassModeNeeded,
temporaryPassword = sessionInfo.temporaryPassword,
onSetupSuccess = onSetupSuccess
)
assertEquals(PostLoginAccountSetup.Result.UserUnlocked(testUserId), result)
coVerify(exactly = 1) { onSetupSuccess() }
val userId = slot<UserId>()
val amount = slot<Long>()
val currency = slot<Currency>()
val cycle = slot<SubscriptionCycle>()
val plans = slot<PlanQuantity>()
val paymentToken = slot<PaymentTokenEntity>()
coVerify {
planRepository.createOrUpdateSubscription(
capture(userId),
capture(amount),
capture(currency),
capture(paymentToken),
codes = null,
capture(plans),
capture(cycle),
subscriptionManagement = SubscriptionManagement.GOOGLE_MANAGED
)
}
assertEquals(testUserId, userId.captured)
assertEquals(99, amount.captured)
assertEquals(Currency.EUR, currency.captured)
assertEquals(SubscriptionCycle.YEARLY, cycle.captured)
assertEquals(mapOf("test-plan-name" to 1), plans.captured)
assertEquals(PaymentTokenEntity(ProtonPaymentToken("test-token")), paymentToken.captured)
val actualPurchase = slot<Purchase>()
coVerify { purchaseRepository.upsertPurchase(capture(actualPurchase)) }
assertEquals(PurchaseState.Subscribed, actualPurchase.captured.purchaseState)
}
@Test
fun `user check error`() = runTest {
val setupError = mockk<PostLoginAccountSetup.UserCheckResult.Error>()

View File

@ -226,8 +226,8 @@ public final class me/proton/core/auth/presentation/AuthOrchestrator {
public final fun startLoginWorkflow (Lme/proton/core/account/domain/entity/AccountType;Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun startLoginWorkflow$default (Lme/proton/core/auth/presentation/AuthOrchestrator;Lme/proton/core/account/domain/entity/AccountType;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun startSecondFactorWorkflow (Lme/proton/core/account/domain/entity/Account;)V
public final fun startSignupWorkflow (Lme/proton/core/account/domain/entity/AccountType;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)V
public static synthetic fun startSignupWorkflow$default (Lme/proton/core/auth/presentation/AuthOrchestrator;Lme/proton/core/account/domain/entity/AccountType;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;ILjava/lang/Object;)V
public final fun startSignupWorkflow (Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)V
public static synthetic fun startSignupWorkflow$default (Lme/proton/core/auth/presentation/AuthOrchestrator;Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;ILjava/lang/Object;)V
public final fun startTwoPassModeWorkflow (Lme/proton/core/account/domain/entity/Account;)V
public final fun unregister ()V
}
@ -1282,15 +1282,19 @@ public final class me/proton/core/auth/presentation/entity/signup/RecoveryMethod
public final class me/proton/core/auth/presentation/entity/signup/SignUpInput : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Lme/proton/core/account/domain/entity/AccountType;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)V
public synthetic fun <init> (Lme/proton/core/account/domain/entity/AccountType;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)V
public synthetic fun <init> (Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lme/proton/core/account/domain/entity/AccountType;
public final fun component2 ()Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;
public final fun copy (Lme/proton/core/account/domain/entity/AccountType;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)Lme/proton/core/auth/presentation/entity/signup/SignUpInput;
public static synthetic fun copy$default (Lme/proton/core/auth/presentation/entity/signup/SignUpInput;Lme/proton/core/account/domain/entity/AccountType;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;ILjava/lang/Object;)Lme/proton/core/auth/presentation/entity/signup/SignUpInput;
public final fun component2 ()Z
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;
public final fun copy (Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)Lme/proton/core/auth/presentation/entity/signup/SignUpInput;
public static synthetic fun copy$default (Lme/proton/core/auth/presentation/entity/signup/SignUpInput;Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;ILjava/lang/Object;)Lme/proton/core/auth/presentation/entity/signup/SignUpInput;
public fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getCancellable ()Z
public final fun getCreatableAccountType ()Lme/proton/core/account/domain/entity/AccountType;
public final fun getEmail ()Ljava/lang/String;
public final fun getSubscriptionDetails ()Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
@ -1767,6 +1771,8 @@ public final class me/proton/core/auth/presentation/ui/UtilsKt {
public final class me/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment : me/proton/core/auth/presentation/ui/signup/SignupFragment {
public static final field ARG_INPUT_ACCOUNT_TYPE Ljava/lang/String;
public static final field ARG_INPUT_CANCELLABLE Ljava/lang/String;
public static final field ARG_INPUT_EMAIL Ljava/lang/String;
public static final field ARG_INPUT_SUBSCRIPTION_DETAILS Ljava/lang/String;
public static final field Companion Lme/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment$Companion;
public fun <init> ()V
@ -1776,7 +1782,8 @@ public final class me/proton/core/auth/presentation/ui/signup/ChooseExternalEmai
}
public final class me/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment$Companion {
public final fun invoke (Lme/proton/core/account/domain/entity/AccountType;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)Lme/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment;
public final fun invoke (Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;)Lme/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment;
public static synthetic fun invoke$default (Lme/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment$Companion;Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Lme/proton/core/auth/presentation/entity/signup/SubscriptionDetails;ILjava/lang/Object;)Lme/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment;
}
public abstract interface class me/proton/core/auth/presentation/ui/signup/ChooseExternalEmailFragment_GeneratedInjector {
@ -1785,6 +1792,7 @@ public abstract interface class me/proton/core/auth/presentation/ui/signup/Choos
public final class me/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment : me/proton/core/auth/presentation/ui/signup/SignupFragment {
public static final field ARG_INPUT_ACCOUNT_TYPE Ljava/lang/String;
public static final field ARG_INPUT_CANCELLABLE Ljava/lang/String;
public static final field ARG_INPUT_DOMAIN Ljava/lang/String;
public static final field ARG_INPUT_USERNAME Ljava/lang/String;
public static final field Companion Lme/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment$Companion;
@ -1795,7 +1803,8 @@ public final class me/proton/core/auth/presentation/ui/signup/ChooseInternalEmai
}
public final class me/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment$Companion {
public final fun invoke (Lme/proton/core/account/domain/entity/AccountType;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment;
public final fun invoke (Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Ljava/lang/String;)Lme/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment;
public static synthetic fun invoke$default (Lme/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment$Companion;Lme/proton/core/account/domain/entity/AccountType;ZLjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment;
}
public abstract interface class me/proton/core/auth/presentation/ui/signup/ChooseInternalEmailFragment_GeneratedInjector {
@ -1813,12 +1822,19 @@ public abstract interface class me/proton/core/auth/presentation/ui/signup/Choos
}
public final class me/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment : me/proton/core/auth/presentation/ui/signup/SignupFragment {
public static final field ARG_INPUT_CANCELLABLE Ljava/lang/String;
public static final field Companion Lme/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment$Companion;
public fun <init> ()V
public fun onBackPressed ()V
public fun onViewCreated (Landroid/view/View;Landroid/os/Bundle;)V
public fun showLoading (Z)V
}
public final class me/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment$Companion {
public final fun invoke (Z)Lme/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment;
public static synthetic fun invoke$default (Lme/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment$Companion;ZILjava/lang/Object;)Lme/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment;
}
public abstract interface class me/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment_GeneratedInjector {
public abstract fun injectChooseUsernameFragment (Lme/proton/core/auth/presentation/ui/signup/ChooseUsernameFragment;)V
}
@ -2044,6 +2060,7 @@ public abstract class me/proton/core/auth/presentation/ui/signup/SignupFragment
public abstract fun onBackPressed ()V
public fun onCreate (Landroid/os/Bundle;)V
public fun onUiComponentCreated (Landroidx/lifecycle/LifecycleOwner;Landroidx/activity/OnBackPressedDispatcherOwner;Landroidx/savedstate/SavedStateRegistryOwner;Lme/proton/core/presentation/utils/UiComponent;)V
public fun setNavigationIcon (Landroidx/appcompat/widget/Toolbar;IZ)V
public fun showError (Ljava/lang/String;)V
public fun showIndefiniteError (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
public static synthetic fun showIndefiniteError$default (Lme/proton/core/auth/presentation/ui/signup/SignupFragment;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
@ -2852,11 +2869,11 @@ public final class me/proton/core/auth/presentation/viewmodel/signup/SignupViewM
}
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;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;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;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;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/humanverification/domain/HumanVerificationExternalInput;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/auth/domain/usecase/PerformLogin;Lme/proton/core/challenge/domain/ChallengeManager;Lme/proton/core/auth/domain/usecase/signup/SignupChallengeConfig;Lme/proton/core/observability/domain/ObservabilityManager;Lme/proton/core/plan/domain/usecase/CanUpgradeToPaid;Lme/proton/core/plan/domain/IsDynamicPlanEnabled;Lme/proton/core/telemetry/domain/TelemetryManager;Landroidx/lifecycle/SavedStateHandle;)Lme/proton/core/auth/presentation/viewmodel/signup/SignupViewModel;
public static fun newInstance (Lme/proton/core/humanverification/domain/HumanVerificationExternalInput;Lme/proton/core/auth/domain/usecase/signup/PerformCreateUser;Lme/proton/core/auth/domain/usecase/signup/PerformCreateExternalEmailUser;Lme/proton/core/auth/domain/usecase/signup/SetCreateAccountSuccess;Lme/proton/core/crypto/common/keystore/KeyStoreCrypto;Lme/proton/core/plan/presentation/PlansOrchestrator;Lme/proton/core/payment/presentation/PaymentsOrchestrator;Lme/proton/core/auth/domain/usecase/PerformLogin;Lme/proton/core/challenge/domain/ChallengeManager;Lme/proton/core/auth/domain/usecase/signup/SignupChallengeConfig;Lme/proton/core/observability/domain/ObservabilityManager;Lme/proton/core/plan/domain/usecase/CanUpgradeToPaid;Lme/proton/core/plan/domain/IsDynamicPlanEnabled;Lme/proton/core/telemetry/domain/TelemetryManager;Landroidx/lifecycle/SavedStateHandle;)Lme/proton/core/auth/presentation/viewmodel/signup/SignupViewModel;
}
public final class me/proton/core/auth/presentation/viewmodel/signup/SignupViewModel_HiltModules {

View File

@ -383,9 +383,12 @@ class AuthOrchestrator @Inject constructor() {
*/
fun startSignupWorkflow(
creatableAccountType: AccountType = AccountType.Internal,
subscriptionDetails: SubscriptionDetails? = null) {
cancellable: Boolean = true,
email: String? = null,
subscriptionDetails: SubscriptionDetails? = null
) {
checkRegistered(signUpWorkflowLauncher).launch(
SignUpInput(creatableAccountType, subscriptionDetails)
SignUpInput(creatableAccountType, cancellable, email, subscriptionDetails)
)
}

View File

@ -25,5 +25,7 @@ import me.proton.core.account.domain.entity.AccountType
@Parcelize
data class SignUpInput(
val creatableAccountType: AccountType,
val cancellable: Boolean = true,
val email: String? = null,
val subscriptionDetails: SubscriptionDetails? = null
) : Parcelable

View File

@ -79,21 +79,35 @@ class ChooseExternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
AccountType.valueOf(requireNotNull(requireArguments().getString(ARG_INPUT_ACCOUNT_TYPE)))
}
private val cancellable: Boolean by lazy {
requireArguments().getBoolean(ARG_INPUT_CANCELLABLE)
}
private val email: String? by lazy {
requireArguments().getString(ARG_INPUT_EMAIL)
}
private val subscriptionDetails: SubscriptionDetails? by lazy {
requireArguments().getParcelable(ARG_INPUT_SUBSCRIPTION_DETAILS)
}
override fun onBackPressed() {
signupViewModel.onFinish()
activity?.finish()
if (cancellable) {
signupViewModel.onFinish()
activity?.finish()
} else {
showError(getString(R.string.auth_signup_error_create_to_continue))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
toolbar.setNavigationIcon(R.drawable.ic_proton_close, cancellable)
toolbar.setNavigationOnClickListener { onBackPressed() }
emailInput.text = email
emailInput.apply {
setOnFocusLostListener { _, _ ->
validateEmail()
@ -139,7 +153,7 @@ class ChooseExternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
}
private fun onSwitchInternal(username: String? = null, domain: String? = null) {
parentFragmentManager.replaceByInternalEmailChooser(creatableAccountType, username, domain)
parentFragmentManager.replaceByInternalEmailChooser(creatableAccountType, cancellable, username, domain)
}
private fun onExternalEmailAvailable(email: String) {
@ -166,14 +180,20 @@ class ChooseExternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
companion object {
const val ARG_INPUT_ACCOUNT_TYPE = "arg.accountType"
const val ARG_INPUT_CANCELLABLE = "arg.cancellable"
const val ARG_INPUT_SUBSCRIPTION_DETAILS = "arg.subscriptionDetails"
const val ARG_INPUT_EMAIL = "arg.email"
operator fun invoke(
creatableAccountType: AccountType,
subscriptionDetails: SubscriptionDetails?
cancellable: Boolean = true,
email: String? = null,
subscriptionDetails: SubscriptionDetails? = null
) = ChooseExternalEmailFragment().apply {
arguments = bundleOf(
ARG_INPUT_ACCOUNT_TYPE to creatableAccountType.name,
ARG_INPUT_CANCELLABLE to cancellable,
ARG_INPUT_EMAIL to email,
ARG_INPUT_SUBSCRIPTION_DETAILS to subscriptionDetails
)
}

View File

@ -83,16 +83,22 @@ class ChooseInternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
private val username by lazy { requireArguments().getString(ARG_INPUT_USERNAME) }
private val domain by lazy { requireArguments().getString(ARG_INPUT_DOMAIN) }
private val cancellable: Boolean by lazy { requireArguments().getBoolean(ARG_INPUT_CANCELLABLE) }
override fun onBackPressed() {
signupViewModel.onFinish()
activity?.finish()
if (cancellable) {
signupViewModel.onFinish()
activity?.finish()
} else {
showError(getString(R.string.auth_signup_error_create_to_continue))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
toolbar.setNavigationIcon(R.drawable.ic_proton_close, cancellable)
toolbar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() }
usernameInput.apply {
@ -113,6 +119,7 @@ class ChooseInternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
separatorView.visibility = View.GONE
footnoteText.visibility = View.GONE
}
AccountType.External -> {
switchButton.visibility = View.VISIBLE
separatorView.visibility = View.VISIBLE
@ -131,7 +138,12 @@ class ChooseInternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
is State.Processing -> showLoading(true)
is State.Ready -> onReady(it.username, it.domain, it.domains)
is State.Success -> onUsernameAvailable(it.username, it.domain)
is State.Error.DomainsNotAvailable -> onDomainsNotAvailable(it.error.getUserMessage(resources))
is State.Error.DomainsNotAvailable -> onDomainsNotAvailable(
it.error.getUserMessage(
resources
)
)
is State.Error.Message -> onError(it.error.getUserMessage(resources))
}.exhaustive
}
@ -159,7 +171,7 @@ class ChooseInternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
}
private fun onSwitchClicked() {
parentFragmentManager.replaceByExternalEmailChooser(creatableAccountType)
parentFragmentManager.replaceByExternalEmailChooser(creatableAccountType, cancellable)
}
private fun onReady(username: String?, domain: String?, domains: List<Domain>) {
@ -219,18 +231,21 @@ class ChooseInternalEmailFragment : SignupFragment(R.layout.fragment_signup_choo
companion object {
const val ARG_INPUT_ACCOUNT_TYPE = "arg.accountType"
const val ARG_INPUT_CANCELLABLE = "arg.cancellable"
const val ARG_INPUT_USERNAME = "arg.username"
const val ARG_INPUT_DOMAIN = "arg.domain"
operator fun invoke(
creatableAccountType: AccountType,
username: String?,
domain: String?
cancellable: Boolean = true,
username: String? = null,
domain: String? = null,
) = ChooseInternalEmailFragment().apply {
arguments = bundleOf(
ARG_INPUT_ACCOUNT_TYPE to creatableAccountType.name,
ARG_INPUT_CANCELLABLE to cancellable,
ARG_INPUT_USERNAME to username,
ARG_INPUT_DOMAIN to domain
ARG_INPUT_DOMAIN to domain,
)
}
}

View File

@ -20,6 +20,7 @@ package me.proton.core.auth.presentation.ui.signup
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
@ -55,15 +56,22 @@ class ChooseUsernameFragment : SignupFragment(R.layout.fragment_signup_choose_us
private val signupViewModel by activityViewModels<SignupViewModel>()
private val binding by viewBinding(FragmentSignupChooseUsernameBinding::bind)
private val cancellable: Boolean by lazy { requireArguments().getBoolean(ARG_INPUT_CANCELLABLE) }
override fun onBackPressed() {
signupViewModel.onFinish()
activity?.finish()
if (cancellable) {
signupViewModel.onFinish()
activity?.finish()
} else {
showError(getString(R.string.auth_signup_error_create_to_continue))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
toolbar.setNavigationIcon(R.drawable.ic_proton_close, cancellable)
toolbar.setNavigationOnClickListener { onBackPressed() }
usernameInput.apply {
@ -128,4 +136,16 @@ class ChooseUsernameFragment : SignupFragment(R.layout.fragment_signup_choose_us
nextButton.setIdle()
}
}
companion object {
const val ARG_INPUT_CANCELLABLE = "arg.cancellable"
operator fun invoke(
cancellable: Boolean = true,
) = ChooseUsernameFragment().apply {
arguments = bundleOf(
ARG_INPUT_CANCELLABLE to cancellable
)
}
}
}

View File

@ -83,9 +83,10 @@ internal fun FragmentManager.showTermsConditions() {
}
internal fun FragmentManager.showUsernameChooser(
cancellable: Boolean = true,
containerId: Int = android.R.id.content
) = findFragmentByTag(TAG_USERNAME_CHOOSER) ?: run {
val fragment = ChooseUsernameFragment()
val fragment = ChooseUsernameFragment(cancellable)
inTransaction {
setCustomAnimations(0, 0)
add(containerId, fragment, TAG_USERNAME_CHOOSER)
@ -95,11 +96,12 @@ internal fun FragmentManager.showUsernameChooser(
internal fun FragmentManager.showInternalEmailChooser(
creatableAccountType: AccountType,
cancellable: Boolean = true,
username: String? = null,
domain: String? = null,
containerId: Int = android.R.id.content
) = findFragmentByTag(TAG_INTERNAL_EMAIL_CHOOSER) ?: run {
val fragment = ChooseInternalEmailFragment(creatableAccountType, username, domain)
val fragment = ChooseInternalEmailFragment(creatableAccountType, cancellable, username, domain)
inTransaction {
setCustomAnimations(0, 0)
add(containerId, fragment, TAG_INTERNAL_EMAIL_CHOOSER)
@ -109,10 +111,12 @@ internal fun FragmentManager.showInternalEmailChooser(
internal fun FragmentManager.showExternalEmailChooser(
creatableAccountType: AccountType,
cancellable: Boolean = true,
email: String? = null,
subscriptionDetails: SubscriptionDetails? = null,
containerId: Int = android.R.id.content
) = findFragmentByTag(TAG_EXTERNAL_EMAIL_CHOOSER) ?: run {
val fragment = ChooseExternalEmailFragment(creatableAccountType, subscriptionDetails)
val fragment = ChooseExternalEmailFragment(creatableAccountType, cancellable, email, subscriptionDetails)
inTransaction {
setCustomAnimations(0, 0)
add(containerId, fragment, TAG_EXTERNAL_EMAIL_CHOOSER)
@ -122,11 +126,12 @@ internal fun FragmentManager.showExternalEmailChooser(
internal fun FragmentManager.replaceByInternalEmailChooser(
creatableAccountType: AccountType,
cancellable: Boolean = true,
username: String? = null,
domain: String? = null,
containerId: Int = android.R.id.content
) = findFragmentByTag(TAG_INTERNAL_EMAIL_CHOOSER) ?: run {
val fragment = ChooseInternalEmailFragment(creatableAccountType, username, domain)
val fragment = ChooseInternalEmailFragment(creatableAccountType, cancellable, username, domain)
inTransaction {
setCustomAnimations(0, 0)
replace(containerId, fragment, TAG_INTERNAL_EMAIL_CHOOSER)
@ -136,10 +141,12 @@ internal fun FragmentManager.replaceByInternalEmailChooser(
internal fun FragmentManager.replaceByExternalEmailChooser(
creatableAccountType: AccountType,
cancellable: Boolean = true,
email: String? = null,
subscriptionDetails: SubscriptionDetails? = null,
containerId: Int = android.R.id.content
) = findFragmentByTag(TAG_EXTERNAL_EMAIL_CHOOSER) ?: run {
val fragment = ChooseExternalEmailFragment(creatableAccountType, subscriptionDetails)
val fragment = ChooseExternalEmailFragment(creatableAccountType, cancellable, email, subscriptionDetails)
inTransaction {
setCustomAnimations(0, 0)
replace(containerId, fragment, TAG_EXTERNAL_EMAIL_CHOOSER)

View File

@ -103,9 +103,19 @@ class SignupActivity : AuthActivity<ActivitySignupBinding>(ActivitySignupBinding
if (savedInstanceState == null) {
with(supportFragmentManager) {
when (input.creatableAccountType) {
AccountType.Username -> showUsernameChooser()
AccountType.Internal -> showInternalEmailChooser(input.creatableAccountType)
AccountType.External -> showExternalEmailChooser(input.creatableAccountType, input.subscriptionDetails)
AccountType.Username -> showUsernameChooser(
cancellable = input.cancellable
)
AccountType.Internal -> showInternalEmailChooser(
creatableAccountType = input.creatableAccountType,
cancellable = input.cancellable
)
AccountType.External -> showExternalEmailChooser(
creatableAccountType = input.creatableAccountType,
cancellable = input.cancellable,
email = input.email,
subscriptionDetails = input.subscriptionDetails
)
}
}
}

View File

@ -19,7 +19,10 @@
package me.proton.core.auth.presentation.ui.signup
import android.os.Bundle
import androidx.annotation.DrawableRes
import androidx.annotation.LayoutRes
import androidx.appcompat.widget.Toolbar
import androidx.core.content.res.ResourcesCompat
import com.google.android.material.snackbar.Snackbar
import me.proton.core.auth.presentation.R
import me.proton.core.presentation.ui.ProtonSecureFragment
@ -63,4 +66,11 @@ abstract class SignupFragment : ProtonSecureFragment, UiComponentProductMetricsD
length = Snackbar.LENGTH_INDEFINITE,
)
}
open fun Toolbar.setNavigationIcon(@DrawableRes res: Int, visible: Boolean) {
navigationIcon = when {
visible -> ResourcesCompat.getDrawable(resources, res, null)
else -> null
}
}
}

View File

@ -36,6 +36,7 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.domain.usecase.PerformLogin
import me.proton.core.auth.domain.usecase.signup.SetCreateAccountSuccess
import me.proton.core.auth.domain.usecase.signup.PerformCreateExternalEmailUser
import me.proton.core.auth.domain.usecase.signup.PerformCreateUser
import me.proton.core.auth.domain.usecase.signup.SignupChallengeConfig
@ -57,15 +58,9 @@ import me.proton.core.observability.domain.metrics.SignupAccountCreationTotal
import me.proton.core.observability.domain.metrics.SignupScreenViewTotalV1
import me.proton.core.observability.domain.metrics.common.AccountTypeLabels
import me.proton.core.observability.domain.metrics.common.toObservabilityAccountType
import me.proton.core.payment.domain.entity.Currency
import me.proton.core.payment.domain.entity.ProtonPaymentToken
import me.proton.core.payment.domain.entity.SubscriptionCycle
import me.proton.core.payment.presentation.LogTag
import me.proton.core.payment.presentation.PaymentsOrchestrator
import me.proton.core.plan.domain.IsDynamicPlanEnabled
import me.proton.core.plan.domain.entity.SubscriptionManagement
import me.proton.core.plan.domain.usecase.CanUpgradeToPaid
import me.proton.core.plan.domain.usecase.PerformSubscribe
import me.proton.core.plan.presentation.PlansOrchestrator
import me.proton.core.presentation.savedstate.flowState
import me.proton.core.presentation.savedstate.state
@ -73,7 +68,6 @@ import me.proton.core.presentation.utils.InputValidationResult
import me.proton.core.telemetry.domain.TelemetryContext
import me.proton.core.telemetry.domain.TelemetryManager
import me.proton.core.user.domain.entity.createUserType
import me.proton.core.util.kotlin.CoreLogger
import me.proton.core.util.kotlin.catchWhen
import me.proton.core.util.kotlin.coroutine.withResultContext
import javax.inject.Inject
@ -83,6 +77,7 @@ internal class SignupViewModel @Inject constructor(
private val humanVerificationExternalInput: HumanVerificationExternalInput,
private val performCreateUser: PerformCreateUser,
private val performCreateExternalEmailUser: PerformCreateExternalEmailUser,
private val setCreateAccountSuccess: SetCreateAccountSuccess,
private val keyStoreCrypto: KeyStoreCrypto,
private val plansOrchestrator: PlansOrchestrator,
private val paymentsOrchestrator: PaymentsOrchestrator,
@ -262,7 +257,7 @@ internal class SignupViewModel @Inject constructor(
referrer = null,
type = currentAccountType.createUserType(),
domain = domain
)
).also { setCreateAccountSuccess() }
}
emit(State.CreateUserSuccess(result.id, username, encryptedPassword))
}.catchWhen(Throwable::userAlreadyExists) {
@ -282,7 +277,7 @@ internal class SignupViewModel @Inject constructor(
email = externalEmail,
password = encryptedPassword,
referrer = null
)
).also { setCreateAccountSuccess() }
}
emit(State.CreateUserSuccess(userId.id, externalEmail, encryptedPassword))
}.catchWhen(Throwable::userAlreadyExists) {

View File

@ -131,6 +131,7 @@
<string name="auth_signup_skip_recovery">Skip</string>
<string name="auth_signup_set_recovery">Set recovery method</string>
<string name="auth_signup_no_connectivity">No connectivity to display Terms and Conditions.</string>
<string name="auth_signup_error_create_to_continue">Please create an account to continue</string>
<string name="auth_signup_error_username_blank">Username must not be blank.</string>
<string name="auth_signup_error_passwords_do_not_match">Passwords do not match.</string>
<string name="auth_signup_validation_password_length">Password should be at least 8 characters long.</string>

View File

@ -32,6 +32,7 @@ import me.proton.core.account.domain.entity.AccountType
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.auth.domain.usecase.GetPrimaryUser
import me.proton.core.auth.domain.usecase.PerformLogin
import me.proton.core.auth.domain.usecase.signup.SetCreateAccountSuccess
import me.proton.core.auth.domain.usecase.signup.PerformCreateExternalEmailUser
import me.proton.core.auth.domain.usecase.signup.PerformCreateUser
import me.proton.core.auth.domain.usecase.signup.SignupChallengeConfig
@ -101,6 +102,9 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
@MockK
private lateinit var performLogin: PerformLogin
@MockK(relaxed = true)
private lateinit var setCreateAccountSuccess: SetCreateAccountSuccess
@MockK(relaxed = true)
private lateinit var challengeManager: ChallengeManager
@ -208,6 +212,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
humanVerificationExternalInput,
performCreateUser,
performCreateExternalUser,
setCreateAccountSuccess,
keyStoreCrypto,
plansOrchestrator,
paymentsOrchestrator,
@ -279,6 +284,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
domain = any()
)
}
coVerify { setCreateAccountSuccess.invoke() }
}
}
@ -316,6 +322,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
domain = any()
)
}
coVerify { setCreateAccountSuccess.invoke() }
}
}
@ -353,6 +360,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
domain = any()
)
}
coVerify { setCreateAccountSuccess.invoke() }
}
}
@ -390,6 +398,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
domain = any()
)
}
coVerify { setCreateAccountSuccess.invoke() }
}
}
@ -428,6 +437,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
domain = any()
)
}
coVerify { setCreateAccountSuccess.invoke() }
}
}
@ -480,6 +490,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
domain = any()
)
}
coVerify(exactly = 0) { setCreateAccountSuccess.invoke() }
}
}
@ -506,6 +517,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
referrer = null
)
}
coVerify { setCreateAccountSuccess.invoke() }
}
}
@ -553,6 +565,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
referrer = null
)
}
coVerify(exactly = 0) { setCreateAccountSuccess.invoke() }
}
}
@ -593,6 +606,7 @@ class SignupViewModelTest : ArchTest by ArchTest(), CoroutinesTest by Coroutines
referrer = null
)
}
coVerify { setCreateAccountSuccess.invoke() }
}
}

View File

@ -39,6 +39,8 @@ import me.proton.core.account.domain.entity.AccountType
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.accountmanager.domain.getPrimaryAccount
import me.proton.core.accountmanager.presentation.observe
import me.proton.core.accountmanager.presentation.onAccountCreateAccountFailed
import me.proton.core.accountmanager.presentation.onAccountCreateAccountNeeded
import me.proton.core.accountmanager.presentation.onAccountCreateAddressFailed
import me.proton.core.accountmanager.presentation.onAccountCreateAddressNeeded
import me.proton.core.accountmanager.presentation.onAccountMigrationNeeded
@ -113,6 +115,8 @@ class AccountViewModel @Inject constructor(
.onSessionSecondFactorFailed { signIn(username = it.username) }
.onAccountTwoPassModeNeeded { startTwoPassModeWorkflow(it) }
.onAccountCreateAddressNeeded { startChooseAddressWorkflow(it) }
.onAccountCreateAccountNeeded { startSignupWorkflow(accountType, cancellable = false) }
.onAccountCreateAccountFailed { accountManager.disableAccount(it.userId) }
.onAccountTwoPassModeFailed { accountManager.disableAccount(it.userId) }
.onAccountCreateAddressFailed { accountManager.disableAccount(it.userId) }
.onAccountMigrationNeeded { context.showToast("MigrationNeeded") }

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 Proton Technologies 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.payment.domain.extension
import me.proton.core.payment.domain.entity.Purchase
import me.proton.core.payment.domain.entity.PurchaseState
import me.proton.core.payment.domain.repository.PurchaseRepository
import me.proton.core.util.kotlin.annotation.ExcludeFromCoverage
@ExcludeFromCoverage
public suspend fun PurchaseRepository.getPurchaseOrNull(
state: PurchaseState
): Purchase? = getPurchases().firstOrNull { it.purchaseState == state }