diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt index 9ae6feaf6..db72ef5b7 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt @@ -9,3 +9,11 @@ object DatastoreKeys { const val omnivoreAuthToken = "omnivoreAuthToken" const val omnivoreAuthCookieString = "omnivoreAuthCookieString" } + +object AppleConstants { + const val clientId = "app.omnivore" + const val redirectURI = "https://api-demo.omnivore.app/api/auth/vercel/apple-redirect" + const val scope = "name%20email" + const val authUrl = "https://appleid.apple.com/auth/authorize" + const val tokenUrl = "https://appleid.apple.com/auth/token" +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt new file mode 100644 index 000000000..5bfd56466 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt @@ -0,0 +1,197 @@ +package app.omnivore.omnivore.ui.auth + +import android.annotation.SuppressLint +import android.util.Log +import android.view.ViewGroup +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.TopAppBar +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Dialog +import app.omnivore.omnivore.AppleConstants +import app.omnivore.omnivore.R +import java.util.* + +@Composable +fun AppleAuthButton(viewModel: LoginViewModel) { + val showDialog = remember { mutableStateOf(false) } + + LoadingButtonWithIcon( + text = "Continue with Apple", + loadingText = "Signing in...", + isLoading = viewModel.isLoading, + icon = painterResource(id = R.drawable.ic_logo_apple), + modifier = Modifier.padding(vertical = 6.dp), + onClick = { showDialog.value = true } + ) + + if (showDialog.value) { + AppleAuthDialog(onDismiss = { + showDialog.value = false + Log.i("Apple payload: ", it ?: "null") + }) + } +} + +@Composable +fun AppleAuthDialog(onDismiss: (String?) -> Unit) { + Dialog(onDismissRequest = { onDismiss(null) }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White + ) { + AppleAuthWebContainerView(onDismiss) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun AppleAuthWebContainerView(onDismiss: (String?) -> Unit) { + Scaffold( + topBar = { TopAppBar(title = { Text("WebView", color = Color.White) }, backgroundColor = Color(0xff0f9d58)) }, + content = { AppleAuthWebView(onDismiss) } + ) +} + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun AppleAuthWebView(onDismiss: (String?) -> Unit) { + val url = AppleConstants.authUrl + + "?client_id=" + + AppleConstants.clientId + + "&redirect_uri=" + + AppleConstants.redirectURI + + "&response_type=code%20id_token&scope=" + + AppleConstants.scope + + "&response_mode=form_post&state=android:login" + +// clientId="app.omnivore" +// scope="name email" +// state="web:login" +// redirectURI={appleAuthRedirectURI} +// responseMode="form_post" +// responseType="code id_token" +// designProp={{ +// color: 'black', + + // Adding a WebView inside AndroidView + // with layout as full screen + AndroidView(factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) +// webViewClient = WebViewClient() + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + Log.i("Apple payload one: ", request?.url.toString() ?: "null") + if (request?.url.toString().startsWith(AppleConstants.redirectURI)) { +// handleUrl(request?.url.toString()) + onDismiss(request?.url.toString()) + // Close the dialog after getting the authorization code + if (request?.url.toString().contains("success=")) { + onDismiss(null) + } + return true + } + return true + } + } + settings.javaScriptEnabled = true + loadUrl(url) + } + }, update = { + it.loadUrl(url) + }) +} + +//// A client to know about WebView navigation +//// For API 21 and above +//class AppleWebViewClient : WebViewClient() { +// @TargetApi(Build.VERSION_CODES.LOLLIPOP) +// override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { +// if (request?.url.toString().startsWith(AppleConstants.redirectURI)) { +// handleUrl(request?.url.toString()) +// // Close the dialog after getting the authorization code +// if (request.url.toString().contains("success=")) { +//// appledialog.dismiss() +// } +// return true +// } +// return true +// } + +// // For API 19 and below +// override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { +// if (url.startsWith(AppleConstants.redirectURI)) { +// handleUrl(url) +// // Close the dialog after getting the authorization code +// if (url.contains("success=")) { +//// appledialog.dismiss() +// } +// return true +// } +// return false +// } + +// @SuppressLint("ClickableViewAccessibility") +// override fun onPageFinished(view: WebView?, url: String?) { +// super.onPageFinished(view, url) +// // retrieve display dimensions +// val displayRectangle = Rect() +// val window = this@AppleWebViewClient.w +// window.decorView.getWindowVisibleDisplayFrame(displayRectangle) +// // Set height of the Dialog to 90% of the screen +// val layoutParams = view?.layoutParams +// layoutParams?.height = (displayRectangle.height() * 0.9f).toInt() +// view?.layoutParams = layoutParams +// } + +// // Check WebView url for access token code or error +// @SuppressLint("LongLogTag") +// private fun handleUrl(url: String) { +// val uri = Uri.parse(url) +// val success = uri.getQueryParameter("success") +// if (success == "true") { +// // Get the Authorization Code from the URL +//// appleAuthCode = uri.getQueryParameter("code") ?: "" +//// Log.i("Apple Code: ", appleAuthCode) +// // Get the Client Secret from the URL +//// appleClientSecret = uri.getQueryParameter("client_secret") ?: "" +//// Log.i("Apple Client Secret: ", appleClientSecret) +// //Check if user gave access to the app for the first time by checking if the url contains their email +// if (url.contains("email")) { +// //Get user's First Name +// val firstName = uri.getQueryParameter("first_name") +// Log.i("Apple User First Name: ", firstName ?: "") +// //Get user's Middle Name +// val middleName = uri.getQueryParameter("middle_name") +// Log.i("Apple User Middle Name: ", middleName ?: "") +// //Get user's Last Name +// val lastName = uri.getQueryParameter("last_name") +// Log.i("Apple User Last Name: ", lastName ?: "") +// //Get user's email +// val email = uri.getQueryParameter("email") +// Log.i("Apple User Email: ", email ?: "Not exists") +// } +// // Exchange the Auth Code for Access Token +//// requestForAccessToken(appleAuthCode, appleClientSecret) +// } else if (success == "false") { +// Log.e("ERROR", "We couldn't get the Auth Code") +// } +// } +//} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt index 524e2941f..874215cff 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt @@ -1,10 +1,11 @@ package app.omnivore.omnivore.ui.auth import android.app.Activity +import android.content.ContentValues +import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -13,19 +14,17 @@ import app.omnivore.omnivore.R import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.Task - -@OptIn(ExperimentalMaterialApi::class) @Composable fun GoogleAuthButton(viewModel: LoginViewModel) { val context = LocalContext.current val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestEmail() .requestIdToken(stringResource(R.string.gcp_id)) - .requestId() - .requestProfile() +// .requestServerAuthCode(stringResource(R.string.gcp_id), true) .build() val startForResult = @@ -48,7 +47,18 @@ fun GoogleAuthButton(viewModel: LoginViewModel) { icon = painterResource(id = R.drawable.ic_logo_google), onClick = { val googleSignIn = GoogleSignIn.getClient(context, signInOptions) - startForResult.launch(googleSignIn.signInIntent) + + googleSignIn.silentSignIn() + .addOnCompleteListener { task -> + if (task.isSuccessful) { + viewModel.handleGoogleAuthTask(task) + } else { + startForResult.launch(googleSignIn.signInIntent) + } + } + .addOnFailureListener { + startForResult.launch(googleSignIn.signInIntent) + } } ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt index 779fe3b4a..77e3b4533 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt @@ -7,14 +7,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import app.omnivore.omnivore.* +import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.common.api.ApiException +import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.Task import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import javax.inject.Inject + enum class RegistrationState { AuthProviderButtons, EmailSignIn @@ -82,8 +85,12 @@ class LoginViewModel @Inject constructor( } fun handleGoogleAuthTask(task: Task) { - val googleIdToken = task?.getResult(ApiException::class.java).idToken - Log.d(ContentValues.TAG, "Google Result?: $googleIdToken") + val result = task?.getResult(ApiException::class.java) + Log.d(ContentValues.TAG, "server auth code?: ${result.serverAuthCode}") + Log.d(ContentValues.TAG, "is Expired?: ${result.isExpired}") + Log.d(ContentValues.TAG, "granted Scopes?: ${result.grantedScopes}") + val googleIdToken = result.idToken + Log.d(ContentValues.TAG, "Google id token?: $googleIdToken") // TODO: submit id token to backend // If token is missing then set the error message diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt index e6b9a997c..ccd6b4162 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -24,9 +23,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController import app.omnivore.omnivore.R -import app.omnivore.omnivore.Routes import com.google.android.gms.common.GoogleApiAvailability import kotlinx.coroutines.launch @@ -140,14 +137,7 @@ fun AuthProviderView( GoogleAuthButton(viewModel) } - // Disable apple auth for now -// LoadingButtonWithIcon( -// text = "Continue with Apple", -// loadingText = "Signing in...", -// icon = painterResource(id = R.drawable.ic_logo_apple), -// modifier = Modifier.padding(vertical = 6.dp), -// onClick = {} -// ) + // AppleAuthButton(viewModel) ClickableText( text = AnnotatedString("Continue with Email"), diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index 73ef5f423..366c15319 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ Omnivore 590528454690-mpmbvcb4c3ifmnojmrultt37eo8f64at.apps.googleusercontent.com + Never miss a great read Learn More Save articles and read them later in our distraction-free reader.