mirror of
https://github.com/ProtonMail/protoncore_android.git
synced 2026-05-15 09:50:41 +00:00
feat(auth): Add SSO confirmation screen.
This commit is contained in:
@@ -29,12 +29,14 @@ import me.proton.core.auth.data.usecase.IsCommonPasswordCheckEnabledImpl
|
||||
import me.proton.core.auth.data.MissingScopeListenerImpl
|
||||
import me.proton.core.auth.data.feature.IsFido2EnabledImpl
|
||||
import me.proton.core.auth.data.repository.AuthRepositoryImpl
|
||||
import me.proton.core.auth.data.repository.DeviceSecretRepositoryImpl
|
||||
import me.proton.core.auth.data.usecase.IsCredentialLessEnabledImpl
|
||||
import me.proton.core.auth.data.usecase.IsSsoCustomTabEnabledImpl
|
||||
import me.proton.core.auth.data.usecase.IsSsoEnabledImpl
|
||||
import me.proton.core.auth.domain.IsCommonPasswordCheckEnabled
|
||||
import me.proton.core.auth.domain.feature.IsFido2Enabled
|
||||
import me.proton.core.auth.domain.repository.AuthRepository
|
||||
import me.proton.core.auth.domain.repository.DeviceSecretRepository
|
||||
import me.proton.core.auth.domain.usecase.IsCredentialLessEnabled
|
||||
import me.proton.core.auth.domain.usecase.IsSsoCustomTabEnabled
|
||||
import me.proton.core.auth.domain.usecase.IsSsoEnabled
|
||||
@@ -52,6 +54,9 @@ public interface CoreAuthModule {
|
||||
@Singleton
|
||||
public fun provideMissingScopeListener(impl: MissingScopeListenerImpl): MissingScopeListener
|
||||
|
||||
@Binds
|
||||
public fun bindDeviceSecretRepository(impl: DeviceSecretRepositoryImpl): DeviceSecretRepository
|
||||
|
||||
public companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2018 Chris Campo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.domain.usecase.sso
|
||||
|
||||
import kotlin.experimental.or
|
||||
|
||||
const val ALPHABET: String = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
val base32Lookup: IntArray = intArrayOf(
|
||||
0xFF, 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
|
||||
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
|
||||
0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
|
||||
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
|
||||
0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
||||
)
|
||||
|
||||
fun encode(bytes: ByteArray): String {
|
||||
var i = 0
|
||||
var index = 0
|
||||
var digit: Int
|
||||
var currByte: Int
|
||||
var nextByte: Int
|
||||
val base32 = StringBuffer((bytes.size + 7) * 8 / 5)
|
||||
|
||||
while (i < bytes.size) {
|
||||
currByte = if (bytes[i] >= 0) bytes[i].toInt() else bytes[i] + 256
|
||||
|
||||
// Is the current digit going to span a byte boundary?
|
||||
if (index > 3) {
|
||||
nextByte = if (i + 1 < bytes.size) {
|
||||
if (bytes[i + 1] >= 0) bytes[i + 1].toInt() else bytes[i + 1] + 256
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
digit = currByte and (0xFF shr index)
|
||||
index = (index + 5) % 8
|
||||
digit = digit shl index
|
||||
digit = digit or (nextByte shr 8 - index)
|
||||
i++
|
||||
} else {
|
||||
digit = currByte shr 8 - (index + 5) and 0x1F
|
||||
index = (index + 5) % 8
|
||||
if (index == 0)
|
||||
i++
|
||||
}
|
||||
base32.append(ALPHABET[digit])
|
||||
}
|
||||
|
||||
return base32.toString()
|
||||
}
|
||||
|
||||
fun decode(base32: String): ByteArray {
|
||||
var i = 0
|
||||
var index = 0
|
||||
var lookup: Int
|
||||
var offset = 0
|
||||
var digit: Int
|
||||
val bytes = ByteArray(base32.length * 5 / 8)
|
||||
|
||||
while (i < base32.length) {
|
||||
lookup = base32[i] - '0'
|
||||
|
||||
// Skip chars outside the lookup table
|
||||
if (lookup < 0 || lookup >= base32Lookup.size) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
digit = base32Lookup[lookup]
|
||||
|
||||
// If this digit is not in the table, ignore it
|
||||
if (digit == 0xFF) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (index <= 3) {
|
||||
index = (index + 5) % 8
|
||||
if (index == 0) {
|
||||
bytes[offset] = bytes[offset] or digit.toByte()
|
||||
offset++
|
||||
if (offset >= bytes.size) break
|
||||
} else {
|
||||
bytes[offset] = bytes[offset] or (digit shl 8 - index).toByte()
|
||||
}
|
||||
} else {
|
||||
index = (index + 5) % 8
|
||||
bytes[offset] = bytes[offset] or digit.ushr(index).toByte()
|
||||
offset++
|
||||
|
||||
if (offset >= bytes.size) break
|
||||
bytes[offset] = bytes[offset] or (digit shl 8 - index).toByte()
|
||||
}
|
||||
i++
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.domain.usecase.sso
|
||||
|
||||
import me.proton.core.auth.domain.repository.DeviceSecretRepository
|
||||
import me.proton.core.crypto.common.context.CryptoContext
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.util.kotlin.HashUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
class GenerateConfirmationCode @Inject constructor(
|
||||
private val deviceSecretRepository: DeviceSecretRepository,
|
||||
private val context: CryptoContext
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
userId: UserId
|
||||
): String {
|
||||
val deviceSecret =
|
||||
deviceSecretRepository.getByUserId(userId) ?: throw IllegalStateException("Device Secret not found.")
|
||||
val decryptedDeviceSecret = context.keyStoreCrypto.decrypt(deviceSecret.secret)
|
||||
val sha256DeviceSecret = HashUtils.sha256(decryptedDeviceSecret)
|
||||
return encode(sha256DeviceSecret.toByteArray()).take(4)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.domain.usecase.sso
|
||||
|
||||
import me.proton.core.auth.domain.repository.DeviceSecretRepository
|
||||
import me.proton.core.crypto.common.context.CryptoContext
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.util.kotlin.HashUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
class ValidateConfirmationCode @Inject constructor(
|
||||
private val deviceSecretRepository: DeviceSecretRepository,
|
||||
private val context: CryptoContext
|
||||
) {
|
||||
|
||||
sealed interface Result {
|
||||
object NoDeviceSecret : Result
|
||||
object ConfirmationCodeInputError : Result
|
||||
object ConfirmationCodeInvalid : Result
|
||||
object ConfirmationCodeValid : Result
|
||||
}
|
||||
|
||||
suspend operator fun invoke(
|
||||
userId: UserId,
|
||||
confirmationCode: String
|
||||
): Result {
|
||||
if (confirmationCode.isEmpty() or (confirmationCode.length != 4)) return Result.ConfirmationCodeInputError
|
||||
val deviceSecret = deviceSecretRepository.getByUserId(userId) ?: return Result.NoDeviceSecret
|
||||
val decryptedDeviceSecret = context.keyStoreCrypto.decrypt(deviceSecret.secret)
|
||||
val sha256DeviceSecret = HashUtils.sha256(decryptedDeviceSecret)
|
||||
return if (encode(sha256DeviceSecret.toByteArray()).take(4) == confirmationCode) Result.ConfirmationCodeValid
|
||||
else Result.ConfirmationCodeInvalid
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
public data class AvailableDeviceUIModel(
|
||||
val id: String,
|
||||
val authDeviceName: String,
|
||||
val localizedClientName: String,
|
||||
val lastActivityTime: Long,
|
||||
val clientType: ClientType,
|
||||
var lastActivityReadable: String? = null
|
||||
)
|
||||
|
||||
public enum class ClientType {
|
||||
Web, Android, iOS
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.proton.core.auth.presentation.compose.R
|
||||
import me.proton.core.compose.component.DeferredCircularProgressIndicator
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTypography
|
||||
import me.proton.core.compose.theme.defaultNorm
|
||||
import me.proton.core.compose.theme.defaultSmallWeak
|
||||
|
||||
internal const val CONFIRMATION_CODE_FIELD_TAG = "CONFIRMATION_CODE_FIELD_TAG"
|
||||
|
||||
@Composable
|
||||
internal fun AvailableDevicesList(
|
||||
modifier: Modifier = Modifier,
|
||||
devices: List<AvailableDeviceUIModel>?
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
horizontal = ProtonDimens.DefaultSpacing,
|
||||
vertical = ProtonDimens.DefaultSpacing
|
||||
)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = ProtonDimens.MediumSpacing),
|
||||
text = stringResource(id = R.string.auth_login_devices_available),
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
}
|
||||
if (devices == null) {
|
||||
item {
|
||||
DeferredCircularProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
} else if (devices.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = ProtonDimens.MediumSpacing),
|
||||
text = stringResource(id = R.string.auth_login_no_devices_available),
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(devices, { device -> device.id }) { device ->
|
||||
AvailableDeviceRow(device = device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun AvailableDeviceRow(
|
||||
device: AvailableDeviceUIModel
|
||||
) {
|
||||
ListItem(
|
||||
icon = {
|
||||
val iconPainterResource = when (device.clientType) {
|
||||
ClientType.Web -> painterResource(id = R.drawable.ic_proton_tv)
|
||||
ClientType.Android,
|
||||
ClientType.iOS -> painterResource(id = R.drawable.ic_proton_mobile)
|
||||
}
|
||||
Icon(
|
||||
painter = iconPainterResource,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = device.authDeviceName,
|
||||
style = ProtonTypography.Default.defaultNorm
|
||||
)
|
||||
},
|
||||
secondaryText = {
|
||||
Column {
|
||||
Text(
|
||||
text = device.localizedClientName,
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.auth_login_device_last_used,
|
||||
device.lastActivityReadable ?: stringResource(R.string.auth_login_not_available)
|
||||
),
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.proton.core.auth.presentation.compose.R
|
||||
import me.proton.core.auth.presentation.compose.SMALL_SCREEN_HEIGHT
|
||||
import me.proton.core.compose.component.DeferredCircularProgressIndicator
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
|
||||
@Composable
|
||||
internal fun ConfirmationDigits(
|
||||
modifier: Modifier = Modifier,
|
||||
digits: List<Char>?,
|
||||
@StringRes titleText: Int = R.string.auth_login_confirmation_code
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(ProtonDimens.SmallSpacing))
|
||||
.border(BorderStroke(1.dp, ProtonTheme.colors.separatorNorm))
|
||||
.padding(ProtonDimens.DefaultSpacing)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = ProtonDimens.DefaultSpacing)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = stringResource(id = titleText),
|
||||
style = ProtonTheme.typography.body2Regular
|
||||
)
|
||||
if (digits == null) {
|
||||
DeferredCircularProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
for (digit in digits) {
|
||||
ConfirmationCodeDigit(digit = digit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmationCodeDigit(
|
||||
digit: Char
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.padding(ProtonDimens.ExtraSmallSpacing)
|
||||
.size(height = 52.dp, width = 41.dp)
|
||||
.clip(RoundedCornerShape(ProtonDimens.SmallSpacing))
|
||||
.background(ProtonTheme.colors.backgroundSecondary)
|
||||
) {
|
||||
Text(
|
||||
text = digit.toString(),
|
||||
style = ProtonTheme.typography.hero
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", showBackground = true)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT)
|
||||
@Preview(name = "Foldable", device = Devices.FOLDABLE)
|
||||
@Preview(name = "Tablet", device = Devices.PIXEL_C)
|
||||
@Preview(name = "Horizontal", widthDp = 800, heightDp = 360)
|
||||
@Composable
|
||||
internal fun ConfirmationDigitsPreview() {
|
||||
ProtonTheme {
|
||||
ConfirmationDigits(digits = listOf('6', '4', 'S', '3'))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", showBackground = true)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT)
|
||||
@Preview(name = "Foldable", device = Devices.FOLDABLE)
|
||||
@Preview(name = "Tablet", device = Devices.PIXEL_C)
|
||||
@Preview(name = "Horizontal", widthDp = 800, heightDp = 360)
|
||||
@Composable
|
||||
internal fun ConfirmationDigitsLoadingPreview() {
|
||||
ProtonTheme {
|
||||
ConfirmationDigits(digits = null)
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.core.auth.presentation.compose.R
|
||||
import me.proton.core.auth.presentation.compose.SMALL_SCREEN_HEIGHT
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.ProtonTextButton
|
||||
import me.proton.core.compose.component.appbar.ProtonTopAppBar
|
||||
import me.proton.core.compose.theme.LocalColors
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.theme.ProtonTypography
|
||||
import me.proton.core.compose.theme.defaultSmallWeak
|
||||
|
||||
@Composable
|
||||
public fun ShareConfirmationCodeWithAdminScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onClose: () -> Unit = {},
|
||||
onErrorMessage: (String?) -> Unit,
|
||||
viewModel: ShareConfirmationCodeWithAdminViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
ShareConfirmationCodeWithAdminScreen(
|
||||
modifier = modifier,
|
||||
onClose = onClose,
|
||||
onCloseClicked = { viewModel.submit(ShareConfirmationCodeAction.Close) },
|
||||
onErrorMessage = onErrorMessage,
|
||||
onCancelClicked = { viewModel.submit(it) },
|
||||
onUseBackUpClicked = { viewModel.submit(it) },
|
||||
state = state
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun ShareConfirmationCodeWithAdminScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onClose: () -> Unit = {},
|
||||
onCloseClicked: () -> Unit = {},
|
||||
onErrorMessage: (String?) -> Unit,
|
||||
onCancelClicked: (ShareConfirmationCodeAction.Cancel) -> Unit = {},
|
||||
onUseBackUpClicked: (ShareConfirmationCodeAction.UseBackUpPassword) -> Unit = {},
|
||||
state: ShareConfirmationCodeWithAdminState
|
||||
) {
|
||||
when (state) {
|
||||
is ShareConfirmationCodeWithAdminState.DataLoaded -> {
|
||||
ShareConfirmationCodeWithAdminScreen(
|
||||
modifier = modifier,
|
||||
onCloseClicked = onCloseClicked,
|
||||
onCancelClicked = onCancelClicked,
|
||||
onUseBackUpClicked = onUseBackUpClicked,
|
||||
username = state.username,
|
||||
confirmationCode = state.confirmationCode.toCharArray().asList()
|
||||
)
|
||||
}
|
||||
is ShareConfirmationCodeWithAdminState.Loading -> {
|
||||
ShareConfirmationCodeWithAdminScreen(
|
||||
modifier = modifier,
|
||||
onCloseClicked = onCloseClicked,
|
||||
onCancelClicked = onCancelClicked,
|
||||
onUseBackUpClicked = onUseBackUpClicked
|
||||
)
|
||||
}
|
||||
|
||||
is ShareConfirmationCodeWithAdminState.Error -> LaunchedEffect(state) { onErrorMessage(state.message) }
|
||||
is ShareConfirmationCodeWithAdminState.Cancel,
|
||||
is ShareConfirmationCodeWithAdminState.Close -> LaunchedEffect(state) { onClose() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun ShareConfirmationCodeWithAdminScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onCloseClicked: () -> Unit = {},
|
||||
onCancelClicked: (ShareConfirmationCodeAction.Cancel) -> Unit = {},
|
||||
onUseBackUpClicked: (ShareConfirmationCodeAction.UseBackUpPassword) -> Unit = {},
|
||||
username: String? = null,
|
||||
confirmationCode: List<Char>? = null
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
ProtonTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onCloseClicked) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_proton_close),
|
||||
contentDescription = stringResource(id = R.string.auth_login_close)
|
||||
)
|
||||
}
|
||||
},
|
||||
backgroundColor = LocalColors.current.backgroundNorm
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.auth_login_share_confirmation_code_with_admin),
|
||||
style = ProtonTypography.Default.headline
|
||||
)
|
||||
|
||||
if (username != null) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = ProtonDimens.MediumSpacing),
|
||||
text = stringResource(
|
||||
id = R.string.auth_login_share_confirmation_code_with_admin_subtitle,
|
||||
username
|
||||
),
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(ProtonDimens.DefaultSpacing))
|
||||
|
||||
ConfirmationDigits(digits = confirmationCode)
|
||||
|
||||
ProtonSolidButton(
|
||||
contained = false,
|
||||
onClick = { onUseBackUpClicked(ShareConfirmationCodeAction.UseBackUpPassword) },
|
||||
modifier = Modifier
|
||||
.padding(top = ProtonDimens.MediumSpacing)
|
||||
.height(ProtonDimens.DefaultButtonMinHeight)
|
||||
) {
|
||||
Text(text = stringResource(R.string.auth_login_use_backup_password))
|
||||
}
|
||||
|
||||
ProtonTextButton(
|
||||
contained = false,
|
||||
onClick = { onCancelClicked(ShareConfirmationCodeAction.Cancel) },
|
||||
modifier = Modifier
|
||||
.padding(vertical = ProtonDimens.MediumSpacing)
|
||||
.height(ProtonDimens.DefaultButtonMinHeight),
|
||||
) {
|
||||
Text(text = stringResource(R.string.auth_login_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode")
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT)
|
||||
@Preview(name = "Foldable", device = Devices.FOLDABLE)
|
||||
@Preview(name = "Tablet", device = Devices.PIXEL_C)
|
||||
@Preview(name = "Horizontal", widthDp = 800, heightDp = 360)
|
||||
@Composable
|
||||
internal fun ShareConfirmationCodeWithAdminScreenPreview() {
|
||||
ProtonTheme {
|
||||
ShareConfirmationCodeWithAdminScreen(
|
||||
confirmationCode = listOf('6', '4', 'S', '3'),
|
||||
username = "test@protonmail.com"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode", showBackground = true)
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT)
|
||||
@Preview(name = "Foldable", device = Devices.FOLDABLE)
|
||||
@Preview(name = "Tablet", device = Devices.PIXEL_C)
|
||||
@Preview(name = "Horizontal", widthDp = 800, heightDp = 360)
|
||||
@Composable
|
||||
internal fun ShareConfirmationCodeWithAdminLoadingScreenPreview() {
|
||||
ProtonTheme {
|
||||
ShareConfirmationCodeWithAdminScreen(
|
||||
confirmationCode = listOf('6', '4', 'S', '3'),
|
||||
username = "test@protonmail.com"
|
||||
)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
public sealed interface ShareConfirmationCodeWithAdminState {
|
||||
public data object Loading : ShareConfirmationCodeWithAdminState
|
||||
public data class DataLoaded(
|
||||
val username: String,
|
||||
val confirmationCode: String
|
||||
) : ShareConfirmationCodeWithAdminState
|
||||
|
||||
public data object Close : ShareConfirmationCodeWithAdminState
|
||||
public data object Cancel : ShareConfirmationCodeWithAdminState
|
||||
|
||||
public data class Error(val message: String?) : ShareConfirmationCodeWithAdminState
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.core.auth.domain.usecase.sso.GenerateConfirmationCode
|
||||
import me.proton.core.auth.presentation.compose.confirmationcode.ShareConfirmationCodeWithAdminScreen.getUserId
|
||||
import me.proton.core.compose.viewmodel.stopTimeoutMillis
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.network.domain.ApiException
|
||||
import me.proton.core.user.domain.usecase.GetUser
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
public class ShareConfirmationCodeWithAdminViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val getUser: GetUser,
|
||||
private val generateConfirmationCode: GenerateConfirmationCode
|
||||
) : ViewModel() {
|
||||
|
||||
private val userId by lazy { savedStateHandle.getUserId() }
|
||||
|
||||
private val actions = MutableSharedFlow<ShareConfirmationCodeAction>()
|
||||
|
||||
public val state: StateFlow<ShareConfirmationCodeWithAdminState> = load()
|
||||
.flatMapLatest {
|
||||
actions
|
||||
.transform { action ->
|
||||
val flow = when (action) {
|
||||
ShareConfirmationCodeAction.Cancel,
|
||||
ShareConfirmationCodeAction.Close -> onClose()
|
||||
|
||||
ShareConfirmationCodeAction.UseBackUpPassword -> useBackUpPassword()
|
||||
}
|
||||
emitAll(flow)
|
||||
}.catch {
|
||||
emit(ShareConfirmationCodeWithAdminState.Error(it.message))
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(stopTimeoutMillis),
|
||||
ShareConfirmationCodeWithAdminState.Loading
|
||||
)
|
||||
|
||||
public fun submit(action: ShareConfirmationCodeAction): Job = viewModelScope.launch {
|
||||
actions.emit(action)
|
||||
}
|
||||
|
||||
private fun load() = flow {
|
||||
emit(ShareConfirmationCodeWithAdminState.Loading)
|
||||
val user = getUser(userId, false)
|
||||
emit(
|
||||
ShareConfirmationCodeWithAdminState.DataLoaded(
|
||||
username = user.email ?: user.name ?: "",
|
||||
confirmationCode = generateConfirmationCode(userId)
|
||||
)
|
||||
)
|
||||
}.catch {
|
||||
when (it) {
|
||||
is ApiException -> emit(ShareConfirmationCodeWithAdminState.Error(it.message))
|
||||
else -> throw it
|
||||
}
|
||||
}
|
||||
|
||||
private fun useBackUpPassword() = flow<ShareConfirmationCodeWithAdminState> {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private fun onClose() = flow<ShareConfirmationCodeWithAdminState> {
|
||||
emit(ShareConfirmationCodeWithAdminState.Close)
|
||||
}
|
||||
}
|
||||
|
||||
public object ShareConfirmationCodeWithAdminScreen {
|
||||
public const val KEY_USERID: String = "UserId"
|
||||
public fun SavedStateHandle.getUserId(): UserId = UserId(get<String>(KEY_USERID)!!)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
public sealed interface ShareConfirmationOperation
|
||||
|
||||
public sealed interface ShareConfirmationCodeAction : ShareConfirmationOperation {
|
||||
public data object Close : ShareConfirmationCodeAction
|
||||
public data object Cancel : ShareConfirmationCodeAction
|
||||
public data object UseBackUpPassword : ShareConfirmationCodeAction
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
public sealed interface SignInRequestedForApprovalOperation
|
||||
|
||||
public sealed interface SignInRequestedForApprovalAction : SignInRequestedForApprovalOperation {
|
||||
public data object Close : SignInRequestedForApprovalAction
|
||||
public data class ValidateConfirmationCode(val confirmationCode: String) : SignInRequestedForApprovalAction
|
||||
public data object Confirm : SignInRequestedForApprovalAction
|
||||
public data object Reject : SignInRequestedForApprovalAction
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.core.auth.presentation.compose.R
|
||||
import me.proton.core.auth.presentation.compose.SMALL_SCREEN_HEIGHT
|
||||
import me.proton.core.compose.component.ProtonOutlinedTextFieldWithError
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.ProtonTextButton
|
||||
import me.proton.core.compose.component.appbar.ProtonTopAppBar
|
||||
import me.proton.core.compose.theme.LocalColors
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.theme.ProtonTypography
|
||||
import me.proton.core.compose.theme.defaultSmallWeak
|
||||
|
||||
@Composable
|
||||
public fun SignInRequestedForApprovalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onClose: () -> Unit = {},
|
||||
onErrorMessage: (String?) -> Unit = {},
|
||||
viewModel: SignInRequestedForApprovalViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
SignInRequestedForApprovalScreen(
|
||||
modifier = modifier,
|
||||
onClose = onClose,
|
||||
onCloseClicked = { viewModel.submit(SignInRequestedForApprovalAction.Close) },
|
||||
onErrorMessage = onErrorMessage,
|
||||
onConfirmClicked = { viewModel.submit(it) },
|
||||
onRejectClicked = { viewModel.submit(it) },
|
||||
onConfirmationCodeInputChange = { viewModel.submit(it) },
|
||||
state = state
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun SignInRequestedForApprovalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onClose: () -> Unit = {},
|
||||
onErrorMessage: (String?) -> Unit = {},
|
||||
onCloseClicked: () -> Unit = {},
|
||||
onConfirmClicked: (SignInRequestedForApprovalAction.Confirm) -> Unit = {},
|
||||
onRejectClicked: (SignInRequestedForApprovalAction.Reject) -> Unit = {},
|
||||
onConfirmationCodeInputChange: (SignInRequestedForApprovalAction.ValidateConfirmationCode) -> Unit = {},
|
||||
state: SignInRequestedForApprovalState
|
||||
) {
|
||||
when (state) {
|
||||
is SignInRequestedForApprovalState.Idle -> {
|
||||
SignInRequestedForApprovalScaffold(
|
||||
modifier = modifier,
|
||||
onCloseClicked = onCloseClicked,
|
||||
onConfirmClicked = onConfirmClicked,
|
||||
onConfirmationCodeInputChange = onConfirmationCodeInputChange,
|
||||
onRejectClicked = onRejectClicked
|
||||
)
|
||||
}
|
||||
|
||||
is SignInRequestedForApprovalState.ConfirmationCodeResult -> {
|
||||
SignInRequestedForApprovalScaffold(
|
||||
modifier = modifier,
|
||||
onCloseClicked = onCloseClicked,
|
||||
onConfirmClicked = onConfirmClicked,
|
||||
onRejectClicked = onRejectClicked,
|
||||
onConfirmationCodeInputChange = onConfirmationCodeInputChange,
|
||||
confirmationButtonClickable = state.success
|
||||
)
|
||||
}
|
||||
|
||||
is SignInRequestedForApprovalState.Error -> LaunchedEffect(state) { onErrorMessage(state.message) }
|
||||
is SignInRequestedForApprovalState.Close -> LaunchedEffect(state) { onClose() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun SignInRequestedForApprovalScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
onCloseClicked: () -> Unit,
|
||||
onConfirmClicked: (SignInRequestedForApprovalAction.Confirm) -> Unit,
|
||||
onRejectClicked: (SignInRequestedForApprovalAction.Reject) -> Unit,
|
||||
onConfirmationCodeInputChange: (SignInRequestedForApprovalAction.ValidateConfirmationCode) -> Unit,
|
||||
confirmationButtonClickable: Boolean = false
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
ProtonTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onCloseClicked) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_proton_close),
|
||||
contentDescription = stringResource(id = R.string.auth_login_close)
|
||||
)
|
||||
}
|
||||
},
|
||||
backgroundColor = LocalColors.current.backgroundNorm
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
ConfirmationCodeInputScreen(
|
||||
onConfirmClicked = onConfirmClicked,
|
||||
onRejectClicked = onRejectClicked,
|
||||
onConfirmationCodeInputChange = onConfirmationCodeInputChange,
|
||||
confirmationButtonClickable = confirmationButtonClickable
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmationCodeInputScreen(
|
||||
onConfirmClicked: (SignInRequestedForApprovalAction.Confirm) -> Unit,
|
||||
onRejectClicked: (SignInRequestedForApprovalAction.Reject) -> Unit,
|
||||
onConfirmationCodeInputChange: (SignInRequestedForApprovalAction.ValidateConfirmationCode) -> Unit,
|
||||
confirmationButtonClickable: Boolean = false
|
||||
) {
|
||||
var confirmationCode by remember { mutableStateOf("") }
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.auth_login_signin_requested),
|
||||
style = ProtonTypography.Default.headline
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(top = ProtonDimens.MediumSpacing),
|
||||
text = stringResource(id = R.string.auth_login_signin_requested_subtitle),
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
|
||||
ProtonOutlinedTextFieldWithError(
|
||||
text = confirmationCode,
|
||||
onValueChanged = {
|
||||
confirmationCode = it
|
||||
onConfirmationCodeInputChange(SignInRequestedForApprovalAction.ValidateConfirmationCode(confirmationCode))
|
||||
},
|
||||
label = { Text(text = stringResource(id = R.string.auth_login_confirmation_code)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = ProtonDimens.DefaultSpacing)
|
||||
.testTag(CONFIRMATION_CODE_FIELD_TAG)
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = ProtonDimens.MediumSpacing),
|
||||
text = stringResource(id = R.string.auth_login_signin_requested_note),
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
|
||||
ProtonSolidButton(
|
||||
contained = false,
|
||||
onClick = { onConfirmClicked(SignInRequestedForApprovalAction.Confirm) },
|
||||
enabled = confirmationButtonClickable,
|
||||
modifier = Modifier
|
||||
.padding(top = ProtonDimens.MediumSpacing)
|
||||
.height(ProtonDimens.DefaultButtonMinHeight)
|
||||
) {
|
||||
Text(text = stringResource(R.string.auth_login_yes_it_was_me))
|
||||
}
|
||||
|
||||
ProtonTextButton(
|
||||
contained = false,
|
||||
onClick = { onRejectClicked(SignInRequestedForApprovalAction.Reject) },
|
||||
modifier = Modifier
|
||||
.padding(vertical = ProtonDimens.MediumSpacing)
|
||||
.height(ProtonDimens.DefaultButtonMinHeight),
|
||||
) {
|
||||
Text(text = stringResource(R.string.auth_login_no_it_wasnt_me))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(name = "Light mode")
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT)
|
||||
@Preview(name = "Foldable", device = Devices.FOLDABLE)
|
||||
@Preview(name = "Tablet", device = Devices.PIXEL_C)
|
||||
@Preview(name = "Horizontal", widthDp = 800, heightDp = 360)
|
||||
@Composable
|
||||
internal fun ConfirmationCodeSignInRequestedForApprovalScreenPreview() {
|
||||
ProtonTheme {
|
||||
SignInRequestedForApprovalScreen(
|
||||
onErrorMessage = {},
|
||||
state = SignInRequestedForApprovalState.Idle
|
||||
)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
public sealed interface SignInRequestedForApprovalState {
|
||||
public data object Idle : SignInRequestedForApprovalState
|
||||
public data class ConfirmationCodeResult(
|
||||
val success: Boolean
|
||||
) : SignInRequestedForApprovalState
|
||||
public data object Close : SignInRequestedForApprovalState
|
||||
|
||||
public data class Error(val message: String?) : SignInRequestedForApprovalState
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.core.auth.domain.usecase.sso.ValidateConfirmationCode
|
||||
import me.proton.core.auth.presentation.compose.confirmationcode.ShareConfirmationCodeWithAdminScreen.getUserId
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
public class SignInRequestedForApprovalViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val validateConfirmationCode: ValidateConfirmationCode
|
||||
) : ViewModel() {
|
||||
|
||||
private val userId by lazy { savedStateHandle.getUserId() }
|
||||
|
||||
private val mutableState: MutableStateFlow<SignInRequestedForApprovalState> =
|
||||
MutableStateFlow(SignInRequestedForApprovalState.Idle)
|
||||
|
||||
public val state: StateFlow<SignInRequestedForApprovalState> = mutableState.asStateFlow()
|
||||
|
||||
public fun submit(action: SignInRequestedForApprovalAction): Job = viewModelScope.launch {
|
||||
when (action) {
|
||||
is SignInRequestedForApprovalAction.Close -> close()
|
||||
is SignInRequestedForApprovalAction.Confirm -> confirmRequest()
|
||||
is SignInRequestedForApprovalAction.Reject -> rejectRequest()
|
||||
is SignInRequestedForApprovalAction.ValidateConfirmationCode -> validateCode(action.confirmationCode)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateCode(confirmationCode: String) {
|
||||
val newState = when (validateConfirmationCode(userId, confirmationCode)) {
|
||||
ValidateConfirmationCode.Result.ConfirmationCodeInputError -> SignInRequestedForApprovalState.Error("")
|
||||
ValidateConfirmationCode.Result.ConfirmationCodeInvalid -> SignInRequestedForApprovalState.ConfirmationCodeResult(
|
||||
success = false
|
||||
)
|
||||
|
||||
ValidateConfirmationCode.Result.ConfirmationCodeValid -> SignInRequestedForApprovalState.ConfirmationCodeResult(
|
||||
success = true
|
||||
)
|
||||
|
||||
ValidateConfirmationCode.Result.NoDeviceSecret -> SignInRequestedForApprovalState.Error("")
|
||||
}
|
||||
mutableState.emit(newState)
|
||||
}
|
||||
|
||||
private fun confirmRequest() {
|
||||
|
||||
}
|
||||
|
||||
private fun rejectRequest() {
|
||||
|
||||
}
|
||||
|
||||
private suspend fun close() {
|
||||
mutableState.emit(SignInRequestedForApprovalState.Close)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
public sealed interface SignInSentForApprovalOperation
|
||||
|
||||
public sealed interface SignInSentForApprovalAction : SignInSentForApprovalOperation {
|
||||
public data object Close : SignInSentForApprovalAction
|
||||
|
||||
public data object UseBackUpPassword : SignInSentForApprovalAction
|
||||
public data object AskAdminForHelp : SignInSentForApprovalAction
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.core.auth.presentation.compose.R
|
||||
import me.proton.core.auth.presentation.compose.SMALL_SCREEN_HEIGHT
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.ProtonTextButton
|
||||
import me.proton.core.compose.component.appbar.ProtonTopAppBar
|
||||
import me.proton.core.compose.theme.LocalColors
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.theme.ProtonTypography
|
||||
import me.proton.core.compose.theme.defaultSmallWeak
|
||||
|
||||
@Composable
|
||||
public fun SignInSentForApprovalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onClose: () -> Unit = {},
|
||||
onErrorMessage: (String?) -> Unit = {},
|
||||
viewModel: SignInSentForApprovalViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
SignInSentForApprovalScreen(
|
||||
modifier = modifier,
|
||||
onClose = onClose,
|
||||
onCloseClicked = { viewModel.submit(SignInSentForApprovalAction.Close) },
|
||||
onErrorMessage = onErrorMessage,
|
||||
onUseBackUpClicked = { viewModel.submit(it) },
|
||||
onAskAdminClicked = { viewModel.submit(it) },
|
||||
state = state
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun SignInSentForApprovalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onClose: () -> Unit = {},
|
||||
onCloseClicked: () -> Unit = {},
|
||||
onErrorMessage: (String?) -> Unit = {},
|
||||
onUseBackUpClicked: (SignInSentForApprovalAction.UseBackUpPassword) -> Unit,
|
||||
onAskAdminClicked: (SignInSentForApprovalAction.AskAdminForHelp) -> Unit,
|
||||
state: SignInSentForApprovalState
|
||||
) {
|
||||
when (state) {
|
||||
is SignInSentForApprovalState.DataLoaded -> {
|
||||
SignInSentForApprovalScreen(
|
||||
modifier = modifier,
|
||||
onCloseClicked = onCloseClicked,
|
||||
onUseBackUpClicked = onUseBackUpClicked,
|
||||
onAskAdminForHelpClicked = onAskAdminClicked,
|
||||
confirmationCode = state.confirmationCode.toCharArray().asList(),
|
||||
availableDevices = state.availableDevices
|
||||
)
|
||||
}
|
||||
|
||||
is SignInSentForApprovalState.Loading -> {
|
||||
SignInSentForApprovalScreen(
|
||||
modifier = modifier,
|
||||
onCloseClicked = onCloseClicked,
|
||||
onUseBackUpClicked = onUseBackUpClicked,
|
||||
onAskAdminForHelpClicked = onAskAdminClicked
|
||||
)
|
||||
}
|
||||
|
||||
is SignInSentForApprovalState.Error -> LaunchedEffect(state) { onErrorMessage(state.message) }
|
||||
|
||||
is SignInSentForApprovalState.Close -> LaunchedEffect(state) { onClose() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun SignInSentForApprovalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onCloseClicked: () -> Unit,
|
||||
onUseBackUpClicked: (SignInSentForApprovalAction.UseBackUpPassword) -> Unit,
|
||||
onAskAdminForHelpClicked: (SignInSentForApprovalAction.AskAdminForHelp) -> Unit,
|
||||
confirmationCode: List<Char>? = null,
|
||||
availableDevices: List<AvailableDeviceUIModel>? = null
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
ProtonTopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onCloseClicked) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_proton_close),
|
||||
contentDescription = stringResource(id = R.string.auth_login_close)
|
||||
)
|
||||
}
|
||||
},
|
||||
backgroundColor = LocalColors.current.backgroundNorm
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.auth_login_approve_signin_another_device),
|
||||
style = ProtonTypography.Default.headline
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(top = ProtonDimens.MediumSpacing),
|
||||
text = stringResource(id = R.string.auth_login_approve_signin_another_device_subtitle),
|
||||
style = ProtonTypography.Default.defaultSmallWeak
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(ProtonDimens.DefaultSpacing))
|
||||
|
||||
ConfirmationDigits(digits = confirmationCode)
|
||||
|
||||
Spacer(Modifier.size(ProtonDimens.DefaultSpacing))
|
||||
|
||||
AvailableDevicesList(devices = availableDevices)
|
||||
|
||||
Spacer(Modifier.size(ProtonDimens.DefaultSpacing))
|
||||
|
||||
ProtonSolidButton(
|
||||
contained = false,
|
||||
onClick = { onUseBackUpClicked(SignInSentForApprovalAction.UseBackUpPassword) },
|
||||
modifier = Modifier
|
||||
.padding(top = ProtonDimens.MediumSpacing)
|
||||
.height(ProtonDimens.DefaultButtonMinHeight)
|
||||
) {
|
||||
Text(text = stringResource(R.string.auth_login_use_backup_password))
|
||||
}
|
||||
|
||||
ProtonTextButton(
|
||||
contained = false,
|
||||
onClick = { onAskAdminForHelpClicked(SignInSentForApprovalAction.AskAdminForHelp) },
|
||||
modifier = Modifier
|
||||
.padding(vertical = ProtonDimens.MediumSpacing)
|
||||
.height(ProtonDimens.DefaultButtonMinHeight),
|
||||
) {
|
||||
Text(text = stringResource(R.string.auth_login_ask_admin_for_help))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode")
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT)
|
||||
@Preview(name = "Foldable", device = Devices.FOLDABLE)
|
||||
@Preview(name = "Tablet", device = Devices.PIXEL_C)
|
||||
@Preview(name = "Horizontal", widthDp = 800, heightDp = 360)
|
||||
@Composable
|
||||
internal fun ApproveSignInScreenPreview() {
|
||||
ProtonTheme {
|
||||
SignInSentForApprovalScreen(
|
||||
confirmationCode = listOf('6', '4', 'S', '3'),
|
||||
availableDevices = listOf(
|
||||
AvailableDeviceUIModel(
|
||||
id = "id1",
|
||||
authDeviceName = "MacOS",
|
||||
localizedClientName = "Proton Mail Chrome",
|
||||
lastActivityTime = 1724945205966,
|
||||
clientType = ClientType.Web
|
||||
),
|
||||
AvailableDeviceUIModel(
|
||||
id = "id2",
|
||||
authDeviceName = "Google Pixel 7a",
|
||||
localizedClientName = "Proton Mail Android",
|
||||
lastActivityTime = 1724945205966,
|
||||
clientType = ClientType.Android
|
||||
)
|
||||
),
|
||||
onUseBackUpClicked = {},
|
||||
onAskAdminForHelpClicked = {},
|
||||
onCloseClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light mode")
|
||||
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(name = "Small screen height", heightDp = SMALL_SCREEN_HEIGHT)
|
||||
@Preview(name = "Foldable", device = Devices.FOLDABLE)
|
||||
@Preview(name = "Tablet", device = Devices.PIXEL_C)
|
||||
@Preview(name = "Horizontal", widthDp = 800, heightDp = 360)
|
||||
@Composable
|
||||
internal fun ApproveSignInScreenLoadingPreview() {
|
||||
ProtonTheme {
|
||||
SignInSentForApprovalScreen(
|
||||
confirmationCode = null,
|
||||
availableDevices = null,
|
||||
onUseBackUpClicked = {},
|
||||
onAskAdminForHelpClicked = {},
|
||||
onCloseClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
public sealed interface SignInSentForApprovalState {
|
||||
public data object Loading : SignInSentForApprovalState
|
||||
public data object Close : SignInSentForApprovalState
|
||||
public data class DataLoaded(
|
||||
val confirmationCode: String,
|
||||
val availableDevices: List<AvailableDeviceUIModel>
|
||||
) : SignInSentForApprovalState
|
||||
|
||||
public data class Error(val message: String?) : SignInSentForApprovalState
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
public class SignInSentForApprovalViewModel @Inject constructor(
|
||||
// TODO: create use case for generating confirmation code & reading available devices from API
|
||||
) : ViewModel() {
|
||||
private val mutableState: MutableStateFlow<SignInSentForApprovalState> =
|
||||
MutableStateFlow(SignInSentForApprovalState.Loading)
|
||||
|
||||
public val state: StateFlow<SignInSentForApprovalState> = mutableState.asStateFlow()
|
||||
|
||||
public fun submit(action: SignInSentForApprovalAction): Job = viewModelScope.launch {
|
||||
when (action) {
|
||||
SignInSentForApprovalAction.AskAdminForHelp -> TODO()
|
||||
SignInSentForApprovalAction.Close -> onClose()
|
||||
SignInSentForApprovalAction.UseBackUpPassword -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onClose() {
|
||||
mutableState.emit(SignInSentForApprovalState.Close)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2024 Proton AG
|
||||
~ This file is part of Proton AG and ProtonCore.
|
||||
~
|
||||
@@ -27,6 +28,24 @@
|
||||
<string name="auth_login_username">Username or email</string>
|
||||
<string name="auth_login_password">Enter your password</string>
|
||||
<string name="auth_login_assistive_text">Please enter your Proton email or username.</string>
|
||||
|
||||
<string name="auth_login_confirmation_code">Confirmation Code</string>
|
||||
<string name="auth_login_use_backup_password">Use backup password instead</string>
|
||||
<string name="auth_login_ask_admin_for_help">Ask admin for help</string>
|
||||
<string name="auth_login_approve_signin_another_device">Approve the sign-in from another device</string>
|
||||
<string name="auth_login_approve_signin_another_device_subtitle">To make sure it’s really you trying to sign in to your account, review the confirmation code and approve the request from another device.</string>
|
||||
<string name="auth_login_share_confirmation_code_with_admin">Share the confirmation code with your administrator</string>
|
||||
<string name="auth_login_share_confirmation_code_with_admin_subtitle">To make sure it’s really you trying to sign-in, share the confirmation code with your administrator admin@privacybydefault.com so that they can approve the request for %s.</string>
|
||||
<string name="auth_login_signin_requested">Sign-in requested on another device. Was it you?</string>
|
||||
<string name="auth_login_signin_requested_subtitle">Check that the confirmation code match the code on your other device.</string>
|
||||
<string name="auth_login_signin_requested_note">If you didn\'t make this request, cancel it now.</string>
|
||||
<string name="auth_login_yes_it_was_me">Yes, it was me</string>
|
||||
<string name="auth_login_no_it_wasnt_me">No, it wasn\'t me</string>
|
||||
<string name="auth_login_devices_available">Devices available:</string>
|
||||
<string name="auth_login_no_devices_available">No devices available:</string>
|
||||
<string name="auth_login_device_last_used">Last used %s</string>
|
||||
<string name="auth_login_cancel">Cancel</string>
|
||||
<string name="auth_login_not_available">N/A</string>
|
||||
<string name="backup_password_input_password_empty">Password cannot be empty</string>
|
||||
<string name="backup_password_input_action_continue">Continue</string>
|
||||
<string name="backup_password_input_action_help">Ask administrator for help</string>
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import app.cash.paparazzi.DeviceConfig
|
||||
import app.cash.paparazzi.Paparazzi
|
||||
import app.cash.paparazzi.detectEnvironment
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class ShareConfirmationCodeWithAdminScreenTest {
|
||||
@get:Rule
|
||||
val paparazzi = Paparazzi(
|
||||
deviceConfig = DeviceConfig.PIXEL_5,
|
||||
theme = "ProtonTheme",
|
||||
// Remove when layoutlib properly supports SDK 34 (https://github.com/cashapp/paparazzi/issues/1025).
|
||||
environment = detectEnvironment().run {
|
||||
copy(compileSdkVersion = 33, platformDir = platformDir.replace("34", "33"))
|
||||
}
|
||||
)
|
||||
|
||||
@Test
|
||||
fun shareConfirmationCodeWithAdminScreenTest() {
|
||||
paparazzi.snapshot {
|
||||
ShareConfirmationCodeWithAdminScreen(
|
||||
onCloseClicked = {},
|
||||
onCancelClicked = {},
|
||||
onUseBackUpClicked = {},
|
||||
onErrorMessage = {},
|
||||
state = ShareConfirmationCodeWithAdminState.DataLoaded(
|
||||
confirmationCode = "64S3",
|
||||
username = "test-username",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import app.cash.paparazzi.DeviceConfig
|
||||
import app.cash.paparazzi.Paparazzi
|
||||
import app.cash.paparazzi.detectEnvironment
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SignInRequestedForApprovalScreenTest {
|
||||
@get:Rule
|
||||
val paparazzi = Paparazzi(
|
||||
deviceConfig = DeviceConfig.PIXEL_5,
|
||||
theme = "ProtonTheme",
|
||||
// Remove when layoutlib properly supports SDK 34 (https://github.com/cashapp/paparazzi/issues/1025).
|
||||
environment = detectEnvironment().run {
|
||||
copy(compileSdkVersion = 33, platformDir = platformDir.replace("34", "33"))
|
||||
}
|
||||
)
|
||||
|
||||
@Test
|
||||
fun signInRequestedForApprovalScreenTest() {
|
||||
paparazzi.snapshot {
|
||||
SignInRequestedForApprovalScreen(
|
||||
onCloseClicked = {},
|
||||
onConfirmClicked = {},
|
||||
onRejectClicked = {},
|
||||
state = SignInRequestedForApprovalState.Idle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package me.proton.core.auth.presentation.compose.confirmationcode
|
||||
|
||||
import app.cash.paparazzi.DeviceConfig
|
||||
import app.cash.paparazzi.Paparazzi
|
||||
import app.cash.paparazzi.detectEnvironment
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class SignInSentForApprovalScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val paparazzi = Paparazzi(
|
||||
deviceConfig = DeviceConfig.PIXEL_5,
|
||||
theme = "ProtonTheme",
|
||||
// Remove when layoutlib properly supports SDK 34 (https://github.com/cashapp/paparazzi/issues/1025).
|
||||
environment = detectEnvironment().run {
|
||||
copy(compileSdkVersion = 33, platformDir = platformDir.replace("34", "33"))
|
||||
}
|
||||
)
|
||||
|
||||
@Test
|
||||
fun signInSentForApprovalScreenTest() {
|
||||
paparazzi.snapshot {
|
||||
SignInSentForApprovalScreen(
|
||||
onCloseClicked = {},
|
||||
onUseBackUpClicked = {},
|
||||
onAskAdminClicked = {},
|
||||
state = SignInSentForApprovalState.DataLoaded(
|
||||
confirmationCode = "64S3", availableDevices =
|
||||
listOf(
|
||||
AvailableDeviceUIModel(
|
||||
id = "device1",
|
||||
authDeviceName = "Device Name",
|
||||
localizedClientName = "Chrome",
|
||||
lastActivityTime = 242344233124,
|
||||
clientType = ClientType.Web
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:666b268866aed69d95f55085068f249d879e72ec593b53e1471a8b289e5002dd
|
||||
size 45089
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b43b08e4238ca8bbb32785c9c23dc5c58e9c57269ed5896906a756c99247061
|
||||
size 32381
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5b8310b9293e4ac4a4b80d3f39765773e19c3aeda3828442865849f7457a4699
|
||||
size 48320
|
||||
@@ -27,6 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import me.proton.android.core.coreexample.db.AppDatabase
|
||||
import me.proton.core.account.data.db.AccountDatabase
|
||||
import me.proton.core.auth.data.db.AuthDatabase
|
||||
import me.proton.core.challenge.data.db.ChallengeDatabase
|
||||
import me.proton.core.contact.data.local.db.ContactDatabase
|
||||
import me.proton.core.eventmanager.data.db.EventMetadataDatabase
|
||||
@@ -124,4 +125,7 @@ abstract class AppDatabaseBindsModule {
|
||||
@Binds
|
||||
abstract fun provideDeviceRecoveryDatabase(appDatabase: AppDatabase): DeviceRecoveryDatabase
|
||||
|
||||
@Binds
|
||||
abstract fun provideAuthDatabase(appDatabase: AppDatabase): AuthDatabase
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user