-
Notifications
You must be signed in to change notification settings - Fork 2
DataStoreStorage replaces the deprecated SharedPreferencesStorage #259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
mrudatsprint
wants to merge
6
commits into
main
Choose a base branch
from
miker/issue-184
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
2af8640
DataStoreStorage replaces the deprecated SharedPreferencesStorage as …
mrudatsprint d839d67
Merge branch 'main' into miker/issue-184
mrudatsprint 154c195
Code scanning issues.
mrudatsprint ff699cf
Let's deprecate and not delete.
mrudatsprint e27791a
Indicate the storage type is deprecated.
mrudatsprint 0d90d50
Make the Storage interface asynchronous and that makes the library mo…
mrudatsprint File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -176,6 +176,7 @@ dependencies { | |
| implementation("androidx.security:security-crypto-ktx:1.1.0") | ||
| implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") | ||
| implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") | ||
| implementation("androidx.datastore:datastore-preferences:1.2.0") | ||
| testImplementation("junit:junit:4.13.2") | ||
| testImplementation("org.mockito:mockito-core:5.21.0") | ||
| testImplementation("org.mockito.kotlin:mockito-kotlin:6.1.0") | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,7 +61,7 @@ object AuthorizationManager { | |
| * | ||
| * @return true if the user is authenticated, false otherwise | ||
| */ | ||
| fun isAuthenticated(): Boolean { | ||
| suspend fun isAuthenticated(): Boolean { | ||
| return !isAccessTokenExpired() | ||
| } | ||
|
|
||
|
|
@@ -107,7 +107,7 @@ object AuthorizationManager { | |
| * | ||
| * @return The access token string or null if not available. | ||
| */ | ||
| fun getAccessToken(): String? { | ||
| suspend fun getAccessToken(): String? { | ||
| return tokenManager.getAuthState()?.accessToken | ||
| } | ||
|
|
||
|
|
@@ -117,7 +117,7 @@ object AuthorizationManager { | |
| * @return The expiration time of the access token, or null if the token manager is not set or the access token is | ||
| * not available. | ||
| */ | ||
| fun getAccessTokenExpirationTime(): Long? { | ||
| suspend fun getAccessTokenExpirationTime(): Long? { | ||
| return tokenManager.getAuthState()?.accessTokenExpirationTime | ||
| } | ||
|
|
||
|
|
@@ -126,7 +126,7 @@ object AuthorizationManager { | |
| * | ||
| * @return true if the access token is expired, false otherwise. | ||
| */ | ||
| fun isAccessTokenExpired(): Boolean { | ||
| suspend fun isAccessTokenExpired(): Boolean { | ||
| return getAccessTokenExpirationTime()?.let { | ||
| it < System.currentTimeMillis() | ||
| } ?: true | ||
|
|
@@ -137,7 +137,7 @@ object AuthorizationManager { | |
| * | ||
| * @return The ID token string, or null if the user is not authenticated. | ||
| */ | ||
| fun getIdToken(): String? { | ||
| suspend fun getIdToken(): String? { | ||
| return tokenManager.getAuthState()?.idToken | ||
| } | ||
|
|
||
|
|
@@ -154,7 +154,7 @@ object AuthorizationManager { | |
| * @return The parsed ID token, or null if it cannot be parsed. | ||
| */ | ||
| @OptIn(ExperimentalEncodingApi::class) | ||
| fun getParsedIdToken(): IdToken? { | ||
| suspend fun getParsedIdToken(): IdToken? { | ||
| return tokenManager.getAuthState()?.idToken?.let { | ||
| val parts = it.split(".") | ||
| require(parts.size == JWT_PARTS) { "Invalid JWT token" } | ||
|
|
@@ -167,7 +167,7 @@ object AuthorizationManager { | |
| * | ||
| * This method clears the authorization state by removing the "authState" key from the storage. | ||
| */ | ||
| fun clearState() { | ||
| suspend fun clearState() { | ||
| tokenManager.clearAuthState() | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,7 +39,7 @@ class TokenManager { | |
| * @throws StorageException if an error occurs while decoding the authorization state. | ||
| */ | ||
| @Suppress("TooGenericExceptionCaught") | ||
| fun getAuthState(): FusionAuthState? { | ||
| suspend fun getAuthState(): FusionAuthState? { | ||
| if (this.authState.get() != null) { | ||
| return this.authState.get() | ||
| } | ||
|
|
@@ -62,7 +62,7 @@ class TokenManager { | |
| * @param authState The authorization state to be saved. | ||
| * @throws NullPointerException if `storage` is null. | ||
| */ | ||
| fun saveAuthState(authState: FusionAuthState) { | ||
| suspend fun saveAuthState(authState: FusionAuthState) { | ||
| if (this.storage == null) throw StorageException.notSet() | ||
|
|
||
| this.authState.set(authState) | ||
|
|
@@ -74,7 +74,7 @@ class TokenManager { | |
| * | ||
| * @throws StorageException if the storage implementation is not set. | ||
| */ | ||
| fun clearAuthState() { | ||
| suspend fun clearAuthState() { | ||
| if (this.storage == null) throw StorageException.notSet() | ||
|
|
||
| this.authState.set(null) | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -477,44 +477,43 @@ class OAuthAuthorizationService internal constructor( | |
| */ | ||
| private suspend fun freshAccessTokenInternal(): String? { | ||
| val config = getConfiguration() | ||
| val authService = getAuthorizationService() | ||
|
|
||
| return suspendCoroutine { | ||
| val authService = getAuthorizationService() | ||
| val tm = tokenManager ?: throw AuthorizationException("TokenManager not available or not configured.") | ||
|
|
||
| val refreshToken = tokenManager?.getAuthState()?.refreshToken | ||
| if (refreshToken == null) { | ||
| it.resumeWithException(AuthorizationException("No refresh token available")) | ||
| return@suspendCoroutine | ||
| } | ||
| val authState = tm.getAuthState() | ||
| ?: throw AuthorizationException("Not authenticated.") | ||
|
|
||
| val refreshToken = authState.refreshToken | ||
| ?: throw AuthorizationException("No refresh token available") | ||
|
|
||
| val response = suspendCoroutine<TokenResponse> { continuation -> | ||
| authService.performTokenRequest( | ||
| TokenRequest.Builder( | ||
| config, | ||
| clientId | ||
| ) | ||
| TokenRequest.Builder(config, clientId) | ||
| .setGrantType(GrantTypeValues.REFRESH_TOKEN) | ||
| .setRefreshToken(refreshToken) | ||
| .build(), | ||
| appAuthState.clientAuthentication | ||
| ) { response, exception -> | ||
| appAuthState.update(response, exception) | ||
| if (response != null) { | ||
| val authState = tokenManager?.getAuthState() | ||
| if (authState != null) { | ||
| val newAuthState = authState.copy( | ||
| accessToken = response.accessToken, | ||
| accessTokenExpirationTime = response.accessTokenExpirationTime, | ||
| idToken = response.idToken, | ||
| refreshToken = response.refreshToken, | ||
| ) | ||
| tokenManager?.saveAuthState(newAuthState) | ||
| } | ||
| it.resume(response.accessToken) | ||
| continuation.resume(response) | ||
| } else { | ||
| it.resumeWithException(exception?.let { ex -> AuthorizationException(ex) } | ||
| continuation.resumeWithException(exception?.let { AuthorizationException(it) } | ||
| ?: AuthorizationException("Unknown error")) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| val newAuthState = authState.copy( | ||
| accessToken = response.accessToken, | ||
| accessTokenExpirationTime = response.accessTokenExpirationTime, | ||
| idToken = response.idToken, | ||
| refreshToken = response.refreshToken ?: authState.refreshToken, | ||
| ) | ||
| tm.saveAuthState(newAuthState) | ||
|
|
||
| return response.accessToken | ||
| } | ||
|
|
||
| /** | ||
|
|
||
61 changes: 61 additions & 0 deletions
61
library/src/main/java/io/fusionauth/mobilesdk/storage/DataStoreStorage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package io.fusionauth.mobilesdk.storage | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import androidx.datastore.preferences.core.edit | ||
| import androidx.datastore.preferences.core.stringPreferencesKey | ||
| import androidx.datastore.preferences.preferencesDataStore | ||
| import kotlinx.coroutines.flow.first | ||
|
|
||
| // Define the DataStore instance as an extension on Context. | ||
| // The name "fusionauth_settings" is the file name where preferences will be stored. | ||
| private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "fusionauth_settings") | ||
|
|
||
| /** | ||
| * An implementation of the [Storage] interface that uses Android's Jetpack DataStore for persistence. | ||
| * This class is designed to be a modern replacement for SharedPreferences-based storage. | ||
| * | ||
| * @param context The application context, used to get the DataStore instance. | ||
| */ | ||
| class DataStoreStorage(private val context: Context) : Storage { | ||
|
|
||
| /** | ||
| * Retrieves the value associated with the given key from DataStore. | ||
| * This operation is performed asynchronously. | ||
| * | ||
| * @param key The key for which to retrieve the value. | ||
| * @return The value associated with the key, or null if the key is not found. | ||
| */ | ||
| override suspend fun get(key: String): String? { | ||
| val prefKey = stringPreferencesKey(key) | ||
| return context.dataStore.data.first()[prefKey] | ||
| } | ||
|
|
||
| /** | ||
| * Sets the value for the given key in DataStore. The content is converted to a String. | ||
| * This operation is performed asynchronously. | ||
| * | ||
| * @param key The key for which to set the value. | ||
| * @param content The value to be set. It will be stored as a String. | ||
| */ | ||
| override suspend fun set(key: String, content: Any) { | ||
| val prefKey = stringPreferencesKey(key) | ||
| context.dataStore.edit { settings -> | ||
| settings[prefKey] = content.toString() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Removes the value associated with the given key from DataStore. | ||
| * This operation is performed asynchronously. | ||
| * | ||
| * @param key The key for which to remove the value. | ||
| */ | ||
| override suspend fun remove(key: String) { | ||
| val prefKey = stringPreferencesKey(key) | ||
| context.dataStore.edit { settings -> | ||
| settings.remove(prefKey) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check notice
Code scanning / mobsfscan
The App logs information. Sensitive information should never be logged. Note