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

  1. 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.

  2. 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 run pod 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.

  3. 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

  1. Open Info.plist of your application.
  2. 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

  1. 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 .
  2. Add the import Sensibill statement
  3. Instantiate CaptureNavigationController
  4. Select a modalPresentationStyle for the CaptureNavigationController .
  5. Set a captureDelegate to an instance of the class you implemented in step 1.
  6. 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();
            }
        }
    });
}

Next steps