JetpackComposeでFirebaseAuth(GoogleOneTap認証)

# GoogleアカウントのOneTap＋FirebaseAuth with JetpackCompose
JetpackComposeで、GoogleアカウントのOneTap＋FirebaseAuthを実装していきます。

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

## エンドポイント
### GoogleアカウントのOneTap
| エンドポイント | 　 |
|:--|:--|
| SignInClient.beginSignin(BeginSignInRequest) | OneTap認証画面を表示します |
| signInClient.getSignInCredentialFromIntent(resultData: Intent) | OneTap認証画面から戻ってきたときのResultDataをGoogleアカウントのCredentialに変換します |

### FirebaseAuth
| エンドポイント | 　 |
|:--|:--|
| GoogleAuthProvider.getCredential(googleCredential) | GoogleアカウントのCredentialをFirebaseAuthのCredentialに変換します |
| Firebase.auth.signInWithCredential(firebaseCredential) | FirebaseAuthのCredentialでサインイン処理を行います |

### 処理の流れ
流れはフローチャートにしました。
複雑ではないのですが、OneTap認証画面の起動と結果処理のところに、JetpackComposeのクセを感じます。
![Frame 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/3ddc6b3d-8d03-b12a-edab-8cc54c44ce09.png)

## dependencies
```diff_gradle:app/build.gradle
android {
    buildFeatures {
        buildConfig = true
    }

+    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}")
    }
}


dependencies {
+    // 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アカウントでの認証には、GoogleCloudConsole＞対象アプリ＞認証情報＞OAuth 2.0 クライアント IDのうち、Web clientのクライアントIDが必要です。
Firebaseコンソールでアプリを作っていると、勝手にできていると思いますので、GoogleCloudConsoleから拾ってきます。
秘密の情報にしておいた方が無難かと思いますので、クライアントIDは「local.properties」に保存しておいて、BuildConfig経由で取得しようと思います。
そのための設定が引用部分になります。

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

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


## インフラ層の実装
[前回](https://qiita.com/jozuko_dev/items/f8106009ad4571740d1f)作成したクラスを拡張していきます。
```diff_kotlin:AccountRepositoryImpl.kt
+    override suspend fun requestGoogleOneTapAuth(signInClient: SignInClient): PendingIntent {
+        val result = signInClient.beginSignIn(signInRequest).await()
+        return result.pendingIntent
+    }

+    override suspend fun signinGoogleOneTapAuth(signInClient: SignInClient, resultData: Intent) {
+        val googleCredential = signInClient.getSignInCredentialFromIntent(resultData)
+        val firebaseCredential = GoogleAuthProvider.getCredential(googleCredential.googleIdToken, null)
+        auth.signInWithCredential(firebaseCredential).await()
+    }
```

| 関数 | 役割 |
|:--|:--|
| requestGoogleOneTapAuth | OneTap認証画面を起動するためのPendingIntentを取得する |
| signinGoogleOneTapAuth | OneTap認証画面からの戻り値を解析し、FirebaseAuthにSignInする |

ここの説明は、処理の流れと同じですので割愛します。

## UseCase層の実装

:::note info
23/7/28追記
引数のActivityをやめて、ApplicationContextをDIするように修正しました
:::

```diff_kotlin:GoogleSigninCase.kt
class GoogleSigninCase @Inject constructor(
    @ApplicationContext private val context: Context,
    private val accountRepository: AccountRepository,
) {
    suspend fun signinOneTap(launcher: ActivityResultLauncher<IntentSenderRequest>) {
        val signInClient = Identity.getSignInClient(context)
        val pendingIntent = accountRepository.requestGoogleOneTapAuth(signInClient)
        val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent).build()
        launcher.launch(intentSenderRequest)
    }

    suspend fun onResultSigninOneTap(result: ActivityResult) {
        if (result.resultCode != Activity.RESULT_OK) {
            Log.d("SigninViewModel", "onResultGoogleSignIn ${SigninOneTapError(result.data).resultMessage}")
            return
        }

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

        val signInClient = Identity.getSignInClient(context)
        accountRepository.signinGoogleOneTapAuth(signInClient, resultData)
    }
}
```
（ViewModelとUseCaseの責務の範囲に関していろいろ迷いました...）
| 関数 | 役割 |
|:--|:--|
| signinOneTap | OneTap認証画面の起動 |
| onResultSigninOneTap | OneTap認証画面の結果解析とSignIn処理 |

### signinOneTap
~~Identity.getSignInClient(activity)で、@ComposableでLocalContext.currentの値をActivityにキャストしたものを引数に、SignInClientを取得します。~~
Identity.getSignInClient(context)で、ApplicationContextをDIしたものを引数に、SignInClientを取得します。
取得したSignInClientを使用して、OneTap認証画面を起動するためのPendingIntentを取得します。
受け取ったPendingIntentを持つIntentSenderRequestを生成して、ActivityResultLauncherで画面起動処理を行うと、OneTap認証画面が起動します。

### onResultSigninOneTap
OneTap認証画面が閉じたときに、ActivityResultが返ってきますので、ActivityResult.resultCodeがOKの場合は、SignIn処理を行います。

## ViewModel層の実装
```diff_kotlin:SigninViewModel.kt
+    fun onClickSignInWithGoogleOneTap(launcher: ActivityResultLauncher<IntentSenderRequest>) {
+        viewModelScope.launch(
+            context = CoroutineExceptionHandler { _, throwable ->
+                val message = throwable.toSnackbarMessage()
+                SnackbarManager.showMessage(throwable.toSnackbarMessage())
+            },
+            block = {
+                googleSigninCase.signinOneTap(launcher)
+            },
+        )
+    }

