Implement Google OAuth linking functionality. Update backend to handle linking existing accounts with Google, including state token management. Enhance frontend to support linking process, including new UI components for user input and feedback. Update mobile app to handle OAuth callbacks and integrate linking features. Refactor related services and controllers for improved error handling and user experience.
This commit is contained in:
Binary file not shown.
@@ -11,11 +11,20 @@
|
||||
android:theme="@style/Theme.TimeClock">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="timeclock"
|
||||
android:host="oauth-callback" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package de.tsschulz.timeclock
|
||||
|
||||
import android.graphics.Color
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.tsschulz.timeclock.ui.TimeClockApp
|
||||
@@ -17,9 +20,12 @@ import de.tsschulz.timeclock.ui.theme.TimeClockTheme
|
||||
import de.tsschulz.timeclock.ui.time.TimeViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val oauthCallback = mutableStateOf<OAuthCallback?>(null)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
oauthCallback.value = intent.toOAuthCallback()
|
||||
Log.i(TAG, "MainActivity.onCreate")
|
||||
window.statusBarColor = Color.rgb(240, 255, 236)
|
||||
window.navigationBarColor = Color.WHITE
|
||||
@@ -30,6 +36,20 @@ class MainActivity : ComponentActivity() {
|
||||
val bookingViewModel: BookingViewModel = viewModel(factory = BookingViewModel.Factory(application))
|
||||
val settingsViewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory(application))
|
||||
val adminViewModel: AdminViewModel = viewModel(factory = AdminViewModel.Factory(application))
|
||||
val callback = oauthCallback.value
|
||||
LaunchedEffect(callback) {
|
||||
when (callback) {
|
||||
is OAuthCallback.Token -> {
|
||||
authViewModel.completeOAuthToken(callback.token)
|
||||
oauthCallback.value = null
|
||||
}
|
||||
is OAuthCallback.Pending -> {
|
||||
authViewModel.requireOAuthLink(callback.pendingToken, callback.email)
|
||||
oauthCallback.value = null
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
TimeClockTheme {
|
||||
TimeClockApp(
|
||||
authViewModel = authViewModel,
|
||||
@@ -58,7 +78,28 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
oauthCallback.value = intent.toOAuthCallback()
|
||||
}
|
||||
|
||||
private fun Intent?.toOAuthCallback(): OAuthCallback? {
|
||||
val data = this?.data ?: return null
|
||||
if (data.scheme != "timeclock" || data.host != "oauth-callback") return null
|
||||
val token = data.getQueryParameter("token")
|
||||
if (!token.isNullOrBlank()) return OAuthCallback.Token(token)
|
||||
val pending = data.getQueryParameter("pending")
|
||||
if (!pending.isNullOrBlank()) return OAuthCallback.Pending(pending, data.getQueryParameter("email"))
|
||||
return null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "TimeClockStartup"
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class OAuthCallback {
|
||||
data class Token(val token: String) : OAuthCallback()
|
||||
data class Pending(val pendingToken: String, val email: String?) : OAuthCallback()
|
||||
}
|
||||
|
||||
@@ -50,3 +50,22 @@ data class LogoutResponse(
|
||||
val success: Boolean = true,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OAuthLinkUrlRequest(
|
||||
val platform: String = "android",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OAuthLinkUrlResponse(
|
||||
val success: Boolean = false,
|
||||
val url: String? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OAuthLinkExistingRequest(
|
||||
val pendingToken: String,
|
||||
val email: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
@@ -69,6 +69,32 @@ class TimeClockApiClient(
|
||||
return decode(LogoutResponse.serializer(), raw.ifBlank { "{}" })
|
||||
}
|
||||
|
||||
fun googleLoginUrl(): String = endpoint("auth/google?platform=android")
|
||||
|
||||
suspend fun createGoogleLinkUrl(): OAuthLinkUrlResponse {
|
||||
val raw = execute(
|
||||
authorized("auth/google/link-url")
|
||||
.post(json.encodeToString(OAuthLinkUrlRequest.serializer(), OAuthLinkUrlRequest()).toRequestBody(JsonMedia))
|
||||
.build(),
|
||||
)
|
||||
return decode(OAuthLinkUrlResponse.serializer(), raw)
|
||||
}
|
||||
|
||||
suspend fun linkExistingOAuthAccount(pendingToken: String, email: String, password: String): LoginResponse {
|
||||
val raw = execute(
|
||||
Request.Builder()
|
||||
.url(endpoint("auth/oauth/link-existing"))
|
||||
.post(
|
||||
json.encodeToString(
|
||||
OAuthLinkExistingRequest.serializer(),
|
||||
OAuthLinkExistingRequest(pendingToken, email, password),
|
||||
).toRequestBody(JsonMedia),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
return decode(LoginResponse.serializer(), raw)
|
||||
}
|
||||
|
||||
suspend fun getCurrentState(): CurrentStateResponse {
|
||||
val raw = execute(authorized("time-entries/current-state").get().build())
|
||||
return decode(CurrentStateResponse.serializer(), raw)
|
||||
|
||||
@@ -63,6 +63,39 @@ class AuthRepository(
|
||||
}
|
||||
}
|
||||
|
||||
fun googleLoginUrl(): String = api.googleLoginUrl()
|
||||
|
||||
suspend fun createGoogleLinkUrl(): Result<String> =
|
||||
try {
|
||||
val res = api.createGoogleLinkUrl()
|
||||
if (res.success && !res.url.isNullOrBlank()) {
|
||||
Result.success(res.url)
|
||||
} else {
|
||||
Result.failure(Exception(res.error ?: "Google-Verknüpfung konnte nicht gestartet werden"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
|
||||
suspend fun completeOAuthLogin(token: String): Result<UserProfile> {
|
||||
tokenStore.saveToken(token)
|
||||
val user = restoreSession()
|
||||
return if (user != null) Result.success(user) else Result.failure(Exception("Google-Login konnte nicht abgeschlossen werden"))
|
||||
}
|
||||
|
||||
suspend fun linkExistingOAuthAccount(pendingToken: String, email: String, password: String): Result<UserProfile> =
|
||||
try {
|
||||
val res = api.linkExistingOAuthAccount(pendingToken, email.trim(), password)
|
||||
if (res.success && !res.token.isNullOrBlank() && res.user != null) {
|
||||
tokenStore.saveToken(res.token)
|
||||
Result.success(res.user.toProfile())
|
||||
} else {
|
||||
Result.failure(Exception(res.error ?: "Google-Verknüpfung fehlgeschlagen"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
try {
|
||||
api.postLogout()
|
||||
|
||||
@@ -20,6 +20,14 @@ class SettingsRepository(
|
||||
suspend fun updateProfile(fullName: String, stateId: String?, weekWorkdays: Int, dailyHours: Double, preferredTitleType: Int) =
|
||||
api.updateProfile(ProfileUpdateRequest(fullName, stateId, weekWorkdays, dailyHours, preferredTitleType))
|
||||
|
||||
suspend fun createGoogleLinkUrl(): String {
|
||||
val response = api.createGoogleLinkUrl()
|
||||
if (!response.success || response.url.isNullOrBlank()) {
|
||||
throw IllegalStateException(response.error ?: "Google-Verknüpfung konnte nicht gestartet werden")
|
||||
}
|
||||
return response.url
|
||||
}
|
||||
|
||||
suspend fun changePassword(oldPassword: String, newPassword: String, confirmPassword: String) =
|
||||
api.changePassword(PasswordChangeRequest(oldPassword, newPassword, confirmPassword))
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.tsschulz.timeclock.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -19,6 +21,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -77,12 +80,24 @@ fun TimeClockApp(
|
||||
val bookingState by bookingViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val settingsState by settingsViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val adminState by adminViewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val openUrl: (String) -> Unit = { url ->
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}
|
||||
LaunchedEffect(settingsState.googleLinkUrl) {
|
||||
settingsState.googleLinkUrl?.let {
|
||||
openUrl(it)
|
||||
settingsViewModel.consumeGoogleLinkUrl()
|
||||
}
|
||||
}
|
||||
|
||||
if (!authState.isAuthenticated) {
|
||||
LaunchedEffect(Unit) { timeViewModel.stop() }
|
||||
LoginScreen(
|
||||
state = authState,
|
||||
onLogin = { e, p, action -> authViewModel.login(e, p, action) },
|
||||
onGoogleLogin = { openUrl(authViewModel.googleLoginUrl()) },
|
||||
onLinkExistingGoogleAccount = { e, p -> authViewModel.linkExistingOAuthAccount(e, p) },
|
||||
onRetryBootstrap = { authViewModel.retryBootstrap() },
|
||||
)
|
||||
return
|
||||
@@ -93,6 +108,8 @@ fun TimeClockApp(
|
||||
LoginScreen(
|
||||
state = authState,
|
||||
onLogin = { e, p, action -> authViewModel.login(e, p, action) },
|
||||
onGoogleLogin = { openUrl(authViewModel.googleLoginUrl()) },
|
||||
onLinkExistingGoogleAccount = { e, p -> authViewModel.linkExistingOAuthAccount(e, p) },
|
||||
onRetryBootstrap = { authViewModel.retryBootstrap() },
|
||||
)
|
||||
return
|
||||
@@ -238,6 +255,7 @@ private fun DemoScreen(
|
||||
onSave = { name, stateId, weekWorkdays, dailyHours, titleType ->
|
||||
settingsViewModel.updateProfile(name, stateId, weekWorkdays, dailyHours, titleType)
|
||||
},
|
||||
onLinkGoogle = { settingsViewModel.startGoogleLink() },
|
||||
)
|
||||
}
|
||||
AppRoute.Password -> PasswordScreen(
|
||||
|
||||
@@ -20,6 +20,9 @@ data class AuthUiState(
|
||||
val user: UserProfile? = null,
|
||||
val loginInProgress: Boolean = false,
|
||||
val error: String? = null,
|
||||
val googleLoginUrl: String? = null,
|
||||
val oauthPendingToken: String? = null,
|
||||
val oauthPendingEmail: String? = null,
|
||||
/** Gespeicherter Token, aber `/auth/me` ist fehlgeschlagen (z. B. offline). */
|
||||
val bootstrapWarn: String? = null,
|
||||
)
|
||||
@@ -35,6 +38,8 @@ class AuthViewModel(
|
||||
viewModelScope.launch { runBootstrap() }
|
||||
}
|
||||
|
||||
fun googleLoginUrl(): String = repository.googleLoginUrl()
|
||||
|
||||
private suspend fun runBootstrap() {
|
||||
val user = repository.restoreSession()
|
||||
val warn = if (user == null && repository.hasStoredToken()) {
|
||||
@@ -88,6 +93,69 @@ class AuthViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun completeOAuthToken(token: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(loginInProgress = true, error = null) }
|
||||
repository.completeOAuthLogin(token).fold(
|
||||
onSuccess = { user ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
loginInProgress = false,
|
||||
isAuthenticated = true,
|
||||
user = user,
|
||||
error = null,
|
||||
oauthPendingToken = null,
|
||||
oauthPendingEmail = null,
|
||||
bootstrapWarn = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.update { it.copy(loginInProgress = false, error = e.message ?: "Google-Login fehlgeschlagen") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun requireOAuthLink(pendingToken: String, email: String?) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
bootstrapping = false,
|
||||
loginInProgress = false,
|
||||
oauthPendingToken = pendingToken,
|
||||
oauthPendingEmail = email,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun linkExistingOAuthAccount(email: String, password: String) {
|
||||
val pendingToken = _uiState.value.oauthPendingToken
|
||||
if (pendingToken.isNullOrBlank()) {
|
||||
_uiState.update { it.copy(error = "Keine Google-Verknüpfung offen") }
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(loginInProgress = true, error = null) }
|
||||
repository.linkExistingOAuthAccount(pendingToken, email, password).fold(
|
||||
onSuccess = { user ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
loginInProgress = false,
|
||||
isAuthenticated = true,
|
||||
user = user,
|
||||
oauthPendingToken = null,
|
||||
oauthPendingEmail = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.update { it.copy(loginInProgress = false, error = e.message ?: "Google-Verknüpfung fehlgeschlagen") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun retryBootstrap() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(bootstrapping = true, bootstrapWarn = null) }
|
||||
|
||||
@@ -49,11 +49,15 @@ import de.tsschulz.timeclock.ui.theme.TcSpacing
|
||||
fun LoginScreen(
|
||||
state: AuthUiState,
|
||||
onLogin: (email: String, password: String, action: String) -> Unit,
|
||||
onGoogleLogin: () -> Unit,
|
||||
onLinkExistingGoogleAccount: (email: String, password: String) -> Unit,
|
||||
onRetryBootstrap: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var email by rememberSaveable { mutableStateOf("") }
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
var linkEmail by rememberSaveable { mutableStateOf("") }
|
||||
var linkPassword by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
@@ -139,45 +143,79 @@ fun LoginScreen(
|
||||
|
||||
state.error?.let { AuthErrorBanner(message = it) }
|
||||
|
||||
AuthFormRow(
|
||||
label = "E-Mail-Adresse",
|
||||
horizontal = useWideFormRows,
|
||||
) {
|
||||
TcTextField(
|
||||
label = "",
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
placeholder = "Ihre E-Mail-Adresse eingeben",
|
||||
showLabel = false,
|
||||
if (!state.oauthPendingToken.isNullOrBlank()) {
|
||||
if (linkEmail.isBlank()) linkEmail = state.oauthPendingEmail.orEmpty()
|
||||
Text(
|
||||
text = "Für ${state.oauthPendingEmail ?: "dieses Google-Konto"} existiert bereits ein Account. Melden Sie sich mit dem bestehenden Account an, um ihn zu verknüpfen.",
|
||||
color = TcColors.Text,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
AuthFormRow(
|
||||
label = "Passwort",
|
||||
horizontal = useWideFormRows,
|
||||
) {
|
||||
TcTextField(
|
||||
label = "",
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
placeholder = "Ihr Passwort eingeben",
|
||||
isPassword = true,
|
||||
showLabel = false,
|
||||
)
|
||||
}
|
||||
AuthFormRow(label = "E-Mail-Adresse", horizontal = useWideFormRows) {
|
||||
TcTextField(label = "", value = linkEmail, onValueChange = { linkEmail = it }, showLabel = false)
|
||||
}
|
||||
AuthFormRow(label = "Passwort", horizontal = useWideFormRows) {
|
||||
TcTextField(label = "", value = linkPassword, onValueChange = { linkPassword = it }, isPassword = true, showLabel = false)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
TcButton(
|
||||
text = if (state.loginInProgress) "Wird verknüpft…" else "Bestehenden Account verknüpfen",
|
||||
variant = ButtonVariant.Primary,
|
||||
onClick = { onLinkExistingGoogleAccount(linkEmail, linkPassword) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
AuthFormRow(
|
||||
label = "E-Mail-Adresse",
|
||||
horizontal = useWideFormRows,
|
||||
) {
|
||||
TcTextField(
|
||||
label = "",
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
placeholder = "Ihre E-Mail-Adresse eingeben",
|
||||
showLabel = false,
|
||||
)
|
||||
}
|
||||
AuthFormRow(
|
||||
label = "Passwort",
|
||||
horizontal = useWideFormRows,
|
||||
) {
|
||||
TcTextField(
|
||||
label = "",
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
placeholder = "Ihr Passwort eingeben",
|
||||
isPassword = true,
|
||||
showLabel = false,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TcButton(
|
||||
text = if (state.loginInProgress) "Wird eingeloggt…" else "Einloggen",
|
||||
variant = ButtonVariant.Primary,
|
||||
onClick = { onLogin(email, password, "0") },
|
||||
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 10.dp),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TcButton(
|
||||
text = if (state.loginInProgress) "Wird eingeloggt…" else "Einloggen",
|
||||
variant = ButtonVariant.Primary,
|
||||
onClick = { onLogin(email, password, "0") },
|
||||
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 10.dp),
|
||||
)
|
||||
}
|
||||
|
||||
OAuthDivider()
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
TcButton(
|
||||
text = "Mit Google anmelden",
|
||||
variant = ButtonVariant.Default,
|
||||
onClick = onGoogleLogin,
|
||||
modifier = Modifier.defaultMinSize(minWidth = 220.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +226,19 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OAuthDivider() {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TcSpacing.Md),
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f), color = TcColors.Border)
|
||||
Text("oder", color = TcColors.DividerText, fontSize = 14.sp)
|
||||
HorizontalDivider(modifier = Modifier.weight(1f), color = TcColors.Border)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoginTopBar() {
|
||||
Row(
|
||||
|
||||
@@ -49,6 +49,7 @@ fun ProfileScreen(
|
||||
state: SettingsUiState,
|
||||
isTablet: Boolean,
|
||||
onSave: (String, String?, Int, Double, Int) -> Unit,
|
||||
onLinkGoogle: () -> Unit,
|
||||
) {
|
||||
val profile = state.profile
|
||||
var fullName by rememberSaveable { mutableStateOf("") }
|
||||
@@ -80,6 +81,8 @@ fun ProfileScreen(
|
||||
TcTextField("Arbeitstage pro Woche", weekWorkdays, { weekWorkdays = it }, placeholder = "5")
|
||||
TcTextField("Stunden pro Tag", dailyHours, { dailyHours = it }, placeholder = "8.0")
|
||||
TitleTypeDropdown(preferredTitleType, { preferredTitleType = it })
|
||||
FieldLabel("Google-Anmeldung")
|
||||
TcButton("Mit Google-Konto verknüpfen", variant = ButtonVariant.Default, onClick = onLinkGoogle)
|
||||
TcButton("Speichern", variant = ButtonVariant.Primary, onClick = {
|
||||
onSave(
|
||||
fullName,
|
||||
|
||||
@@ -27,6 +27,7 @@ data class SettingsUiState(
|
||||
val timewishes: List<TimewishDto> = emptyList(),
|
||||
val invites: List<InvitationDto> = emptyList(),
|
||||
val watchers: List<WatcherDto> = emptyList(),
|
||||
val googleLinkUrl: String? = null,
|
||||
)
|
||||
|
||||
class SettingsViewModel(
|
||||
@@ -48,6 +49,19 @@ class SettingsViewModel(
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
fun startGoogleLink() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(loading = true, error = null, googleLinkUrl = null) }
|
||||
runCatching { repository.createGoogleLinkUrl() }
|
||||
.onSuccess { url -> _uiState.update { it.copy(loading = false, googleLinkUrl = url) } }
|
||||
.onFailure { e -> _uiState.update { it.copy(loading = false, error = e.message ?: "Google-Verknüpfung konnte nicht gestartet werden") } }
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeGoogleLinkUrl() {
|
||||
_uiState.update { it.copy(googleLinkUrl = null) }
|
||||
}
|
||||
|
||||
fun changePassword(oldPassword: String, newPassword: String, confirmPassword: String) = launchMutation("Passwort geändert") {
|
||||
repository.changePassword(oldPassword, newPassword, confirmPassword)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user