Todas las versiones siguen Semantic Versioning:
androidx.credentials + googleid) — botón “Continuar con Google” en la pantalla de login, bajo divisor “o” e icono oficial multicolor. El idToken se valida contra el endpoint existente POST /api/auth/google y el flujo de sesión reutiliza EncryptedSharedPreferences del login email/contraseña.subia_cache/notification_days_before, default 3).ActivityResultContracts.CreateDocument (Storage Access Framework). El fichero se llama subia_suscripciones.csv y contiene todas las suscripciones con todos los campos del modelo, con escape RFC 4180.TopSuscripcionesChartCard en el Dashboard con un ColumnCartesianLayer de Vico 2.0.0-beta.2 (primer uso real de la librería en el proyecto). Normaliza YEARLY→mensual en DashboardViewModel.calcularTopSuscripciones.shared/commonMain (multiplataforma, listos para iOS):
AuthRepository.loginWithGoogle(idToken): Result<Unit>AuthViewModel.loginWithGoogle(idToken) + showGoogleError(mensaje)RenovacionWorker ahora lee el umbral de días desde SharedPreferences en vez del literal 3. Si no hay valor guardado, mantiene el comportamiento anterior (3 días).SubIAApp.kt: el dropdown del TopAppBar incorpora un item “Ajustes” encima de “Cerrar sesión”.androidApp/build.gradle.kts: androidx.credentials:credentials:1.3.0, androidx.credentials:credentials-play-services-auth:1.3.0, com.google.android.libraries.identity.googleid:googleid:1.1.1.SUBIA_GOOGLE_WEB_CLIENT_ID leída de local.properties y expuesta como BuildConfig.SUBIA_GOOGLE_WEB_CLIENT_ID (fallback ""). El Web Client ID no se commitea al repositorio.suscriptwallet) con package com.subia.android: uno con el SHA-1 release y otro con el SHA-1 debug.isTrial y trialEndsAt en suscripciones para distinguir pruebas gratuitas de pagos realesisTrial y trialEndsAt propagados en SubscriptionDtoesPrueba y fechaFinPrueba en Subscription.ktis_trial y trial_ends_at en tabla subscriptionsprod con application-prod.properties para conexión SSL a AivenFix: logos del catálogo Android resueltos desde el campo domain del servidor.
CatalogItem (shared model): nuevo campo domain (@SerialName("domain"), opcional) que recibe el dominio del servidor para cada servicio del catálogoServiceLogo: nuevo parámetro opcional domain — si se proporciona, tiene prioridad sobre la derivación local mediante getLogoDomain(nombre)CatalogoScreen: pasa item.domain a ServiceLogo para que los logos de catálogo usen el dominio exacto definido en el backend, eliminando falsos negativos en la resolución de logosAPI REST completa (P1) para consumo desde la app móvil.
/api/subscriptions (CRUD), /api/categories (CRUD), /api/dashboardApiResponse<T> wrapper genérico con ApiError estructurado { data, error }ApiExceptionHandler (@RestControllerAdvice): gestión de 404, 409, 400 y 500 con mensajes en castellanodto/api/: SubscriptionRequestDto, SubscriptionResponseDto, CategoryRequestDto, CategoryResponseDto, DashboardStatsDtocontroller/api/: ApiSubscriptionController, ApiCategoryController, ApiDashboardControllerDashboardService.getDashboard() (no getDashboardData())activeCount calculado con subscriptionService.findActive().sizealertCount calculado con dash.alertRenewals.sizesave() con id seteado en la entidad (no método update() separado)Migración de base de datos a PostgreSQL para persistencia real.
flyway-database-postgresql para soporte PG16docker-compose.yml en la raíz para arrancar PostgreSQL con un solo comandoApplicationRunner en SubIaApplication.kt)Primera versión funcional completa de la aplicación web.
/api/catalog (base para la futura app móvil)PROMPT.md con roadmap completo: PostgreSQL, API REST, app móvil, tests y seguridad#httpServletRequest eliminado en Thymeleaf 3.1 → resuelto con interceptor CurrentPathInterceptorBillingCycle comparado con string en SpEL siempre devolvía falso → cycle.name() == 'MONTHLY'Autenticación JWT (P2) para securizar la API REST de cara a la app móvil.
CustomAuthEntryPoint + CustomAccessDeniedHandler)JwtConfig.kt separado de JwtService.kt para evitar dependencia circularTokenService con auto-detección de hash BCrypt en app.auth.passwordApp móvil nativa Android (P4) con Kotlin Multiplatform Mobile, preparada para iOS.
Backend:
GET /api/dashboard/stats — endpoint específico para la app móvil. Devuelve gastoMensual, gastoAnual, totalSuscripciones y renovacionesProximas con diasRestantes. JSON en camelCase.DashboardMobileStatsDto + ProximaRenovacionMobileDto — DTOs propios para la respuesta mobile.DashboardService.getDashboardStats() — reutiliza la lógica de normalización de precios existente.App móvil (KMM — mobile/):
shared con código 100% compartido entre Android e iOS:
Subscription, Category, CatalogItem, DashboardSummary, AuthTokensApiClient con Ktor 3.1.0 y refresh de JWT atómico (Mutex) para evitar race conditionsAuthRepository, DashboardRepository, SubscriptionRepository, CategoryRepository, CatalogRepositoryStateFlow y sealed UiState (Loading/Success/Error/Offline/SesionExpirada)expect/actual para TokenStorage (EncryptedSharedPreferences / iOS Keychain), PlatformContext y HttpEnginesharedModule + androidModule)androidApp:
@Serializable routeslibs.versions.toml — Version Catalog de Gradle para gestión centralizada de dependenciasmobile/local.properties ignorado en git (SDK path + URL del backend, configuración local)iosApp (Compose Multiplatform iOS target)App Android operativa con datos reales. Rename del proyecto a Suscript Wallet.
ServiceLogo.kt + LogoUtils.kt)gradle.properties en mobile/ — android.useAndroidX=true, enableJetifier, kotlin.native.ignoreDisabledTargets=true (iOS targets ignorados en Windows)username (no email), cleartext traffic habilitado en el emulador, respuesta del backend envuelta en ApiResponse correctamente@SerialName en todos los modelos KMM para mapear camelCase del backend; wrappers ApiResponse en CatalogController y ApiDashboardControllersrc/main/ a src/androidMain/ (convención KMM), imports corregidos, dependencias alineadasSuscripcionFormViewModel — prefilling desde catálogo y edición operativos con los nuevos @SerialNamesettings.gradle.kts, README, CHANGELOG, repo GitHub)