From a6b8fb3d81c9fd01c3fbaa9603929010a1332e05 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 27 Mar 2024 11:20:03 -0400 Subject: [PATCH 01/12] Adding Authentication to iOS --- .../Droidcon.xcodeproj/project.pbxproj | 6 +- ios/Droidcon/Droidcon/AppDelegate.swift | 30 ++++++++++ .../Droidcon/Settings/SettingsView.swift | 32 +++++++++- .../Droidcon/Utils/FirebaseService.swift | 60 +++++++++++++++++++ .../Droidcon/en.lproj/Localizable.strings | 2 + ios/Droidcon/Podfile | 2 + ios/Droidcon/Podfile.lock | 43 ++++++++++++- 7 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 ios/Droidcon/Droidcon/Utils/FirebaseService.swift diff --git a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj index 84cb4b44..e866e055 100644 --- a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj +++ b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -49,6 +49,7 @@ A35DC2E328AB6C6F00C7B298 /* ComposeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35DC2E228AB6C6F00C7B298 /* ComposeController.swift */; }; A35DEF2228AA265C0072605A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = A35DEF2128AA265C0072605A /* Settings.bundle */; }; A35DEF2428AA26C80072605A /* SettingsBundleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35DEF2328AA26C80072605A /* SettingsBundleHelper.swift */; }; + F127D3592BB46A0A00E08281 /* FirebaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F127D3582BB46A0A00E08281 /* FirebaseService.swift */; }; F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */; }; F1465F0A23AA94BF0055F7C3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F1465F0923AA94BF0055F7C3 /* Assets.xcassets */; }; /* End PBXBuildFile section */ @@ -97,6 +98,7 @@ A35DEF2328AA26C80072605A /* SettingsBundleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBundleHelper.swift; sourceTree = ""; }; DD90C0C0A4D331CEBDBE69E8 /* Pods-Droidcon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Droidcon.release.xcconfig"; path = "Target Support Files/Pods-Droidcon/Pods-Droidcon.release.xcconfig"; sourceTree = ""; }; EBA278C64D82609BA00FE2A5 /* Pods_Droidcon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Droidcon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F127D3582BB46A0A00E08281 /* FirebaseService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseService.swift; sourceTree = ""; }; F1465EFD23AA94BF0055F7C3 /* Droidcon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Droidcon.app; sourceTree = BUILT_PRODUCTS_DIR; }; F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F1465F0923AA94BF0055F7C3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -166,6 +168,7 @@ 68DCBC6326C51E260084C70D /* TextView.swift */, 681C95A026C555D90011330B /* VisualEffectView.swift */, 681C95A226C56B100011330B /* CustomOverlayView.swift */, + F127D3582BB46A0A00E08281 /* FirebaseService.swift */, ); path = Utils; sourceTree = ""; @@ -447,6 +450,7 @@ 684FAA7726B2A4EA00673AFF /* SettingsView.swift in Sources */, 689DD2FB26B40F1800A9B009 /* LazyView.swift in Sources */, 689DD30526B438CA00A9B009 /* Avatar.swift in Sources */, + F127D3592BB46A0A00E08281 /* FirebaseService.swift in Sources */, 681C95A126C555D90011330B /* VisualEffectView.swift in Sources */, 1833221026B0CF5600D79482 /* DroidconApp.swift in Sources */, 684FAA7426B2A4D400673AFF /* ScheduleView.swift in Sources */, diff --git a/ios/Droidcon/Droidcon/AppDelegate.swift b/ios/Droidcon/Droidcon/AppDelegate.swift index fcffdb5b..f0578a7e 100644 --- a/ios/Droidcon/Droidcon/AppDelegate.swift +++ b/ios/Droidcon/Droidcon/AppDelegate.swift @@ -1,4 +1,6 @@ import UIKit +import FirebaseAuth +import GoogleSignIn import Firebase import DroidconKit @@ -8,6 +10,8 @@ class AppDelegate: NSObject, UIApplicationDelegate { lazy var analytics = koin.get(objCProtocol: AnalyticsService.self, qualifier: nil) as! AnalyticsService lazy var appChecker = koin.get(objCClass: AppChecker.self) as! AppChecker + var firebaseAuthListener:AuthStateDidChangeListenerHandle? + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -21,7 +25,33 @@ class AppDelegate: NSObject, UIApplicationDelegate { analytics.logEvent(name: AnalyticsServiceCompanion().EVENT_STARTED, params: [:]) + + firebaseAuthListener = Auth.auth().addStateDidChangeListener() { auth, user in + // TODO + if let user { + _ = UserContext( + isAuthenticated: false, + userData: UserData(id: user.uid, name: user.displayName, email: user.email, pictureUrl: user.photoURL?.absoluteString) + ) + } else { + _ = UserContext(isAuthenticated: false, userData: nil) + } + } + log.v(message: { "App Started" }) return true } + + func application(_ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + return GIDSignIn.sharedInstance.handle(url) + } + + func applicationWillTerminate(_ application: UIApplication) { + if let firebaseAuthListener { + Auth.auth().removeStateDidChangeListener(firebaseAuthListener) + } + } + } diff --git a/ios/Droidcon/Droidcon/Settings/SettingsView.swift b/ios/Droidcon/Droidcon/Settings/SettingsView.swift index c3eb06f8..f5519082 100644 --- a/ios/Droidcon/Droidcon/Settings/SettingsView.swift +++ b/ios/Droidcon/Droidcon/Settings/SettingsView.swift @@ -1,10 +1,15 @@ import SwiftUI +import FirebaseAuth import DroidconKit struct SettingsView: View { @ObservedObject private(set) var viewModel: SettingsViewModel - + private let firebaseService = FirebaseService() + + @State var errorMessage: String = "" + @State var showingAlert: Bool = false + var body: some View { NavigationView { ZStack { @@ -34,12 +39,37 @@ struct SettingsView: View { Divider().padding(.horizontal) + if Auth.auth().currentUser != nil { + Button("Settings.SignOut"){ + if let error = firebaseService.signOut() { + errorMessage = error + showingAlert = true + } + } + } else { + Button("Settings.SignIn"){ + firebaseService.signIn(onError: { error in + errorMessage = error + showingAlert = true + }) + } + } + AboutView(viewModel: viewModel.about) } } } .navigationTitle("Settings.Title") .navigationBarTitleDisplayMode(.inline) + .alert(isPresented: $showingAlert, content: { + Alert( + title: Text("Error Occurred"), + message: Text(errorMessage), + dismissButton: .default(Text("Got it!")){ + showingAlert = false + } + ) + }) } .navigationViewStyle(StackNavigationViewStyle()) } diff --git a/ios/Droidcon/Droidcon/Utils/FirebaseService.swift b/ios/Droidcon/Droidcon/Utils/FirebaseService.swift new file mode 100644 index 00000000..00579185 --- /dev/null +++ b/ios/Droidcon/Droidcon/Utils/FirebaseService.swift @@ -0,0 +1,60 @@ +// +// FirebaseService.swift +// Droidcon +// +// Created by Kevin Schildhorn on 3/27/24. +// Copyright © 2024 Touchlab. All rights reserved. +// + +import Foundation +import FirebaseCore +import GoogleSignIn +import FirebaseAuth + +class FirebaseService { + + func signIn(onError: @escaping (String) -> Void) { + guard let clientID = FirebaseApp.app()?.options.clientID else { + onError("No ClientId") + return + } + + let config = GIDConfiguration(clientID: clientID) + GIDSignIn.sharedInstance.configuration = config + guard let presentingViewController = UIApplication.shared.windows.first?.rootViewController + else { + onError("No Presenting Controller") + return + } + + GIDSignIn.sharedInstance.signIn(withPresenting: presentingViewController) { result, error in + guard error == nil else { + onError(error?.localizedDescription ?? "") + return + } + + guard let user = result?.user, + let idToken = user.idToken?.tokenString + else { + onError("No User Found") + return + } + + let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: user.accessToken.tokenString) + Auth.auth().signIn(with: credential) { result, error in + if let error { + onError(error.localizedDescription) + } + } + } + } + + func signOut() -> String? { + do { + try Auth.auth().signOut() + return nil + } catch let signOutError as NSError { + return signOutError.localizedDescription + } + } +} diff --git a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings index 6790e796..6b3347f6 100644 --- a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings +++ b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings @@ -37,6 +37,8 @@ "Settings.Reminders" = "Enable reminders"; "Settings.Compose" = "Use compose for iOS"; "Settings.About" = "About"; +"Settings.SignIn" = "Sign In"; +"Settings.SignOut" = "Sign Out"; "About.Title" = "About"; diff --git a/ios/Droidcon/Podfile b/ios/Droidcon/Podfile index 08d08586..ec36eeb2 100644 --- a/ios/Droidcon/Podfile +++ b/ios/Droidcon/Podfile @@ -8,4 +8,6 @@ target 'Droidcon' do pod 'Kingfisher', '~> 7.8.1' pod 'Firebase/Analytics' pod 'Firebase/Crashlytics' + pod 'GoogleSignIn' + pod 'FirebaseAuth' end diff --git a/ios/Droidcon/Podfile.lock b/ios/Droidcon/Podfile.lock index 7f8b0e27..738efee7 100644 --- a/ios/Droidcon/Podfile.lock +++ b/ios/Droidcon/Podfile.lock @@ -1,4 +1,10 @@ PODS: + - AppAuth (1.7.3): + - AppAuth/Core (= 1.7.3) + - AppAuth/ExternalUserAgent (= 1.7.3) + - AppAuth/Core (1.7.3) + - AppAuth/ExternalUserAgent (1.7.3): + - AppAuth/Core - Firebase/Analytics (10.15.0): - Firebase/Core - Firebase/Core (10.15.0): @@ -27,6 +33,14 @@ PODS: - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseAppCheckInterop (10.23.0) + - FirebaseAuth (10.23.0): + - FirebaseAppCheckInterop (~> 10.17) + - FirebaseCore (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.8) + - GoogleUtilities/Environment (~> 7.8) + - GTMSessionFetcher/Core (< 4.0, >= 2.1) + - RecaptchaInterop (~> 100.0) - FirebaseCore (10.15.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) @@ -80,6 +94,10 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) + - GoogleSignIn (7.1.0): + - AppAuth (< 2.0, >= 1.7.3) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) - GoogleUtilities/AppDelegateSwizzler (7.11.5): - GoogleUtilities/Environment - GoogleUtilities/Logger @@ -99,6 +117,10 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/UserDefaults (7.11.5): - GoogleUtilities/Logger + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher/Core (3.3.2) - Kingfisher (7.8.1) - nanopb (2.30909.0): - nanopb/decode (= 2.30909.0) @@ -108,16 +130,22 @@ PODS: - PromisesObjC (2.3.1) - PromisesSwift (2.3.1): - PromisesObjC (= 2.3.1) + - RecaptchaInterop (100.0.0) DEPENDENCIES: - Firebase/Analytics - Firebase/Crashlytics + - FirebaseAuth + - GoogleSignIn - Kingfisher (~> 7.8.1) SPEC REPOS: trunk: + - AppAuth - Firebase - FirebaseAnalytics + - FirebaseAppCheckInterop + - FirebaseAuth - FirebaseCore - FirebaseCoreExtension - FirebaseCoreInternal @@ -126,15 +154,22 @@ SPEC REPOS: - FirebaseSessions - GoogleAppMeasurement - GoogleDataTransport + - GoogleSignIn - GoogleUtilities + - GTMAppAuth + - GTMSessionFetcher - Kingfisher - nanopb - PromisesObjC - PromisesSwift + - RecaptchaInterop SPEC CHECKSUMS: + AppAuth: a13994980c1ec792f7e2e665acd4d4aa6be43240 Firebase: 66043bd4579e5b73811f96829c694c7af8d67435 FirebaseAnalytics: 47cef43728f81a839cf1306576bdd77ffa2eac7e + FirebaseAppCheckInterop: a1955ce8c30f38f87e7d091630e871e91154d65d + FirebaseAuth: 22eb85d3853141de7062bfabc131aa7d6335cade FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e FirebaseCoreExtension: c08d14c7b22e07994e876d837e6f58642f340087 FirebaseCoreInternal: b444828ea7cfd594fca83046b95db98a2be4f290 @@ -143,12 +178,16 @@ SPEC CHECKSUMS: FirebaseSessions: e5f4caa188dc8bc6142abc974355be75b042215e GoogleAppMeasurement: 722db6550d1e6d552b08398b69a975ac61039338 GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 + GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 0e876eea9782ec6462e91ab872711c357322c94f Kingfisher: 63f677311d36a3473f6b978584f8a3845d023dc5 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 + RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 -PODFILE CHECKSUM: 4654f3ccb4ae86c4f9440935f0df48a0ebdaaeb8 +PODFILE CHECKSUM: 2e35e57413390790899d1dead666725334a49b5b -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 From c6d4132e812a9a88d951e17a1a84208d6c9cd21a Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 27 Mar 2024 15:35:23 -0400 Subject: [PATCH 02/12] Getting initial Android Version Working --- android/build.gradle.kts | 11 ++- .../droidcon/android/FirebaseService.kt | 93 +++++++++++++++++++ .../touchlab/droidcon/android/MainActivity.kt | 62 +++++++++++-- .../co/touchlab/droidcon/android/MainApp.kt | 9 +- gradle/libs.versions.toml | 4 + .../co/touchlab/droidcon/ui/util/MainView.kt | 13 ++- .../ui/BottomNavigationView.kt | 13 ++- .../ui/MainComposeView.kt | 9 +- .../ui/settings/SettingsView.kt | 23 ++++- .../droidcon/ui/ComposeRootController.kt | 11 ++- .../co/touchlab/droidcon/UserContext.kt | 13 +++ .../droidcon/domain/service/UserIdProvider.kt | 4 + .../service/impl/DefaultUserIdProvider.kt | 5 + 13 files changed, 249 insertions(+), 21 deletions(-) create mode 100644 android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/UserContext.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 1d7011af..5899479e 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -21,12 +21,15 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - applicationId = "co.touchlab.droidcon.london" + applicationId = "co.touchlab.droidconauthtest" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() versionCode = 20201 versionName = "2.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + val clientId = properties.getProperty("clientId", "") + buildConfigField("String", "CLIENT_ID", clientId) } packaging { resources.excludes.add("META-INF/*.kotlin_module") @@ -34,6 +37,7 @@ android { if (releaseEnabled) { signingConfigs { create("release") { + keyAlias = "key0" keyPassword = releasePassword storeFile = file("./release.jks") @@ -65,10 +69,12 @@ android { buildFeatures { compose = true + buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } + } kotlin { @@ -89,6 +95,9 @@ dependencies { implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) + implementation(libs.firebase.auth) + implementation(libs.playservices.auth) + implementation(libs.hyperdrive.multiplatformx.api) implementation(libs.bundles.androidx.compose) diff --git a/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt b/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt new file mode 100644 index 00000000..6c66d257 --- /dev/null +++ b/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt @@ -0,0 +1,93 @@ +package co.touchlab.droidcon.android + +import android.app.Activity +import android.content.IntentSender +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import co.touchlab.droidcon.BuildConfig +import co.touchlab.droidcon.UserContext +import co.touchlab.droidcon.UserData +import co.touchlab.droidcon.domain.service.UserIdProvider +import co.touchlab.kermit.Logger +import com.google.android.gms.auth.api.identity.BeginSignInRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.tasks.Task +import com.google.firebase.auth.AuthResult +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import com.google.firebase.auth.ktx.auth +import com.google.firebase.ktx.Firebase +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class FirebaseService : KoinComponent { + + private val auth: FirebaseAuth = Firebase.auth + private val logger = Logger.withTag("Authentication") + private val userIdProvider: UserIdProvider by inject() + private val clientId = BuildConfig.CLIENT_ID + + fun performGoogleLogin( + activity: Activity, + resultLauncher: ActivityResultLauncher + ) { + logger.i { "Performing Google Login" } + val oneTapClient = Identity.getSignInClient(activity) + val signInRequest = BeginSignInRequest.builder() + .setGoogleIdTokenRequestOptions( + BeginSignInRequest.GoogleIdTokenRequestOptions.builder() + .setSupported(true) + .setServerClientId(clientId) + .setFilterByAuthorizedAccounts(false) + .build() + ) + .build() + logger.v { "Beginning Sign In" } + oneTapClient.beginSignIn(signInRequest) + .addOnSuccessListener(activity) { result -> + logger.v { "Success! Starting Intent Sender" } + try { + val request = IntentSenderRequest + .Builder(result.pendingIntent.intentSender) + .build() + resultLauncher.launch(request) + } catch (e: IntentSender.SendIntentException) { + logger.e(e) { "Couldn't Start Intent" } + } + } + .addOnFailureListener(activity) { e -> + logger.e(e) { "Failed to Sign in" } + } + } + + fun handleResultTask(task: Task) { + if (task.isSuccessful) { + logger.d { "signInWithCredential:success" } + auth.currentUser?.let { user -> + saveCredentials(user) + } + } else { + logger.e(task.exception) { "signInWithCredential:failure" } + } + } + + fun performLogout() { + Firebase.auth.signOut() + } + + fun saveCredentials(firebaseUser: FirebaseUser?) { + userIdProvider.saveUserContext( + UserContext( + isAuthenticated = firebaseUser != null, + userData = firebaseUser?.let { + UserData( + id = it.uid, + name = it.displayName, + email = it.email, + pictureUrl = it.photoUrl?.toString() + ) + } + ) + ) + } +} \ No newline at end of file diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt index 68fd1ad6..146d14e4 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt @@ -4,6 +4,9 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -32,6 +35,12 @@ import co.touchlab.droidcon.util.AppChecker import co.touchlab.droidcon.util.NavigationController import co.touchlab.droidcon.viewmodel.ApplicationViewModel import co.touchlab.kermit.Logger +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.common.api.ApiException +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.ktx.auth +import com.google.firebase.ktx.Firebase import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import org.brightify.hyperdrive.multiplatformx.LifecycleGraph @@ -39,20 +48,45 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject class MainActivity : ComponentActivity(), KoinComponent { - + private val notificationSchedulingService: NotificationSchedulingService by inject() private val syncService: SyncService by inject() private val analyticsService: AnalyticsService by inject() private val applicationViewModel: ApplicationViewModel by inject() - private val root = LifecycleGraph.Root(this) + private val firebaseService: FirebaseService by inject() + + // Firebase Auth + private lateinit var auth: FirebaseAuth + + private val isAuthenticated: Boolean + get() = auth.currentUser != null + + private val firebaseAuthListener = FirebaseAuth.AuthStateListener { firebaseAuth -> + firebaseService.saveCredentials(firebaseAuth.currentUser) + } + + private val firebaseIntentResultLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> + val logger = Logger.withTag("Authentication") + try { + val oneTapClient = Identity.getSignInClient(baseContext) + val credential = oneTapClient.getSignInCredentialFromIntent(result.data) + val firebaseCredential = + GoogleAuthProvider.getCredential(credential.googleIdToken, null) + auth.signInWithCredential(firebaseCredential) + .addOnCompleteListener(this) { firebaseService.handleResultTask(it) } + } catch (e: ApiException) { + logger.e(e) { "NO ID Token" } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + auth = Firebase.auth + auth.addAuthStateListener(firebaseAuthListener) installSplashScreen() - AppChecker.checkTimeZoneHash() analyticsService.logEvent(AnalyticsService.EVENT_STARTED) @@ -69,7 +103,14 @@ class MainActivity : ComponentActivity(), KoinComponent { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { - MainView(viewModel = applicationViewModel) + MainView(viewModel = applicationViewModel, isAuthenticated = isAuthenticated) { + if (isAuthenticated) firebaseService.performLogout() + else + firebaseService.performGoogleLogin( + this, + firebaseIntentResultLauncher, + ) + } val showSplashScreen by applicationViewModel.showSplashScreen.collectAsState() Crossfade(targetState = showSplashScreen) { shouldShowSplashScreen -> @@ -111,8 +152,12 @@ class MainActivity : ComponentActivity(), KoinComponent { } private fun handleNotificationDeeplink(intent: Intent) { - val type = intent.getStringExtra(AndroidNotificationService.NOTIFICATION_TYPE_EXTRA_KEY) ?: return - val sessionId = intent.getStringExtra(AndroidNotificationService.NOTIFICATION_SESSION_ID_EXTRA_KEY) ?: return + val type = + intent.getStringExtra(AndroidNotificationService.NOTIFICATION_TYPE_EXTRA_KEY) + ?: return + val sessionId = + intent.getStringExtra(AndroidNotificationService.NOTIFICATION_SESSION_ID_EXTRA_KEY) + ?: return applicationViewModel.notificationReceived( sessionId, when (type) { @@ -133,6 +178,7 @@ class MainActivity : ComponentActivity(), KoinComponent { override fun onDestroy() { super.onDestroy() + auth.removeAuthStateListener(firebaseAuthListener) // Workaround for a crash we could not reproduce: https://console.firebase.google.com/project/droidcon-148cc/crashlytics/app/android:co.touchlab.droidcon.london/issues/8c559569e69164d7109bd6b1be99ade5 if (root.hasChild(applicationViewModel.lifecycle)) { root.removeChild(applicationViewModel.lifecycle) @@ -144,4 +190,4 @@ class MainActivity : ComponentActivity(), KoinComponent { super.onBackPressed() } } -} +} \ No newline at end of file diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index b96e87dc..e256f606 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -31,7 +31,10 @@ class MainApp : Application() { single { this@MainApp } single> { MainActivity::class.java } single { - get().getSharedPreferences("DROIDCON_SETTINGS_2023", Context.MODE_PRIVATE) + get().getSharedPreferences( + "DROIDCON_SETTINGS_2023", + Context.MODE_PRIVATE + ) } single { SharedPreferencesSettings(delegate = get()) } @@ -50,6 +53,10 @@ class MainApp : Application() { single { AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) } + + single { + FirebaseService() + } } + uiModule ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ddd380e1..711aa3bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,6 +57,10 @@ accompanist-navigationAnimation = { module = "com.google.accompanist:accompanist firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx", version = "_" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx", version = "_" } +firebase-auth = { module = "com.google.firebase:firebase-auth", version = "_" } +playservices-auth = { module = "com.google.android.gms:play-services-auth", version = "21.0.0" } + + hyperdrive-multiplatformx-api = { module = "org.brightify.hyperdrive:multiplatformx-api", version.ref = "hyperdrive" } android-desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugaring" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } diff --git a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt b/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt index 43609b07..fd834892 100644 --- a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt +++ b/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt @@ -7,6 +7,15 @@ import co.touchlab.droidcon.ui.MainComposeView import co.touchlab.droidcon.viewmodel.ApplicationViewModel @Composable -fun MainView(viewModel: ApplicationViewModel) { - MainComposeView(viewModel = viewModel, modifier = Modifier.systemBarsPadding()) +fun MainView( + viewModel: ApplicationViewModel, + isAuthenticated: Boolean, + onAuthRequest: () -> Unit, +) { + MainComposeView( + viewModel = viewModel, + isAuthenticated, + onAuthRequest, + modifier = Modifier.systemBarsPadding(), + ) } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt index 53cf5696..d66e15a0 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt @@ -24,7 +24,12 @@ import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.viewmodel.ApplicationViewModel @Composable -internal fun BottomNavigationView(viewModel: ApplicationViewModel, modifier: Modifier = Modifier) { +internal fun BottomNavigationView( + viewModel: ApplicationViewModel, + modifier: Modifier = Modifier, + isAuthenticated: Boolean, + onAuthRequest: () -> Unit, +) { val selectedTab by viewModel.observeSelectedTab.observeAsState() Scaffold( @@ -60,7 +65,11 @@ internal fun BottomNavigationView(viewModel: ApplicationViewModel, modifier: Mod ApplicationViewModel.Tab.Schedule -> SessionListView(viewModel.schedule) ApplicationViewModel.Tab.MyAgenda -> SessionListView(viewModel.agenda) ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) - ApplicationViewModel.Tab.Settings -> SettingsView(viewModel.settings) + ApplicationViewModel.Tab.Settings -> SettingsView( + viewModel.settings, + isAuthenticated, + onAuthRequest + ) } } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt index b6247827..c2725935 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt @@ -10,11 +10,16 @@ import coil3.compose.setSingletonImageLoaderFactory @OptIn(ExperimentalCoilApi::class) @Composable -internal fun MainComposeView(viewModel: ApplicationViewModel, modifier: Modifier = Modifier) { +internal fun MainComposeView( + viewModel: ApplicationViewModel, + isAuthenticated: Boolean, + onAuthRequest: () -> Unit, + modifier: Modifier = Modifier, +) { setSingletonImageLoaderFactory { context -> dcImageLoader(context, true) } DroidconTheme { - BottomNavigationView(viewModel = viewModel, modifier = modifier) + BottomNavigationView(viewModel = viewModel, modifier = modifier, isAuthenticated, onAuthRequest) } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt index b95b70f2..9f0be668 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MailOutline import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.Button import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -34,7 +35,11 @@ import org.brightify.hyperdrive.multiplatformx.property.MutableObservablePropert @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun SettingsView(viewModel: SettingsViewModel) { +internal fun SettingsView( + viewModel: SettingsViewModel, + isAuthenticated: Boolean, + onAuthRequest: () -> Unit +) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -68,6 +73,16 @@ internal fun SettingsView(viewModel: SettingsViewModel) { Divider() + Button(onClick = onAuthRequest) { + if (isAuthenticated) { + Text("Sign Out") + } else { + Text("Sign In") + } + } + + Divider() + PlatformSpecificSettingsView(viewModel = viewModel) AboutView(viewModel.about) @@ -76,7 +91,11 @@ internal fun SettingsView(viewModel: SettingsViewModel) { } @Composable -internal fun IconTextSwitchRow(text: String, image: ImageVector, checked: MutableObservableProperty) { +internal fun IconTextSwitchRow( + text: String, + image: ImageVector, + checked: MutableObservableProperty +) { val isChecked by checked.observeAsState() Row( modifier = Modifier diff --git a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt b/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt index ea984965..e98ff29b 100644 --- a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt +++ b/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt @@ -3,6 +3,11 @@ package co.touchlab.droidcon.ui import androidx.compose.ui.window.ComposeUIViewController import co.touchlab.droidcon.viewmodel.ApplicationViewModel -fun getRootController(viewModel: ApplicationViewModel) = ComposeUIViewController { - MainComposeView(viewModel) -} +fun getRootController( + viewModel: ApplicationViewModel, + isAuthenticated: Boolean, + onAuthRequest: () -> Unit, +) = + ComposeUIViewController { + MainComposeView(viewModel, isAuthenticated, onAuthRequest) + } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/UserContext.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/UserContext.kt new file mode 100644 index 00000000..61c81399 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/UserContext.kt @@ -0,0 +1,13 @@ +package co.touchlab.droidcon + +data class UserContext( + val isAuthenticated: Boolean, + val userData: UserData?, +) + +data class UserData( + val id: String, + val name: String?, + val email: String?, + val pictureUrl: String?, +) diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/UserIdProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/UserIdProvider.kt index 25f2b5d4..3ec21b23 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/UserIdProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/UserIdProvider.kt @@ -1,5 +1,9 @@ package co.touchlab.droidcon.domain.service +import co.touchlab.droidcon.UserContext + interface UserIdProvider { suspend fun getId(): String + + fun saveUserContext(userContext: UserContext) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultUserIdProvider.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultUserIdProvider.kt index 53959911..e699a25e 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultUserIdProvider.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultUserIdProvider.kt @@ -1,5 +1,6 @@ package co.touchlab.droidcon.domain.service.impl +import co.touchlab.droidcon.UserContext import co.touchlab.droidcon.domain.service.UserIdProvider import com.benasher44.uuid.uuid4 import com.russhwolf.settings.ExperimentalSettingsApi @@ -23,4 +24,8 @@ class DefaultUserIdProvider( } return id } + + override fun saveUserContext(userContext: UserContext) { + // TODO + } } From e32c0a896618c6e508081c57c91589a77ce0b91e Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 Mar 2024 11:32:34 -0400 Subject: [PATCH 03/12] Finalizing code --- android/build.gradle.kts | 1 - .../droidcon/android/FirebaseService.kt | 112 +++++++----------- .../touchlab/droidcon/android/MainActivity.kt | 43 +++++-- .../co/touchlab/droidcon/android/MainApp.kt | 4 +- .../Droidcon.xcodeproj/project.pbxproj | 6 +- ios/Droidcon/Droidcon/FirebaseService.swift | 83 +++++++++++++ ios/Droidcon/Droidcon/Koin.swift | 6 +- .../Droidcon/Settings/SettingsView.swift | 19 +-- .../Droidcon/Utils/FirebaseService.swift | 60 ---------- .../droidcon/ios/DependencyInjection.kt | 5 +- .../co/touchlab/droidcon/ui/util/MainView.kt | 4 - .../ui/BottomNavigationView.kt | 4 - .../ui/MainComposeView.kt | 4 +- .../co.touchlab.droidcon/ui/UiModule.kt | 31 ++++- .../ui/settings/SettingsView.kt | 9 +- .../viewmodel/settings/SettingsViewModel.kt | 15 ++- .../droidcon/ui/ComposeRootController.kt | 4 +- .../domain/service/AuthenticationService.kt | 47 ++++++++ 18 files changed, 281 insertions(+), 176 deletions(-) create mode 100644 ios/Droidcon/Droidcon/FirebaseService.swift delete mode 100644 ios/Droidcon/Droidcon/Utils/FirebaseService.swift create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 5899479e..5e06de25 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -74,7 +74,6 @@ android { composeOptions { kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } - } kotlin { diff --git a/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt b/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt index 6c66d257..31620fea 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt @@ -5,89 +5,67 @@ import android.content.IntentSender import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import co.touchlab.droidcon.BuildConfig -import co.touchlab.droidcon.UserContext -import co.touchlab.droidcon.UserData -import co.touchlab.droidcon.domain.service.UserIdProvider +import co.touchlab.droidcon.domain.service.AuthenticationService import co.touchlab.kermit.Logger import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.Identity -import com.google.android.gms.tasks.Task -import com.google.firebase.auth.AuthResult import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseUser import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import org.brightify.hyperdrive.utils.WeakReference -class FirebaseService : KoinComponent { +class FirebaseService : AuthenticationService(FirebaseAuth.getInstance().currentUser != null) { - private val auth: FirebaseAuth = Firebase.auth private val logger = Logger.withTag("Authentication") - private val userIdProvider: UserIdProvider by inject() private val clientId = BuildConfig.CLIENT_ID - - fun performGoogleLogin( + + private lateinit var weakActivity: WeakReference + private lateinit var weakLauncher: WeakReference> + + fun setActivity( activity: Activity, - resultLauncher: ActivityResultLauncher + launcher: ActivityResultLauncher, ) { - logger.i { "Performing Google Login" } - val oneTapClient = Identity.getSignInClient(activity) - val signInRequest = BeginSignInRequest.builder() - .setGoogleIdTokenRequestOptions( - BeginSignInRequest.GoogleIdTokenRequestOptions.builder() - .setSupported(true) - .setServerClientId(clientId) - .setFilterByAuthorizedAccounts(false) - .build() - ) - .build() - logger.v { "Beginning Sign In" } - oneTapClient.beginSignIn(signInRequest) - .addOnSuccessListener(activity) { result -> - logger.v { "Success! Starting Intent Sender" } - try { - val request = IntentSenderRequest - .Builder(result.pendingIntent.intentSender) - .build() - resultLauncher.launch(request) - } catch (e: IntentSender.SendIntentException) { - logger.e(e) { "Couldn't Start Intent" } - } - } - .addOnFailureListener(activity) { e -> - logger.e(e) { "Failed to Sign in" } - } + weakActivity = WeakReference(activity) + weakLauncher = WeakReference(launcher) } - fun handleResultTask(task: Task) { - if (task.isSuccessful) { - logger.d { "signInWithCredential:success" } - auth.currentUser?.let { user -> - saveCredentials(user) - } - } else { - logger.e(task.exception) { "signInWithCredential:failure" } + override fun performGoogleLogin(): Boolean { + weakActivity.get()?.let { activity -> + logger.i { "Performing Google Login" } + val oneTapClient = Identity.getSignInClient(activity) + val signInRequest = BeginSignInRequest.builder() + .setGoogleIdTokenRequestOptions( + BeginSignInRequest.GoogleIdTokenRequestOptions.builder() + .setSupported(true) + .setServerClientId(clientId) + .setFilterByAuthorizedAccounts(false) + .build() + ) + .build() + logger.v { "Beginning Sign In" } + oneTapClient.beginSignIn(signInRequest) + .addOnSuccessListener(activity) { result -> + logger.v { "Success! Starting Intent Sender" } + try { + val request = IntentSenderRequest + .Builder(result.pendingIntent.intentSender) + .build() + weakLauncher.get()?.launch(request) + } catch (e: IntentSender.SendIntentException) { + logger.e(e) { "Couldn't Start Intent" } + } + } + .addOnFailureListener(activity) { e -> + logger.e(e) { "Failed to Sign in" } + } + return true } + return false } - fun performLogout() { + override fun performLogout(): Boolean { Firebase.auth.signOut() + return super.performLogout() } - - fun saveCredentials(firebaseUser: FirebaseUser?) { - userIdProvider.saveUserContext( - UserContext( - isAuthenticated = firebaseUser != null, - userData = firebaseUser?.let { - UserData( - id = it.uid, - name = it.displayName, - email = it.email, - pictureUrl = it.photoUrl?.toString() - ) - } - ) - ) - } -} \ No newline at end of file +} diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt index 146d14e4..9e40ecea 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt @@ -27,6 +27,7 @@ import co.touchlab.droidcon.R import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.application.service.NotificationService import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.AuthenticationService import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.service.AndroidNotificationService import co.touchlab.droidcon.ui.theme.Colors @@ -48,14 +49,14 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject class MainActivity : ComponentActivity(), KoinComponent { - + private val notificationSchedulingService: NotificationSchedulingService by inject() private val syncService: SyncService by inject() private val analyticsService: AnalyticsService by inject() private val applicationViewModel: ApplicationViewModel by inject() private val root = LifecycleGraph.Root(this) - private val firebaseService: FirebaseService by inject() + private val firebaseService: AuthenticationService by inject() // Firebase Auth private lateinit var auth: FirebaseAuth @@ -64,7 +65,15 @@ class MainActivity : ComponentActivity(), KoinComponent { get() = auth.currentUser != null private val firebaseAuthListener = FirebaseAuth.AuthStateListener { firebaseAuth -> - firebaseService.saveCredentials(firebaseAuth.currentUser) + with(firebaseAuth) { + firebaseService.updateCredentials( + isAuthenticated = currentUser != null, + id = currentUser?.uid ?: "", + name = currentUser?.displayName, + email = currentUser?.email, + pictureUrl = currentUser?.photoUrl?.toString(), + ) + } } private val firebaseIntentResultLauncher: ActivityResultLauncher = @@ -76,7 +85,22 @@ class MainActivity : ComponentActivity(), KoinComponent { val firebaseCredential = GoogleAuthProvider.getCredential(credential.googleIdToken, null) auth.signInWithCredential(firebaseCredential) - .addOnCompleteListener(this) { firebaseService.handleResultTask(it) } + .addOnCompleteListener(this) { task -> + if (task.isSuccessful) { + logger.d { "signInWithCredential:success" } + auth.currentUser?.let { user -> + firebaseService.updateCredentials( + isAuthenticated = true, + id = user.uid, + name = user.displayName, + email = user.email, + pictureUrl = user.photoUrl?.toString(), + ) + } + } else { + logger.e(task.exception) { "signInWithCredential:failure" } + } + } } catch (e: ApiException) { logger.e(e) { "NO ID Token" } } @@ -89,6 +113,7 @@ class MainActivity : ComponentActivity(), KoinComponent { installSplashScreen() AppChecker.checkTimeZoneHash() + (firebaseService as FirebaseService).setActivity(this, firebaseIntentResultLauncher) analyticsService.logEvent(AnalyticsService.EVENT_STARTED) applicationViewModel.lifecycle.removeFromParent() @@ -103,15 +128,7 @@ class MainActivity : ComponentActivity(), KoinComponent { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { - MainView(viewModel = applicationViewModel, isAuthenticated = isAuthenticated) { - if (isAuthenticated) firebaseService.performLogout() - else - firebaseService.performGoogleLogin( - this, - firebaseIntentResultLauncher, - ) - } - + MainView(viewModel = applicationViewModel) val showSplashScreen by applicationViewModel.showSplashScreen.collectAsState() Crossfade(targetState = showSplashScreen) { shouldShowSplashScreen -> if (shouldShowSplashScreen) { diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index e256f606..8f61a862 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -9,12 +9,14 @@ import co.touchlab.droidcon.android.service.impl.DefaultParseUrlViewService import co.touchlab.droidcon.android.util.NotificationLocalizedStringFactory import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.AuthenticationService import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.util.ClasspathResourceReader import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.auth.FirebaseAuth import com.google.firebase.ktx.Firebase import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings @@ -54,7 +56,7 @@ class MainApp : Application() { AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) } - single { + single{ FirebaseService() } } + uiModule diff --git a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj index e866e055..3aa7adfb 100644 --- a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj +++ b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj @@ -168,7 +168,6 @@ 68DCBC6326C51E260084C70D /* TextView.swift */, 681C95A026C555D90011330B /* VisualEffectView.swift */, 681C95A226C56B100011330B /* CustomOverlayView.swift */, - F127D3582BB46A0A00E08281 /* FirebaseService.swift */, ); path = Utils; sourceTree = ""; @@ -257,6 +256,7 @@ F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */, 46B5284C249C5CF400A7725D /* Koin.swift */, 8404D80D26C64B9E00AE200F /* IOSAnalyticsService.swift */, + F127D3582BB46A0A00E08281 /* FirebaseService.swift */, F1465F0923AA94BF0055F7C3 /* Assets.xcassets */, F1465F0B23AA94BF0055F7C3 /* LaunchScreen.storyboard */, F1465F0E23AA94BF0055F7C3 /* Info.plist */, @@ -649,7 +649,7 @@ "\"DroidconKit\"", "-lsqlite3", ); - PRODUCT_BUNDLE_IDENTIFIER = co.touchlab.droidcon.ios.london; + PRODUCT_BUNDLE_IDENTIFIER = co.touchlab.droidconauthtest; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -686,7 +686,7 @@ "\"DroidconKit\"", "-lsqlite3", ); - PRODUCT_BUNDLE_IDENTIFIER = co.touchlab.droidcon.ios.london; + PRODUCT_BUNDLE_IDENTIFIER = co.touchlab.droidconauthtest; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; diff --git a/ios/Droidcon/Droidcon/FirebaseService.swift b/ios/Droidcon/Droidcon/FirebaseService.swift new file mode 100644 index 00000000..6ffa6dd1 --- /dev/null +++ b/ios/Droidcon/Droidcon/FirebaseService.swift @@ -0,0 +1,83 @@ +// +// FirebaseService.swift +// Droidcon +// +// Created by Kevin Schildhorn on 3/27/24. +// Copyright © 2024 Touchlab. All rights reserved. +// + +import Foundation +import FirebaseCore +import GoogleSignIn +import FirebaseAuth +import DroidconKit + +class FirebaseService : AuthenticationService { + + let logger = Logger.companion.withTag(tag: "Authentication") + + init() { + super.init(isSignedIn: Auth.auth().currentUser != nil) + } + + override func performGoogleLogin() -> Bool { + logger.i(message: { "Performing Google Login" }) + guard let clientID = FirebaseApp.app()?.options.clientID else { + logger.e(message: { "No ClientId" }) + return false + } + + let config = GIDConfiguration(clientID: clientID) + GIDSignIn.sharedInstance.configuration = config + guard let presentingViewController = UIApplication.shared.windows.first?.rootViewController + else { + logger.e(message: { "No Presenting Controller" }) + return false + } + + logger.v(message: { "Sign In with Shared Google Instance" }) + GIDSignIn.sharedInstance.signIn(withPresenting: presentingViewController) { result, error in + guard error == nil else { + self.logger.e(message: { error?.localizedDescription ?? "" }) + return + } + + guard let user = result?.user, + let idToken = user.idToken?.tokenString + else { + self.logger.e(message: { "No User Found" }) + return + } + + self.logger.v(message: { "Get Credentials from Auth Provider" }) + let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: user.accessToken.tokenString) + Auth.auth().signIn(with: credential) { result, error in + if let error { + self.logger.e(message: { error.localizedDescription }) + self.updateCredentials(isAuthenticated: false, id: "", name: nil, email: nil, pictureUrl: nil) + } else { + self.logger.v(message: { "Got results from Auth!" }) + self.updateCredentials( + isAuthenticated: true, + id: result?.user.uid ?? "", + name: result?.user.displayName, + email: result?.user.email, + pictureUrl: result?.user.photoURL?.absoluteString + ) + } + } + } + return true + } + + override func performLogout() -> Bool { + do { + self.logger.v(message: { "Performing Logout" }) + try Auth.auth().signOut() + return super.performLogout() + } catch let signOutError as NSError { + self.logger.v(message: { "Error occured signing out" }) + return false + } + } +} diff --git a/ios/Droidcon/Droidcon/Koin.swift b/ios/Droidcon/Droidcon/Koin.swift index 66359b9a..d3a8d59f 100644 --- a/ios/Droidcon/Droidcon/Koin.swift +++ b/ios/Droidcon/Droidcon/Koin.swift @@ -4,7 +4,11 @@ import DroidconKit func startKoin() { let userDefaults = UserDefaults(suiteName: "DROIDCON2023_SETTINGS")! - let koinApplication = DependencyInjectionKt.doInitKoinIos(userDefaults: userDefaults, analyticsService: IOSAnalyticsService()) + let koinApplication = DependencyInjectionKt.doInitKoinIos( + userDefaults: userDefaults, + analyticsService: IOSAnalyticsService(), + authenticationService: FirebaseService() + ) _koin = koinApplication.koin } diff --git a/ios/Droidcon/Droidcon/Settings/SettingsView.swift b/ios/Droidcon/Droidcon/Settings/SettingsView.swift index f5519082..9e8818d3 100644 --- a/ios/Droidcon/Droidcon/Settings/SettingsView.swift +++ b/ios/Droidcon/Droidcon/Settings/SettingsView.swift @@ -5,7 +5,6 @@ import DroidconKit struct SettingsView: View { @ObservedObject private(set) var viewModel: SettingsViewModel - private let firebaseService = FirebaseService() @State var errorMessage: String = "" @State var showingAlert: Bool = false @@ -39,19 +38,25 @@ struct SettingsView: View { Divider().padding(.horizontal) - if Auth.auth().currentUser != nil { + if viewModel.isAuthenticated { Button("Settings.SignOut"){ - if let error = firebaseService.signOut() { - errorMessage = error + if viewModel.signOut() { + errorMessage = "" + showingAlert = false + } else { + errorMessage = "Failed to Sign Out" showingAlert = true } } } else { Button("Settings.SignIn"){ - firebaseService.signIn(onError: { error in - errorMessage = error + if viewModel.signIn() { + errorMessage = "" + showingAlert = false + } else { + errorMessage = "Failed To Sign In" showingAlert = true - }) + } } } diff --git a/ios/Droidcon/Droidcon/Utils/FirebaseService.swift b/ios/Droidcon/Droidcon/Utils/FirebaseService.swift deleted file mode 100644 index 00579185..00000000 --- a/ios/Droidcon/Droidcon/Utils/FirebaseService.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// FirebaseService.swift -// Droidcon -// -// Created by Kevin Schildhorn on 3/27/24. -// Copyright © 2024 Touchlab. All rights reserved. -// - -import Foundation -import FirebaseCore -import GoogleSignIn -import FirebaseAuth - -class FirebaseService { - - func signIn(onError: @escaping (String) -> Void) { - guard let clientID = FirebaseApp.app()?.options.clientID else { - onError("No ClientId") - return - } - - let config = GIDConfiguration(clientID: clientID) - GIDSignIn.sharedInstance.configuration = config - guard let presentingViewController = UIApplication.shared.windows.first?.rootViewController - else { - onError("No Presenting Controller") - return - } - - GIDSignIn.sharedInstance.signIn(withPresenting: presentingViewController) { result, error in - guard error == nil else { - onError(error?.localizedDescription ?? "") - return - } - - guard let user = result?.user, - let idToken = user.idToken?.tokenString - else { - onError("No User Found") - return - } - - let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: user.accessToken.tokenString) - Auth.auth().signIn(with: credential) { result, error in - if let error { - onError(error.localizedDescription) - } - } - } - } - - func signOut() -> String? { - do { - try Auth.auth().signOut() - return nil - } catch let signOutError as NSError { - return signOutError.localizedDescription - } - } -} diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt index b7207246..33005aaa 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -2,6 +2,7 @@ package co.touchlab.droidcon.ios import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService +import co.touchlab.droidcon.domain.service.AuthenticationService import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.ios.service.DefaultParseUrlViewService @@ -24,7 +25,8 @@ import platform.Foundation.NSUserDefaults @OptIn(ExperimentalSettingsApi::class) fun initKoinIos( userDefaults: NSUserDefaults, - analyticsService: AnalyticsService + analyticsService: AnalyticsService, + authenticationService: AuthenticationService, ): KoinApplication = initKoin( module { single { BundleProvider(bundle = NSBundle.mainBundle) } @@ -38,6 +40,7 @@ fun initKoinIos( } single { analyticsService } + single { authenticationService } single { DefaultParseUrlViewService() } } + uiModule diff --git a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt b/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt index fd834892..5a73691d 100644 --- a/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt +++ b/shared-ui/src/androidMain/kotlin/co/touchlab/droidcon/ui/util/MainView.kt @@ -9,13 +9,9 @@ import co.touchlab.droidcon.viewmodel.ApplicationViewModel @Composable fun MainView( viewModel: ApplicationViewModel, - isAuthenticated: Boolean, - onAuthRequest: () -> Unit, ) { MainComposeView( viewModel = viewModel, - isAuthenticated, - onAuthRequest, modifier = Modifier.systemBarsPadding(), ) } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt index d66e15a0..bd42efc7 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/BottomNavigationView.kt @@ -27,8 +27,6 @@ import co.touchlab.droidcon.viewmodel.ApplicationViewModel internal fun BottomNavigationView( viewModel: ApplicationViewModel, modifier: Modifier = Modifier, - isAuthenticated: Boolean, - onAuthRequest: () -> Unit, ) { val selectedTab by viewModel.observeSelectedTab.observeAsState() @@ -67,8 +65,6 @@ internal fun BottomNavigationView( ApplicationViewModel.Tab.Sponsors -> SponsorsView(viewModel.sponsors) ApplicationViewModel.Tab.Settings -> SettingsView( viewModel.settings, - isAuthenticated, - onAuthRequest ) } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt index c2725935..bacb5344 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/MainComposeView.kt @@ -12,14 +12,12 @@ import coil3.compose.setSingletonImageLoaderFactory @Composable internal fun MainComposeView( viewModel: ApplicationViewModel, - isAuthenticated: Boolean, - onAuthRequest: () -> Unit, modifier: Modifier = Modifier, ) { setSingletonImageLoaderFactory { context -> dcImageLoader(context, true) } DroidconTheme { - BottomNavigationView(viewModel = viewModel, modifier = modifier, isAuthenticated, onAuthRequest) + BottomNavigationView(viewModel = viewModel, modifier = modifier) } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt index 5e644fbb..93659107 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt @@ -84,15 +84,38 @@ val uiModule = module { single { SpeakerDetailViewModel.Factory(parseUrlViewService = get()) } - single { SponsorListViewModel.Factory(sponsorGateway = get(), sponsorGroupFactory = get(), sponsorDetailFactory = get()) } + single { + SponsorListViewModel.Factory( + sponsorGateway = get(), + sponsorGroupFactory = get(), + sponsorDetailFactory = get() + ) + } single { SponsorGroupViewModel.Factory(sponsorGroupItemFactory = get()) } single { SponsorGroupItemViewModel.Factory() } - single { SponsorDetailViewModel.Factory(sponsorGateway = get(), speakerListItemFactory = get(), speakerDetailFactory = get()) } + single { + SponsorDetailViewModel.Factory( + sponsorGateway = get(), + speakerListItemFactory = get(), + speakerDetailFactory = get() + ) + } - single { SettingsViewModel.Factory(settingsGateway = get(), aboutFactory = get()) } + single { + SettingsViewModel.Factory( + settingsGateway = get(), + authenticationService = get(), + aboutFactory = get() + ) + } single { AboutViewModel.Factory(aboutRepository = get(), parseUrlViewService = get()) } - single { FeedbackDialogViewModel.Factory(sessionGateway = get(), get(parameters = { parametersOf("FeedbackDialogViewModel") })) } + single { + FeedbackDialogViewModel.Factory( + sessionGateway = get(), + get(parameters = { parametersOf("FeedbackDialogViewModel") }) + ) + } single { SessionDetailScrollStateStorage() } } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt index 9f0be668..b53c0737 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt @@ -37,9 +37,8 @@ import org.brightify.hyperdrive.multiplatformx.property.MutableObservablePropert @Composable internal fun SettingsView( viewModel: SettingsViewModel, - isAuthenticated: Boolean, - onAuthRequest: () -> Unit ) { + val isAuthenticated by viewModel.observeIsAuthenticated.observeAsState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -73,7 +72,11 @@ internal fun SettingsView( Divider() - Button(onClick = onAuthRequest) { + Button( + onClick = { + if (isAuthenticated) viewModel.signOut() else viewModel.signIn() + } + ) { if (isAuthenticated) { Text("Sign Out") } else { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt index 0507acaa..40fb4daa 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt @@ -1,10 +1,12 @@ package co.touchlab.droidcon.viewmodel.settings import co.touchlab.droidcon.application.gateway.SettingsGateway +import co.touchlab.droidcon.domain.service.AuthenticationService import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SettingsViewModel( settingsGateway: SettingsGateway, + private val authenticationService: AuthenticationService, private val aboutFactory: AboutViewModel.Factory, ) : BaseViewModel() { @@ -46,11 +48,22 @@ class SettingsViewModel( ) val observeUseCompose by observe(::useCompose) + var isAuthenticated: Boolean by binding( + authenticationService.isAuthenticated, + mapping = { it }, + set = { } + ) + val observeIsAuthenticated by observe(::isAuthenticated) + + fun signIn() = authenticationService.performGoogleLogin() + fun signOut() = authenticationService.performLogout() + class Factory( private val settingsGateway: SettingsGateway, + private val authenticationService: AuthenticationService, private val aboutFactory: AboutViewModel.Factory, ) { - fun create() = SettingsViewModel(settingsGateway, aboutFactory) + fun create() = SettingsViewModel(settingsGateway, authenticationService, aboutFactory) } } diff --git a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt b/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt index e98ff29b..d017d961 100644 --- a/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt +++ b/shared-ui/src/iosMain/kotlin/co/touchlab/droidcon/ui/ComposeRootController.kt @@ -5,9 +5,7 @@ import co.touchlab.droidcon.viewmodel.ApplicationViewModel fun getRootController( viewModel: ApplicationViewModel, - isAuthenticated: Boolean, - onAuthRequest: () -> Unit, ) = ComposeUIViewController { - MainComposeView(viewModel, isAuthenticated, onAuthRequest) + MainComposeView(viewModel) } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt new file mode 100644 index 00000000..2864049e --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt @@ -0,0 +1,47 @@ +package co.touchlab.droidcon.domain.service + +import co.touchlab.droidcon.UserContext +import co.touchlab.droidcon.UserData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +abstract class AuthenticationService(isSignedIn: Boolean) : KoinComponent { + private val userIdProvider: UserIdProvider by inject() + + private val _isAuthenticated = MutableStateFlow(isSignedIn) + val isAuthenticated: StateFlow = _isAuthenticated + + abstract fun performGoogleLogin(): Boolean + open fun performLogout(): Boolean { + updateCredentials(false, "", null, null, null) + return true + } + + fun updateCredentials( + isAuthenticated: Boolean, + id: String, + name: String?, + email: String?, + pictureUrl: String?, + ) { + _isAuthenticated.update { isAuthenticated } + userIdProvider.saveUserContext( + UserContext( + isAuthenticated = isAuthenticated, + userData = if (isAuthenticated) { + UserData( + id = id, + name = name, + email = email, + pictureUrl = pictureUrl, + ) + } else { + null + } + ) + ) + } +} From 77ac4030e3fe3167f03fdc1a8464947f5a6ea9f3 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 Mar 2024 11:51:45 -0400 Subject: [PATCH 04/12] Cleaning up code --- .../droidcon/android/FirebaseService.kt | 2 +- .../touchlab/droidcon/android/MainActivity.kt | 22 +++++------- gradle/libs.versions.toml | 1 - ios/Droidcon/Droidcon/AppDelegate.swift | 14 ++++---- ios/Droidcon/Droidcon/FirebaseService.swift | 7 ++-- .../domain/service/AuthenticationService.kt | 35 +++++++++++-------- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt b/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt index 31620fea..1cdef176 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt @@ -16,7 +16,7 @@ import org.brightify.hyperdrive.utils.WeakReference class FirebaseService : AuthenticationService(FirebaseAuth.getInstance().currentUser != null) { - private val logger = Logger.withTag("Authentication") + private val logger = Logger.withTag("AuthenticationService") private val clientId = BuildConfig.CLIENT_ID private lateinit var weakActivity: WeakReference diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt index 9e40ecea..29f70bf4 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt @@ -58,22 +58,17 @@ class MainActivity : ComponentActivity(), KoinComponent { private val root = LifecycleGraph.Root(this) private val firebaseService: AuthenticationService by inject() - // Firebase Auth private lateinit var auth: FirebaseAuth - private val isAuthenticated: Boolean - get() = auth.currentUser != null - private val firebaseAuthListener = FirebaseAuth.AuthStateListener { firebaseAuth -> - with(firebaseAuth) { - firebaseService.updateCredentials( - isAuthenticated = currentUser != null, - id = currentUser?.uid ?: "", - name = currentUser?.displayName, - email = currentUser?.email, - pictureUrl = currentUser?.photoUrl?.toString(), + firebaseAuth.currentUser?.let { user -> + firebaseService.setCredentials( + id = user.uid, + name = user.displayName, + email = user.email, + pictureUrl = user.photoUrl?.toString(), ) - } + } ?: run { firebaseService.clearCredentials() } } private val firebaseIntentResultLauncher: ActivityResultLauncher = @@ -89,8 +84,7 @@ class MainActivity : ComponentActivity(), KoinComponent { if (task.isSuccessful) { logger.d { "signInWithCredential:success" } auth.currentUser?.let { user -> - firebaseService.updateCredentials( - isAuthenticated = true, + firebaseService.setCredentials( id = user.uid, name = user.displayName, email = user.email, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 711aa3bf..01cc48e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,6 @@ firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" firebase-auth = { module = "com.google.firebase:firebase-auth", version = "_" } playservices-auth = { module = "com.google.android.gms:play-services-auth", version = "21.0.0" } - hyperdrive-multiplatformx-api = { module = "org.brightify.hyperdrive:multiplatformx-api", version.ref = "hyperdrive" } android-desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugaring" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } diff --git a/ios/Droidcon/Droidcon/AppDelegate.swift b/ios/Droidcon/Droidcon/AppDelegate.swift index f0578a7e..690b33aa 100644 --- a/ios/Droidcon/Droidcon/AppDelegate.swift +++ b/ios/Droidcon/Droidcon/AppDelegate.swift @@ -9,9 +9,11 @@ class AppDelegate: NSObject, UIApplicationDelegate { lazy var log = koin.get(objCClass: Logger.self, parameter: "AppDelegate") as! Logger lazy var analytics = koin.get(objCProtocol: AnalyticsService.self, qualifier: nil) as! AnalyticsService lazy var appChecker = koin.get(objCClass: AppChecker.self) as! AppChecker + lazy var firebaseService = koin.get(objCClass: AuthenticationService.self, qualifier: nil) as! AuthenticationService var firebaseAuthListener:AuthStateDidChangeListenerHandle? + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -25,16 +27,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { analytics.logEvent(name: AnalyticsServiceCompanion().EVENT_STARTED, params: [:]) - firebaseAuthListener = Auth.auth().addStateDidChangeListener() { auth, user in - // TODO if let user { - _ = UserContext( - isAuthenticated: false, - userData: UserData(id: user.uid, name: user.displayName, email: user.email, pictureUrl: user.photoURL?.absoluteString) + self.firebaseService.setCredentials( + id: user.uid, + name: user.displayName, + email: user.email, + pictureUrl: user.photoURL?.absoluteString ) } else { - _ = UserContext(isAuthenticated: false, userData: nil) + self.firebaseService.clearCredentials() } } diff --git a/ios/Droidcon/Droidcon/FirebaseService.swift b/ios/Droidcon/Droidcon/FirebaseService.swift index 6ffa6dd1..9ae08195 100644 --- a/ios/Droidcon/Droidcon/FirebaseService.swift +++ b/ios/Droidcon/Droidcon/FirebaseService.swift @@ -14,7 +14,7 @@ import DroidconKit class FirebaseService : AuthenticationService { - let logger = Logger.companion.withTag(tag: "Authentication") + let logger = Logger.companion.withTag(tag: "AuthenticationService") init() { super.init(isSignedIn: Auth.auth().currentUser != nil) @@ -54,11 +54,10 @@ class FirebaseService : AuthenticationService { Auth.auth().signIn(with: credential) { result, error in if let error { self.logger.e(message: { error.localizedDescription }) - self.updateCredentials(isAuthenticated: false, id: "", name: nil, email: nil, pictureUrl: nil) + self.clearCredentials() } else { self.logger.v(message: { "Got results from Auth!" }) - self.updateCredentials( - isAuthenticated: true, + self.setCredentials( id: result?.user.uid ?? "", name: result?.user.displayName, email: result?.user.email, diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt index 2864049e..08949056 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt @@ -16,31 +16,36 @@ abstract class AuthenticationService(isSignedIn: Boolean) : KoinComponent { abstract fun performGoogleLogin(): Boolean open fun performLogout(): Boolean { - updateCredentials(false, "", null, null, null) + clearCredentials() return true } - fun updateCredentials( - isAuthenticated: Boolean, + fun setCredentials( id: String, name: String?, email: String?, pictureUrl: String?, ) { - _isAuthenticated.update { isAuthenticated } + _isAuthenticated.update { true } userIdProvider.saveUserContext( UserContext( - isAuthenticated = isAuthenticated, - userData = if (isAuthenticated) { - UserData( - id = id, - name = name, - email = email, - pictureUrl = pictureUrl, - ) - } else { - null - } + isAuthenticated = true, + userData = UserData( + id = id, + name = name, + email = email, + pictureUrl = pictureUrl, + ) + ) + ) + } + + fun clearCredentials() { + _isAuthenticated.update { false } + userIdProvider.saveUserContext( + UserContext( + isAuthenticated = false, + userData = null ) ) } From d57fbbed746786774b2550776dc240e715c07810 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Thu, 28 Mar 2024 14:42:58 -0400 Subject: [PATCH 05/12] formatting --- .../main/java/co/touchlab/droidcon/android/MainActivity.kt | 4 ++-- android/src/main/java/co/touchlab/droidcon/android/MainApp.kt | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt index 29f70bf4..7b093a11 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt @@ -61,7 +61,7 @@ class MainActivity : ComponentActivity(), KoinComponent { private lateinit var auth: FirebaseAuth private val firebaseAuthListener = FirebaseAuth.AuthStateListener { firebaseAuth -> - firebaseAuth.currentUser?.let { user -> + firebaseAuth.currentUser?.let { user -> firebaseService.setCredentials( id = user.uid, name = user.displayName, @@ -201,4 +201,4 @@ class MainActivity : ComponentActivity(), KoinComponent { super.onBackPressed() } } -} \ No newline at end of file +} diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index 8f61a862..3a09d6dc 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -16,7 +16,6 @@ import co.touchlab.droidcon.service.ParseUrlViewService import co.touchlab.droidcon.ui.uiModule import co.touchlab.droidcon.util.ClasspathResourceReader import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.auth.FirebaseAuth import com.google.firebase.ktx.Firebase import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ObservableSettings @@ -56,7 +55,7 @@ class MainApp : Application() { AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) } - single{ + single { FirebaseService() } } + uiModule From c81872c6e462ef339c368d64e1c2df9e30146d54 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 Mar 2024 11:59:02 -0400 Subject: [PATCH 06/12] Separating into two services --- .../touchlab/droidcon/android/MainActivity.kt | 13 +++-- .../co/touchlab/droidcon/android/MainApp.kt | 7 +-- .../impl/AndroidGoogleSignInService.kt} | 12 ++--- .../Droidcon.xcodeproj/project.pbxproj | 8 +-- ...ice.swift => IOSGoogleSignInService.swift} | 23 +++------ ios/Droidcon/Droidcon/Koin.swift | 2 +- .../droidcon/ios/DependencyInjection.kt | 6 +-- .../co.touchlab.droidcon/ui/UiModule.kt | 1 + .../viewmodel/settings/SettingsViewModel.kt | 9 ++-- .../kotlin/co/touchlab/droidcon/Koin.kt | 3 ++ .../domain/service/AuthenticationService.kt | 45 ++--------------- .../domain/service/GoogleSignInService.kt | 6 +++ .../impl/DefaultAuthenticationService.kt | 49 +++++++++++++++++++ 13 files changed, 101 insertions(+), 83 deletions(-) rename android/src/main/java/co/touchlab/droidcon/android/{FirebaseService.kt => service/impl/AndroidGoogleSignInService.kt} (88%) rename ios/Droidcon/Droidcon/{FirebaseService.swift => IOSGoogleSignInService.swift} (73%) create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/GoogleSignInService.kt create mode 100644 shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt index 7b093a11..612e63fc 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainActivity.kt @@ -24,10 +24,12 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import co.touchlab.droidcon.R +import co.touchlab.droidcon.android.service.impl.AndroidGoogleSignInService import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.application.service.NotificationService import co.touchlab.droidcon.domain.service.AnalyticsService import co.touchlab.droidcon.domain.service.AuthenticationService +import co.touchlab.droidcon.domain.service.GoogleSignInService import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.service.AndroidNotificationService import co.touchlab.droidcon.ui.theme.Colors @@ -56,19 +58,20 @@ class MainActivity : ComponentActivity(), KoinComponent { private val applicationViewModel: ApplicationViewModel by inject() private val root = LifecycleGraph.Root(this) - private val firebaseService: AuthenticationService by inject() + private val authenticationService: AuthenticationService by inject() + private val googleSignInService: GoogleSignInService by inject() private lateinit var auth: FirebaseAuth private val firebaseAuthListener = FirebaseAuth.AuthStateListener { firebaseAuth -> firebaseAuth.currentUser?.let { user -> - firebaseService.setCredentials( + authenticationService.setCredentials( id = user.uid, name = user.displayName, email = user.email, pictureUrl = user.photoUrl?.toString(), ) - } ?: run { firebaseService.clearCredentials() } + } ?: run { authenticationService.clearCredentials() } } private val firebaseIntentResultLauncher: ActivityResultLauncher = @@ -84,7 +87,7 @@ class MainActivity : ComponentActivity(), KoinComponent { if (task.isSuccessful) { logger.d { "signInWithCredential:success" } auth.currentUser?.let { user -> - firebaseService.setCredentials( + authenticationService.setCredentials( id = user.uid, name = user.displayName, email = user.email, @@ -107,7 +110,7 @@ class MainActivity : ComponentActivity(), KoinComponent { installSplashScreen() AppChecker.checkTimeZoneHash() - (firebaseService as FirebaseService).setActivity(this, firebaseIntentResultLauncher) + (googleSignInService as AndroidGoogleSignInService).setActivity(this, firebaseIntentResultLauncher) analyticsService.logEvent(AnalyticsService.EVENT_STARTED) applicationViewModel.lifecycle.removeFromParent() diff --git a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt index 3a09d6dc..be32929b 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/MainApp.kt @@ -5,11 +5,12 @@ import android.app.Application import android.content.Context import android.content.SharedPreferences import co.touchlab.droidcon.android.service.impl.AndroidAnalyticsService +import co.touchlab.droidcon.android.service.impl.AndroidGoogleSignInService import co.touchlab.droidcon.android.service.impl.DefaultParseUrlViewService import co.touchlab.droidcon.android.util.NotificationLocalizedStringFactory import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService -import co.touchlab.droidcon.domain.service.AuthenticationService +import co.touchlab.droidcon.domain.service.GoogleSignInService import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.service.ParseUrlViewService @@ -55,8 +56,8 @@ class MainApp : Application() { AndroidAnalyticsService(firebaseAnalytics = Firebase.analytics) } - single { - FirebaseService() + single { + AndroidGoogleSignInService() } } + uiModule ) diff --git a/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt b/android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidGoogleSignInService.kt similarity index 88% rename from android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt rename to android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidGoogleSignInService.kt index 1cdef176..57c215c3 100644 --- a/android/src/main/java/co/touchlab/droidcon/android/FirebaseService.kt +++ b/android/src/main/java/co/touchlab/droidcon/android/service/impl/AndroidGoogleSignInService.kt @@ -1,20 +1,20 @@ -package co.touchlab.droidcon.android +package co.touchlab.droidcon.android.service.impl import android.app.Activity import android.content.IntentSender import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import co.touchlab.droidcon.BuildConfig -import co.touchlab.droidcon.domain.service.AuthenticationService +import co.touchlab.droidcon.domain.service.GoogleSignInService import co.touchlab.kermit.Logger import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.Identity -import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase import org.brightify.hyperdrive.utils.WeakReference +import org.koin.core.component.KoinComponent -class FirebaseService : AuthenticationService(FirebaseAuth.getInstance().currentUser != null) { +class AndroidGoogleSignInService : GoogleSignInService, KoinComponent { private val logger = Logger.withTag("AuthenticationService") private val clientId = BuildConfig.CLIENT_ID @@ -64,8 +64,8 @@ class FirebaseService : AuthenticationService(FirebaseAuth.getInstance().current return false } - override fun performLogout(): Boolean { + override fun performGoogleLogout(): Boolean { Firebase.auth.signOut() - return super.performLogout() + return true } } diff --git a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj index 3aa7adfb..d3c24135 100644 --- a/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj +++ b/ios/Droidcon/Droidcon.xcodeproj/project.pbxproj @@ -49,7 +49,7 @@ A35DC2E328AB6C6F00C7B298 /* ComposeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35DC2E228AB6C6F00C7B298 /* ComposeController.swift */; }; A35DEF2228AA265C0072605A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = A35DEF2128AA265C0072605A /* Settings.bundle */; }; A35DEF2428AA26C80072605A /* SettingsBundleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35DEF2328AA26C80072605A /* SettingsBundleHelper.swift */; }; - F127D3592BB46A0A00E08281 /* FirebaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F127D3582BB46A0A00E08281 /* FirebaseService.swift */; }; + F127D3592BB46A0A00E08281 /* IOSGoogleSignInService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F127D3582BB46A0A00E08281 /* IOSGoogleSignInService.swift */; }; F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */; }; F1465F0A23AA94BF0055F7C3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F1465F0923AA94BF0055F7C3 /* Assets.xcassets */; }; /* End PBXBuildFile section */ @@ -98,7 +98,7 @@ A35DEF2328AA26C80072605A /* SettingsBundleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBundleHelper.swift; sourceTree = ""; }; DD90C0C0A4D331CEBDBE69E8 /* Pods-Droidcon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Droidcon.release.xcconfig"; path = "Target Support Files/Pods-Droidcon/Pods-Droidcon.release.xcconfig"; sourceTree = ""; }; EBA278C64D82609BA00FE2A5 /* Pods_Droidcon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Droidcon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F127D3582BB46A0A00E08281 /* FirebaseService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseService.swift; sourceTree = ""; }; + F127D3582BB46A0A00E08281 /* IOSGoogleSignInService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSGoogleSignInService.swift; sourceTree = ""; }; F1465EFD23AA94BF0055F7C3 /* Droidcon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Droidcon.app; sourceTree = BUILT_PRODUCTS_DIR; }; F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F1465F0923AA94BF0055F7C3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -256,7 +256,7 @@ F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */, 46B5284C249C5CF400A7725D /* Koin.swift */, 8404D80D26C64B9E00AE200F /* IOSAnalyticsService.swift */, - F127D3582BB46A0A00E08281 /* FirebaseService.swift */, + F127D3582BB46A0A00E08281 /* IOSGoogleSignInService.swift */, F1465F0923AA94BF0055F7C3 /* Assets.xcassets */, F1465F0B23AA94BF0055F7C3 /* LaunchScreen.storyboard */, F1465F0E23AA94BF0055F7C3 /* Info.plist */, @@ -450,7 +450,7 @@ 684FAA7726B2A4EA00673AFF /* SettingsView.swift in Sources */, 689DD2FB26B40F1800A9B009 /* LazyView.swift in Sources */, 689DD30526B438CA00A9B009 /* Avatar.swift in Sources */, - F127D3592BB46A0A00E08281 /* FirebaseService.swift in Sources */, + F127D3592BB46A0A00E08281 /* IOSGoogleSignInService.swift in Sources */, 681C95A126C555D90011330B /* VisualEffectView.swift in Sources */, 1833221026B0CF5600D79482 /* DroidconApp.swift in Sources */, 684FAA7426B2A4D400673AFF /* ScheduleView.swift in Sources */, diff --git a/ios/Droidcon/Droidcon/FirebaseService.swift b/ios/Droidcon/Droidcon/IOSGoogleSignInService.swift similarity index 73% rename from ios/Droidcon/Droidcon/FirebaseService.swift rename to ios/Droidcon/Droidcon/IOSGoogleSignInService.swift index 9ae08195..674321fb 100644 --- a/ios/Droidcon/Droidcon/FirebaseService.swift +++ b/ios/Droidcon/Droidcon/IOSGoogleSignInService.swift @@ -12,15 +12,11 @@ import GoogleSignIn import FirebaseAuth import DroidconKit -class FirebaseService : AuthenticationService { +class IOSGoogleSignInService : GoogleSignInService { - let logger = Logger.companion.withTag(tag: "AuthenticationService") - - init() { - super.init(isSignedIn: Auth.auth().currentUser != nil) - } + let logger = Logger.companion.withTag(tag: "IOSGoogleSignInService") - override func performGoogleLogin() -> Bool { + func performGoogleLogin() -> Bool { logger.i(message: { "Performing Google Login" }) guard let clientID = FirebaseApp.app()?.options.clientID else { logger.e(message: { "No ClientId" }) @@ -54,28 +50,21 @@ class FirebaseService : AuthenticationService { Auth.auth().signIn(with: credential) { result, error in if let error { self.logger.e(message: { error.localizedDescription }) - self.clearCredentials() } else { self.logger.v(message: { "Got results from Auth!" }) - self.setCredentials( - id: result?.user.uid ?? "", - name: result?.user.displayName, - email: result?.user.email, - pictureUrl: result?.user.photoURL?.absoluteString - ) } } } return true } - override func performLogout() -> Bool { + func performGoogleLogout() -> Bool { do { self.logger.v(message: { "Performing Logout" }) try Auth.auth().signOut() - return super.performLogout() + return true } catch let signOutError as NSError { - self.logger.v(message: { "Error occured signing out" }) + self.logger.e(message: { "Error occured signing out: \(signOutError.localizedDescription)" }) return false } } diff --git a/ios/Droidcon/Droidcon/Koin.swift b/ios/Droidcon/Droidcon/Koin.swift index d3a8d59f..faad2632 100644 --- a/ios/Droidcon/Droidcon/Koin.swift +++ b/ios/Droidcon/Droidcon/Koin.swift @@ -7,7 +7,7 @@ func startKoin() { let koinApplication = DependencyInjectionKt.doInitKoinIos( userDefaults: userDefaults, analyticsService: IOSAnalyticsService(), - authenticationService: FirebaseService() + googleSignInService: IOSGoogleSignInService() ) _koin = koinApplication.koin } diff --git a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt index 33005aaa..b0973895 100644 --- a/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt +++ b/ios/src/iosMain/kotlin/co/touchlab/droidcon/ios/DependencyInjection.kt @@ -2,7 +2,7 @@ package co.touchlab.droidcon.ios import co.touchlab.droidcon.application.service.NotificationSchedulingService import co.touchlab.droidcon.domain.service.AnalyticsService -import co.touchlab.droidcon.domain.service.AuthenticationService +import co.touchlab.droidcon.domain.service.GoogleSignInService import co.touchlab.droidcon.domain.service.impl.ResourceReader import co.touchlab.droidcon.initKoin import co.touchlab.droidcon.ios.service.DefaultParseUrlViewService @@ -26,7 +26,7 @@ import platform.Foundation.NSUserDefaults fun initKoinIos( userDefaults: NSUserDefaults, analyticsService: AnalyticsService, - authenticationService: AuthenticationService, + googleSignInService: GoogleSignInService, ): KoinApplication = initKoin( module { single { BundleProvider(bundle = NSBundle.mainBundle) } @@ -40,7 +40,7 @@ fun initKoinIos( } single { analyticsService } - single { authenticationService } + single { googleSignInService } single { DefaultParseUrlViewService() } } + uiModule diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt index 93659107..36991d7e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/UiModule.kt @@ -105,6 +105,7 @@ val uiModule = module { SettingsViewModel.Factory( settingsGateway = get(), authenticationService = get(), + googleSignInService = get(), aboutFactory = get() ) } diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt index 40fb4daa..4a0317ad 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt @@ -2,11 +2,13 @@ package co.touchlab.droidcon.viewmodel.settings import co.touchlab.droidcon.application.gateway.SettingsGateway import co.touchlab.droidcon.domain.service.AuthenticationService +import co.touchlab.droidcon.domain.service.GoogleSignInService import org.brightify.hyperdrive.multiplatformx.BaseViewModel class SettingsViewModel( settingsGateway: SettingsGateway, private val authenticationService: AuthenticationService, + private val googleSignInService: GoogleSignInService, private val aboutFactory: AboutViewModel.Factory, ) : BaseViewModel() { @@ -55,15 +57,16 @@ class SettingsViewModel( ) val observeIsAuthenticated by observe(::isAuthenticated) - fun signIn() = authenticationService.performGoogleLogin() - fun signOut() = authenticationService.performLogout() + fun signIn() = googleSignInService.performGoogleLogin() + fun signOut() = googleSignInService.performGoogleLogout() class Factory( private val settingsGateway: SettingsGateway, private val authenticationService: AuthenticationService, + private val googleSignInService: GoogleSignInService, private val aboutFactory: AboutViewModel.Factory, ) { - fun create() = SettingsViewModel(settingsGateway, authenticationService, aboutFactory) + fun create() = SettingsViewModel(settingsGateway, authenticationService, googleSignInService, aboutFactory) } } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt index eda6e3b0..a00d046b 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/Koin.kt @@ -27,6 +27,7 @@ import co.touchlab.droidcon.domain.repository.impl.SqlDelightSessionRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorGroupRepository import co.touchlab.droidcon.domain.repository.impl.SqlDelightSponsorRepository import co.touchlab.droidcon.domain.repository.impl.adapter.InstantSqlDelightAdapter +import co.touchlab.droidcon.domain.service.AuthenticationService import co.touchlab.droidcon.domain.service.DateTimeService import co.touchlab.droidcon.domain.service.FeedbackService import co.touchlab.droidcon.domain.service.ScheduleService @@ -34,6 +35,7 @@ import co.touchlab.droidcon.domain.service.ServerApi import co.touchlab.droidcon.domain.service.SyncService import co.touchlab.droidcon.domain.service.UserIdProvider import co.touchlab.droidcon.domain.service.impl.DefaultApiDataSource +import co.touchlab.droidcon.domain.service.impl.DefaultAuthenticationService import co.touchlab.droidcon.domain.service.impl.DefaultDateTimeService import co.touchlab.droidcon.domain.service.impl.DefaultFeedbackService import co.touchlab.droidcon.domain.service.impl.DefaultScheduleService @@ -235,6 +237,7 @@ private val coreModule = module { clock = get(), ) } + single { DefaultAuthenticationService() } } internal inline fun Scope.getWith(vararg params: Any?): T { diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt index 08949056..22032f18 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt @@ -1,52 +1,15 @@ package co.touchlab.droidcon.domain.service -import co.touchlab.droidcon.UserContext -import co.touchlab.droidcon.UserData -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -abstract class AuthenticationService(isSignedIn: Boolean) : KoinComponent { - private val userIdProvider: UserIdProvider by inject() - - private val _isAuthenticated = MutableStateFlow(isSignedIn) - val isAuthenticated: StateFlow = _isAuthenticated - - abstract fun performGoogleLogin(): Boolean - open fun performLogout(): Boolean { - clearCredentials() - return true - } +interface AuthenticationService { + val isAuthenticated: StateFlow fun setCredentials( id: String, name: String?, email: String?, pictureUrl: String?, - ) { - _isAuthenticated.update { true } - userIdProvider.saveUserContext( - UserContext( - isAuthenticated = true, - userData = UserData( - id = id, - name = name, - email = email, - pictureUrl = pictureUrl, - ) - ) - ) - } - - fun clearCredentials() { - _isAuthenticated.update { false } - userIdProvider.saveUserContext( - UserContext( - isAuthenticated = false, - userData = null - ) - ) - } + ) + fun clearCredentials() } diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/GoogleSignInService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/GoogleSignInService.kt new file mode 100644 index 00000000..8f08b261 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/GoogleSignInService.kt @@ -0,0 +1,6 @@ +package co.touchlab.droidcon.domain.service + +interface GoogleSignInService { + fun performGoogleLogin(): Boolean + fun performGoogleLogout(): Boolean +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt new file mode 100644 index 00000000..fa01f57b --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt @@ -0,0 +1,49 @@ +package co.touchlab.droidcon.domain.service.impl + +import co.touchlab.droidcon.UserContext +import co.touchlab.droidcon.UserData +import co.touchlab.droidcon.domain.service.AuthenticationService +import co.touchlab.droidcon.domain.service.UserIdProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class DefaultAuthenticationService : AuthenticationService, KoinComponent { + + private val userIdProvider: UserIdProvider by inject() + + private val _isAuthenticated = MutableStateFlow(false) + override val isAuthenticated: StateFlow = _isAuthenticated + + override fun setCredentials( + id: String, + name: String?, + email: String?, + pictureUrl: String?, + ) { + _isAuthenticated.update { true } + userIdProvider.saveUserContext( + UserContext( + isAuthenticated = true, + userData = UserData( + id = id, + name = name, + email = email, + pictureUrl = pictureUrl, + ) + ) + ) + } + + override fun clearCredentials() { + _isAuthenticated.update { false } + userIdProvider.saveUserContext( + UserContext( + isAuthenticated = false, + userData = null + ) + ) + } +} From c8d468e7fea9f283cf8ce2fdb948f48ab2a69f45 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 Mar 2024 12:06:14 -0400 Subject: [PATCH 07/12] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 19e7a2cb..a6061681 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,11 @@ This project has a pair of native mobile applications backed by the Sessionize d The apps need a Firebase account set up to run. You'll need to get the `google-services.json` and put it in `android/google-services.json` for Android, and the `GoogleService-Info.plist` and put that in `ios/Droidcon/Droidcon/GoogleService-Info.plist` for iOS. +Additionally for Firebase Authentication you'll need to pass in your client ID into the project. + +For Android you'll need to add a `clientId` property to your `local.properties`. +For iOS you'll need to pass the clientId into your [URL Types](https://firebase.google.com/docs/auth/ios/google-signin#implement_google_sign-in). + ## Compose UI for both! We're running a very early version of Compose UI for iOS as the iOS interface. It mostly shares the screen code with the Android app. While Native Compose UI is obviously experimental, it works surprisingly well. From 109bd4542235451119848e0270f590f8646ae87c Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 Mar 2024 12:54:18 -0400 Subject: [PATCH 08/12] Updating buttons --- .../continue_with_google_rd.xml | 28 +++++++++++++++++++ ios/Droidcon/Droidcon/AppDelegate.swift | 6 ++-- .../Contents.json | 12 ++++++++ .../android_neutral_rd_ctn.svg | 15 ++++++++++ .../Droidcon/Settings/SettingsView.swift | 7 +++-- .../Droidcon/en.lproj/Localizable.strings | 1 - .../ui/settings/SettingsView.kt | 15 ++++++++-- 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 android/src/main/res/drawable-nodpi/continue_with_google_rd.xml create mode 100644 ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/Contents.json create mode 100644 ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/android_neutral_rd_ctn.svg diff --git a/android/src/main/res/drawable-nodpi/continue_with_google_rd.xml b/android/src/main/res/drawable-nodpi/continue_with_google_rd.xml new file mode 100644 index 00000000..6a2f91e6 --- /dev/null +++ b/android/src/main/res/drawable-nodpi/continue_with_google_rd.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/ios/Droidcon/Droidcon/AppDelegate.swift b/ios/Droidcon/Droidcon/AppDelegate.swift index 690b33aa..7e3740e5 100644 --- a/ios/Droidcon/Droidcon/AppDelegate.swift +++ b/ios/Droidcon/Droidcon/AppDelegate.swift @@ -9,7 +9,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { lazy var log = koin.get(objCClass: Logger.self, parameter: "AppDelegate") as! Logger lazy var analytics = koin.get(objCProtocol: AnalyticsService.self, qualifier: nil) as! AnalyticsService lazy var appChecker = koin.get(objCClass: AppChecker.self) as! AppChecker - lazy var firebaseService = koin.get(objCClass: AuthenticationService.self, qualifier: nil) as! AuthenticationService + lazy var authenticationService = koin.get(objCProtocol: AuthenticationService.self, qualifier: nil) as! AuthenticationService var firebaseAuthListener:AuthStateDidChangeListenerHandle? @@ -29,14 +29,14 @@ class AppDelegate: NSObject, UIApplicationDelegate { firebaseAuthListener = Auth.auth().addStateDidChangeListener() { auth, user in if let user { - self.firebaseService.setCredentials( + self.authenticationService.setCredentials( id: user.uid, name: user.displayName, email: user.email, pictureUrl: user.photoURL?.absoluteString ) } else { - self.firebaseService.clearCredentials() + self.authenticationService.clearCredentials() } } diff --git a/ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/Contents.json b/ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/Contents.json new file mode 100644 index 00000000..35bc0fb5 --- /dev/null +++ b/ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "android_neutral_rd_ctn.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/android_neutral_rd_ctn.svg b/ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/android_neutral_rd_ctn.svg new file mode 100644 index 00000000..158d3228 --- /dev/null +++ b/ios/Droidcon/Droidcon/Assets.xcassets/continue_with_google_rd.imageset/android_neutral_rd_ctn.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/ios/Droidcon/Droidcon/Settings/SettingsView.swift b/ios/Droidcon/Droidcon/Settings/SettingsView.swift index 9e8818d3..e69e3afe 100644 --- a/ios/Droidcon/Droidcon/Settings/SettingsView.swift +++ b/ios/Droidcon/Droidcon/Settings/SettingsView.swift @@ -48,8 +48,9 @@ struct SettingsView: View { showingAlert = true } } + .buttonStyle(FilledButtonStyle()) } else { - Button("Settings.SignIn"){ + Button(action: { if viewModel.signIn() { errorMessage = "" showingAlert = false @@ -57,7 +58,9 @@ struct SettingsView: View { errorMessage = "Failed To Sign In" showingAlert = true } - } + }, label: { + Image("continue_with_google_rd") + }) } AboutView(viewModel: viewModel.about) diff --git a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings index 6b3347f6..06801ed1 100644 --- a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings +++ b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings @@ -37,7 +37,6 @@ "Settings.Reminders" = "Enable reminders"; "Settings.Compose" = "Use compose for iOS"; "Settings.About" = "About"; -"Settings.SignIn" = "Sign In"; "Settings.SignOut" = "Sign Out"; "About.Title" = "About"; diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt index b53c0737..30d380e3 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt @@ -1,7 +1,9 @@ package co.touchlab.droidcon.ui.settings +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -18,6 +20,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -25,10 +28,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import co.touchlab.droidcon.ui.theme.Dimensions +import co.touchlab.droidcon.ui.util.LocalImage import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel import org.brightify.hyperdrive.multiplatformx.property.MutableObservableProperty @@ -75,12 +80,16 @@ internal fun SettingsView( Button( onClick = { if (isAuthenticated) viewModel.signOut() else viewModel.signIn() - } + }, + contentPadding = PaddingValues(), ) { if (isAuthenticated) { - Text("Sign Out") + Text("Sign Out", modifier = Modifier.padding(horizontal = 20.dp)) } else { - Text("Sign In") + LocalImage( + imageResourceName = "continue_with_google_rd", + modifier = Modifier + ) } } From 497b8cd18dfd675c5aacecee87022b5935dbeb79 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Fri, 29 Mar 2024 12:57:11 -0400 Subject: [PATCH 09/12] Update SettingsView.kt --- .../kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt index 30d380e3..603fbb6e 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt @@ -1,6 +1,5 @@ package co.touchlab.droidcon.ui.settings -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -20,7 +19,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -28,7 +26,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp From 1267b9b268cd87ee460bd66e5d925beb40015463 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Mon, 1 Apr 2024 15:03:21 -0400 Subject: [PATCH 10/12] Updating style --- .../Droidcon/Settings/SettingsView.swift | 72 ++++++++++++------- .../Droidcon/en.lproj/Localizable.strings | 1 + .../ui/settings/SettingsView.kt | 66 ++++++++++++----- 3 files changed, 94 insertions(+), 45 deletions(-) diff --git a/ios/Droidcon/Droidcon/Settings/SettingsView.swift b/ios/Droidcon/Droidcon/Settings/SettingsView.swift index e69e3afe..d702498a 100644 --- a/ios/Droidcon/Droidcon/Settings/SettingsView.swift +++ b/ios/Droidcon/Droidcon/Settings/SettingsView.swift @@ -19,9 +19,9 @@ struct SettingsView: View { } .padding(.vertical, 8) .padding(.horizontal) - + Divider().padding(.horizontal) - + Toggle(isOn: $viewModel.isRemindersEnabled) { Label("Settings.Reminders", systemImage: "calendar") } @@ -29,40 +29,60 @@ struct SettingsView: View { .padding(.horizontal) Divider().padding(.horizontal) - + Toggle(isOn: $viewModel.useCompose) { Label("Settings.Compose", systemImage: "doc.text.image") } .padding(.vertical, 8) .padding(.horizontal) - + Divider().padding(.horizontal) - - if viewModel.isAuthenticated { - Button("Settings.SignOut"){ - if viewModel.signOut() { - errorMessage = "" - showingAlert = false - } else { - errorMessage = "Failed to Sign Out" - showingAlert = true + HStack{ + Label("Settings.Account", systemImage: "person.fill") + .padding(.horizontal) + Spacer() + if viewModel.isAuthenticated { + + Button(action: { + if viewModel.signOut() { + errorMessage = "" + showingAlert = false + } else { + errorMessage = "Failed to Sign Out" + showingAlert = true + } + }) { + Text("Settings.SignOut") + .frame(height: 10) + .padding() + .foregroundColor(.white) + .background( + RoundedRectangle(cornerRadius: 25) + .fill(Color("NavBar_Background")) + ) } + .padding(.vertical, 8) + .padding(.horizontal) + .buttonStyle(PlainButtonStyle()) + } else { + Button(action: { + if viewModel.signIn() { + errorMessage = "" + showingAlert = false + } else { + errorMessage = "Failed To Sign In" + showingAlert = true + } + }, label: { + Image("continue_with_google_rd") + }) + .padding(.vertical, 8) + .padding(.horizontal) } - .buttonStyle(FilledButtonStyle()) - } else { - Button(action: { - if viewModel.signIn() { - errorMessage = "" - showingAlert = false - } else { - errorMessage = "Failed To Sign In" - showingAlert = true - } - }, label: { - Image("continue_with_google_rd") - }) } + Divider().padding(.horizontal) + AboutView(viewModel: viewModel.about) } } diff --git a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings index 06801ed1..c35b128e 100644 --- a/ios/Droidcon/Droidcon/en.lproj/Localizable.strings +++ b/ios/Droidcon/Droidcon/en.lproj/Localizable.strings @@ -38,6 +38,7 @@ "Settings.Compose" = "Use compose for iOS"; "Settings.About" = "About"; "Settings.SignOut" = "Sign Out"; +"Settings.Account" = "Account"; "About.Title" = "About"; diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt index 603fbb6e..d47329cd 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt @@ -1,9 +1,11 @@ package co.touchlab.droidcon.ui.settings +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -12,13 +14,16 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MailOutline import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -26,8 +31,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import co.touchlab.droidcon.ui.theme.Dimensions import co.touchlab.droidcon.ui.util.LocalImage @@ -74,20 +81,25 @@ internal fun SettingsView( Divider() - Button( - onClick = { - if (isAuthenticated) viewModel.signOut() else viewModel.signIn() - }, - contentPadding = PaddingValues(), + SettingRow( + text = "Account", + image = Icons.Default.Person, ) { if (isAuthenticated) { - Text("Sign Out", modifier = Modifier.padding(horizontal = 20.dp)) + Button( + onClick = { viewModel.signOut() }, + ) { + Text("Sign Out") + } } else { - LocalImage( - imageResourceName = "continue_with_google_rd", - modifier = Modifier - ) + TextButton( + onClick = { viewModel.signIn() }, + contentPadding = PaddingValues(), + ) { + LocalImage(imageResourceName = "continue_with_google_rd") + } } + } Divider() @@ -106,14 +118,34 @@ internal fun IconTextSwitchRow( checked: MutableObservableProperty ) { val isChecked by checked.observeAsState() + SettingRow( + text = text, + image = image, + modifier = Modifier.clickable { checked.value = !checked.value }, + ) { + Switch( + modifier = Modifier.padding(start = Dimensions.Padding.default), + checked = isChecked, + onCheckedChange = { checked.value = it }, + ) + } +} + +@Composable +private fun SettingRow( + text: String, + image: ImageVector, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit, +) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .clickable { checked.value = !checked.value }, + .padding(vertical = Dimensions.Padding.half, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( - modifier = Modifier.padding(Dimensions.Padding.default), + modifier = Modifier.padding(end = Dimensions.Padding.default), imageVector = image, contentDescription = text, ) @@ -121,10 +153,6 @@ internal fun IconTextSwitchRow( modifier = Modifier.weight(1f), text = text, ) - Switch( - modifier = Modifier.padding(vertical = Dimensions.Padding.half, horizontal = 24.dp), - checked = isChecked, - onCheckedChange = { checked.value = it }, - ) + content() } -} +} \ No newline at end of file From b83d5e4342690d0af68b7c63bc2a872f89336006 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Tue, 2 Apr 2024 10:50:54 -0400 Subject: [PATCH 11/12] Adding email to sign in --- .../co.touchlab.droidcon/ui/settings/SettingsView.kt | 3 ++- .../viewmodel/settings/SettingsViewModel.kt | 8 ++++++++ .../droidcon/domain/service/AuthenticationService.kt | 1 + .../domain/service/impl/DefaultAuthenticationService.kt | 5 +++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt index d47329cd..7882d707 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt @@ -48,6 +48,7 @@ internal fun SettingsView( viewModel: SettingsViewModel, ) { val isAuthenticated by viewModel.observeIsAuthenticated.observeAsState() + val email by viewModel.observeEmail.observeAsState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -82,7 +83,7 @@ internal fun SettingsView( Divider() SettingRow( - text = "Account", + text = email ?: "Account", image = Icons.Default.Person, ) { if (isAuthenticated) { diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt index 4a0317ad..d0d4c842 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/viewmodel/settings/SettingsViewModel.kt @@ -57,6 +57,14 @@ class SettingsViewModel( ) val observeIsAuthenticated by observe(::isAuthenticated) + + var email: String? by binding( + authenticationService.email, + mapping = { it }, + set = { } + ) + val observeEmail by observe(::email) + fun signIn() = googleSignInService.performGoogleLogin() fun signOut() = googleSignInService.performGoogleLogout() diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt index 22032f18..d6452110 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/AuthenticationService.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.StateFlow interface AuthenticationService { val isAuthenticated: StateFlow + val email: StateFlow fun setCredentials( id: String, diff --git a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt index fa01f57b..ef6d61b4 100644 --- a/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt +++ b/shared/src/commonMain/kotlin/co/touchlab/droidcon/domain/service/impl/DefaultAuthenticationService.kt @@ -17,6 +17,9 @@ class DefaultAuthenticationService : AuthenticationService, KoinComponent { private val _isAuthenticated = MutableStateFlow(false) override val isAuthenticated: StateFlow = _isAuthenticated + private val _email = MutableStateFlow(null) + override val email: StateFlow = _email + override fun setCredentials( id: String, name: String?, @@ -24,6 +27,7 @@ class DefaultAuthenticationService : AuthenticationService, KoinComponent { pictureUrl: String?, ) { _isAuthenticated.update { true } + _email.update { email } userIdProvider.saveUserContext( UserContext( isAuthenticated = true, @@ -39,6 +43,7 @@ class DefaultAuthenticationService : AuthenticationService, KoinComponent { override fun clearCredentials() { _isAuthenticated.update { false } + _email.update { null } userIdProvider.saveUserContext( UserContext( isAuthenticated = false, From 29f00b5032298313913565f730f8a1d1069f41eb Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Tue, 2 Apr 2024 11:15:12 -0400 Subject: [PATCH 12/12] Adding Account to signed in view --- .../Droidcon/Settings/SettingsView.swift | 13 ++++++++--- .../ui/settings/SettingsView.kt | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/ios/Droidcon/Droidcon/Settings/SettingsView.swift b/ios/Droidcon/Droidcon/Settings/SettingsView.swift index d702498a..d790c1b8 100644 --- a/ios/Droidcon/Droidcon/Settings/SettingsView.swift +++ b/ios/Droidcon/Droidcon/Settings/SettingsView.swift @@ -38,11 +38,18 @@ struct SettingsView: View { Divider().padding(.horizontal) HStack{ - Label("Settings.Account", systemImage: "person.fill") - .padding(.horizontal) + Image(systemName: "person.fill") + .padding(EdgeInsets(top: 20, leading: 16, bottom: 20, trailing: 0)) + VStack(alignment: .leading) { + Text("Settings.Account") + if viewModel.email != nil { + Text(viewModel.email ?? "") + .font(.caption) + .foregroundColor(.gray) + } + } Spacer() if viewModel.isAuthenticated { - Button(action: { if viewModel.signOut() { errorMessage = "" diff --git a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt index 7882d707..cc57b2a9 100644 --- a/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt +++ b/shared-ui/src/commonMain/kotlin/co.touchlab.droidcon/ui/settings/SettingsView.kt @@ -34,9 +34,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import co.touchlab.droidcon.ui.theme.Dimensions +import co.touchlab.droidcon.ui.theme.Typography import co.touchlab.droidcon.ui.util.LocalImage import co.touchlab.droidcon.ui.util.observeAsState import co.touchlab.droidcon.viewmodel.settings.SettingsViewModel @@ -83,7 +86,8 @@ internal fun SettingsView( Divider() SettingRow( - text = email ?: "Account", + text = "Account", + subtext = email, image = Icons.Default.Person, ) { if (isAuthenticated) { @@ -136,6 +140,7 @@ internal fun IconTextSwitchRow( private fun SettingRow( text: String, image: ImageVector, + subtext: String? = null, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) { @@ -150,10 +155,20 @@ private fun SettingRow( imageVector = image, contentDescription = text, ) - Text( + Column( modifier = Modifier.weight(1f), - text = text, - ) + ) { + Text( + text = text, + ) + subtext?.let { + Text( + text = it, + color = Color.Gray, + style = Typography.typography.labelMedium, + ) + } + } content() } } \ No newline at end of file