# iOS Integration

# General Information

Getting Started
Push Notifications

# 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":

  • BillReceipt:
    All subtypes of "Bill" have been removed and replaced with a unified Receipt. 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:

    • observableBills LiveData → observableReceipts LiveData
  • Method Renames:

    • exportsBillAsPDFgetReceiptPdf
    • updateBillupdateReceiptNote
    • setIsFavouritetoggleIsFavourite
  • Receipt Retrieval Updates:

    • Methods such as getAllBillsFromApi, 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

# Integration with Cocoapods

# Cocoapods Plugin

In order to resolve the anybill SDK using Cocoapods, you need the cocoapods-art plugin which presents Artifactory repositories as Specs repos and pod sources.

You can download the cocoapods-art (opens new window) plugin as a Gem, and its sources can be found on Github (opens new window)

To resolve the artifactory repository, execute the following steps:

  1. Install cocoapods-art plugin
gem install cocoapods-art
  1. The next step is to add the repository by using the pod 'repo-art add' command:
pod repo-art add anybill_ios_sdk "https://anybill.jfrog.io/artifactory/api/pods/anybill_ios_sdk"
  1. Once the repository is added, add the following in your Podfile:
plugin 'cocoapods-art', :sources => [
        'anybill_ios_sdk'
    ]

repo-art uses authentication as specified in your standard netrc file.

machine anybill.jfrog.io
login <USERNAME>
password <PASSWORD>

Further information about the cocoapods plugin can be found on the artifactory documentation (opens new window).

# Podfile configuration

  1. First of all you need to specify the usage of dynamic frameworks in your target/project with use_frameworks!, if you did not already.

  2. Then you can add the desired artifacts to your podfile (The AnybillBase module is required for the basic usage of the SDK)

Below is the podfile of an example project, which implements the anybill SDK:

platform :ios, '14.0'

source 'https://github.com/CocoaPods/Specs.git'
plugin 'cocoapods-art', :sources => [
  'anybill_ios_sdk'
]

target 'your target name' do
  
  use_frameworks!
  
  pod 'AnybillBase'

end

# Add this script for Cocoapods <1.11.0 to fix a Cocoapods problem with XCFrameworks
post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
    end
  end
end

# Integration with Swift Package Manager

  1. First of all you need to set the package-registry using the following command:
swift package-registry set --global "https://anybill.jfrog.io/artifactory/api/swift/anybill_ios_sdk-spm"
  1. Then you need to make sure you are logged in to jfrog in order to resolve the package
swift package-registry login https://anybill.jfrog.io/artifactory/api/swift/anybill_ios_sdk-spm  --username yourUsername  --password yourPassword
  1. Add the desired anybill modules in Xcode: File > Add package dependencies. Search for e.g. 'anybill.AnybillBase' and add the dependency.

# Instantiate the log module in your AppDelegate file

In your AppDelegate file import AnybillBase and add AnybillLogger.initLogger() to the body of the application(_:didFinishLaunchingWithOptions:) method

# Setting the client Id

To set the Client Id in your app, add the Id as value for the key anybill_client_id in your info.plist 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.

# Change the api mode

For developing purposes you can change the api environment to staging by adding stg or test as value for the key anybill_base_url in your info.plist file

# 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 your custom url base as value for the key custom_url_base in your info.plist file

# Usage of the SDK

# Error Handling

The anybill sdk uses a custom error handling model. All methods return either the result object or an AnybillErrorobject. The error object can either be the default model with an errors parameter containing detailed information about the error or a spcified error model. The error types can be distinguished via the variant parameter.

/**
 Error model protocol used as the base model for the different error models
 - variant: AnybillErrorVariant with the error model type
 - code: Int with the response code of the api call
 - title: String with the title of the error
 - traceId: String used for error logging
 - type: AnybillErrorType used to distinguish between error types
 */
public protocol AnybillError: Decodable, Error {
    var variant: AnybillErrorVariant { get set }
    var code: Int { get set }
    var title: String? { get set }
    var traceId: String? { get set }
    var type: AnybillErrorType { get set }
}

/**
 Enum used to distinguish between generic and network errors
 */
public enum AnybillErrorType: Codable {
    /// This error type is used for generic errors that do not fit into any other category.
    case genericError,
         
         /// This error type is used when there is a network error while making a request.
         networkError,
         
         /// This error type is used when there is an issue with refreshing the user's access token.
         invalidRefreshTokenError,
         
         /// This error type is used when there is no user logged in.
         noUserError,
         
         /// This error type is used when the issue can't be defined. Typically a critcal error.
         unknown
}

Example usage of the error model based on the login method of the AuthProvider.

authService.loginUser(username: username, password: password) { result in
        switch result {
            case .success(()):
                //Continue
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
        }
    }

Back to top

# Push Notifications

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 uses push notifications to inform the user about certain events. A detailed list of these events can be found in the push notification tab of the App API documentation.

To enable Push Notifications you'll have to provide a APN Authentication Key of your application. After creating a Firebase application in the anybill Firebase project, you are going to receive a Google App Id and Firebase Client Id, which you will have to use in your application code. Please contact us before implementing anybill Push Notifications (dev@anybill.de).

# Integration without firebase already being integrated in your project

First of all you need to import Firebase and AnybillBase in your AppDelegate.

Then add the following code to the body of your application(_:didFinishLaunchingWithOptions:) function:

AnybillFirebaseInitializer().initFirebaseProject(googleAppId: "yourGoogleAppId", firebaseClientId: "yourFirebaseClientId")

Messaging.messaging().delegate = self

if #available(iOS 10.0, *) {
  // For iOS 10 display notification (sent via APNS)
  UNUserNotificationCenter.current().delegate = self

  let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
  UNUserNotificationCenter.current().requestAuthorization( options: authOptions, completionHandler: {_, _ in })
} else {
  let settings: UIUserNotificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
  UIApplication.shared.registerUserNotificationSettings(settings)
}
application.registerForRemoteNotifications()

Subsequently add these extensions to the end of your AppDelegate file:


@available(iOS 10, *)
extension AppDelegate : UNUserNotificationCenterDelegate {

  // Receive displayed notifications for iOS 10 devices.
  func userNotificationCenter(_ center: UNUserNotificationCenter,
                              willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    
    //Called when the app is about to show a notification
    
    // Change this to your preferred presentation option
    completionHandler([[.banner, .sound]])
  }

  func userNotificationCenter(_ center: UNUserNotificationCenter,
                              didReceive response: UNNotificationResponse,
                              withCompletionHandler completionHandler: @escaping () -> Void) {
                              
    //Called when the users clicks the notification
    
    completionHandler()
  }
}



extension AppDelegate : MessagingDelegate {
    
  func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
    
    let dataDict:[String: String] = ["token": fcmToken ?? ""]
    NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
    
    //Notify the anybill sdk to update its FCM token
    
    AnybillFirebaseInitializer().didReceiveRegistrationToken()
}

# Integration with firebase already being integrated in your project

If you already have firebase integrated in your app, simply follow the steps mentioned above and initialize the anybill firebase project after you initialized your firebase project


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

// Initialize your Firebase project:

FirebaseApp.configure()

// Initialize the anybill Firebase project:

AnybillFirebaseInitializer().initFirebaseProject(googleAppId: "yourGoogleAppId", firebaseClientId: "yourFirebaseClientId")

Messaging.messaging().delegate = self

...
}

# Default handling of push notifications

As just demonstrated, notifications are handled inside the userNotificationCenter() method. The simplest way of handling the notifications is to just dislpay them as local iOS notifications. However the SDK also offers the possibility to display them on UI components of the SDK as a snackbar. Additionally some special notifications trigger specific actions when handled by the SDK such as automatically updating the list of bills when a notifications arrives, stating that a new receipt has been added to the logged in account. To enable the SDK internal handling of notifications, the method handleAnybillNotification of the class AnybillNotificationCenter can be called like so:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification,  withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
      AnybillNotificationCenter.shared.handleAnybillNotification(notification: notification)
      completionHandler(.sound)
}

To avoid displaying of multiple snackbars/notifications it is advised to first check if the SDK UI is currently being used and based on the result either let the SDK handle the notification or let the implementing app display the notification.

# Open added receipt after push notification

To open a receipt in the anybill ui module, that was added though a push notification you need to implement the observable object 'NotificationManager.shared' and pass it to the AnybillUIModule view as an environmentObject.

Additionally you have to listen for push notifications and pass the recieving 'UNNotification' object to the 'dumbData' parameter of the 'NotificationManager.shared'.

If the notification is a valid add receipt notification from anybill the notification data will be parsed and the relevant information will be available through the @Published variable 'anybillNotificationResult'.

You should listen for changes of the 'anybillNotificationResult' parameter and naviagte to the anybill ui module if it is not equal to nil.

Passing the notification info to the NotificationManager:


    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions)
                                -> Void) {
        self.notificationManager.dumbData = notification
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        self.notificationManager.dumbData = response.notification
        completionHandler()
    }   

Example of naviagting to the anybill ui module after an add receipt push notification in swiftUI:

(Note that the NotificationManager instance is passed to the ContentView as an environmentObject)


struct ContentView: View {
    @State private var tab = 1
    @EnvironmentObject var notificationCenter: NotificationManager
    
    var body: some View {
        TabView(selection: $tab) {
            Home()
                .tabItem {
                    Label("Home", image: "home")
                        .foregroundColor(.black)
                }
                .tag(1)
            AnybillUIModule()
                .tabItem {
                    Label("Receipts", image: "anybillLogo")
                        .foregroundColor(.black)
                }
                .tag(2)
            Settings()
                .tabItem {
                    Label("Settings", systemImage: "gearshape")
                        .foregroundColor(.black)
                }
                .tag(3)
        }
        .onAppear {
            if notificationCenter.anybillNotificationResult != nil {
                self.tab = 2
            }
        }
        .onChange(of: notificationCenter.anybillNotificationResult) { _ in
            self.tab = 2
        }
    }
}

Example of naviagting to the anybill ui module in UIKit:


class CustomTabBarController: UITabBarController {
    
    let notificationManager = NotificationManager.shared
    
    var cancellableBag = Set<AnyCancellable>()

        override func viewDidLoad() {
            super.viewDidLoad()
            
            notificationManager.$anybillNotificationResult.sink { value in
                self.selectedIndex = 1
            }.store(in: &cancellableBag)
        }
}

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

    authService.loginUser(username: username, password: password) { result in
        switch result {
            case .success(()):
                //Continue
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
        }
    }

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
    let result = await YourApi.getTokenForUser()
    let tokenUser = TokenUser.init(accessToken: result.accessToken, expiresIn: result.expiresIn, refreshToken: result.refreshToken)
    authProvider.loginTokenUser(tokenUser: tokenUser){ result in
        switch result {
            case .success:
                //Continue
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
        }
    }

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 the type parameter of the AnybillError:

    authService.loginUser(username: username, password: password) { result in
        switch result {
            case .success(()):
                //Continue
            case .failure(let error):
               if error.type == .invalidRefreshTokenError {
                    // Reauthenticate user by fetching token information using the anybill Partner Platform Api    
                }
        }
    }

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 AnybillError with AnybillErrorType.noUserError, 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:

 
    /**
    Struct representing an Anybill User
    - id: String with the user's id
    - email: String with the user's email
    - isAnonymous: Bool describing if a user is anonymous
    - notificationConfiguration: NotificationConfiguration for notifications of the user
    - externalId: Optional external id of the user
    */
    public struct AnybillUser: Decodable, Encodable, Equatable {
        
        public let id: String
        public let email: String
        public let isAnonymous: Bool
        public let notificationConfiguration: NotificationConfiguration?
        public let externalId: String?
    }

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.


authService.getUserInfo(useCache: true) { result in
        switch result {
            case .success(let user):
                //Continue
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
        }
    }

# Logout

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

    authService.logoutUser { result in
        switch result {
            case .success:
                //Continue
            case .failure:
                // Handle the failed deletion of the token
        }
    }

# Receipts

Singleton ReceiptProvider grants access to the anybill receipt functions.

# 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:


    @Published public var receipts: [Receipt] = []

    private var continuationtoken: String? = nil

    func getFirstPage() {
        receiptService.getReceipts(take: 100, orderBy: .Date, orderDirection: .Descending, continuationToken: null) { result in
            switch result {
                case .success(let continuationList):
                    //Access receipts through the observableReceipts observable in the ReceiptProvider or through the returned object
                    handleContuationResult(continuationList)
                case .failure(let error):
                    switch error.type {
                        case .genericError:
                            // handle errors 400..499
                        case .networkError:
                            // handle network error
                        case .noUserError:
                            // handle no user error
                        case .invalidRefreshTokenError:
                            // handle error when access token could not be refreshed
                        default:
                            // handle unknown error
                    }
            }
        }
    }

    // Continue query with Continuation Token
    func getNextPage() {
       receiptService.getReceipts(take: 100, orderBy: .Date, orderDirection: .Descending, continuationToken: continuationtoken) { result in
            switch result {
                case .success(let continuationList):
                    //Access receipts through the observableReceipts observable in the ReceiptProvider or through the returned object
                    handleContuationResult(continuationList)
                case .failure(let error):
                    switch error.type {
                        case .genericError:
                            // handle errors 400..499
                        case .networkError:
                            // handle network error
                        case .noUserError:
                            // handle no user error
                        case .invalidRefreshTokenError:
                            // handle error when access token could not be refreshed
                        default:
                            // handle unknown error
                    }
            }
        }
    }


    func handleContiuationResult(contiuationReceiptList: ContinuationReceiptList) {
        receipts.append(contuationReceiptList.receipts)

            // Check for more results
        guard let contuationToken = continuationReceiptList.continuationToken {
            // Save continuationReceiptList.continuationToken to be used in the next request
            continuationToken = continuationToken
        } else {
            // Remove continuationToken - no more receipts to fetch
            continuationToken = null
        }
    }

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 Published object, observableReceipts, 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.

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 Published object accordingly.

Example implementation in a ViewModel:

Setup Live Data

The exposed ReceiptProvider.$observableReceipts 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 a receipts variable, that is updateded when the cache is modified
     */

     @Published var receipts: [Receipt] = []

    init() {
        observeReceiptsObservable()
    }

    private var cancellables = Set<AnyCancellable>()
    
    private func observeReceiptsObservable() {
        receiptService.$observableReceipts
            .sink { [weak self] in
                self?.receipts = $0
            }
            .store(in: &cancellables)
    }

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.


     func loadInitalReceipts() {
        receiptService.initiateReceiptQuery(take: receiptBatchSize, orderDirection: sortAscending ? .Ascending : .Descending) { result in
            switch result {
            case .success(let continuationResponse):
                self.totalReceipts = continuationResponse.totalCount
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
            }
            self.isLoading = false
        }
    }

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.


    func loadNextReceipts() {
        receiptService.continueReceiptQuery(take: receiptBatchSize) { result in
            switch result {
            case .success:
                self.isLoading = false
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
                self.isLoading = false
            }
        }
    }

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 use the following implementation:

Example Implementation in the view model



    // Keyword used to query the receipt list for.      
    @Published var searchText = ""

    // Sorting direction of the receipts
    @Published var sortAscending = false {
        didSet {
            loadInitalReceipts()
        }
    }

    // Total number of user receipts
    private var totalReceipts: Int?
    
    // Toggle activating the filtering of receipts
    @Published var showFavourites = false

    // Filtered receipt list, that can be used in the ui
    var filteredReceipts: [Receipt] {
        if showFavourites {
            return receipts.filter { $0.misc.isFavourite }.searchReceipts(for: searchText)
        } else {
            return receipts.searchReceipts(for: searchText)
        }
    }   

    // Flag specifying if all receipts have been retrieved
    var finishedLoadingReceipts: Bool {
        if let totalReceipts = totalReceipts {
            return receipts.count >= totalReceipts
        }
        return false
    }

    // Method to retrieve receipts in the ui
    func loadReceipts() {
        guard !finishedLoadingReceipts && !isLoading else { return }
        
        isLoading = true
        
        if (receipts.isEmpty) {
            loadInitalReceipts()
        } else {
            loadNextReceipts()
        }
    }

Example implementation in your View


import SwiftUI

struct ReceiptList: View {
    @StateObject var viewModel: ViewModel
    
    var body: some View {
        ScrollView {
            if !viewModel.receipts.isEmpty {
                LazyVStack(alignment: .center) {
                    ForEach(viewModel.filteredReceipts, id: \.self) { receipt in
                        ReceiptListItem(receipt: receipt)
                    }
                    if !viewModel.finishedLoadingReceipts && !viewModel.isLoading {
                        ProgressView()
                            .onAppear {
                                viewModel.loadReceipts()
                            }
                    }
                }
             } else {
                 ProgressView()
                     .onAppear {
                         viewModel.loadReceipts()
                     }
             }
        }
    }
}

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


    @Published var receipt: Receipt

    func toggleFavourite() {
        receiptService.toggleIsFavourite(receiptId: receipt.id) { result in
            switch result {
            case .success(let receipt):
                // Update your displayed receipt if you are on a single receipt view
                self.receipt = receipt
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
            }
        }
    }


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 the receipt list.


    @Published var receipt: Receipt

    func setNote(note: String) {
        receiptService.updateReceiptNote(receiptId: receipt.id, note: note) { result in
            switch result {
            case .success(let receipt):
                self.receipt = receipt
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
            }
        }
    }

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 handled internally in the SDK.

# Filtering receipt list

The anybill SDK delivers receipts as structured data, enabling you to filter by any receipt field seamlessly. 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 an Array<Receipt> called .searchReceipts(). 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


    // Unfiltered receipts
    @Published var receipts: [Receipt] = []
    
    // Keyword used to query the receipt list for.
    @Published var searchText = ""
    
    // Toggle activating the filtering of receipts
    @Published var showFavourites = false
    
    // Filtered receipt list, that can be used in the ui
    var filteredReceipts: [Receipt] {
        if showFavourites {
            return receipts.filter { $0.misc.isFavourite }.searchReceipts(for: searchText)
        } else {
            return receipts.searchReceipts(for: searchText)
        }
    }

# 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 Data object. Use the Data object to either visualize a PDF in a PDFView using PDFKit or share it to other apps as a File.

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.


    @State private var pdfData: Data? = nil

    receiptService.getReceiptPdf(receiptId: id) { result in
        switch result {
        case .success(let data):
            self.pdfData = data
        case .failure(let error):
            switch error.type {
                case .genericError:
                    // handle errors 400..499
                case .networkError:
                    // handle network error
                case .noUserError:
                    // handle no user error
                case .invalidRefreshTokenError:
                    // handle error when access token could not be refreshed
                default:
                    // handle unknown error
            }
        }
    }

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 DeleteReceiptsError 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:

    receiptService.deleteReceipts(receiptIds: ["4d8b1eb1-a1c6-45da-a271-5ab1167d6018", "4d8b1eb1-a1c6-45da-a271-5ab1167d6019"]) { result in
            switch result {
            case .success:
                /// Receipts deleted
            case .failure(let error):
                switch error.type {
                    case .genericError:
                        if let deleteError = error as? DeleteReceiptsError {
                            //Handle failed receipts
                            deleteError.failedReceipts
                        }
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
            }
        }

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

Get content area

Use the getContentArea() method to retrieve a content area object. When called the content area is cached in a local database.

    contentAreaService.getContentArea(receiptId: String) { result in
        switch result {
            case .success(let contentArea):
                //Continue
            case .failure(let error):
               switch error.type {
                    case .genericError:
                        // handle errors 400..499
                    case .networkError:
                        // handle network error
                    case .noUserError:
                        // handle no user error
                    case .invalidRefreshTokenError:
                        // handle error when access token could not be refreshed
                    default:
                        // handle unknown error
                }
        }
    }

Back to top