+    fun onResultSignInWithGoogleOneTap(result: ActivityResult) {
+        viewModelScope.launch(
+            context = CoroutineExceptionHandler { _, throwable ->
+                val message = throwable.toSnackbarMessage()
+                SnackbarManager.showMessage(throwable.toSnackbarMessage())
+            },
+            block = {
+                googleSigninCase.onResultSigninOneTap(result)
+            },
+        )
+    }
```
これはUseCaseを呼び出すだけなので説明は割愛です。

## Screenの実装
```diff_kotlin:SigninScreen.kt
/* ...いろいろ実装...*/

+val startForResultGoogleSignin = rememberLauncherForActivityResult(
+    contract = ActivityResultContracts.StartIntentSenderForResult(),
+    onResult = { result -> viewModel.onResultSignInWithGoogleOneTap(result) },
+)

+BasicButton(
+    text = R.string.sign_in_with_google,
+    modifier = Modifier.basicButton(),
+    action = { viewModel.onClickSignInWithGoogleOneTap(startForResultGoogleSignin) },
+)
```
rememberLauncherForActivityResultを使って、ActivityResultLauncherを作成します。
ActivityResultLauncherのonResultに、「viewModel.onResultSignInWithGoogleOneTap」を指定して、結果処理を行います。

BasicButtonのactionが、onClickの時に動く処理です。
「viewModel.onClickSignInWithGoogleOneTap」を指定して、OneTop認証画面の起動を行います。

:::note info
テストでキャンセルしすぎた場合は、「*#*#66382723#*#*」に電話をかけましょう。制限がオフになります。
オンに戻すときは同じ番号にもう一度電話をかけましょう
https://developers.google.com/identity/one-tap/android/get-saved-credentials?hl=ja#disable-one-tap
:::

## いざ実行
~~[コードはこちら](https://github.com/jozuko/ComposeFirebaseSample/tree/c09a54ae9f76197fe467e06a23f74fa514e25f68)です。~~
<b>23/7/28追記</b>
[Identity.getSignInClientの引数をActivityからApplicationContextに変更したソースはこちらです。](https://github.com/jozuko/ComposeFirebaseSample/commit/5c4f092ca63e191f62c331dd65f980e883e1829b)

ちょっと実名が出ちゃうので、動画はなしで。。。
![onetap_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/361a6e62-8ba8-a282-647e-99ad4a80e189.png)![onetap_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/3b31036d-10e0-ae73-1e6f-a470e66d12e0.png)![onetap_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/4f7b7c78-8d86-6692-cad4-d10d50e04a4d.png)![onetap_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/34422b49-dd68-0f6b-32bd-f1a76dc5a436.png)