Skip to content

Commit

Permalink
Resolve google sign in with updated credentials call (#1080)
Browse files Browse the repository at this point in the history
* Resolve google sign in with updated credentials call

* Restore Firebase BOM

* Adjust Firebase AppCheck reference

---------

Co-authored-by: Ashley Davies <[email protected]>
  • Loading branch information
ashdavies and ashdavies authored Jul 25, 2024
1 parent dc4ff25 commit d69ae96
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import io.ashdavies.circuit.uiFactoryOf
import io.ashdavies.content.PlatformContext
import io.ashdavies.content.reportFullyDrawn

public fun afterPartyPresenterFactory(): Presenter.Factory {
public fun afterPartyPresenterFactory(context: PlatformContext): Presenter.Factory {
return presenterFactoryOf<AfterPartyScreen> { _, navigator ->
AfterPartyPresenter(navigator)
AfterPartyPresenter(context, navigator)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,68 @@
package io.ashdavies.party

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.slack.circuit.foundation.onNavEvent
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.screen.Screen
import io.ashdavies.content.PlatformContext
import io.ashdavies.gallery.GalleryScreen
import io.ashdavies.identity.CredentialQueries
import io.ashdavies.identity.IdentityManager
import io.ashdavies.identity.IdentityState
import io.ashdavies.sql.rememberLocalQueries
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@Composable
internal fun AfterPartyPresenter(navigator: Navigator): AfterPartyScreen.State {
internal fun AfterPartyPresenter(
platformContext: PlatformContext,
navigator: Navigator,
): AfterPartyScreen.State = AfterPartyPresenter(
identityManager = rememberIdentityManager(
platformContext = platformContext,
credentialQueries = rememberLocalQueries {
it.credentialQueries
},
),
coroutineScope = rememberCoroutineScope(),
navigator = navigator,
)

@Composable
private fun AfterPartyPresenter(
identityManager: IdentityManager,
coroutineScope: CoroutineScope,
navigator: Navigator,
): AfterPartyScreen.State {
val identityState by identityManager.state.collectAsState(IdentityState.Unauthenticated)
var screen by rememberRetained { mutableStateOf<Screen>(GalleryScreen) }

return AfterPartyScreen.State(
identityState = IdentityState.Unsupported,
identityState = identityState,
screen = screen,
) { event ->
when (event) {
is AfterPartyScreen.Event.Login -> coroutineScope.launch { identityManager.signIn() }
is AfterPartyScreen.Event.ChildNav -> navigator.onNavEvent(event.navEvent)
is AfterPartyScreen.Event.BottomNav -> screen = event.screen
}
}
}

@Composable
private fun rememberIdentityManager(
platformContext: PlatformContext,
credentialQueries: CredentialQueries,
): IdentityManager = remember(platformContext, credentialQueries) {
IdentityManager(
platformContext = platformContext,
credentialQueries = credentialQueries,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ internal object AfterPartyScreen : Parcelable, Screen {
sealed interface Event : CircuitUiEvent {
data class ChildNav(val navEvent: NavEvent) : Event
data class BottomNav(val screen: Screen) : Event

data object Login : Event
}

data class State(
Expand All @@ -66,7 +68,7 @@ internal fun AfterPartyScreen(
actions = {
ProfileActionButton(
identityState = state.identityState,
onClick = { },
onClick = { eventSink(AfterPartyScreen.Event.Login) },
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,24 @@ internal fun ProfileActionButton(
) {
Crossfade(identityState, modifier) { state ->
when (state) {
is IdentityState.Authenticated -> IconButton(onClick = onClick) {
is IdentityState.Authenticated -> IconButton(onClick) {
Image(
painter = rememberAsyncImagePainter(state.pictureProfileUrl),
contentDescription = null,
contentDescription = "Profile",
modifier = Modifier.clip(CircleShape),
)
}

is IdentityState.Failure -> Image(
imageVector = Icons.Filled.Warning,
contentDescription = "Failure",
modifier = Modifier.clickable(onClick = onClick),
colorFilter = ColorFilter.tint(tintColor),
)
is IdentityState.Failure -> IconButton(onClick) {
Image(
imageVector = Icons.Filled.Warning,
contentDescription = "Failure",
modifier = Modifier.clickable(onClick = onClick),
colorFilter = ColorFilter.tint(tintColor),
)
}

IdentityState.Unauthenticated -> IconButton(onClick = onClick) {
IdentityState.Unauthenticated -> IconButton(onClick) {
Image(
imageVector = Icons.Filled.AccountCircle,
contentDescription = "SignIn",
Expand Down
1 change: 0 additions & 1 deletion app-check/app-check-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ kotlin {

androidMain.dependencies {
implementation(dependencies.platform(libs.google.firebase.bom))
implementation(libs.gitlive.firebase.app)
implementation(libs.google.firebase.appcheck.playintegrity)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import androidx.compose.runtime.remember
import com.google.firebase.appcheck.AppCheckToken
import com.google.firebase.appcheck.FirebaseAppCheck
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.app
import io.ashdavies.http.AppCheckToken
import io.ashdavies.http.ProvideHttpClient
import io.ktor.client.HttpClient
Expand All @@ -21,7 +19,7 @@ import kotlinx.coroutines.flow.channelFlow

@Composable
public actual fun ProvideAppCheckToken(client: HttpClient, content: @Composable () -> Unit) {
val appCheck = remember { Firebase.app.android.appCheck }
val appCheck = remember { FirebaseAppCheck.getInstance() }
val token by produceState<AppCheckToken?>(null) {
appCheck.appCheckToken().collect { value = it }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public fun Circuit(context: PlatformContext): Circuit = Circuit.Builder()
.build()

private fun getPresenterFactories(context: PlatformContext) = listOf(
afterPartyPresenterFactory(),
afterPartyPresenterFactory(context),
dominionPresenterFactory(),
eventsPresenterFactory(),
galleryPresenterFactory(context),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,91 @@
package io.ashdavies.identity

import androidx.credentials.CredentialManager
import androidx.credentials.CredentialOption
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialException
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import io.ashdavies.content.PlatformContext

internal actual class GoogleIdIdentityService actual constructor(
private val context: PlatformContext,
) : IdentityService<GoogleIdIdentityRequest> {

override suspend fun request(request: GoogleIdIdentityRequest): IdentityResponse {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(request.filterByAuthorizedAccounts)
.setAutoSelectEnabled(request.autoSelectEnabled)
.setServerClientId(request.serverClientId)
.setNonce(request.nonce)
.build()
private val credentialManager: CredentialManager by lazy(LazyThreadSafetyMode.NONE) {
CredentialManager.create(context)
}

val getCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
override suspend fun request(request: GoogleIdIdentityRequest): IdentityResponse {
val getCredentialResponse = try {
getCredential(request, filterByAuthorizedAccounts = true)
} catch (ignored: GetCredentialException) {
try {
getCredential(request, filterByAuthorizedAccounts = false)
} catch (ignored: GetCredentialException) {
val signInWithGoogleOption = GetSignInWithGoogleOption
.Builder(request.serverClientId)
.setNonce(request.nonce)
.build()

val credentialManager = CredentialManager.create(context)
val getCredentialResponse = credentialManager.getCredential(
context = context,
request = getCredentialRequest,
)
getCredential(signInWithGoogleOption)
}
}

val googleIdCredential = when (val credential = getCredentialResponse.credential) {
is GoogleIdTokenCredential -> credential
return when (val credential = getCredentialResponse.credential) {
is GoogleIdTokenCredential -> IdentityResponse(
uuid = credential.id,
pictureProfileUrl = credential
.profilePictureUri
?.toString(),
)

is CustomCredential -> when (credential.type) {
GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> {
GoogleIdTokenCredential.createFrom(credential.data)
is CustomCredential -> {
check(credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
"Unrecognised credential type ${credential.type}"
}

else -> throw UnsupportedOperationException()
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)

IdentityResponse(
uuid = googleIdTokenCredential.id,
pictureProfileUrl = googleIdTokenCredential
.profilePictureUri
?.toString(),
)
}

else -> throw UnsupportedOperationException()
else -> throw UnsupportedOperationException("Unrecognised credential $credential")
}
}

return IdentityResponse(
uuid = googleIdCredential.idToken,
pictureProfileUrl = googleIdCredential
.profilePictureUri
?.toString(),
private suspend fun getCredential(
request: GoogleIdIdentityRequest,
filterByAuthorizedAccounts: Boolean,
): GetCredentialResponse {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(filterByAuthorizedAccounts)
.setAutoSelectEnabled(request.autoSelectEnabled)
.setServerClientId(request.serverClientId)
.setNonce(request.nonce)
.build()

return getCredential(googleIdOption)
}

private suspend fun getCredential(
option: CredentialOption,
): GetCredentialResponse {
val request = GetCredentialRequest.Builder()
.addCredentialOption(option)
.build()

return credentialManager.getCredential(
context = context,
request = request,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ internal expect class GoogleIdIdentityService(

internal data class GoogleIdIdentityRequest(
val serverClientId: String,
val filterByAuthorizedAccounts: Boolean = true,
val autoSelectEnabled: Boolean = true,
val nonce: String? = randomUuid(),
) : IdentityRequest
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package io.ashdavies.identity

import io.ashdavies.content.PlatformContext
import io.ashdavies.sql.mapToOneOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.merge

public interface IdentityManager {
public val state: Flow<IdentityState>
public suspend fun signIn()
}

public fun IdentityManager(
platformContext: io.ashdavies.content.PlatformContext,
platformContext: PlatformContext,
credentialQueries: CredentialQueries,
): IdentityManager = IdentityManager(
credentialQueries = credentialQueries,
Expand All @@ -23,15 +26,20 @@ internal fun IdentityManager(
identityService: GoogleIdIdentityService,
): IdentityManager = object : IdentityManager {

override val state: Flow<IdentityState> = credentialQueries.selectAll().mapToOneOrNull {
private val states = MutableStateFlow<IdentityState>(IdentityState.Unauthenticated)

private val queries = credentialQueries.selectAll().mapToOneOrNull {
if (it != null) IdentityState.Authenticated(it.profilePictureUrl) else IdentityState.Unauthenticated
}

override val state: Flow<IdentityState> = merge(states, queries)

override suspend fun signIn() {
val identityRequest = GoogleIdIdentityRequest(BuildConfig.SERVER_CLIENT_ID)
val identityResponse = try {
identityService.request(identityRequest)
} catch (_: UnsupportedOperationException) {
} catch (exception: UnsupportedOperationException) {
states.value = IdentityState.Failure(exception.message)
return
}

Expand Down

0 comments on commit d69ae96

Please sign in to comment.