Add auth presentation module.

Add gopenpgp and related config.
Add viewmodels and activities.
Update auth presentation build config and manifest.
Incorporate auth login changes into core example.
This commit is contained in:
dkadrikj 2020-10-15 13:17:27 +02:00
parent c5dfb54420
commit 5f7d8f4f4c
56 changed files with 3219 additions and 31 deletions

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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
import me.proton.core.auth.domain.entity.Account
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.session.Session
import me.proton.core.network.domain.session.SessionId
/**
* Handler for all Account/Authentication Workflow, like SecondFactor or HumanVerification.
*
* Note: Implementation of [AccountManager] should probably also implement this one.
*/
interface AccountWorkflowHandler {
/**
* Handle a new [Session] for a new or existing [Account] from Login workflow.
*/
suspend fun handleSession(account: Account, session: Session)
/**
* Handle TwoPassMode success.
*/
suspend fun handleTwoPassModeSuccess(sessionId: SessionId)
/**
* Handle TwoPassMode failure.
*
* Note: The Workflow must succeed within maximum 10 min of authentication.
*/
suspend fun handleTwoPassModeFailed(sessionId: SessionId)
/**
* Handle SecondFactor success.
*
* @param updatedScopes the new updated full list of scopes.
*/
suspend fun handleSecondFactorSuccess(sessionId: SessionId, updatedScopes: List<String>)
/**
* Handle SecondFactor failure.
*
* Note: Maximum number of failure is 3, then the session will be invalidated and API will return HTTP 401.
*/
suspend fun handleSecondFactorFailed(sessionId: SessionId)
/**
* Handle HumanVerification success.
*
* Note: TokenType and tokenCode must be part of the next API calls.
*/
suspend fun handleHumanVerificationSuccess(sessionId: SessionId, tokenType: String, tokenCode: String)
/**
* Handle HumanVerification failure.
*/
suspend fun handleHumanVerificationFailed(sessionId: SessionId)
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.entity
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.session.SessionId
/**
* @author Dino Kadrikj.
*/
data class Account(
val userId: UserId,
val username: String,
val email: String?,
val sessionId: SessionId,
val isMailboxLoginNeeded: Boolean,
val isSecondFactorNeeded: Boolean
)

View File

@ -40,7 +40,9 @@ dependencies {
project(Module.domain),
project(Module.networkDomain),
project(Module.presentation),
project(Module.authDomain),
project(Module.humanVerificationDomain),
project(Module.humanVerificationPresentation),
// Kotlin
`kotlin-jdk7`,
@ -63,7 +65,10 @@ dependencies {
`hilt-android-compiler`,
`hilt-androidx-compiler`
)
compileOnly(`assistedInject-annotations-dagger`)
compileOnly(
project(Module.gopenpgp),
`assistedInject-annotations-dagger`
)
testImplementation(project(Module.androidTest))
androidTestImplementation(project(Module.androidInstrumentedTest))

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation
enum class AccountType {
/**
* Account with the lowest level of setup. No email address is associated with it.
*/
Username,
/**
* Account with at least one internal email address associated with it.
*/
Internal,
/**
* Account with at least one external email address associated with it.
*/
External
}

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import me.proton.core.auth.presentation.entity.ScopeResult
import me.proton.core.auth.presentation.entity.SessionResult
import me.proton.core.auth.presentation.entity.UserResult
import me.proton.core.auth.presentation.ui.StartLogin
import me.proton.core.auth.presentation.ui.StartMailboxLogin
import me.proton.core.auth.presentation.ui.StartSecondFactor
import me.proton.core.humanverification.presentation.entity.HumanVerificationInput
import me.proton.core.humanverification.presentation.entity.HumanVerificationResult
import me.proton.core.humanverification.presentation.ui.StartHumanVerification
import me.proton.core.network.domain.humanverification.HumanVerificationDetails
import me.proton.core.network.domain.session.SessionId
class AuthOrchestrator {
// region result launchers
private var loginWorkflowLauncher: ActivityResultLauncher<List<String>>? = null
private var secondFactorWorkflowLauncher: ActivityResultLauncher<SessionId>? = null
private var mailboxWorkflowLauncher: ActivityResultLauncher<SessionId>? = null
private var humanWorkflowLauncher: ActivityResultLauncher<HumanVerificationInput>? = null
// endregion
// region private module functions
private fun registerLoginWorkflowLauncher(
context: ComponentActivity,
onSessionResult: (result: SessionResult?) -> Unit = {}
): ActivityResultLauncher<List<String>> =
context.registerForActivityResult(StartLogin()) { result ->
result?.let {
if (it.isSecondFactorNeeded) {
startSecondFactorWorkflow(SessionId(it.sessionId))
} else if (it.isMailboxLoginNeeded) {
startMailboxLoginWorkflow(SessionId(it.sessionId))
}
onSessionResult(it)
}
}
private fun registerMailboxLoginWorkflowLauncher(
context: ComponentActivity,
onUserResult: (result: UserResult?) -> Unit = {}
): ActivityResultLauncher<SessionId> =
context.registerForActivityResult(StartMailboxLogin()) {
onUserResult(it)
}
private fun registerSecondFactorWorkflow(
context: ComponentActivity,
onScopeResult: (result: ScopeResult?) -> Unit = {}
): ActivityResultLauncher<SessionId> =
context.registerForActivityResult(StartSecondFactor()) { result ->
result?.let {
if (it.isMailboxLoginNeeded) {
startMailboxLoginWorkflow(SessionId(it.sessionId))
}
onScopeResult(it)
}
}
private fun registerHumanVerificationWorkflow(
context: ComponentActivity,
onHumanVerificationResult: (result: HumanVerificationResult?) -> Unit = {}
): ActivityResultLauncher<HumanVerificationInput> =
context.registerForActivityResult(StartHumanVerification()) {
onHumanVerificationResult(it)
}
/**
* Start a Second Factor workflow.
*/
private fun startSecondFactorWorkflow(input: SessionId) {
secondFactorWorkflowLauncher?.launch(input)
?: throw IllegalStateException("You must call register before any start workflow function!")
}
/**
* Start a MailboxLogin workflow.
*/
private fun startMailboxLoginWorkflow(input: SessionId) {
mailboxWorkflowLauncher?.launch(input)
?: throw IllegalStateException("You must call register before any start workflow function!")
}
// endregion
// region public API
/**
* Register all needed workflow for internal usage.
*
* Note: This function have to be called [ComponentActivity.onCreate]] before [ComponentActivity.onResume].
*/
fun register(context: ComponentActivity) {
loginWorkflowLauncher ?: run { loginWorkflowLauncher = registerLoginWorkflowLauncher(context) }
humanWorkflowLauncher ?: run { humanWorkflowLauncher = registerHumanVerificationWorkflow(context) }
secondFactorWorkflowLauncher ?: run { secondFactorWorkflowLauncher = registerSecondFactorWorkflow(context) }
mailboxWorkflowLauncher ?: run { mailboxWorkflowLauncher = registerMailboxLoginWorkflowLauncher(context) }
}
/**
* Starts the Login workflow.
*/
fun startLoginWorkflow(requiredFeatures: List<String> = emptyList()) {
loginWorkflowLauncher?.launch(requiredFeatures)
?: throw IllegalStateException("You must call register before any start workflow function!")
}
/**
* Start a Human Verification workflow.
*/
fun startHumanVerificationWorkflow(sessionId: SessionId, details: HumanVerificationDetails?) {
humanWorkflowLauncher?.launch(
HumanVerificationInput(
sessionId.id,
details?.verificationMethods?.map { it.value },
details?.captchaVerificationToken
)
) ?: throw IllegalStateException("You must call register before any start workflow function!")
}
// endregion
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.entity
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import me.proton.core.auth.domain.entity.ScopeInfo
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.session.SessionId
@Parcelize
data class ScopeResult(
val sessionId: String,
val scopes: List<String>,
val isMailboxLoginNeeded: Boolean = false
) : Parcelable {
constructor(sessionId: SessionId, scopeInfo: ScopeInfo, isMailboxLoginNeeded: Boolean = false)
: this(sessionId.id, scopeInfo.scopes, isMailboxLoginNeeded)
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.entity
import android.os.Parcelable
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import me.proton.core.auth.domain.entity.SessionInfo
import me.proton.core.network.domain.session.SessionId
@Parcelize
data class SessionResult(
val username: String,
val accessToken: String,
val expiresIn: Long,
val tokenType: String,
val scope: String,
val scopes: List<String>,
val sessionId: String,
val userId: String,
val refreshToken: String,
val eventId: String,
val serverProof: String,
val localId: Int,
val passwordMode: Int,
val loginPassword: ByteArray? = null,
val isSecondFactorNeeded: Boolean
) : Parcelable {
@IgnoredOnParcel
val isMailboxLoginNeeded = passwordMode == 2
companion object {
fun from(sessionInfo: SessionInfo): SessionResult = SessionResult(
username = sessionInfo.username,
accessToken = sessionInfo.accessToken,
expiresIn = sessionInfo.expiresIn,
tokenType = sessionInfo.tokenType,
scope = sessionInfo.scope,
scopes = sessionInfo.scopes,
sessionId = sessionInfo.sessionId,
userId = sessionInfo.userId,
refreshToken = sessionInfo.refreshToken,
eventId = sessionInfo.eventId,
serverProof = sessionInfo.serverProof,
localId = sessionInfo.localId,
passwordMode = sessionInfo.passwordMode,
loginPassword = sessionInfo.loginPassword,
isSecondFactorNeeded = sessionInfo.isSecondFactorNeeded
)
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.entity
import android.os.Parcelable
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import me.proton.core.auth.domain.entity.User
import me.proton.core.auth.domain.entity.UserKey
@Parcelize
data class UserResult(
val id: String,
val name: String,
val usedSpace: Long,
val currency: String,
val credit: Int,
val maxSpace: Long,
val maxUpload: Long,
val role: Int,
val private: Boolean,
val subscribed: Boolean,
val delinquent: Boolean,
val email: String,
val displayName: String,
val keys: List<UserKeyResult>,
val generatedMailboxPassphrase: ByteArray?
) : Parcelable {
@IgnoredOnParcel
val primaryKey = keys.find { it.primary == 1 }
companion object {
fun from(user: User) = UserResult(
id = user.id,
name = user.name,
usedSpace = user.usedSpace,
currency = user.currency,
credit = user.credit,
maxSpace = user.maxSpace,
maxUpload = user.maxUpload,
role = user.role,
private = user.private,
subscribed = user.subscribed,
delinquent = user.delinquent,
email = user.email,
displayName = user.displayName,
keys = user.keys.map { UserKeyResult.from(it) },
generatedMailboxPassphrase = user.generatedMailboxPassphrase
)
}
}
@Parcelize
data class UserKeyResult(
val id: String,
val version: Int,
val privateKey: String,
val fingerprint: String,
val activation: String? = null,
val primary: Int
) : Parcelable {
companion object {
fun from(key: UserKey) = UserKeyResult(
id = key.id,
version = key.version,
privateKey = key.privateKey,
fingerprint = key.fingerprint,
activation = key.activation,
primary = key.primary
)
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.srp
import com.proton.gopenpgp.crypto.Crypto
import me.proton.core.auth.domain.crypto.CryptoProvider
import javax.inject.Inject
/**
* @author Dino Kadrikj.
*/
class CryptoProviderImpl @Inject constructor() : CryptoProvider {
@Suppress("TooGenericExceptionCaught")
// Proton GopenPGP lib throws this generic exception, so we have to live with this detekt warning
// until the lib is updated
override fun passphraseCanUnlockKey(armoredKey: String, passphrase: ByteArray): Boolean {
return try {
val unlockedKey = Crypto.newKeyFromArmored(armoredKey).unlock(passphrase)
unlockedKey.clearPrivateParams()
true
} catch (ignored: Exception) {
// means that the unlock check has failed. This is how gopenpgp works.
false
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.srp
import com.proton.gopenpgp.srp.Auth
import com.proton.gopenpgp.srp.Proofs
import me.proton.core.auth.domain.crypto.SrpProofProvider
import me.proton.core.auth.domain.crypto.SrpProofs
import me.proton.core.auth.domain.entity.LoginInfo
import javax.inject.Inject
/**
* Implementation of the [SrpProofProvider] interface which returns the generated proofs based on the SRP library.
* @author Dino Kadrikj.
*/
class SrpProofProviderImpl @Inject constructor() : SrpProofProvider {
/**
* Generates SRP Proofs for login.
*/
override fun generateSrpProofs(username: String, passphrase: ByteArray, info: LoginInfo): SrpProofs {
val auth = Auth(
info.version.toLong(),
username,
String(passphrase),
info.salt,
info.modulus,
info.serverEphemeral
)
return auth.generateProofs(SRP_PROOF_BITS).toSrpProofs()
}
companion object {
const val SRP_PROOF_BITS: Long = 2048
}
}
internal fun Proofs.toSrpProofs(): SrpProofs = SrpProofs(
clientEphemeral,
clientProof,
expectedServerProof
)

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import me.proton.core.auth.presentation.entity.ScopeResult
import me.proton.core.auth.presentation.entity.SessionResult
import me.proton.core.auth.presentation.entity.UserResult
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.session.SessionId
class StartLogin : ActivityResultContract<List<String>, SessionResult?>() {
override fun createIntent(context: Context, input: List<String>) =
Intent(context, LoginActivity::class.java).apply {
putStringArrayListExtra(LoginActivity.ARG_REQUIRED_FEATURES, ArrayList(input))
}
override fun parseResult(resultCode: Int, result: Intent?): SessionResult? {
if (resultCode != Activity.RESULT_OK) return null
return result?.getParcelableExtra(LoginActivity.ARG_SESSION_RESULT)
}
}
class StartSecondFactor : ActivityResultContract<SessionId, ScopeResult?>() {
override fun createIntent(context: Context, sessionId: SessionId) =
Intent(context, SecondFactorActivity::class.java).apply {
putExtra(SecondFactorActivity.ARG_SESSION_ID, sessionId.id)
}
override fun parseResult(resultCode: Int, result: Intent?): ScopeResult? {
if (resultCode != Activity.RESULT_OK) return null
return result?.getParcelableExtra(SecondFactorActivity.ARG_SCOPE_RESULT)
}
}
class StartMailboxLogin : ActivityResultContract<SessionId, UserResult?>() {
override fun createIntent(context: Context, sessionId: SessionId) =
Intent(context, MailboxLoginActivity::class.java).apply {
putExtra(MailboxLoginActivity.ARG_SESSION_ID, sessionId.id)
}
override fun parseResult(resultCode: Int, result: Intent?): UserResult? {
if (resultCode != Activity.RESULT_OK) return null
return result?.getParcelableExtra(MailboxLoginActivity.ARG_USER_RESULT)
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
/**
* Interface common for all authentication activities.
*
* @author Dino Kadrikj.
*/
interface AuthActivity {
/**
* Instructs the activity to show loading animation (custom for eacch activity).
*/
fun showLoading(loading: Boolean)
/**
* Provide default implementation for error UI.
*/
fun showError(message: String?)
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
import androidx.databinding.ViewDataBinding
import me.proton.android.core.presentation.ui.ProtonActivity
/**
* Bridge between authentication activities and the interface.
*
* @author Dino Kadrikj.
*/
interface AuthActivityComponent<DB : ViewDataBinding> : AuthActivity {
/**
* Sets and initializes the authentication activity that want to implement [AuthActivity].
*/
fun initializeAuth(protonAuthActivity: ProtonActivity<DB>)
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
import android.os.Build
import android.view.View
import androidx.databinding.ViewDataBinding
import me.proton.android.core.presentation.ui.ProtonActivity
import me.proton.android.core.presentation.utils.errorSnack
import me.proton.core.auth.presentation.R
/**
* Delegate class implementing the [AuthActivity] interface.
*
* @author Dino Kadrikj.
*/
class AuthActivityDelegate<DB : ViewDataBinding> : AuthActivityComponent<DB> {
/** A reference to the Activity that will handle the rotation */
private lateinit var activity : ProtonActivity<DB>
override fun initializeAuth(protonAuthActivity: ProtonActivity<DB>) {
activity = protonAuthActivity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
override fun showLoading(loading: Boolean) {
// noop
}
override fun showError(message: String?) {
showLoading(false)
activity.binding.root.errorSnack(message = message ?: activity.getString(R.string.auth_login_general_error))
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
import android.os.Bundle
import me.proton.android.core.presentation.ui.ProtonActivity
import me.proton.android.core.presentation.utils.onClick
import me.proton.android.core.presentation.utils.openBrowserLink
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.databinding.ActivityAuthHelpBinding
/**
* Authentication help Activity which offers common authentication problems help.
* @author Dino Kadrikj.
*/
class AuthHelpActivity : ProtonActivity<ActivityAuthHelpBinding>(), AuthActivityComponent<ActivityAuthHelpBinding> by AuthActivityDelegate() {
override fun layoutId(): Int = R.layout.activity_auth_help
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initializeAuth(this)
binding.apply {
closeButton.onClick {
finish()
}
helpOptionCustomerSupport.itemHelpLayout.onClick {
openBrowserLink(getString(R.string.contact_support_link))
}
helpOptionOtherIssues.itemHelpLayout.onClick {
openBrowserLink(getString(R.string.common_login_problems_link))
}
helpOptionForgotPassword.itemHelpLayout.onClick {
openBrowserLink(getString(R.string.forgot_password_link))
}
helpOptionForgotUsername.itemHelpLayout.onClick {
openBrowserLink(getString(R.string.forgot_username_link))
}
}
}
override fun showLoading(loading: Boolean) {
// no-operation
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import me.proton.android.core.presentation.ui.ProtonActivity
import me.proton.android.core.presentation.utils.hideKeyboard
import me.proton.android.core.presentation.utils.onClick
import me.proton.android.core.presentation.utils.onFailure
import me.proton.android.core.presentation.utils.onSuccess
import me.proton.android.core.presentation.utils.validatePassword
import me.proton.android.core.presentation.utils.validateUsername
import me.proton.core.auth.domain.entity.SessionInfo
import me.proton.core.auth.domain.usecase.PerformLogin
import me.proton.core.auth.presentation.AuthOrchestrator
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.databinding.ActivityLoginBinding
import me.proton.core.auth.presentation.entity.SessionResult
import me.proton.core.auth.presentation.viewmodel.LoginViewModel
import me.proton.core.util.kotlin.exhaustive
/**
* Login Activity which allows users to Login to any Proton client application.
*/
@AndroidEntryPoint
class LoginActivity : ProtonActivity<ActivityLoginBinding>(),
AuthActivityComponent<ActivityLoginBinding> by AuthActivityDelegate() {
private val viewModel by viewModels<LoginViewModel>()
private val authOrchestrator = AuthOrchestrator()
override fun layoutId(): Int = R.layout.activity_login
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initializeAuth(this)
authOrchestrator.register(this)
binding.apply {
closeButton.onClick {
finish()
}
helpButton.onClick {
startActivity(Intent(this@LoginActivity, AuthHelpActivity::class.java))
}
signInButton.onClick(::onSignInClicked)
}
viewModel.loginState.observeData {
when (it) {
is PerformLogin.LoginState.Processing -> showLoading(true)
is PerformLogin.LoginState.Success -> onSuccess(it.sessionInfo)
is PerformLogin.LoginState.Error.Message -> onError(it.validation, it.message)
is PerformLogin.LoginState.Error.EmptyCredentials -> onError(
true,
getString(R.string.auth_login_empty_credentials)
)
}.exhaustive
}
}
/**
* Invoked on successful completed login operation.
*/
private fun onSuccess(sessionInfo: SessionInfo) {
val intent = Intent().putExtra(ARG_SESSION_RESULT, SessionResult.from(sessionInfo))
setResult(Activity.RESULT_OK, intent)
finish()
}
/**
* Invoked on error result from login operation.
*/
private fun onError(triggerValidation: Boolean, message: String?) {
if (triggerValidation) {
binding.apply {
usernameInput.setInputError()
passwordInput.setInputError()
}
}
showError(message)
}
override fun showLoading(loading: Boolean) = with(binding) {
if (loading) {
signInButton.setLoading()
} else {
signInButton.setIdle()
}
}
private fun onSignInClicked() {
with(binding) {
hideKeyboard()
usernameInput.validateUsername()
.onFailure { usernameInput.setInputError() }
.onSuccess(::onUsernameValidationSuccess)
}
}
private fun onUsernameValidationSuccess(username: String) {
with(binding) {
passwordInput.validatePassword()
.onFailure { passwordInput.setInputError() }
.onSuccess { password ->
signInButton.setLoading()
viewModel.startLoginWorkflow(username, password.toByteArray())
}
}
}
companion object {
const val ARG_REQUIRED_FEATURES = "arg.requiredFeatures"
const val ARG_SESSION_RESULT = "arg.sessionResult"
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import me.proton.android.core.presentation.ui.ProtonActivity
import me.proton.android.core.presentation.utils.hideKeyboard
import me.proton.android.core.presentation.utils.onClick
import me.proton.android.core.presentation.utils.onFailure
import me.proton.android.core.presentation.utils.onSuccess
import me.proton.android.core.presentation.utils.openBrowserLink
import me.proton.android.core.presentation.utils.validatePassword
import me.proton.core.auth.domain.entity.User
import me.proton.core.auth.domain.usecase.PerformMailboxLogin
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.databinding.ActivityMailboxLoginBinding
import me.proton.core.auth.presentation.entity.UserResult
import me.proton.core.auth.presentation.viewmodel.MailboxLoginViewModel
import me.proton.core.network.domain.session.SessionId
import me.proton.core.util.kotlin.exhaustive
/**
* Mailbox Login Activity which allows users to unlock their Mailbox.
* Note that this is only valid for accounts which are 2 password accounts (they use separate password for login and
* mailbox).
*/
@AndroidEntryPoint
class MailboxLoginActivity : ProtonActivity<ActivityMailboxLoginBinding>(),
AuthActivityComponent<ActivityMailboxLoginBinding> by AuthActivityDelegate() {
private val sessionId: SessionId by lazy {
intent?.extras?.get(ARG_SESSION_ID) as SessionId
}
private val viewModel by viewModels<MailboxLoginViewModel>()
override fun layoutId(): Int = R.layout.activity_mailbox_login
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initializeAuth(this)
binding.apply {
closeButton.onClick {
finish()
}
forgotPasswordButton.onClick {
openBrowserLink(getString(R.string.forgot_password_link))
}
unlockButton.onClick(::onUnlockClicked)
}
viewModel.mailboxLoginState.observeData {
when (it) {
is PerformMailboxLogin.MailboxLoginState.Processing -> showLoading(true)
is PerformMailboxLogin.MailboxLoginState.Success -> onSuccess(it.user)
is PerformMailboxLogin.MailboxLoginState.Error.Message -> onError(false, it.message)
is PerformMailboxLogin.MailboxLoginState.Error.EmptyCredentials -> onError(
true,
getString(R.string.auth_mailbox_empty_credentials)
)
else -> onError(false)
}.exhaustive
}
}
override fun showLoading(loading: Boolean) = with(binding) {
if (loading) {
unlockButton.setLoading()
} else {
unlockButton.setIdle()
}
}
/**
* Invoked on successful completed mailbox login operation.
*/
private fun onSuccess(user: User) {
val intent = Intent().apply { putExtra(ARG_USER_RESULT, UserResult.from(user)) }
setResult(Activity.RESULT_OK, intent)
finish()
}
private fun onError(triggerValidation: Boolean, message: String? = null) {
if (triggerValidation) {
binding.mailboxPasswordInput.setInputError()
}
showError(message)
}
private fun onUnlockClicked() {
hideKeyboard()
with(binding) {
mailboxPasswordInput.validatePassword()
.onFailure {
mailboxPasswordInput.setInputError()
}
.onSuccess {
viewModel.startMailboxLoginFlow(sessionId, it.toByteArray())
}
}
}
companion object {
const val ARG_SESSION_ID = "arg.sessionId"
const val ARG_USER_RESULT = "arg.userResult"
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.InputType
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import me.proton.android.core.presentation.ui.ProtonActivity
import me.proton.android.core.presentation.utils.hideKeyboard
import me.proton.android.core.presentation.utils.onClick
import me.proton.android.core.presentation.utils.onFailure
import me.proton.android.core.presentation.utils.onSuccess
import me.proton.android.core.presentation.utils.validate
import me.proton.core.auth.domain.entity.ScopeInfo
import me.proton.core.auth.domain.usecase.PerformSecondFactor
import me.proton.core.auth.presentation.R
import me.proton.core.auth.presentation.databinding.Activity2faBinding
import me.proton.core.auth.presentation.entity.ScopeResult
import me.proton.core.auth.presentation.viewmodel.SecondFactorViewModel
import me.proton.core.network.domain.session.SessionId
import me.proton.core.util.kotlin.exhaustive
/**
* Second Factor Activity responsible for entering the second factor code.
* It also supports recovery code mode, which allows the user to enter a second factor recovery code.
* Optional, only shown for accounts with 2FA login enabled.
*/
@AndroidEntryPoint
class SecondFactorActivity : ProtonActivity<Activity2faBinding>(),
AuthActivityComponent<Activity2faBinding> by AuthActivityDelegate() {
private val sessionId: SessionId by lazy {
intent?.extras?.get(ARG_SESSION_ID) as SessionId
}
private val twoPassMode: Boolean by lazy {
intent?.extras?.get(ARG_TWO_PASS_MODE) as Boolean
}
// initial mode is the second factor input mode.
private var mode = Mode.TWO_FACTOR
private val viewModel by viewModels<SecondFactorViewModel>()
override fun layoutId(): Int = R.layout.activity_2fa
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initializeAuth(this)
binding.apply {
closeButton.onClick {
finish()
}
recoveryCodeButton.onClick {
when (mode) {
Mode.TWO_FACTOR -> switchToRecovery()
Mode.RECOVERY_CODE -> switchToTwoFactor()
}
}
authenticateButton.onClick(::onAuthenticateClicked)
}
viewModel.secondFactorState.observeData {
when (it) {
is PerformSecondFactor.SecondFactorState.Processing -> showLoading(true)
is PerformSecondFactor.SecondFactorState.Success -> onSuccess(
it.sessionId,
it.scopeInfo,
it.isMailboxLoginNeeded
)
is PerformSecondFactor.SecondFactorState.Error.Message -> onError(false, it.message)
is PerformSecondFactor.SecondFactorState.Error.EmptyCredentials -> {
onError(true, getString(R.string.auth_2fa_error_empty_code))
}
}.exhaustive
}
}
override fun showLoading(loading: Boolean) = with(binding) {
if (loading) {
authenticateButton.setLoading()
} else {
authenticateButton.setIdle()
}
}
private fun onAuthenticateClicked() {
hideKeyboard()
with(binding) {
secondFactorInput.validate()
.onFailure { secondFactorInput.setInputError() }
.onSuccess { secondFactorCode ->
viewModel.startSecondFactorFlow(sessionId, secondFactorCode, twoPassMode)
}
}
}
/**
* Invoked on successful completed mailbox login operation.
*/
private fun onSuccess(sessionId: SessionId, scopeInfo: ScopeInfo, isMailboxLoginNeeded: Boolean) {
val intent =
Intent().putExtra(
ARG_SCOPE_RESULT,
ScopeResult(sessionId, scopeInfo, isMailboxLoginNeeded)
)
setResult(Activity.RESULT_OK, intent)
finish()
}
private fun onError(triggerValidation: Boolean, message: String?) {
if (triggerValidation) {
binding.secondFactorInput.setInputError()
}
showError(message)
}
/**
* Switches the mode to recovery code. It also handles the UI for the new mode.
*/
private fun Activity2faBinding.switchToRecovery() {
mode = Mode.RECOVERY_CODE
secondFactorInput.apply {
text = ""
helpText = getString(R.string.auth_2fa_recovery_code_assistive_text)
labelText = getString(R.string.auth_2fa_recovery_code_label)
inputType = InputType.TYPE_CLASS_TEXT
}
recoveryCodeButton.text = getString(R.string.auth_2fa_use_2fa_code)
}
/**
* Switches the mode to second factor code. It also handles the UI for the new mode.
*/
private fun Activity2faBinding.switchToTwoFactor() {
mode = Mode.TWO_FACTOR
secondFactorInput.apply {
text = ""
helpText = getString(R.string.auth_2fa_assistive_text)
labelText = getString(R.string.auth_2fa_label)
inputType = InputType.TYPE_CLASS_NUMBER
}
recoveryCodeButton.text = getString(R.string.auth_2fa_use_recovery_code)
}
/**
* Working modes of this View.
*/
private enum class Mode {
TWO_FACTOR,
RECOVERY_CODE
}
companion object {
const val ARG_SESSION_ID = "arg.sessionId"
const val ARG_TWO_PASS_MODE = "arg.twoPassMode"
const val ARG_SCOPE_RESULT = "arg.scopeResult"
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.android.core.presentation.viewmodel.ProtonViewModel
import me.proton.core.auth.domain.AccountWorkflowHandler
import me.proton.core.auth.domain.entity.Account
import me.proton.core.auth.domain.usecase.PerformLogin
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.session.Session
import me.proton.core.network.domain.session.SessionId
import me.proton.core.util.kotlin.DispatcherProvider
import studio.forface.viewstatestore.ViewStateStore
import studio.forface.viewstatestore.ViewStateStoreScope
/**
* View model class serving the Login activity.
*/
class LoginViewModel @ViewModelInject constructor(
private val accountWorkflow: AccountWorkflowHandler,
private val performLogin: PerformLogin
) : ProtonViewModel(), ViewStateStoreScope {
val loginState = ViewStateStore<PerformLogin.LoginState>().lock
/**
* Attempts to make the login call.
*
* @param username the account's username entered as input
* @param password the accounts's password entered as input
*/
fun startLoginWorkflow(
username: String,
password: ByteArray
) {
performLogin(username, password)
.onEach {
if (it is PerformLogin.LoginState.Success) {
// on success result, contact account manager
onSuccess(it)
}
// inform the view for each state change
loginState.post(it)
}
.launchIn(viewModelScope)
}
private suspend fun onSuccess(success: PerformLogin.LoginState.Success) {
val result = success.sessionInfo
val account = Account(
username = result.username,
userId = UserId(result.userId),
email = null,
sessionId = SessionId(result.sessionId),
isMailboxLoginNeeded = result.isMailboxLoginNeeded,
isSecondFactorNeeded = result.isSecondFactorNeeded
)
val session = Session(
sessionId = SessionId(result.sessionId),
accessToken = result.accessToken,
refreshToken = result.refreshToken,
scopes = result.scopes
)
accountWorkflow.handleSession(account, session)
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.android.core.presentation.viewmodel.ProtonViewModel
import me.proton.core.auth.domain.AccountWorkflowHandler
import me.proton.core.auth.domain.usecase.PerformMailboxLogin
import me.proton.core.domain.entity.UserId
import me.proton.core.network.domain.session.SessionId
import me.proton.core.util.kotlin.DispatcherProvider
import studio.forface.viewstatestore.ViewStateStore
import studio.forface.viewstatestore.ViewStateStoreScope
/**
* View model class for handling the mailbox login and passphrase generation.
*/
class MailboxLoginViewModel @ViewModelInject constructor(
private val accountWorkflowHandler: AccountWorkflowHandler,
private val performMailboxLogin: PerformMailboxLogin
) : ProtonViewModel(), ViewStateStoreScope {
val mailboxLoginState = ViewStateStore<PerformMailboxLogin.MailboxLoginState>().lock
/**
* Attempts the mailbox login flow. This includes whole procedure with passphrase generation and API handling.
*/
fun startMailboxLoginFlow(
sessionId: SessionId,
password: ByteArray
) {
performMailboxLogin(sessionId, password)
.onEach {
if (it is PerformMailboxLogin.MailboxLoginState.Success) {
accountWorkflowHandler.handleTwoPassModeSuccess(sessionId)
} else if (it is PerformMailboxLogin.MailboxLoginState.Error) {
accountWorkflowHandler.handleTwoPassModeFailed(sessionId)
}
mailboxLoginState.post(it)
}.launchIn(viewModelScope)
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.android.core.presentation.viewmodel.ProtonViewModel
import me.proton.core.auth.domain.AccountWorkflowHandler
import me.proton.core.auth.domain.usecase.PerformSecondFactor
import me.proton.core.network.domain.session.SessionId
import me.proton.core.util.kotlin.DispatcherProvider
import studio.forface.viewstatestore.ViewStateStore
import studio.forface.viewstatestore.ViewStateStoreScope
/**
* View Model that serves the Second Factor authentication.
*/
class SecondFactorViewModel @ViewModelInject constructor(
private val accountWorkflowHandler: AccountWorkflowHandler,
private val performSecondFactor: PerformSecondFactor
) : ProtonViewModel(), ViewStateStoreScope {
val secondFactorState = ViewStateStore<PerformSecondFactor.SecondFactorState>().lock
fun startSecondFactorFlow(
sessionId: SessionId,
secondFactorCode: String,
isMailboxLoginNeeded: Boolean
) {
performSecondFactor(sessionId, secondFactorCode)
.onEach {
when (it) {
is PerformSecondFactor.SecondFactorState.Success -> {
secondFactorState.post(it.copy(isMailboxLoginNeeded = isMailboxLoginNeeded))
accountWorkflowHandler.handleSecondFactorSuccess(
sessionId = sessionId,
updatedScopes = it.scopeInfo.scopes
)
}
is PerformSecondFactor.SecondFactorState.Error -> {
secondFactorState.post(it)
accountWorkflowHandler.handleSecondFactorFailed(sessionId)
}
else -> {
secondFactorState.post(it)
}
}
}
.launchIn(viewModelScope)
}
}

View File

@ -0,0 +1,4 @@
<!--~ Copyright (c) 2020 Proton Technologies AG ~ This file is part of Proton Technologies 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/>.-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M10.065 12H15V3H1.5v9h3v5.565L10.065 12zM3 10.5v-6h10.5v6H9.435L6 13.935V10.5H3zM16.5 9V7.5h6v9h-3v5.565L13.935 16.5H9v-3h1.5V15h4.065L18 18.435v-3.42h1.5V15H21V9h-4.5z" android:fillColor="#17181C" android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,22 @@
<!--~ Copyright (c) 2020 Proton Technologies AG ~ This file is part of Proton Technologies 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/>.-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="312dp" android:height="196dp" android:viewportWidth="312" android:viewportHeight="196">
<group>
<clip-path android:pathData="M0-164h312v360H0z"/>
<path android:pathData="M0-164h312v360H0z" android:fillColor="@android:color/transparent"/>
<path android:pathData="M113.753 51.381c-4.326 10.419-0.216 22.29 2.4 31.886 0.153 0.558 0.315 1.134 0.767 1.545 0.307 0.277 0.727 0.464 1.143 0.642 10.151 4.375 22.735 9.726 30.095 5.335 7.196-4.293 15.074-19.291 29.794-26.264 2.639-1.25-2.597-4.874-2.103-7.077 0.878-3.922-12.875 1.262-15.111-1.802-1.56-2.137-3.213-4.248-5.421-5.965-2.271-1.766-5.111-3.091-8.302-3.875-1.459-0.358-3.026-0.605-4.683-0.429-2.062 0.218-4.085 1.065-6.118 1.692-4.762 1.466-9.813 1.733-14.096 0.744l-8.365 3.568z" android:fillColor="#5064B6"/>
<path android:pathData="M169.383 37.577c1.521-8.241 7.28-14.984 13.034-21.077 3.543-3.753 7.667-7.666 12.82-7.982 5.945-0.365 10.945 4.14 15.173 8.336 2.226 2.208 8.653 3.099 9.459 6.13 0.699 2.63 0.453 18.723-8.955 29.956-4.342 5.183-31.389 30.702-32.867 37.302-3.38 15.092 1.372 22.119 3.991 30.788 0.635 2.105-1.123 4.218-3.303 3.942-5.461-0.691-12.751-4.41-16.406-11.01-5.575-10.067-5.506-20.183-3.61-26.333 3.195-10.36 8.713-18.08 6.897-30.643-2.114-14.608 3.344-17.122 3.767-19.409z" android:fillColor="#8397E9" android:fillType="evenOdd"/>
<path android:pathData="M145.522 115.527c-0.544-2.331 0.906-4.661 3.236-5.204 2.331-0.544 4.661 0.905 5.204 3.235 0.544 2.332-0.904 4.662-3.236 5.205-2.331 0.544-4.659-0.905-5.204-3.236zm7.398 74.623c0.291 1.245 0.96 2.153 1.496 2.028 0.016-0.004 0.029-0.016 0.044-0.022l8.706-2.029c0.17-0.039 0.339 0.066 0.378 0.235l0.633 2.713c0 0.046 0.004 0.093 0.015 0.139 0.205 0.876 2.02 1.2 4.056 0.726 2.036-0.475 3.521-1.57 3.316-2.445-0.01-0.047-0.028-0.09-0.047-0.133l-12.232-52.496c-0.205-0.881 0.166-1.812 0.945-2.271 6.736-3.976 10.246-12.232 7.705-20.239-2.577-8.117-10.858-13.208-19.265-11.851-10.039 1.621-16.445 11.352-14.175 21.086 1.766 7.57 8.237 12.815 15.6 13.367 0.902 0.069 1.645 0.739 1.85 1.62l8.415 36.133c0.039 0.168-0.065 0.337-0.235 0.377l-6.323 1.474 0.001 0.002c-0.017 0.001-0.035-0.005-0.051-0.001-0.536 0.125-0.734 1.236-0.443 2.481 0.289 1.245 0.96 2.153 1.495 2.028 0.016-0.003 0.029-0.016 0.045-0.022l-0.001 0.002 6.324-1.475c0.17-0.039 0.338 0.065 0.379 0.235l0.803 3.449c0.039 0.168-0.066 0.337-0.235 0.378l-8.705 2.029c-0.017 0.003-0.034-0.002-0.049 0.002-0.536 0.124-0.735 1.234-0.445 2.48z" android:fillColor="#3C4B88" android:fillType="evenOdd"/>
<path android:pathData="M241.5-151.5c13.037-50.367-39.53-44.144-59-42-21.705 2.392-21.913 94.058-11.474 148.798 2.135 11.192 9.49 26.847-1.704 43.109-7.8 11.332-53.005 16.343-61.414 27.231-1.993 2.579 2.186 7.091 0.537 9.884-0.876 1.485-6.041 3.39-6.751 6.634-0.506 2.317 1.699 5.85 0.894 8.101-0.938 2.626-7.616 7.123-4.258 13.287 4.381 8.043 2.144 13.04 1.652 26.865-0.376 10.532-5.378 25.77 6.233 32.373 5.885 3.347 24.449 13.963 30.09 18.803 6.709-1.86 3.139-12.238-6.393-20.721-3.415-3.039-8.46-5.173-8.866-9.151-0.97-9.483-1.303-10.732 0.465-16.377 3.227-10.309 53.536-26.023 75.561-39.501 25.608-15.668 22.686-28.12 22.797-32.851 1.079-46.248 8.594-124.117 21.631-174.484z" android:fillColor="#8397E9"/>
<path android:pathData="M171.016-188.14c-10.569 5.428-8.63 108.488 0.01 143.438 2.742 11.089 9.49 26.848-1.704 43.11-7.8 11.331-53.005 16.342-61.414 27.23-0.802 1.038-12.541 17.891-14.786 36.834-3.33 28.105 4.621 49.35 7.197 27.885 2.698-22.488 60.318-36.762 73.761-42.277 11.126-4.565 35.081-35.081 35.081-60.268 0-6.243 28.42-210.133-38.145-175.952z" android:fillAlpha="0.48" android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient android:startY="-49.7121" android:startX="122.03" android:endY="-3.33564" android:endX="183.403" android:type="linear">
<item android:offset="0" android:color="#FFFFFFFF"/>
<item android:offset="1" android:color="#00FFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M202.251 107.392c1.628-1.249 3.961-0.942 5.21 0.687 1.25 1.628 0.942 3.961-0.686 5.21-1.628 1.249-3.961 0.942-5.21-0.685-1.25-1.63-0.942-3.961 0.686-5.212zM96.246 11.224c-0.945 1.821-3.188 2.53-5.009 1.584-1.822-0.945-2.532-3.188-1.586-5.01 0.946-1.821 3.189-2.531 5.011-1.585 1.821 0.947 2.531 3.189 1.584 5.011z" android:fillColor="#2BCBBA" android:fillType="evenOdd"/>
<path android:pathData="M231.407 94.151l5.455-3.966 3.979 5.493-5.454 3.965-3.98-5.492zM60.728 22.879l5.455-3.965 3.978 5.492-5.453 3.966-3.98-5.493z" android:fillColor="#BABDC6" android:fillType="evenOdd"/>
<path android:pathData="M238.226 22.778c1.855-0.878 4.071-0.086 4.948 1.77 0.877 1.856 0.084 4.071-1.771 4.948-1.855 0.878-4.071 0.085-4.948-1.77-0.877-1.856-0.084-4.071 1.771-4.948z" android:fillColor="#2BCBBA" android:fillType="evenOdd"/>
</group>
</vector>

View File

@ -0,0 +1,4 @@
<!--~ Copyright (c) 2020 Proton Technologies AG ~ This file is part of Proton Technologies 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/>.-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M21.375 5.775L22.5 4.71 19.83 2.1l-9.255 9.24c-2.223-1.61-5.312-1.23-7.078 0.87-1.767 2.101-1.61 5.21 0.357 7.123 1.968 1.913 5.08 1.982 7.13 0.157s2.343-4.923 0.67-7.1l5.88-5.88 1.5 1.5L20.1 6.96l-1.5-1.5 1.245-1.245 1.53 1.56zm-11.22 12.45c-1.466 1.462-3.838 1.46-5.301-0.004-1.463-1.465-1.463-3.837 0-5.301 1.463-1.465 3.835-1.467 5.3-0.005 1.453 1.472 1.453 3.838 0 5.31z" android:fillColor="#17181C" android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,4 @@
<!--~ Copyright (c) 2020 Proton Technologies AG ~ This file is part of Proton Technologies 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/>.-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 1.5C6.201 1.5 1.5 6.201 1.5 12S6.201 22.5 12 22.5 22.5 17.799 22.5 12c0-2.785-1.106-5.455-3.075-7.425C17.455 2.606 14.785 1.5 12 1.5zM12 21c-4.97 0-9-4.03-9-9s4.03-9 9-9 9 4.03 9 9c0 2.387-0.948 4.676-2.636 6.364C16.676 20.052 14.387 21 12 21zm1.035-15.12c-1.195-0.317-2.469-0.062-3.45 0.69-0.956 0.752-1.51 1.904-1.5 3.12h1.5C9.575 8.938 9.912 8.224 10.5 7.755c0.61-0.467 1.404-0.623 2.145-0.42 0.824 0.215 1.473 0.847 1.71 1.665 0.219 0.74 0.074 1.539-0.39 2.154C13.5 11.77 12.77 12.128 12 12.12h-0.75V15h1.5v-1.5c1.801-0.349 3.124-1.893 3.193-3.727 0.068-1.833-1.137-3.472-2.908-3.953v0.06zM11.25 18v-1.5h1.5V18h-1.5z" android:fillColor="#17181C" android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,4 @@
<!--~ Copyright (c) 2020 Proton Technologies AG ~ This file is part of Proton Technologies 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/>.-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M14.826 1.88C19.366 3.15 22.503 7.287 22.5 12c0.03 2.69-0.974 5.288-2.805 7.26l-0.75 0.66c-0.24 0.212-0.49 0.413-0.75 0.6-1.538 1.124-3.353 1.808-5.25 1.98H12c-2.537-0.03-4.979-0.966-6.885-2.64L4.35 19.185c-3.228-3.434-3.773-8.598-1.332-12.63 2.44-4.032 7.27-5.943 11.808-4.675zm1.464 17.935c0.191-0.094 0.377-0.199 0.555-0.315l0.255-0.12L14.865 18h-5.73L6.9 19.32l0.255 0.18c0.18 0.12 0.375 0.225 0.57 0.33l0.645 0.315c0.29 0.125 0.585 0.235 0.885 0.33h0.21c1.658 0.465 3.412 0.465 5.07 0h0.225c0.237-0.075 0.475-0.17 0.72-0.27l0.15-0.06 0.66-0.33zm-0.945-3.375l3 1.875v0.06C20.043 16.687 20.998 14.393 21 12c0.058-4.103-2.666-7.724-6.624-8.807C10.42 2.11 6.23 3.84 4.191 7.4c-2.039 3.56-1.412 8.048 1.524 10.914l3-1.875h6.63zM7.5 10.5C7.5 8.014 9.515 6 12 6s4.5 2.014 4.5 4.5c0 2.485-2.015 4.5-4.5 4.5s-4.5-2.015-4.5-4.5zm1.5 0c0 1.657 1.343 3 3 3 0.796 0 1.559-0.316 2.121-0.879 0.563-0.563 0.88-1.326 0.88-2.121 0-1.657-1.344-3-3-3-1.658 0-3 1.343-3 3z" android:fillColor="#17181C" android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/loginContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground">
<ScrollView
android:id="@+id/scrollContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/auth_parent_default_padding"
android:paddingTop="104dp"
android:paddingEnd="@dimen/auth_parent_default_padding">
<TextView
android:id="@+id/titleText"
style="@style/ProtonTextView.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/auth_2fa_title" />
<me.proton.android.core.presentation.ui.view.ProtonInput
android:id="@+id/secondFactorInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:layout_marginTop="@dimen/auth_content_top_margin"
app:help="@string/auth_2fa_assistive_text"
app:label="@string/auth_2fa_label" />
<me.proton.android.core.presentation.ui.view.ProtonProgressButton
android:id="@+id/authenticateButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_separator_top_margin"
android:text="@string/auth_2fa_action" />
<me.proton.android.core.presentation.ui.view.ProtonButton
android:id="@+id/recoveryCodeButton"
style="@style/ProtonButton.Borderless.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_view_items_top_margin"
android:text="@string/auth_2fa_use_recovery_code" />
</LinearLayout>
</ScrollView>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/closeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_marginStart="@dimen/auth_close_button_margin_start"
android:layout_marginTop="@dimen/auth_close_button_margin_top"
android:background="?attr/selectableItemBackgroundBorderless"
android:elevation="4dp"
app:srcCompat="@drawable/ic_back_no_toolbar" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:fitsSystemWindows="true">
<!-- the scroll view is needed for landscape orientation -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/auth_parent_default_padding"
android:paddingEnd="@dimen/auth_parent_default_padding">
<TextView
android:id="@+id/title"
style="@style/ProtonTextView.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_no_toolbar_top_margin"
android:text="@string/auth_help_title" />
<include
android:id="@+id/helpOptionForgotUsername"
layout="@layout/content_help_item_forgot_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_top_margin" />
<include
android:id="@+id/helpOptionForgotPassword"
layout="@layout/content_help_item_forgot_password"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include
android:id="@+id/helpOptionOtherIssues"
layout="@layout/content_help_item_other_issues"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
style="@style/ProtonTextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_separator_top_margin"
android:text="@string/auth_help_other" />
<include
android:id="@+id/helpOptionCustomerSupport"
layout="@layout/content_help_item_customer_support"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_top_margin" />
</LinearLayout>
</ScrollView>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/closeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_marginStart="@dimen/auth_close_button_margin_start"
android:layout_marginTop="@dimen/auth_close_button_margin_top"
android:background="?attr/selectableItemBackgroundBorderless"
android:elevation="4dp"
app:srcCompat="@drawable/ic_close_no_toolbar" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/loginContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground">
<ScrollView
android:id="@+id/scrollContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/auth_parent_default_padding"
android:paddingEnd="@dimen/auth_parent_default_padding">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/headerImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_hand_key" />
<TextView
android:id="@+id/titleText"
style="@style/ProtonTextView.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/auth_sign_in" />
<TextView
android:id="@+id/subtitleText"
style="@style/ProtonTextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_subtitle_top_margin"
android:text="@string/auth_account_details" />
<me.proton.android.core.presentation.ui.view.ProtonInput
android:id="@+id/usernameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_top_margin"
android:hint="@string/auth_email_username_hint"
app:help="@string/auth_login_assistive_text"
app:label="@string/auth_email_username" />
<me.proton.android.core.presentation.ui.view.ProtonInput
android:id="@+id/passwordInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_top_margin"
android:hint="@string/auth_password"
android:inputType="textPassword"
app:label="@string/auth_password_label" />
<me.proton.android.core.presentation.ui.view.ProtonProgressButton
android:id="@+id/signInButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_separator_top_margin"
android:text="@string/auth_sign_in_action" />
<me.proton.android.core.presentation.ui.view.ProtonButton
android:id="@+id/helpButton"
style="@style/ProtonButton.Borderless.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_view_items_top_margin"
android:text="@string/auth_need_help" />
</LinearLayout>
</ScrollView>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/closeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_marginStart="@dimen/auth_close_button_margin_start"
android:layout_marginTop="@dimen/auth_close_button_margin_top"
android:background="?attr/selectableItemBackgroundBorderless"
android:elevation="4dp"
app:srcCompat="@drawable/ic_close_no_toolbar" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/loginContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground">
<ScrollView
android:id="@+id/scrollContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/auth_parent_default_padding"
android:paddingTop="104dp"
android:paddingEnd="@dimen/auth_parent_default_padding">
<TextView
android:id="@+id/titleText"
style="@style/ProtonTextView.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/auth_mailbox_title" />
<me.proton.android.core.presentation.ui.view.ProtonInput
android:id="@+id/mailboxPasswordInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_top_margin"
android:inputType="textPassword"
android:hint="@string/auth_mailbox_password"
app:label="@string/auth_mailbox_password_label" />
<me.proton.android.core.presentation.ui.view.ProtonProgressButton
android:id="@+id/unlockButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_content_separator_top_margin"
android:text="@string/auth_mailbox_unlock_action" />
<me.proton.android.core.presentation.ui.view.ProtonButton
android:id="@+id/forgotPasswordButton"
style="@style/ProtonButton.Borderless.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/auth_view_items_top_margin"
android:text="@string/auth_forgot_password" />
</LinearLayout>
</ScrollView>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/closeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_marginStart="@dimen/auth_close_button_margin_start"
android:layout_marginTop="@dimen/auth_close_button_margin_top"
android:background="?attr/selectableItemBackgroundBorderless"
android:elevation="4dp"
app:srcCompat="@drawable/ic_back_no_toolbar" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/itemHelpLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/help_item_height"
android:minHeight="@dimen/help_item_height"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/auth_help_item_horizontal_padding"
android:paddingEnd="@dimen/auth_help_item_horizontal_padding">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_comments" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/nextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="@color/icon_default"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_right" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/auth_help_item_horizontal_margin"
android:layout_marginEnd="@dimen/auth_help_item_horizontal_margin"
android:gravity="center_vertical"
android:text="@string/auth_help_option_customer_support"
android:textSize="@dimen/auth_help_item_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/nextButton"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/horizontal_separator_height"
android:layout_gravity="bottom"
android:background="@color/layout_weak" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/itemHelpLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/help_item_height"
android:minHeight="@dimen/help_item_height"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/auth_help_item_horizontal_padding"
android:paddingEnd="@dimen/auth_help_item_horizontal_padding">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_key" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/nextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="@color/icon_default"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_right" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/auth_help_item_horizontal_margin"
android:layout_marginEnd="@dimen/auth_help_item_horizontal_margin"
android:text="@string/auth_help_option_forgot_password"
android:textSize="@dimen/auth_help_item_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/nextButton"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/horizontal_separator_height"
android:layout_gravity="bottom"
android:background="@color/layout_weak" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/itemHelpLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/help_item_height"
android:minHeight="@dimen/help_item_height"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/auth_help_item_horizontal_padding"
android:paddingEnd="@dimen/auth_help_item_horizontal_padding">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_user_circle" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/nextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="@color/icon_default"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_right" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/auth_help_item_horizontal_margin"
android:layout_marginEnd="@dimen/auth_help_item_horizontal_margin"
android:text="@string/auth_help_option_forgot_username"
android:textSize="@dimen/auth_help_item_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/nextButton"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/horizontal_separator_height"
android:layout_gravity="bottom"
android:background="@color/layout_weak" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/itemHelpLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/help_item_height"
android:clickable="true"
android:focusable="true"
android:minHeight="@dimen/help_item_height">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/auth_help_item_horizontal_padding"
android:paddingEnd="@dimen/auth_help_item_horizontal_padding">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_question_circle" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/nextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="@color/icon_default"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_right" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/auth_help_item_horizontal_margin"
android:layout_marginEnd="@dimen/auth_help_item_horizontal_margin"
android:text="@string/auth_help_option_other"
android:textSize="@dimen/auth_help_item_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/nextButton"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="@dimen/horizontal_separator_height"
android:layout_gravity="bottom"
android:background="@color/layout_weak" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<resources>
<string name="forgot_username_link">https://protonmail.com/username</string>
<string name="forgot_password_link">https://mail.protonmail.com/help/reset-login-password</string>
<string name="common_login_problems_link">https://protonmail.com/support/knowledge-base/common-login-problems</string>
<string name="contact_support_link">https://protonmail.com/support-form</string>
</resources>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<resources>
<dimen name="auth_parent_default_padding">@dimen/default_gap</dimen>
<dimen name="auth_content_top_margin">@dimen/default_gap</dimen>
<dimen name="auth_view_items_top_margin">8dp</dimen>
<dimen name="auth_subtitle_top_margin">8dp</dimen>
<dimen name="auth_content_separator_top_margin">44dp</dimen> <!-- this should be 48 maybe, as a double to 24 -->
<dimen name="auth_help_item_horizontal_margin">12dp</dimen>
<dimen name="auth_help_item_horizontal_padding">16dp</dimen>
<dimen name="auth_help_item_text_size">16sp</dimen>
<dimen name="horizontal_separator_height">1px</dimen>
<dimen name="auth_close_button_margin_start">12dp</dimen>
<dimen name="auth_close_button_margin_top">28dp</dimen>
<dimen name="auth_no_toolbar_top_margin">96dp</dimen>
</resources>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Proton Technologies AG
~ This file is part of Proton Technologies 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/>.
-->
<resources>
<string name="auth_password">Password</string>
<string name="auth_password_label">Password</string>
<string name="auth_email_username">Email or username</string>
<string name="auth_email_username_hint">example@protonmail.com</string>
<string name="auth_account_details">Enter your Proton Account details below</string>
<string name="auth_sign_in">Sign in</string>
<string name="auth_sign_in_action">Sign in</string>
<string name="auth_need_help">Need help?</string>
<string name="auth_login_assistive_text">Please enter your Proton email or username.</string>
<string name="auth_help_title">How can we help?</string>
<string name="auth_help_option_forgot_username">Forgot username</string>
<string name="auth_help_option_forgot_password">Forgot password</string>
<string name="auth_help_option_other">Other sign-in issues</string>
<string name="auth_help_other">Still need help? Contact us directly.</string>
<string name="auth_help_option_customer_support">Customer support</string>
<string name="auth_2fa_title">Two-factor authentication</string>
<string name="auth_2fa_label">Two-factor code</string>
<string name="auth_2fa_recovery_code_label">Recovery code</string>
<string name="auth_2fa_assistive_text">Enter the 6-digit code.</string>
<string name="auth_2fa_recovery_code_assistive_text">Enter a 8-character recovery code.</string>
<string name="auth_2fa_action">Authenticate</string>
<string name="auth_2fa_use_recovery_code">Use recovery code</string>
<string name="auth_2fa_use_2fa_code">Use two-factor code</string>
<string name="auth_2fa_error_empty_code">Two-factor code should not be empty</string>
<string name="auth_mailbox_title">Unlock your mailbox</string>
<string name="auth_mailbox_password">Mailbox password</string>
<string name="auth_mailbox_password_label">Mailbox password</string>
<string name="auth_mailbox_unlock_action">Unlock</string>
<string name="auth_forgot_password">Forgot password</string>
<string name="auth_login_general_error">An error occurred.</string>
<string name="auth_login_empty_credentials">Credentials should not be empty.</string>
<string name="auth_mailbox_empty_credentials">Credentials should not be empty.</string>
</resources>

View File

@ -0,0 +1,238 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.viewmodel
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import me.proton.core.auth.domain.AccountWorkflowHandler
import me.proton.core.auth.domain.crypto.SrpProofProvider
import me.proton.core.auth.domain.entity.Account
import me.proton.core.auth.domain.entity.SessionInfo
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.auth.domain.usecase.PerformLogin
import me.proton.core.network.domain.session.Session
import me.proton.core.test.android.ArchTest
import me.proton.core.test.kotlin.CoroutinesTest
import me.proton.core.test.kotlin.assertIs
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* @author Dino Kadrikj.
*/
@ExperimentalCoroutinesApi
class LoginViewModelTest : ArchTest, CoroutinesTest {
// region mocks
private val authRepository = mockk<AuthRepository>(relaxed = true)
private val srpProofProvider = mockk<SrpProofProvider>(relaxed = true)
private val accountManager = mockk<AccountWorkflowHandler>(relaxed = true)
private val useCase = mockk<PerformLogin>()
// endregion
// region test data
private val testUserName = "test-username"
private val testPassword = "test-password"
private val testSessionId = "test-session-id"
// endregion
private lateinit var viewModel: LoginViewModel
@Before
fun beforeEveryTest() {
viewModel = LoginViewModel(accountManager, useCase)
}
@Test
fun `login happy path flow states are handled correctly`() = coroutinesTest {
// GIVEN
val sessionInfo = mockk<SessionInfo>(relaxed = true)
every { sessionInfo.username } returns testUserName
coEvery { useCase.invoke(any(), any()) } returns flowOf(
PerformLogin.LoginState.Processing,
PerformLogin.LoginState.Success(sessionInfo)
)
val observer = mockk<(PerformLogin.LoginState) -> Unit>(relaxed = true)
viewModel.loginState.observeDataForever(observer)
// WHEN
viewModel.startLoginWorkflow(testUserName, testPassword.toByteArray())
// THEN
val arguments = mutableListOf<PerformLogin.LoginState>()
verify(exactly = 2) { observer(capture(arguments)) }
assertIs<PerformLogin.LoginState.Processing>(arguments[0])
val successState = arguments[1]
assertTrue(successState is PerformLogin.LoginState.Success)
assertEquals(sessionInfo, successState.sessionInfo)
assertEquals(testUserName, successState.sessionInfo.username)
}
@Test
fun `login error path flow states are handled correctly`() = coroutinesTest {
// GIVEN
val sessionInfo = mockk<SessionInfo>()
every { sessionInfo.username } returns testUserName
coEvery { useCase.invoke(any(), any()) } returns flowOf(
PerformLogin.LoginState.Processing,
PerformLogin.LoginState.Error.Message(message = "test error")
)
val observer = mockk<(PerformLogin.LoginState) -> Unit>(relaxed = true)
viewModel.loginState.observeDataForever(observer)
// WHEN
viewModel.startLoginWorkflow(testUserName, testPassword.toByteArray())
// THEN
val arguments = mutableListOf<PerformLogin.LoginState>()
verify(exactly = 2) { observer(capture(arguments)) }
assertIs<PerformLogin.LoginState.Processing>(arguments[0])
val errorState = arguments[1]
assertTrue(errorState is PerformLogin.LoginState.Error.Message)
assertEquals("test error", errorState.message)
}
@Test
fun `login empty username returns correct state`() = coroutinesTest {
// GIVEN
viewModel = LoginViewModel(
accountManager,
PerformLogin(authRepository, srpProofProvider, "test-client-secret")
)
val observer = mockk<(PerformLogin.LoginState) -> Unit>(relaxed = true)
viewModel.loginState.observeDataForever(observer)
// WHEN
viewModel.startLoginWorkflow("", testPassword.toByteArray())
// THEN
val arguments = mutableListOf<PerformLogin.LoginState>()
verify {
observer(capture(arguments))
}
val errorState = arguments[0]
assertTrue(errorState is PerformLogin.LoginState.Error.EmptyCredentials)
}
@Test
fun `login empty password returns correct state`() = coroutinesTest {
// GIVEN
viewModel = LoginViewModel(
accountManager,
PerformLogin(authRepository, srpProofProvider, "test-client-secret")
)
val observer = mockk<(PerformLogin.LoginState) -> Unit>(relaxed = true)
viewModel.loginState.observeDataForever(observer)
// WHEN
viewModel.startLoginWorkflow(testUserName, "".toByteArray())
// THEN
val arguments = mutableListOf<PerformLogin.LoginState>()
verify {
observer(capture(arguments))
}
val errorState = arguments[0]
assertTrue(errorState is PerformLogin.LoginState.Error.EmptyCredentials)
}
@Test
fun `login happy path dispatch account called`() = coroutinesTest {
// GIVEN
val testAccessToken = "test-access-token"
val sessionInfo = mockk<SessionInfo>(relaxed = true)
every { sessionInfo.username } returns testUserName
every { sessionInfo.accessToken } returns testAccessToken
coEvery { useCase.invoke(any(), any()) } returns flowOf(
PerformLogin.LoginState.Processing,
PerformLogin.LoginState.Success(sessionInfo)
)
val observer = mockk<(PerformLogin.LoginState) -> Unit>(relaxed = true)
viewModel.loginState.observeDataForever(observer)
// WHEN
viewModel.startLoginWorkflow(testUserName, testPassword.toByteArray())
// THEN
val arguments = mutableListOf<PerformLogin.LoginState>()
val accountArgument = slot<Account>()
val sessionArgument = slot<Session>()
verify(exactly = 2) { observer(capture(arguments)) }
coVerify(exactly = 1) { accountManager.handleSession(capture(accountArgument), capture(sessionArgument)) }
assertIs<PerformLogin.LoginState.Processing>(arguments[0])
val successState = arguments[1]
assertTrue(successState is PerformLogin.LoginState.Success)
assertEquals(sessionInfo, successState.sessionInfo)
assertEquals(testUserName, successState.sessionInfo.username)
val account = accountArgument.captured
val session = sessionArgument.captured
assertNotNull(account)
assertEquals(testUserName, account.username)
assertEquals(testAccessToken, session.accessToken)
}
@Test
fun `login happy path second factor needed`() = coroutinesTest {
// GIVEN
val sessionInfo = mockk<SessionInfo>(relaxed = true)
every { sessionInfo.isSecondFactorNeeded } returns true
every { sessionInfo.sessionId } returns testSessionId
coEvery { useCase.invoke(any(), any()) } returns flowOf(
PerformLogin.LoginState.Processing,
PerformLogin.LoginState.Success(sessionInfo)
)
val observer = mockk<(PerformLogin.LoginState) -> Unit>(relaxed = true)
viewModel.loginState.observeDataForever(observer)
// WHEN
viewModel.startLoginWorkflow(testUserName, testPassword.toByteArray())
// THEN
val accountArgument = slot<Account>()
val sessionArgument = slot<Session>()
coVerify(exactly = 1) { accountManager.handleSession(capture(accountArgument), capture(sessionArgument)) }
val account = accountArgument.captured
val session = sessionArgument.captured
assertNotNull(account)
assertNotNull(session)
assertEquals(testSessionId, session.sessionId.id)
}
@Test
fun `login happy path mailbox login needed`() = coroutinesTest {
// GIVEN
val sessionInfo = mockk<SessionInfo>(relaxed = true)
every { sessionInfo.isMailboxLoginNeeded } returns true
every { sessionInfo.sessionId } returns testSessionId
coEvery { useCase.invoke(any(), any()) } returns flowOf(
PerformLogin.LoginState.Processing,
PerformLogin.LoginState.Success(sessionInfo)
)
val observer = mockk<(PerformLogin.LoginState) -> Unit>(relaxed = true)
viewModel.loginState.observeDataForever(observer)
// WHEN
viewModel.startLoginWorkflow(testUserName, testPassword.toByteArray())
// THEN
val accountArgument = slot<Account>()
val sessionArgument = slot<Session>()
coVerify(exactly = 1) { accountManager.handleSession(capture(accountArgument), capture(sessionArgument)) }
val account = accountArgument.captured
val session = sessionArgument.captured
assertNotNull(account)
assertTrue(account.isMailboxLoginNeeded)
assertEquals(testSessionId, session.sessionId.id)
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.viewmodel
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import me.proton.core.auth.domain.AccountWorkflowHandler
import me.proton.core.auth.domain.crypto.CryptoProvider
import me.proton.core.auth.domain.entity.User
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.auth.domain.usecase.PerformMailboxLogin
import me.proton.core.network.domain.session.SessionId
import me.proton.core.test.android.ArchTest
import me.proton.core.test.kotlin.CoroutinesTest
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* @author Dino Kadrikj.
*/
class MailboxLoginViewModelTest : ArchTest, CoroutinesTest {
// region mocks
private val authRepository = mockk<AuthRepository>(relaxed = true)
private val cryptoProvider = mockk<CryptoProvider>(relaxed = true)
private val accountManager = mockk<AccountWorkflowHandler>(relaxed = true)
private val useCase = mockk<PerformMailboxLogin>()
private val testUser = mockk<User>(relaxed = true)
// endregion
// region test data
private val testSessionId = "test-session-id"
private val testPassword = "test-password"
private val testName = "test-name"
private val testEmail = "test-email"
// endregion
private lateinit var viewModel: MailboxLoginViewModel
@Before
fun beforeEveryTest() {
viewModel = MailboxLoginViewModel(accountManager, useCase)
}
@Test
fun `mailbox login happy flow states are handled correctly`() = coroutinesTest {
// GIVEN
every { testUser.name } returns testName
every { testUser.email } returns testEmail
coEvery { useCase.invoke(SessionId(testSessionId), testPassword.toByteArray()) } returns flowOf(
PerformMailboxLogin.MailboxLoginState.Processing,
PerformMailboxLogin.MailboxLoginState.Success(testUser)
)
val observer = mockk<(PerformMailboxLogin.MailboxLoginState) -> Unit>(relaxed = true)
viewModel.mailboxLoginState.observeDataForever(observer)
// WHEN
viewModel.startMailboxLoginFlow(SessionId(testSessionId), testPassword.toByteArray())
// THEN
val arguments = mutableListOf<PerformMailboxLogin.MailboxLoginState>()
verify(exactly = 2) { observer(capture(arguments)) }
val processingState = arguments[0]
val successState = arguments[1]
assertTrue(processingState is PerformMailboxLogin.MailboxLoginState.Processing)
assertTrue(successState is PerformMailboxLogin.MailboxLoginState.Success)
assertEquals(testUser, successState.user)
assertEquals(testName, successState.user.name)
assertEquals(testEmail, successState.user.email)
}
@Test
fun `mailbox login empty password returns correct state of events`() = coroutinesTest {
// GIVEN
viewModel =
MailboxLoginViewModel(accountManager, PerformMailboxLogin(authRepository, cryptoProvider))
val observer = mockk<(PerformMailboxLogin.MailboxLoginState) -> Unit>(relaxed = true)
viewModel.mailboxLoginState.observeDataForever(observer)
// WHEN
viewModel.startMailboxLoginFlow(SessionId(testSessionId), "".toByteArray())
// THEN
val arguments = slot<PerformMailboxLogin.MailboxLoginState>()
verify { observer(capture(arguments)) }
val argument = arguments.captured
assertTrue(argument is PerformMailboxLogin.MailboxLoginState.Error.EmptyCredentials)
}
@Test
fun `success mailbox login invokes success on account manager`() = coroutinesTest {
// GIVEN
coEvery { useCase.invoke(SessionId(testSessionId), testPassword.toByteArray()) } returns flowOf(
PerformMailboxLogin.MailboxLoginState.Processing,
PerformMailboxLogin.MailboxLoginState.Success(testUser)
)
// WHEN
viewModel.startMailboxLoginFlow(SessionId(testSessionId), testPassword.toByteArray())
// THEN
val arguments = slot<SessionId>()
coVerify(exactly = 1) { accountManager.handleTwoPassModeSuccess(capture(arguments)) }
coVerify(exactly = 0) { accountManager.handleTwoPassModeFailed(any()) }
assertEquals(testSessionId, arguments.captured.id)
}
@Test
fun `failed mailbox login invokes failed on account manager`() = coroutinesTest {
// GIVEN
coEvery { useCase.invoke(SessionId(testSessionId), testPassword.toByteArray()) } returns flowOf(
PerformMailboxLogin.MailboxLoginState.Processing,
PerformMailboxLogin.MailboxLoginState.Error.Message("test error")
)
// WHEN
viewModel.startMailboxLoginFlow(SessionId(testSessionId), testPassword.toByteArray())
// THEN
val arguments = slot<SessionId>()
coVerify(exactly = 1) { accountManager.handleTwoPassModeFailed(capture(arguments)) }
coVerify(exactly = 0) { accountManager.handleTwoPassModeSuccess(any()) }
assertEquals(testSessionId, arguments.captured.id)
}
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.presentation.viewmodel
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import me.proton.core.auth.domain.AccountWorkflowHandler
import me.proton.core.auth.domain.entity.ScopeInfo
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.auth.domain.usecase.PerformSecondFactor
import me.proton.core.network.domain.session.SessionId
import me.proton.core.test.android.ArchTest
import me.proton.core.test.kotlin.CoroutinesTest
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* @author Dino Kadrikj.
*/
class SecondFactorViewModelTest : ArchTest, CoroutinesTest {
// region mocks
private val authRepository = mockk<AuthRepository>(relaxed = true)
private val accountManager = mockk<AccountWorkflowHandler>(relaxed = true)
private val useCase = mockk<PerformSecondFactor>()
private lateinit var viewModel: SecondFactorViewModel
private val testScopeInfo = mockk<ScopeInfo>(relaxed = true)
// endregion
// region test data
private val testSessionId = "test-session-id"
private val testSecondFactorCode = "123456"
// endregion
@Before
fun beforeEveryTest() {
viewModel = SecondFactorViewModel(accountManager, useCase)
}
@Test
fun `submit 2fa happy flow states are handled correctly`() = coroutinesTest {
// GIVEN
val isMailboxLoginNeeded = false
coEvery { useCase.invoke(SessionId(testSessionId), testSecondFactorCode) } returns flowOf(
PerformSecondFactor.SecondFactorState.Processing,
PerformSecondFactor.SecondFactorState.Success(SessionId(testSessionId), testScopeInfo)
)
val observer = mockk<(PerformSecondFactor.SecondFactorState) -> Unit>(relaxed = true)
viewModel.secondFactorState.observeDataForever(observer)
// WHEN
viewModel.startSecondFactorFlow(SessionId(testSessionId), testSecondFactorCode, isMailboxLoginNeeded)
// THEN
val arguments = mutableListOf<PerformSecondFactor.SecondFactorState>()
verify(exactly = 2) { observer(capture(arguments)) }
val processingState = arguments[0]
val successState = arguments[1]
assertTrue(processingState is PerformSecondFactor.SecondFactorState.Processing)
assertTrue(successState is PerformSecondFactor.SecondFactorState.Success)
}
@Test
fun `submit empty 2fa states flow are handled correctly`() = coroutinesTest {
// GIVEN
val isMailboxLoginNeeded = false
viewModel = SecondFactorViewModel(accountManager, PerformSecondFactor(authRepository))
val observer = mockk<(PerformSecondFactor.SecondFactorState) -> Unit>(relaxed = true)
viewModel.secondFactorState.observeDataForever(observer)
// WHEN
viewModel.startSecondFactorFlow(SessionId(testSessionId), "", isMailboxLoginNeeded)
// THEN
val arguments = slot<PerformSecondFactor.SecondFactorState>()
verify { observer(capture(arguments)) }
val argument = arguments.captured
assertTrue(argument is PerformSecondFactor.SecondFactorState.Error.EmptyCredentials)
}
@Test
fun `submit 2fa single pass mode flow states are handled correctly`() = coroutinesTest {
// GIVEN
val isMailboxLoginNeeded = false
coEvery { useCase.invoke(SessionId(testSessionId), testSecondFactorCode) } returns flowOf(
PerformSecondFactor.SecondFactorState.Processing,
PerformSecondFactor.SecondFactorState.Success(SessionId(testSessionId), testScopeInfo)
)
val observer = mockk<(PerformSecondFactor.SecondFactorState) -> Unit>(relaxed = true)
viewModel.secondFactorState.observeDataForever(observer)
// WHEN
viewModel.startSecondFactorFlow(SessionId(testSessionId), testSecondFactorCode, isMailboxLoginNeeded)
// THEN
val arguments = mutableListOf<PerformSecondFactor.SecondFactorState>()
val accountManagerArguments = slot<SessionId>()
verify(exactly = 2) { observer(capture(arguments)) }
coVerify(exactly = 1) { accountManager.handleSecondFactorSuccess(capture(accountManagerArguments), any()) }
coVerify(exactly = 0) { accountManager.handleSecondFactorFailed(any()) }
val processingState = arguments[0]
val successState = arguments[1]
assertTrue(processingState is PerformSecondFactor.SecondFactorState.Processing)
assertTrue(successState is PerformSecondFactor.SecondFactorState.Success)
assertFalse(successState.isMailboxLoginNeeded)
assertEquals(SessionId(testSessionId), accountManagerArguments.captured)
}
@Test
fun `submit 2fa two pass mode flow states are handled correctly`() = coroutinesTest {
// GIVEN
val isMailboxLoginNeeded = true
coEvery { useCase.invoke(SessionId(testSessionId), testSecondFactorCode) } returns flowOf(
PerformSecondFactor.SecondFactorState.Processing,
PerformSecondFactor.SecondFactorState.Success(SessionId(testSessionId), testScopeInfo)
)
val observer = mockk<(PerformSecondFactor.SecondFactorState) -> Unit>(relaxed = true)
viewModel.secondFactorState.observeDataForever(observer)
// WHEN
viewModel.startSecondFactorFlow(SessionId(testSessionId), testSecondFactorCode, isMailboxLoginNeeded)
// THEN
val arguments = mutableListOf<PerformSecondFactor.SecondFactorState>()
val accountManagerArguments = slot<SessionId>()
verify(exactly = 2) { observer(capture(arguments)) }
coVerify(exactly = 1) { accountManager.handleSecondFactorSuccess(capture(accountManagerArguments), any()) }
coVerify(exactly = 0) { accountManager.handleSecondFactorFailed(any()) }
val processingState = arguments[0]
val successState = arguments[1]
assertTrue(processingState is PerformSecondFactor.SecondFactorState.Processing)
assertTrue(successState is PerformSecondFactor.SecondFactorState.Success)
assertTrue(successState.isMailboxLoginNeeded)
assertEquals(SessionId(testSessionId), accountManagerArguments.captured)
}
}

View File

@ -33,7 +33,11 @@
android:theme="@style/ProtonTheme.NoToolbar" />
<activity
android:name=".presentation.ui.TwoFactorActivity"
android:name=".presentation.ui.SecondFactorActivity"
android:theme="@style/ProtonTheme.NoToolbar" />
<activity
android:name=".presentation.ui.MailboxLoginActivity"
android:theme="@style/ProtonTheme.NoToolbar" />
</application>

View File

@ -38,6 +38,7 @@ dependencies {
// features
project(Module.humanVerification),
project(Module.auth),
project(Module.domain),
`kotlin-jdk7`,
`coroutines-android`,

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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.android.core.coreexample
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import me.proton.android.core.coreexample.api.CoreExampleApiClient
import me.proton.core.auth.domain.crypto.CryptoProvider
import me.proton.core.auth.domain.crypto.SrpProofProvider
import me.proton.core.auth.presentation.srp.CryptoProviderImpl
import me.proton.core.auth.presentation.srp.SrpProofProviderImpl
import me.proton.core.network.domain.ApiClient
/**
* @author Dino Kadrikj.
*/
@Module
@InstallIn(ApplicationComponent::class)
abstract class ApplicationBindsModule {
@Binds
abstract fun provideSrpProofProvider(srpProofProviderImpl: SrpProofProviderImpl): SrpProofProvider
@Binds
abstract fun provideApiClient(coreExampleApiClient: CoreExampleApiClient): ApiClient
@Binds
abstract fun provideCryptoProvider(cryptoProviderImpl: CryptoProviderImpl): CryptoProvider
}

View File

@ -28,7 +28,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import me.proton.android.core.coreexample.Constants.BASE_URL
import me.proton.android.core.coreexample.api.CoreExampleApiClient
import me.proton.core.auth.domain.AccountWorkflowHandler
import me.proton.core.auth.domain.ClientSecret
import me.proton.core.auth.domain.entity.Account
import me.proton.core.auth.domain.entity.KeySalts
import me.proton.core.auth.domain.entity.LoginInfo
import me.proton.core.auth.domain.entity.ScopeInfo
import me.proton.core.auth.domain.entity.SecondFactorProof
import me.proton.core.auth.domain.entity.SessionInfo
import me.proton.core.auth.domain.entity.User
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.domain.arch.DataResult
import me.proton.core.humanverification.data.repository.HumanVerificationLocalRepositoryImpl
import me.proton.core.humanverification.data.repository.HumanVerificationRemoteRepositoryImpl
import me.proton.core.humanverification.domain.repository.HumanVerificationLocalRepository
@ -45,6 +55,7 @@ import me.proton.core.network.domain.session.Session
import me.proton.core.network.domain.session.SessionId
import me.proton.core.network.domain.session.SessionListener
import me.proton.core.network.domain.session.SessionProvider
import me.proton.core.util.kotlin.DispatcherProvider
import javax.inject.Singleton
/**
@ -64,11 +75,6 @@ object ApplicationModule {
fun provideNetworkPrefs(@ApplicationContext context: Context) =
NetworkPrefs(context)
@Provides
@Singleton
fun provideApiClient(): ApiClient =
CoreExampleApiClient()
@Provides
@Singleton
fun provideApiFactory(
@ -82,11 +88,6 @@ object ApplicationModule {
CoroutineScope(Job() + Dispatchers.Default)
)
@Provides
@Singleton
fun provideApiProvider(apiFactory: ApiFactory): ApiProvider =
ApiProvider(apiFactory)
@Provides
fun provideSessionProvider(): SessionProvider = object : SessionProvider {
override fun getSession(sessionId: SessionId): Session? {
@ -112,6 +113,66 @@ object ApplicationModule {
}
}
@Provides
fun provideAuthRepository(apiProvider: ApiProvider): AuthRepository = object : AuthRepository {
/**
* Get Login Info needed to start the login process.
*/
override suspend fun getLoginInfo(username: String, clientSecret: String): DataResult<LoginInfo> {
TODO("Not yet implemented")
}
/**
* Perform Login to create a session (accessToken, refreshToken, sessionId, ...).
*/
override suspend fun performLogin(
username: String,
clientSecret: String,
clientEphemeral: String,
clientProof: String,
srpSession: String
): DataResult<SessionInfo> {
TODO("Not yet implemented")
}
/**
* Perform Two Factor for the Login process for a given [SessionId].
*/
override suspend fun performSecondFactor(
sessionId: SessionId,
secondFactorProof: SecondFactorProof
): DataResult<ScopeInfo> {
TODO("Not yet implemented")
}
/**
* Returns the basic user information for a given [SessionId].
*/
override suspend fun getUser(sessionId: SessionId): DataResult<User> {
TODO("Not yet implemented")
}
/**
* Returns the key salt information for a given [SessionId].
*/
override suspend fun getSalts(sessionId: SessionId): DataResult<KeySalts> {
TODO("Not yet implemented")
}
/**
* Perform Two Factor for the Login process for a given [SessionId].
*/
override suspend fun revokeSession(sessionId: SessionId): DataResult<Boolean> {
TODO("Not yet implemented")
}
}
@Provides
@Singleton
fun provideApiProvider(apiFactory: ApiFactory): ApiProvider =
ApiProvider(apiFactory)
@Provides
fun provideLocalRepository(@ApplicationContext context: Context): HumanVerificationLocalRepository =
HumanVerificationLocalRepositoryImpl(context)
@ -119,4 +180,81 @@ object ApplicationModule {
@Provides
fun provideRemoteRepository(apiProvider: ApiProvider): HumanVerificationRemoteRepository =
HumanVerificationRemoteRepositoryImpl(apiProvider)
@Provides
@ClientSecret
fun provideClientSecret(): String = ""
@Provides
fun provideDispatcherProvider() = object : DispatcherProvider {
override val Io = Dispatchers.IO
override val Comp = Dispatchers.Default
override val Main = Dispatchers.Main
}
@Provides
@Singleton
fun provideAccountWorkflowHandler(): AccountWorkflowHandler = object : AccountWorkflowHandler {
/**
* Handle a new [Session] for a new or existing [Account] from Login workflow.
*/
override suspend fun handleSession(account: Account, session: Session) {
TODO("Not yet implemented")
}
/**
* Handle TwoPassMode success.
*/
override suspend fun handleTwoPassModeSuccess(sessionId: SessionId) {
TODO("Not yet implemented")
}
/**
* Handle TwoPassMode failure.
*
* Note: The Workflow must succeed within maximum 10 min of authentication.
*/
override suspend fun handleTwoPassModeFailed(sessionId: SessionId) {
TODO("Not yet implemented")
}
/**
* Handle SecondFactor success.
*
* @param updatedScopes the new updated full list of scopes.
*/
override suspend fun handleSecondFactorSuccess(sessionId: SessionId, updatedScopes: List<String>) {
TODO("Not yet implemented")
}
/**
* Handle SecondFactor failure.
*
* Note: Maximum number of failure is 3, then the session will be invalidated and API will return HTTP 401.
*/
override suspend fun handleSecondFactorFailed(sessionId: SessionId) {
TODO("Not yet implemented")
}
/**
* Handle HumanVerification success.
*
* Note: TokenType and tokenCode must be part of the next API calls.
*/
override suspend fun handleHumanVerificationSuccess(
sessionId: SessionId,
tokenType: String,
tokenCode: String
) {
TODO("Not yet implemented")
}
/**
* Handle HumanVerification failure.
*/
override suspend fun handleHumanVerificationFailed(sessionId: SessionId) {
TODO("Not yet implemented")
}
}
}

View File

@ -21,13 +21,14 @@ package me.proton.android.core.coreexample.api
import android.os.Build
import me.proton.core.network.domain.ApiClient
import java.util.Locale
import javax.inject.Inject
/**
* @author Dino Kadrikj.
*/
const val VERSION_NAME = "0.0.1"
class CoreExampleApiClient : ApiClient {
class CoreExampleApiClient @Inject constructor() : ApiClient {
/**
* Tells the lib if DoH should be used in a given moment (based e.g. on user setting or whether
* VPN connection is active). Will be checked before each API call.

2
gopenpgp/build.gradle Normal file
View File

@ -0,0 +1,2 @@
configurations.maybeCreate("default")
artifacts.add("default", file('gopenpgp.aar'))

BIN
gopenpgp/gopenpgp.aar Normal file

Binary file not shown.

View File

@ -50,6 +50,7 @@ fun org.gradle.api.Project.android(
// SDK
minSdkVersion(minSdk)
targetSdkVersion(targetSdk)
ndkVersion = "20.0.5594570"
// Other
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -104,6 +105,10 @@ fun org.gradle.api.Project.android(
exclude("org/codehaus/plexus/*.xml")
exclude("org/cyberneko/html/res/*.txt")
exclude("org/cyberneko/html/res/*.properties")
pickFirst("lib/armeabi-v7a/libgojni.so")
pickFirst("lib/arm64-v8a/libgojni.so")
pickFirst("lib/x86/libgojni.so")
pickFirst("lib/x86_64/libgojni.so")
}
apply(config)

View File

@ -39,6 +39,7 @@ object Module {
const val domain = ":domain"
const val presentation = ":presentation"
const val data = ":data"
const val gopenpgp = ":gopenpgp"
// endregion
// region Support

View File

@ -22,7 +22,7 @@ import android.os.Bundle
import android.view.View
import me.proton.android.core.presentation.ui.ProtonDialogFragment
import me.proton.android.core.presentation.utils.onClick
import me.proton.android.core.presentation.utils.openLinkInBrowser
import me.proton.android.core.presentation.utils.openBrowserLink
import me.proton.core.humanverification.presentation.R
import me.proton.core.humanverification.presentation.databinding.FragmentHumanVerificationHelpBinding
@ -43,10 +43,10 @@ class HumanVerificationHelpFragment :
headerNavigation.helpButton.visibility = View.GONE
headerNavigation.optionsTitle.text = getString(R.string.human_verification_help)
verificationManual.manualVerificationLayout.onClick {
requireContext().openLinkInBrowser(getString(R.string.manual_verification_link))
requireContext().openBrowserLink(getString(R.string.manual_verification_link))
}
verificationHelp.helpLayout.onClick {
requireContext().openLinkInBrowser(getString(R.string.verification_help_link))
requireContext().openBrowserLink(getString(R.string.verification_help_link))
}
}
}

View File

@ -25,7 +25,7 @@ plugins {
kotlin("plugin.serialization")
}
libVersion = Version(0, 4, 0)
libVersion = Version(0, 4, 1)
android()
@ -46,7 +46,8 @@ dependencies {
`appcompat`,
`constraint-layout`,
`fragment`,
`material`
`material`,
`viewStateStore`
)
// Android

View File

@ -22,15 +22,16 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import studio.forface.viewstatestore.ViewStateActivity
/**
* Base Proton Activity from which all project activities should extend.
*
* @author Dino Kadrikj.
*/
abstract class ProtonActivity<DB : ViewDataBinding> : AppCompatActivity() {
abstract class ProtonActivity<DB : ViewDataBinding> : AppCompatActivity(), ViewStateActivity {
protected lateinit var binding: DB
lateinit var binding: DB
protected abstract fun layoutId(): Int

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2020 Proton Technologies AG
* This file is part of Proton Technologies 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/>.
*/
@file:JvmName("TextUtils")
package me.proton.android.core.presentation.utils
import android.content.Context
import android.view.Gravity
import android.widget.Toast
import androidx.annotation.StringRes
/*
* A file containing extensions for Text
* Author: Davide Farella
*/
private const val DEFAULT_TOAST_LENGTH = Toast.LENGTH_LONG
private const val DEFAULT_TOAST_GRAVITY = Gravity.BOTTOM
/**
* An extension for show a [Toast] within a [Context]
* @param messageRes [StringRes] of message to show
* @param length [Int] length of the [Toast]. Default is [DEFAULT_TOAST_LENGTH]
* @param gravity [Int] gravity for the [Toast]. Default is [DEFAULT_TOAST_GRAVITY]
*/
@JvmOverloads
fun Context.showToast(
@StringRes messageRes: Int,
length: Int = DEFAULT_TOAST_LENGTH,
gravity: Int = DEFAULT_TOAST_GRAVITY
) {
@Suppress("SENSELESS_COMPARISON") // It could be `null` if called from Java
if (this != null) {
Toast.makeText(this, messageRes, length).apply {
setGravity(gravity, 0, 0)
}.show()
}
}
/**
* An extension for show a [Toast] within a [Context]
* @param message [CharSequence] message to show
* @param length [Int] length of the [Toast]. Default is [DEFAULT_TOAST_LENGTH]
* @param gravity [Int] gravity for the [Toast]. Default is [DEFAULT_TOAST_GRAVITY]
*/
@JvmOverloads
fun Context.showToast(
message: CharSequence,
length: Int = DEFAULT_TOAST_LENGTH,
gravity: Int = DEFAULT_TOAST_GRAVITY
) {
@Suppress("SENSELESS_COMPARISON") // It could be `null` if called from Java
if (this != null) {
Toast.makeText(this, message, length).apply {
setGravity(gravity, 0, 0)
}.show()
}
}

View File

@ -18,36 +18,48 @@
package me.proton.android.core.presentation.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.snackbar.Snackbar
import me.proton.android.core.presentation.R
/**
* @author Dino Kadrikj.
*/
inline fun FragmentManager.inTransaction(block: FragmentTransaction.() -> FragmentTransaction) {
val transaction = beginTransaction()
transaction.block()
transaction.commit()
}
fun Context.openLinkInBrowser(link: String) {
fun Context.openBrowserLink(link: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
intent.resolveActivity(packageManager)?.let {
startActivity(intent)
} ?: Toast.makeText(
this,
getString(R.string.presentation_browser_missing),
Toast.LENGTH_SHORT
).show()
} ?: run {
Toast.makeText(
this,
getString(R.string.presentation_browser_missing),
Toast.LENGTH_SHORT
).show()
}
}
fun AppCompatActivity.hideKeyboard() {
hideKeyboard(currentFocus ?: window.decorView.rootView)
}
fun Context.hideKeyboard(view: View) {
val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
}

View File

@ -119,11 +119,20 @@ fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = false):
/**
* Shows red error snack bar. Usually as a general way to display various errors to the user.
*
* @param messageRes the String resource error message id
*/
fun View.errorSnack(@StringRes messageRes: Int) {
snack(messageRes = messageRes, color = R.drawable.background_error)
}
/**
* Shows red error snack bar. Usually as a general way to display various errors to the user.
*/
fun View.errorSnack(message: String) {
snack(message = message, color = R.drawable.background_error)
}
/**
* Shows green success snack bar. Usually as a general way to display success result of an operation to the user.
*/
@ -134,6 +143,8 @@ fun View.successSnack(@StringRes messageRes: Int) {
/**
* General snack bar util function which takes message and color as config.
* The default showing length is [Snackbar.LENGTH_LONG].
*
* @param messageRes the String resource message id
*/
fun View.snack(
@StringRes messageRes: Int,
@ -144,6 +155,9 @@ fun View.snack(
/**
* General snack bar util function which takes message, color and length as config.
* The default showing length is [Snackbar.LENGTH_LONG].
*
* @param message the message as String
*/
fun View.snack(
message: String,

View File

@ -19,6 +19,8 @@
package me.proton.android.core.presentation.viewmodel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
/**
* TBD.