برنامه نویسی

جریان ورود با Google Identity Services و Firebase

اکثر برنامه های اندروید دارای نوعی احراز هویت هستند. برای این پست، نحوه عملکرد این جریان با استفاده از ورود به سیستم با یک ضربه، Firebase و Amity گوگل را خواهیم دید.

پشته فناوری که ما استفاده خواهیم کرد این است:

  • اسکریپت کاتلین (KTS) برای Gradle ما
  • Jetpack Compose برای رابط کاربری ما
  • معماری MVVM
  • هیلت برای تزریق وابستگی
  • SDK اجتماعی Amity
  • احراز هویت OneTap گوگل
  • احراز هویت Firebase

جریان به شرح زیر است: ابتدا، بررسی می کنیم که آیا جلسه Amity معتبر است یا خیر. اگر اینطور باشد، ما به جریان اصلی خود ادامه خواهیم داد، در غیر این صورت، کاربران خود را به صفحه ورود به سیستم هدایت خواهیم کرد. در آنجا، ابتدا سعی می کنیم کاربران خود را با استفاده از گوگل وارد کنیم. اگر قبلاً به برنامه ما وارد شده باشند، این کار با موفقیت انجام می شود و سپس می توانیم با استفاده از Firebase وارد شوید. اگر نه، ابتدا باید کاربران خود را در برنامه خود ثبت نام کنیم و سپس با Firebase ادامه دهیم.

پیکربندی

خوب، بیایید شروع کنیم! ابتدا برای پیکربندی پروژه خود از راهنمای رسمی استفاده می کنیم. از آنجایی که ما از Groovy استفاده نمی کنیم، وابستگی ها مطابق شکل زیر اضافه می شوند.

plugins {
    ...
    id("com.google.gms.google-services") version "4.3.14"
}

dependencies {
    ...
    implementation("com.google.android.gms:play-services-auth:20.4.0")
}

apply(plugin = "com.google.gms.google-services")
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

*در حالی که باید از فایل libs.versions ارائه شده (کاتالوگ نسخه ها) استفاده کنید، برای خوانایی کد در این پست، نسخه هایی مانند این را اضافه می کنیم.

از آنجایی که اینجا هستیم، وابستگی‌های Amity & Firebase را نیز اضافه می‌کنیم که بعداً به آن نیاز خواهیم داشت. متأسفانه، افزودن وابستگی‌های Firebase در قالب هنوز از طریق دستیار Android Studio و بدون استثنا امکان‌پذیر نیست، بنابراین ما آن‌ها را به صورت دستی اضافه می‌کنیم.

