From b4095ae23b1c1744d51fc86832ef3a8bc740641f Mon Sep 17 00:00:00 2001 From: Matt Hamann Date: Fri, 18 Apr 2025 10:47:43 -0400 Subject: [PATCH 1/2] fix(instant): option to force instant users to add an identity --- .../io/rownd/android/models/RowndConfig.kt | 6 +++++ .../io/rownd/android/models/domain/User.kt | 3 ++- .../io/rownd/android/models/network/Auth.kt | 2 ++ .../io/rownd/android/models/network/User.kt | 7 ++++-- .../io/rownd/android/models/repos/AuthRepo.kt | 6 +++++ .../rownd/android/models/repos/StateRepo.kt | 25 +++++++++++++++++++ .../java/io/rownd/android/util/AuthLevel.kt | 16 ++++++++++++ .../io/rownd/android/views/RowndWebView.kt | 22 +++++++++++----- 8 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 android/src/main/java/io/rownd/android/util/AuthLevel.kt diff --git a/android/src/main/java/io/rownd/android/models/RowndConfig.kt b/android/src/main/java/io/rownd/android/models/RowndConfig.kt index de6665f..80b43c1 100644 --- a/android/src/main/java/io/rownd/android/models/RowndConfig.kt +++ b/android/src/main/java/io/rownd/android/models/RowndConfig.kt @@ -28,6 +28,12 @@ data class RowndConfig( @Transient var subdomainExtension: String = ".rownd.link", @Transient + var forceInstantUserConversion: Boolean = false, + @Transient + var enableDebugMode: Boolean = false, + + // Internals + @Transient internal var stateFileName: String = "rownd_state.json" ) { @Inject diff --git a/android/src/main/java/io/rownd/android/models/domain/User.kt b/android/src/main/java/io/rownd/android/models/domain/User.kt index 94d6ee5..20f083d 100644 --- a/android/src/main/java/io/rownd/android/models/domain/User.kt +++ b/android/src/main/java/io/rownd/android/models/domain/User.kt @@ -4,6 +4,7 @@ import android.util.Log import io.rownd.android.models.repos.StateRepo import io.rownd.android.models.repos.UserRepo import io.rownd.android.util.AnyValueSerializer +import io.rownd.android.util.AuthLevel import io.rownd.android.util.Encryption import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -15,7 +16,7 @@ data class User( val redacted: MutableList = mutableListOf(), val state: String? = "enabled", @SerialName("auth_level") - val authLevel: String? = "unverified", + val authLevel: AuthLevel? = AuthLevel.Unverified, var isLoading: Boolean = false ) { fun asNetworkModel(stateRepo: StateRepo, userRepo: UserRepo): NetworkUser { diff --git a/android/src/main/java/io/rownd/android/models/network/Auth.kt b/android/src/main/java/io/rownd/android/models/network/Auth.kt index 78680f3..6ba7f1a 100644 --- a/android/src/main/java/io/rownd/android/models/network/Auth.kt +++ b/android/src/main/java/io/rownd/android/models/network/Auth.kt @@ -34,6 +34,8 @@ data class TokenRequestBody internal constructor( val appId: String? = null, @SerialName("intent") val intent: RowndSignInIntent? = null, + @SerialName("instant_user_id") + var instantUserId: String? = null, ) @Serializable diff --git a/android/src/main/java/io/rownd/android/models/network/User.kt b/android/src/main/java/io/rownd/android/models/network/User.kt index 2ffe6a9..3b90b47 100644 --- a/android/src/main/java/io/rownd/android/models/network/User.kt +++ b/android/src/main/java/io/rownd/android/models/network/User.kt @@ -5,6 +5,7 @@ import io.rownd.android.models.domain.AppSchemaEncryptionState import io.rownd.android.models.repos.StateRepo import io.rownd.android.models.repos.UserRepo import io.rownd.android.util.AnyValueSerializer +import io.rownd.android.util.AuthLevel import io.rownd.android.util.Encryption import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,12 +17,14 @@ data class User( val redacted: List = listOf(), val state: String? = "enabled", @SerialName("auth_level") - val authLevel: String? = null + val authLevel: AuthLevel? = null ) { fun asDomainModel(stateRepo: StateRepo, userRepo: UserRepo): DomainUser { return DomainUser( data = dataAsDecrypted(stateRepo, userRepo), - redacted = redacted.toMutableList() + redacted = redacted.toMutableList(), + state = state, + authLevel = authLevel ) } diff --git a/android/src/main/java/io/rownd/android/models/repos/AuthRepo.kt b/android/src/main/java/io/rownd/android/models/repos/AuthRepo.kt index 34f461b..87608d0 100644 --- a/android/src/main/java/io/rownd/android/models/repos/AuthRepo.kt +++ b/android/src/main/java/io/rownd/android/models/repos/AuthRepo.kt @@ -21,6 +21,7 @@ import io.rownd.android.models.network.SignOutRequestBody import io.rownd.android.models.network.SignOutResponse import io.rownd.android.models.network.TokenRequestBody import io.rownd.android.models.network.TokenResponse +import io.rownd.android.util.AuthLevel import io.rownd.android.util.AuthenticatedApiClient import io.rownd.android.util.InvalidRefreshTokenException import io.rownd.android.util.NetworkConnectionFailureException @@ -96,6 +97,11 @@ class AuthRepo @Inject constructor() { idToken = idToken, intent = intent ) + + if (stateRepo.state.value.user.authLevel == AuthLevel.Instant) { + tokenRequest.instantUserId = stateRepo.state.value.user.data["user_id"]?.toString() + } + return fetchTokenAsync(tokenRequest, intent, type).await() } diff --git a/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt b/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt index 886ffd9..b9a4f8f 100644 --- a/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt +++ b/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt @@ -21,9 +21,12 @@ import io.rownd.android.models.domain.AuthState import io.rownd.android.models.domain.SignInState import io.rownd.android.models.domain.User import io.rownd.android.util.AppLifecycleListener +import io.rownd.android.util.AuthLevel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable @@ -149,6 +152,8 @@ class StateRepo @Inject constructor() { userRepo.loadUserAsync().await() } + tmpForceInstantUserConversionIfRequested(CoroutineScope(Dispatchers.IO)) + // Persist all state updates to cache when changes occur store.stateAsStateFlow().collect { val updatedState = it @@ -161,6 +166,26 @@ class StateRepo @Inject constructor() { return store } + private fun tmpForceInstantUserConversionIfRequested(scope: CoroutineScope) { + if (!Rownd.config.forceInstantUserConversion) { + return + } + + scope.launch { + Rownd.state + .map { it.auth.isAuthenticated to it.user } + .distinctUntilChanged() + .collect { (isAuthenticated, user) -> + if ( + isAuthenticated && + user.authLevel == AuthLevel.Instant + ) { + Rownd.requestSignIn() + } + } + } + } + val state = store.stateAsStateFlow() fun getStore(): Store { diff --git a/android/src/main/java/io/rownd/android/util/AuthLevel.kt b/android/src/main/java/io/rownd/android/util/AuthLevel.kt new file mode 100644 index 0000000..8143954 --- /dev/null +++ b/android/src/main/java/io/rownd/android/util/AuthLevel.kt @@ -0,0 +1,16 @@ +package io.rownd.android.util + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class AuthLevel { + @SerialName("instant") + Instant, + @SerialName("guest") + Guest, + @SerialName("unverified") + Unverified, + @SerialName("verified") + Verified +} \ No newline at end of file diff --git a/android/src/main/java/io/rownd/android/views/RowndWebView.kt b/android/src/main/java/io/rownd/android/views/RowndWebView.kt index 406ec37..f1916e8 100644 --- a/android/src/main/java/io/rownd/android/views/RowndWebView.kt +++ b/android/src/main/java/io/rownd/android/views/RowndWebView.kt @@ -164,7 +164,6 @@ class RowndWebViewClient(private val webView: RowndWebView, private val context: private var timeout: Boolean = true init { - CoroutineScope(Dispatchers.IO).launch { delay(20000) if (timeout) { @@ -294,6 +293,7 @@ class RowndWebViewClient(private val webView: RowndWebView, private val context: } setFeatureFlagJs() + setDebugFlags() view.setLayerType(WebView.LAYER_TYPE_HARDWARE, null) @@ -361,6 +361,16 @@ class RowndWebViewClient(private val webView: RowndWebView, private val context: evaluateJavascript(code) } + private fun setDebugFlags() { + if (Rownd.config.enableDebugMode) { + evaluateJavascript(""" + if (rownd?.setLogLevel) { + rownd.setLogLevel('default', 'debug'); + } + """.trimIndent()) + } + } + private fun handleScriptReturn(value: String) { Log.d("Rownd.hub", value) } @@ -488,9 +498,9 @@ class RowndJavascriptInterface constructor( val authChallengeMessage = (interopMessage as AuthChallengeInitiatedMessage) Rownd.store.dispatch( StateAction.SetAuth( - AuthState( + parentWebView.rowndClient.stateRepo.state.value.auth.copy( challengeId = authChallengeMessage.payload.challengeId, - userIdentifier = authChallengeMessage.payload.userIdentifier + userIdentifier = authChallengeMessage.payload.userIdentifier, ) ) ) @@ -500,9 +510,9 @@ class RowndJavascriptInterface constructor( parentWebView.rowndClient.store.currentState.auth.let { Rownd.store.dispatch( StateAction.SetAuth( - AuthState( - accessToken = it.accessToken, - refreshToken = it.refreshToken, + it.copy( + challengeId = null, + userIdentifier = null, ) ) ) From 01c94b194fb71c65c93d54f9f4f1a221a0311992 Mon Sep 17 00:00:00 2001 From: Matt Hamann Date: Fri, 18 Apr 2025 16:40:02 -0400 Subject: [PATCH 2/2] fix(instant): add titles to modal --- android/src/main/java/io/rownd/android/Rownd.kt | 4 +++- .../main/java/io/rownd/android/models/repos/StateRepo.kt | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/rownd/android/Rownd.kt b/android/src/main/java/io/rownd/android/Rownd.kt index 19d0af3..bc330e9 100644 --- a/android/src/main/java/io/rownd/android/Rownd.kt +++ b/android/src/main/java/io/rownd/android/Rownd.kt @@ -456,7 +456,9 @@ abstract class RowndSignInOptionsBase() { data class RowndSignInOptions( @SerialName("post_login_redirect") var postSignInRedirect: String? = Rownd.config.postSignInRedirect, - var intent: RowndSignInIntent? = null + var intent: RowndSignInIntent? = null, + var title: String? = null, + var subtitle: String? = null, ) : RowndSignInOptionsBase() { override fun toJsonString(): String { return json.encodeToString(serializer(), this) diff --git a/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt b/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt index b9a4f8f..27aab95 100644 --- a/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt +++ b/android/src/main/java/io/rownd/android/models/repos/StateRepo.kt @@ -11,8 +11,10 @@ import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import io.opentelemetry.api.trace.StatusCode import io.rownd.android.Rownd +import io.rownd.android.RowndSignInIntent import io.rownd.android.RowndSignInJsOptions import io.rownd.android.RowndSignInLoginStep +import io.rownd.android.RowndSignInOptions import io.rownd.android.models.Action import io.rownd.android.models.State import io.rownd.android.models.Store @@ -180,7 +182,12 @@ class StateRepo @Inject constructor() { isAuthenticated && user.authLevel == AuthLevel.Instant ) { - Rownd.requestSignIn() + val signInOptions = RowndSignInOptions( + intent = RowndSignInIntent.SignUp, + title = "Add a sign-in method", + subtitle = "To ensure you can always access your account, please add a sign-in method." + ) + Rownd.requestSignIn(signInOptions) } } }