diff --git a/app/build.gradle b/app/build.gradle
index 2f75ab8..7f63033 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -48,7 +48,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- def composeBom = platform("androidx.compose:compose-bom:2025.04.00")
+ def composeBom = platform("androidx.compose:compose-bom:$composeBomVersion")
implementation composeBom
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose'
diff --git a/gradle.properties b/gradle.properties
index 23339e0..e96ff85 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -19,3 +19,6 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
+
+composeBomVersion=2025.04.00
+navigationComposeVersion=2.8.9
\ No newline at end of file
diff --git a/webapp/build.gradle b/webapp/build.gradle
index 2a02abe..091b772 100644
--- a/webapp/build.gradle
+++ b/webapp/build.gradle
@@ -1,15 +1,16 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
+apply plugin: 'org.jetbrains.kotlin.plugin.compose'
android {
namespace "com.yoti.mobile.android.sdk.yotidocscan.websample"
- compileSdk 33
+ compileSdk 34
defaultConfig {
applicationId "com.yoti.mobile.android.sdk.yotidocscan.websample"
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode 1
versionName "1.0"
@@ -24,6 +25,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
+ compose true
}
buildTypes {
@@ -38,12 +40,12 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.core:core-ktx:1.3.0'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation 'com.google.android.material:material:1.1.0'
-
- // Multi-module projects: Add this dependency because of Android Studio Issue
- // and androix.navigation dependencies management https://issuetracker.google.com/issues/152245564
- implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
+
+ def composeBom = platform("androidx.compose:compose-bom:$composeBomVersion")
+ implementation composeBom
+ implementation 'androidx.compose.material3:material3'
+ implementation 'androidx.activity:activity-compose'
+ debugImplementation 'androidx.compose.ui:ui-tooling'
+
+ implementation "androidx.navigation:navigation-compose:$navigationComposeVersion"
}
diff --git a/webapp/src/main/AndroidManifest.xml b/webapp/src/main/AndroidManifest.xml
index e41b2ec..4a7a81d 100644
--- a/webapp/src/main/AndroidManifest.xml
+++ b/webapp/src/main/AndroidManifest.xml
@@ -3,8 +3,6 @@
-
-
diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AccelerometerListener.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AccelerometerListener.kt
deleted file mode 100644
index c82accc..0000000
--- a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AccelerometerListener.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.yoti.mobile.android.sdk.yotidocscan.websample
-
-import android.content.Context
-import android.hardware.Sensor
-import android.hardware.Sensor.TYPE_ACCELEROMETER
-import android.hardware.SensorEvent
-import android.hardware.SensorEventListener
-import android.hardware.SensorManager
-import android.hardware.SensorManager.SENSOR_DELAY_NORMAL
-import android.util.Log
-import kotlin.math.sqrt
-
-private const val TAG = "AccelerometerListener"
-
-class AccelerometerListener(
- private val context: Context,
- private val shakeListener: ShakeListener?
-) : SensorEventListener {
-
- interface ShakeListener {
- fun onShake()
- }
-
- private var sensorManager: SensorManager? = null
- private var shake = 0f
- private var currentShake = 0f
- private var lastShake = 0f
-
- fun start() {
- sensorManager = (context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager)
- sensorManager?.getDefaultSensor(TYPE_ACCELEROMETER)?.let { accelerometerSensor ->
- Log.d(TAG, "Start: register SensorManager listener")
- sensorManager?.registerListener(this, accelerometerSensor, SENSOR_DELAY_NORMAL)
- }?:run { Log.d(TAG, "Accelerometer sensor is not available") }
- }
-
- fun stop() {
- Log.d(TAG, "Stop: unregister SensorManager listener")
- sensorManager?.unregisterListener(this)
- }
-
- override fun onSensorChanged(event: SensorEvent?) {
- event?.run {
- val x = values[0]
- val y = values[1]
- val z = values[2]
- lastShake = currentShake
- currentShake = sqrt((x * x + y * y + z * z))
- val delta = currentShake - lastShake
- shake = shake * 0.9f + delta
- if (shake > 12) {
- Log.d(TAG, "Shake detected")
- shakeListener?.onShake()
- }
- }
- }
-
- override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
- // Nothing to do here
- }
-}
\ No newline at end of file
diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AppDestinations.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AppDestinations.kt
new file mode 100644
index 0000000..5f02fad
--- /dev/null
+++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AppDestinations.kt
@@ -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"
+}
\ No newline at end of file
diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt
index 088924a..01871dc 100644
--- a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt
+++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt
@@ -1,29 +1,35 @@
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.graphics.Color.TRANSPARENT
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.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
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
@@ -63,24 +69,14 @@ import java.util.Locale
* - Use a NoActionBar theme
* - Manage back navigation: users can press back hardware button and exit from the flow
*/
-private const val CAPTURE_REQUEST_CODE = 1112
-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 : ComponentActivity() {
private lateinit var cameraCaptureFileUri: Uri
private var filePathCallback: ValueCallback>? = 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",
@@ -88,129 +84,111 @@ class MainActivity : AppCompatActivity(), SessionConfigurationListener {
".jpg" to "image/jpeg"
)
- private lateinit var binding: ActivityMainBinding
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.light(
+ scrim = TRANSPARENT,
+ darkScrim = TRANSPARENT
+ ),
+ navigationBarStyle = SystemBarStyle.light(
+ scrim = TRANSPARENT,
+ darkScrim = TRANSPARENT
+ )
+ )
isViewRecreated = savedInstanceState?.getBoolean(KEY_IS_VIEW_RECREATED) ?: false
- requestPermissions()
-
- binding.webview.configureForYdsWeb()
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
+ setContent {
+ val navController = rememberNavController()
+ var sessionUrl by rememberSaveable { mutableStateOf("") }
+ var showMissingPermissionsDialog by remember { mutableStateOf(false) }
+ var showSessionFinishedDialog by remember { mutableStateOf(false) }
- if (requestCode == CAPTURE_REQUEST_CODE) {
- if (!isViewRecreated && resultCode == Activity.RESULT_OK) {
- val resultUri = data?.data ?: cameraCaptureFileUri
- filePathCallback?.onReceiveValue(arrayOf(resultUri))
- } else {
- filePathCallback?.onReceiveValue(null)
+ val permissionsLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ showMissingPermissionsDialog = !(permissions.values.all { it })
+ }
+ LaunchedEffect(Unit) {
+ permissionsLauncher.launch(arrayOf(permission.CAMERA, permission.RECORD_AUDIO))
}
- }
-
- }
-
- 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(
- requestCode: Int, permissions: Array, grantResults: IntArray
- ) {
- if (requestCode == PERMISSIONS_REQUEST_CODE) {
- grantResults.firstOrNull { it != PackageManager.PERMISSION_GRANTED }?.let {
- AlertDialog.Builder(this)
- .setTitle("Permissions needed")
- .setMessage("All permissions are needed to continue with the YDS session")
- .setPositiveButton("OK") { _, _ -> super.finish() }
- .show()
+ val cameraAndFilePickerChooserLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result -> handleCameraAndFilePickerChooserResult(result) }
+
+ YotiDocScanWebSampleAppTheme {
+ Scaffold { innerPadding ->
+ NavHost(
+ navController = navController,
+ startDestination = AppDestinations.MAIN_SCREEN,
+ modifier = Modifier.padding(innerPadding)
+ ) {
+ composable(route = AppDestinations.MAIN_SCREEN) {
+ MainScreen(
+ sessionUrl = sessionUrl,
+ showMissingPermissionsDialog = showMissingPermissionsDialog,
+ onSessionUrlChanged = { sessionUrl = it },
+ onStartSessionClicked = {
+ navController.navigate(AppDestinations.WEB_SCREEN)
+ },
+ onMissingPermissionsConfirmed = { finish() }
+ )
+ }
+
+ 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
+ }
+ },
+ onShowCameraAndFilePickerChooser = { callback, fileChooserParams ->
+ filePathCallback = callback
+ val intent = createCameraAndFilePickerChooserIntent(
+ fileChooserParams
+ )
+ cameraAndFilePickerChooserLauncher.launch(intent)
+ },
+ onCloseSession = { navController.popBackStack() },
+ onSessionFinished = { finish() }
+ )
+ }
+ }
+ }
}
}
}
- 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)
}
- private fun requestPermissions() {
- val permissions = listOf(
- permission.CAMERA,
- permission.RECORD_AUDIO,
- permission.READ_EXTERNAL_STORAGE,
- permission.WRITE_EXTERNAL_STORAGE
- )
-
- val permissionsRequest = permissions.mapNotNull { permission ->
- if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
- permission
- } else null
- }.toTypedArray()
-
- if (permissionsRequest.isNotEmpty()) {
- ActivityCompat.requestPermissions(
- this,
- permissionsRequest,
- PERMISSIONS_REQUEST_CODE
- )
+ private fun handleCameraAndFilePickerChooserResult(result: ActivityResult) {
+ if (!isViewRecreated && result.resultCode == RESULT_OK) {
+ val resultUri = result.data?.data ?: cameraCaptureFileUri
+ filePathCallback?.onReceiveValue(arrayOf(resultUri))
+ } else {
+ filePathCallback?.onReceiveValue(null)
}
}
- @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) {
-
+ private fun createCameraAndFilePickerChooserIntent(fileChooserParams: FileChooserParams): Intent {
cameraCaptureFileUri = createFileUri()
-
- Intent.createChooser(createFilePickerIntent(fileChooserParams), fileChooserParams.title)
- .run {
- putExtra(
- Intent.EXTRA_INITIAL_INTENTS,
- listOf(createCameraIntent(cameraCaptureFileUri)).toTypedArray()
- )
- startActivityForResult(this, CAPTURE_REQUEST_CODE)
- }
+ return Intent.createChooser(
+ createFilePickerIntent(fileChooserParams),
+ fileChooserParams.title
+ ).also {
+ it.putExtra(
+ Intent.EXTRA_INITIAL_INTENTS,
+ listOf(createCameraIntent(cameraCaptureFileUri)).toTypedArray()
+ )
+ }
}
private fun createFilePickerIntent(params: FileChooserParams): Intent? {
@@ -246,64 +224,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>?,
- fileChooserParams: FileChooserParams?
- ): Boolean {
- this@MainActivity.filePathCallback = 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") { _, _ -> this@MainActivity.finish() }
- .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
- }
-}
+}
\ No newline at end of file
diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainScreen.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainScreen.kt
new file mode 100644
index 0000000..b9eb20c
--- /dev/null
+++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainScreen.kt
@@ -0,0 +1,122 @@
+package com.yoti.mobile.android.sdk.yotidocscan.websample
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.yoti.mobile.android.sdk.yotidocscan.websample.ui.YotiDocScanWebSampleAppTheme
+
+@Composable
+fun MainScreen(
+ sessionUrl: String,
+ showMissingPermissionsDialog: Boolean,
+ onSessionUrlChanged: (String) -> Unit,
+ onStartSessionClicked: () -> Unit,
+ onMissingPermissionsConfirmed: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ OutlinedTextField(
+ value = sessionUrl,
+ onValueChange = onSessionUrlChanged,
+ label = {
+ Text(text = stringResource(id = R.string.session_url_hint))
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Text,
+ imeAction = ImeAction.Done
+ ),
+ modifier = Modifier.fillMaxWidth()
+ )
+ Spacer(Modifier.size(16.dp))
+ Button(
+ onClick = onStartSessionClicked,
+ enabled = sessionUrl.isNotBlank()
+ ) {
+ Text(text = stringResource(id = R.string.start_session_button))
+ }
+
+ if (showMissingPermissionsDialog) {
+ MissingPermissionsDialog { onMissingPermissionsConfirmed() }
+ }
+ }
+}
+
+@Composable
+private fun MissingPermissionsDialog(onConfirm: () -> Unit) {
+ AlertDialog(
+ title = { Text(stringResource(id = R.string.missing_permissions_dialog_title)) },
+ text = { Text(stringResource(id = R.string.missing_permissions_dialog_text)) },
+ confirmButton = {
+ TextButton(onClick = onConfirm) {
+ Text(stringResource(id = R.string.missing_permissions_dialog_confirm_button))
+ }
+ },
+ onDismissRequest = {}
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewEmptyMainScreen() {
+ YotiDocScanWebSampleAppTheme {
+ MainScreen(
+ sessionUrl = "",
+ showMissingPermissionsDialog = false,
+ onSessionUrlChanged = {},
+ onStartSessionClicked = {},
+ onMissingPermissionsConfirmed = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewMainScreen() {
+ YotiDocScanWebSampleAppTheme {
+ MainScreen(
+ sessionUrl = "https://example.com/session",
+ showMissingPermissionsDialog = false,
+ onSessionUrlChanged = {},
+ onStartSessionClicked = {},
+ onMissingPermissionsConfirmed = {}
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewMainScreenWithMissingPermissionsDialog() {
+ YotiDocScanWebSampleAppTheme {
+ MainScreen(
+ sessionUrl = "",
+ showMissingPermissionsDialog = true,
+ onSessionUrlChanged = {},
+ onStartSessionClicked = {},
+ onMissingPermissionsConfirmed = {}
+ )
+ }
+}
\ No newline at end of file
diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/SessionBottomSheet.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/SessionBottomSheet.kt
deleted file mode 100644
index 419175c..0000000
--- a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/SessionBottomSheet.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.yoti.mobile.android.sdk.yotidocscan.websample
-
-
-import android.content.Context
-import android.content.DialogInterface
-import android.content.SharedPreferences
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-import com.yoti.mobile.android.sdk.yotidocscan.websample.databinding.BottomSheetBinding
-
-private const val SESSION_PREFERENCES_ID = "SESSION_PREFERENCES_ID"
-private const val SESSION_CONFIGURATION_KEY = "SESSION_CONFIGURATION_KEY"
-
-class SessionBottomSheet: BottomSheetDialogFragment() {
-
- private var sessionConfigurationListener: SessionConfigurationListener? = null
- lateinit var sharedPreferences: SharedPreferences
-
- private var _binding: BottomSheetBinding? = null
- private val binding: BottomSheetBinding get() = _binding!!
-
- interface SessionConfigurationListener {
- fun onSessionConfigurationSuccess(sessionUrl: String)
- fun onSessionConfigurationDismiss()
- }
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
- ): View {
- _binding = BottomSheetBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- with (binding) {
- sessionUrl.setText(sharedPreferences.getString(SESSION_CONFIGURATION_KEY, ""))
-
- startSessionButton.setOnClickListener {
- sessionConfigurationListener?.onSessionConfigurationSuccess(
- sessionUrl.text.toString()
- )
- dismiss()
- }
- }
- }
-
- override fun onDismiss(dialog: DialogInterface) {
- sharedPreferences.edit().putString(SESSION_CONFIGURATION_KEY, binding.sessionUrl.text.toString()).apply()
- sessionConfigurationListener?.onSessionConfigurationDismiss()
- super.onDismiss(dialog)
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- sharedPreferences = requireContext().getSharedPreferences(SESSION_PREFERENCES_ID, Context.MODE_PRIVATE)
- sessionConfigurationListener = context as? SessionConfigurationListener
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-
- override fun onDetach() {
- sessionConfigurationListener = null
- super.onDetach()
- }
-
- companion object {
- const val FRAGMENT_TAG = "com.yoti.mobile.android.sdk.yotidocscan.websample.SessionBottomSheet"
-
- fun newInstance() = SessionBottomSheet()
- }
-}
\ No newline at end of file
diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/WebScreen.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/WebScreen.kt
new file mode 100644
index 0000000..4bf5146
--- /dev/null
+++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/WebScreen.kt
@@ -0,0 +1,159 @@
+package com.yoti.mobile.android.sdk.yotidocscan.websample
+
+import android.annotation.SuppressLint
+import android.net.Uri
+import android.webkit.PermissionRequest
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
+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,
+ onShowCameraAndFilePickerChooser: (ValueCallback>?, FileChooserParams) -> Unit,
+ onCloseSession: () -> Unit,
+ onSessionFinished: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(modifier = modifier.fillMaxSize()) {
+ AndroidWebView(sessionUrl, onPageCommitVisible, onShowCameraAndFilePickerChooser)
+
+ 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 AndroidWebView(
+ sessionUrl: String,
+ onPageCommitVisible: (String?) -> Unit,
+ onShowCameraAndFilePickerChooser: (ValueCallback>?, FileChooserParams) -> Unit
+) {
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context ->
+ WebView(context).apply {
+ WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
+ configureSettings(settings)
+ configureWebViewClient(this, onPageCommitVisible)
+ configureWebChromeClient(this, onShowCameraAndFilePickerChooser)
+ }
+ },
+ update = { webView ->
+ sessionUrl.takeIf { it.isNotBlank() }?.let { webView.loadUrl(it) }
+ }
+ )
+}
+
+@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))
+ }
+ },
+ 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))
+ }
+ },
+ onDismissRequest = {}
+ )
+}
+
+@Suppress("DEPRECATION")
+@SuppressLint("SetJavaScriptEnabled")
+private fun configureSettings(settings: WebSettings) {
+ with(settings) {
+ javaScriptEnabled = true
+ allowFileAccess = true
+ allowUniversalAccessFromFileURLs = true
+ domStorageEnabled = true
+ javaScriptCanOpenWindowsAutomatically = true
+ mediaPlaybackRequiresUserGesture = false
+ }
+}
+
+private fun configureWebViewClient(webView: WebView, onPageCommitVisible: (String?) -> Unit) {
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageCommitVisible(view: WebView?, url: String?) {
+ super.onPageCommitVisible(view, url)
+ onPageCommitVisible(url)
+ }
+ }
+}
+
+private fun configureWebChromeClient(
+ webView: WebView,
+ onShowCameraAndFilePickerChooser: (ValueCallback>?, FileChooserParams) -> Unit
+) {
+ webView.webChromeClient = object : 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>?,
+ fileChooserParams: FileChooserParams?
+ ): Boolean {
+ return if (fileChooserParams?.mode == FileChooserParams.MODE_OPEN) {
+ onShowCameraAndFilePickerChooser(filePathCallback, fileChooserParams)
+ true
+ } else {
+ false
+ }
+ }
+
+ // Grant permission requested
+ override fun onPermissionRequest(request: PermissionRequest?) {
+ request?.grant(request.resources)
+ }
+ }
+}
\ No newline at end of file
diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/ui/Theme.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/ui/Theme.kt
new file mode 100644
index 0000000..9b7f132
--- /dev/null
+++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/ui/Theme.kt
@@ -0,0 +1,18 @@
+package com.yoti.mobile.android.sdk.yotidocscan.websample.ui
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+private val LightColorScheme = lightColorScheme(
+ background = Color.White
+)
+
+@Composable
+fun YotiDocScanWebSampleAppTheme(content: @Composable () -> Unit) {
+ MaterialTheme(
+ colorScheme = LightColorScheme,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/webapp/src/main/res/drawable/ic_shake.xml b/webapp/src/main/res/drawable/ic_shake.xml
deleted file mode 100644
index 1d861ec..0000000
--- a/webapp/src/main/res/drawable/ic_shake.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/webapp/src/main/res/layout/activity_main.xml b/webapp/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 3fd8ff7..0000000
--- a/webapp/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/webapp/src/main/res/layout/bottom_sheet.xml b/webapp/src/main/res/layout/bottom_sheet.xml
deleted file mode 100644
index 88347fb..0000000
--- a/webapp/src/main/res/layout/bottom_sheet.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/webapp/src/main/res/values/colors.xml b/webapp/src/main/res/values/colors.xml
deleted file mode 100644
index 1ebfc66..0000000
--- a/webapp/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- #229dff
- #CCCCCC
- #229dff
-
diff --git a/webapp/src/main/res/values/strings.xml b/webapp/src/main/res/values/strings.xml
index 5068b9c..94a18af 100644
--- a/webapp/src/main/res/values/strings.xml
+++ b/webapp/src/main/res/values/strings.xml
@@ -1,3 +1,15 @@
webapp
-
+ Session URL
+ Start session
+ Close IDV Session
+ Are you sure you want to close the IDV session?
+ Yes
+ No
+ IDV Session
+ Session finished
+ OK
+ Permissions needed
+ All permissions are needed to continue with the IDV session
+ OK
+
\ No newline at end of file
diff --git a/webapp/src/main/res/values/styles.xml b/webapp/src/main/res/values/styles.xml
index 0eb88fe..5e606f1 100644
--- a/webapp/src/main/res/values/styles.xml
+++ b/webapp/src/main/res/values/styles.xml
@@ -1,11 +1,4 @@
-
-
-
+