Skip to content

Commit

Permalink
Replace WebView with custom tabs for Android OAuth login (#104)
Browse files Browse the repository at this point in the history
Co-authored-by: Omid Ghenatnevi <[email protected]>
  • Loading branch information
evant and crocsandcoffee authored Dec 21, 2022
1 parent 4818e85 commit 4f08ef5
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 77 deletions.
32 changes: 21 additions & 11 deletions app-android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:name="social.androiddev.dodo.DodoApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
android:name="social.androiddev.dodo.DodoApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:exported="true"
android:name="social.androiddev.dodo.MainActivity">
android:name="social.androiddev.dodo.MainActivity"
android:exported="true"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<category android:name="android.intent.category.LAUNCHER"/>
<data android:scheme="dodooauth2redirect" />
</intent-filter>
</activity>
</application>
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose-foundation" }
androidx-browser = { module = "androidx.browser:browser", version = "1.0.0" }

com-arkivanov-decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "com-arkivanov-decompose" }
com-arkivanov-decompose-extensions-compose-jetbrains = { module = "com.arkivanov.decompose:extensions-compose-jetbrains", version.ref = "com-arkivanov-decompose" }
Expand Down
2 changes: 2 additions & 0 deletions ui/signed-out/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ kotlin {
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.browser)
implementation(libs.androidx.activity.compose)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@
*/
package social.androiddev.signedout.selectserver

actual val redirectScheme: String get() = "oauth2redirect"
/**
* Note: this _must_ match the value in the manifest for deep linking back to the app to work
* correctly.
*/
actual val redirectScheme: String get() = "dodooauth2redirect"
Original file line number Diff line number Diff line change
Expand Up @@ -9,89 +9,98 @@
*/
package social.androiddev.signedout.signin

import android.graphics.Color
import android.webkit.CookieManager
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebStorage
import android.webkit.WebView
import android.webkit.WebViewClient
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.app.OnNewIntentProvider
import androidx.core.util.Consumer

@Composable
actual fun SignInWebView(
modifier: Modifier,
url: String,
onWebError: (message: String) -> Unit,
onCancel: () -> Unit,
shouldCancelLoadingUrl: (url: String) -> Boolean,
) {
val webIntent = webBrowserIntent(url, MaterialTheme.colors.primary, MaterialTheme.colors.secondary)
val handler = Handler(Looper.getMainLooper())

DisposableEffect(Unit) {
onDispose {
// Remove user session from WebView
WebStorage.getInstance().deleteAllData()
CookieManager.getInstance().removeAllCookies(null)
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_CANCELED) {
// post to a handler to wait for a redirect intent as that should supersede this
handler.post { onCancel() }
}
}
}

AndroidView(
modifier = modifier,
factory = {
WebView(it).apply {
setBackgroundColor(Color.TRANSPARENT)

webViewClient = object : WebViewClient() {

override fun onPageFinished(view: WebView?, url: String?) {
}
OnNewIntent { intent ->
val redirectUrl = intent?.data?.toString()
if (redirectUrl != null) {
if (shouldCancelLoadingUrl(redirectUrl)) {
handler.removeCallbacksAndMessages(null)
} else {
onCancel()
}
}
}

override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
onWebError(error.toString())
}
Box(Modifier.fillMaxSize()) {
CircularProgressIndicator(
Modifier
.align(Alignment.Center)
.size(84.dp)
)
}

override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return shouldOverrideUrlLoading(request.url.toString())
}
DisposableEffect(url) {
launcher.launch(webIntent)
onDispose {
handler.removeCallbacksAndMessages(null)
}
}
}

/* overriding this deprecated method is necessary for it to work on api levels < 24 */
@Suppress("OVERRIDE_DEPRECATION")
override fun shouldOverrideUrlLoading(
view: WebView?,
urlString: String?
): Boolean {
return if (urlString == null) {
false
} else {
shouldOverrideUrlLoading(urlString)
}
}
private fun webBrowserIntent(url: String, primaryColor: Color, secondaryColor: Color): Intent {
val intent = CustomTabsIntent.Builder()
.setToolbarColor(primaryColor.toArgb())
.setSecondaryToolbarColor(secondaryColor.toArgb())
.build()
.intent
intent.data = Uri.parse(url)
return intent
}

fun shouldOverrideUrlLoading(url: String): Boolean {
return shouldCancelLoadingUrl(url)
}
}
@Composable
private fun OnNewIntent(callback: (Intent?) -> Unit) {
val context = LocalContext.current
val newIntentProvider = context as OnNewIntentProvider

// JavaScript needs to be enabled because otherwise 2FA does not work in some instances
settings.javaScriptEnabled = true
settings.allowContentAccess = false
settings.allowFileAccess = false
settings.databaseEnabled = false
settings.displayZoomControls = false
settings.javaScriptCanOpenWindowsAutomatically = false
settings.userAgentString += " Dodo/1.0"
val listener = remember(newIntentProvider) { Consumer<Intent?> { callback(it) } }

loadUrl(url)
}
DisposableEffect(listener) {
newIntentProvider.addOnNewIntentListener(listener)
onDispose {
newIntentProvider.removeOnNewIntentListener(listener)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ fun SignInContent(
url = state.oauthAuthorizeUrl,
modifier = Modifier.fillMaxSize(),
shouldCancelLoadingUrl = component::shouldCancelLoadingUrl,
onWebError = component::onErrorFromOAuth
onWebError = component::onErrorFromOAuth,
onCancel = component::onCloseClicked,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ expect fun SignInWebView(
modifier: Modifier,
url: String,
onWebError: (message: String) -> Unit,
onCancel: () -> Unit,
shouldCancelLoadingUrl: (url: String) -> Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ actual fun SignInWebView(
modifier: Modifier,
url: String,
onWebError: (message: String) -> Unit,
onCancel: () -> Unit,
shouldCancelLoadingUrl: (url: String) -> Boolean,
) {

Expand Down

0 comments on commit 4f08ef5

Please sign in to comment.