Aggiunge gestione account con cambio password su web e app.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,22 @@ data class LoginRequest(val email: String, val password: String)
|
||||
|
||||
data class RefreshRequest(@Json(name = "refresh_token") val refreshToken: String)
|
||||
|
||||
data class ForgotPasswordRequest(val email: String)
|
||||
|
||||
data class ForgotPasswordResponse(val message: String)
|
||||
|
||||
data class UpdateAccountRequest(val name: String)
|
||||
|
||||
data class ChangePasswordRequest(
|
||||
@Json(name = "current_password") val currentPassword: String,
|
||||
val password: String,
|
||||
@Json(name = "password_confirmation") val passwordConfirmation: String,
|
||||
)
|
||||
|
||||
data class MessageResponse(val message: String)
|
||||
|
||||
data class ApiErrorResponse(val error: String? = null)
|
||||
|
||||
data class LoginResponse(
|
||||
val user: UserDto,
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
|
||||
+12
@@ -23,6 +23,18 @@ interface MatchLiveApi {
|
||||
@POST("auth/logout")
|
||||
suspend fun logout()
|
||||
|
||||
@POST("auth/password/forgot")
|
||||
suspend fun forgotPassword(@Body body: ForgotPasswordRequest): ForgotPasswordResponse
|
||||
|
||||
@GET("account")
|
||||
suspend fun account(): UserDto
|
||||
|
||||
@PATCH("account")
|
||||
suspend fun updateAccount(@Body body: UpdateAccountRequest): UserDto
|
||||
|
||||
@PATCH("account/password")
|
||||
suspend fun changePassword(@Body body: ChangePasswordRequest): MessageResponse
|
||||
|
||||
@GET("sports")
|
||||
suspend fun sports(): List<SportDto>
|
||||
|
||||
|
||||
+31
@@ -2,9 +2,13 @@ package com.matchlivetv.match_live_tv.data.repository
|
||||
|
||||
import com.matchlivetv.match_live_tv.core.StoredSession
|
||||
import com.matchlivetv.match_live_tv.core.TokenStore
|
||||
import com.matchlivetv.match_live_tv.data.api.ChangePasswordRequest
|
||||
import com.matchlivetv.match_live_tv.data.api.ForgotPasswordRequest
|
||||
import com.matchlivetv.match_live_tv.data.api.LoginRequest
|
||||
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
|
||||
import com.matchlivetv.match_live_tv.data.api.RefreshRequest
|
||||
import com.matchlivetv.match_live_tv.data.api.UpdateAccountRequest
|
||||
import com.matchlivetv.match_live_tv.domain.User
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class AuthRepository(
|
||||
@@ -46,6 +50,33 @@ class AuthRepository(
|
||||
return session
|
||||
}
|
||||
|
||||
suspend fun forgotPassword(email: String): String {
|
||||
return api.forgotPassword(ForgotPasswordRequest(email.trim())).message
|
||||
}
|
||||
|
||||
suspend fun fetchAccount(): User = api.account().toDomain()
|
||||
|
||||
suspend fun updateName(name: String): User {
|
||||
val user = api.updateAccount(UpdateAccountRequest(name.trim())).toDomain()
|
||||
val stored = currentSession() ?: return user
|
||||
tokenStore.saveSession(stored.copy(user = user))
|
||||
return user
|
||||
}
|
||||
|
||||
suspend fun changePassword(
|
||||
currentPassword: String,
|
||||
password: String,
|
||||
passwordConfirmation: String,
|
||||
) {
|
||||
api.changePassword(
|
||||
ChangePasswordRequest(
|
||||
currentPassword = currentPassword,
|
||||
password = password,
|
||||
passwordConfirmation = passwordConfirmation,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
runCatching { api.logout() }
|
||||
tokenStore.clear()
|
||||
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
package com.matchlivetv.match_live_tv.ui.account
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.matchlivetv.match_live_tv.R
|
||||
import com.matchlivetv.match_live_tv.data.AppContainer
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountScreen(
|
||||
container: AppContainer,
|
||||
onBack: () -> Unit,
|
||||
onLoggedOut: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var email by remember { mutableStateOf("") }
|
||||
var role by remember { mutableStateOf("") }
|
||||
var name by remember { mutableStateOf("") }
|
||||
var currentPassword by remember { mutableStateOf("") }
|
||||
var newPassword by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var loadingProfile by remember { mutableStateOf(true) }
|
||||
var savingProfile by remember { mutableStateOf(false) }
|
||||
var savingPassword by remember { mutableStateOf(false) }
|
||||
var loggingOut by remember { mutableStateOf(false) }
|
||||
var profileMessage by remember { mutableStateOf<String?>(null) }
|
||||
var profileError by remember { mutableStateOf<String?>(null) }
|
||||
var passwordMessage by remember { mutableStateOf<String?>(null) }
|
||||
var passwordError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val errGeneric = stringResource(R.string.common_error_generic)
|
||||
val profileSaved = stringResource(R.string.account_profile_saved)
|
||||
val passwordSaved = stringResource(R.string.account_password_saved)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
runCatching { container.authRepository.fetchAccount() }
|
||||
.onSuccess { user ->
|
||||
email = user.email
|
||||
name = user.name
|
||||
role = user.role
|
||||
}
|
||||
.onFailure {
|
||||
profileError = apiErrorMessage(it) ?: errGeneric
|
||||
}
|
||||
loadingProfile = false
|
||||
}
|
||||
|
||||
MatchScreenScaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.account_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.common_close),
|
||||
tint = MatchColors.TextSecondary,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MatchColors.Background,
|
||||
titleContentColor = Color.White,
|
||||
),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_profile_heading),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = { Text(stringResource(R.string.login_email)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = accountFieldColors(),
|
||||
)
|
||||
if (role.isNotBlank()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.account_role, role),
|
||||
color = MatchColors.TextSecondary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(stringResource(R.string.account_name_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
enabled = !loadingProfile && !savingProfile,
|
||||
colors = accountFieldColors(),
|
||||
)
|
||||
profileError?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(it, color = MatchColors.PrimaryRed)
|
||||
}
|
||||
profileMessage?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(it, color = MatchColors.TextSecondary)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
MatchPrimaryButton(
|
||||
label = stringResource(R.string.account_save_profile),
|
||||
loading = savingProfile,
|
||||
enabled = !loadingProfile && name.isNotBlank() && !savingProfile,
|
||||
onClick = {
|
||||
savingProfile = true
|
||||
profileError = null
|
||||
profileMessage = null
|
||||
scope.launch {
|
||||
runCatching { container.authRepository.updateName(name) }
|
||||
.onSuccess {
|
||||
name = it.name
|
||||
profileMessage = profileSaved
|
||||
}
|
||||
.onFailure {
|
||||
profileError = apiErrorMessage(it) ?: errGeneric
|
||||
}
|
||||
savingProfile = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.account_password_heading),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
PasswordField(
|
||||
value = currentPassword,
|
||||
onValueChange = { currentPassword = it },
|
||||
label = stringResource(R.string.account_current_password),
|
||||
visible = passwordVisible,
|
||||
onToggleVisible = { passwordVisible = !passwordVisible },
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
PasswordField(
|
||||
value = newPassword,
|
||||
onValueChange = { newPassword = it },
|
||||
label = stringResource(R.string.account_new_password),
|
||||
visible = passwordVisible,
|
||||
onToggleVisible = { passwordVisible = !passwordVisible },
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
PasswordField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = stringResource(R.string.account_confirm_password),
|
||||
visible = passwordVisible,
|
||||
onToggleVisible = { passwordVisible = !passwordVisible },
|
||||
)
|
||||
passwordError?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(it, color = MatchColors.PrimaryRed)
|
||||
}
|
||||
passwordMessage?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(it, color = MatchColors.TextSecondary)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
MatchPrimaryButton(
|
||||
label = stringResource(R.string.account_save_password),
|
||||
loading = savingPassword,
|
||||
enabled = currentPassword.isNotBlank() &&
|
||||
newPassword.isNotBlank() &&
|
||||
confirmPassword.isNotBlank() &&
|
||||
!savingPassword,
|
||||
onClick = {
|
||||
savingPassword = true
|
||||
passwordError = null
|
||||
passwordMessage = null
|
||||
scope.launch {
|
||||
runCatching {
|
||||
container.authRepository.changePassword(
|
||||
currentPassword = currentPassword,
|
||||
password = newPassword,
|
||||
passwordConfirmation = confirmPassword,
|
||||
)
|
||||
}.onSuccess {
|
||||
currentPassword = ""
|
||||
newPassword = ""
|
||||
confirmPassword = ""
|
||||
passwordMessage = passwordSaved
|
||||
}.onFailure {
|
||||
passwordError = apiErrorMessage(it) ?: errGeneric
|
||||
}
|
||||
savingPassword = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(40.dp))
|
||||
MatchSecondaryButton(
|
||||
label = stringResource(R.string.action_logout),
|
||||
enabled = !loggingOut,
|
||||
onClick = {
|
||||
loggingOut = true
|
||||
scope.launch {
|
||||
container.authRepository.logout()
|
||||
onLoggedOut()
|
||||
}
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordField(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
visible: Boolean,
|
||||
onToggleVisible: () -> Unit,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(label) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (visible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = onToggleVisible) {
|
||||
Icon(
|
||||
imageVector = if (visible) Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||
contentDescription = null,
|
||||
tint = MatchColors.TextSecondary,
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
colors = accountFieldColors(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun accountFieldColors() = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
disabledTextColor = MatchColors.TextSecondary,
|
||||
focusedBorderColor = MatchColors.PrimaryRed,
|
||||
unfocusedBorderColor = MatchColors.Outline,
|
||||
disabledBorderColor = MatchColors.Outline,
|
||||
focusedLabelColor = MatchColors.PrimaryRed,
|
||||
unfocusedLabelColor = MatchColors.TextSecondary,
|
||||
disabledLabelColor = MatchColors.TextSecondary,
|
||||
cursorColor = MatchColors.PrimaryRed,
|
||||
)
|
||||
|
||||
private fun apiErrorMessage(error: Throwable): String? {
|
||||
if (error is HttpException) {
|
||||
val body = error.response()?.errorBody()?.string().orEmpty()
|
||||
val match = Regex("\"error\"\\s*:\\s*\"([^\"]+)\"").find(body)
|
||||
if (match != null) return match.groupValues[1]
|
||||
}
|
||||
return error.message
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package com.matchlivetv.match_live_tv.ui.login
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.matchlivetv.match_live_tv.R
|
||||
import com.matchlivetv.match_live_tv.data.AppContainer
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
|
||||
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ForgotPasswordScreen(
|
||||
container: AppContainer,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var email by remember { mutableStateOf("") }
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var success by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val errGeneric = stringResource(R.string.common_error_generic)
|
||||
val successMessage = stringResource(R.string.forgot_password_success)
|
||||
|
||||
MatchScreenScaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.forgot_password_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.common_close),
|
||||
tint = MatchColors.TextSecondary,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MatchColors.Background,
|
||||
titleContentColor = Color.White,
|
||||
),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.forgot_password_lead),
|
||||
color = MatchColors.TextSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text(stringResource(R.string.login_email)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
enabled = !success,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { }),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
focusedBorderColor = MatchColors.PrimaryRed,
|
||||
unfocusedBorderColor = MatchColors.Outline,
|
||||
focusedLabelColor = MatchColors.PrimaryRed,
|
||||
unfocusedLabelColor = MatchColors.TextSecondary,
|
||||
cursorColor = MatchColors.PrimaryRed,
|
||||
),
|
||||
)
|
||||
error?.let {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(it, color = MatchColors.PrimaryRed, textAlign = TextAlign.Center)
|
||||
}
|
||||
if (success) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(successMessage, color = MatchColors.TextSecondary, textAlign = TextAlign.Center)
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
MatchPrimaryButton(
|
||||
label = stringResource(R.string.forgot_password_submit),
|
||||
loading = loading,
|
||||
enabled = email.isNotBlank() && !loading && !success,
|
||||
onClick = {
|
||||
loading = true
|
||||
error = null
|
||||
scope.launch {
|
||||
runCatching { container.authRepository.forgotPassword(email) }
|
||||
.onSuccess { success = true }
|
||||
.onFailure { error = it.message ?: errGeneric }
|
||||
loading = false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -1,5 +1,6 @@
|
||||
package com.matchlivetv.match_live_tv.ui.login
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -38,6 +39,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.matchlivetv.match_live_tv.R
|
||||
import com.matchlivetv.match_live_tv.data.AppContainer
|
||||
@@ -53,6 +55,7 @@ import kotlinx.coroutines.launch
|
||||
fun LoginScreen(
|
||||
container: AppContainer,
|
||||
onLoggedIn: () -> Unit,
|
||||
onForgotPassword: () -> Unit,
|
||||
) {
|
||||
var email by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
@@ -170,6 +173,16 @@ fun LoginScreen(
|
||||
keyboardActions = KeyboardActions(onDone = { submitLogin() }),
|
||||
colors = matchTextFieldColors(),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.login_forgot_password),
|
||||
color = MatchColors.TextSecondary,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.clickable(onClick = onForgotPassword)
|
||||
.padding(vertical = 4.dp),
|
||||
)
|
||||
error?.let {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
|
||||
+5
-10
@@ -17,9 +17,9 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -67,7 +67,7 @@ fun MatchesScreen(
|
||||
container: AppContainer,
|
||||
onOpenSetup: (matchId: String) -> Unit,
|
||||
onOpenBroadcast: (sessionId: String) -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onOpenAccount: () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
@@ -179,15 +179,10 @@ fun MatchesScreen(
|
||||
tint = MatchColors.TextSecondary,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
container.authRepository.logout()
|
||||
onLogout()
|
||||
}
|
||||
}) {
|
||||
IconButton(onClick = onOpenAccount) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||
contentDescription = stringResource(R.string.action_logout),
|
||||
imageVector = Icons.Filled.Person,
|
||||
contentDescription = stringResource(R.string.account_title),
|
||||
tint = MatchColors.TextSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
+30
-7
@@ -7,7 +7,9 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.matchlivetv.match_live_tv.data.AppContainer
|
||||
import com.matchlivetv.match_live_tv.ui.account.AccountScreen
|
||||
import com.matchlivetv.match_live_tv.ui.broadcast.BroadcastScreen
|
||||
import com.matchlivetv.match_live_tv.ui.login.ForgotPasswordScreen
|
||||
import com.matchlivetv.match_live_tv.ui.login.LoginScreen
|
||||
import com.matchlivetv.match_live_tv.ui.matches.MatchesScreen
|
||||
import com.matchlivetv.match_live_tv.ui.splash.SplashScreen
|
||||
@@ -34,11 +36,23 @@ fun AppNavHost(container: AppContainer) {
|
||||
)
|
||||
}
|
||||
composable(Routes.Login) {
|
||||
LoginScreen(container) {
|
||||
navController.navigate(Routes.Matches) {
|
||||
popUpTo(Routes.Login) { inclusive = true }
|
||||
}
|
||||
}
|
||||
LoginScreen(
|
||||
container = container,
|
||||
onLoggedIn = {
|
||||
navController.navigate(Routes.Matches) {
|
||||
popUpTo(Routes.Login) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onForgotPassword = {
|
||||
navController.navigate(Routes.ForgotPassword)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(Routes.ForgotPassword) {
|
||||
ForgotPasswordScreen(
|
||||
container = container,
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
composable(Routes.Matches) {
|
||||
MatchesScreen(
|
||||
@@ -49,9 +63,18 @@ fun AppNavHost(container: AppContainer) {
|
||||
onOpenBroadcast = { sessionId ->
|
||||
navController.navigate(Routes.broadcast(sessionId))
|
||||
},
|
||||
onLogout = {
|
||||
onOpenAccount = {
|
||||
navController.navigate(Routes.Account)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(Routes.Account) {
|
||||
AccountScreen(
|
||||
container = container,
|
||||
onBack = { navController.popBackStack() },
|
||||
onLoggedOut = {
|
||||
navController.navigate(Routes.Login) {
|
||||
popUpTo(Routes.Matches) { inclusive = true }
|
||||
popUpTo(0) { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
+2
@@ -3,7 +3,9 @@ package com.matchlivetv.match_live_tv.ui.navigation
|
||||
object Routes {
|
||||
const val Splash = "splash"
|
||||
const val Login = "login"
|
||||
const val ForgotPassword = "forgot_password"
|
||||
const val Matches = "matches"
|
||||
const val Account = "account"
|
||||
const val Setup = "setup/{matchId}/{step}"
|
||||
const val Broadcast = "broadcast/{sessionId}"
|
||||
|
||||
|
||||
@@ -241,4 +241,21 @@ Dies kann nicht rückgängig gemacht werden.</string>
|
||||
<string name="login_email_placeholder">coach@team.com</string>
|
||||
<string name="action_delete">Löschen</string>
|
||||
<string name="wizard_color_hue">Farbton</string>
|
||||
<string name="login_forgot_password">Passwort vergessen?</string>
|
||||
<string name="forgot_password_title">Passwort vergessen</string>
|
||||
<string name="forgot_password_lead">Gib die E-Mail des Kontos ein: Wir senden dir einen Link zum Zurücksetzen des Passworts.</string>
|
||||
<string name="forgot_password_submit">Reset-Link senden</string>
|
||||
<string name="forgot_password_success">Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen.</string>
|
||||
<string name="account_title">Konto</string>
|
||||
<string name="account_profile_heading">Profil</string>
|
||||
<string name="account_password_heading">Passwort ändern</string>
|
||||
<string name="account_name_label">Name</string>
|
||||
<string name="account_role">Rolle: %1$s</string>
|
||||
<string name="account_current_password">Aktuelles Passwort</string>
|
||||
<string name="account_new_password">Neues Passwort (mind. 8 Zeichen)</string>
|
||||
<string name="account_confirm_password">Passwort bestätigen</string>
|
||||
<string name="account_save_profile">Profil speichern</string>
|
||||
<string name="account_save_password">Passwort aktualisieren</string>
|
||||
<string name="account_profile_saved">Profil aktualisiert</string>
|
||||
<string name="account_password_saved">Passwort aktualisiert</string>
|
||||
</resources>
|
||||
|
||||
@@ -241,4 +241,21 @@ This cannot be undone.</string>
|
||||
<string name="login_email_placeholder">coach@team.com</string>
|
||||
<string name="action_delete">Delete</string>
|
||||
<string name="wizard_color_hue">Hue</string>
|
||||
<string name="login_forgot_password">Forgot password?</string>
|
||||
<string name="forgot_password_title">Forgot password</string>
|
||||
<string name="forgot_password_lead">Enter your account email: we will send you a link to reset your password.</string>
|
||||
<string name="forgot_password_submit">Send reset link</string>
|
||||
<string name="forgot_password_success">If the email is registered, you will receive a password reset link shortly.</string>
|
||||
<string name="account_title">Account</string>
|
||||
<string name="account_profile_heading">Profile</string>
|
||||
<string name="account_password_heading">Change password</string>
|
||||
<string name="account_name_label">Name</string>
|
||||
<string name="account_role">Role: %1$s</string>
|
||||
<string name="account_current_password">Current password</string>
|
||||
<string name="account_new_password">New password (min. 8 characters)</string>
|
||||
<string name="account_confirm_password">Confirm password</string>
|
||||
<string name="account_save_profile">Save profile</string>
|
||||
<string name="account_save_password">Update password</string>
|
||||
<string name="account_profile_saved">Profile updated</string>
|
||||
<string name="account_password_saved">Password updated</string>
|
||||
</resources>
|
||||
|
||||
@@ -241,4 +241,21 @@ Esta acción no se puede deshacer.</string>
|
||||
<string name="login_email_placeholder">coach@equipo.com</string>
|
||||
<string name="action_delete">Eliminar</string>
|
||||
<string name="wizard_color_hue">Tono</string>
|
||||
<string name="login_forgot_password">¿Olvidaste la contraseña?</string>
|
||||
<string name="forgot_password_title">Contraseña olvidada</string>
|
||||
<string name="forgot_password_lead">Introduce el email de la cuenta: te enviaremos un enlace para restablecer la contraseña.</string>
|
||||
<string name="forgot_password_submit">Enviar enlace</string>
|
||||
<string name="forgot_password_success">Si el email está registrado, recibirás en breve un enlace para restablecer la contraseña.</string>
|
||||
<string name="account_title">Cuenta</string>
|
||||
<string name="account_profile_heading">Perfil</string>
|
||||
<string name="account_password_heading">Cambiar contraseña</string>
|
||||
<string name="account_name_label">Nombre</string>
|
||||
<string name="account_role">Rol: %1$s</string>
|
||||
<string name="account_current_password">Contraseña actual</string>
|
||||
<string name="account_new_password">Nueva contraseña (mín. 8 caracteres)</string>
|
||||
<string name="account_confirm_password">Confirmar contraseña</string>
|
||||
<string name="account_save_profile">Guardar perfil</string>
|
||||
<string name="account_save_password">Actualizar contraseña</string>
|
||||
<string name="account_profile_saved">Perfil actualizado</string>
|
||||
<string name="account_password_saved">Contraseña actualizada</string>
|
||||
</resources>
|
||||
|
||||
@@ -241,4 +241,21 @@ Cette action est irréversible.</string>
|
||||
<string name="login_email_placeholder">coach@equipe.com</string>
|
||||
<string name="action_delete">Supprimer</string>
|
||||
<string name="wizard_color_hue">Teinte</string>
|
||||
<string name="login_forgot_password">Mot de passe oublié ?</string>
|
||||
<string name="forgot_password_title">Mot de passe oublié</string>
|
||||
<string name="forgot_password_lead">Saisissez l’e-mail du compte : nous vous enverrons un lien pour réinitialiser le mot de passe.</string>
|
||||
<string name="forgot_password_submit">Envoyer le lien</string>
|
||||
<string name="forgot_password_success">Si l’e-mail est enregistré, vous recevrez bientôt un lien de réinitialisation.</string>
|
||||
<string name="account_title">Compte</string>
|
||||
<string name="account_profile_heading">Profil</string>
|
||||
<string name="account_password_heading">Changer le mot de passe</string>
|
||||
<string name="account_name_label">Nom</string>
|
||||
<string name="account_role">Rôle : %1$s</string>
|
||||
<string name="account_current_password">Mot de passe actuel</string>
|
||||
<string name="account_new_password">Nouveau mot de passe (min. 8 caractères)</string>
|
||||
<string name="account_confirm_password">Confirmer le mot de passe</string>
|
||||
<string name="account_save_profile">Enregistrer le profil</string>
|
||||
<string name="account_save_password">Mettre à jour le mot de passe</string>
|
||||
<string name="account_profile_saved">Profil mis à jour</string>
|
||||
<string name="account_password_saved">Mot de passe mis à jour</string>
|
||||
</resources>
|
||||
|
||||
@@ -241,4 +241,21 @@ L\'operazione non si può annullare.</string>
|
||||
<string name="login_email_placeholder">coach@team.com</string>
|
||||
<string name="action_delete">Elimina</string>
|
||||
<string name="wizard_color_hue">Tonalità</string>
|
||||
<string name="login_forgot_password">Password dimenticata?</string>
|
||||
<string name="forgot_password_title">Password dimenticata</string>
|
||||
<string name="forgot_password_lead">Inserisci l’email dell’account: ti invieremo un link per reimpostare la password.</string>
|
||||
<string name="forgot_password_submit">Invia link di reset</string>
|
||||
<string name="forgot_password_success">Se l’email è registrata, riceverai a breve un link per reimpostare la password.</string>
|
||||
<string name="account_title">Account</string>
|
||||
<string name="account_profile_heading">Profilo</string>
|
||||
<string name="account_password_heading">Cambia password</string>
|
||||
<string name="account_name_label">Nome</string>
|
||||
<string name="account_role">Ruolo: %1$s</string>
|
||||
<string name="account_current_password">Password attuale</string>
|
||||
<string name="account_new_password">Nuova password (min. 8 caratteri)</string>
|
||||
<string name="account_confirm_password">Conferma password</string>
|
||||
<string name="account_save_profile">Salva profilo</string>
|
||||
<string name="account_save_password">Aggiorna password</string>
|
||||
<string name="account_profile_saved">Profilo aggiornato</string>
|
||||
<string name="account_password_saved">Password aggiornata</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user