JetpackComposeでFirebaseAuth(Google認証)

# Googleアカウント認証＋FirebaseAuth with JetpackCompose
OneTapAuthじゃない方（今までのやり方）のGoogle認証+FirebaseAuth認証を実装していきます。
[OneTap認証](https://qiita.com/jozuko_dev/items/706fc6c89e6982bfd7e2)で実装したクラスを拡張していきます。

## 関連記事
- [JetpackComposeでFirebaseAuth(パスワード認証)](https://qiita.com/jozuko_dev/items/f8106009ad4571740d1f)
- [JetpackComposeでFirebaseAuth(GoogleOneTap認証)](https://qiita.com/jozuko_dev/items/706fc6c89e6982bfd7e2)
- [JetpackComposeでFirebaseAuth(メールリンク認証)](https://qiita.com/jozuko_dev/items/bf343ba3f2b37a0184be)

- [GoogleDevelopersのページ](https://developers.google.com/identity/sign-in/android/sign-in?hl=ja)

## エンドポイント
### Googleアカウントのエンドポイント
| エンドポイント |   |
|:--|:--|
| googleSignInClient.signInIntent | Googleアカウント認証画面（アカウント一覧が出る画面）を表示します |
| GoogleSignIn.getSignedInAccountFromIntent | 認証画面の戻り値をGoogleAccountに変換します |

### FirebaseAuthのエンドポイント
| エンドポイント |   |
|:--|:--|
| GoogleAuthProvider.getCredential(idToken, accessToken) | GoogleアカウントのidTokenからFirebaseAuthのCredentialに取得します |
| Firebase.auth.signInWithCredential(firebaseCredential) | FirebaseAuthのCredentialでサインイン処理を行います |

### 処理の流れ
流れはフローチャートにしました。OneTap認証とほぼ同じ流れになります。
![GoogleAuthLegacy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/d16c94a9-430f-b059-8068-21cc13285c8b.png)

## build.gradle
```diff_gradle:app/build.gradle
android {
+    def properties = new Properties()
+    properties.load(project.rootProject.file('local.properties').newDataInputStream())
+    def googleOauthServerClientId = properties.getProperty('google_oauth_server_client_id')

+    defaultConfig {
+        buildConfigField("String", "GOOGLE_OAUTH_SERVER_CLIENT_ID", "${googleOauthServerClientId}")
+    }
}

+    // for GoogleSignIn
+    implementation 'com.google.android.gms:play-services-auth:20.6.0'
```
> def googleOauthServerClientId = properties.getProperty('google_oauth_server_client_id')
> buildConfigField("String", "GOOGLE_OAUTH_SERVER_CLIENT_ID", "${googleOauthServerClientId}")

GoogleアカウントとFirebaseAuthを紐付けるのに、Googleアカウントの<b>IdToken</b>が必要です。
このIdTokenを取得するには、GoogleCloudConsole＞対象アプリ＞認証情報＞OAuth 2.0 クライアント IDのうち、<b>Web clientのクライアントID</b>が必要になります。
秘密の情報にしておいた方が無難かと思いますので、クライアントIDは「local.properties」に保存しておいて、ｋｏｔｌｉｎからはBuildConfig経由で取得できるようにするための設定になります。

> implementation 'com.google.android.gms:play-services-auth:20.6.0'

Googleアカウントの認証には、play-services-authが必要です。

## DIの実装
Googleアカウントの認証に使用するGoogleSignInなどのインスタンス生成を追記します。
GoogleSignInOptions.Builder().requestIdToken(oauth_web_client_id)を呼び出すと、GoogleアカウントのIdTokenを取得できるようになります。

```diff_kotlin:FirebaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object FirebaseModule {
    @Provides
    fun auth(): FirebaseAuth = Firebase.auth

    /** Google認証(OneTap)用の認証画面構築クラス */
    @Provides
    fun provideBeginSignInRequest(): BeginSignInRequest =
        BeginSignInRequest.builder()
            .setGoogleIdTokenRequestOptions(
                BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                    .setSupported(true)
                    .setServerClientId(BuildConfig.GOOGLE_OAUTH_SERVER_CLIENT_ID)
                    .setFilterByAuthorizedAccounts(true)
                    .build()
            )
            .build()

    /** Google認証(OneTap)用の認証クライアント */
    @Provides
    fun provideSignInClient(
        @ApplicationContext context: Context,
    ): SignInClient = Identity.getSignInClient(context)

+    /** Google認証（Legacy）用のオプション定義 */
+    @Provides
+    fun provideGoogleSignInOptions(): GoogleSignInOptions =
+        GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+            .requestIdToken(BuildConfig.GOOGLE_OAUTH_SERVER_CLIENT_ID)
+            .build()

+    /** Google認証（Legacy）用の認証クライアント */
+    @Provides
+    fun provideGoogleSignInClient(
+        @ApplicationContext context: Context,
+        googleSignInOptions: GoogleSignInOptions,
+    ): GoogleSignInClient = GoogleSignIn.getClient(context, googleSignInOptions)
}
```


## インフラ層の実装
Googleアカウント認証画面を起動するためのIntentを生成する関数と、
認証画面からの戻り値を使用してFirebaseAuthでsigninする関数の2つを追加します。
処理の流れは、前述のフローチャートの通りです。

```diff_kotlin:AccountRepository.kt
+    fun requestGoogleLegacyAuth(): Intent
+    suspend fun signinGoogleLegacy(resultData: Intent)
```
```diff_kotlin:AccountRepositoryImpl.kt
class AccountRepositoryImpl @Inject constructor(
    private val auth: FirebaseAuth,
    private val signInRequest: BeginSignInRequest,
    private val signInClient: SignInClient,
+    private val googleSignInClient: GoogleSignInClient,
) : AccountRepository {
+    override fun requestGoogleLegacyAuth(): Intent {
+        return googleSignInClient.signInIntent
+    }

+    override suspend fun signinGoogleLegacy(resultData: Intent) {
+        val googleSignInAccount: GoogleSignInAccount = GoogleSignIn.getSignedInAccountFromIntent(resultData).await()
+        val firebaseCredential = GoogleAuthProvider.getCredential(googleSignInAccount.idToken, null)
+        auth.signInWithCredential(firebaseCredential).await()
+    }
}
```

## UseCaseの実装
```diff_kotlin:GoogleLegacySigninCase.kt
class GoogleLegacySigninCase @Inject constructor(
    private val accountRepository: AccountRepository,
) {
    // ①signin関数
    fun signin(launcher: ActivityResultLauncher<Intent>) {
        val intent = accountRepository.requestGoogleLegacyAuth()
        launcher.launch(intent)
    }

    // ②onResultSignin関数
    suspend fun onResultSignin(result: ActivityResult) {
        if (result.resultCode != Activity.RESULT_OK) {
            Log.d("GoogleLegacySigninCase", "onResultSignin ${SigninGoogleLegacyError(result.data).resultMessage}")
            return
        }

        val resultData = result.data ?: let {
            Log.d("GoogleLegacySigninCase", "onResultSignin result.data is null")
            return
        }

        accountRepository.signinGoogleLegacy(resultData)
    }
}
```
①signin関数
Google認証用のアカウント一覧画面を起動します。

②onResultSignin関数
Google認証用のアカウント一覧画面の戻り値を処理します。
result.resultCodeがActivity.RESULT_OKの場合は、Signin処理を行っています

## ViewModelの実装
これはUseCaseを呼び出すだけなので説明は割愛です。
```diff_kotlin:SigninViewModel.kt
@HiltViewModel
class SigninViewModel @Inject constructor(
    private val signinUsecase: SigninUsecase,
    private val signupUsecase: SignupUsecase,
    private val signOutUsecase: SignOutUsecase,
    private val googleOneTapSigninCase: GoogleOneTapSigninCase,
+    private val googleLegacySigninCase: GoogleLegacySigninCase,
    accountRepository: AccountRepository,
) : ViewModel() {
    /* ...いろいろ実装... */

+    fun onClickSignInWithGoogleLegacy(launcher: ActivityResultLauncher<Intent>) {
+        try {
+            googleLegacySigninCase.signin(launcher)
+        } catch (e: Exception) {
+            val message = e.toSnackbarMessage()
+            SnackbarManager.showMessage(e.toSnackbarMessage())
+        }
+    }

+    fun onResultSignInWithGoogleLegacy(result: ActivityResult) {
+        viewModelScope.launch(
+            context = CoroutineExceptionHandler { _, throwable ->
+                val message = throwable.toSnackbarMessage()
+                SnackbarManager.showMessage(throwable.toSnackbarMessage())
+            },
+            block = {
+                googleLegacySigninCase.onResultSignin(result)
+            },
+        )
+    }
```

## Screenの実装

```diff_kotlin:SigninScreen.kt
@Composable
+ private fun GoogleLegacySigninButton(viewModel: SigninViewModel) {
+    // ① Google認証画面ランチャの生成
+    val startForResult = rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.StartActivityForResult(),
+        onResult = viewModel::onResultSignInWithGoogleLegacy,
+    )
+
+    BasicButton(
+        text = R.string.sign_in_with_google_legacy,
+        modifier = Modifier.basicButton(),
+    ) {
+        // ② ボタンクリック
+        viewModel.onClickSignInWithGoogleLegacy(startForResult)
+    }
+ }
```
① Google認証画面ランチャの生成
rememberLauncherForActivityResultを使って、ActivityResultLauncherを作成します。
ActivityResultLauncherのonResultに、「viewModel.onResultSignInWithGoogleLegacy」を指定して、結果処理を行います。

② ボタンクリック
Googleアカウント認証画面を起動するトリガーです

## いざ実行
[コードはこちら](https://github.com/jozuko/ComposeFirebaseSample/tree/3e51dd0a1eaf6295891e8190958ab51f4fb2c6c7)です。
![google_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/209b851d-1477-e635-a869-bf19b1d30522.png)![google_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/ce266c85-2f13-f9e2-c4cf-ed66141a8e7a.png)![google_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/7fca0085-4f2c-c906-8bba-13c323223f72.png)