Skip to content
Open
2 changes: 2 additions & 0 deletions webapp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ dependencies {
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose'
debugImplementation 'androidx.compose.ui:ui-tooling'

implementation 'androidx.navigation:navigation-compose:2.8.9'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it ok to have the version hardcoded?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: 74bddca 🚀

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.yoti.mobile.android.sdk.yotidocscan.websample

object AppDestinations {

const val MAIN_SCREEN = "main_screen"
const val WEB_SCREEN = "web_screen"
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
package com.yoti.mobile.android.sdk.yotidocscan.websample

import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.view.View.VISIBLE
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebChromeClient.FileChooserParams
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.setContent
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.yoti.mobile.android.sdk.yotidocscan.websample.AccelerometerListener.ShakeListener
import com.yoti.mobile.android.sdk.yotidocscan.websample.SessionBottomSheet.SessionConfigurationListener
import com.yoti.mobile.android.sdk.yotidocscan.websample.databinding.ActivityMainBinding
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.yoti.mobile.android.sdk.yotidocscan.websample.ui.YotiDocScanWebSampleAppTheme
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
Expand Down Expand Up @@ -69,38 +72,70 @@ private const val PERMISSIONS_REQUEST_CODE = 1114
private const val KEY_IS_VIEW_RECREATED = "MainActivity.KEY_IS_VIEW_RECREATED"
private const val FINISH_SESSION_URL = "https://www.yoti.com/"

class MainActivity : AppCompatActivity(), SessionConfigurationListener {

private var sessionBottomSheet: SessionBottomSheet? = null
class MainActivity : AppCompatActivity() {

private lateinit var cameraCaptureFileUri: Uri
private var filePathCallback: ValueCallback<Array<Uri>>? = null
private var isViewRecreated: Boolean = false
private val shakeListener = AccelerometerListener(this, object: ShakeListener {
override fun onShake() {
showOptionsDialog()
}
})

private val mimeTypeMap = mapOf(
".pdf" to "application/pdf",
".png" to "image/png",
".jpg" to "image/jpeg"
)

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

isViewRecreated = savedInstanceState?.getBoolean(KEY_IS_VIEW_RECREATED) ?: false

requestPermissions()
isViewRecreated = savedInstanceState?.getBoolean(KEY_IS_VIEW_RECREATED) ?: false

binding.webview.configureForYdsWeb()
setContent {
val navController = rememberNavController()
var sessionUrl by remember { mutableStateOf("") }
var showSessionFinishedDialog by remember { mutableStateOf(false) }

YotiDocScanWebSampleAppTheme {
Scaffold { innerPadding ->
NavHost(
navController = navController,
startDestination = AppDestinations.MAIN_SCREEN,
modifier = Modifier.padding(innerPadding)
) {
composable(route = AppDestinations.MAIN_SCREEN) {
MainScreen(
sessionUrl = sessionUrl,
onSessionUrlChanged = { sessionUrl = it },
onStartSessionClicked = {
navController.navigate(AppDestinations.WEB_SCREEN)
}
)
}

composable(route = AppDestinations.WEB_SCREEN) {
WebScreen(
sessionUrl = sessionUrl,
showSessionFinishedDialog = showSessionFinishedDialog,
onPageCommitVisible = { url ->
// Detect the URL that indicates that flow is finished and close the app
if (url == FINISH_SESSION_URL) {
showSessionFinishedDialog = true
}
},
onFilePathCallbackReady = { callback ->
filePathCallback = callback
},
onShowCameraAndFilePickerChooser = { fileChooserParams ->
showCameraAndFilePickerChooser(fileChooserParams)
},
onCloseSession = { navController.popBackStack() },
onSessionFinished = { finish() }
)
}
}
}
}
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Expand All @@ -114,22 +149,6 @@ class MainActivity : AppCompatActivity(), SessionConfigurationListener {
filePathCallback?.onReceiveValue(null)
}
}

}

override fun onResume() {
super.onResume()
shakeListener.start()
}

override fun onPause() {
shakeListener.stop()
super.onPause()
}

override fun onDestroy() {
binding.webview.destroy()
super.onDestroy()
}

override fun onRequestPermissionsResult(
Expand All @@ -146,15 +165,6 @@ class MainActivity : AppCompatActivity(), SessionConfigurationListener {
}
}

override fun onBackPressed() {
AlertDialog.Builder(this)
.setTitle("Close YDS Session")
.setMessage("Are you sure you want to finish YDS session?")
.setPositiveButton("Yes") { _, _ -> super.onBackPressed() }
.setNegativeButton("No", null)
.show()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_IS_VIEW_RECREATED, true)
Expand All @@ -178,26 +188,8 @@ class MainActivity : AppCompatActivity(), SessionConfigurationListener {
}
}

@SuppressLint("SetJavaScriptEnabled")
private fun WebView.configureForYdsWeb() {
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)

this.settings.apply {
javaScriptEnabled = true
allowFileAccess = true
allowUniversalAccessFromFileURLs = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = true
mediaPlaybackRequiresUserGesture = false
}
this.webViewClient = YdsWebClient()
this.webChromeClient = YdsWebChromeClient()
}

private fun showCameraAndFilePickerChooser(fileChooserParams: FileChooserParams) {

cameraCaptureFileUri = createFileUri()

Intent.createChooser(createFilePickerIntent(fileChooserParams), fileChooserParams.title)
.run {
putExtra(
Expand Down Expand Up @@ -241,64 +233,4 @@ class MainActivity : AppCompatActivity(), SessionConfigurationListener {
File.createTempFile(imageFileName, ".jpg", storageDir)
)
}

private inner class YdsWebChromeClient: WebChromeClient() {
// Launch file picker or camera intent and set the
// filePathCallback to set the capture results to the webview
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
[email protected] = filePathCallback

return if (fileChooserParams?.mode == FileChooserParams.MODE_OPEN) {
showCameraAndFilePickerChooser(fileChooserParams)
true
} else {
false
}
}

// Grant permission requested
override fun onPermissionRequest(request: PermissionRequest?) {
request?.grant(request.resources)
}
}

private inner class YdsWebClient : WebViewClient() {
// Detect the URL that indicates that YDS flow is finished
// and close the app
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
if (url == FINISH_SESSION_URL) {
AlertDialog.Builder(this@MainActivity)
.setTitle("YDS Session")
.setMessage("Session finished")
.setPositiveButton("OK") { _, _ -> [email protected]() }
.show()
}
}
}

private fun showOptionsDialog() {
if (sessionBottomSheet != null) return

sessionBottomSheet = SessionBottomSheet.newInstance()
sessionBottomSheet?.show(
supportFragmentManager,
SessionBottomSheet.FRAGMENT_TAG
)
}

override fun onSessionConfigurationSuccess(sessionUrl: String) {
with(binding) {
webview.visibility = VISIBLE
webview.loadUrl(sessionUrl)
}
}

override fun onSessionConfigurationDismiss() {
sessionBottomSheet = null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,103 @@ import android.webkit.WebChromeClient.FileChooserParams
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView

@Composable
fun WebScreen(
sessionUrl: String,
showSessionFinishedDialog: Boolean,
onPageCommitVisible: (String?) -> Unit,
onFilePathCallbackReady: (ValueCallback<Array<Uri>>?) -> Unit,
onShowCameraAndFilePickerChooser: (FileChooserParams) -> Unit,
onCloseSession: () -> Unit,
onSessionFinished: () -> Unit,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
configureSettings(settings)
configureWebViewClient(this, onPageCommitVisible)
configureWebChromeClient(
this,
onFilePathCallbackReady,
onShowCameraAndFilePickerChooser
)
Box(modifier = modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
configureSettings(settings)
configureWebViewClient(this, onPageCommitVisible)
configureWebChromeClient(
this,
onFilePathCallbackReady,
onShowCameraAndFilePickerChooser
)
}
},
update = { webView ->
sessionUrl.takeIf { it.isNotBlank() }?.let { webView.loadUrl(it) }
}
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a dedicated @Composable private fun

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here: a78069a 🚀


var showCloseSessionDialog by remember { mutableStateOf(false) }
BackHandler { showCloseSessionDialog = true }
if (showCloseSessionDialog) {
CloseSessionDialog(
onConfirm = {
showCloseSessionDialog = false
onCloseSession()
},
onDismiss = { showCloseSessionDialog = false })
}

if (showSessionFinishedDialog) {
SessionFinishedDialog(onConfirm = { onSessionFinished() })
}
}
}

@Composable
private fun CloseSessionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
title = { Text(stringResource(id = R.string.close_session_dialog_title)) },
text = { Text(stringResource(id = R.string.close_session_dialog_text)) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(id = R.string.close_session_dialog_confirm_button))
}
},
update = { webView ->
sessionUrl.takeIf { it.isNotBlank() }?.let { webView.loadUrl(it) }
}
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(id = R.string.close_session_dialog_dismiss_button))
}
},
onDismissRequest = onDismiss
)
}

@Composable
private fun SessionFinishedDialog(onConfirm: () -> Unit) {
AlertDialog(
title = { Text(stringResource(id = R.string.session_finished_dialog_title)) },
text = { Text(stringResource(id = R.string.session_finished_dialog_text)) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(id = R.string.session_finished_dialog_confirm_button))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No color or font size/style ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered above

}
},
onDismissRequest = {}
)
}

@Suppress("DEPRECATION")
@SuppressLint("SetJavaScriptEnabled")
private fun configureSettings(settings: WebSettings) {
with(settings) {
Expand Down
Loading