From 9b18d8e127c304b1c7cf94a1fb755f35d76e97bf Mon Sep 17 00:00:00 2001 From: Gimun Kim <101035437+kmkim2689@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:18:24 +0900 Subject: [PATCH] :sparkles: implement PreferencesDatastore for storing tokens --- android/app/build.gradle.kts | 3 + .../auth/AuthorizationLocalDataSource.kt | 21 ++++ .../DefaultAuthorizationLocalDataSource.kt | 99 +++++++++++++++++++ .../AuthorizationPreferencesDataStore.kt | 10 ++ .../android/data/model/auth/Authorization.kt | 9 ++ .../android/data/model/auth/Platform.kt | 13 +++ .../auth/AuthorizationRepository.kt | 21 ++++ .../auth/DefaultAuthorizationRepository.kt | 37 +++++++ 8 files changed, 213 insertions(+) create mode 100644 android/app/src/main/java/net/pengcook/android/data/datasource/auth/AuthorizationLocalDataSource.kt create mode 100644 android/app/src/main/java/net/pengcook/android/data/datasource/auth/DefaultAuthorizationLocalDataSource.kt create mode 100644 android/app/src/main/java/net/pengcook/android/data/local/preferences/AuthorizationPreferencesDataStore.kt create mode 100644 android/app/src/main/java/net/pengcook/android/data/model/auth/Authorization.kt create mode 100644 android/app/src/main/java/net/pengcook/android/data/model/auth/Platform.kt create mode 100644 android/app/src/main/java/net/pengcook/android/data/repository/auth/AuthorizationRepository.kt create mode 100644 android/app/src/main/java/net/pengcook/android/data/repository/auth/DefaultAuthorizationRepository.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e3fcb109..0de765b2 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -112,4 +112,7 @@ dependencies { // Indicator animation open source implementation("com.tbuonomo:dotsindicator:5.0") + + // Preferences Datastore + implementation("androidx.datastore:datastore-preferences:1.1.1") } diff --git a/android/app/src/main/java/net/pengcook/android/data/datasource/auth/AuthorizationLocalDataSource.kt b/android/app/src/main/java/net/pengcook/android/data/datasource/auth/AuthorizationLocalDataSource.kt new file mode 100644 index 00000000..2a1ca58a --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/datasource/auth/AuthorizationLocalDataSource.kt @@ -0,0 +1,21 @@ +package net.pengcook.android.data.datasource.auth + +import kotlinx.coroutines.flow.Flow +import net.pengcook.android.data.model.auth.Authorization +import net.pengcook.android.data.model.auth.Platform + +interface AuthorizationLocalDataSource { + val authorizationData: Flow + + suspend fun updatePlatformToken(platformToken: String?) + + suspend fun updateAccessToken(accessToken: String?) + + suspend fun updateRefreshToken(refreshToken: String?) + + suspend fun updateFcmToken(fcmToken: String?) + + suspend fun updateCurrentPlatform(platform: Platform) + + suspend fun clearAll() +} diff --git a/android/app/src/main/java/net/pengcook/android/data/datasource/auth/DefaultAuthorizationLocalDataSource.kt b/android/app/src/main/java/net/pengcook/android/data/datasource/auth/DefaultAuthorizationLocalDataSource.kt new file mode 100644 index 00000000..01dd5656 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/datasource/auth/DefaultAuthorizationLocalDataSource.kt @@ -0,0 +1,99 @@ +package net.pengcook.android.data.datasource.auth + +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.core.IOException +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import net.pengcook.android.data.model.auth.Authorization +import net.pengcook.android.data.model.auth.Platform + +class DefaultAuthorizationLocalDataSource( + private val dataStore: DataStore, +) : AuthorizationLocalDataSource { + override val authorizationData: Flow = + dataStore + .data + .catchException() + .map(::mapUserPreferences) + + override suspend fun updatePlatformToken(platformToken: String?) { + dataStore.modifyValue(KEY_PLATFORM_TOKEN, platformToken) + } + + override suspend fun updateAccessToken(accessToken: String?) { + dataStore.modifyValue(KEY_ACCESS_TOKEN, accessToken) + } + + override suspend fun updateRefreshToken(refreshToken: String?) { + dataStore.modifyValue(KEY_REFRESH_TOKEN, refreshToken) + } + + override suspend fun updateFcmToken(fcmToken: String?) { + dataStore.modifyValue(KEY_FCM_TOKEN, fcmToken) + } + + override suspend fun updateCurrentPlatform(platform: Platform) { + dataStore.modifyValue(KEY_CURRENT_PLATFORM, platform.platformName) + } + + override suspend fun clearAll() { + dataStore.edit { preferences -> + preferences.clear() + } + } + + private suspend fun DataStore.modifyValue( + key: Preferences.Key, + value: String?, + ) { + edit { preferences -> + if (value == null) { + preferences.remove(key) + return@edit + } + preferences[key] = value + } + } + + private fun Flow.catchException(): Flow = + catch { exception -> + if (exception is IOException) { + Log.e(TAG, EXCEPTION_ERROR_READING_EXCEPTION, exception) + emit(emptyPreferences()) + } else { + throw exception + } + } + + private fun mapUserPreferences(preferences: Preferences): Authorization { + val platformToken = preferences[KEY_PLATFORM_TOKEN] + val accessToken = preferences[KEY_ACCESS_TOKEN] + val refreshToken = preferences[KEY_REFRESH_TOKEN] + val fcmToken = preferences[KEY_FCM_TOKEN] + val currentPlatform = preferences[KEY_CURRENT_PLATFORM] ?: Platform.NONE.platformName + + return Authorization( + platformToken = platformToken, + accessToken = accessToken, + refreshToken = refreshToken, + fcmToken = fcmToken, + currentPlatform = currentPlatform, + ) + } + + companion object { + private const val TAG = "DefaultAuthorizationLocalDataSource" + private const val EXCEPTION_ERROR_READING_EXCEPTION = "Error reading preferences." + private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token") + private val KEY_REFRESH_TOKEN = stringPreferencesKey("refresh_token") + private val KEY_PLATFORM_TOKEN = stringPreferencesKey("platform_token") + private val KEY_FCM_TOKEN = stringPreferencesKey("fcm_token") + private val KEY_CURRENT_PLATFORM = stringPreferencesKey("current_platform") + } +} diff --git a/android/app/src/main/java/net/pengcook/android/data/local/preferences/AuthorizationPreferencesDataStore.kt b/android/app/src/main/java/net/pengcook/android/data/local/preferences/AuthorizationPreferencesDataStore.kt new file mode 100644 index 00000000..962e9a47 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/local/preferences/AuthorizationPreferencesDataStore.kt @@ -0,0 +1,10 @@ +package net.pengcook.android.data.local.preferences + +import android.content.Context +import androidx.datastore.preferences.preferencesDataStore + +private const val AUTHORIZATION_PREFERENCES = "authorization_preferences" + +private val Context.dataStore by preferencesDataStore( + name = AUTHORIZATION_PREFERENCES, +) diff --git a/android/app/src/main/java/net/pengcook/android/data/model/auth/Authorization.kt b/android/app/src/main/java/net/pengcook/android/data/model/auth/Authorization.kt new file mode 100644 index 00000000..4d757ee1 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/model/auth/Authorization.kt @@ -0,0 +1,9 @@ +package net.pengcook.android.data.model.auth + +data class Authorization( + val platformToken: String?, + val accessToken: String?, + val refreshToken: String?, + val fcmToken: String?, + val currentPlatform: String?, +) diff --git a/android/app/src/main/java/net/pengcook/android/data/model/auth/Platform.kt b/android/app/src/main/java/net/pengcook/android/data/model/auth/Platform.kt new file mode 100644 index 00000000..ab8e585c --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/model/auth/Platform.kt @@ -0,0 +1,13 @@ +package net.pengcook.android.data.model.auth + +enum class Platform(val platformName: String?) { + GOOGLE("google"), + NONE(null), + ; + + companion object { + fun find(platformName: String): Platform? { + return Platform.entries.find { it.platformName == platformName } + } + } +} diff --git a/android/app/src/main/java/net/pengcook/android/data/repository/auth/AuthorizationRepository.kt b/android/app/src/main/java/net/pengcook/android/data/repository/auth/AuthorizationRepository.kt new file mode 100644 index 00000000..82a7c6b8 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/repository/auth/AuthorizationRepository.kt @@ -0,0 +1,21 @@ +package net.pengcook.android.data.repository.auth + +import kotlinx.coroutines.flow.Flow +import net.pengcook.android.data.model.auth.Authorization +import net.pengcook.android.data.model.auth.Platform + +interface AuthorizationRepository { + val authorizationData: Flow + + suspend fun updatePlatformToken(platformToken: String?) + + suspend fun updateAccessToken(accessToken: String?) + + suspend fun updateRefreshToken(refreshToken: String?) + + suspend fun updateFcmToken(fcmToken: String?) + + suspend fun updateCurrentPlatform(platform: Platform) + + suspend fun clearAll() +} diff --git a/android/app/src/main/java/net/pengcook/android/data/repository/auth/DefaultAuthorizationRepository.kt b/android/app/src/main/java/net/pengcook/android/data/repository/auth/DefaultAuthorizationRepository.kt new file mode 100644 index 00000000..0c601de3 --- /dev/null +++ b/android/app/src/main/java/net/pengcook/android/data/repository/auth/DefaultAuthorizationRepository.kt @@ -0,0 +1,37 @@ +package net.pengcook.android.data.repository.auth + +import kotlinx.coroutines.flow.Flow +import net.pengcook.android.data.datasource.auth.AuthorizationLocalDataSource +import net.pengcook.android.data.model.auth.Authorization +import net.pengcook.android.data.model.auth.Platform + +class DefaultAuthorizationRepository( + private val authorizationLocalDataSource: AuthorizationLocalDataSource, +) : AuthorizationRepository { + override val authorizationData: Flow = + authorizationLocalDataSource.authorizationData + + override suspend fun updatePlatformToken(platformToken: String?) { + authorizationLocalDataSource.updatePlatformToken(platformToken) + } + + override suspend fun updateAccessToken(accessToken: String?) { + authorizationLocalDataSource.updateAccessToken(accessToken) + } + + override suspend fun updateRefreshToken(refreshToken: String?) { + authorizationLocalDataSource.updateRefreshToken(refreshToken) + } + + override suspend fun updateFcmToken(fcmToken: String?) { + authorizationLocalDataSource.updateFcmToken(fcmToken) + } + + override suspend fun updateCurrentPlatform(platform: Platform) { + authorizationLocalDataSource.updateCurrentPlatform(platform) + } + + override suspend fun clearAll() { + authorizationLocalDataSource.clearAll() + } +}