Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TokenDataStore @Inject constructor(
private val AUTH_TOKEN = stringPreferencesKey("auth_token")
private val USER_ID = longPreferencesKey("user_id")
private val GROUP_ID = longPreferencesKey("group_id")
private val LAST_USER_ID = longPreferencesKey("last_user_id")
}

/** Flow that emits the current authentication token, or null if not set. */
Expand All @@ -38,11 +39,18 @@ class TokenDataStore @Inject constructor(
val groupId: Flow<Long?> =
dataStore.data.map { it[GROUP_ID] }

/** Saves the provided authentication token to DataStore preferences. */
/** Saves the provided authentication token to DataStore preferences.
* If the userId differs from the last known user, the cached groupId is cleared
* so a different user won't use a stale group from the previous session. */
suspend fun saveToken(token: String, userId: Long) {
dataStore.edit {
it[AUTH_TOKEN] = token
it[USER_ID] = userId
dataStore.edit { prefs ->
val lastUserId = prefs[LAST_USER_ID]
prefs[AUTH_TOKEN] = token
prefs[USER_ID] = userId
prefs[LAST_USER_ID] = userId
if (lastUserId != null && lastUserId != userId) {
prefs.remove(GROUP_ID)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class LoggingInterceptor(
Log.d("HttpLogger", "HEADERS:")
headers.forEach { (name, value) ->
if (name.equals("Authorization", ignoreCase = true)) {
Log.d("HttpLogger", " $name: Bearer ${value.take(20)}...")
Log.d("HttpLogger", " $name: ${value.take(20)}...")
} else {
Log.d("HttpLogger", " $name: $value")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ class InvitationRepositoryImpl @Inject constructor(
)
}

override suspend fun acceptInvitationByToken(token: String): NetworkResult<Unit> = safeCall {
override suspend fun acceptInvitationByToken(token: String): NetworkResult<Long> = safeCall {
val userId = sessionRepository.getUserId() ?: throw IllegalStateException("User ID not found in session")
val dto = familyFacadeService.getPublicInvitation(token)
val invitationId = dto.id ?: throw IllegalStateException("Invitation ID not found in public response")
val familyId = dto.familyId ?: throw IllegalStateException("Family ID not found in public response")
familyFacadeService.acceptInvitation(userId, invitationId)
Unit
familyId
}

override suspend fun rejectInvitationByToken(token: String): NetworkResult<String> = safeCall {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface InvitationRepository {

suspend fun rejectInvitation(invitationId: Long): NetworkResult<Invitation>

suspend fun acceptInvitationByToken(token: String): NetworkResult<Unit>
suspend fun acceptInvitationByToken(token: String): NetworkResult<Long>

suspend fun rejectInvitationByToken(token: String): NetworkResult<String>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import jakarta.inject.Inject
class AcceptInvitationByTokenUseCase @Inject constructor(
private val invitationRepository: InvitationRepository
) {
suspend operator fun invoke(token: String): NetworkResult<Unit> {
suspend operator fun invoke(token: String): NetworkResult<Long> {
if (token.isBlank()) {
return NetworkResult.Error("Token is required")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ fun FamilyScreen(

var showQrOptionsSheet by remember { mutableStateOf(false) }
var showQrScanner by remember { mutableStateOf(false) }
var showCreateFamilySheet by remember { mutableStateOf(false) }
var showMyQrSheet by remember { mutableStateOf(false) }
var showCameraDeniedDialog by remember { mutableStateOf(false) }
var qrScanConsumed by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -190,9 +191,14 @@ fun FamilyScreen(

is UiState.Idle -> {
NoFamilyContent(
onCreateFamily = { name, description ->
viewModel.createFamily(name, description)
}
onJoinFamily = {
if (cameraPermissionGranted) {
showQrScanner = true
} else {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
},
onCreateFamily = { showCreateFamilySheet = true }
)
}

Expand Down Expand Up @@ -367,6 +373,16 @@ fun FamilyScreen(
)
}

if (showCreateFamilySheet) {
CreateFamilyBottomSheet(
onDismiss = { showCreateFamilySheet = false },
onCreateFamily = { name, description ->
viewModel.createFamily(name, description)
showCreateFamilySheet = false
}
)
}

if (showCameraDeniedDialog) {
AlertDialog(
onDismissRequest = { showCameraDeniedDialog = false },
Expand Down Expand Up @@ -633,11 +649,9 @@ private fun MyQrBottomSheet(

@Composable
private fun NoFamilyContent(
onCreateFamily: (name: String, description: String) -> Unit
onJoinFamily: () -> Unit,
onCreateFamily: () -> Unit
) {
var familyName by remember { mutableStateOf("") }
var familyDesc by remember { mutableStateOf("") }

Column(
modifier = Modifier
.fillMaxSize()
Expand Down Expand Up @@ -672,56 +686,160 @@ private fun NoFamilyContent(
Spacer(modifier = Modifier.height(8.dp))

Text(
text = "Crea un grupo para gestionar finanzas\nen familia y hacer seguimiento conjunto.",
text = "Únete a una familia existente o crea\nuna nueva para gestionar finanzas juntos.",
fontSize = 14.sp,
color = IntivaColors.TextSecondary,
textAlign = TextAlign.Center,
lineHeight = 20.sp
)

Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(40.dp))

OutlinedTextField(
value = familyName,
onValueChange = { familyName = it },
label = { Text("Nombre del grupo") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
singleLine = true
)

Spacer(modifier = Modifier.height(12.dp))

OutlinedTextField(
value = familyDesc,
onValueChange = { familyDesc = it },
label = { Text("Descripción (opcional)") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
maxLines = 2
)
Button(
onClick = onJoinFamily,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFCCFF00),
contentColor = Color(0xFF0D0D0D)
)
) {
Icon(
imageVector = Icons.Outlined.QrCodeScanner,
contentDescription = null,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Unirme a un grupo familiar",
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}

Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = {
if (familyName.isNotBlank()) {
onCreateFamily(familyName.trim(), familyDesc.trim())
}
},
onClick = onCreateFamily,
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(26.dp),
colors = ButtonDefaults.buttonColors(containerColor = IntivaColors.PrimaryBrand),
enabled = familyName.isNotBlank()
.height(56.dp),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(containerColor = IntivaColors.PrimaryBrand)
) {
Icon(
imageVector = Icons.Default.Group,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Crear grupo familiar",
text = "Crear un grupo familiar",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CreateFamilyBottomSheet(
onDismiss: () -> Unit,
onCreateFamily: (name: String, description: String) -> Unit
) {
var familyName by remember { mutableStateOf("") }
var familyDesc by remember { mutableStateOf("") }

ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = Color.White
) {
Column(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Crear grupo familiar",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = IntivaColors.TextPrimary,
textAlign = TextAlign.Center
)

Spacer(modifier = Modifier.height(8.dp))

Text(
text = "Completa los datos para crear tu grupo",
fontSize = 14.sp,
color = IntivaColors.TextSecondary,
textAlign = TextAlign.Center
)

Spacer(modifier = Modifier.height(24.dp))

OutlinedTextField(
value = familyName,
onValueChange = { familyName = it },
label = { Text("Nombre del grupo") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
singleLine = true
)

Spacer(modifier = Modifier.height(12.dp))

OutlinedTextField(
value = familyDesc,
onValueChange = { familyDesc = it },
label = { Text("Descripción (opcional)") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
maxLines = 2
)

Spacer(modifier = Modifier.height(24.dp))

Button(
onClick = {
if (familyName.isNotBlank()) {
onCreateFamily(familyName.trim(), familyDesc.trim())
}
},
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(26.dp),
colors = ButtonDefaults.buttonColors(containerColor = IntivaColors.PrimaryBrand),
enabled = familyName.isNotBlank()
) {
Text(
text = "Crear grupo familiar",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 15.sp
)
}

Spacer(modifier = Modifier.height(8.dp))

Text(
text = "Cancelar",
color = IntivaColors.PrimaryBrand,
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
modifier = Modifier
.padding(8.dp)
.clickable { onDismiss() }
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class FamilyViewModel @Inject constructor(

when (val result = acceptInvitationByTokenUseCase(token)) {
is NetworkResult.Success -> {
sessionRepository.saveGroupId(result.data)
_uiState.update { it.copy(scanResultState = UiState.Success("Te uniste al grupo exitosamente")) }
loadFamily()
loadMembers()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
Expand All @@ -40,6 +41,7 @@ fun QrCodeScannerContent(
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val analyzerExecutor = remember { Executors.newSingleThreadExecutor() }
var detected by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -99,7 +101,7 @@ fun QrCodeScannerContent(
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
ctx as androidx.lifecycle.LifecycleOwner,
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class InvitationDetailViewModel @Inject constructor(
_actionState.value = InvitationActionState.Loading
when (val result = acceptInvitationByTokenUseCase(token)) {
is NetworkResult.Success -> {
sessionRepository.saveGroupId(result.data)
_actionState.value = InvitationActionState.Success("Invitación aceptada con éxito")
}
is NetworkResult.Error -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ class InviteViewModel @Inject constructor(

when (val result = acceptInvitationByTokenUseCase(token)) {
is NetworkResult.Success -> {
sessionRepository.saveGroupId(result.data)
_uiState.update { it.copy(actionState = UiState.Success("Invitación aceptada")) }
}
is NetworkResult.Error -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import retrofit2.Retrofit
Expand Down Expand Up @@ -59,7 +59,6 @@ class AuthServiceTest {
assertEquals("/authentication/sign-up", request.path)
assertEquals("POST", request.method)

assertTrue(response.isSuccessful)
assertEquals("test@mail.com", response.body()?.email)
assertEquals("test@mail.com", response.email)
}
}
Loading
Loading