Android

Integration in a 100% native Android app requires setting up a separate activity that contains the WebView where GetID (IDScan) will load from a custom page on your website.

This guide will walk you through setting the WebView app and make sure your app has direct connection to GetID and can receive events and data from it.

1. Create and host an html page for loading GetID (IDScan)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>GetID</title>
  <!-- Load the Checkin.com library -->
  <script src="https://[partner].regily.com/[key].js" async></script>
</head>
<body>
  <script>
    // Trigger GetID on window load
    window.onCheckinLoad = (sdk) => {
      sdk.settings.setLang('en')
      sdk.signUp.open()

      sdk.dataFlow.setOnComplete(async ({ data, completeData }) => {
        // Process and send the data to your back-end here 
        console.log('Complete captured', { data })
        // Send a request to your backend to save the data 
        const status = await saveDataToBackend(data)

        if (status === 'OK') {
          // redirect user on successful 
        } else {
          // If something goes wrong you can display an error towards the user on your end or redirect the user
        }

        // Triggers checkinComplete on Android to close the WebView
        window.Android.checkinComplete()
      })
    }

    function saveDataToBackend(data) {
      return new Promise(function (resolve, reject) {
        // Simulated saveDataToBackend
        setTimeout(resolve, 2000)
      })
    }
  </script>
</body>
</html>

Method setOnComplete is called when the flow ends and it can be used to handle the user data.

For more information on handling data, please see GetID (Standalone)

2. Create a new Android Activity

Create a new activity called WebView for instance. This activity is responsible for loading GetID inside a fullscreen webview, and listening to flow events:

package com.example.checkin_webview_integration;
import androidx.activity.result.ActivityResult;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.icu.text.SimpleDateFormat;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebChromeClient;
import android.webkit.PermissionRequest;
import android.widget.Toast;
import android.webkit.ValueCallback;
import android.content.Intent;
import java.io.*;
import org.json.JSONException;
import org.json.JSONObject;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import android.net.Uri;
import android.app.AlertDialog;
import android.content.DialogInterface;
import java.util.Locale;


public class WebActivity extends AppCompatActivity {
  private ActivityResultLauncher<Intent> mFileChooser;
  private ValueCallback<Uri[]> mediaCallback;
  private static final String filename = "yyyy-MM-dd-HH-mm-ss-SSS";

  private Uri imageUri;
  private WebView webView;

  private static final int PERMISSION_REQUEST_CODE = 1;
  private static int PERMISSION_DENIED_COUNT = 0;
  private static final String[] PERMISSIONS = {
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.CAMERA
  };

  private ActivityResultLauncher<Intent> getMediaChooserResult = registerForActivityResult(
    new ActivityResultContracts.StartActivityForResult(), result -> parseMediaChooserResult(result)
  );

  private ActivityResultLauncher<Uri> getCameraResult = registerForActivityResult(
    new ActivityResultContracts.TakePicture(), result -> {
      if (result != null) { parseCameraResult(result); }
    }
  );

  // Activity on Create
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_web);

    WebView myWebView = (WebView) findViewById(R.id.webview);
    myWebView.setWebChromeClient(new MyWebChromeClient(WebActivity.this, this.mFileChooser, myWebView));

    if (!hasPermissions(PERMISSIONS)) {
      ActivityCompat.requestPermissions(WebActivity.this, PERMISSIONS, PERMISSION_REQUEST_CODE);
    }

    // Enable JavaScript on the WebView
    WebSettings webSettings = myWebView.getSettings();
    webSettings.setJavaScriptEnabled(true);

    // Set this Activity as a JavaScript interface for events handling
    myWebView.addJavascriptInterface(this, "Android");

    // Load GetID with Android support (Change link!)
    myWebView.loadUrl("https://partner.com/checkin-getid/index.html");
  }

  // Custom WebChromeClient Class
  private class MyWebChromeClient extends WebChromeClient {
    private WebActivity webActivity;
    private ActivityResultLauncher mFileChooser;
    private WebView myWebView;

    private static final int PERMISSION_REQUEST_CODE = 1;

    final Handler handler = new Handler();

    // To refresh camera preview on selfie & liveness screens
    private Runnable runnable = new Runnable() {
      @Override
      public void run() {
        myWebView.invalidate();
        handler.postDelayed(this, 33); // Refresh at 30fps (1000ms / 30fps = 33.33ms per frame)
      }
    };

    public MyWebChromeClient(WebActivity webActivity, ActivityResultLauncher mFileChooser, WebView myWebView) {
      this.webActivity = webActivity;
      this.mFileChooser = mFileChooser;
      this.myWebView = myWebView;
    }

    // Handles permissions request event originating from within the custom WebChromeClient
    @Override
    public void onPermissionRequest(final PermissionRequest request) {
      try {
        webActivity.runOnUiThread(new Runnable() {
          @Override
          public void run() {
            final String[] requestedResources = request.getResources();
            Context context = WebActivity.this;

            // Rechecks if permissions are available and regrants it on the UI thread as required by Android, with a failsafe to prompt user to grant permissions with input
            if (!hasPermissions(PERMISSIONS)) {
              ActivityCompat.requestPermissions(WebActivity.this, PERMISSIONS, PERMISSION_REQUEST_CODE);
            }

            if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
              if (ActivityCompat.shouldShowRequestPermissionRationale(WebActivity.this, Manifest.permission.CAMERA)) {
                AlertDialog.Builder builder = new AlertDialog.Builder(webActivity);
                builder
                  .setTitle("Camera permission required")
                  .setMessage("The camera is required to advance in the process, please grant this permission.")
                  .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                      request.grant(requestedResources);
                    }
                  })
                  .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                      request.deny();
                    }
                  });

                AlertDialog dialog = builder.create();
                dialog.show();
              }
            } else {
              request.grant(requestedResources);
            }

            handler.post(runnable);
          }
        });
      } catch (Exception exception) {
        System.out.println(exception);
      }
    }

    // Handles file chooser call event
    @Override
    public boolean onShowFileChooser(
            WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
      mediaCallback = filePathCallback;
      Context context = WebActivity.this;

      try {
        if (fileChooserParams.isCaptureEnabled() == true) { // When using camera on document scanning
          String name = new SimpleDateFormat(filename, Locale.US).format(System.currentTimeMillis());
          imageUri = FileProvider.getUriForFile(
            context,
            context.getApplicationContext().getPackageName() + ".fileprovider",
            new File(context.getCacheDir(), name + ".jpeg")
          );

          if (imageUri != null) { getCameraResult.launch(imageUri); }
        } else { // When using gallery on document scanning
          if (mFileChooser == null) {
              mFileChooser = getMediaChooserResult;
            webActivity.setmFileChooser(mFileChooser);
          }
          // Launch the file chooser
          mFileChooser.launch(fileChooserParams.createIntent());
        }
      } catch (Exception exception) {
        System.out.println(exception);
      }

      return true;
    }
  }

  // Handles the result of permission requests
  @Override
  public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
      @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == PERMISSION_REQUEST_CODE) {
      if (!(grantResults.length > 0
          && grantResults[0] == PackageManager.PERMISSION_GRANTED
          && grantResults[1] == PackageManager.PERMISSION_GRANTED
          && grantResults[2] == PackageManager.PERMISSION_GRANTED)) {
        // Permission denied
        handlePermissionsDenied();
      }
      // If permission is granted, no action is needed as the user will be able to go through the flow
    }
  }

  // This prevents chromium from crashing
  @Override
  public void onDestroy() {
    super.onDestroy();
    webView=null; 
  }

  // Check if the app has required permissions
  private boolean hasPermissions(String[] permissions) {
    for (String permission : permissions) {
      if (ContextCompat.checkSelfPermission(WebActivity.this, permission)
              != PackageManager.PERMISSION_GRANTED) {
        return false;
      }
    }
    return true;
  }

  // Handler for file chooser result on document scanning
  private void parseMediaChooserResult(ActivityResult result) {
    if (mediaCallback != null) {
      switch (result.getResultCode()) {
        case RESULT_OK:
          if (result.getData() != null) {
            Uri uriResult = result.getData().getData();
            if (uriResult != null) {
              mediaCallback.onReceiveValue(new Uri[] { uriResult });
            } else {
              mediaCallback.onReceiveValue(new Uri[0]);
            }
          } else {
            mediaCallback.onReceiveValue(new Uri[0]);
          }
          break;
        default:
          mediaCallback.onReceiveValue(new Uri[0]);
          break;
      }
    }
  }

  // Handler for camera result on document scanning
  private void parseCameraResult(boolean result) {
    if (mediaCallback != null) {
      if (result) {
        if (imageUri != null) {
          mediaCallback.onReceiveValue(new Uri[] { imageUri });
        } else {
          mediaCallback.onReceiveValue(new Uri[0]);
        }
      } else {
        mediaCallback.onReceiveValue(new Uri[0]);
      }
    }
  }

  // Handler for when the user denies permissions request
  private void handlePermissionsDenied() {
    AlertDialog.Builder builder = new AlertDialog.Builder(WebActivity.this);
    if(PERMISSION_DENIED_COUNT != 0 || permissionRationalesFalse()) {
      // Show alert that redirects user to app settings to give permissions manually
      builder
              .setTitle("Permissions settings")
              .setMessage("Access to camera and gallery are required to advance in the process, please go to the app settings to enable this permission.")
              .setPositiveButton("OK", (dialog, id) -> goToAppSettings())
              .setNegativeButton("Cancel", (dialog, id) -> WebActivity.this.finish());
      AlertDialog dialog = builder.create();
      dialog.show();
    } else {
      // Show alert that prompts user to give permissions again if they refuse the 1st time
      builder
              .setTitle("Permissions required")
              .setMessage("Access to camera and gallery are required to advance in the process, please grant these permissions.")
              .setPositiveButton("OK", (dialog, id) -> {
                ActivityCompat.requestPermissions(WebActivity.this, PERMISSIONS, PERMISSION_REQUEST_CODE);
              })
              .setNegativeButton("Cancel", (dialog, id) -> {
                WebActivity.this.finish();
              });
      AlertDialog dialog = builder.create();
      dialog.show();

      PERMISSION_DENIED_COUNT++;
    }
  }

  // Navigate to the app settings
  private void goToAppSettings() {
    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    Uri uri = Uri.fromParts("package", getPackageName(), null);
    intent.setData(uri);
    startActivity(intent);
  }

  // Checks if user has denied permissions twice. Android doesn't allow apps to ask for permissions more than 2 times
  private boolean permissionRationalesFalse() {
    for (String permission : PERMISSIONS) {
      if (!ActivityCompat.shouldShowRequestPermissionRationale(WebActivity.this, permission)) {
        return true;
      }
    }
    return false;
  }

  public void setmFileChooser(ActivityResultLauncher<Intent> mFileChooser) {
    this.mFileChooser = mFileChooser;
  }

  // Handler for Module state events. E.g. open/close
  @JavascriptInterface
  public void onEvent(String eventData) throws JSONException {
    JSONObject data = new JSONObject(eventData);

    switch (data.getString("action")) {
      case "open-module":
        System.out.println("open-module");
        break;
      case "close-module":
        System.out.println("close-module");
        break;
    }
  }

  @JavascriptInterface
  public void checkinComplete() throws JSONException {
    System.out.println("checkinComplete");
    this.finish();
  }
}

3. Add a Webview to your Activity layout file

<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layout_behavior="@string/appbar_scrolling_view_behavior"
  tools:context=".WebActivity">

<WebView
  android:id="@+id/webview"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4. Create a custom file provider

Create a new class file CustomFileProvider.java

package com.example.checkin_webview_integration;

import androidx.core.content.FileProvider;

public class CustomFileProvider extends FileProvider {}

Then create provider_paths.xml inside res/xml directory

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <cache-path
    name="cache_path"
    path="." />
  <external-path
    name="external"
    path="." />
  <external-files-path
    name="external_files"
    path="." />
  <external-cache-path
    name="external_cache"
    path="." />
  <files-path
    name="files"
      path="." />
</paths>

5. Update your AndroidManifest

Remove ActionBar from your Webview Activity

This is one of the things you can do do make GetID look better without distraction from the ActionBar. Other things you can do is making your Activity transparent so the original app content is still visible behind GetID. Also adding configChanges prevents the webview from reloading when user rotates their device.

Modify your new Activity in the AndroidManifest.xml file to look like this:

<activity android:name=".WebActivity" android:exported="false" android:configChanges="orientation|screenSize" />

Give your app permission to access the internet, files and camera

This can be done by adding the following to your AndroidManifest.xml file

<manifest ... >
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.RESOURCE_VIDEO_CAPTURE"/>
  <uses-permission android:name="android.permission.RESOURCE_AUDIO_CAPTURE"/>
  ...
</manifest>

Add custom file provider config inside application

<application ... >
  <provider
    android:name=".CustomFileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/provider_paths" />
  </provider>
  ...
</application>

6. Launch GetID (IDScan) with a button tap!

Put a button you want to use to open GetID in one of your Activities

<Button
  android:id="@+id/button"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:onClick="openGetID"
  android:text="GetID" />

Then add a openGetID function to open GetID in the new activity

public void openGetID(View view) {
  Intent intent = new Intent(this, WebActivity.class);
  startActivity(intent);
}