Standalone Capture
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 with Receipt Management 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 Installation Troubleshooting steps.
Adding the Required Entitlements
When a user navigates to the Capture Receipt screen of the Sensibill SDK, SDK will use device’s camera to capture receipts. In order to allow camera usage, your application must prompt the user for consent to use their device’s camera, and thus requires a camera usage entitlement. Additionally Sensibill SDK allows the users to select an existing receipt images from the device’s photo gallery, and to download previously uploaded receipt images. These operations require an access to photo library and an ability to save images in photo library entitlements correspondingly.
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 saving the receipt images to the photo gallery:
NSPhotoLibraryAddUsageDescription
(displayed as Privacy - Photo Library Additions Usage Description).
Note: If the entitlements were not provided, the application will crash with the following error:
This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app’s Info.plist must contain an NSCameraUsageDescription (or NSPhotoLibraryUsageDescription / NSPhotoLibraryAddUsageDescription) key with a string value explaining to the user how the app uses this data.
To correct the crash, add the required entitlements, as described above.
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, adding the following to your project build.gradle
file will grant access to the private maven repository.
allprojects {
repositories {
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"
// ...
}
Note: The relase 2022.0.11 of Sensibill SDK uses some of the Java 11 language APIs, and hence requires desugaring. See Java 8+ API desugaring support for more information.
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 receipts are captured. 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 . - Add the
import Sensibill
statement - 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
.
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() {
let captureNavigationController = CaptureNavigationController()
captureNavigationController.modalPresentationStyle = .overFullScreen
captureNavigationController.captureDelegate = self
present(captureNavigationController, animated: true, completion: nil)
}
}
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 config = new CaptureConfig(
false,
FlashMode.FLASH_MODE_OFF,
true,
true,
true,
true,
true,
true,
5,
false,
true,
true,
true,
true
);
// 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
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 SDKConfiguration
setting, as described in Captured Images Format section below. The Data
extension, implememented in Sensibill SDK, also allows you to retrieve the image metadata, using metadata
property, and image location 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.
The protocol can be implemented as an extension to an existing class, which instantiates capture (as shown in the example below), or in a separate class.
Example
extension YourClass: CaptureNavigationControllerDelegate {
func captureNavigationController(_ controller: CaptureNavigationController, didFinishCapture result: CaptureResult) {
guard !result.images.isEmpty else {
print("User ddidn'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.
One of the common use cases is sending the resulting images to Sensibill’s API for receipt processing. To ensure a successful processing of the receipt by Sensibill’s API, the images must comply with the size and quality requirements specified in Sensibill API documentation.
Sensibill SDK for iOS provides a SDKConfiguration.shared.capture.features.compressForSensibillAPI
flag, which allows to ensure that the resulting CaptureResult
contains the images compressed and resized to a format compatible with Sensibill API. The flag is set to true
by default.
If you require the images the uncomplressed images in their original size, you can set compressForSensibillAPI
flag to false
.
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();
}
}
});
}