diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37accdd..0f20126 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation("com.google.android.gms:play-services-maps:18.2.0") implementation("com.google.android.gms:play-services-location:21.2.0") implementation("com.google.android.libraries.places:places:3.4.0") + implementation("androidx.test.ext:junit-ktx:1.1.5") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -113,9 +114,12 @@ dependencies { implementation("com.squareup.retrofit2:converter-gson:2.10.0") implementation ("com.google.firebase:firebase-auth-ktx:22.3.1") - implementation ("com.google.android.gms:play-services-auth:21.0.0") + implementation ("com.google.android.gms:play-services-auth:21.1.0") //Okhttp implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + //mockito + implementation("org.mockito:mockito-core:4.0.0") } \ No newline at end of file diff --git a/app/src/main/java/com/samuelokello/kazihub/MainActivity.kt b/app/src/main/java/com/samuelokello/kazihub/MainActivity.kt index 8110d50..90df342 100644 --- a/app/src/main/java/com/samuelokello/kazihub/MainActivity.kt +++ b/app/src/main/java/com/samuelokello/kazihub/MainActivity.kt @@ -3,10 +3,13 @@ package com.samuelokello.kazihub import android.Manifest import android.content.pm.PackageManager import android.location.Location +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.annotation.RequiresExtension import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -18,6 +21,7 @@ import androidx.navigation.compose.rememberNavController import com.google.android.gms.maps.model.LatLng import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.rememberNavHostEngine +import com.samuelokello.kazihub.presentation.business.BusinessProfileViewModel import com.samuelokello.kazihub.presentation.shared.NavGraphs import com.samuelokello.kazihub.presentation.shared.components.StandardScaffold import com.samuelokello.kazihub.presentation.shared.destinations.HomeScreenDestination @@ -33,6 +37,7 @@ class MainActivity : ComponentActivity() { private val LOCATION_PERMISSION_REQUEST_CODE = 1 private lateinit var locationManager: LocationManager + private val businessProfileViewModel: BusinessProfileViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -97,9 +102,12 @@ class MainActivity : ComponentActivity() { // Handle the error } + @RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) override fun onLocationLatLngReceived(latLng: LatLng) { val latitude = latLng.latitude val longitude = latLng.longitude + + businessProfileViewModel.updateLocation(latitude, longitude) } }) } else { @@ -108,117 +116,3 @@ class MainActivity : ComponentActivity() { } } - -//@AndroidEntryPoint -//class MainActivity : ComponentActivity() { -// -// private val LOCATION_PERMISSION_REQUEST_CODE = 1 -// private lateinit var locationManager: LocationManager -// -// @RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) -// override fun onCreate(savedInstanceState: Bundle?) { -// super.onCreate(savedInstanceState) -// -// setContent { -// KaziHubTheme { -// // A surface container using the 'background' color from the theme -// Surface( -// modifier = Modifier.fillMaxSize(), -// color = MaterialTheme.colorScheme.background -// ) { -// val navController = rememberNavController() -// val navHostEngine = rememberNavHostEngine() -// -// val newBackStackEntry by navController.currentBackStackEntryAsState() -// val route = newBackStackEntry?.destination?.route -// -// StandardScaffold( -// navController = navController, -// showBottomBar = route in listOf( -// HomeScreenDestination.route, -// MessagesScreenDestination.route, -// ProfileScreenDestination.route, -// SettingsScreenDestination.route -// ) -// ) { -// DestinationsNavHost( -// navGraph = NavGraphs.root, -// navController = navController, -// engine = navHostEngine, -// ) -// } -// } -// } -// } -// } -// -// override fun onRequestPermissionsResult( -// requestCode: Int, -// permissions: Array, -// grantResults: IntArray -// ) { -// val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions()) { permissions -> -// permissions.entries.forEach { -// val permissionName = it.key -// val isGranted = it.value -// } -// -// } -// when (requestCode) { -// LOCATION_PERMISSION_REQUEST_CODE -> { -// if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { -// // Permission was granted, you can now get the user location -// locationManager.getUserLocation(object : LocationManager.LocationCallback { -// override fun onLocationReceived(location: Location) { -// // Handle the location -// } -// -// override fun onLocationError(errorMessage: String) { -// // Handle the error -// } -// }) -// } else { -// // Permission was denied, handle the error -// } -// return -// } -// } -// } -// -// sealed class PermissionRequestResult { -// object Granted : PermissionRequestResult() -// data class Denied(val deniedPermissions: List) : PermissionRequestResult() -// object Error : PermissionRequestResult() -// } -// -// val requestPermissionLauncher = registerForActivityResult( -// ActivityResultContracts.RequestMultiplePermissions() -// ) { result -> -// val permissionRequestResult = when { -// result.all { it.value } -> PermissionRequestResult.Granted -// result.any { !it.value } -> PermissionRequestResult.Denied(result.filter { !it.value }.keys.toList()) -// else -> PermissionRequestResult.Error -// } -// -// // Handle the permission request result -// -// when (permissionRequestResult) { -// PermissionRequestResult.Granted -> { -// // Permission was granted, get the user location -// locationManager.getUserLocation { location -> -// // Handle the location -// } -// } -// -// PermissionRequestResult.Denied -> { -// // Permission was denied, handle the error -// } -// -// PermissionRequestResult.Error -> { -// // An error occurred, handle the error -// } -// } -// } -// -// -//} diff --git a/app/src/main/java/com/samuelokello/kazihub/data/remote/KaziHubApi.kt b/app/src/main/java/com/samuelokello/kazihub/data/remote/KaziHubApi.kt index a30cb51..14a5468 100644 --- a/app/src/main/java/com/samuelokello/kazihub/data/remote/KaziHubApi.kt +++ b/app/src/main/java/com/samuelokello/kazihub/data/remote/KaziHubApi.kt @@ -18,7 +18,7 @@ interface KaziHubApi { suspend fun signUp(@Body signUpRequest: SignUpRequest): SignUpResponse @POST("auth/signin") - suspend fun sigIn(@Body signInRequest: com.samuelokello.kazihub.domain.model.shared.auth.sign_in.SignInRequest): SignInResponse + suspend fun signIn(@Body signInRequest: com.samuelokello.kazihub.domain.model.shared.auth.sign_in.SignInRequest): SignInResponse @POST("/business/profiles/create") suspend fun createBusinessProfile( @@ -36,16 +36,23 @@ interface KaziHubApi { @PUT("/business/profiles/{bus_profile_id}/update") suspend fun updateBusinessProfile( + @Header("Authorization") token: String, @Path("bus_profile_id") id: Int, @Body businessProfile: BusinessProfileRequest ): BusinessProfileResponse @PUT("/business/profiles/{bus_profile_id}/update/image") suspend fun updateBusinessProfileImage( + @Header("Authorization") token: String, @Path("bus_profile_id") id: Int, @Body businessProfile: BusinessProfileRequest ): BusinessProfileResponse + @PUT("/business/profiles/image/{profile_id}") + suspend fun getBusinessProfileImage( + @Path("profile_id") id: Int + ): BusinessProfileResponse + @GET("/business/verify/with_email/") suspend fun verifyBusinessByEmail( @Path("bus_profile_id") id: Int, diff --git a/app/src/main/java/com/samuelokello/kazihub/data/repository/KaziHubRepository.kt b/app/src/main/java/com/samuelokello/kazihub/data/repository/KaziHubRepository.kt index 063c35f..cdeb36c 100644 --- a/app/src/main/java/com/samuelokello/kazihub/data/repository/KaziHubRepository.kt +++ b/app/src/main/java/com/samuelokello/kazihub/data/repository/KaziHubRepository.kt @@ -1,12 +1,14 @@ package com.samuelokello.kazihub.data.repository -import android.content.Context import android.os.Build +import android.content.Context +import android.os.Build import android.util.Log import androidx.annotation.RequiresExtension import com.samuelokello.kazihub.data.model.sign_in.SignInResponse import com.samuelokello.kazihub.data.remote.KaziHubApi import com.samuelokello.kazihub.domain.model.Bussiness.BusinessProfileRequest import com.samuelokello.kazihub.domain.model.Bussiness.BusinessProfileResponse +import com.samuelokello.kazihub.domain.model.shared.auth.sign_in.SignInRequest import com.samuelokello.kazihub.domain.model.sign_up.SignUpRequest import com.samuelokello.kazihub.domain.model.sign_up.SignUpResponse import com.samuelokello.kazihub.utils.LocationManager @@ -37,9 +39,9 @@ class KaziHubRepository } - suspend fun signIn(signInRequest: com.samuelokello.kazihub.domain.model.shared.auth.sign_in.SignInRequest): Resource = withContext(Dispatchers.IO){ + suspend fun signIn(signInRequest: SignInRequest): Resource = withContext(Dispatchers.IO){ return@withContext try { - val response = api.sigIn(signInRequest) + val response = api.signIn(signInRequest) storeAccessToken(context, response.data?.accessToken!!) Log.d("AuthRepository", "signIn: ${response.data.accessToken}") Resource.Success(response) @@ -53,8 +55,10 @@ class KaziHubRepository return@withContext try { val token = getAccessToken(context) val response =if (token != null) { + Log.d("KaziHubRepository", "createBusinessProfile: $token") api.createBusinessProfile(" Bearer $token",request) } else { + Log.d("KaziHubRepository", "createBusinessProfile: Token is null") throw Exception("Token is null") } Resource.Success(response) diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessHomeScreen.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessHomeScreen.kt index c3bf369..4a772ad 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessHomeScreen.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessHomeScreen.kt @@ -1,35 +1,36 @@ package com.samuelokello.kazihub.presentation.business +import android.annotation.SuppressLint import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.DrawerValue +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.navigation.compose.rememberNavController import com.samuelokello.kazihub.presentation.shared.components.AppBar -import com.samuelokello.kazihub.presentation.shared.components.ReusableModalNavigationDrawer -import com.samuelokello.kazihub.presentation.shared.components.StandardScaffold import com.samuelokello.kazihub.ui.theme.KaziHubTheme +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun BusinessHomeScreen() { - val navcontroller = rememberNavController() + val navController = rememberNavController() val drawerState = rememberDrawerState(DrawerValue.Closed) - ReusableModalNavigationDrawer( - drawerState = drawerState, - drawerContent = { }, + Scaffold ( topBar = { - AppBar(onNavigationIconClick = { }) + AppBar(onNavigationIconClick = {}) }, - bottomBar = { - StandardScaffold( - navController = navcontroller, - content = { /*TODO*/ } - ) - } - ) { - Column { + modifier = Modifier.padding(bottom = 32.dp) + ){ + Column( + modifier = Modifier + .fillMaxSize() + ) { Text(text = "Business Home Screen") } } diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessProfileViewModel.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessProfileViewModel.kt index 5e8d72a..6676195 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessProfileViewModel.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/business/BusinessProfileViewModel.kt @@ -1,20 +1,23 @@ package com.samuelokello.kazihub.presentation.business -import android.Manifest import android.content.Context -import android.content.pm.PackageManager -import android.location.Location import android.os.Build +import android.util.Log import android.util.Patterns import androidx.annotation.RequiresExtension -import androidx.core.app.ActivityCompat +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationServices import com.google.android.gms.maps.model.LatLng +import com.google.android.libraries.places.api.Places +import com.google.android.libraries.places.api.model.AutocompletePrediction +import com.google.android.libraries.places.api.model.AutocompleteSessionToken +import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest +import com.google.android.libraries.places.api.net.PlacesClient import com.samuelokello.kazihub.data.repository.KaziHubRepository import com.samuelokello.kazihub.domain.model.Bussiness.BusinessProfileRequest +import com.samuelokello.kazihub.presentation.business.state.BusinessEvent +import com.samuelokello.kazihub.presentation.business.state.BusinessProfileState import com.samuelokello.kazihub.utils.LocationManager import com.samuelokello.kazihub.utils.Resource import dagger.hilt.android.lifecycle.HiltViewModel @@ -22,8 +25,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.tasks.await import javax.inject.Inject @RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) @@ -32,87 +35,146 @@ class BusinessProfileViewModel @Inject constructor( private val repository: KaziHubRepository, private val locationManager: LocationManager, @ApplicationContext private val context: Context -) : ViewModel() -{ - private val _profile = MutableStateFlow(BusinessProfileState()) - val profile = _profile.asStateFlow() - +) : ViewModel() { + private val _state = MutableStateFlow(BusinessProfileState()) + val state = _state.asStateFlow() + + private val sharedPreferences = context.getSharedPreferences("user", Context.MODE_PRIVATE) + + private val locationSuggestion = MutableLiveData>() + private fun showLoading() { + _state.update { + it.copy( + isLoading = true + ) + } + } - init { - fetchProfile() + private fun hideLoading() { + _state.update { + it.copy( + isLoading = false + ) + } } + fun getPlacesClient() = locationManager.getPlacesClient() - fun onEmailChange(email: String) { - _profile.value = _profile.value.copy(email = email) - } - fun onPhoneChange(newPhone: String) { - _profile.value = _profile.value.copy(phone = newPhone) - } - fun onBioChange(newBio: String) { - _profile.value = _profile.value.copy(bio = newBio) + fun updateLocation(latitude: Double, longitude: Double) { + _state.update { it.copy(locationLatLng = LatLng(latitude, longitude)) } } - fun onLocationChange(newLocation: String,placeId: String ) { - _profile.value = _profile.value.copy(location = newLocation, placeId = placeId) + fun onLocationChange(newLocation: String, placeId: String) { + _state.update { it.copy(location = newLocation, placeId = placeId) } if (placeId.isNotEmpty()) { viewModelScope.launch(Dispatchers.IO) { locationManager.getLatLngFromPlaceId(placeId, - callback = { latLng-> - _profile.value = _profile.value.copy(locationLatLng = latLng) + callback = { latLng -> + _state.update { it.copy(locationLatLng = latLng) } }, - errorCallback ={error -> + errorCallback = { error -> //handle error }) } } } - fun isEmailValid(email: String): Boolean { + private fun isEmailValid(email: String): Boolean { return Patterns.EMAIL_ADDRESS.matcher(email).matches() } - fun isPhoneValid(phone: String): Boolean { + private fun isPhoneValid(phone: String): Boolean { return Patterns.PHONE.matcher(phone).matches() } - fun isFormComplete(email: String,phone: String,location: String): Boolean{ + private fun isFormComplete(email: String, phone: String, location: String): Boolean { return isEmailValid(email) && isPhoneValid(phone) && location.isNotEmpty() } + fun onEvent(event: BusinessEvent) { + when (event) { + is BusinessEvent.OnEmailChanged -> { + _state.update { it.copy(email = event.email) } + } - @RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) - suspend fun createProfile() { - val email = _profile.value.email - val phone = _profile.value.phone - val bio = _profile.value.bio - val location = _profile.value.location - val locationLatLng = _profile.value.locationLatLng - val latitude = locationLatLng?.latitude ?: 0.0 - val longitude =locationLatLng?.longitude ?: 0.0 - - val request = BusinessProfileRequest( - email = email, - phone = phone, - bio = bio, - location = location, - latitude = latitude, - longitude = longitude - ) - viewModelScope.launch { - when (val response = repository.createBusinessProfile(request)) { - is Resource.Success -> { - _profile.value = _profile.value.copy(navigateToHome = true) - } + is BusinessEvent.OnLocationChanged -> { + _state.update { it.copy(location = event.location) } + } - is Resource.Error -> { - _profile.value = - _profile.value.copy(error = response.message ?: "An error occurred") - } + is BusinessEvent.OnPhoneNumberChanged -> { + _state.update { it.copy(phone = event.phone) } + } + + is BusinessEvent.OnBioChanged -> { + _state.update { it.copy(bio = event.bio) } + } + + is BusinessEvent.OnLocationInputChanged -> { + fetchLocationSuggestions(event.input) } + is BusinessEvent.OnLocationSuggestionsFetched -> { + locationSuggestion.value = event.suggestions + } + + is BusinessEvent.OnCreateProfileClicked -> { + val email = _state.value.email + val phone = _state.value.phone + val bio = _state.value.bio + val location = _state.value.location + val locationLatLng = _state.value.locationLatLng ?: return + + val request = BusinessProfileRequest( + email = email, + phone = phone, + bio = bio, + location = location, + latitude = locationLatLng.latitude, + longitude = locationLatLng.longitude + ) + + try { + viewModelScope.launch(Dispatchers.IO) { + showLoading() + Log.d("BusinessProfileModel", "Making API call to create profile") + if (isFormComplete(email, phone, location)) { + when (val response = repository.createBusinessProfile(request)) { + is Resource.Success -> { + _state.update { + it.copy( + navigateToHome = true, + isLoading = true + ) + } + hideLoading() + } + + is Resource.Error -> { + _state.update { + it.copy( +// error = response.message ?: "An error occurred", + isLoading = false + ) + } + + Log.d( + "BusinessProfileModel", + "createProfile: ${response.message}" + ) + } + } + + } + } + } catch (e: Exception) { + Log.e("BusinessProfileModel", "Exception Occurred: ${e.message}") + hideLoading() + } finally { + hideLoading() + } + } } } @@ -123,55 +185,52 @@ class BusinessProfileViewModel @Inject constructor( when (val result = repository.fetchBusinessProfile(id)) { is Resource.Success -> { val profile = result.data - _profile.value = _profile.value.copy( - email = profile?.data?.email ?: "", - phone = profile?.data?.phone ?: "", - bio = profile?.data?.bio ?: "", - location = profile?.data?.location ?: "", -// latitude = profile?.data?.lat ?: 0.0, -// longitude = profile?.data?.lon ?: 0.0 - ) + _state.update { + it.copy( + email = profile?.data?.email ?: "", + phone = profile?.data?.phone ?: "", + bio = profile?.data?.bio ?: "", + location = profile?.data?.location ?: "", + ) + } } is Resource.Error -> { - _profile.value = - _profile.value.copy(error = result.message ?: "An error occurred") +// _state.update { +// it +// .copy(error = result.message ?: "An error occurred") +// } } } } } - suspend fun getCurrentLocation(): Location? { - val fusedLocationClient = - LocationServices.getFusedLocationProviderClient(context) - - return if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - ) { - fusedLocationClient.getCurrentLocation( - LocationRequest.PRIORITY_HIGH_ACCURACY, - null - ).await() - } else { - null + private fun fetchLocationSuggestions(input: String) { + //initialize places client + val placesClient: PlacesClient = Places.createClient(context) + + // create a new token for autocomplete session + val token = AutocompleteSessionToken.newInstance() + + // create a new request + val request = FindAutocompletePredictionsRequest.builder() + .setSessionToken(token) + .setQuery(input) + .build() + + // fetch predictions + placesClient.findAutocompletePredictions(request).addOnSuccessListener { response -> + // Update the location suggestions in the state with the results + val suggestions = response.autocompletePredictions.map { it } + _state.update { it.copy(locationSuggestion = suggestions) } + }.addOnFailureListener { + // handle error +// _state.update { it.copy(error = "An Error occurred") } } } } -data class BusinessProfileState( - val isLoading: Boolean = false, - val error: String = "", - val email: String = "", - val phone: String = "", - val bio: String = "", - val location: String = "", - val locationLatLng: LatLng? = null, -// val latitude: Number = 0.0, -// val longitude: Number = 0.0, - val placeId: String = "", - val navigateToHome: Boolean = false -) + + diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/business/CreateBusinessProfile.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/business/CreateBusinessProfile.kt index c41ee1b..012af30 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/business/CreateBusinessProfile.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/business/CreateBusinessProfile.kt @@ -1,6 +1,7 @@ package com.samuelokello.kazihub.presentation.business import android.os.Build +import android.util.Log import androidx.annotation.RequiresApi import androidx.annotation.RequiresExtension import androidx.compose.foundation.layout.Column @@ -13,44 +14,46 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.google.android.libraries.places.api.net.PlacesClient import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.samuelokello.kazihub.presentation.business.state.BusinessEvent +import com.samuelokello.kazihub.presentation.business.state.BusinessProfileState +import com.samuelokello.kazihub.presentation.common.HandleError +import com.samuelokello.kazihub.presentation.common.HandleLoading +import com.samuelokello.kazihub.presentation.common.HandleSuccess import com.samuelokello.kazihub.presentation.shared.components.CustomButton import com.samuelokello.kazihub.presentation.shared.components.EditTextField import com.samuelokello.kazihub.presentation.shared.components.LocationDropDown import com.samuelokello.kazihub.presentation.shared.destinations.HomeScreenDestination import com.samuelokello.kazihub.ui.theme.KaziHubTheme import com.samuelokello.kazihub.utils.UserRole -import kotlinx.coroutines.launch @RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) @RequiresApi(Build.VERSION_CODES.M) @Composable fun CreateBusinessProfile(navigator: DestinationsNavigator,userType: UserRole) { - val viewModel: BusinessProfileViewModel = hiltViewModel() - val state = viewModel.profile.collectAsState().value - LocalContext.current - val placesClient = viewModel.getPlacesClient() + Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { KaziHubTheme { + val viewModel: BusinessProfileViewModel = hiltViewModel() + val state by viewModel.state.collectAsState() + BusinessProfileForm( state = state, - viewModel = viewModel, - placesClient = placesClient, + onEvent = viewModel::onEvent, navigateToDashBoard = { navigator.navigate(HomeScreenDestination(userType)) } - ) + ) } } @@ -60,11 +63,23 @@ fun CreateBusinessProfile(navigator: DestinationsNavigator,userType: UserRole) { @Composable fun BusinessProfileForm( state: BusinessProfileState, - viewModel: BusinessProfileViewModel, - placesClient: PlacesClient, + onEvent: (BusinessEvent)-> Unit, navigateToDashBoard : () -> Unit ) { - val coroutineScope = rememberCoroutineScope() + + HandleLoading(state) + HandleError(state) + HandleSuccess(state = state, successMessage = state.error) + + LaunchedEffect (state.navigateToHome){ + if(state.navigateToHome) { + navigateToDashBoard() + } + } + + fun isFormComplete(state: BusinessProfileState): Boolean { + return state.email.isNotEmpty() && state.phone.isNotEmpty() && state.location.isNotEmpty() + } Column ( modifier = Modifier .fillMaxSize() @@ -80,9 +95,10 @@ fun BusinessProfileForm( Column { EditTextField( value = state.email, - onValueChange = { viewModel.onEmailChange(it) }, + onValueChange = { email -> + onEvent(BusinessEvent.OnEmailChanged(email))}, label = "Email", - error = !viewModel.isEmailValid(state.email), +// error = !viewModel.isEmailValid(state.email), singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, @@ -92,9 +108,11 @@ fun BusinessProfileForm( Spacer(modifier = Modifier.height(16.dp)) EditTextField( value = state.phone, - onValueChange = { viewModel.onPhoneChange(it) }, + onValueChange = { phone -> + onEvent(BusinessEvent.OnPhoneNumberChanged(phone)) + }, label = "Phone", - error = !viewModel.isPhoneValid(state.phone), +// error = !viewModel.isPhoneValid(state.phone), singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, @@ -104,17 +122,22 @@ fun BusinessProfileForm( Spacer(modifier = Modifier.height(16.dp)) LocationDropDown( value = state.location, - onValueChange = { - newLocation, placeId -> - viewModel.onLocationChange(newLocation, placeId) + onValueChange = { newValue, input-> + onEvent(BusinessEvent.OnLocationInputChanged(input)) + onEvent(BusinessEvent.OnLocationChanged(newValue)) }, - places = placesClient, - label = "Location", + locationSuggestions = state.locationSuggestion, + onLocationInputChanged = { input -> + onEvent(BusinessEvent.OnLocationInputChanged(input)) + }, + label = "Location" ) Spacer(modifier = Modifier.height(16.dp)) EditTextField( value = state.bio, - onValueChange = { viewModel.onBioChange(it)}, + onValueChange = { bio -> + onEvent(BusinessEvent.OnBioChanged(bio)) + }, label = "Bio", error = false, singleLine = false, @@ -129,15 +152,21 @@ fun BusinessProfileForm( Column { CustomButton( onClick = { - coroutineScope.launch { - viewModel.createProfile() - } -// navigateToDashBoard() + onEvent( + BusinessEvent.OnCreateProfileClicked( + email = state.email, + phone = state.phone, + location = state.location, + bio = state.bio + )) + Log.d("BusinessProfile UI", "createProfile: ${state.navigateToHome}") }, text = "Create Profile", - isEnabled = viewModel.isFormComplete(state.email,state.phone,state.location) + isEnabled = state.email.isNotEmpty() && state.phone.isNotEmpty() && state.location.isNotEmpty() && state.bio.isNotEmpty(), + ) } + } } diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/business/state/BusinessEvent.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/business/state/BusinessEvent.kt new file mode 100644 index 0000000..0cecd25 --- /dev/null +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/business/state/BusinessEvent.kt @@ -0,0 +1,19 @@ +package com.samuelokello.kazihub.presentation.business.state + +import com.google.android.libraries.places.api.model.AutocompletePrediction + +sealed interface BusinessEvent { + data class OnEmailChanged(val email: String): BusinessEvent + data class OnPhoneNumberChanged(val phone: String): BusinessEvent + data class OnLocationChanged(val location: String): BusinessEvent + data class OnLocationInputChanged(val input: String): BusinessEvent + data class OnLocationSuggestionsFetched(val suggestions: List): BusinessEvent + data class OnCreateProfileClicked( + val email: String, + val phone: String, + val location: String, + val bio: String + ): BusinessEvent + + data class OnBioChanged(val bio: String) : BusinessEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/business/state/BusinessProfileState.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/business/state/BusinessProfileState.kt new file mode 100644 index 0000000..9e6cb3e --- /dev/null +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/business/state/BusinessProfileState.kt @@ -0,0 +1,18 @@ +package com.samuelokello.kazihub.presentation.business.state + +import com.google.android.gms.maps.model.LatLng +import com.google.android.libraries.places.api.model.AutocompletePrediction +import com.samuelokello.kazihub.presentation.common.AuthState + +data class BusinessProfileState( + override val isLoading: Boolean = false, + val email: String = "", + val phone: String = "", + val bio: String = "", + val location: String = "", + val locationLatLng: LatLng? = null, + val placeId: String = "", + val locationSuggestion: List = listOf(), + val navigateToHome: Boolean = false +): AuthState + diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/common/AuthState.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/common/AuthState.kt similarity index 97% rename from app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/common/AuthState.kt rename to app/src/main/java/com/samuelokello/kazihub/presentation/common/AuthState.kt index 4dba339..5bc09d2 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/common/AuthState.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/common/AuthState.kt @@ -1,4 +1,4 @@ -package com.samuelokello.kazihub.presentation.shared.authentication.common +package com.samuelokello.kazihub.presentation.common import android.widget.Toast import androidx.compose.foundation.background diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInScreen.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInScreen.kt index 72e59fa..105d9ac 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInScreen.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInScreen.kt @@ -51,9 +51,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.samuelokello.kazihub.R -import com.samuelokello.kazihub.presentation.shared.authentication.common.HandleError -import com.samuelokello.kazihub.presentation.shared.authentication.common.HandleLoading -import com.samuelokello.kazihub.presentation.shared.authentication.common.HandleSuccess +import com.samuelokello.kazihub.presentation.common.HandleError +import com.samuelokello.kazihub.presentation.common.HandleLoading +import com.samuelokello.kazihub.presentation.common.HandleSuccess import com.samuelokello.kazihub.presentation.shared.destinations.CreateProfileScreenDestination import com.samuelokello.kazihub.ui.theme.KaziHubTheme import com.samuelokello.kazihub.ui.theme.primaryLight diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInState.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInState.kt index 2b48a19..12486eb 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInState.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/SignIn/SignInState.kt @@ -1,6 +1,7 @@ package com.samuelokello.kazihub.presentation.shared.authentication.SignIn -import com.samuelokello.kazihub.presentation.shared.authentication.common.AuthState +import com.samuelokello.kazihub.presentation.common.AuthState + data class SignInState( val isSignInSuccessful: Boolean = false, diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpScreen.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpScreen.kt index 0810eb3..87bc349 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpScreen.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpScreen.kt @@ -51,9 +51,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.samuelokello.kazihub.R -import com.samuelokello.kazihub.presentation.shared.authentication.common.HandleError -import com.samuelokello.kazihub.presentation.shared.authentication.common.HandleLoading -import com.samuelokello.kazihub.presentation.shared.authentication.common.HandleSuccess +import com.samuelokello.kazihub.presentation.common.HandleError +import com.samuelokello.kazihub.presentation.common.HandleLoading +import com.samuelokello.kazihub.presentation.common.HandleSuccess import com.samuelokello.kazihub.presentation.shared.authentication.sign_up.SignUpEvent import com.samuelokello.kazihub.presentation.shared.authentication.sign_up.SignUpState import com.samuelokello.kazihub.presentation.shared.authentication.sign_up.SignUpViewModel diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpState.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpState.kt index 354b92f..df0e20f 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpState.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/authentication/sign_up/SignUpState.kt @@ -1,6 +1,6 @@ package com.samuelokello.kazihub.presentation.shared.authentication.sign_up -import com.samuelokello.kazihub.presentation.shared.authentication.common.AuthState +import com.samuelokello.kazihub.presentation.common.AuthState data class SignUpState ( // val isSignUpSuccessful: Boolean = false, diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/AppBar.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/AppBar.kt index a9ab921..ea246ce 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/AppBar.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/AppBar.kt @@ -1,6 +1,8 @@ package com.samuelokello.kazihub.presentation.shared.components import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -15,6 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource @@ -29,7 +32,12 @@ fun AppBar( ) { TopAppBar( title = { - Text(text = stringResource(id = R.string.app_name)) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ){ + Text(text = stringResource(id = R.string.app_name)) + } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface @@ -41,7 +49,7 @@ fun AppBar( contentDescription = "Toggle drawer", tint = MaterialTheme.colorScheme.primary, modifier = Modifier - .size(32.dp) + .size(20.dp) .padding(0.dp) .clip(RoundedCornerShape(0.dp)) ) diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/CustomButton.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/CustomButton.kt index cf3edcc..4a1f67a 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/CustomButton.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/CustomButton.kt @@ -16,7 +16,7 @@ import com.samuelokello.kazihub.ui.theme.primaryLight fun CustomButton( onClick: () -> Unit, modifier: Modifier = Modifier, - isEnabled: Boolean = true, + isEnabled: Boolean = false, text: String, containerColor: Color = primaryLight, contentColor: Color = Color.White, diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/LocationDropDown.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/LocationDropDown.kt index 1fb3265..7ed7823 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/LocationDropDown.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/LocationDropDown.kt @@ -1,5 +1,7 @@ package com.samuelokello.kazihub.presentation.shared.components +import android.os.Build +import androidx.annotation.RequiresExtension import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape @@ -8,63 +10,95 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.google.android.libraries.places.api.model.AutocompletePrediction -import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest -import com.google.android.libraries.places.api.net.PlacesClient -import kotlinx.coroutines.tasks.await +@RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) @Composable fun LocationDropDown( value: String, onValueChange: (String, String) -> Unit, - places: PlacesClient, + locationSuggestions: List, + onLocationInputChanged: (String) -> Unit, label: String ) { - var suggestions by remember { mutableStateOf(listOf())} - - LaunchedEffect(value) { - if(value.isNotEmpty()) { - val request = FindAutocompletePredictionsRequest.builder() - .setQuery(value) - .build() - - val response = places.findAutocompletePredictions(request).await() - suggestions = response.autocompletePredictions - } else { - suggestions = emptyList() - } - } - Column { OutlinedTextField( value = value, - onValueChange = { onValueChange(it, "") }, + onValueChange = { + onValueChange(it, "") + onLocationInputChanged(it) + }, label = { Text(label) }, shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) DropdownMenu( - expanded = suggestions.isNotEmpty(), - onDismissRequest = {suggestions = listOf() } + expanded = locationSuggestions.isNotEmpty(), + onDismissRequest = { onLocationInputChanged("") } ) { - suggestions.forEach { suggestion -> + locationSuggestions.forEach { suggestion -> DropdownMenuItem( text = { Text(text = suggestion.getFullText(null).toString()) }, onClick = { val newLocation = suggestion.getFullText(null).toString() val placeId = suggestion.placeId onValueChange(newLocation, placeId) - suggestions = listOf() - }) + onLocationInputChanged("") + }, + modifier = Modifier.fillMaxWidth() + ) } } } -} \ No newline at end of file +} +//@Composable +//fun LocationDropDown( +// value: String, +// onValueChange: (String, String) -> Unit, +// places: PlacesClient, +// label: String +//) { +// var suggestions by remember { mutableStateOf(listOf())} +// +// LaunchedEffect(value) { +// if(value.isNotEmpty()) { +// val request = FindAutocompletePredictionsRequest.builder() +// .setQuery(value) +// .build() +// +// val response = places.findAutocompletePredictions(request).await() +// suggestions = response.autocompletePredictions +// } else { +// suggestions = emptyList() +// } +// } +// +// Column { +// OutlinedTextField( +// value = value, +// onValueChange = { onValueChange(it, "") }, +// label = { Text(label) }, +// shape = RoundedCornerShape(8.dp), +// modifier = Modifier.fillMaxWidth() +// ) +// +// DropdownMenu( +// expanded = suggestions.isNotEmpty(), +// onDismissRequest = {suggestions = listOf() } +// ) { +// suggestions.forEach { suggestion -> +// DropdownMenuItem( +// text = { Text(text = suggestion.getFullText(null).toString()) }, +// onClick = { +// val newLocation = suggestion.getFullText(null).toString() +// val placeId = suggestion.placeId +// onValueChange(newLocation, placeId) +// suggestions = listOf() +// }) +// } +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/ReusableModalNavigationDrawer.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/ReusableModalNavigationDrawer.kt index 83f4006..c0e8b9c 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/ReusableModalNavigationDrawer.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/components/ReusableModalNavigationDrawer.kt @@ -1,6 +1,6 @@ package com.samuelokello.kazihub.presentation.shared.components -import androidx.compose.foundation.layout.PaddingValues +import android.annotation.SuppressLint import androidx.compose.foundation.layout.padding import androidx.compose.material3.DrawerState import androidx.compose.material3.ModalNavigationDrawer @@ -9,13 +9,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun ReusableModalNavigationDrawer( drawerState: DrawerState, drawerContent: @Composable () -> Unit, topBar: @Composable () -> Unit, bottomBar: @Composable () -> Unit, - content: @Composable (PaddingValues) -> Unit +// content: @Composable (PaddingValues) -> Unit ) { ModalNavigationDrawer( drawerState = drawerState, @@ -24,7 +25,7 @@ fun ReusableModalNavigationDrawer( Scaffold( topBar = topBar, modifier = Modifier.padding(bottom = 32.dp), - content = content + content = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/BusinessProfileScreen.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/BusinessProfileScreen.kt index 7f7c7a0..8ec4875 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/BusinessProfileScreen.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/BusinessProfileScreen.kt @@ -12,7 +12,7 @@ import com.samuelokello.kazihub.presentation.business.BusinessProfileViewModel @RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) @Composable fun BusinessProfileScreen(viewModel: BusinessProfileViewModel) { - val state by viewModel.profile.collectAsState() + val state by viewModel.state.collectAsState() Column { Text(text = "Profile") diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/CreateProfile.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/CreateProfile.kt index 69c4158..30c77f9 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/CreateProfile.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/shared/profile/CreateProfile.kt @@ -4,11 +4,13 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.samuelokello.kazihub.presentation.business.CreateBusinessProfile import com.samuelokello.kazihub.presentation.worker.WorkerProfile import com.samuelokello.kazihub.utils.UserRole + @RequiresApi(Build.VERSION_CODES.M) @Destination @Composable diff --git a/app/src/main/java/com/samuelokello/kazihub/presentation/worker/CreateWorkerProfile.kt b/app/src/main/java/com/samuelokello/kazihub/presentation/worker/CreateWorkerProfile.kt index 4b87931..be7b879 100644 --- a/app/src/main/java/com/samuelokello/kazihub/presentation/worker/CreateWorkerProfile.kt +++ b/app/src/main/java/com/samuelokello/kazihub/presentation/worker/CreateWorkerProfile.kt @@ -16,12 +16,10 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.api.net.PlacesClient import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.samuelokello.kazihub.presentation.shared.components.CustomButton import com.samuelokello.kazihub.presentation.shared.components.EditTextField -import com.samuelokello.kazihub.presentation.shared.components.LocationDropDown import com.samuelokello.kazihub.utils.UserRole @Composable @@ -85,13 +83,13 @@ fun WorkerProfileForm( ) ) Spacer(modifier = Modifier.height(16.dp)) - LocationDropDown( - value = state.location, - onValueChange = { newValue, newLocationLatLng -> - viewModel.onLocationChange(newValue, newLocationLatLng as LatLng)}, - places = placesClient, - label = "Location" - ) +// LocationDropDown( +// value = state.location, +// onValueChange = { newValue, newLocationLatLng -> +// viewModel.onLocationChange(newValue, newLocationLatLng as LatLng)}, +// places = placesClient, +// label = "Location" +// ) Spacer(modifier = Modifier.height(16.dp)) EditTextField( value = state.bio, diff --git a/app/src/main/java/com/samuelokello/kazihub/utils/LocationManager.kt b/app/src/main/java/com/samuelokello/kazihub/utils/LocationManager.kt index 7895628..298d925 100644 --- a/app/src/main/java/com/samuelokello/kazihub/utils/LocationManager.kt +++ b/app/src/main/java/com/samuelokello/kazihub/utils/LocationManager.kt @@ -17,9 +17,15 @@ import com.samuelokello.kazihub.R import kotlinx.coroutines.tasks.await import javax.inject.Inject +/** + * This class manages location related functionalities including permission checks, + * retrieving user's current location, and getting LatLng from a Place ID. + */ class LocationManager @Inject constructor(private val context: Context) { + // Used to get the user's location private val focusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + // Client for interacting with Google Places API private val placesClient: PlacesClient init { @@ -27,18 +33,27 @@ class LocationManager @Inject constructor(private val context: Context) { placesClient = Places.createClient(context) } + /** + * Interface for receiving location updates and errors. + */ interface LocationCallback { fun onLocationReceived(location: Location) fun onLocationError(errorMessage: String) fun onLocationLatLngReceived(latLng: LatLng) } + /** + * Checks if location permission is granted and requests it if not. + * + * @param activity The activity requesting permission. + */ fun checkAndRequestLocationPermission(activity: Activity) { if (ActivityCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED ) { + // Request fine location permission if not granted ActivityCompat.requestPermissions( activity, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), @@ -47,6 +62,11 @@ class LocationManager @Inject constructor(private val context: Context) { } } + /** + * Retrieves the user's current location and calls the provided callback with the results. + * + * @param callback The callback to receive location updates and errors. + */ fun getUserLocation(callback: LocationCallback) { if (ActivityCompat.checkSelfPermission( context, @@ -59,6 +79,7 @@ class LocationManager @Inject constructor(private val context: Context) { callback.onLocationError("Location permission not granted") return } + // Use FusedLocationProviderClient to get user's last known location focusedLocationClient.lastLocation.addOnSuccessListener { location -> if (location != null) { callback.onLocationReceived(location) @@ -71,7 +92,13 @@ class LocationManager @Inject constructor(private val context: Context) { callback.onLocationError("Failed to get user location: ${exception.message}") } } - + /** + * Fetches the LatLng from a given Place ID using Places API asynchronously. + * + * @param placeId The ID of the Place to get LatLng from. + * @param callback The callback to receive the LatLng if successful. + * @param errorCallback The callback to receive an error message if unsuccessful. + */ suspend fun getLatLngFromPlaceId(placeId: String, callback: (LatLng) -> Unit, errorCallback: (String) -> Unit) { val placeFields = listOf(Place.Field.LAT_LNG) val fetchPlaceRequest = FetchPlaceRequest.newInstance(placeId, placeFields) @@ -94,105 +121,4 @@ class LocationManager @Inject constructor(private val context: Context) { companion object { private const val REQUEST_LOCATION_PERMISSION = 1 } -} - - -//class LocationManager @Inject constructor(private val context: Context) { -// private val focusedLocationClient: FusedLocationProviderClient = -// LocationServices.getFusedLocationProviderClient(context) -// private val placesClient: PlacesClient -// -// init { -// Places.initializeWithNewPlacesApiEnabled(context, context.getString(R.string.maps_api_key)) -// placesClient = Places.createClient(context) -// } -// -// fun checkAndRequestLocationPermission(): Boolean { -// if (ActivityCompat.checkSelfPermission( -// context, -// Manifest.permission.ACCESS_FINE_LOCATION -// ) != PackageManager.PERMISSION_GRANTED -// ) { -// if (context is Activity) { -// ActivityCompat.requestPermissions( -// context, -// arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), -// 1 -// ) -// } else { -// Log.e(TAG, "Context is not an activity") -// } -// return false -// } -// return true -// } -// -// fun getUserLocation(hasLocationPermission: Boolean, input: String) { -// -// val token = AutocompleteSessionToken.newInstance() -// -// val request = FindAutocompletePredictionsRequest.builder() -// .setSessionToken(token) -// .setQuery(input) -// .build() -// -// placesClient.findAutocompletePredictions(request).addOnSuccessListener { response -> -// for (prediction in response.autocompletePredictions) { -// Log.i(TAG, prediction.getFullText(null).toString()) -// } -// }.addOnFailureListener { exception -> -// if (exception is ApiException) { -// Log.e(TAG, "Place not found: " + exception.statusCode) -// } -// } -// -// -// if (!hasLocationPermission) { -// ActivityCompat.requestPermissions( -// context as Activity, -// arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), -// 1 -// ) -// } -// -// -// if (ActivityCompat.checkSelfPermission( -// context, -// Manifest.permission.ACCESS_FINE_LOCATION -// ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( -// context, -// Manifest.permission.ACCESS_COARSE_LOCATION -// ) != PackageManager.PERMISSION_GRANTED -// ) { -// // TODO: Consider calling -// // ActivityCompat#requestPermissions -// // here to request the missing permissions, and then overriding -// // public void onRequestPermissionsResult(int requestCode, String[] permissions, -// // int[] grantResults) -// // to handle the case where the user grants the permission. See the documentation -// // for ActivityCompat#requestPermissions for more details. -// return -// } -// focusedLocationClient.lastLocation.addOnSuccessListener { location -> -// if (location != null) { -// -// } -// } -// -// } -// -// suspend fun getLatLngFromPlaceId(placeId:String): LatLng{ -// val placeFields = listOf(Place.Field.LAT_LNG) -// val fetchPlaceRequest = FetchPlaceRequest.newInstance(placeId, placeFields) -// -// return try { -// val placeResponse = placesClient.fetchPlace(fetchPlaceRequest).await() -// placeResponse.place.latLng!! -// } catch (e: Exception) { -// LatLng(0.0, 0.0) -// } -// } -// -// fun getPlacesClient() = placesClient -// -//} \ No newline at end of file +} \ No newline at end of file