Spend Manager SDK
This guide will take you through the process of setting up the Spend Manager SDK, incorporating:
- Receipt capture
- Receipt management UI
For a guide on setting up standalone capture to only capture images see Standalone Capture .
Before you begin
Before you can authenticate and run the SDK you will need to setup your authentication server.
To arrange access to our SDK repositories and authentication 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.
Configuring Sensibill Environment Properties
When started, Sensibill SDK will communicate with Sensibill environment. The definition of the environment the SDK will attempt to contact must be specified in Sensibill.plist
configuration file, which can be provided in the main bundle of the host app, or in a separate bundle, provided via externalBundle
parameter of SDKConfiguration
(further referred as an “external bundle”).
If you wish to add Sensibill.plist
, and other assets, like custom images and strings, in an external bundle, see iOS External Bundles
page for more information.
Note: If a valid configuration file is provided in both the main and external bundle, the file from the external bundle will be used.
Steps
You can download an example of the properly structured properies file: Sensibill.plist
Create
Sensibill.plist
in the main bundle of your target, or in the external bundle you created.Add the following required properties to the
Sensibill.plist
file:ENVIRONMENT: String CERTIFICATE_PINNING_FILES: Array of Strings
Note: Please ensure the URL provided for the ENVIRONMENT key does not have a trailing slash.
If you are using a your company’s Authentication Server, and a custom
TokenProvider
, no additional values are required. When using Sensibill’s Password Authentication, follow Password Authentication section to provide additional keys.
Configuring Certificate Pinning
By default certificate pinning is enabled on Sensibill SDK. For certificate pinning to function, your application’s Target must include the certificates provided by Sensibill (named COMODO.cer
and GLOBOSIGN.cer
), and must contain a list of valid certificate names inside the Sensibill.plist
file.
Steps
Import certificates provided by Sensibill into your project.
Include them as a Bundle resource in every app’s target that uses the Sensibill SDK and make sure target membership for the certificates is checked for the application.
Add the names of the certificate files under the
CERTIFICATE_PINNING_FILES
key ofSensibill.plist
:CERTIFICATE_PINNING_FILES Item 0: COMODO.cer Item 1: GLOBALSIGN.cer
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.
The primary method of installing the SDK is via Sensibill’s private maven server. All stable SDK 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 get a username and password. Once you have maven credentials, adding the following to your project-level build.gradle
file will allow access to the private maven server.
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'
}
}
}
}
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.
Sensibill Gradle Dependencies
Once you have access to Sensibill Artifacts, adding the SDK to your project is a matter of adding the correct dependencies to your app
level build.gradle
file.
For the full SDK add the below where x.x.x is the version.
implementation "com.getsensibill:sensibill-sdk-all:X.X.X"
See Installation options for other options.
The Sensibill SDK is available through npm.
NPM
# Using npm
npm i @getsensibill/web-sdk
# Using yarn
yarn add @getsensibill/web-sdk
Import
Import the module where needed:
import { SensibillSDK } from '@getsensibill/web-sdk'
Authentication
Sensibill recommends the use of Custom Token Provider Authentication to allow the host app to control token management. The SDK supports OAuth2-compatible tokens, and JWT tokens.
For other authentication options see Authentication Options
Implement a custom token provider, by adhering to TokenProvider
protocol, whose function provideTokenReplacement
will provide the tokens to iOS SDK:
import Sensibill
class CustomTokenProvider: TokenProvider {
func provideTokenReplacement(completion: @escaping (Credentials?, Error?) -> ()) {
// Obtain a token from your integration server
var request = URLRequest(...)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// On callback from your authentication server
// 1. Check for error. If error is returned, pass this error in callback to SDK:
if error != nil {
completion(nil, error)
return
}
// 2. Parse response to obtain access token
let accessToken = ...
// 3. Construct a Credentials object and return it in a callback to SDK:
let credentials = Credentials.init(accessToken: accessToken)
completion(credentials, nil)
}
task.resume()
}
}
Pass the custom token provider instance to the SDK on start. The SDK will retain an instance of the custom token provider until it is stopped. See Launching the SDK section below for further information.
The SDK will request user access tokens using a TokenProvider
passed to the SDK during initialization. The SDK will call TokenProvider.provideTokenReplacement()
when a new token is required.The SDK may request a token from the token provider when:
- The SDK is starting
- The previous token has expired
- The previous token is invalid
Extra notes:
- The
TokenProvider
must provide a unique token for each user - The
userIdentifier
parameter should be used to identify users to the Integration server, which should return a different access token for each user - The token provided is used for communication with the Sensibill API. Each token is tied to a particular user, therefore it is important to ensure the token provided is always for the intended user.
- WARNING: Reusing tokens across users will result in information for the wrong user being displayed in your integration.
Kotlin
val tokenProvider = TokenProvider.fromLambda { oldAuthToken, userIdentifier, listener ->
// The Sensibill SDK calls this method when a new token is required.
// Provide new valid token for user matching userIdentifier
MyIntegrationServer.fetchNewAccessToken(object : IntegrationServerListener {
override fun onNewAccessTokenRetrieved(newAccessToken: String?) {
if (!newAccessToken.isNullOrEmpty()) {
// Pass new access token to SDK via the `onTokenProvided` method
listener.onTokenProvided(newAccessToken)
} else {
// If unable to provide new access token, call the `onFailed` method
listener.onFailed("Couldn't retrieve new token!")
}
}
})
}
Java
final TokenProvider tokenProvider = new TokenProvider() {
@Override
public void provideTokenReplacement(@Nullable String oldAuthToken, @NotNull String userIdentifier, @NotNull TokenProvider.OnTokenProviderListener listener) {
// The Sensibill SDK calls this method when a new token is required.
// Provide new valid token for user matching userIdentifier.
MyIntegrationServer.fetchNewAccessToken(new IntegrationServerListener() {
@Override
public void onNewAccessTokenRetrieved(String newAccessToken) {
if (newAccessToken != null && !newAccessToken.isEmpty()) {
// Pass new access token to SDK via the `onTokenProvided` method
listener.onTokenProvided(newAccessToken);
} else {
// If unable to provide new access token, call the `onFailed` method
listener.onFailed("Couldn't retrieve new token!");
}
}
});
}
};
You must specify the environment you wish to connect to, failure to do so will result in an invalid token error.
Available environments
- sandbox -client integration sandbox
- production
To authenticate pass a valid access token to the SensibillSDK instance:
const instance = await SensibillSDK.create(
// Provide a reference to the element you want to contain the iframe
document.getElementById('your-element'),
// Provide an initialization object which contains your client id along with the environment you wish to // connect to
{ clientID: 'your-client-id', environment: 'sandbox' },
// Provide a configuration object which contains your access token at a minimum
{ authentication: { accessToken: 'a-valid-access-token' } }
);
To support the automatic refresh of token register a listener as below:
function refreshAuthentication() {
// Do what you need to get your new token. This can be synchronous, or asynchronous. The iframe will wait.
const myNewToken = getNewToken();
// Once you have it, set the new token on the instance
instance.setToken(myNewToken);
}
// Run the refreshAuthentication function whenever the instance asks for a new token
instance.registerListener(refreshAuthentication, 'getNewToken')
Launching the SDK
The start
function will first ensure the valid authentication exists for the user (it may call token provider to obtain a new token, or use an existing token if it’s valid). Once the user is authenticated, the start
function will acquire the information required for interaction with SDK. This information is cached on device
, but is verified and updated from Sensibill’s backend on every launch.
Info
The start
function will not work in offline mode, it requires a network connection to succeed.
Steps
- Create an instance of your custom token provider, and perform all the steps required to make sure the
provideTokenReplacement
function is ready to be called. Note: token provider instance can always be accessed usingSensibillSDK.shared.tokenProvider?
- Call the
start
function start
is an asynchronous operation. Caller must wait for the start callback to return and succeed before attempting any further interaction with the SDK.
Cache Identifier
The cache identifier parameter of the start
function allows the SDK to keep a unique cache for the user, and support multiple user identities (for example personal and business accounts) cached on the same device. Hence, the cache identifier should be unique for each user identity. A specific format for identifier depends on hosting application usage of SDK.
Examples of good cache identifiers:
- User email, e.g.
john.smith@example.com
- User email, together with type of account:
john.smith@example.com-business
import Sensibill
// Make sure externalBundle is initialized (if used) - see Configuring SDK section.
let tokenProvider = CustomTokenProvider()
// Additional operations required to prepare token provider
SensibillSDK.start(tokenProvider: tokenProvider, cacheIdentifier: "cacheIdentifier") { error in
// Check for an error. If an error is returned, interaction with the SDK will not be possible
guard case .none = error else {
// Add custom error handling
return
}
// Start interacting with the SDK
}
Error Codes
start
may return the following error codes
sdkCannotStart
will be returned by the start callback if thestart
function was called when the SDK was already starting, running, or stopping. If the SDK is starting or running, callstop
first. If the SDK is stopping, wait for the stop operation to complete.sdkStartWasInterrupted
will be returned by the start callback, if thestop
function was called while the SDK was starting. This result is provided for information only: since the app called stop while the SDK was starting, the result of start callback is presumably no longer important.
Start Spend Manager
At the minimum, a host UIViewController
must be provided to start the Spend Manager. By default, Sensibill’s UI will be presented modally, with an animation, over the provided host. You can customize presentation and animation options. See Launch Spend Manager
page for more customization options.
The integrating app must also implement a SensibillUICoordinatorDelegate
protocol, and implement its required coordinatorWillTerminateWebUI
function.
Example:
private var sensibillUICoordinator = SensibillUICoordinator(host: self)
sensibillUICoordinator.delegate = self
sensibillUICoordinator.start()
...
extension MyViewController: SensibillUICoordinatorDelegate {
// Called when the SDK requests termination
func coordinatorWillTerminateWebUI(_ coordinator: SensibillUICoordinator) {
// Clean up the sensibillUICoordinator
sensibillUICoordinator = nil
}
}
Initializing the SDK
InitializationBuilder
Initialization can begin by using the InitializationBuilder
to create a builder object which will be used to build the Initializer
required to initialize the SDK.
Start by creating the builder with the application context and a Sensibill DefaultEnvironment Kotlin
val sensibillEnv = DefaultEnvironment.RECEIPTS_SANDBOX
val builder = InitializationBuilder(applicationContext, sensibillEnv)
Java
final DefaultEnvironment sensibillEnv = DefaultEnvironment.RECEIPTS_SANDBOX;
final Context applicationContext = getApplicationContext();
final InitializationBuilder builder = new InitializationBuilder(applicationContext, sensibillEnv);
The TokenProvider
created in the previous Authentication must also be passed to this builder.
Kotlin
builder.authTokenProvider(tokenProvider)
Java
builder.authTokenProvider(tokenProvider);
If required, some additional customization such as the certificate pinning settings can also be provided here. See the InitializationBuilder Reference Documentation for a complete list of customizations that can be provided.
Build and Initialize
Once the InitializationBuilder
object is created and optional extensions are added, an Initializer
can be built by calling builder.build()
. This can then be passed to the SensibillSDK.initialize()
method along with an SDKInitializeListener
.See below for example:
NOTE: By the Sensibill SDK lifecycle rules, if SensibillSDK.initialize()
and / or SensibillSDK.start()
have already been called in this instance of the app, SensibillSDK.release()
should be called before re-initializing.
Kotlin
SensibillSDK.initialize(builder.build(), object : SDKInitializeListener {
override fun onInitialized() {
// Initialization succeeded, you can now continue with starting the SDK
}
override fun onInitializationFailed() {
// Initialization failed. Try again.
}
})
Java
SensibillSDK.getInstance().initialize(builder.build(), new SDKInitializeListener() {
@Override
public void onInitialized() {
// Initialization succeeded, you can now continue with starting the SDK
}
@Override
public void onInitializationFailed() {
// Initialization failed. Try again.
}
});
Starting the SDK
Once the SDK has been initialized, it can then be started with SensibillSDK.start()
.
Note: the userIdentifier
you provide to .start()
will be the one passed to your TokenProvider
when the SDK requests new tokens.
Kotlin
val userIdentifier = "currentUser"
SensibillSDK.start(userIdentifier, object : SDKStartup {
override fun onSDKStarted() {
// The SDK has successfully started. All functionality can now be safely used.
}
override fun onSDKFailed(loginError: LoginError?, errorMessage: String?) {
// The SDK failed to start. This may be because it has not been initialized yet, or the `TokenProvider`
// was not able to provide a token. See `loginError` for details
}
})
Java
final String userIdentifier = "currentUser";
SensibillSDK.getInstance().start(userIdentifier, new SDKStartup() {
@Override
public void onSDKStarted() {
// The SDK has successfully started. All functionality can now be safely used.
}
@Override
public void onSDKFailed(LoginError loginError, String errorMessage) {
// The SDK failed to start. This may be because it has not been initialized yet, or the `TokenProvider`
// was not able to provide a token. See `loginError` for details
}
});
Launching Spend Manager UI
Once the SensibillSDK
is started, we can launch the Spend Manager UI.
The most basic way to launch the Spend Manager UI is to directly start WebUiActivity
. This will use the default configurations, navigating directly to the dashboard.
Kotlin
startActivity(Intent(this, WebUiActivity::class.java))
Java
startActivity(new Intent(this, WebUiActivity.class));
Opening a Sensibill view
Pass in the name of the element you would like to host the SDK in, this will create an iframe and load the SDK. SensibillSDK.create
returns a promise. Use await to ensure instance
promise is resolved before referencing instance
later.
const instance = await SensibillSDK.create(
// Provide a reference to the element you want to contain the iframe
document.getElementById('your-element'),
// Provide an initialization object which contains your client id
{ clientID: 'your-client-id', environment: 'sandbox' },
// Provide a configuration object which contains your access token at a minimum
{ authentication: { accessToken: 'a-valid-access-token' } }
);
Stopping the SDK
By default the stop
function will first attempt to call Sensibill’s backend to invalidate the user token and clear the user session. Regardless of the function’s success, the local state will be cleared and the result of the remote call will be returned in error
of the stop
callback.
To disable token invalidation, provide a flag to the stop function: stop(invalidateToken: false) { ... }
Once the stop
function is complete, the SDK can be started again.
Note
- For the
stop
function having a network connection is optimal, but the SDK will stop and reset its state locally regardless. - The stop function cannot run if another
stop
function is already in progress. Calling stop on already stopped SDK is supported (clears local state only)
Steps
- Call the
stop
function stop
is an asynchronous operation. The caller must wait for the stop callback to return before attempting to start the SDK again.
Note: although stop
may return an error in the case of a failed remote call (e.g. when the app is offline), the local state will still be cleared. Hence handling an error returned in stop
callback is optional.
Example
import Sensibill
SensibillSDK.stop { _ in
// SDK is stopped
}
Error Codes
stop
may return the following error codes
sdkIsAlreadyStopping
will be returned by the stop callback, ifstop
is called while another stop operation is already running.
Note:
- If
stop
was called while the application is starting, stop may need to wait to be able to interrupt the start. - Two
stop
operations cannot run simultaneously, but runningstop
when the SDK is already stopped will return no error and is supported.
Once Sensibill SDK functionality is no longer being used, the SDK should be stopped in order to free up resources.
To stop the SDK call SensibillSDK.release()
.
Kotlin
SensibillSDK.release()
Java
SensibillSDK.getInstance().release();
Not applicable.