From 832f27078c0e13ddc920290ea9187fd3e561747d Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Thu, 11 Apr 2024 13:09:26 +0200 Subject: [PATCH] Add Google signin (#255) --- .../androidmakers/AndroidMakersApplication.kt | 5 +- .../fr/paug/androidmakers/MainActivity.kt | 30 +- .../androidmakers/MainActivityViewModel.kt | 39 -- .../paug/androidmakers/di/ViewModelModule.kt | 9 - gradle/libs.versions.toml | 4 +- shared/data/build.gradle.kts | 2 +- .../graphql/ApolloClientBuilder.android.kt | 19 +- .../src/commonMain/graphql/bookmarks.graphql | 15 +- .../src/commonMain/graphql/extra.graphqls | 2 +- .../src/commonMain/graphql/schema.graphqls | 607 ++++++++---------- .../store/firebase/FirebaseUserRepository.kt | 27 +- .../androidmakers/store/firebase/mappers.kt | 4 +- .../graphql/SessionsGraphQLRepository.kt | 85 +-- .../fr/androidmakers/di/DataModule.android.kt | 9 +- shared/domain/build.gradle.kts | 1 + .../fr/androidmakers/domain/model/User.kt | 8 +- .../domain/repo/UserRepository.kt | 3 + .../kotlin/com/androidmakers/ui/MainLayout.kt | 11 +- .../androidmakers/ui/common/SigninButton.kt | 79 +++ .../ui/common/navigation/AVALayout.kt | 14 +- .../ui/common/navigation/UserViewModel.kt | 9 + .../wear/ui/main/MainViewModel.kt | 5 +- .../wear/ui/signin/GoogleSignInViewModel.kt | 19 + 23 files changed, 510 insertions(+), 496 deletions(-) delete mode 100644 androidApp/src/main/java/fr/paug/androidmakers/MainActivityViewModel.kt delete mode 100644 androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt create mode 100644 shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/SigninButton.kt create mode 100644 shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt diff --git a/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt b/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt index 6c401b20..33aa2700 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt @@ -3,7 +3,6 @@ package fr.paug.androidmakers import android.app.Application import com.androidmakers.di.viewModelModule import fr.androidmakers.di.DependenciesBuilder -import fr.paug.androidmakers.di.androidViewModelModule class AndroidMakersApplication : Application() { @@ -12,9 +11,9 @@ class AndroidMakersApplication : Application() { DependenciesBuilder(this).inject( listOf( - androidViewModelModule, - viewModelModule + viewModelModule, ) ) } } + diff --git a/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt b/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt index e7cb75c8..dbd3256f 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt @@ -11,12 +11,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.androidmakers.ui.LocalPlatformContext import com.androidmakers.ui.MainLayout +import com.androidmakers.ui.common.SigninCallbacks +import com.androidmakers.ui.common.navigation.UserData import com.androidmakers.ui.theme.AndroidMakersTheme import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount @@ -29,16 +30,12 @@ import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.auth.GoogleAuthProvider import dev.gitlive.firebase.auth.auth import fr.androidmakers.store.firebase.toUser -import fr.paug.androidmakers.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.compose.KoinContext class MainActivity : AppCompatActivity() { - private val viewModel: MainActivityViewModel by viewModel() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -49,14 +46,12 @@ class MainActivity : AppCompatActivity() { setContent { val rememberedActivity = remember { this } - val userState = viewModel.user.collectAsState(null) - val darkTheme = isSystemInDarkTheme() - CompositionLocalProvider( LocalPlatformContext provides rememberedActivity, ) { KoinContext { AndroidMakersTheme { + val darkTheme = isSystemInDarkTheme() DisposableEffect(darkTheme) { enableEdgeToEdge( statusBarStyle = SystemBarStyle.auto( @@ -72,9 +67,12 @@ class MainActivity : AppCompatActivity() { } MainLayout( - user = userState.value, versionName = BuildConfig.VERSION_NAME, versionCode = BuildConfig.VERSION_CODE.toString(), + signinCallbacks = SigninCallbacks( + signin = { signin() }, + signout = { signout() }, + ) ) } } @@ -114,7 +112,9 @@ class MainActivity : AppCompatActivity() { val result = auth.signInWithCredential(firebaseCredential) // Sign in success, update UI with the signed-in user's information lifecycleScope.launch { - viewModel.setUser(result.user?.toUser()) + UserData().userRepository.setUser(result.user?.toUser()) + println("user id=${result.user?.uid}") + println("idToken=${result.user?.getIdToken(true)}") } } } @@ -124,7 +124,7 @@ class MainActivity : AppCompatActivity() { } catch (e: ApiException) { e.printStackTrace() lifecycleScope.launch { - viewModel.setUser(null) + UserData().userRepository.setUser(null) } } } @@ -134,7 +134,7 @@ class MainActivity : AppCompatActivity() { fun signout() { val activity = this val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(activity.getString(R.string.default_web_client_id)) + .requestIdToken("985196411897-r7edbi9jgo3hfupekcmdrg66inonj0o5.apps.googleusercontent.com") .build() val googleSignInClient = GoogleSignIn.getClient(activity, gso) @@ -142,14 +142,14 @@ class MainActivity : AppCompatActivity() { Firebase.auth.signOut() googleSignInClient.signOut() googleSignInClient.revokeAccess() - viewModel.setUser(null) + UserData().userRepository.setUser(null) } } fun signin() { val activity = this val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(activity.getString(R.string.default_web_client_id)) + .requestIdToken("985196411897-r7edbi9jgo3hfupekcmdrg66inonj0o5.apps.googleusercontent.com") .build() val googleSignInClient = GoogleSignIn.getClient(activity, gso) @@ -159,5 +159,5 @@ class MainActivity : AppCompatActivity() { companion object { const val REQ_SIGNIN = 33 } - } + diff --git a/androidApp/src/main/java/fr/paug/androidmakers/MainActivityViewModel.kt b/androidApp/src/main/java/fr/paug/androidmakers/MainActivityViewModel.kt deleted file mode 100644 index c4b8bf8c..00000000 --- a/androidApp/src/main/java/fr/paug/androidmakers/MainActivityViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -package fr.paug.androidmakers - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import fr.androidmakers.domain.interactor.SyncBookmarksUseCase -import fr.androidmakers.domain.model.User -import fr.androidmakers.domain.repo.UserRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - -class MainActivityViewModel( - private val userRepository: UserRepository, - val syncBookmarksUseCase: SyncBookmarksUseCase - -) : ViewModel() { - private val _user = MutableStateFlow(null) - val user: Flow = _user - - init { - viewModelScope.launch { - _user.emit(userRepository.getUser()) - - val currentUser = _user.value - if (currentUser != null) { - // fire & forget - // This is racy but oh well... - syncBookmarksUseCase(currentUser.id) - } - } - } - - suspend fun setUser(user: User?) { - _user.emit(user) - user?.let { - syncBookmarksUseCase(it.id) - } - } -} diff --git a/androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt b/androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt deleted file mode 100644 index c1f4c155..00000000 --- a/androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package fr.paug.androidmakers.di - -import fr.paug.androidmakers.MainActivityViewModel -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.dsl.module - -val androidViewModelModule = module { - viewModel { MainActivityViewModel(get(), get()) } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 459a4024..aec23841 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ datastore = "1.1.0-beta02" skie = "0.6.2" firebase = "1.11.1" koin = "3.5.3" -precompose = "1.6.0-rc02" +precompose = "1.6.0" compose-plugin = "1.6.1" image-loader = "1.7.8" pullrefresh = "1.3.0" @@ -88,7 +88,7 @@ android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", ver firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "crashlytics-plugin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } materii-pullrefresh = { group = "dev.materii.pullrefresh", name = "pullrefresh", version.ref = "pullrefresh" } - +okio = "com.squareup.okio:okio:3.9.0" # Wear dependencies play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version.ref = "playServicesWearable" } androidx-splashscreen = "androidx.core:core-splashscreen:1.0.1" diff --git a/shared/data/build.gradle.kts b/shared/data/build.gradle.kts index 5302ad77..7f862a22 100644 --- a/shared/data/build.gradle.kts +++ b/shared/data/build.gradle.kts @@ -47,7 +47,7 @@ apollo { service("service") { packageName.set("fr.androidmakers.store.graphql") generateDataBuilders.set(true) - mapScalar("LocalDateTime", "kotlinx.datetime.LocalDateTime", "com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter") + mapScalar("GraphQLLocalDateTime", "kotlinx.datetime.LocalDateTime", "com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter") introspection { schemaFile.set(file("src/commonMain/graphql/schema.graphqls")) diff --git a/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt b/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt index 0346a844..0b0a2fa4 100644 --- a/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt +++ b/shared/data/src/androidMain/kotlin/fr/androidmakers/store/graphql/ApolloClientBuilder.android.kt @@ -1,6 +1,7 @@ package fr.androidmakers.store.graphql import android.content.Context +import android.service.autofill.UserData import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse @@ -11,11 +12,15 @@ import com.apollographql.apollo3.network.http.HttpInterceptor import com.apollographql.apollo3.network.http.HttpInterceptorChain import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase +import fr.androidmakers.domain.repo.UserRepository +import fr.androidmakers.store.firebase.FirebaseUserRepository +import java.util.PrimitiveIterator actual class ApolloClientBuilder( - context: Context, - private val url: String, - private val conference: String + context: Context, + private val url: String, + private val conference: String, + private val userRepository: UserRepository, ) { private val memoryCacheFactory = MemoryCacheFactory(20_000_000).chain(SqlNormalizedCacheFactory(context)) actual fun build(): ApolloClient { @@ -30,10 +35,10 @@ actual class ApolloClientBuilder( /** * */ -// val token = Firebase.auth.currentUser?.getIdToken(false)?.result?.token -// if (token != null) { -// addHeader("Authorization", "Bearer $token") -// } + val token = userRepository.getUser()?.idToken + if (token != null) { + addHeader("Authorization", "Bearer $token") + } } .build() ) diff --git a/shared/data/src/commonMain/graphql/bookmarks.graphql b/shared/data/src/commonMain/graphql/bookmarks.graphql index 5ac8134a..ba28575a 100644 --- a/shared/data/src/commonMain/graphql/bookmarks.graphql +++ b/shared/data/src/commonMain/graphql/bookmarks.graphql @@ -1,18 +1,23 @@ query Bookmarks { - bookmarks { - id - sessionIds + bookmarkConnection { + nodes { + id + } } } mutation AddBookmark($sessionId: String!) { addBookmark(sessionId: $sessionId) { - sessionIds + nodes { + id + } } } mutation RemoveBookmark($sessionId: String!) { removeBookmark(sessionId: $sessionId) { - sessionIds + nodes { + id + } } } \ No newline at end of file diff --git a/shared/data/src/commonMain/graphql/extra.graphqls b/shared/data/src/commonMain/graphql/extra.graphqls index c01d319a..67b4dd02 100644 --- a/shared/data/src/commonMain/graphql/extra.graphqls +++ b/shared/data/src/commonMain/graphql/extra.graphqls @@ -3,7 +3,7 @@ extend type Room @typePolicy(keyFields: "id") extend type Session @typePolicy(keyFields: "id") -extend type Query @fieldPolicy(forField: "session", keyArgs: "id") +extend type RootQuery @fieldPolicy(forField: "session", keyArgs: "id") extend type Speaker @typePolicy(keyFields: "id") diff --git a/shared/data/src/commonMain/graphql/schema.graphqls b/shared/data/src/commonMain/graphql/schema.graphqls index 4bb0a3b2..22eda925 100644 --- a/shared/data/src/commonMain/graphql/schema.graphqls +++ b/shared/data/src/commonMain/graphql/schema.graphqls @@ -1,173 +1,219 @@ -type Bookmarks { - id: String! +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +type __Schema { + description: String + + types: [__Type!]! + + queryType: __Type! - sessionIds: [String!]! + mutationType: __Type + + subscriptionType: __Type + + directives: [__Directive!]! } -""" -Built-in Boolean -""" -scalar Boolean +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +type __Type { + kind: __TypeKind! -type Conference { - days: [LocalDate!]! + name: String - id: String! + description: String - name: String! + fields(includeDeprecated: Boolean = false): [__Field!] - timezone: String! -} + interfaces: [__Type!] -enum ConferenceField { - DAYS -} + possibleTypes: [__Type!] -input ConferenceOrderByInput { - direction: OrderByDirection! + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - field: ConferenceField! + inputFields(includeDeprecated: Boolean = false): [__InputValue!] + + ofType: __Type + + specifiedByURL: String } -""" -Built-in Float -""" -scalar Float +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +enum __TypeKind { + SCALAR -""" -A type representing a formatted kotlinx.datetime.Instant -""" -scalar Instant + OBJECT -""" -Built-in Int -""" -scalar Int + INTERFACE -""" -A type representing a formatted kotlinx.datetime.LocalDate -""" -scalar LocalDate + UNION -""" -A type representing a formatted kotlinx.datetime.LocalDateTime -""" -scalar LocalDateTime + ENUM + + INPUT_OBJECT -input LocalDateTimeFilterInput { - after: LocalDateTime + LIST - before: LocalDateTime + NON_NULL } -type Mutation { - addBookmark(sessionId: String!): Bookmarks! +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +type __Field { + name: String! - removeBookmark(sessionId: String!): Bookmarks! -} + description: String -enum OrderByDirection { - ASCENDING + args(includeDeprecated: Boolean = false): [__InputValue!]! - DESCENDING -} + type: __Type! -type PageInfo { - endCursor: String + isDeprecated: Boolean! + + deprecationReason: String } -type Partner { - logoUrl(dark: Boolean): String! +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +type __InputValue { + name: String! + + description: String + + type: __Type! + + defaultValue: String + isDeprecated: Boolean! + + deprecationReason: String +} + +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +type __EnumValue { name: String! - url: String! + description: String + + isDeprecated: Boolean! + + deprecationReason: String } -type PartnerGroup { - partners: [Partner!]! +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +type __Directive { + name: String! - title: String! + description: String + + locations: [__DirectiveLocation!]! + + args(includeDeprecated: Boolean = false): [__InputValue!]! + + isRepeatable: Boolean! } -type Query { - bookmarks: Bookmarks +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +enum __DirectiveLocation { + QUERY - conferences(orderBy: ConferenceOrderByInput): [Conference!]! + MUTATION - config: Conference! + SUBSCRIPTION - partnerGroups: [PartnerGroup!]! + FIELD - rooms: [Room!]! + FRAGMENT_DEFINITION - session(id: String!): Session! + FRAGMENT_SPREAD - sessions(first: Int, after: String, filter: SessionFilterInput, orderBy: SessionOrderByInput): SessionConnection! + INLINE_FRAGMENT - speaker(id: String!): Speaker! + VARIABLE_DEFINITION - speakers: [Speaker!]! @deprecated(reason: "Use speakersPage instead") + SCHEMA - speakersPage(first: Int, after: String): SpeakerConnection! + SCALAR - venue(id: String!): Venue! + OBJECT - venues: [Venue!]! -} + FIELD_DEFINITION -type Room { - capacity: Int + ARGUMENT_DEFINITION - id: String! + INTERFACE - name: String! + UNION + + ENUM + + ENUM_VALUE + + INPUT_OBJECT + + INPUT_FIELD_DEFINITION } -type Session { - complexity: String +""" + A type representing a formatted kotlinx.datetime.Instant +""" +scalar GraphQLInstant - description: String +scalar GraphQLLocalDate - endInstant: Instant! @deprecated(reason: "use endsAt instead") +scalar GraphQLLocalDateTime - endsAt: LocalDateTime! +type RootQuery { + rooms: [Room!]! - feedbackId: String + sessions(first: Int! = 10, after: String = null, orderBy: SessionOrderBy! = { + field: STARTS_AT + direction: ASCENDING + } + ): SessionConnection! - id: String! + speakers: [Speaker!]! - isServiceSession: Boolean! + speakersPage(first: Int! = 10, after: String = null): SpeakerConnection! - """ - either "French" or "English" - """ - language: String + speaker(id: String!): Speaker! - """ - A shorter version of description for use when real estate is scarce like watches for an example. - This field might have the same value as description if a shortDescription is not available - """ - shortDescription: String + venue(id: String!): Venue! - startInstant: Instant! @deprecated(reason: "use startsAt instead") + venues: [Venue!]! - startsAt: LocalDateTime! + partnerGroups: [PartnerGroup!]! - tags: [String!]! + session(id: String!): Session! - title: String! + config: Conference! + + bookmarkConnection: BookmarkConnection! + + conferences(orderBy: ConferenceOrderBy = null): [Conference!]! +} + +type RootMutation { + addBookmark(sessionId: String!): BookmarkConnection! + + removeBookmark(sessionId: String!): BookmarkConnection! """ - One of "break", "lunch", "party", "keynote", "talk" or any other conference-specific format + Deletes the current user account, requires authentication """ - type: String! + deleteAccount: Boolean! +} - room: Room +type Room { + id: String! - rooms: [Room!]! + name: String! - speakers: [Speaker!]! + capacity: Int } type SessionConnection { @@ -176,50 +222,32 @@ type SessionConnection { pageInfo: PageInfo! } -enum SessionField { - STARTS_AT -} - -input SessionFilterInput { - endsAt: LocalDateTimeFilterInput - - startsAt: LocalDateTimeFilterInput -} +input SessionOrderBy { + field: SessionField! -input SessionOrderByInput { direction: OrderByDirection! - - field: SessionField! } -type Social { - icon: String - - link: String! @deprecated(reason: "use url instead, replace with url") +type Speaker implements Node { + id: String! name: String! - url: String! -} - -type Speaker { bio: String - city: String + tagline: String company: String companyLogoUrl: String - id: String! + city: String - name: String! + socials: [Social!]! photoUrl: String - socials: [Social!]! - - tagline: String + photoUrlThumbnail: String sessions: [Session!]! } @@ -231,311 +259,188 @@ type SpeakerConnection { } """ -Built-in String + @property floorPlanUrl the url to an image containing the floor plan """ -scalar String - type Venue { - address: String - - coordinates: String @deprecated(reason: "use latitude and longitude instead") - - descriptionFr: String! @deprecated(reason: "use description(language: \"fr\") instead") - - floorPlanUrl: String - id: String! - imageUrl: String + name: String! latitude: Float longitude: Float - name: String! - - description(language: String): String! -} - -type __Directive { - """ - The __Directive type represents a Directive that a server supports. - """ - name: String! - - description: String - - isRepeatable: Boolean! - - locations: [__DirectiveLocation!]! + address: String - args(includeDeprecated: Boolean = false): [__InputValue!]! + imageUrl: String - onOperation: Boolean @deprecated(reason: "Use `locations`.") + floorPlanUrl: String - onFragment: Boolean @deprecated(reason: "Use `locations`.") + coordinates: String - onField: Boolean @deprecated(reason: "Use `locations`.") -} + descriptionFr: String! -""" -An enum describing valid locations where a directive can be placed -""" -enum __DirectiveLocation { """ - Indicates the directive is valid on queries. - """ - QUERY + The description of the venue. [description] may contain emojis and '\n' Chars but no markdown or HTML. + May be null if no description is available. """ - Indicates the directive is valid on mutations. - """ - MUTATION + description(language: String = "en"): String! +} - """ - Indicates the directive is valid on subscriptions. - """ - SUBSCRIPTION +type PartnerGroup { + title: String! - """ - Indicates the directive is valid on fields. - """ - FIELD + partners: [Partner!]! +} - """ - Indicates the directive is valid on fragment definitions. - """ - FRAGMENT_DEFINITION +type Session implements Node { + id: String! - """ - Indicates the directive is valid on fragment spreads. - """ - FRAGMENT_SPREAD + title: String! """ - Indicates the directive is valid on inline fragments. - """ - INLINE_FRAGMENT + The description of the event. [description] may contain emojis and '\n' Chars but no markdown or HTML. + May be null if no description is available. """ - Indicates the directive is valid on variable definitions. - """ - VARIABLE_DEFINITION + description: String """ - Indicates the directive is valid on a schema SDL definition. + A shorter version of description for use when real estate is scarce like watches for an example. + This field might have the same value as description if a shortDescription is not available """ - SCHEMA + shortDescription: String """ - Indicates the directive is valid on a scalar SDL definition. + An [IETF language code](https://en.wikipedia.org/wiki/IETF_language_tag) like en-US """ - SCALAR + language: String - """ - Indicates the directive is valid on an object SDL definition. - """ - OBJECT + tags: [String!]! - """ - Indicates the directive is valid on a field SDL definition. - """ - FIELD_DEFINITION + startsAt: GraphQLLocalDateTime! - """ - Indicates the directive is valid on a field argument SDL definition. - """ - ARGUMENT_DEFINITION + endsAt: GraphQLLocalDateTime! - """ - Indicates the directive is valid on an interface SDL definition. - """ - INTERFACE + complexity: String - """ - Indicates the directive is valid on an union SDL definition. - """ - UNION + feedbackId: String """ - Indicates the directive is valid on an enum SDL definition. + One of "break", "lunch", "party", "keynote", "talk" or any other conference-specific format """ - ENUM + type: String! - """ - Indicates the directive is valid on an enum value SDL definition. - """ - ENUM_VALUE + links: [Link!]! - """ - Indicates the directive is valid on an input object SDL definition. - """ - INPUT_OBJECT + speakers: [Speaker!]! - """ - Indicates the directive is valid on an input object field SDL definition. - """ - INPUT_FIELD_DEFINITION + room: Room + + rooms: [Room!]! } -type __EnumValue { +type Conference { + id: String! + name: String! - description: String + timezone: String! - isDeprecated: Boolean! + days: [GraphQLLocalDate!]! - deprecationReason: String + themeColor: String } -type __Field { - name: String! - - description: String +type BookmarkConnection { + nodes: [Session!]! +} - args(includeDeprecated: Boolean = false): [__InputValue!]! +input ConferenceOrderBy { + field: ConferenceField! - type: __Type! + direction: OrderByDirection! +} - isDeprecated: Boolean! +type PageInfo { + endCursor: String +} - deprecationReason: String +enum SessionField { + STARTS_AT } -type __InputValue { - name: String! +enum OrderByDirection { + ASCENDING - description: String + DESCENDING +} - type: __Type! +type Social { + icon: String - defaultValue: String + link: String! - isDeprecated: Boolean + name: String! - deprecationReason: String + url: String! } -""" -A GraphQL Introspection defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, the entry points for query, mutation, and subscription operations. -""" -type __Schema { - description: String - - """ - A list of all types supported by this server. - """ - types: [__Type!]! - - """ - The type that query operations will be rooted at. - """ - queryType: __Type! +interface Node { + id: String! +} - """ - If this server supports mutation, the type that mutation operations will be rooted at. - """ - mutationType: __Type +type Partner { + name: String! - """ - 'A list of all directives supported by this server. - """ - directives: [__Directive!]! + url: String! """ - 'If this server support subscription, the type that subscription operations will be rooted at. + @param dark returns the logo for use on a dark background or fallbacks to the light mode if none exist """ - subscriptionType: __Type + logoUrl(dark: Boolean = false): String! } -type __Type { - kind: __TypeKind! +type Link { + type: LinkType! - name: String - - description: String - - fields(includeDeprecated: Boolean = false): [__Field!] - - interfaces: [__Type!] - - possibleTypes: [__Type!] - - enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - - inputFields(includeDeprecated: Boolean = false): [__InputValue!] - - ofType: __Type - - specifiedByUrl: String + url: String! } -""" -An enum describing what kind of type a given __Type is -""" -enum __TypeKind { - """ - Indicates this type is a scalar. 'specifiedByUrl' is a valid field - """ - SCALAR - - """ - Indicates this type is an object. `fields` and `interfaces` are valid fields. - """ - OBJECT - - """ - Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. - """ - INTERFACE - - """ - Indicates this type is a union. `possibleTypes` is a valid field. - """ - UNION +enum ConferenceField { + DAYS +} - """ - Indicates this type is an enum. `enumValues` is a valid field. - """ - ENUM +enum LinkType { + YouTube - """ - Indicates this type is an input object. `inputFields` is a valid field. - """ - INPUT_OBJECT + Audio - """ - Indicates this type is a list. `ofType` is a valid field. - """ - LIST + AudioUncompressed - """ - Indicates this type is a non-null. `ofType` is a valid field. - """ - NON_NULL + Other } -""" -Directs the executor to include this field or fragment only when the `if` argument is true -""" -directive @include ("Included when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +directive @skip (if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT -""" -Directs the executor to skip this field or fragment when the `if` argument is true. -""" -directive @skip ("Skipped when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +directive @include (if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT -""" -Marks the field, argument, input field or enum value as deprecated -""" -directive @deprecated ("The reason for the deprecation" reason: String = "No longer supported") on FIELD_DEFINITION|ARGUMENT_DEFINITION|ENUM_VALUE|INPUT_FIELD_DEFINITION +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +directive @deprecated (reason: String = "No longer supported") on FIELD_DEFINITION|ARGUMENT_DEFINITION|INPUT_FIELD_DEFINITION|ENUM_VALUE -""" -Exposes a URL that specifies the behaviour of this scalar. -""" -directive @specifiedBy ("The URL that specifies the behaviour of this scalar." url: String!) on SCALAR +directive @defer (label: String, if: Boolean! = true) on FRAGMENT_SPREAD|INLINE_FRAGMENT + +# See https://github.com/JetBrains/js-graphql-intellij-plugin/issues/665 +# noinspection GraphQLTypeRedefinition +directive @specifiedBy (url: String!) on SCALAR schema { - query: Query - mutation: Mutation + query: RootQuery + mutation: RootMutation } diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt index 418d987b..b230b5fd 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/FirebaseUserRepository.kt @@ -4,13 +4,30 @@ import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.auth.auth import fr.androidmakers.domain.model.User import fr.androidmakers.domain.repo.UserRepository +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +@OptIn(DelicateCoroutinesApi::class) class FirebaseUserRepository : UserRepository { - override fun getUser(): User? { - return try { - Firebase.auth.currentUser?.toUser() - } catch (e: Exception) { - null + override val user = MutableStateFlow(null) + + init { + GlobalScope.launch { + try { + setUser(Firebase.auth.currentUser?.toUser()) + } catch (e: Exception) { + e.printStackTrace() + } } } + + override fun getUser(): User? { + return user.value + } + + override suspend fun setUser(user: User?) { + this.user.emit(user) + } } diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/mappers.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/mappers.kt index 3038ca76..249997f2 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/mappers.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/firebase/mappers.kt @@ -3,6 +3,6 @@ package fr.androidmakers.store.firebase import dev.gitlive.firebase.auth.FirebaseUser import fr.androidmakers.domain.model.User -fun FirebaseUser.toUser(): User { - return User(uid, photoURL) +suspend fun FirebaseUser.toUser(): User { + return User(uid, photoURL, this.getIdToken(false)) } diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt index 5697eed4..e5652417 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt @@ -10,29 +10,36 @@ import com.apollographql.apollo3.cache.normalized.refetchPolicy import com.apollographql.apollo3.cache.normalized.watch import fr.androidmakers.domain.model.Session import fr.androidmakers.domain.repo.SessionsRepository -import fr.androidmakers.store.graphql.type.buildBookmarks +import fr.androidmakers.store.graphql.type.buildBookmarkConnection +import fr.androidmakers.store.graphql.type.buildSession import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -class SessionsGraphQLRepository(private val apolloClient: ApolloClient): SessionsRepository { +class SessionsGraphQLRepository(private val apolloClient: ApolloClient) : SessionsRepository { suspend fun addBookmark(uid: String?, sessionId: String): Boolean { - return modifyBookmarks(uid, AddBookmarkMutation(sessionId)) { sessionIds, id -> + return modifyBookmarks(uid, AddBookmarkMutation(sessionId)) { sessionIds -> AddBookmarkMutation.Data { - addBookmark = buildBookmarks { - this.id = id - this.sessionIds = sessionIds + sessionId + addBookmark = buildBookmarkConnection { + nodes = (sessionIds + sessionId).map { + buildSession { + this.id = it + } + } } } } } suspend fun removeBookmark(uid: String?, sessionId: String): Boolean { - return modifyBookmarks(uid, RemoveBookmarkMutation(sessionId)) { sessionIds, id -> + return modifyBookmarks(uid, RemoveBookmarkMutation(sessionId)) { sessionIds -> RemoveBookmarkMutation.Data { - removeBookmark = buildBookmarks { - this.id = id - this.sessionIds = sessionIds - sessionId + removeBookmark = buildBookmarkConnection { + nodes = (sessionIds - sessionId).map { + buildSession { + this.id = it + } + } } } } @@ -52,55 +59,55 @@ class SessionsGraphQLRepository(private val apolloClient: ApolloClient): Session override fun getSession(id: String): Flow> { return apolloClient.query(GetSessionQuery(id)) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() - .map { - it.dataAssertNoErrors.session.sessionDetails.toSession() - } - .toResultFlow() + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .watch() + .ignoreCacheMisses() + .map { + it.dataAssertNoErrors.session.sessionDetails.toSession() + } + .toResultFlow() } override fun getBookmarks(uid: String): Flow>> { return apolloClient.query(BookmarksQuery()) - .fetchPolicy(FetchPolicy.NetworkOnly) - .refetchPolicy(FetchPolicy.CacheOnly) - .watch().map { - it.data!!.bookmarks!!.sessionIds.toSet() - }.toResultFlow() + .fetchPolicy(FetchPolicy.NetworkOnly) + .refetchPolicy(FetchPolicy.CacheOnly) + .watch().map { + it.data!!.bookmarkConnection!!.nodes.map { it.id }.toSet() + }.toResultFlow() } private suspend fun modifyBookmarks( - uid: String?, - mutation: Mutation, - data: (sessionIds: List, id: String) -> D + uid: String?, + mutation: Mutation, + data: (sessionIds: List) -> D ): Boolean { val optimisticData = try { - val bookmarks = apolloClient.apolloStore.readOperation(BookmarksQuery()).bookmarks - data(bookmarks!!.sessionIds, bookmarks.id) + val bookmarks = apolloClient.apolloStore.readOperation(BookmarksQuery()).bookmarkConnection + data(bookmarks!!.nodes.map { it.id }) } catch (e: Exception) { null } val response = apolloClient.mutation(mutation) - .apply { - if (optimisticData != null) { - optimisticUpdates(optimisticData) - } + .apply { + if (optimisticData != null) { + optimisticUpdates(optimisticData) } - .execute() + } + .execute() return response.data != null } override fun getSessions(): Flow>> { return apolloClient.query(GetSessionsQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() - .map { - it.dataAssertNoErrors.sessions.nodes.map { it.sessionDetails.toSession() } - } - .toResultFlow() + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .watch() + .ignoreCacheMisses() + .map { + it.dataAssertNoErrors.sessions.nodes.map { it.sessionDetails.toSession() } + } + .toResultFlow() } } diff --git a/shared/di/src/androidMain/kotlin/fr/androidmakers/di/DataModule.android.kt b/shared/di/src/androidMain/kotlin/fr/androidmakers/di/DataModule.android.kt index c006bc49..68385543 100644 --- a/shared/di/src/androidMain/kotlin/fr/androidmakers/di/DataModule.android.kt +++ b/shared/di/src/androidMain/kotlin/fr/androidmakers/di/DataModule.android.kt @@ -8,10 +8,13 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module actual val dataPlatformModule = module { - single { ApolloClientBuilder( + single { + ApolloClientBuilder( androidContext(), - "https://androidmakers.fr/graphql", - "androidmakers2024") + "https://androidmakers.fr/graphql", + "androidmakers2024", + get() + ) } single> { diff --git a/shared/domain/build.gradle.kts b/shared/domain/build.gradle.kts index 2559f3af..f7eb592b 100644 --- a/shared/domain/build.gradle.kts +++ b/shared/domain/build.gradle.kts @@ -20,6 +20,7 @@ kotlin { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) api(libs.kotlinx.datetime) + implementation(libs.okio) } } } diff --git a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/User.kt b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/User.kt index 104af69b..8a140844 100644 --- a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/User.kt +++ b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/model/User.kt @@ -1,6 +1,10 @@ package fr.androidmakers.domain.model +import okio.Buffer +import okio.ByteString.Companion.decodeBase64 + data class User( val id: String, - val photoUrl: String? -) + val photoUrl: String?, + val idToken: String? +) \ No newline at end of file diff --git a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt index 92b91e84..26fe22e0 100644 --- a/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt +++ b/shared/domain/src/commonMain/kotlin/fr/androidmakers/domain/repo/UserRepository.kt @@ -1,7 +1,10 @@ package fr.androidmakers.domain.repo import fr.androidmakers.domain.model.User +import kotlinx.coroutines.flow.StateFlow interface UserRepository { + val user: StateFlow fun getUser(): User? + suspend fun setUser(user: User?) } diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/MainLayout.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/MainLayout.kt index a9bebadd..77ac52b3 100644 --- a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/MainLayout.kt +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/MainLayout.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf import com.androidmakers.ui.agenda.SessionDetailScreen import com.androidmakers.ui.agenda.SessionDetailViewModel +import com.androidmakers.ui.common.SigninCallbacks import com.androidmakers.ui.common.navigation.AVALayout import com.androidmakers.ui.common.navigation.MainNavigationRoute import com.androidmakers.ui.speakers.SpeakerDetailsRoute @@ -25,7 +26,7 @@ import org.koin.core.parameter.parametersOf fun MainLayout( versionCode: String, versionName: String, - user: User? + signinCallbacks: SigninCallbacks, ) { val navigator = rememberNavigator() MainNavHost( @@ -33,12 +34,12 @@ fun MainLayout( onSessionClick = { sessionId -> navigator.navigate("${MainNavigationRoute.SESSION_DETAIL.name}/$sessionId") }, - user = user, navigateToSpeakerDetails = { speakerId -> navigator.navigate("${MainNavigationRoute.SPEAKER_DETAIL.name}/$speakerId") }, versionCode = versionCode, versionName = versionName, + signingCallbacks = signinCallbacks, ) } @@ -48,8 +49,8 @@ private fun MainNavHost( versionName: String, mainNavController: Navigator, onSessionClick: (sessionId: String) -> Unit, - user: User?, navigateToSpeakerDetails: (SpeakerId) -> Unit, + signingCallbacks: SigninCallbacks, ) { NavHost( navigator = mainNavController, @@ -61,8 +62,8 @@ private fun MainNavHost( versionCode = versionCode, versionName = versionName, onSessionClick = onSessionClick, - user = user, - navigateToSpeakerDetails = navigateToSpeakerDetails + navigateToSpeakerDetails = navigateToSpeakerDetails, + signinCallbacks = signingCallbacks, ) } diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/SigninButton.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/SigninButton.kt new file mode 100644 index 00000000..74cbe249 --- /dev/null +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/SigninButton.kt @@ -0,0 +1,79 @@ +package com.androidmakers.ui.common + +import androidx.compose.foundation.Image +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AccountCircle +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.seiko.imageloader.rememberImagePainter +import dev.icerock.moko.resources.compose.stringResource +import fr.androidmakers.domain.model.User +import fr.paug.androidmakers.ui.MR + +class SigninCallbacks( + val signin: () -> Unit, + val signout: () -> Unit, +) + +@Composable +fun SigninButton( + user: User?, + callbacks: SigninCallbacks, +) { + val expandedState = remember { mutableStateOf(false) } + + IconButton( + onClick = { + expandedState.value = true + } + ) { + if (user == null) { + Icon( + imageVector = Icons.Rounded.AccountCircle, + contentDescription = stringResource(MR.strings.signin) + ) + } else { + Image( + modifier = Modifier.clip(CircleShape), + painter = rememberImagePainter(user.photoUrl ?: ""), + contentDescription = stringResource(MR.strings.signout) + ) + } + } + + DropdownMenu( + expanded = expandedState.value, + onDismissRequest = { expandedState.value = false } + ) { + if (user == null) { + DropdownMenuItem( + text = { + Text(stringResource(MR.strings.signin)) + }, + onClick = { + expandedState.value = false + callbacks.signin() + } + ) + } else { + DropdownMenuItem( + text = { + Text(stringResource(MR.strings.signout)) + }, + onClick = { + expandedState.value = false + callbacks.signout() + } + ) + } + } +} \ No newline at end of file diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/AVALayout.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/AVALayout.kt index 1403fa8b..3acca39f 100644 --- a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/AVALayout.kt +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/AVALayout.kt @@ -34,6 +34,7 @@ import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,15 +44,17 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.androidmakers.ui.about.AboutScreen import com.androidmakers.ui.agenda.AgendaLayout +import com.androidmakers.ui.common.SigninButton +import com.androidmakers.ui.common.SigninCallbacks import com.androidmakers.ui.speakers.SpeakerListViewModel import com.androidmakers.ui.speakers.SpeakerScreen import com.androidmakers.ui.sponsors.SponsorsScreen import com.androidmakers.ui.venue.VenuePager import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import fr.androidmakers.domain.model.User import fr.paug.androidmakers.ui.MR import kotlinx.coroutines.launch +import moe.tlaster.precompose.flow.collectAsStateWithLifecycle import moe.tlaster.precompose.koin.koinViewModel import moe.tlaster.precompose.navigation.NavHost import moe.tlaster.precompose.navigation.NavOptions @@ -71,15 +74,19 @@ fun AVALayout( versionCode: String, versionName: String, onSessionClick: (sessionId: String) -> Unit, - user: User?, navigateToSpeakerDetails: (String) -> Unit, + signinCallbacks: SigninCallbacks, ) { val avaNavController = rememberNavigator() val navBackStackEntry by avaNavController.currentEntry.collectAsState(null) val currentRoute = navBackStackEntry?.route?.route + val userFlow = remember { UserData().userRepository.user } val agendaFilterDrawerState = rememberDrawerState(DrawerValue.Closed) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + val user by userFlow.collectAsStateWithLifecycle() + Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), contentWindowInsets = WindowInsets(0, 0, 0, 0), @@ -89,7 +96,6 @@ fun AVALayout( colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.background, scrolledContainerColor = MaterialTheme.colorScheme.background, -// navigationIconContentColor =, titleContentColor = MaterialTheme.colorScheme.onBackground, actionIconContentColor = MaterialTheme.colorScheme.onBackground, ), @@ -129,7 +135,7 @@ fun AVALayout( ) } } - //SigninButton(user) + SigninButton(user, signinCallbacks) } ) }, diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt new file mode 100644 index 00000000..024319ff --- /dev/null +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt @@ -0,0 +1,9 @@ +package com.androidmakers.ui.common.navigation + +import fr.androidmakers.domain.repo.UserRepository +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class UserData: KoinComponent { + val userRepository: UserRepository by inject() +} diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt index 3e1b5d5c..5de33d2c 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/main/MainViewModel.kt @@ -14,7 +14,6 @@ import fr.androidmakers.domain.interactor.SyncBookmarksUseCase import fr.androidmakers.domain.model.Agenda import fr.androidmakers.domain.model.User import fr.androidmakers.domain.repo.UserRepository -import fr.paug.androidmakers.wear.R import fr.paug.androidmakers.wear.applicationContext import fr.paug.androidmakers.wear.data.LocalPreferencesRepository import fr.paug.androidmakers.wear.ui.session.UISession @@ -100,8 +99,8 @@ class MainViewModel( } fun onSignInSuccess() { - _user.value = userRepository.getUser() viewModelScope.launch { + _user.value = userRepository.getUser() maybeSyncBookmarks() } } @@ -110,7 +109,7 @@ class MainViewModel( val googleSignInClient = GoogleSignIn.getClient( applicationContext, GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestIdToken(applicationContext.getString(R.string.default_web_client_id)) + .requestIdToken("985196411897-r7edbi9jgo3hfupekcmdrg66inonj0o5.apps.googleusercontent.com") .build() ) googleSignInClient.signOut() diff --git a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt index f8d9de2c..e110a3e9 100644 --- a/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt +++ b/wearApp/src/main/java/fr/paug/androidmakers/wear/ui/signin/GoogleSignInViewModel.kt @@ -8,11 +8,16 @@ import com.google.android.horologist.auth.data.googlesignin.GoogleSignInEventLis import com.google.android.horologist.auth.ui.googlesignin.signin.GoogleSignInViewModel import com.google.firebase.Firebase import com.google.firebase.auth.FirebaseAuthException +import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.GoogleAuthProvider import com.google.firebase.auth.auth +import fr.androidmakers.domain.model.User +import fr.androidmakers.domain.repo.UserRepository import fr.paug.androidmakers.wear.R import fr.paug.androidmakers.wear.applicationContext import kotlinx.coroutines.tasks.await +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject private val TAG = GoogleSignInViewModel::class.java.simpleName @@ -33,11 +38,25 @@ class GoogleSignInViewModel( try { val credential = GoogleAuthProvider.getCredential(idToken!!, null) Firebase.auth.signInWithCredential(credential).await() + UserData().userRepository.setUser(Firebase.auth.currentUser?.toUser()) onSignInSuccess() } catch (e: FirebaseAuthException) { Log.w(TAG, "Could not get Firebase auth credential from Google id token", e) + UserData().userRepository.setUser(null) onSignInFailed() } } } ) + +private suspend fun FirebaseUser.toUser(): User { + return User( + id = this.uid, + photoUrl = this.photoUrl.toString(), + idToken = this.getIdToken(true).await().token + ) +} + +class UserData: KoinComponent { + val userRepository: UserRepository by inject() +}