Add first-login About flow to Android
This commit is contained in:
+7
-1
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
## Versión actual
|
## Versión actual
|
||||||
|
|
||||||
- Versión base: `1.2.0`
|
- Versión base: `1.2.1`
|
||||||
|
|
||||||
## Historial
|
## Historial
|
||||||
|
|
||||||
|
### 1.2.1 - 2026-04-23
|
||||||
|
|
||||||
|
- Android muestra `About` automáticamente en el primer login de cada usuario y luego vuelve por defecto a `Documentos`.
|
||||||
|
- `About` agrega acceso a los documentos legales firmados del registro para abrirlos o descargarlos directamente en el teléfono.
|
||||||
|
- La preferencia de haber visto `About` queda persistente entre reinicios y actualizaciones de la app.
|
||||||
|
|
||||||
### 1.2.0 - 2026-04-23
|
### 1.2.0 - 2026-04-23
|
||||||
|
|
||||||
- Se separa el acceso biométrico Android de la sesión normal usando un token biométrico persistente.
|
- Se separa el acceso biométrico Android de la sesión normal usando un token biométrico persistente.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId "net.linkedcorp.linkedsign"
|
applicationId "net.linkedcorp.linkedsign"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
versionCode 10200
|
versionCode 10201
|
||||||
versionName "1.2.0"
|
versionName "1.2.1"
|
||||||
|
|
||||||
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
buildConfigField "String", "API_BASE_URL", "\"${apiBaseUrl}\""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ interface LinkedSignApiService {
|
|||||||
@Header("Authorization") authorization: String
|
@Header("Authorization") authorization: String
|
||||||
): ApiEnvelope<List<SignedDocumentItem>>
|
): ApiEnvelope<List<SignedDocumentItem>>
|
||||||
|
|
||||||
|
@GET("legal")
|
||||||
|
suspend fun listLegalDocuments(
|
||||||
|
@Header("Authorization") authorization: String
|
||||||
|
): ApiEnvelope<List<LegalDocumentItem>>
|
||||||
|
|
||||||
@DELETE("signatures/{id}")
|
@DELETE("signatures/{id}")
|
||||||
suspend fun deleteSignedDocument(
|
suspend fun deleteSignedDocument(
|
||||||
@Header("Authorization") authorization: String,
|
@Header("Authorization") authorization: String,
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ class LinkedSignRepository(private val context: Context) {
|
|||||||
unwrap(service.changePassword(auth, ChangePasswordRequest(currentPassword, newPassword)))
|
unwrap(service.changePassword(auth, ChangePasswordRequest(currentPassword, newPassword)))
|
||||||
} }
|
} }
|
||||||
|
|
||||||
|
suspend fun listLegalDocuments(): List<LegalDocumentItem> = execute { withAuth { auth ->
|
||||||
|
unwrap(service.listLegalDocuments(auth))
|
||||||
|
} }
|
||||||
|
|
||||||
suspend fun syncAll(): CacheSnapshot = execute { withAuth { auth ->
|
suspend fun syncAll(): CacheSnapshot = execute { withAuth { auth ->
|
||||||
val documents = unwrap(service.listDocuments(auth)).rows
|
val documents = unwrap(service.listDocuments(auth)).rows
|
||||||
val certificates = unwrap(service.listCertificates(auth))
|
val certificates = unwrap(service.listCertificates(auth))
|
||||||
@@ -226,6 +230,12 @@ class LinkedSignRepository(private val context: Context) {
|
|||||||
return refreshSession(session).accessToken
|
return refreshSession(session).accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasSeenAboutForUser(email: String): Boolean = sessionStore.hasSeenAboutForUser(email)
|
||||||
|
|
||||||
|
fun markAboutSeenForUser(email: String) {
|
||||||
|
sessionStore.markAboutSeenForUser(email)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun <T> withAuth(block: suspend (String) -> T): T {
|
private suspend fun <T> withAuth(block: suspend (String) -> T): T {
|
||||||
val session = sessionStore.load()
|
val session = sessionStore.load()
|
||||||
val accessToken = session.accessToken ?: throw AppException("Debes iniciar sesión.")
|
val accessToken = session.accessToken ?: throw AppException("Debes iniciar sesión.")
|
||||||
|
|||||||
@@ -138,6 +138,21 @@ data class SignedDocumentItem(
|
|||||||
val signedAt: String?
|
val signedAt: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class LegalDocumentItem(
|
||||||
|
val id: String,
|
||||||
|
val slug: String?,
|
||||||
|
val title: String?,
|
||||||
|
val version: String?,
|
||||||
|
@Json(name = "effectiveDate")
|
||||||
|
val effectiveDate: String?,
|
||||||
|
@Json(name = "createdAt")
|
||||||
|
val createdAt: String?,
|
||||||
|
@Json(name = "signedFilename")
|
||||||
|
val signedFilename: String?,
|
||||||
|
@Json(name = "hashSha256")
|
||||||
|
val hashSha256: String?
|
||||||
|
)
|
||||||
|
|
||||||
data class SignRequest(
|
data class SignRequest(
|
||||||
val documentId: Int,
|
val documentId: Int,
|
||||||
val certificateId: Int,
|
val certificateId: Int,
|
||||||
|
|||||||
@@ -123,6 +123,24 @@ class SessionStore(context: Context) {
|
|||||||
return email to biometricToken
|
return email to biometricToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasSeenAboutForUser(email: String): Boolean {
|
||||||
|
val normalizedEmail = email.trim().lowercase()
|
||||||
|
if (normalizedEmail.isBlank()) return true
|
||||||
|
return preferences.getStringSet(KEY_ABOUT_SEEN_USERS, emptySet()).orEmpty().contains(normalizedEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markAboutSeenForUser(email: String) {
|
||||||
|
val normalizedEmail = email.trim().lowercase()
|
||||||
|
if (normalizedEmail.isBlank()) return
|
||||||
|
|
||||||
|
val current = preferences.getStringSet(KEY_ABOUT_SEEN_USERS, emptySet()).orEmpty().toMutableSet()
|
||||||
|
if (current.add(normalizedEmail)) {
|
||||||
|
preferences.edit()
|
||||||
|
.putStringSet(KEY_ABOUT_SEEN_USERS, current)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_ACCESS = "access_token"
|
private const val KEY_ACCESS = "access_token"
|
||||||
private const val KEY_REFRESH = "refresh_token"
|
private const val KEY_REFRESH = "refresh_token"
|
||||||
@@ -132,6 +150,7 @@ class SessionStore(context: Context) {
|
|||||||
private const val KEY_BIO_CONFIGURED = "bio_configured"
|
private const val KEY_BIO_CONFIGURED = "bio_configured"
|
||||||
private const val KEY_BIO_EMAIL = "bio_email"
|
private const val KEY_BIO_EMAIL = "bio_email"
|
||||||
private const val KEY_BIO_TOKEN = "bio_token"
|
private const val KEY_BIO_TOKEN = "bio_token"
|
||||||
|
private const val KEY_ABOUT_SEEN_USERS = "about_seen_users"
|
||||||
private const val DEFAULT_SESSION_MS = 15 * 60 * 1000L
|
private const val DEFAULT_SESSION_MS = 15 * 60 * 1000L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.linkedcorp.linkedsign.ui
|
package net.linkedcorp.linkedsign.ui
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@@ -17,6 +18,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -133,6 +135,7 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import net.linkedcorp.linkedsign.data.DocumentItem
|
import net.linkedcorp.linkedsign.data.DocumentItem
|
||||||
|
import net.linkedcorp.linkedsign.data.LegalDocumentItem
|
||||||
import net.linkedcorp.linkedsign.data.SimpleMessageData
|
import net.linkedcorp.linkedsign.data.SimpleMessageData
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import net.linkedcorp.linkedsign.data.SignedDocumentItem
|
import net.linkedcorp.linkedsign.data.SignedDocumentItem
|
||||||
@@ -333,7 +336,7 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching { viewModel.biometricLogin() }
|
runCatching { viewModel.biometricLogin() }
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
selectedTab = AppTab.Documents
|
selectedTab = if (viewModel.consumeAboutLandingForCurrentUser()) AppTab.About else AppTab.Documents
|
||||||
appUnlocked = true
|
appUnlocked = true
|
||||||
snackbarHostState.showSnackbar("Sesión iniciada con huella.")
|
snackbarHostState.showSnackbar("Sesión iniciada con huella.")
|
||||||
}
|
}
|
||||||
@@ -371,7 +374,7 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching { viewModel.login(email, password) }
|
runCatching { viewModel.login(email, password) }
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
selectedTab = AppTab.Documents
|
selectedTab = if (viewModel.consumeAboutLandingForCurrentUser()) AppTab.About else AppTab.Documents
|
||||||
appUnlocked = true
|
appUnlocked = true
|
||||||
showBiometricOptIn = biometricAvailable && !state.biometricConfigured
|
showBiometricOptIn = biometricAvailable && !state.biometricConfigured
|
||||||
snackbarHostState.showSnackbar("Sesión iniciada.")
|
snackbarHostState.showSnackbar("Sesión iniciada.")
|
||||||
@@ -446,7 +449,8 @@ fun LinkedSignApp(viewModel: MainViewModel, appContext: Context, activity: Compo
|
|||||||
pendingSignDocumentId = documentId.toString()
|
pendingSignDocumentId = documentId.toString()
|
||||||
selectedTab = AppTab.Sign
|
selectedTab = AppTab.Sign
|
||||||
},
|
},
|
||||||
accessToken = { viewModel.accessToken() }
|
accessToken = { viewModel.accessToken() },
|
||||||
|
loadLegalDocuments = { viewModel.listLegalDocuments() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,7 +657,8 @@ private fun AppFlow(
|
|||||||
onDeleteDocument: (UnifiedDocumentItem) -> Unit,
|
onDeleteDocument: (UnifiedDocumentItem) -> Unit,
|
||||||
onSign: (Int, Int, String, String, String, Int, SignatureRect?) -> Unit,
|
onSign: (Int, Int, String, String, String, Int, SignatureRect?) -> Unit,
|
||||||
onSelectDocumentToSign: (Int) -> Unit,
|
onSelectDocumentToSign: (Int) -> Unit,
|
||||||
accessToken: suspend () -> String?
|
accessToken: suspend () -> String?,
|
||||||
|
loadLegalDocuments: suspend () -> List<LegalDocumentItem>
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -678,13 +683,37 @@ private fun AppFlow(
|
|||||||
onSign = onSign
|
onSign = onSign
|
||||||
)
|
)
|
||||||
AppTab.Certificates -> CertificatesScreen(state = state, onSync = onSync, onUploadCertificate = onUploadCertificate)
|
AppTab.Certificates -> CertificatesScreen(state = state, onSync = onSync, onUploadCertificate = onUploadCertificate)
|
||||||
AppTab.About -> AboutScreen()
|
AppTab.About -> AboutScreen(
|
||||||
|
appContext = appContext,
|
||||||
|
accessToken = accessToken,
|
||||||
|
loadLegalDocuments = loadLegalDocuments,
|
||||||
|
reloadKey = state.userEmail
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AboutScreen() {
|
private fun AboutScreen(
|
||||||
|
appContext: Context,
|
||||||
|
accessToken: suspend () -> String?,
|
||||||
|
loadLegalDocuments: suspend () -> List<LegalDocumentItem>,
|
||||||
|
reloadKey: String?
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var legalDocuments by remember { mutableStateOf<List<LegalDocumentItem>>(emptyList()) }
|
||||||
|
var legalLoading by remember { mutableStateOf(true) }
|
||||||
|
var legalError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(reloadKey) {
|
||||||
|
legalLoading = true
|
||||||
|
legalError = null
|
||||||
|
runCatching { loadLegalDocuments() }
|
||||||
|
.onSuccess { legalDocuments = it }
|
||||||
|
.onFailure { legalError = it.message ?: "No se pudieron cargar los documentos legales." }
|
||||||
|
legalLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 20.dp),
|
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 20.dp),
|
||||||
@@ -722,6 +751,156 @@ private fun AboutScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
SectionCard(title = "Documentos legales firmados") {
|
||||||
|
Text(
|
||||||
|
"Aquí puedes abrir o descargar en tu teléfono los documentos de protección de datos y aceptación legal firmados durante tu registro inicial. No se muestran dentro de la app.",
|
||||||
|
color = TextMuted,
|
||||||
|
fontSize = AppFontSizes.sm,
|
||||||
|
lineHeight = 19.sp
|
||||||
|
)
|
||||||
|
when {
|
||||||
|
legalLoading -> {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), color = Brand, strokeWidth = 2.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legalError != null -> {
|
||||||
|
Surface(
|
||||||
|
color = WarningBg,
|
||||||
|
shape = RoundedCornerShape(14.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
legalError ?: "No se pudieron cargar los documentos legales.",
|
||||||
|
color = Warning,
|
||||||
|
fontSize = AppFontSizes.sm,
|
||||||
|
modifier = Modifier.padding(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legalDocuments.isEmpty() -> {
|
||||||
|
Text("Aún no hay documentos legales asociados a esta cuenta.", color = TextMuted, fontSize = AppFontSizes.sm)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
legalDocuments.forEach { item ->
|
||||||
|
LegalDocumentRow(
|
||||||
|
item = item,
|
||||||
|
onOpenExternal = {
|
||||||
|
scope.launch {
|
||||||
|
runCatching {
|
||||||
|
val token = accessToken()
|
||||||
|
val file = downloadSecuredFileToCache(
|
||||||
|
context = appContext,
|
||||||
|
url = "${BuildConfig.API_BASE_URL}/legal/${item.id}/download",
|
||||||
|
accessToken = token,
|
||||||
|
fileName = item.signedFilename ?: "${item.slug ?: "legal"}.pdf"
|
||||||
|
)
|
||||||
|
openDownloadedFile(appContext, file)
|
||||||
|
}.onFailure {
|
||||||
|
Toast.makeText(appContext, it.message ?: "No se pudo abrir el documento en el teléfono.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDownload = {
|
||||||
|
scope.launch {
|
||||||
|
runCatching {
|
||||||
|
downloadFile(
|
||||||
|
context = appContext,
|
||||||
|
url = "${BuildConfig.API_BASE_URL}/legal/${item.id}/download",
|
||||||
|
accessToken = accessToken(),
|
||||||
|
fileName = item.signedFilename ?: "${item.slug ?: "legal"}.pdf"
|
||||||
|
)
|
||||||
|
}.onSuccess {
|
||||||
|
Toast.makeText(appContext, "Descarga iniciada.", Toast.LENGTH_SHORT).show()
|
||||||
|
}.onFailure {
|
||||||
|
Toast.makeText(appContext, it.message ?: "No se pudo descargar el documento.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LegalDocumentRow(
|
||||||
|
item: LegalDocumentItem,
|
||||||
|
onOpenExternal: () -> Unit,
|
||||||
|
onDownload: () -> Unit
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
color = Color.White,
|
||||||
|
border = BorderStroke(1.dp, StrokeSoft)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(38.dp)
|
||||||
|
.background(BrandLight, RoundedCornerShape(12.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(Icons.Outlined.Verified, contentDescription = null, tint = Brand)
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(item.title ?: "Documento legal", color = BrandDark, fontSize = AppFontSizes.base, fontWeight = FontWeight.SemiBold)
|
||||||
|
Text(item.signedFilename ?: "PDF firmado", color = TextMuted, fontSize = AppFontSizes.xs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AssistChip(
|
||||||
|
onClick = {},
|
||||||
|
enabled = false,
|
||||||
|
label = { Text("Firmado") },
|
||||||
|
colors = AssistChipDefaults.assistChipColors(
|
||||||
|
containerColor = SuccessBg,
|
||||||
|
labelColor = Success,
|
||||||
|
disabledContainerColor = SuccessBg,
|
||||||
|
disabledLabelColor = Success
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Text("Versión ${item.version ?: "-"}", color = TextMuted, fontSize = AppFontSizes.xs)
|
||||||
|
Text("Vigente desde ${item.effectiveDate ?: "-"}", color = TextMuted, fontSize = AppFontSizes.xs)
|
||||||
|
Text("Firmado el ${formatFriendlyDate(item.createdAt)}", color = TextMuted, fontSize = AppFontSizes.xs)
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
OutlinedActionButton(
|
||||||
|
"Abrir en el teléfono",
|
||||||
|
Icons.Outlined.FolderOpen,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = onOpenExternal
|
||||||
|
)
|
||||||
|
FilledActionButton(
|
||||||
|
"Descargar",
|
||||||
|
Icons.Outlined.Download,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = onDownload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1949,6 +2128,17 @@ private fun queryFileName(context: Context, uri: Uri): String? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatFriendlyDate(value: String?): String {
|
||||||
|
if (value.isNullOrBlank()) return "-"
|
||||||
|
return runCatching {
|
||||||
|
java.time.OffsetDateTime.parse(value)
|
||||||
|
.toLocalDateTime()
|
||||||
|
.toString()
|
||||||
|
.replace('T', ' ')
|
||||||
|
.substring(0, 16)
|
||||||
|
}.getOrElse { value.replace('T', ' ').take(16).ifBlank { value } }
|
||||||
|
}
|
||||||
|
|
||||||
private fun downloadFile(context: Context, url: String, accessToken: String?, fileName: String) {
|
private fun downloadFile(context: Context, url: String, accessToken: String?, fileName: String) {
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
val request = DownloadManager.Request(Uri.parse(url))
|
||||||
.setTitle(fileName)
|
.setTitle(fileName)
|
||||||
@@ -1964,6 +2154,24 @@ private fun downloadFile(context: Context, url: String, accessToken: String?, fi
|
|||||||
manager.enqueue(request)
|
manager.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openDownloadedFile(context: Context, file: File) {
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
.setDataAndType(uri, "application/pdf")
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.startActivity(Intent.createChooser(intent, "Abrir documento"))
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
throw AppException("No hay una app en el teléfono para abrir PDFs.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun downloadSecuredFileToCache(
|
private suspend fun downloadSecuredFileToCache(
|
||||||
context: Context,
|
context: Context,
|
||||||
url: String,
|
url: String,
|
||||||
|
|||||||
@@ -171,6 +171,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
refreshState()
|
refreshState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun listLegalDocuments(): List<net.linkedcorp.linkedsign.data.LegalDocumentItem> =
|
||||||
|
repository.listLegalDocuments()
|
||||||
|
|
||||||
|
fun consumeAboutLandingForCurrentUser(): Boolean {
|
||||||
|
val email = repository.loadLocalSnapshot().first.user?.email ?: return false
|
||||||
|
if (repository.hasSeenAboutForUser(email)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
repository.markAboutSeenForUser(email)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildState(
|
private fun buildState(
|
||||||
session: SessionSnapshot,
|
session: SessionSnapshot,
|
||||||
cache: CacheSnapshot,
|
cache: CacheSnapshot,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "linkedsign-backend",
|
"name": "linkedsign-backend",
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Initial backend for LinkedSign",
|
"description": "Initial backend for LinkedSign",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user