Merge pull request #1150 from omnivore-app/feature/android-apple-token-handling

Android Apple token handling
This commit is contained in:
Satindar Dhillon 2022-08-28 22:51:22 -07:00 committed by GitHub
commit fe967e9250
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 296 deletions

View file

@ -29,7 +29,7 @@
</activity>
<activity
android:name=".ui.save.NewFlowActivity"
android:name=".ui.save.SaveSheetActivity"
android:exported="true"
android:theme="@style/Theme.AppCompat.Translucent">
<intent-filter>

View file

@ -12,8 +12,7 @@ object DatastoreKeys {
object AppleConstants {
const val clientId = "app.omnivore"
const val redirectURI = "https://api-demo.omnivore.app/api/auth/vercel/apple-redirect"
const val redirectURI = "https%3A%2F%2Fapi-demo.omnivore.app%2Fapi%2Fmobile-auth%2Fandroid-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

@ -1,11 +1,11 @@
package app.omnivore.omnivore.ui.auth
import android.annotation.SuppressLint
import android.content.ContentValues
import android.net.Uri
import android.util.Log
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.TopAppBar
@ -37,9 +37,11 @@ fun AppleAuthButton(viewModel: LoginViewModel) {
)
if (showDialog.value) {
AppleAuthDialog(onDismiss = {
AppleAuthDialog(onDismiss = { token ->
if (token != null ) {
viewModel.handleAppleToken(token)
}
showDialog.value = false
Log.i("Apple payload: ", it ?: "null")
})
}
}
@ -51,41 +53,21 @@ fun AppleAuthDialog(onDismiss: (String?) -> Unit) {
shape = RoundedCornerShape(16.dp),
color = Color.White
) {
AppleAuthWebContainerView(onDismiss)
AppleAuthWebView(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',
"?client_id=" + AppleConstants.clientId +
"&redirect_uri=" + AppleConstants.redirectURI +
"&response_type=code%20id_token" +
"&scope=" + AppleConstants.scope +
"&response_mode=form_post" +
"&state=android:login"
// Adding a WebView inside AndroidView
// with layout as full screen
@ -95,18 +77,13 @@ fun AppleAuthWebView(onDismiss: (String?) -> Unit) {
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
if (request?.url.toString().contains("android-apple-token")) {
val uri = Uri.parse(request!!.url.toString())
val token = uri.getQueryParameter("token")
onDismiss(token)
}
return true
}
@ -118,80 +95,3 @@ fun AppleAuthWebView(onDismiss: (String?) -> Unit) {
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

@ -68,6 +68,12 @@ class LoginViewModel @Inject constructor(
}
}
fun handleAppleToken(authToken: String) {
submitAuthProviderPayload(
params = SignInParams(token = authToken, provider = "APPLE")
)
}
fun logout() {
viewModelScope.launch {
datastoreRepo.clear()
@ -84,11 +90,7 @@ class LoginViewModel @Inject constructor(
fun handleGoogleAuthTask(task: Task<GoogleSignInAccount>) {
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")
// If token is missing then set the error message
if (googleIdToken == null) {
@ -96,15 +98,19 @@ class LoginViewModel @Inject constructor(
return
}
submitAuthProviderPayload(
params = SignInParams(token = googleIdToken, provider = "GOOGLE")
)
}
private fun submitAuthProviderPayload(params: SignInParams) {
val login = RetrofitHelper.getInstance().create(AuthProviderLoginSubmit::class.java)
viewModelScope.launch {
isLoading = true
errorMessage = null
val result = login.submitAuthProviderLogin(
SignInParams(token = googleIdToken, provider = "GOOGLE")
)
val result = login.submitAuthProviderLogin(params)
isLoading = false

View file

@ -128,16 +128,14 @@ fun AuthProviderView(
) {
Spacer(modifier = Modifier.weight(1.0F))
Column(
// verticalArrangement = Arrangement.Center,
// horizontalAlignment = Alignment.CenterHorizontally
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isGoogleAuthAvailable) {
GoogleAuthButton(viewModel)
}
// AppleAuthButton(viewModel)
AppleAuthButton(viewModel)
ClickableText(
text = AnnotatedString("Continue with Email"),

View file

@ -1,12 +0,0 @@
package app.omnivore.omnivore.ui.save
import androidx.compose.material.ExperimentalMaterialApi
import app.omnivore.omnivore.ui.save.SaveSheetActivity
// Not sure why we need this class, but directly opening SaveSheetActivity
// causes the app to crash.
@OptIn(ExperimentalMaterialApi::class)
class NewFlowActivity : SaveSheetActivity() {
}

View file

@ -1,4 +1,4 @@
package app.omnivore.omnivore
package app.omnivore.omnivore.ui.save
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -12,37 +12,36 @@ import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import app.omnivore.omnivore.ui.save.SaveViewModel
import kotlinx.coroutines.launch
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomSheetState, modifier: Modifier) {
val coroutineScope = rememberCoroutineScope()
val coroutineScope = rememberCoroutineScope()
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()
.padding(top = 48.dp, bottom = 32.dp)
) {
Text(text = viewModel.message ?: "Saving")
Button(onClick = {
coroutineScope.launch {
modalBottomSheetState.hide()
}
},
colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
backgroundColor = Color(0xffffd234)
)
) {
Text(text = "Dismiss")
}
}
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()
.padding(top = 48.dp, bottom = 32.dp)
) {
Text(text = viewModel.message ?: "Saving")
Button(onClick = {
coroutineScope.launch {
modalBottomSheetState.hide()
}
},
colors = ButtonDefaults.buttonColors(
contentColor = Color(0xFF3D3D3D),
backgroundColor = Color(0xffffd234)
)
) {
Text(text = "Dismiss")
}
}
}
}

View file

@ -18,153 +18,154 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import app.omnivore.omnivore.SaveContent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
// Not sure why we need this class, but directly opening SaveSheetActivity
// causes the app to crash.
class SaveSheetActivity : SaveSheetActivityBase() {}
@AndroidEntryPoint
@OptIn(ExperimentalMaterialApi::class)
abstract class SaveSheetActivity: AppCompatActivity() {
abstract class SaveSheetActivityBase: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: SaveViewModel by viewModels()
var extractedText: String? = null
val viewModel: SaveViewModel by viewModels()
var extractedText: String? = null
when (intent?.action) {
Intent.ACTION_SEND -> {
if (intent.type?.startsWith("text/plain") == true) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
Log.d(ContentValues.TAG, "Extracted text: $extractedText")
extractedText = it
viewModel.saveURL(it)
}
}
if (intent.type?.startsWith("text/html") == true) {
intent.getStringExtra(Intent.EXTRA_HTML_TEXT)?.let {
extractedText = it
}
}
}
else -> {
// Handle other intents, such as being started from the home screen
}
when (intent?.action) {
Intent.ACTION_SEND -> {
if (intent.type?.startsWith("text/plain") == true) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
Log.d(ContentValues.TAG, "Extracted text: $extractedText")
extractedText = it
viewModel.saveURL(it)
}
}
setContent {
val coroutineScope = rememberCoroutineScope()
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val isSheetOpened = remember { mutableStateOf(false) }
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Transparent,
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetUI {
ScreenContent(viewModel, modalBottomSheetState)
}
}
) {}
BackHandler {
onFinish(coroutineScope, modalBottomSheetState)
}
// Take action based on hidden state
LaunchedEffect(modalBottomSheetState.currentValue) {
when (modalBottomSheetState.currentValue) {
ModalBottomSheetValue.Hidden -> {
handleBottomSheetAtHiddenState(
isSheetOpened,
modalBottomSheetState
)
}
else -> {
Log.i(TAG, "Bottom sheet ${modalBottomSheetState.currentValue} state")
}
}
}
if (intent.type?.startsWith("text/html") == true) {
intent.getStringExtra(Intent.EXTRA_HTML_TEXT)?.let {
extractedText = it
}
}
}
else -> {
// Handle other intents, such as being started from the home screen
}
}
@Composable
private fun BottomSheetUI(content: @Composable () -> Unit) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
.background(Color.White)
.statusBarsPadding()
) {
content()
setContent {
val coroutineScope = rememberCoroutineScope()
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val isSheetOpened = remember { mutableStateOf(false) }
Divider(
color = Color.Gray,
thickness = 5.dp,
modifier = Modifier
.padding(top = 15.dp)
.align(TopCenter)
.width(80.dp)
.clip(RoundedCornerShape(50.dp))
ModalBottomSheetLayout(
sheetBackgroundColor = Color.Transparent,
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetUI {
ScreenContent(viewModel, modalBottomSheetState)
}
}
) {}
BackHandler {
onFinish(coroutineScope, modalBottomSheetState)
}
// Take action based on hidden state
LaunchedEffect(modalBottomSheetState.currentValue) {
when (modalBottomSheetState.currentValue) {
ModalBottomSheetValue.Hidden -> {
handleBottomSheetAtHiddenState(
isSheetOpened,
modalBottomSheetState
)
}
else -> {
Log.i(TAG, "Bottom sheet ${modalBottomSheetState.currentValue} state")
}
}
}
}
}
// Helper methods
private suspend fun handleBottomSheetAtHiddenState(
isSheetOpened: MutableState<Boolean>,
modalBottomSheetState: ModalBottomSheetState
@Composable
private fun BottomSheetUI(content: @Composable () -> Unit) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
.background(Color.White)
.statusBarsPadding()
) {
when {
!isSheetOpened.value -> initializeModalLayout(isSheetOpened, modalBottomSheetState)
else -> exit()
}
}
content()
private suspend fun initializeModalLayout(
isSheetOpened: MutableState<Boolean>,
modalBottomSheetState: ModalBottomSheetState
) {
isSheetOpened.value = true
modalBottomSheetState.show()
Divider(
color = Color.Gray,
thickness = 5.dp,
modifier = Modifier
.padding(top = 15.dp)
.align(TopCenter)
.width(80.dp)
.clip(RoundedCornerShape(50.dp))
)
}
}
open fun exit() = finish()
private fun onFinish(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
withResults: Boolean = false,
result: Intent? = null
) {
coroutineScope.launch {
if (withResults) setResult(RESULT_OK)
result?.let { intent = it}
modalBottomSheetState.hide() // will trigger the LaunchedEffect
}
// Helper methods
private suspend fun handleBottomSheetAtHiddenState(
isSheetOpened: MutableState<Boolean>,
modalBottomSheetState: ModalBottomSheetState
) {
when {
!isSheetOpened.value -> initializeModalLayout(isSheetOpened, modalBottomSheetState)
else -> exit()
}
}
@Composable
fun ScreenContent(
viewModel: SaveViewModel,
modalBottomSheetState: ModalBottomSheetState
) {
Box(modifier = Modifier.height(300.dp).background(Color.White)) {
SaveContent(viewModel, modalBottomSheetState, modifier = Modifier.fillMaxSize())
}
}
private suspend fun initializeModalLayout(
isSheetOpened: MutableState<Boolean>,
modalBottomSheetState: ModalBottomSheetState
) {
isSheetOpened.value = true
modalBottomSheetState.show()
}
override fun onPause() {
super.onPause()
overridePendingTransition(0, 0)
}
open fun exit() = finish()
companion object {
private val TAG = SaveSheetActivity::class.java.simpleName
private fun onFinish(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
withResults: Boolean = false,
result: Intent? = null
) {
coroutineScope.launch {
if (withResults) setResult(RESULT_OK)
result?.let { intent = it}
modalBottomSheetState.hide() // will trigger the LaunchedEffect
}
}
@Composable
fun ScreenContent(
viewModel: SaveViewModel,
modalBottomSheetState: ModalBottomSheetState
) {
Box(modifier = Modifier.height(300.dp).background(Color.White)) {
SaveContent(viewModel, modalBottomSheetState, modifier = Modifier.fillMaxSize())
}
}
override fun onPause() {
super.onPause()
overridePendingTransition(0, 0)
}
companion object {
private val TAG = SaveSheetActivity::class.java.simpleName
}
}

View file

@ -38,9 +38,11 @@ class SaveViewModel @Inject constructor(
isLoading = true
message = "Saving to Omnivore..."
val apiKey = getAuthToken()
val authToken = getAuthToken()
if (apiKey == null) {
Log.d(ContentValues.TAG, "AuthToken: $authToken")
if (authToken == null) {
message = "You are not logged in. Please login before saving."
isLoading = false
return@launch
@ -48,7 +50,7 @@ class SaveViewModel @Inject constructor(
val apolloClient = ApolloClient.Builder()
.serverUrl("${Constants.apiURL}/api/graphql")
.addHttpHeader("Authorization", value = apiKey)
.addHttpHeader("Authorization", value = authToken)
.build()
val response = apolloClient.mutation(
@ -70,7 +72,7 @@ class SaveViewModel @Inject constructor(
"There was an error saving your page"
}
Log.d(ContentValues.TAG, "Saved URL?: ${success.toString()}")
Log.d(ContentValues.TAG, "Saved URL?: $success")
}
}
}