Skip to main content
If our SDK solutions do not meet your requirements, you can set up backend API calls and render your own WebView to open the Tabby Hosted Payment Page.

WebView Permissions

Android and iOS WebView permissions were updated as of 04.2025. Ensure that your app’s WebView has the necessary permissions to access the camera and upload images.
These permissions are required for uploading national IDs for new customers.
Below is a detailed description of how to implement permission handling for Android WebView so that the web app can access the camera (and, if needed, the microphone):

1. Interaction Between WebView and Permissions

When a web page requests access to the camera or microphone, the WebView calls the onPermissionRequest callback method of the WebChromeClient. This method receives a PermissionRequest object containing the list of resources (permissions) requested by the web page. For more detailed information, please refer to the documentation.A basic implementation of handling this request might look like:
object : WebChromeClient() {
    override fun onPermissionRequest(request: PermissionRequest) {
        request.grant(request.resources)
    }
}
In this example, all requested resources are immediately granted. While this approach is acceptable for testing, it is not recommended for production due to security concerns.

2. Mapping Permission Types

The PermissionRequest object may request different types of permissions. For example:
PermissionRequest.RESOURCE_VIDEO_CAPTURE corresponds to the camera permission – Manifest.permission.CAMERA.
PermissionRequest.RESOURCE_AUDIO_CAPTURE corresponds to the audio recording permission – Manifest.permission.RECORD_AUDIO.
In your code, you need to map the requested resources to the corresponding Android permissions to properly request them from the user.

3. Adding Permissions to the Manifest

To allow your application to use the camera and audio, you must declare the appropriate permissions and features in your AndroidManifest.xml. For example, to support the permissions mentioned above, add the following:
<uses-feature
    android:name="android.hardware.camera"
    android:required="false" />

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
These entries indicate that your application uses the camera and audio, while also declaring that the camera hardware is not mandatory for the app to function.

4. Requesting Dangerous Permissions at Runtime

Permissions such as CAMERA and RECORD_AUDIO are classified as “dangerous” and must be requested from the user at runtime. An example implementation using ActivityResultContracts.RequestPermission is provided below:
class MainActivity : ComponentActivity() {

    private val permissionRequester: PermissionRequester = PermissionRequester()

    private val permissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission(),
        permissionRequester
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        permissionRequester.activityResultLauncher = permissionLauncher
        setContent {
            TabbyLiveCodingTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    CheckoutWebScreen(
                        modifier = Modifier.padding(innerPadding),
                        url = "https://url.to.web-app.net",
                        webChromeClient = object : WebChromeClient() {
                            override fun onPermissionRequest(request: PermissionRequest) {
                                lifecycleScope.launch {
                                    val result = permissionRequester.requestPermissions(
                                        request.resources.mapNotNull {
                                            WebViewPermissions.byPermission(it)
                                        }
                                    )
                                    val granted = result.filter { it.isGranted == true }

                                    if (granted.isNotEmpty()) {
                                        request.grant(granted.map { it.permission.permission }
                                            .toTypedArray())
                                    } else {
                                        request.deny()
                                    }
                                }
                            }
                        },
                    )
                }
            }
        }
    }
}

class PermissionRequester : ActivityResultCallback<Boolean> {

    lateinit var activityResultLauncher: ActivityResultLauncher<String>

    private var currentRequest: CompletableDeferred<Boolean>? = null

    private var inProgress: Boolean = false

    suspend fun requestPermissions(permissions: List<WebViewPermissions>): List<PermissionsCheck> {
        if (inProgress) {
            return permissions.map { PermissionsCheck(it, false) }
        }

        inProgress = true

        val result = permissions.map {
            currentRequest = CompletableDeferred()
            activityResultLauncher.launch(it.androidPermission())
            val result = currentRequest?.await()
            currentRequest = null
            PermissionsCheck(it, result)
        }

        inProgress = false

        return result
    }

    override fun onActivityResult(result: Boolean) {
        currentRequest?.complete(result)
    }
}

data class PermissionsCheck(
    val permission: WebViewPermissions,
    var isGranted: Boolean? = null,
)

enum class WebViewPermissions(val permission: String) {
    VideoCapture(PermissionRequest.RESOURCE_VIDEO_CAPTURE),
    AudioCapture(PermissionRequest.RESOURCE_AUDIO_CAPTURE);

    companion object {
        fun byPermission(permission: String): WebViewPermissions? {
            return entries.find { it.permission == permission }
        }
    }
}

fun WebViewPermissions.androidPermission(): String = when (this) {
    WebViewPermissions.VideoCapture -> Manifest.permission.CAMERA
    WebViewPermissions.AudioCapture -> Manifest.permission.RECORD_AUDIO
}

Key Points of the Implementation:

  • Requesting Permissions: When a permission request is received via onPermissionRequest, a coroutine is launched that uses the PermissionRequester object to request the necessary dangerous permissions from the user.
  • Handling the Result: If at least one of the required permissions is granted, the app calls request.grant with the corresponding resources; otherwise, it calls request.deny.
  • Permission Mapping: The WebViewPermissions enum is used to map the permissions requested by the PermissionRequest to the actual Android permissions.

Java implementation

import android.webkit.PermissionRequest;
import android.webkit.WebChromeClient;

import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class PermissionRequester implements ActivityResultCallback<Map<String, Boolean>> {
    private PermissionRequest permissionRequest;

    public ActivityResultLauncher<String[]> multiplePermissionLauncher;

    public PermissionRequester() {}

    public WebChromeClient getWebChromeClient() {
        return new WebChromeClient() {
            @Override
            public void onPermissionRequest(PermissionRequest request) {
                permissionRequest = request;
                List<String> permissions = getRuntimePermissions(request.getResources());
                if (permissions.isEmpty()) {
                    request.grant(request.getResources());
                } else {
                    multiplePermissionLauncher.launch(permissions.toArray(new String[0]));
                }
            }
        };
    }

    @Override
    public void onActivityResult(Map<String, Boolean> o) {
        boolean allGranted = true;
        for (Boolean granted : o.values()) {
            if (!granted) {
                allGranted = false;
                break;
            }
        }
        if (allGranted) {
            permissionRequest.grant(permissionRequest.getResources());
        } else {
            permissionRequest.deny();
        }
    }

    private List<String> getRuntimePermissions(String[] resources) {
        List<String> permissions = new ArrayList<>();
        for (String resource : resources) {
            switch (resource) {
                case PermissionRequest.RESOURCE_VIDEO_CAPTURE:
                    permissions.add(android.Manifest.permission.CAMERA);
                    break;
                case PermissionRequest.RESOURCE_AUDIO_CAPTURE:
                    permissions.add(android.Manifest.permission.RECORD_AUDIO);
                    break;
            }
        }
        return permissions;
    }
}

Conclusion

To enable WebView to use the camera, you need to:
  • Implement the onPermissionRequest method in a WebChromeClient.
  • Map the permission types from the PermissionRequest to Android permissions (e.g., camera and audio).
  • Declare the required permissions and features in the AndroidManifest.xml.
  • Request dangerous permissions from the user at runtime before granting them to WebView.
This approach ensures that your web application can securely and correctly access device functions while complying with Android’s security requirements.

Checkout flow

The Checkout session request, payment method display, and code snippets should be integrated in the same way as in the Online Custom Integration. When customers complete the Tabby checkout (whether the payment is successful, rejected, or canceled), you can handle merchant_urls, listen to Checkout events passed in the WebView, or manage Webhooks received on the app backend. Below is an example of the Webview events handing for iOS (Swift):

tabby-ios-manual-integration.md

Payment Verification and Processing

Payment Verification and Processing through the OMS/ERP has to be integrated on the Merchant’s Backend the same way as on Web integration:
I