From a068b86403049cb5864aab09855223383f84e0cb Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:11:02 +0530 Subject: [PATCH] ref: continue without account to use local db instead of anon login --- .../kotlin/com/nuvio/app/MainActivity.kt | 2 + .../app/core/auth/AuthStorage.android.kt | 26 ++++++++++ .../com/nuvio/app/core/auth/AuthRepository.kt | 39 +++++++++++---- .../com/nuvio/app/core/auth/AuthStorage.kt | 7 +++ .../com/nuvio/app/features/auth/AuthScreen.kt | 6 +-- .../features/profiles/ProfileRepository.kt | 48 +++++++++++++++++++ .../nuvio/app/core/auth/AuthStorage.ios.kt | 18 +++++++ 7 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/com/nuvio/app/core/auth/AuthStorage.android.kt create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthStorage.kt create mode 100644 composeApp/src/iosMain/kotlin/com/nuvio/app/core/auth/AuthStorage.ios.kt diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index 94553a051..dc22be810 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -7,6 +7,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.SystemBarStyle import androidx.core.view.WindowCompat +import com.nuvio.app.core.auth.AuthStorage import com.nuvio.app.core.deeplink.handleAppUrl import com.nuvio.app.core.storage.PlatformLocalAccountDataCleaner import com.nuvio.app.features.addons.AddonStorage @@ -43,6 +44,7 @@ class MainActivity : ComponentActivity() { window.isNavigationBarContrastEnforced = false WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = false AddonStorage.initialize(applicationContext) + AuthStorage.initialize(applicationContext) LibraryStorage.initialize(applicationContext) WatchedStorage.initialize(applicationContext) MetaScreenSettingsStorage.initialize(applicationContext) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/auth/AuthStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/auth/AuthStorage.android.kt new file mode 100644 index 000000000..3f6a104f9 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/auth/AuthStorage.android.kt @@ -0,0 +1,26 @@ +package com.nuvio.app.core.auth + +import android.content.Context +import android.content.SharedPreferences + +actual object AuthStorage { + private const val PREFS_NAME = "nuvio_auth" + private const val KEY_ANONYMOUS_USER_ID = "anonymous_user_id" + + private var preferences: SharedPreferences? = null + + fun initialize(context: Context) { + preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + } + + actual fun loadAnonymousUserId(): String? = + preferences?.getString(KEY_ANONYMOUS_USER_ID, null) + + actual fun saveAnonymousUserId(userId: String) { + preferences?.edit()?.putString(KEY_ANONYMOUS_USER_ID, userId)?.apply() + } + + actual fun clearAnonymousUserId() { + preferences?.edit()?.remove(KEY_ANONYMOUS_USER_ID)?.apply() + } +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthRepository.kt index dc1273d0b..d17dc078d 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthRepository.kt @@ -7,6 +7,8 @@ import io.github.jan.supabase.auth.auth import io.github.jan.supabase.auth.providers.builtin.Email import io.github.jan.supabase.auth.status.SessionStatus import io.github.jan.supabase.functions.functions +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -30,22 +32,33 @@ object AuthRepository { fun initialize() { if (initialized) return initialized = true + + val savedAnonId = AuthStorage.loadAnonymousUserId() + if (savedAnonId != null) { + _state.value = AuthState.Authenticated( + userId = savedAnonId, + email = null, + isAnonymous = true, + ) + } + scope.launch { SupabaseProvider.client.auth.sessionStatus.collect { status -> + if (AuthStorage.loadAnonymousUserId() != null) return@collect when (status) { is SessionStatus.Authenticated -> { val user = status.session.user _state.value = AuthState.Authenticated( userId = user?.id ?: "", email = user?.email, - isAnonymous = user?.email.isNullOrBlank(), + isAnonymous = false, ) } is SessionStatus.NotAuthenticated -> { _state.value = AuthState.Unauthenticated } is SessionStatus.Initializing -> { - _state.value = AuthState.Loading + if (savedAnonId == null) _state.value = AuthState.Loading } is SessionStatus.RefreshFailure -> { _state.value = AuthState.Unauthenticated @@ -55,13 +68,16 @@ object AuthRepository { } } - suspend fun signInAnonymously(): Result = runCatching { + @OptIn(ExperimentalUuidApi::class) + fun signInAnonymously() { _error.value = null - SupabaseProvider.client.auth.signInAnonymously() - Unit - }.onFailure { e -> - log.e(e) { "Anonymous sign-in failed" } - _error.value = e.message ?: "Anonymous sign-in failed" + val userId = Uuid.random().toString() + AuthStorage.saveAnonymousUserId(userId) + _state.value = AuthState.Authenticated( + userId = userId, + email = null, + isAnonymous = true, + ) } suspend fun signUpWithEmail(email: String, password: String): Result = runCatching { @@ -89,7 +105,12 @@ object AuthRepository { suspend fun signOut(): Result = runCatching { _error.value = null - SupabaseProvider.client.auth.signOut() + val wasAnonymous = AuthStorage.loadAnonymousUserId() != null + AuthStorage.clearAnonymousUserId() + if (!wasAnonymous) { + SupabaseProvider.client.auth.signOut() + } + _state.value = AuthState.Unauthenticated LocalAccountDataCleaner.wipe() }.onFailure { e -> log.e(e) { "Sign-out failed" } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthStorage.kt new file mode 100644 index 000000000..c5f8766c8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/auth/AuthStorage.kt @@ -0,0 +1,7 @@ +package com.nuvio.app.core.auth + +internal expect object AuthStorage { + fun loadAnonymousUserId(): String? + fun saveAnonymousUserId(userId: String) + fun clearAnonymousUserId() +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt index 15482ca03..f2781efa2 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/auth/AuthScreen.kt @@ -327,11 +327,7 @@ fun AuthScreen( Button( onClick = { - isLoading = true - scope.launch { - AuthRepository.signInAnonymously() - isLoading = false - } + AuthRepository.signInAnonymously() }, modifier = Modifier .fillMaxWidth() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt index 49c5f69fa..c4bc72543 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt @@ -3,6 +3,7 @@ package com.nuvio.app.features.profiles import co.touchlab.kermit.Logger import com.nuvio.app.core.auth.AuthRepository import com.nuvio.app.core.auth.AuthState +import com.nuvio.app.core.auth.isAnonymous import com.nuvio.app.core.network.SupabaseProvider import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.details.MetaScreenSettingsRepository @@ -94,6 +95,12 @@ object ProfileRepository { } suspend fun pullProfiles() { + if (AuthRepository.state.value.isAnonymous) { + if (!_state.value.isLoaded) { + _state.value = _state.value.copy(isLoaded = true) + } + return + } runCatching { val result = SupabaseProvider.client.postgrest.rpc("sync_pull_profiles") val profiles = result.decodeList() @@ -139,6 +146,10 @@ object ProfileRepository { } suspend fun pushProfiles(profiles: List) { + if (AuthRepository.state.value.isAnonymous) { + applyPayloadsLocally(profiles) + return + } runCatching { val params = buildJsonObject { put("p_profiles", json.encodeToJsonElement(profiles)) @@ -211,6 +222,18 @@ object ProfileRepository { } suspend fun deleteProfile(profileIndex: Int) { + if (AuthRepository.state.value.isAnonymous) { + val remaining = _state.value.profiles.filter { it.profileIndex != profileIndex } + _state.value = _state.value.copy( + profiles = remaining, + activeProfile = if (_state.value.activeProfile?.profileIndex == profileIndex) remaining.firstOrNull() else _state.value.activeProfile, + ) + if (_state.value.activeProfile != null) { + activeProfileIndex = _state.value.activeProfile!!.profileIndex + } + persist() + return + } runCatching { val params = buildJsonObject { put("p_profile_id", profileIndex) } SupabaseProvider.client.postgrest.rpc("sync_delete_profile_data", params) @@ -284,6 +307,31 @@ object ProfileRepository { } } + private fun applyPayloadsLocally(payloads: List) { + val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return + val profiles = payloads.map { p -> + NuvioProfile( + id = "", + userId = authState.userId, + profileIndex = p.profileIndex, + name = p.name, + avatarColorHex = p.avatarColorHex, + avatarId = p.avatarId, + usesPrimaryAddons = p.usesPrimaryAddons, + usesPrimaryPlugins = p.usesPrimaryPlugins, + ) + }.sortedBy { it.profileIndex } + _state.value = _state.value.copy( + profiles = profiles, + isLoaded = true, + activeProfile = profiles.find { it.profileIndex == activeProfileIndex } ?: profiles.firstOrNull(), + ) + if (_state.value.activeProfile != null) { + activeProfileIndex = _state.value.activeProfile!!.profileIndex + } + persist() + } + private fun persist() { val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return ProfileStorage.savePayload( diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/auth/AuthStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/auth/AuthStorage.ios.kt new file mode 100644 index 000000000..895cacbf7 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/auth/AuthStorage.ios.kt @@ -0,0 +1,18 @@ +package com.nuvio.app.core.auth + +import platform.Foundation.NSUserDefaults + +actual object AuthStorage { + private const val KEY_ANONYMOUS_USER_ID = "anonymous_user_id" + + actual fun loadAnonymousUserId(): String? = + NSUserDefaults.standardUserDefaults.stringForKey(KEY_ANONYMOUS_USER_ID) + + actual fun saveAnonymousUserId(userId: String) { + NSUserDefaults.standardUserDefaults.setObject(userId, forKey = KEY_ANONYMOUS_USER_ID) + } + + actual fun clearAnonymousUserId() { + NSUserDefaults.standardUserDefaults.removeObjectForKey(KEY_ANONYMOUS_USER_ID) + } +}