Introduction
E-commerce Platforms
- Shopify
- Salla
- Zid
- WooCommerce
- Magento 2
- OpenCart
- Salesforce
- ExpandCart
Pay in 4 Custom Integration
Testing Guidelines
Integration without SDK (WebView)
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 WebView permissions were updated on Feb 25. 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.
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.
To be able select image files to provide it to web app the following steps should be done. Note: our web app supports only images to be selected. Other file types such as PDF are not supported.
- Custom web chrome client should be set to your android WebView:
WebView(context).apply {
webChromeClient = object : WebChromeClient() {
// Custom implementations.
}
}
onShowFileChooser
should beoverridden
to be able to open your file chooser:
object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
// File chooser shouold be opened here.
}
}
- We suggest you use android file chooser. In this case it won’t be required any of the permissions to get access to your images. If you are using your custom file chooser all required permissions should be handled by yourself. To open android file chooser please use the following code:
var uploadMessageCallback: ValueCallback<Array<Uri>>? = null
val fileChooserContract = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { result ->
val callback = uploadMessageCallback ?: return@registerForActivityResult
val URIs = result?.let { arrayOf(it) }
callback.onReceiveValue(URIs)
uploadMessageCallback = null
}
// Web chrome client implementation here. This implementation should be set to your WebView.
object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
uploadMessageCallback = filePathCallback
fileChooserContract.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
return true
}
}
Checkout flow
The Checkout session request, payment method display, and code snippets should be integrated in the same way as in the Pay in 4 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:
Was this page helpful?