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:
parent
c5dfb54420
commit
5f7d8f4f4c
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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?)
|
||||
}
|
|
@ -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>)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
// features
|
||||
project(Module.humanVerification),
|
||||
project(Module.auth),
|
||||
project(Module.domain),
|
||||
|
||||
`kotlin-jdk7`,
|
||||
`coroutines-android`,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
configurations.maybeCreate("default")
|
||||
artifacts.add("default", file('gopenpgp.aar'))
|
Binary file not shown.
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue