mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1128 from omnivore-app/feature/android-apple-auth
Apple auth - Android
This commit is contained in:
commit
f0eda57a22
6 changed files with 233 additions and 20 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue