Get Started
This guide will take you through the process of setting up the Standalone Capture Module.
For a guide on setting up the full Sensibill SDK see Spend Manager SDK .
Before you begin
To arrange access to our SDK repositories please contact your Account Manager.
Installation
Adding Sensibill SDK to a Project
The Sensibill SDK for iOS is distributed in two formats: a binary package format (XCFramework), and a universal archive (legacy format). Both formats can be installed using CocoaPods dependency manager (a preferred method), or by adding the dependency manually to your project.
This section explains how to install Sensibill SDK for iOS in XCFramework format using CocoaPods dependency manager. For other options, please see Installation Options page.
Steps
If your project isn’t using CocoaPods already, follow the Installation and Getting Started guides on the official CocoaPods website . Once your project is integrated with the CocoaPods, you should have a
Podfile
in the root directory of your project.Add the Sensibill SDK for iOS as a dependency to your project’s
Podfile
in the following format:pod 'Sensibill', :git => 'git@github.com:getsensibill/sdk-ios.git', :tag => 'vX.Y.Z'
where
vX.Y.Z
represents to version of the SDK you want to use.Note: once Sensibill SDK was added to
Podfile
, don’t forget to runpod install
from the root directory of your application’s project in your terminal. This will add Sensibill SDK for iOS to your project’s Pods.Validate the integration by importing the Sensibill module into one of your Swift classes:
import Sensibill
and try to build your project. If the project builds successfully, the Sensibill SDK for iOS was integrated correctly. Otherwise follow the Troubleshooting steps on Installation Options page.
Adding the Required Entitlements
When a user navigates to the Capture Document screen of the Sensibill SDK, SDK will use device’s camera to capture documents. In order to allow camera usage, your application must prompt the user for consent to use their device’s camera, and thus requires a Privacy - Camera Usage Description entitlement.
Sensibill SDK also allows the users to select an existing document images from the device’s photo gallery, which requires a Privacy - Photo Library Usage Description entitlement. Alternatively you can disable gallery access by setting the
Capture.FeatureFlags.enableImageGallery
tofalse
.If you would like to attach user’s device Location data to captured document, the Privacy - Location When In Use Usage Description entitlement is required. By default the location data is not collected, and you need to set
Capture.FeatureFlags.attachLocationData
totrue
to enable attaching the location data. Note that if user denies the access to the location, the location data will not be attached to a document, even if it was enabled for the app.
Steps
- Open
Info.plist
of your application. - Locate or add each of the following entitlements, and provide a description of why the entitlement is required.
Required Entitlements
- For the camera usage:
NSCameraUsageDescription
(displayed as Privacy - Camera Usage Description) - For the photo gallery access:
NSPhotoLibraryUsageDescription
(displayed as Privacy - Photo Library Usage Description). - For attaching user’s location data:
NSLocationWhenInUseUsageDescription
(displayed as Privacy - Location When In Use Usage Description).
Please contact your Account Manager for access to Maven.
The primary method of installing this module is via Sensibill’s private maven server. All stable artifacts are stored on a private maven server than can only be accessed using an authorized username and password. Contact Sensibill’s Client Services to acquire credentials. Once you have maven credentials, one of the two following changes to your gradle configuration will allow you to import Sensibill SDK dependencies.
// (old gradle style) IF Maven repositories are listed in the project level `build.gradle`
// file, Sensibill's Maven server should be added to the `allprojects.repositories` block
allprojects {
repositories {
// Other Maven Servers ...
maven {
// Sensibill SDK repository
url 'https://maven.pkg.github.com/getsensibill/sdk-android'
credentials {
username 'to be provided by sensibill'
password 'to be provided by sensibill'
}
}
}
}
// (new gradle style) IF Maven repositories are listed in the `settings.gradle` file,
// Sensibill's Maven server should be added in the `dependencyResolutionManagement.repositories` block
dependencyResolutionManagement {
repositories {
// Other Maven Servers ...
maven {
// Sensibill SDK repository
url 'https://maven.pkg.github.com/getsensibill/sdk-android'
credentials {
username 'to be provided by sensibill'
password 'to be provided by sensibill'
}
}
}
}
Maven Dependency
To add the Standalone Capture module to your Android project, add the following to the dependencies
section of your main app
module’s build.gradle
file.
dependencies {
// ...
// See the rest of the documentation for the most up to date version
def sensibillSdkVersion = 'X.X.X'
// Standalone Capture Module
implementation "com.getsensibill:sensibill-capture-standalone:$sensibillSdkVersion"
// ...
}
Launch Capture
Capture entry point is a CaptureNavigationController
class - a subclass of UINavigationController
. When Capture starts, it displays a Camera Preview screen with options that allow the user to control how the documents are captured.
On instantiation, the caller can optionally provide an instance of Capture.RuntimeSettings
, which allow to customize different aspects of capture experience. If this object is not provided, a Capture will be presented with default settings. See Global Theme
page for more details on customization.
Upon its completion, the Capture needs to communiate the results to its caller. To do that, a caller has to implement CaptureNavigationControllerDelegate
in one of the classes, and pass the reference to an instance of the delegate before presenting a Capture.
Steps
Declare the conformance to
CaptureNavigationControllerDelegate
in one of the classes (for example as an extension of the class from where Capture is started), and add the required method. Note: the details on delegate implementation are provided in the next section, Using Captured Images .(Optional) Customize a
Capture.RuntimeSettings
. This step will not be covered in this example, and is detailed on Global Theme page.Instantiate CaptureNavigationController .
Select a modalPresentationStyle for the
CaptureNavigationController
.Set a
captureDelegate
to an instance of the class you implemented in step 1.Present the
CaptureNavigationController
.
Note:
- Code described in steps 3-6 is dealing with UI and hence must run on
DispatchQueue.main
(or using@MainActor
).
Example
In the following example modalPresentationStyle
was set to overFullScreen
, and captureDelegate
was set to the class that starts the Capture, which implements CaptureNavigationControllerDelegate
:
import Sensibill
class YourClass {
// This function must run on main thread
func startCapture() {
// 3. Instantiate
let captureNavigationController = CaptureNavigationController()
// 4. Select a modalPresentationStyle
captureNavigationController.modalPresentationStyle = .overFullScreen
// 5. Set a captureDelegate
captureNavigationController.captureDelegate = self
// 6. Present
present(captureNavigationController, animated: true, completion: nil)
}
}
// 1. Conformance to CaptureNavigationControllerDelegate
extension YourClass: CaptureNavigationControllerDelegate {
func captureNavigationController(_ controller: CaptureNavigationController, didFinishCapture result: CaptureResult) {
}
}
The only entry point to this module is via the CaptureStandaloneActivity
(reference
). The Activity optionally takes a CaptureConfig
(reference
) object for configuration as an intent extras. If none is provided, it will use the default CaptureConfig
held at CaptureConfig.defaultConfig
.
As Android has recently deprecated startActivityForResult
, the below example will include both the classic startActivityForResult
as well as the registerForActivityResult
methods of starting the capture activity.Our ActivityResultContract
implementation for the capture flow is CaptureImages
(reference
).
Example
Kotlin
class MyActivity : AppCompatActivity() {
// ...
// ---------- Using `startActivityForResult` ----------
private fun launchCapture() {
// Create configuration object if required
val config = CaptureConfig(
allowFlashToggling = false,
defaultFlashMode = FlashMode.FLASH_MODE_OFF,
maxPages = 5,
enableLongCapture = false,
enableSecureWindow = true
)
// Create intent and attach config
val intent = Intent(this, CaptureStandaloneActivity::class.java).apply {
putExtra(CaptureStandaloneActivity.EXTRA_CAPTURE_CONFIG, config)
}
// Start the capture activity
startActivityForResult(intent, REQUEST_CODE_TO_CAPTURE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_CODE_TO_CAPTURE -> {
// See next section for handling the result
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
companion object {
private const val REQUEST_CODE_TO_CAPTURE = 0x123
}
// ---------- Using `registerForActivityResult` ----------
private lateinit var launcher: ActivityResultLauncher<CaptureConfig>
private fun registerForCaptureResult() {
val contract = CaptureImages()
launcher = registerForActivityResult(contract) { result ->
// See next section for handling the result
}
}
private fun launchCaptureWithRegistration() {
launcher.launch(CaptureConfig())
}
}
Java
public class MyActivity extends AppCompatActivity {
private static final int REQUEST_CODE_TO_CAPTURE = 0x123;
// ...
// ---------- Using `startActivityForResult` ----------
private void launchCapture() {
// Create configuration object if required
final CaptureConfig captureConfig = new CaptureConfig(
true,
true,
FlashMode.FLASH_MODE_OFF,
true,
true,
false,
true,
3,
false,
true,
false,
true,
true,
true,
DocumentTypeStrings.Companion.getDefaultReceiptStrings(),
CaptureDocumentType.RECEIPT
);
// Create intent and attach config
final Intent intent = new Intent(this, CaptureStandaloneActivity.class);
intent.putExtra(CaptureStandaloneActivity.EXTRA_CAPTURE_CONFIG, config);
// Start the capture activity
startActivityForResult(intent, REQUEST_CODE_TO_CAPTURE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable @org.jetbrains.annotations.Nullable Intent data) {
if (requestCode == REQUEST_CODE_TO_CAPTURE) {
// See next section for handling the result
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
// ---------- Using `registerForActivityResult` ----------
private ActivityResultLauncher<CaptureConfig> launcher;
private void registerForCaptureResult() {
CaptureImages contract = new CaptureImages();
launcher = registerForActivityResult(contract, new ActivityResultCallback<BareCapturedReceiptData[]>() {
@Override
public void onActivityResult(BareCapturedReceiptData[] result) {
// See next section for handling the result
}
});
}
private void launchCaptureWithRegistration() {
launcher.launch(new CaptureConfig());
}
// ...
}
Using Captured Images
Implementing a Capture Delegate
The protocol CaptureNavigationControllerDelegate
can be implemented as an extension to an existing class (for example in a class that instantiates capture, as shown in the examples on this page), or in a separate class.
Capture provides a CaptureNavigationControllerDelegate protocol to return the result of the capture back to the integrator’s app.
The result of the capture - a CaptureResult
object - will contain all images user captured or selected from the gallery, as images
array in Data
format.
The data represents a JPEG-encoded image, and can be saved to file, or displayed. The image size and quality depends on settings, described in Captured Images Format section below.
The public Data
extension, included with Sensibill SDK, also allows you to
- Retrieve the image metadata, using
metadata
property, - Get image location (if enabled and captured) in CLLocation
format, using
getLocation()
function.
If a user cancels the operation, or an error occurs during capture, the images
array in the CaptureResult
will be empty.
Example
extension YourClass: CaptureNavigationControllerDelegate {
func captureNavigationController(_ controller: CaptureNavigationController, didFinishCapture result: CaptureResult) {
guard !result.isEmpty else {
print("User didn't capture any images, or an error occurred")
return
}
print("Received \(result.images.count) photos")
// Get image metadata
for image in result.images {
print(image.metadata)
}
}
}
Captured Images Format
All images in CaptureResult
are JPEG-encoded. However the compression quality and size of the images depends on configuration, as explained below.
The most common use case is sending the captured images to Sensibill’s API for processing. To ensure the successful processing of the document by Sensibill’s API, the images must comply with the size and quality requirements specified in Sensibill API documentation. Therefore by default the captured images are compressed and resized to a format compatible with Sensibill API.
If you require the uncompressed images in their original size, you can customize Capture.FeatureFlags
and set compressForSensibillApi
flag to false
. See Feature Switching
page for more information on how to configure Capture feature flags.
Upon completion, the CaptureStandaloneActivity
will provide the calling activity an array of BareCapturedReceiptData
(reference
) objects representing the captured receipt images.The BareCapturedReceiptData
array will be attached to the activity result data
Intent
as a Serializable
with the key CaptureStandaloneActivity.EXTRA_CAPTURED_RECEIPTS
.
If the user cancels the flow or the flow ends unexpectedly, the resultCode
will be Activity.RESULT_CANCELED
, or the returned data will be null
.
As above, examples have been provided for both the deprecated startActivityForResult
flow, as well as the registerForActivityResult
flow.
Kotlin
// ---------- Using `startActivityForResult` ----------
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_CODE_TO_CAPTURE -> {
// Result is OK when the flow was completed and images were captured
if (resultCode == Activity.RESULT_OK && data != null) {
// Retrieve captured images
val capturedImages = data.getSerializableExtra(CaptureStandaloneActivity.EXTRA_CAPTURED_RECEIPTS)
as Array<BareCapturedReceiptData>
// Process images
// ...
// *Important* clean up images
capturedImages.forEach { it.cleanup() }
} else {
// Capture flow was cancelled
Toast.makeText(this, "No images captured", Toast.LENGTH_LONG).show()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
// ---------- Using `registerForActivityResult` ----------
private fun registerForCaptureResult() {
val contract = CaptureImages()
launcher = registerForActivityResult(contract) { capturedImages ->
if (capturedImages != null) {
// Process images
// ...
// *Important* clean up images
capturedImages.forEach { it.cleanup() }
} else {
// The user cancelled the capture flow
Toast.makeText(this, "No images captured", Toast.LENGTH_LONG).show()
}
}
}
Java
// ---------- Using `startActivityForResult` ----------
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_CODE_TO_CAPTURE) {
if (resultCode == Activity.RESULT_OK && data != null) {
// Retrieve captured images
BareCapturedReceiptData[] capturedImages = (BareCapturedReceiptData[]) data.getSerializableExtra(CaptureStandaloneActivity.EXTRA_CAPTURED_RECEIPTS);
// Process images
// ...
// *Important* clean up images
for (BareCapturedReceiptData capturedImage : capturedImages) {
capturedImage.cleanup();
}
} else {
// Capture flow was cancelled
Toast.makeText(this, "No images captured", Toast.LENGTH_LONG).show();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
// ---------- Using `registerForActivityResult` ----------
private void registerForCaptureResult() {
CaptureImages contract = new CaptureImages();
launcher = registerForActivityResult(contract, new ActivityResultCallback<BareCapturedReceiptData[]>() {
@Override
public void onActivityResult(BareCapturedReceiptData[] result) {
if (result != null) {
// Process images
// ...
// *Important* clean up images
for (BareCapturedReceiptData capturedImage : result) {
capturedImage.cleanup();
}
} else {
// Capture flow was cancelled
Toast.makeText(MyActivity.this, "No images captured", Toast.LENGTH_LONG).show();
}
}
});
}