dependencies {   
    ... 

    // Amity
    implementation("com.github.AmityCo.Amity-Social-Cloud-SDK-Android:amity-sdk:5.33.2")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.6.4")

    // Firebase
    implementation(platform("com.google.firebase:firebase-bom:31.1.1"))
    implementation("com.google.firebase:firebase-analytics-ktx")
    implementation("com.google.firebase:firebase-auth-ktx")
    implementation("com.google.android.gms:play-services-auth:20.4.0")
    implementation("com.google.firebase:firebase-firestore-ktx")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آخرین مرحله ما پیکربندی کنسول Firebase است. طبق اسناد رسمی:

برای استفاده از یک ارائه دهنده احراز هویت، باید آن را در کنسول Firebase فعال کنید. به صفحه Sign-in Method در بخش Firebase Authentication بروید تا ورود ایمیل/گذرواژه و سایر ارائه دهندگان هویتی را که می خواهید برای برنامه خود فعال کنید.

البته ما ورود به سیستم Google را فعال خواهیم کرد. سپس به تنظیمات پروژه می رویم و اثر انگشت گواهی SHA خود را اضافه می کنیم.

اکنون ما آماده ایم تا کد ورود خود را اضافه کنیم! ابتدا فایل های مورد نیاز خود را ایجاد می کنیم: MainNavigation ، MainViewModel ، LoginScreen ، LoginViewModel ، AuthRepository.

مشاهده وضعیت احراز هویت

اولین Composable ما که نامیده می شود MainNavigation. برای بررسی مداوم اعتبار جلسه Amity، اینجاست که وضعیت آن را بررسی می کنیم. از آنجایی که می خواهیم وضعیت برنامه ما وضعیت جلسه را منعکس کند، آن را به a نگاشت می کنیم StateFlow.

StateFlow یک جریان قابل مشاهده توسط دارنده حالت است که به روز رسانی های وضعیت فعلی و جدید را به جمع کننده های خود منتشر می کند. وضعیت فعلی value همچنین می توان از طریق ویژگی ارزش آن خواند. در اندروید، StateFlow برای کلاس هایی که نیاز به حفظ حالت قابل مشاهده قابل مشاهده دارند مناسب است.

ما شروع می کنیم و دریافت می کنیم SessionState در AuthRepository، سپس در ما MainViewModel ما جریان خود را به a تبدیل می کنیم StateFlow، و در نهایت در ما MainNavigation ما آن را مشاهده می کنیم.

override val amitySession = flow {
        emit(AmityCoreClient.currentSessionState)
        AmityCoreClient.observeSessionState().asFlow()
    }
val uiState: StateFlow<MainUiState> = authRepository
    .amitySession.map {
        when(it) {
            SessionState.NotLoggedIn,
            SessionState.Establishing -> MainUiState.LoggedOut
            SessionState.Established,
            SessionState.TokenExpired -> MainUiState.LoggedIn
            is SessionState.Terminated -> MainUiState.Banned
        }
    }
    .catch { Error(it) }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainUiState.Loading)LaunchedEffect(lifecycleOwner) {
    viewModel.uiState.collect { state ->
        when(state) {
            MainUiState.Banned -> {} //TODO snackbar
            is MainUiState.Error -> {}  //TODO snackbar
            MainUiState.Loading -> { /* no-op */ }
            MainUiState.LoggedIn -> navController.navigate("main") { popUpTo(0) }
            MainUiState.LoggedOut -> navController.navigate("login") { popUpTo(0) }
        }
    }
}
LaunchedEffect(lifecycleOwner) {
    viewModel.uiState.collect { state ->
        when(state) {
            MainUiState.Banned -> showSnackbar(scope, snackbarHostState, userBannedText)
            is MainUiState.Error -> showSnackbar(scope, snackbarHostState, userErrorText)
            MainUiState.Loading -> { /* no-op */ }
            MainUiState.LoggedIn -> navController.navigate(Route.UsersList.route) { popUpTo(0) }
            MainUiState.LoggedOut -> navController.navigate(Route.Login.route) { popUpTo(0) }
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جالب است، اکنون می توانیم وضعیت کاربران خود را مشاهده کنیم! 🎉🎉🎉

ورود به سیستم Google OneTap

کاربران ما اکنون صفحه ورود ✨ درخشان✨ ما را می بینند که واقعاً در حال حاضر فقط به شرح زیر است

@Composable
fun LoginScreen(
    modifier: Modifier = Modifier,
    navigateToUsers: () -> Unit,
    viewModel: LoginViewModel = hiltViewModel()
) {
    Column(modifier) {
        Button(onClick = { /* TODO */ }) {
            Text(text = stringResource(R.string.login_google_bt))
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

صفحه ورود ما
(صفحه ورود ما)

بالاخره اونقدر هم براق نیست😛

برای شروع، ما به تماس ورود به سیستم خود نیاز داریم AuthRepository. *

ما از یک رابط برای کپسوله کردن خود استفاده می کنیم AuthRepositoryتوابع این به طور خاص برای ایجاد یک ماکت مفید خواهد بود AuthRepository برای آزمایشات ما در آینده

ما signInRequest، SignUpRequest و همچنین Firebase و مشتریان Google در اختیار ما قرار می گیرند AuthRepository پیاده سازی در طول تزریق وابستگی همانطور که در زیر نشان داده شده است.

@Module
@InstallIn(SingletonComponent::class)
class AuthModule {
    private val signInRequest = BeginSignInRequest.builder()
        .setGoogleIdTokenRequestOptions(
            BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                .setSupported(true)
                .setServerClientId(BuildConfig.SERVER_CLIENT_ID)
                // Only show accounts previously used to sign in.
                .setFilterByAuthorizedAccounts(true)
                .build())
        // Automatically sign in when exactly one credential is retrieved.
        .setAutoSelectEnabled(true)
        .build()

    private val signUpRequest = BeginSignInRequest.builder()
        .setGoogleIdTokenRequestOptions(
            BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                .setSupported(true)
                .setServerClientId(BuildConfig.SERVER_CLIENT_ID)
                .setFilterByAuthorizedAccounts(false)
                .build())
        .build()

    @Provides
    @Singleton
    fun provideAuthRepository(@ApplicationContext appContext: Context) : AuthRepository {
        return AuthRepositoryImp(
            Identity.getSignInClient(appContext),
            signInRequest,
            signUpRequest,
            Firebase.auth,
            Firebase.firestore
        )
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بازگشت به جریان ورود به سیستم! همانطور که در بالا ذکر شد، اگر اولین بار است که کاربر ما سعی می کند با این حساب وارد سیستم شود، این یک استثنا ایجاد می کند. برای جلوگیری از نمایش پیام‌های خطای نادرست به کاربر، وقتی در روش ورود به سیستم خود استثناء دریافت می‌کنیم، سعی می‌کنیم ثبت نام کنیم. اگر موفقیت آمیز بود، ادامه می دهیم، اگر نه، خطا را مدیریت می کنیم.

//AuthRepositoryImpl.kt

override suspend fun signInWithGoogle(): OneTapResponse {
    return try {
        val result = oneTapClient.beginSignIn(signInRequest).await()
        ApiResponse.Success(result)
    } catch (e: Exception) {
        ApiResponse.Failure(e)
    }
}

override suspend fun signUpWithGoogle(): OneTapResponse {
    return try {
        val result = oneTapClient.beginSignIn(signUpRequest).await()
        ApiResponse.Success(result)
    } catch (e: Exception) {
        ApiResponse.Failure(e)
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//LoginViewModel

suspend fun googleSignIn(launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>) {
    when (val oneTapResponse: ApiResponse<BeginSignInResult> =
        authRepository.signInWithGoogle()) {
        is ApiResponse.Success -> {
            val result = oneTapResponse.data!!
            val intent = IntentSenderRequest.Builder(result.pendingIntent.intentSender).build()
            launcher.launch(intent)
        }
        is ApiResponse.Loading -> { /* no-op */ }
        else -> {
            // No saved credentials found. Launch the One Tap sign-up flow
            googleSignUp(launcher)
        }
    }
}

private suspend fun googleSignUp(launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>) {
    when(val oneTapResponse: ApiResponse<BeginSignInResult> = authRepository.signUpWithGoogle())  {
        is ApiResponse.Success -> {
            val result = oneTapResponse.data!!
            val intent = IntentSenderRequest.Builder(result.pendingIntent.intentSender).build()
            launcher.launch(intent)
        }
        is ApiResponse.Loading -> { /* no-op */ }
        else -> handleSignUpError()
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر به جای Compose از Views استفاده می کردیم، نتیجه intent را در خود مدیریت می کردیم onActivityForResult روش. در عوض، ما از ManagedActivityResultLauncher در خود استفاده خواهیم کرد LoginScreen. اگر راه‌انداز ما با نتیجه مثبت بازگردد، شناسه کاربر خود را دریافت می‌کنیم و وارد Firebase می‌شویم.

@Composable
fun LoginScreen(
    modifier: Modifier = Modifier,
    viewModel: LoginViewModel = hiltViewModel()
) {

    val launcher = rememberFirebaseAuthLauncher(viewModel = viewModel)
    val scope = rememberCoroutineScope()

    Column(modifier) {
        Button(onClick = { scope.launch { viewModel.googleSignIn(launcher) } }) {
            Text(text = stringResource(R.string.login_google_bt))
        }
    }
}

@Composable
private fun rememberFirebaseAuthLauncher(viewModel: LoginViewModel): ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> {
    val scope = rememberCoroutineScope()
    val context = LocalContext.current

    return rememberLauncherForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
        result.data.let {
            try {
                scope.launch {
                    val credentials =
                        Identity.getSignInClient(context).getSignInCredentialFromIntent(result.data)
                    val googleIdToken = credentials.googleIdToken
                    val googleCredentials = getCredential(googleIdToken, null)
                    // TO-DO sign-in to Firebase
                }
            } catch (e: Exception) {
                Log.e("LOG", e.message.toString())
                // TO-DO show error to user.
            }
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ورود به سیستم Firebase و Amity

اوف، تقریبا تمام شد!!!

بازگشت به ما AuthRepository ابتدا تماس های خود را برای Firebase و Amity اضافه می کنیم.

override suspend fun firebaseSignInWithGoogle(googleCredential: AuthCredential): SignInToFirebaseResponse {
    return try {
        val authResult = auth.signInWithCredential(googleCredential).await()
        val isNewUser = authResult.additionalUserInfo?.isNewUser ?: false
        if (isNewUser) {
            addUserToFirestore()
        }
        ApiResponse.Success(true)
    } catch (e: Exception) {
        ApiResponse.Failure(e)
    }
}

override suspend fun amityLogIn() = login(userId = currentUserId)
    .build()
    .submit()
    .toSuspend()
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر کاربر جدید است، ما همچنین آن را به Firestore اضافه می کنیم که برای برنامه چت ما لازم است، اما این اجباری نیست. سپس در ما LoginViewModel ما توابع تعلیق خود را می نامیم:

fun firebaseSignIn(authCredential: AuthCredential) {
    viewModelScope.launch {
        authRepository.firebaseSignInWithGoogle(authCredential)
            .also { authRepository.amityLogIn() }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و TO-DO ما را با این فراخوان در لانچر ما جایگزین کنید.

در حال دور شدن از ورود ما

… یا مشاهده وضعیت فرآیند ورود به سیستم. آخرین قدم ما، قول می دهم!

از خودمان شروع می کنیم AuthRepository دوباره، و تماس نهایی خود را اضافه کنید.

override val isSignedIn = callbackFlow {
    val authStateListener = FirebaseAuth.AuthStateListener { auth ->
        trySend(auth.currentUser != null)
    }
    auth.addAuthStateListener(authStateListener)
    awaitClose {
        auth.removeAuthStateListener(authStateListener)
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از آنجایی که این یک callback است، به این معنی است که هر بار که مقدار auth.currentUser تغییر کند، به ما اطلاع داده می شود. سپس در ما LoginViewModel ما آن را به یک وضعیت UI نگاشت می کنیم و البته آن را به حالت خود مشاهده می کنیم LoginScreen.

// LoginViewModel
val uiState: StateFlow<LoginUiState> = authRepository
    .isSignedIn.map {if (it) LoginUiState.Authorized else LoginUiState.Unauthorized()}
    .catch { LoginUiState.Unauthorized(it) }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), LoginUiState.Loading)
// LoginScreen
val authStatus by produceState<LoginUiState>(
    initialValue = LoginUiState.Unauthorized(),
    key1 = lifecycle,
    key2 = viewModel.uiState
) {
    lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
        viewModel.uiState.collect { value = it }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مسیرهای شاد و ... نه آن شادمان. رسیدگی به خطاها در هنگام ورود به سیستم بسیار مهم است!
(مسیرهای شاد و … نه چندان خوشحال ما. رسیدگی به خطاها در هنگام ورود به سیستم بسیار مهم است!)

اووووووو این همه مردمی!

اگر به نحوه استفاده از این کد برای یک برنامه چت علاقه مند هستید، حتما پست بعدی وبلاگ ما را بررسی کنید!

کد کامل را می توانید در اینجا پیدا کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

همچنین ببینید
بستن
دکمه بازگشت به بالا