getting started auth
This commit is contained in:
@@ -47,6 +47,7 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
implementation(libs.okhttp)
|
||||
@@ -54,8 +55,6 @@ dependencies {
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.auth.ktx)
|
||||
implementation(libs.kotlinx.coroutines.play.services)
|
||||
implementation(platform("com.google.firebase:firebase-bom:34.8.0"))
|
||||
implementation(libs.google.firebase.auth.ktx)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -4,13 +4,14 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.android.trisolarispms.ui.auth.AuthScreen
|
||||
import com.android.trisolarispms.ui.auth.AuthViewModel
|
||||
import com.android.trisolarispms.ui.auth.NameScreen
|
||||
import com.android.trisolarispms.ui.auth.UnauthorizedScreen
|
||||
import com.android.trisolarispms.ui.home.HomeScreen
|
||||
import com.android.trisolarispms.ui.theme.TrisolarisPMSTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@@ -19,29 +20,22 @@ class MainActivity : ComponentActivity() {
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
TrisolarisPMSTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
val authViewModel: AuthViewModel = viewModel()
|
||||
val state by authViewModel.state.collectAsState()
|
||||
|
||||
if (state.unauthorized) {
|
||||
UnauthorizedScreen(
|
||||
message = state.error ?: "Not authorized. Contact admin.",
|
||||
onSignOut = authViewModel::signOut
|
||||
)
|
||||
} else if (state.apiVerified && state.needsName) {
|
||||
NameScreen(viewModel = authViewModel)
|
||||
} else if (state.apiVerified) {
|
||||
HomeScreen(userId = state.userId, userName = state.userName)
|
||||
} else {
|
||||
AuthScreen(viewModel = authViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
TrisolarisPMSTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.android.trisolarispms.data.api
|
||||
|
||||
interface ApiService :
|
||||
AuthApi,
|
||||
OrgApi,
|
||||
PropertyApi,
|
||||
RoomTypeApi,
|
||||
RoomApi,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.android.trisolarispms.data.api
|
||||
|
||||
import com.android.trisolarispms.data.api.model.AuthVerifyResponse
|
||||
import com.android.trisolarispms.data.api.model.AuthMeUpdateRequest
|
||||
import com.android.trisolarispms.data.api.model.AppUserDto
|
||||
import com.android.trisolarispms.data.api.model.UserDto
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Body
|
||||
|
||||
interface AuthApi {
|
||||
@POST("auth/verify")
|
||||
@@ -12,4 +16,7 @@ interface AuthApi {
|
||||
|
||||
@GET("auth/me")
|
||||
suspend fun me(): Response<UserDto>
|
||||
|
||||
@PUT("auth/me")
|
||||
suspend fun updateMe(@Body body: AuthMeUpdateRequest): Response<AppUserDto>
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.android.trisolarispms.data.api
|
||||
|
||||
import com.android.trisolarispms.data.api.model.OrgCreateRequest
|
||||
import com.android.trisolarispms.data.api.model.OrgDto
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface OrgApi {
|
||||
@POST("orgs")
|
||||
suspend fun createOrg(@Body body: OrgCreateRequest): Response<OrgDto>
|
||||
|
||||
@GET("orgs/{orgId}")
|
||||
suspend fun getOrg(@Path("orgId") orgId: String): Response<OrgDto>
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.android.trisolarispms.data.api
|
||||
|
||||
import com.android.trisolarispms.data.api.model.ActionResponse
|
||||
import com.android.trisolarispms.data.api.model.PropertyCreateRequest
|
||||
import com.android.trisolarispms.data.api.model.PropertyDto
|
||||
import com.android.trisolarispms.data.api.model.PropertyUpdateRequest
|
||||
import com.android.trisolarispms.data.api.model.UserDto
|
||||
import com.android.trisolarispms.data.api.model.UserRolesUpdateRequest
|
||||
import com.android.trisolarispms.data.api.model.ActionResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
@@ -15,14 +15,11 @@ import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface PropertyApi {
|
||||
@POST("orgs/{orgId}/properties")
|
||||
suspend fun createProperty(
|
||||
@Path("orgId") orgId: String,
|
||||
@Body body: PropertyCreateRequest
|
||||
): Response<PropertyDto>
|
||||
@POST("properties")
|
||||
suspend fun createProperty(@Body body: PropertyCreateRequest): Response<PropertyDto>
|
||||
|
||||
@GET("orgs/{orgId}/properties")
|
||||
suspend fun listProperties(@Path("orgId") orgId: String): Response<List<PropertyDto>>
|
||||
@GET("properties")
|
||||
suspend fun listProperties(): Response<List<PropertyDto>>
|
||||
|
||||
@PUT("properties/{propertyId}")
|
||||
suspend fun updateProperty(
|
||||
@@ -30,9 +27,6 @@ interface PropertyApi {
|
||||
@Body body: PropertyUpdateRequest
|
||||
): Response<PropertyDto>
|
||||
|
||||
@GET("orgs/{orgId}/users")
|
||||
suspend fun listOrgUsers(@Path("orgId") orgId: String): Response<List<UserDto>>
|
||||
|
||||
@GET("properties/{propertyId}/users")
|
||||
suspend fun listPropertyUsers(@Path("propertyId") propertyId: String): Response<List<UserDto>>
|
||||
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
package com.android.trisolarispms.data.api.model
|
||||
|
||||
data class AuthVerifyResponse(
|
||||
val user: UserDto? = null,
|
||||
val orgId: String? = null,
|
||||
val propertyId: String? = null
|
||||
val status: String? = null,
|
||||
val user: AppUserDto? = null,
|
||||
val properties: List<PropertyUserDto>? = null
|
||||
)
|
||||
|
||||
data class AuthMeUpdateRequest(
|
||||
val name: String
|
||||
)
|
||||
|
||||
data class AppUserDto(
|
||||
val id: String? = null,
|
||||
val firebaseUid: String? = null,
|
||||
val phoneE164: String? = null,
|
||||
val name: String? = null,
|
||||
val disabled: Boolean? = null,
|
||||
val superAdmin: Boolean? = null
|
||||
)
|
||||
|
||||
data class PropertyUserDto(
|
||||
val userId: String? = null,
|
||||
val propertyId: String? = null,
|
||||
val roles: List<String>? = null
|
||||
)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.android.trisolarispms.data.api.model
|
||||
|
||||
data class OrgCreateRequest(
|
||||
val name: String,
|
||||
val emailAliases: List<String>? = null,
|
||||
val allowedTransportModes: List<String>? = null
|
||||
)
|
||||
|
||||
data class OrgDto(
|
||||
val id: String? = null,
|
||||
val name: String? = null,
|
||||
val emailAliases: List<String>? = null,
|
||||
val allowedTransportModes: List<String>? = null
|
||||
)
|
||||
@@ -25,7 +25,6 @@ data class PropertyUpdateRequest(
|
||||
|
||||
data class PropertyDto(
|
||||
val id: String? = null,
|
||||
val orgId: String? = null,
|
||||
val code: String? = null,
|
||||
val name: String? = null,
|
||||
val addressText: String? = null,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.android.trisolarispms.ui.auth
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
|
||||
@Composable
|
||||
fun AuthScreen(viewModel: AuthViewModel = viewModel()) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
val activity = context as? ComponentActivity
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(text = "Sign in", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.phone,
|
||||
onValueChange = viewModel::onPhoneChange,
|
||||
label = { Text("Phone number") },
|
||||
placeholder = { Text("10-digit mobile") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
|
||||
prefix = { Text(state.countryCode) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = "Default country: India (+91)",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (activity != null) {
|
||||
viewModel.sendCode(activity)
|
||||
} else {
|
||||
viewModel.reportError("Unable to access activity for phone auth")
|
||||
}
|
||||
},
|
||||
enabled = !state.isLoading
|
||||
) {
|
||||
Text(if (state.isCodeSent) "Resend code" else "Send code")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.code,
|
||||
onValueChange = viewModel::onCodeChange,
|
||||
label = { Text("Verification code") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Button(
|
||||
onClick = viewModel::verifyCode,
|
||||
enabled = state.isCodeSent && !state.isLoading
|
||||
) {
|
||||
Text("Verify")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
state.error?.let { message ->
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(text = message)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = viewModel::clearError,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Text("Dismiss")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.userId != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = "Firebase user: ${state.userId}")
|
||||
Text(text = "API verified: ${state.apiVerified}")
|
||||
}
|
||||
|
||||
if (state.noProperties) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "No properties yet. Create a property to continue.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.android.trisolarispms.ui.auth
|
||||
|
||||
data class AuthUiState(
|
||||
val countryCode: String = "+91",
|
||||
val phone: String = "",
|
||||
val code: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val isCodeSent: Boolean = false,
|
||||
val verificationId: String? = null,
|
||||
val error: String? = null,
|
||||
val userId: String? = null,
|
||||
val apiVerified: Boolean = false,
|
||||
val isSuperAdmin: Boolean = false,
|
||||
val noProperties: Boolean = false,
|
||||
val userName: String? = null,
|
||||
val nameInput: String = "",
|
||||
val needsName: Boolean = false,
|
||||
val unauthorized: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.android.trisolarispms.ui.auth
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.trisolarispms.data.api.ApiClient
|
||||
import com.google.firebase.FirebaseException
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.auth.PhoneAuthCredential
|
||||
import com.google.firebase.auth.PhoneAuthOptions
|
||||
import com.google.firebase.auth.PhoneAuthProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AuthViewModel(
|
||||
private val auth: FirebaseAuth = FirebaseAuth.getInstance()
|
||||
) : ViewModel() {
|
||||
private val _state = MutableStateFlow(AuthUiState())
|
||||
val state: StateFlow<AuthUiState> = _state
|
||||
|
||||
private var resendToken: PhoneAuthProvider.ForceResendingToken? = null
|
||||
|
||||
init {
|
||||
val user = auth.currentUser
|
||||
if (user != null) {
|
||||
verifyExistingSession(user.uid)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPhoneChange(value: String) {
|
||||
val digits = value.filter { it.isDigit() }.take(10)
|
||||
_state.update { it.copy(phone = digits, error = null) }
|
||||
}
|
||||
|
||||
fun onCodeChange(value: String) {
|
||||
_state.update { it.copy(code = value, error = null) }
|
||||
}
|
||||
|
||||
fun onNameChange(value: String) {
|
||||
_state.update { it.copy(nameInput = value, error = null) }
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_state.update { it.copy(error = null) }
|
||||
}
|
||||
|
||||
fun reportError(message: String) {
|
||||
setError(message)
|
||||
}
|
||||
|
||||
private fun setError(message: String) {
|
||||
_state.update { it.copy(isLoading = false, error = message) }
|
||||
}
|
||||
|
||||
fun sendCode(activity: ComponentActivity) {
|
||||
val phone = buildE164Phone()
|
||||
if (phone == null) {
|
||||
setError("Enter a valid 10-digit phone number")
|
||||
return
|
||||
}
|
||||
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
|
||||
val callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
|
||||
override fun onVerificationCompleted(credential: PhoneAuthCredential) {
|
||||
signInWithCredential(credential)
|
||||
}
|
||||
|
||||
override fun onVerificationFailed(e: FirebaseException) {
|
||||
setError(e.localizedMessage ?: "Verification failed")
|
||||
}
|
||||
|
||||
override fun onCodeSent(
|
||||
verificationId: String,
|
||||
token: PhoneAuthProvider.ForceResendingToken
|
||||
) {
|
||||
resendToken = token
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
isCodeSent = true,
|
||||
verificationId = verificationId,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val options = PhoneAuthOptions.newBuilder(auth)
|
||||
.setPhoneNumber(phone)
|
||||
.setTimeout(60L, TimeUnit.SECONDS)
|
||||
.setActivity(activity)
|
||||
.setCallbacks(callbacks)
|
||||
.apply {
|
||||
resendToken?.let { setForceResendingToken(it) }
|
||||
}
|
||||
.build()
|
||||
|
||||
PhoneAuthProvider.verifyPhoneNumber(options)
|
||||
}
|
||||
|
||||
private fun buildE164Phone(): String? {
|
||||
val digits = state.value.phone.trim()
|
||||
if (digits.length != 10) return null
|
||||
return "${state.value.countryCode}$digits"
|
||||
}
|
||||
|
||||
fun verifyCode() {
|
||||
val verificationId = state.value.verificationId
|
||||
val code = state.value.code.trim()
|
||||
|
||||
if (verificationId.isNullOrBlank()) {
|
||||
setError("Request a code first")
|
||||
return
|
||||
}
|
||||
if (code.isBlank()) {
|
||||
setError("Enter the code")
|
||||
return
|
||||
}
|
||||
|
||||
val credential = PhoneAuthProvider.getCredential(verificationId, code)
|
||||
signInWithCredential(credential)
|
||||
}
|
||||
|
||||
private fun signInWithCredential(credential: PhoneAuthCredential) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val result = auth.signInWithCredential(credential).await()
|
||||
val userId = result.user?.uid
|
||||
val api = ApiClient.create(auth = auth)
|
||||
val response = api.verifyAuth()
|
||||
handleVerifyResponse(userId, response)
|
||||
} catch (e: Exception) {
|
||||
setError(e.localizedMessage ?: "Sign-in failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyExistingSession(userId: String) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null, userId = userId) }
|
||||
try {
|
||||
val api = ApiClient.create(auth = auth)
|
||||
val response = api.verifyAuth()
|
||||
handleVerifyResponse(userId, response)
|
||||
} catch (e: Exception) {
|
||||
setError(e.localizedMessage ?: "Session verify failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerifyResponse(
|
||||
userId: String?,
|
||||
response: retrofit2.Response<com.android.trisolarispms.data.api.model.AuthVerifyResponse>
|
||||
) {
|
||||
val body = response.body()
|
||||
val userName = body?.user?.name
|
||||
val isSuperAdmin = body?.user?.superAdmin == true
|
||||
when {
|
||||
response.isSuccessful && (body?.status == "OK" || body?.status == "SUPER_ADMIN") -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
userId = userId,
|
||||
apiVerified = true,
|
||||
needsName = userName.isNullOrBlank(),
|
||||
nameInput = userName ?: "",
|
||||
userName = userName,
|
||||
isSuperAdmin = isSuperAdmin,
|
||||
noProperties = body?.status == "NO_PROPERTIES",
|
||||
unauthorized = false,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
response.isSuccessful && body?.status == "NO_PROPERTIES" -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
userId = userId,
|
||||
apiVerified = true,
|
||||
needsName = userName.isNullOrBlank(),
|
||||
nameInput = userName ?: "",
|
||||
userName = userName,
|
||||
isSuperAdmin = isSuperAdmin,
|
||||
noProperties = true,
|
||||
unauthorized = false,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
response.code() == 401 -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
apiVerified = false,
|
||||
isSuperAdmin = false,
|
||||
noProperties = false,
|
||||
unauthorized = true,
|
||||
error = "Not authorized. Contact admin."
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
apiVerified = false,
|
||||
isSuperAdmin = false,
|
||||
noProperties = false,
|
||||
unauthorized = false,
|
||||
error = "API verify failed: ${response.code()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun signOut() {
|
||||
auth.signOut()
|
||||
_state.update { AuthUiState() }
|
||||
}
|
||||
|
||||
fun submitName() {
|
||||
val name = state.value.nameInput.trim()
|
||||
if (name.isBlank()) {
|
||||
setError("Please enter your name")
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val api = ApiClient.create(auth = auth)
|
||||
val body = com.android.trisolarispms.data.api.model.AuthMeUpdateRequest(name = name)
|
||||
val response = api.updateMe(body)
|
||||
if (response.isSuccessful) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
needsName = false,
|
||||
userName = name,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setError("Update failed: ${response.code()}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
setError(e.localizedMessage ?: "Update failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.android.trisolarispms.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
|
||||
@Composable
|
||||
fun NameScreen(viewModel: AuthViewModel = viewModel()) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(text = "Finish setup", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.nameInput,
|
||||
onValueChange = viewModel::onNameChange,
|
||||
label = { Text("Your name") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Button(
|
||||
onClick = viewModel::submitName,
|
||||
enabled = !state.isLoading
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
|
||||
if (state.isLoading) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
state.error?.let {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.android.trisolarispms.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun UnauthorizedScreen(message: String, onSignOut: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(text = "Not authorized", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = message, style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(onClick = onSignOut, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Sign out")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.android.trisolarispms.ui.home
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(userId: String?, userName: String?) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
val title = if (!userName.isNullOrBlank()) "Welcome, $userName" else "Welcome"
|
||||
Text(text = title, style = MaterialTheme.typography.headlineMedium)
|
||||
if (userId != null) {
|
||||
Text(text = "User: $userId", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user