Merge pull request #1128 from omnivore-app/feature/android-apple-auth

Apple auth - Android
This commit is contained in:
Satindar Dhillon 2022-08-25 06:45:57 -07:00 committed by GitHub
commit f0eda57a22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 233 additions and 20 deletions

View file

@ -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"
}

View file

@ -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")
// }
// }
//}

View file

@ -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)
}
}
)
}

View file

@ -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<GoogleSignInAccount>) {
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

View file

@ -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"),

View file

@ -1,6 +1,7 @@
<resources>
<string name="app_name">Omnivore</string>
<string name="gcp_id">590528454690-mpmbvcb4c3ifmnojmrultt37eo8f64at.apps.googleusercontent.com</string>
<!-- <string name="web_client_id">590528454690-g535h6sd708andjrvrmqgaj1sergtqi9.apps.googleusercontent.com</string>-->
<string name="welcome_title">Never miss a great read</string>
<string name="learn_more">Learn More</string>
<string name="welcome_subtitle">Save articles and read them later in our distraction-free reader.</string>