# Android Integration

# General Information

Getting Started
Push Notification

# Modules

AnybillBase
AnybillContentArea

WARNING

# Breaking Changes in 2.0.0 - Migration Guide

With the release of version 2.0.0, several changes have been introduced that require adjustments on your side. This migration guide is designed to help you transition smoothly and understand the key changes.


# 1. Renaming

We have standardized our terminology by migrating to the correct naming of our product. All "Bill" references have been renamed to "Receipt":

  • BillDtoReceiptDto:
    All subtypes of "Bill" have been removed and replaced with a unified ReceiptDto. This means you no longer need to handle multiple types.

  • BillProviderReceiptProvider:
    The ReceiptProvider now consolidates all methods previously found in BillProvider.


# 2. Receipt Model Changes

  • The receipt model has been streamlined to include only frontend-relevant parameters.
  • The isFavourite flag has been relocated to Receipt.Misc.isFavourite for better categorization.

# 3. Receipt Method Renaming

The renaming of classes and models has also impacted the method names in the ReceiptProvider. Below are the most relevant changes:

  • LiveData Updates:

    • bills LiveData → receipts LiveData
  • Method Renames:

    • exportBillAsPDFgetReceiptPdf
    • updateBillCommentupdateReceiptNote
    • updateIsFavouritetoggleIsFavourite
  • Receipt Retrieval Updates:

    • Methods such as getBills, updateBills, and similar have been replaced with a new, optimized receipt retrieval process (details below).

# 4. Receipt Retrieval Overhaul

We’ve introduced a significantly improved pagination and caching system designed to handle a larger volume of receipts efficiently.

  • Refer to this Guide for implementing the new optimized receipt retrieval process.

# Deprecated Documentation

If you are still using a previous version, documentation for deprecated APIs can be found here.


# Support

If you encounter any issues during the migration process, don’t hesitate to reach out to us. We're here to help!

# Getting Started

# Resolving the SDK using gradle:

The anybill SDK for android is hosted in a maven repository. To resolve the sdk using gradle, follow these steps:

  1. Add this to your project's 'build.gradle'. You can find your credentials in the provided integration documents.
allprojects {
    repositories {
        mavenCentral()
        maven {
            url "https://anybill.jfrog.io/artifactory/anybill_android_sdk"
            credentials {
                username = {username}
                password = {password}
            }
        }
    }
}
  1. Add the desired anybill modules to your module's 'build.gradle'. The base module required for the basic usage of the SDK
dependencies {
    implementation 'de.anybill.anybill_android_sdk:base:{latest version}'
}

# Setting the client Id

Within the provided integration documents you are going find a Client ID. To set the Client ID in your app, add the ID as meta data value anybill_client_id in your app's manifest file. This is going to allow us to hook all of your API activity to your Client Id which can be used for analytics or support purposes later on.

<application
...
>
    ...
    <meta-data android:name="anybill_client_id" android:value="{your_client_id}"/>
    ...
</application>

# Change the api mode to staging

For developing purposes you can change the api environment to Staging by adding the Staging url (https://app.stg.anybill.de/) as value for the key anybill_base_url in your AndroidManifest.xml:

<application
...
>
    ...
    <meta-data android:name="anybill_base_url" android:value="https://app.stg.anybill.de/"/>
    ...
</application>

# Set custom base url

If your integration requires additional security, traffic control, or compliance measures—such as routing through a reverse proxy, API gateway, or similar infrastructure—you can configure the SDK to use a custom base URL for all API calls to the anybill API. This setup enables you to route traffic through a custom URL hosted by your organization, providing greater flexibility for monitoring, load balancing, and compliance.

To set a custom base URL, add the following configuration in your AndroidManifest.xml file by defining the key anybill_base_url with your custom URL as the value:

<application
...
>
    ...
    <meta-data android:name="anybill_base_url" android:value="https://your.custom.url/"/>
    ...
</application>

# Enable java.time for device under API 26

If you are targeting devices under API 26, we recommend enabling java.time usage for lower API usages. Add following line to your compile options in your app build.gradle:


    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        ...
    }

Exceptions

As our SDK has a minimum API Version of 24, we still support API 24 and 25. However without enabling java.time exceptions may occur during the saving of Token Data.

# Initialize AnybillLogger as second Datadog instance

The anybill SDK internally utilizes a Datadog instance to log errors that occur within the SDK. We do not log any information from your applicatio as our logger strictly filters for the SDK's package name.

If your application already uses Datadog, manual initialization of the AnybillLogger is required during the app's startup.

Disabling Automatic Initialization

To disable the automatic initialization of the AnybillLogger as the default instance, add the following meta-data Boolean flag to your AndroidManifest.xml:


<meta-data android:name="anybill_toggle_logger_initialization" android:value="true"/>

Manual Initialization of AnybillLogger

To manually initialize the anybill logger as a secondary Datadog instance, ensure that you invoke AnybillLogger.initAnybillLogger() after successfully initializing your Datadog SDK.

Here’s an example of how to set up the logging:


fun initLogging() {
    // Step 1: Initialize your Datadog instance as usual
    val configuration = Configuration.Builder(..).build()
    val yourInstance = Datadog.initialize(..)
    val logsConfig = LogsConfiguration.Builder().build()
    Logs.enable(logsConfig, yourInstance)
    val yourLogger = Logger.Builder(yourInstance).build()

    // Step 2: Initialize the anybill Logger after your Datadog instance setup is complete
    AnybillLogger.initAnybillLogger()
}
    

# Usage of the SDK

# Error Handling

The anybill sdk uses a custom error handling model. All methods return a type of the sealed class ApiResult including the return object on success and/or information about the error which occurred. Detailed description of the possible error codes can be found in the corresponding documentation of the methods.

sealed class ApiResult<out T> {

    /**
     * Success\
     * Used when Api Call and serialization was successful\
     *
     * @param T Type [T] of the object which should be returned if Api call and JSON Serialization was successful
     * @property value Object of type [T] holding the serialized result of the api call
     */

    data class Success<out T>(val value: T) : ApiResult<T>()

    /**
     * Generic error\
     * Used if a generic error occurs during execution of the api call or serialization of the received data\
     * Possible GenericError's of each operation are listed in the method documentation
     *
     * @property code Nullable [Int] with the Error Code of the exception
     * @property error Nullable [ErrorResponse] object with further exception information
     */

    data class GenericError(val code: Int? = null, val error: ErrorResponse? = null) : ApiResult<Nothing>()

    /**
     * Empty success\
     * Used if the execution of the api call and the serialization of the result was successful without a need of a result object (e.g. token operations)\
     *
     */

    object EmptySuccess : ApiResult<Nothing>()

    /**
     * Network error\
     * Used if a network error occurs during execution of the api call (e.g. timeout, no network)
     */

    object NetworkError : ApiResult<Nothing>()
}

Example usage of the error model based on the user info method of the AuthProvider.

fun getUserInfo() {
        viewModelScope.launch {
            when (val userInfoTask = AuthProvider.getUserInfo()) {
                is NetworkError -> //Error Handling
                is GenericError -> {
                    when(userInfoTask.code){
                        NO_USER -> //Error Handling
                        NOT_SYNCABLE -> //Error Handling
                        SERVER_ERROR -> //Error Handling
                        INVALID_REFRESH_TOKEN -> //Error Handling
                    }       
                }
                is Success -> {
                    userInfoTask.value //Further implementation
                }
            }
        }
    }

Back to top

# Push Notification

WARNING

Due to increasing restrictions of Firebase allowing secondary projects in mobile application, we do recommend implementing our receipt webhook instead. The webhook will notify you in real time about a new receipt for a user, allowing you to trigger the Push Notification from your system.

The anybill sdk also provides the possibility to receive push notification from anybill's firebase project. Push notifications are used for notification for newly received receipts. The usage of push notification is optional for the core modules of the anybill sdk.

How to implement push notifications for the anybill sdk:

# Without an existing Firebase project

If the app is in background, push notification are handled by google services and displayed automatically. If the app is in foreground push notification are caught by the FirebaseMessagingService and have to be displayed manually. The anybill sdk provides an interface which can be implemented to your own notification service to display notifications:

  1. Create a notification handler:
class YourNotificationHandler(private val context: Context) : AnybillMessagingHandler {

    override fun onAnybillMessageReceived(remoteMessage: RemoteMessage) {
        // Display Notification
    }
    
}
  1. Initialize anybill notification service with your messaging handler in the onCreate() of your application.
class YourApplication : Application() {
        override fun onCreate() {
            super.onCreate()

            // Initialize the anybill Firebase project

            AnybillFirebaseInitializer.initFirebaseProject(this)

            // Set your notification handler

            AnybillMessagingService.setMessageHandler(YourNotificationHandler(this))

        }
    }

Notifications can be customized or extended with Intents to navigate different views.

# With an existing Firebase project

If your app already uses a Firebase project, the anybill Firebase project can be initialized as a secondary Firebase project. To ensure that the anybill project is not initialized as the default project, call the initializing method after your default project was initialized. The default Firebase project gets initialized by the FirebaseInitProvider. To enable a manual initialization of the Firebase project, disable the provider by adding this to your app's manifest:

<application>
    ...
    <provider
        android:name="com.google.firebase.provider.FirebaseInitProvider"
        android:authorities="${applicationId}.firebaseinitprovider"
        tools:node="remove"
    />
    ...
</application>

The default project can now be initialized using FirebaseApp.initializeApp() in the onCreate() method of your application:


    class YourApplication : Application() {
        override fun onCreate() {
            super.onCreate()
            
            // Initialize your default Firebase project

            FirebaseApp.initializeApp(this)

            // Initialize the anybill Firebase project

            AnybillFirebaseInitializer.initFirebaseProject(this)
        }
    }

Implementation of notifications for a secondary project differs if the default project also uses Firebase Messaging:

# Default project does not use Firebase Messaging

If the default project does not use Firebase Messaging, displaying notification works as described in the section Without an existing Firebase project. To make sure that the anybill project is not initialized as the default project, initialize your Firebase project before initializing the anybill Firebase project (see step 2). The anybill sdk provides an interface which can be implemented to your own notification service to display notifications:

  1. Create an notification handler:
class YourNotificationHandler(private val context: Context) : AnybillMessagingHandler {

    override fun onAnybillMessageReceived(message: RemoteMessage) {
        // Display Notification
    }
    
}
  1. Initialize anybill notification service with your messaging handler in the onCreate() of your application.
class YourApplication : Application() {
        override fun onCreate() {
            super.onCreate()
            
            // Initialize your default Firebase project

            FirebaseApp.initializeApp(this)

            // Initialize the anybill Firebase project

            AnybillFirebaseInitializer.initFirebaseProject(this)

            // Set your notification handler

            AnybillMessagingService.setMessageHandler(YourNotificationHandler(this))

        }
    }

# Default project uses Firebase Messaging

Firebase only allows the usage of one FirebaseMessagingService. To allow the anybill sdk to execute operation on specific notifications the notification you receive in your notification service have to be given to the anybill sdk.

  1. Sample code of your notification service:
class YourNotificationService : FirebaseMessagingService(), AnybillMessagingHandler {

    //Initialize an instance of AnybillMessagingService

    private val anybillMessagingService by lazy {
        AnybillMessagingService.getInstance(this)
    }

    //In the onMessageReceived of your FirebaseMessagingService:
    //Check if a notification should be handled by the anybill sdk by calling isAnybillMessage()
    //Let anybill messaging service handle the notification

    override fun onMessageReceived(message: RemoteMessage) {
        if (message.isAnybillMessage()) {
            anybillMessagingService.onAnybillMessageReceived(message)
        } else {
            //Display Notification
        }
        super.onMessageReceived(message)
    }

    //Firebase Messaging Tokens mostly get reset for both projects simultaneously.
    //To reset the notification token of the logged in anybill user, call onNewAnybillToken in your onNewToken method

    override fun onNewToken(token: String) {
        anybillMessagingService.onNewAnybillToken()
        super.onNewToken(token)
    }

    //Display the received anybill notification after it was handled by the anybill sdk.

    override fun onAnybillMessageReceived(messageBody: RemoteMessage) {
        //Display Notification
    }
}

Back to top

# AnybillBase

Table of Contents

# Authentication

The Base module provides essential authentication functions within the anybill SDK. Most of these functions are accessible through the AuthProvider singleton, which manages user authentication and token storage.

# Authentication Overview

The anybill SDK handles authentication seamlessly within its internal processes. Once a user successfully authenticates, an Access Token and a Refresh Token are securely stored in the device's local keystore:

  • Access Token: Valid for 24 hours and used to authorize user requests to the anybill API.
  • Refresh Token: Valid for 90 days and used to renew the Access Token upon expiration. When the Refresh Token expires, the user will need to reauthenticate.

This automated process minimizes the need for manual token handling, ensuring a smooth and secure experience for both users and developers.

# Integration with Loyalty Card and Payment Card Services

For integrations involving receipt retrieval by loyalty card or payment card, you will need to create users and obtain tokens via the Partner Platform API. These tokens can then be used to initialize the anybill SDK, enabling receipt functionality tied to specific loyalty or payment card details. For detailed instructions, refer to the Partner Platform API documentation.

# Authenticate User

You can authenticate a user in the SDK through two methods:

  1. Credentials Login: Authenticate an existing anybill user using valid credentials (email and password).
  2. Token-Based Login: Use token information obtained from the Partner Platform API to initialize the SDK and authenticate the user without requiring credentials.

Credentials Login

Anybill users can be logged in using the loginUser() method of the AuthProvider. It requires valid login information of a registered anybill user (email and password).

    fun loginUser(email: String, password: String) {
        viewModelScope.launch {
           when (val loginCall = AuthProvider.loginUser(email = email, password = password)){
               is ApiResult.EmptySuccess -> //Success
               is ApiResult.GenericError -> //Error Handling
               is ApiResult.NetworkError -> //Error Handling
           }             
        }
    }

Back to top

Token-Based Login

If your App has an own user account system you can link an anybill user to your user account by using the linked token user. For detailed instructions, refer to the Partner Platform API documentation.

  • Get a token from the anybill Partner API by linking you account system to an anybill id.
  • Create an instance of TokenUser with the received token-information
  • Login the TokenUser with the anybill sdk

    fun getTokenForUser() {
        viewModelScope.launch {
            val result = YourApi.getTokenForUser()
            val user = TokenUser(result.accessToken, result.refreshToken, result.expiresIn)
            loginAnybillUser(user)
        }
    }


    suspend fun loginAnybillUser(tokenUser: TokenUser){
        when(val tokenLogin = AuthProvider.loginTokenUser(tokenUser)){
            is ApiResult.EmptySuccess -> //Success
            is ApiResult.GenericError -> //Error Handling
        }
    }

TIP

When using the Token Based Login you'll have to check for a failing Refresh Token Call on the first anybill API Call you invoke. When the error is triggered you'll have to retrieve new authentication information from the anybill Partner Platform API.

Re Auth Sdk

Check for failing Refresh Token using an Extension of ApiResult.GenericError:

isExpiredRefreshTokenError()


viewModelScope.launch {
    when (val userCall = AuthProvider.getUserInfo()) {
        is Success -> // Continue
        is GenericError -> {

            if (userCall.isExpiredRefreshTokenError()) {
                // Reauthenticate user by fetching token information using the anybill Partner Platform Api
            } else {
                 when(userCall.code) {
                    AuthErrors.NO_USER -> // Handle Error
                    DefaultErrors.UNKNOWN_ERROR_CODE -> // Handle Error
                }
            }

        }
        is NetworkError -> // Network Error Handling
    }
}

TIP

If the token refresh process fails, the SDK automatically logs the user out, assuming that their authentication session has expired. Subsequent API calls will return an ApiResult.GenericError with the error code AuthErrors.NO_USER, indicating the absence of an authenticated user session.

We recommend calling AuthProvider.getUserInfo() during your app's initialization phase to verify the presence of a valid authenticated session.

Important Note: We strongly advise against re-fetching the authentication token from our Partner Platform API on every app launch. Doing so can generate excessive network traffic, negating the performance benefits provided by the mobile SDK's token caching and session management capabilities.

Back to top

# Retrieve User Information

Once a user is authenticated, you can retrieve information about the anybill user using the anybill SDK. The AnybillUser model provides the following parameters:

data class AnybillUser(
    /**
    The internal anybill ID of the user.
     */
    val id: String,
    
    /**
    The email of the anybill user. If you are using Token-Based Login,
    this email is auto-generated during user creation and does not represent a 
    valid email address.
     */
    val email: String,
    
    /**
    A flag indicating if the user is anonymous 
    (i.e., credentials are not known to end user).
     */
    val isAnonymous: Boolean,
    
    /**
    The external ID used during the creation of the user in the Partner Platform API. 
    In Token-Based Login, this parameter represents your `userId`, `customerId`, or `customerCard`. 
    This identifier is used to recognize the user at the POS and to assign receipts to them.
     */
    val externalId: String?,
    
    /**
    Notification configuration for the user. Currently, only email notifications are supported.
     */
    val notificationConfiguration: NotificationConfiguration?
)

You can use following methods to retrieve information about the user:

GetUserInfo

Retrieve the complete user model using either the API or cached user information (note that cached data may be outdated). You can enable cache usage by setting the useCache parameter to true.

viewModelScope.launch {
    when (val userCall = AuthProvider.getUserInfo(useCache = true)) {
        is Success -> {
            val anybillUser = userCall.value
        }
        is GenericError -> // Handle generic error
        is NetworkError -> // Handle network error
    }
}

GetUserId

For quick access to the user ID without needing a coroutine scope, you can use AuthProvider.getUserId. This method retrieves the cached user ID from the SDK's shared preferences. The ID is updated with each authentication change and can be used as a quick indicator of a logged-in user.

WARNING

The presence of a user ID does not confirm the user's authentication status, as this call does not validate the cached token.


AuthProvider.getUserId()

Back to top

# Logout

Logging out an user deletes all of user's app data including cached receipts, authentication information and app settings (of the anybill sdk).

    AuthProvider.logoutUser()

Back to top

# Receipts

Singleton ReceiptProvider grants access to the anybill receipt functions. Most functions of the ReceiptProvider are suspend functions and have to be called in a coroutine scope.

# Retrieving receipts

The anybill SDK offers two distinct approaches for fetching user receipts:

  1. Direct API Access: Use the ReceiptProvider.getReceipts() method to directly access the API. This approach allows you to implement custom fetching and pagination logic based on your specific requirements.

  2. Optimized SDK Caching Process: Leverage the SDK's built-in caching and optimized pagination for efficient receipt retrieval by using the initiateReceiptQuery() and continueReceiptQuery() methods in combination with an exposed observable receipt list. This approach simplifies the retrieval process and reduces the need for manual pagination handling.

Detailed information about both approaches is provided below:

Direct API Access

The getReceipts() method allows you to retrieve user receipts with pagination support. The result includes the receipts, the total count of available receipts, and a continuation token that can be used to fetch subsequent batches.

You can customize the request with the following parameters:

  • take: Specifies the number of receipts to fetch in each batch. The default and maximum value is 100.

  • continuationToken: A nullable token used for paginating through the query results. If null, a new query is initiated. To continue fetching from the previous result, use the continuationToken provided in the last response.

  • orderBy: Specifies the field used for ordering the receipts. Currently, only Date is available.

  • orderDirection: Defines the sort direction for the receipts, either Ascending or Descending.

TIP

Important:

Due to database restrictions, you must specify the orderBy and orderDirection parameters for every page of the query.

Also, remember to reset the continuationToken if you modify any query parameters.

Example implementation in ViewModel:


    val receipts: MutableList<Receipt> = mutableListOf()

    var continuationToken: String? = null

    // Iniate query without Continuation Token
    fun getFirstPage(){
        viewModelScope.launch {
            when (val receiptCall = ReceiptProvider.getReceipts(
                100, 
                null, 
                ReceiptOrderField.Date, 
                OrderDirection.Descending
            )) {
                is NetworkError -> // Network Error Handling
                is GenericError -> // Generic Error Handling
                is Success -> {
                    handleContinuationResult(receiptCall.value)
                }
            }
        }
    }

    // Continue query with Continuation Token
    fun getNextPage(){
        viewModelScope.launch {
            when (val receiptCall = ReceiptProvider.getReceipts(
                100, 
                continuationToken, 
                ReceiptOrderField.Date, 
                OrderDirection.Descending
            )) {
                is NetworkError -> // Network Error Handling
                is GenericError -> // Generic Error Handling
                is Success -> {
                    handleContinuationResult(receiptCall.value)
                }
            }
        }
    }

    private fun handleContinuationResult(continuationReceiptList: ContinuationReceiptList){
        // add continuationReceiptList.receipts to displayed list
        receipts.addAll(continuationReceiptList.receipts)

        // Check for more results
        if (continuationReceiptList.continuationToken.isNullOrEmpty()) {
            // Remove continuationToken - no more receipts to fetch
            continuationToken = null
        } else {
            // Save continuationReceiptList.continuationToken to be used in the next request
            continuationToken = continuationReceiptList.continuationToken
        }
    }

Back to top

Optimized SDK Caching Process

The anybill SDK offers an optimized receipt pagination process with automatic caching, providing efficient querying and display of receipts. This feature stores receipts in a local database, allowing for quicker access and better performance. Receipt actions such as deletion, edits, or marking receipts as favorites are automatically updated in the cached receipt list, making it easy to integrate receipt-related features without manual updates to the displayed list.

To enable this, the ReceiptProvider exposes a LiveData object, receipts, which represents a live, up-to-date view of the cached receipts. The initiateReceiptQuery and continueReceiptsQuery methods allow you to refresh or extend the receipt list with new data as needed.

Similar to direct API access, you can customize the query with the following parameters:

  • take: Specifies the number of receipts to retrieve in each batch, with a maximum of 100 by default.
  • orderBy: Specifies the field for ordering receipts. Currently, only Date is supported.
  • orderDirection: Defines the sort direction for receipts, either Ascending or Descending.

TIP

Note on Pagination and Sorting

The continuation token and query management are handled internally by the SDK, so there is no need for manual handling to load additional pages. However, you can check if further pages are available by evaluating whether ContinuationReceiptList.continuationToken == null.

If you wish to change the sorting or direction of the receipt list, use the initiateReceiptQuery() method again. This will reset the locally cached receipts and update the LiveData object accordingly.

Example implementation in a ViewModel:

Setup Live Data

The exposed ReceiptProvider.receipts automatically includes the cached receipts of previous queries. Without calling an API Call you can already display these receipts to quickly provide information to the end user.


    /**
      Setup LiveData for receipts. Use a MediatorLiveData to easily allow filtering / sorting. 
      More information can be found later in the documentation. 
     */

    private val _receipts: MediatorLiveData<List<Receipt>> =
        MediatorLiveData<List<Receipt>>().apply { value = emptyList() }

    // Observe this LiveData in your view and forward to your list/adapter
    val receipts: LiveData<List<Receipt>> = _receipts

    init {
        initReceiptLiveData()
    }

     private fun initReceiptLiveData() {
        _receipts.addSource(ReceiptProvider.receipts) {
            // Filter and Queries can be applied here later on
            _receipts.postValue(it)
        }
    }

Fetch first page / Update receipt list

The initiateReceiptQuery method resets any existing query and caches the newly fetched receipts in the local database. We recommend calling this method in the following scenarios to ensure an up-to-date receipt list:

  • Initial Display of Receipt List
    When the receipt list is displayed for the first time in a session (e.g., when the user navigates to the receipt list view), this method should be called to display the latest receipt data. Note that this call is unnecessary after actions such as editing, deleting, or marking a receipt as favorite, as these are automatically handled within the SDK.

  • New Receipt Received
    If a new receipt is issued while the user is in the app, the list should be refreshed upon notification of the new receipt (e.g., triggered by a webhook event). This ensures the receipt list reflects the latest transactions.

  • Manual User Update
    For scenarios where a user manually refreshes the list, such as through a "pull-to-refresh" gesture or a refresh button, use this method to re-fetch the latest data for the first page.

  • Change in Sort Order
    When changing the sorting parameters of the receipt list (e.g., switching the sort order), call this method with the new parameters. This will reset the cache to reflect the updated sorting criteria.

WARNING

Cache Reset Consideration

As this method resets the cache and fills it with a new first page, any previously cached pages will be cleared and must be re-fetched. Avoid calling this method when navigating back to the receipt list from a single receipt view to prevent unnecessary reloading.


     fun initiateReceiptQuery() {
        viewModelScope.launch {
            when (val receiptCall = ReceiptProvider.initiateReceiptQuery(100)) {
                is ApiResult.Success -> {
                    // The receipts live data is automatically updated. You do not need to use the result of this API Call.

                    // Check if next page is available
                    if (receiptCall.value.continuationToken.isNullOrEmpty()) {
                        _lastPage.postValue(true)
                    } else {
                        _lastPage.postValue(false)
                    }

                }
                is NetworkError -> // Network Error Handling
                is GenericError -> // Generic Error Handling
            }
        }
    }

Fetch next page

To retrieve the next batch of receipts in the existing query, use continueReceiptQuery(). This method automatically applies the previously retrieved continuation token to fetch the subsequent set of receipts, seamlessly updating both the cached receipt list and the associated LiveData object.


     fun continueReceiptQuery() {
        viewModelScope.launch {
            when (val receiptCall = ReceiptProvider.continueReceiptQuery()) {
                is ApiResult.Success -> {
                    // The receipts live data is automatically updated. You do not need to use the result of this API Call.

                    // Check if next page is available
                    if (receiptCall.value.continuationToken.isNullOrEmpty()) {
                        _lastPage.postValue(true)
                    } else {
                        _lastPage.postValue(false)
                    }

                }
                is NetworkError -> // Network Error Handling
                is GenericError -> // Generic Error Handling
            }
        }
    }

To easily combine the caching function with filtering and querying of the receipt we recommending displaying the receipts in one unified list and not implementing actual pages with this method. To automatically fetch new receipts when the user scrolls to the end of the list we can you ScrollListeners on our RecyclerView:

Example PaginationScrollListener


abstract class PaginationScrollListener(private val layoutManager: LinearLayoutManager) :
    RecyclerView.OnScrollListener() {

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        val visibleItemCount: Int = layoutManager.childCount
        val totalItemCount: Int = layoutManager.itemCount
        val firstVisibleItemPosition: Int = layoutManager.findFirstVisibleItemPosition()
        if (!isLoading() && !isLastPage()) {
            if (visibleItemCount + firstVisibleItemPosition >= totalItemCount
                && firstVisibleItemPosition >= 0
            ) {
                loadMoreItems()
            }
        }
    }

    protected abstract fun loadMoreItems()
    abstract fun isLastPage(): Boolean
    abstract fun isLoading(): Boolean
}

Example implementation n your View


    private lateinit var receiptListAdapter: YourReceiptAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setReceiptListAdapter()
        initOnScrollObserver()
        viewModel.initiateQuery()
    }

    private fun setReceiptListAdapter() {
        receiptListAdapter = YourReceiptAdapter { receipt ->
            //Open Detail Receipt View
        }
        binding.yourReceiptList.adapter = receiptListAdapter
        binding.yourReceiptList.layoutManager = LinearLayoutManager(requireContext())
    }

    private fun initOnScrollObserver() {
        binding.yourReceiptList.addOnScrollListener(object : PaginationScrollListener(binding.yourReceiptList.layoutManager as LinearLayoutManager) {
            override fun loadMoreItems() {
                viewModel.getNextPage()
            }
            override fun isLastPage() = viewModel.lastPage.value ?: false

            override fun isLoading() = viewModel.loading.value ?: false
        })
    }

We recommend implementing a loading indicator while fetching pages to improve the user experience and prevent simultaneous API calls.


    private val _loading = MutableLiveData<Boolean>()
    var loading: LiveData<Boolean> = _loading

     fun continueReceiptQuery() {
        viewModelScope.launch {
            _loading.postValue(true)
            when (val receiptCall = ReceiptProvider.continueReceiptQuery()) {
                is ApiResult.Success -> {
                    // The receipts live data is automatically updated. You do not need to use the result of this API Call.

                    // Check if next page is available
                    if (receiptCall.value.continuationToken.isNullOrEmpty()) {
                        _lastPage.postValue(true)
                    } else {
                        _lastPage.postValue(false)
                    }
                    _loading.postValue(false)
                }
                is NetworkError -> {
                    // Network Error Handling
                    _loading.postValue(false)
                }
                is GenericError -> {
                    // Generic Error Handling
                    _loading.postValue(false)
                }
            }
        }
    }


# Marking Receipts

The anybill SDK provides following methods to edit or mark receipts:

Mark a receipt as favourite

Allowing to mark a receipt as favourite toggling the Receipt.Misc.isFavouite flag.


    val receipt : MutableLiveData<Receipt>()

    fun toggleFavourite() {
        viewModelScope.launch {
            when (val favouriteCall = ReceiptProvider.toggleIsFavourite(receipt.value.id)) {
                is ApiResult.Success -> {
                    // Update your displayed receipt if you are on a single receipt view
                    receipt.postValue(favouriteCall.value)
                }
                is NetworkError -> // Network Error Handling
                is GenericError -> // Generic Error Handling
            }
        }
    }

Set custom note for receipt

Using the ReceiptProvider.updateReceiptNote() method, a custom note can be set for a receipt which can be retrieved in the Receipt.Misc.Note field. This field can later on be used for querying and filtering of the receipt list.


    val receipt : MutableLiveData<Receipt>()

    fun setNote(note: String? = null) {
        viewModelScope.launch {
            when (val noteCall = receiptProvider.updateReceiptNote(note, receipt.value.id)) {
                is ApiResult.Success -> {
                    // Update your displayed receipt if you are on a single receipt view
                    receipt.postValue(noteCall.value)
                }
                is NetworkError -> // Network Error Handling
                is GenericError -> // Generic Error Handling
            }
        }
    }

TIP

When implementing with the Optimized SDK Caching Process of the sdk, the receipt list does not have to be updated when editing the receipt. This is handeled internally in the SDK.

# Filtering receipt list

The anybill SDK delivers receipts as structured data, enabling you to filter by any receipt field seamlessly. When using the recommended optimized SDK caching process combined with a unified receipt list and a ScrollListener, filters automatically trigger API calls. This allows you to display all relevant results dynamically, without the need to explicitly initiate API calls with query parameters.

Common use cases include filtering the receipt list for favorites or searching for specific string values.

WARNING

Currently, the SDK does not support advanced search functionality, such as Elasticsearch. However, this feature is planned for development in 2025.

For an easy query of the receipt list the anybill SDK provides an extension function for a List<Receipt> called .filterForQuery(). The method filters store name, address, amount, note, and line and discount descriptions. As this method is highly performance costing, we do recommend checking for a min length of the keyword (e.g. > 3) before executing.

Example implementation of allowing to simultaneously filter for favourites and query for a string value


    /**
      Setup LiveData for receipts. Use a MediatorLiveData to easily allow filtering / sorting. 
     */

    private val _receipts: MediatorLiveData<List<Receipt>> =
        MediatorLiveData<List<Receipt>>().apply { value = emptyList() }

    // Observe this LiveData in your view and forward to your list/adapter
    val receipts: LiveData<List<Receipt>> = _receipts

    // Keyword used to query the receipt list for.
    var searchKeyword: String? = null
        set(value) {
            if (field != value) {
                if(!value.isNullOrEmpty()) {
                    _receipts.postValue(ReceiptProvider.receipts.value?.filterForQuery(value))
                } else {
                    _receipts.postValue(ReceiptProvider.receipts.value)
                }
            }
            field = value
        }

    // Toggle activating the filtering of receipts
    var filterForFavourites: Boolean = false
        set(value) {
            if (field != value) {
                if (value) {
                    _receipts.postValue(ReceiptProvider.receipts.value?.filter { it.misc.isFavourite })
                } else {
                    _receipts.postValue(ReceiptProvider.receipts.value)
                }
            }
            field = value
        }

    init {
        initReceiptLiveData()
    }

    // Check for both filter methods when initiating the LiveData source.
    private fun initReceiptLiveData() {
        _receipts.addSource(ReceiptProvider.receipts) {
            if (!filterForFavourites) {
                if (!searchKeyword.isNullOrEmpty()) {
                    _receipts.postValue(it.filterForQuery(searchKeyword))
                } else {
                    _receipts.postValue(it)
                }
            } else {
                if (!searchKeyword.isNullOrEmpty()) {
                    _receipts.postValue(it.filter { receipt -> receipt.misc.isFavourite }.filterForQuery(searchKeyword))
                } else {
                    _receipts.postValue(it.filter { receipt -> receipt.misc.isFavourite })
                }
            }
        }
    }

# Export as PDF

To ensure legally compliant receipts, the original receipt must be retrievable as a PDF file. The structured data provided by the anybill SDK does not represent the original receipt but serves to display relevant receipt information.

To access the original receipt, the anybill SDK offers the method ReceiptProvider.getReceiptPdf(), which generates the PDF on our system and returns it as a URI pointing to a cached PDF file within the app's directory. This URI can then be used to share the receipt via an intent, save it to the user’s device, or visualize it using a PDF library.

Parameters for Customization:

isPrintedVersion:
Generates a multi-page DIN A4 PDF version of the receipt, making it easier for end users to print a physical copy of the receipt, such as for return processes that require paper receipts.

includeReturnReceipts:
Includes all return receipts linked in Receipt.Misc.ReceiptReferences within the generated PDF. Note that this option can significantly increase the duration of the API call, as multiple PDFs must be generated and merged.

fileName:
Specifies a custom file name for the cached URI. By default, the file name is set to "'Receipt.Id'.pdf". For convenience, you can use the extension function Receipt.getPDFFileName() to generate a meaningful file name based on the store name and date, or you can provide your own custom file name.

    fun exportReceiptAsPDF(receipt: Receipt) {
        viewModelScope.launch {
            when (val exportCall = ReceiptProvider.getReceiptPdf(
                    receipt.id, 
                    false, 
                    false,
                    receipt.getPDFFileName()
                )) {
                is ApiResult.Success -> {
                    // Use exportCall.value to share Intent or Display PDF
                }
                is ApiResult.GenericError -> //Error Handling
                is ApiResult.NetworkError -> //Error Handling
            }
        }
    }

Back to top

# Deleting receipts

The anybill SDK provides functionality to delete either a single receipt or a batch of receipts. When utilizing the optimized caching process, receipts are automatically removed from the local cache, ensuring the observable data is kept up-to-date without requiring manual intervention.

Methods for Deleting Receipts

ReceiptProvider.deleteReceipt():
Deletes a single receipt from the user’s receipt list. This method can be used for operations where only one specific receipt needs to be removed.

ReceiptProvider.deleteReceipts():
Deletes multiple receipts (up to 100) from the user’s receipt list in a single operation. If an error occurs during the batch deletion process, a GenericError will be returned, providing details about which receipt IDs could not be deleted. This allows for targeted error handling in such cases.

# Example Use Case for Error Handling:

viewModelScope.launch {
    when (val deleteCall = ReceiptProvider.deleteReceipts(receiptIds)) {
        is Success -> {
            // All Receipts successfully deleted
        }
        is GenericError -> {
            
            if (deleteCall.code == ReceiptErrors.FAILED_DELETION) {
                // Receipt deletion partially failed for following receipts
                val failedReceipts = deleteCall.error?.receiptIds
            }

        }
        is NetworkError -> {
            // Handle network errors
        }
    }
}

Back to top

# AnybillContentArea

The Content Area Module enables the Content Area feature, which enables the displaying of additional information (e.g. advertisement or news) on receipts. With this module the data of these content areas can be queried to display them in the SDK implementing app. Another possibility is to use this in conjunction with the Content Area UI Module for a working implementation out of the box.

# Retrieving a content area

Instances of ContentArea can be retrieved through the ContentAreaProvider. This Provider exposes the method getContentArea(contentAreaId: String), returning a LiveData of type content area which updates automatically, if an update is requested. The methods only parameter is the ID of the content area to fetch, which can be found in the receipt data model. The SDK utilizes caching and returns a local copy of the latest cached value.

Sample code for your viewmodel:

    private val _contentArea: MediatorLiveData<ContentArea?> =
        MediatorLiveData<ContentArea?>()

    private var currentContentAreaLiveData: LiveData<ContentArea?>? = null

    private val anybillContentAreaProvider = ContentAreaProvider

    fun loadContentAreaById(contentAreaId: String) {
        if (_contentArea.hasObservers()) {
            currentContentAreaLiveData?.let {
                _contentArea.removeSource(it)
            }
        }

        _contentArea.addSource(anybillContentAreaProvider.getContentArea(contentAreaId)) {
            _contentArea.postValue(it)
        }
    }

# Updating a content area

In order to fetch the latest content area data from the backend, the ContentAreaProvider exposes the method updateContentArea(contentAreaId: String), returning an ApiResult of Type ContentArea. The resulting ApiResult indicates, whether or not the backend request succeeded or failed. In case of a successful request the local cache is automatically updated and the new data automatically published via the aforementioned LiveData.

 viewModelScope.launch {
            handleApiResult(anybillContentAreaProvider.updateContentArea(contentAreaId))
        }

Back to top