diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d07ff18..2ee308c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/android/trisolarispms/MainActivity.kt b/app/src/main/java/com/android/trisolarispms/MainActivity.kt index 106734e..d99de20 100644 --- a/app/src/main/java/com/android/trisolarispms/MainActivity.kt +++ b/app/src/main/java/com/android/trisolarispms/MainActivity.kt @@ -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") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/android/trisolarispms/data/api/ApiService.kt b/app/src/main/java/com/android/trisolarispms/data/api/ApiService.kt index 39a0277..e9f0076 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/ApiService.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/ApiService.kt @@ -2,7 +2,6 @@ package com.android.trisolarispms.data.api interface ApiService : AuthApi, - OrgApi, PropertyApi, RoomTypeApi, RoomApi, diff --git a/app/src/main/java/com/android/trisolarispms/data/api/AuthApi.kt b/app/src/main/java/com/android/trisolarispms/data/api/AuthApi.kt index 0ecaf28..ddef436 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/AuthApi.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/AuthApi.kt @@ -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 + + @PUT("auth/me") + suspend fun updateMe(@Body body: AuthMeUpdateRequest): Response } diff --git a/app/src/main/java/com/android/trisolarispms/data/api/OrgApi.kt b/app/src/main/java/com/android/trisolarispms/data/api/OrgApi.kt deleted file mode 100644 index ddcc903..0000000 --- a/app/src/main/java/com/android/trisolarispms/data/api/OrgApi.kt +++ /dev/null @@ -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 - - @GET("orgs/{orgId}") - suspend fun getOrg(@Path("orgId") orgId: String): Response -} diff --git a/app/src/main/java/com/android/trisolarispms/data/api/PropertyApi.kt b/app/src/main/java/com/android/trisolarispms/data/api/PropertyApi.kt index bb212be..45ad3ee 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/PropertyApi.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/PropertyApi.kt @@ -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 + @POST("properties") + suspend fun createProperty(@Body body: PropertyCreateRequest): Response - @GET("orgs/{orgId}/properties") - suspend fun listProperties(@Path("orgId") orgId: String): Response> + @GET("properties") + suspend fun listProperties(): Response> @PUT("properties/{propertyId}") suspend fun updateProperty( @@ -30,9 +27,6 @@ interface PropertyApi { @Body body: PropertyUpdateRequest ): Response - @GET("orgs/{orgId}/users") - suspend fun listOrgUsers(@Path("orgId") orgId: String): Response> - @GET("properties/{propertyId}/users") suspend fun listPropertyUsers(@Path("propertyId") propertyId: String): Response> diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/AuthModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/AuthModels.kt index 53ee367..a3555b0 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/AuthModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/AuthModels.kt @@ -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? = 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? = null ) diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/OrgModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/OrgModels.kt deleted file mode 100644 index 9571826..0000000 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/OrgModels.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.trisolarispms.data.api.model - -data class OrgCreateRequest( - val name: String, - val emailAliases: List? = null, - val allowedTransportModes: List? = null -) - -data class OrgDto( - val id: String? = null, - val name: String? = null, - val emailAliases: List? = null, - val allowedTransportModes: List? = null -) diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/PropertyModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/PropertyModels.kt index 7f6b5a0..354700a 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/PropertyModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/PropertyModels.kt @@ -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, diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthScreen.kt new file mode 100644 index 0000000..5ae180c --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthScreen.kt @@ -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 + ) + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthUiState.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthUiState.kt new file mode 100644 index 0000000..4a3d84b --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthUiState.kt @@ -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 +) diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt new file mode 100644 index 0000000..293c190 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt @@ -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 = _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 + ) { + 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") + } + } + } + +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/NameScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/NameScreen.kt new file mode 100644 index 0000000..85277c7 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/NameScreen.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/UnauthorizedScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/UnauthorizedScreen.kt new file mode 100644 index 0000000..2739751 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/UnauthorizedScreen.kt @@ -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") + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt new file mode 100644 index 0000000..1d162d1 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt @@ -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) + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 1263c00..99baeb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,5 +2,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.compose) apply false - id("com.google.gms.google-services") version "4.4.4" apply false + alias(libs.plugins.google.services) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a00e43..307b276 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,20 @@ [versions] agp = "9.0.0" -coreKtx = "1.10.1" +coreKtx = "1.17.0" +firebaseBomVersion = "34.8.0" junit = "4.13.2" -junitVersion = "1.1.5" -espressoCore = "3.5.1" -lifecycleRuntimeKtx = "2.6.1" -activityCompose = "1.8.0" -kotlin = "2.0.21" -composeBom = "2024.09.00" -retrofit = "2.11.0" -okhttp = "4.12.0" -firebaseBom = "33.5.1" -coroutinesPlayServices = "1.7.3" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +lifecycleRuntimeKtx = "2.10.0" +activityCompose = "1.12.2" +kotlin = "2.3.0" +composeBom = "2026.01.00" +retrofit = "3.0.0" +okhttp = "5.3.2" +firebaseBom = "34.8.0" +coroutinesPlayServices = "1.10.2" +googleServices = "4.4.4" +lifecycleViewModelCompose = "2.10.0" firebaseAuthKtx = "24.0.1" [libraries] @@ -20,6 +23,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewModelCompose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } @@ -34,10 +38,10 @@ retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter- okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } -firebase-auth-ktx = { group = "com.google.firebase", name = "firebase-auth-ktx" } +firebase-auth-ktx = { module = "com.google.firebase:firebase-auth" } kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "coroutinesPlayServices" } -google-firebase-auth-ktx = { group = "com.google.firebase", name = "firebase-auth-ktx", version.ref = "firebaseAuthKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }