NAV Navbar
Logo
Swift

IOS SDK Introduction

Note:

Code samples will appear in this column.

The Rezolve InsideTM SDK is a software development kit that enables mobile app developers to integrate Rezolve’s mobile commerce and engagement capabilities into their new or existing mobile apps.

The Rezolve SDK communicates with the Rezolve online product stack – a set of dashboards and services designed to provide retailers and brands with everything they need to promote and sell via Rezolve, and also provide consumers with a seamless mobile experience.

Reference App

A Rezolve Reference App is available. This app was designed to showcase the unique capabilities of the Rezolve system, and to demonstrate the consumer experience.

To get the app, go here: Rezolve Reference App

Download links, sample media, and instructions are all included.

Capabilities

The Rezolve InsideTM SDK is a full-featured application suite. Capabilities that can be integrated into your application include:

Intended audience

This document is intended for experienced IOS developers. It is expected that you have built apps before, and know your way around an IDE.

Term Definitions

term definition
Partner The Partner is the owner of a mobile application and audience. It is the Partner who wishes to integrate the Rezolve InsideTM SDK capabilities with their app.
Developer The Developer is the mobile app developer of the Partner.
Merchant A merchant runs an ecommerce site that offers products for sale. Merchants also create Shoppable Ads and link them to products. A merchant may also offer device accounts that need Top Up.
Consumer The end user of the Partner's mobile app. The customer who buys merchant products.
SDK Refers to the Rezolve InsideTM SDK, unless otherwise specified.
RCE Rezolve Cloud Engine, the online Rezolve application for managing your categories and products.
SSP Self-Serve Portal, the online Rezolve portal for creating engagements, and linking them to target offerings.

For more information

For more information on Rezolve, see http://rezolve.com.

Getting Started

Supported OS Versions

OS Minimum Version
IOS IOS 12 and up
SWIFT 5

Implement a Crash Reporting Sytem

To provide quality support, we require quality information.

Rezolve advises using Firebase Crashlytics to capture app problems on both IOS and Android.

We will not provide development support if you have not integrated Crashlytics or a similar tool.

Get the Sample App project

The best way to get started with SDK integration is to download our Sample App project. This is a complete Xcode project that produces a working application, showcasing the features described in this documentation.

Just fill in your API details in Config.swift and compile.

rezolve_sdk_sampleapp_ios-20210401.zip

Download the SDK & Get an API Key

Request API Key

Latest release versions:

If this is your first time downloading the SDK, you will be provided with an API Key and the required environment information to begin development.

Set up the SDK - IOS

The target IDE for IOS instructions is XCode. If you use a different IDE you may have to follow a different series of steps, please refer to your IDE documentation to understand how to incorporate third party SDKs into your IDE.

CocoaPods

The Rezolve IOS SDK is distributed through CocoaPods. In order to proceed with installing the SDK, as a prerequisite, first install the latest version of CocoaPods:

sudo gem install cocoapods

Install Rezolve SDK

platform :ios, '12.0'

use_modular_headers!

target 'Sample' do
  pod 'RezolveSDK'
end

The SDK can now be installed in your own project by adding the following in Podfile:

Keep in mind that Sample is just a placeholder, and should be substituted by your own App Target name. Now just run the following command on a Terminal window pointing to the directory of your Podfile:

pod install

Don’t forget to use the .xcworkspace file to open your project in Xcode, instead of the .xcodeproj file, from here on out.

Updating the SDK

Every new release of the RezolveSDK can be updated by typing

pod update ResolveSDK

SDK Feature Use

This section describes the usage of the SDK to build specific feature-related functionalities.

Automatic thumbnail generation

Example JSON for 1 large image, and 4 associated "thumbnail" images

      "image": "https:\/\/s3.amazonaws.com\/1\/27\/Image.png",
      "image_thumbs": [
        "https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_400x550.png",
        "https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_800x1100.png",
        "https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_1200x1651.png",
        "https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_1600x2201.png"
      ],

When large images are uploaded to the merchant portal, the portal automatically generates a series of smaller images, which are referred to as thumbnails even though some are fairly large. The rules for thumbnail generation are as follows:

Each uploaded image will have a corresponding array of thumbnails. If an object can have an array of images, it will have an array of thumbnail arrays.

User Management Introduction

For the purpose of the SDK, it is assumed the partner has an existing community of consumers, and has a method of authenticating them in the partner app. It is further assumed the partner wishes to introduce their consumers to Rezolve capabilities. Each partner consumer that wishes to utilize Rezolve services will need a Rezolve account.

The Rezolve SDK has no built-in authentication mechanism, but that is not to say there is no security. Rezolve is utilizing a server-to-server JWT authorization system, conformant with the https://tools.ietf.org/html/rfc7519 standard. If you are not familiar with JSON Web Tokens, the site https://jwt.io/ provides an excellent primer on the use of JWTs, as well as links to various JWT libraries you can utilize.

Exactly how you implement authentication in your app will depend on what authorization system you use. You may either:

The primary task in implementing user management is to create the JWT that will allow you to bind a Partner user to a Rezolve profile. Through a server-to-server transaction, you can create a new Rezolve User Profile, and get back an identifying entityId that you should persist in your authentication database, as an addition to your user record. This entityId will then be used by the Rezolve SDK to identify the user to the Rezolve Server.

A valid JWT is required to initialize the SDK and start a session.

JWT Authentication

Topics

As of release 1.6.0, the Rezolve SDK no longer includes an authentication system. Resultingly, the AuthenticationManager.register and AuthenticationManager.logout methods have been deprecated.

Instead, Rezolve is utilizing a server-to-server JWT authentication system, conformant with the https://tools.ietf.org/html/rfc7519 standard. If you are not familar with JSON Web Tokens, the site https://jwt.io/ provides an excellent primer on the use of JWTs, as well as links to various JWT libraries you can utilize.

Rezolve expects the primary authentication of users will happen outside the SDK. Thus, exactly how you implement authentication in your app will depend on your existing auth system. In the server-to-server realm, however, there is only one instance in which your authentication server must interact with the Rezolve server.

After that, whether or not the SDK can talk to Rezolve depends on supplying a valid JWT to the SDK from your auth system.

When the Partner creates a new user on their auth server, or wishes to associate an existing user with Rezolve for the first time, the partner must generate the Registration JWT, and then POST it to the /api/v1/authentication/register endpoint. The Rezolve server will validate the JWT, create a new user, create the user's public/private key pair, and return to you the Rezolve EntityId.

When a user logs in to your auth system, generate a new Login JWT and supply to CreateSession in the SDK. As long as the JWT is valid, the SDK can talk to Rezolve. A method is suggested below for smoothly handling JWT timeouts without interrupting the SDK session.

Terminology

Term Definition
partner_id A numerical id you are assigned by Rezolve. Usually a 2-4 digit integer.
partner_api_key The API key you are assigned by Rezolve. 36 characters including dashes.
partner_auth_key The Auth key you are assigned by Rezolve. This plays the role of the JWT Secret. The partner_auth_key is typically a ~90 character hash.
JWT token A JSON Web Token, consisting of a header, payload, and signature. The header and signature are signed with the parther_auth_key, above. It is used as a bearer token when communicating with the Rezolve server.
accessToken In the IOS and Android code samples, the accessToken is the JWT Token you generated.
deviceId An id that is randomly generated upon app install and stored. This id is placed in both the JWT payload and x-header sent by the SDK. The Rezolve server checks that these values match to deter request origin spoofing. All calls except Registration calls require this.

JWT Flow


[ View full size ]

Create the Registration JWT

Requirements

You must possess:

field description example
partner_id The numerical id you are assigned by Rezolve 317
partner_api_key The API key you are assigned by Rezolve a1b2c3d4-e5f6-g7h8-i9j0-a1b2c3d4e5f6
partner_auth_key The Auth key you are assigned by Rezolve qwer+4ty ... JYG6XavJg== (approx 90 characters)

JWT Header

{
    "alg": "HS512",
    "typ": "JWT"
}
key value notes
alg HS512 algorithm, HMAC SHA-512
typ JWT type

JWT Payload

{
    "rezolve_entity_id": ":NONE:",
    "partner_entity_id": "partner_entity_id",
    "exp": 1520869470
}
key value notes
rezolve_entity_id :NONE: use :NONE: when registering
partner_entity_id your_user_id The unique identifier for your user record. This may be a numerical id, or an identifying string such as email address.
exp 1520869470 Expiration, as a unix timestamp integer. Set the expiration value to a small number, now() + 30 minutes or less.

Signature

HMACSHA512(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    ${$partner_auth_key}
)

Sign the header and payload with the partner_auth_key. It is not necessary to decode the key before using it. Pass the whole value as a string to your library's getBytes method.

You may need to specify the charset as UTF8. Example 1, Microsoft: SecretKey(Encoding.UTF8.GetBytes(key));
Example 2, Java: Secret.getBytes("UTF-8");

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.
eyJyZXpvbHZlX2VudGl0eV9pZCI6IjpOT05FOiIsInBhcnRuZXJfZW50aXR5X2lkIjoiMTIzIiwiZXhwIjoxNTIwODY5NDcwfQ.
5y2e6QpUKcqTNLTv75nO6a6iFPVxrF8YeAH5NTg2ZO9dkub31GEs0N46Hu2IJf1bQ_vC2IOO9Z2N7drmvA_AKg

The resulting JWT will look something like this (except without linebreaks); the first third is the header, the second third the payload, and the last third the signature:

Register a new Rezolve User

POST: https://core.sbx.eu.rezolve.com/api/v1/authentication/register
-H content-type: application/json
-H x-rezolve-partner-apikey: your-api-key
-H authorization: Bearer signed-jwt
-d {
"email": "user@example.com"
}

After the JWT is created, POST it to the Rezolve registration endpoint. This will create a Rezolve User, generate a public/private keypair for the user, and return to you the corresponding Entity Id.

Example, using Sandbox endpoint:

{
    "entity_id" : "entity123",
    "partner_id" : "3"
}

The endpoint will reply with an entity id and the partner id. You should save the Rezolve Entity Id to your authentication database and associate it with the user's record.

Logging in a User

Once a Rezolve User has been registered and an entity_id obtained, you can log in the user using the instructions below.

For returning users, log them in via your normal method in your auth server, and then follow the instructions below.

Create a new Login JWT, and use it as the accessToken in the createSession method.

JWT Header

Note the addition of the "auth" line.

{
    "auth": "v2",
    "alg": "HS512",
    "typ": "JWT"
}
key value notes
auth v2 auth version to use, login uses v2
alg HS512 algorithm, HMAC SHA-512
typ JWT type

JWT Payload

Note the addition of the device_id.

{
    "rezolve_entity_id": "entity123",
    "partner_entity_id": "partner_entity_id",
    "exp": 1520869470, 
    "device_id": "wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a"
}
key value notes
rezolve_entity_id your_rezolve_entity_id use the entity_id you obtained during registration
partner_entity_id your_partner_entity_id set it to the unique identifier for your user record
exp 1520869470 Expiration, as a unix timestamp integer. Set the expiration value to a small number, now() + 30 minutes or less.
device_id wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a An id randomly generated upon app installation and stored. This id is placed in both the JWT payload and x-header sent by the SDK. See below for generation instructions.

Signature

HMACSHA512(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    ${$partner_auth_key}
)

Sign the header and payload with the partner_auth_key.

Generating the device_id

// iOS uses the iOS "identifierForVendor" string, so there is no need to generate it. 
// The iOS SDK pulls this value automatically using `UIDevice.current.identifierForVendor?.uuidString` and supplies it to the SDK for the x-header.
// You will need to use this same call to supply the device_id to your auth server for storage with the user profile.

UIDevice.current.identifierForVendor?.uuidString

To generate and set the device_id, see the OS specific examples to the right. On IOS it is handled automatically using identifierForVendor?.uuidString. Android must manually generate, store, read, and pass the device_id to the SDK.

Create the Session

In the samples to the right, accessToken is the Login JWT token you created above.

Note in the Android sample you are also passing in an AuthRequestProvider. This is used for handling expiring JWT sessions, and is explained in the next section.

import UIKit
import RezolveSDK

class SandboxViewController: UIViewController {

    private let API_KEY: String = "your_api_key"
    private let API_ENVIRONMENT: RezolveEnv = .sandbox
    private let accessToken: String = "abc123.abc123.abc123"
    private let entityId: String = "123"
    private var partnerId: String = "123"

    override func viewDidLoad() {
        super.viewDidLoad()

        let sdk = Rezolve(apiKey: API_KEY,
                          partnerId: partnerId,
                          subPartnerId: nil,
                          environment: API_ENVIRONMENT,
                          config: nil,
                          sspActManagerSettings: nil,
                          coordinatesConverter: .default)

        sdk.createSession(accessToken: accessToken, username: "", entityId: entityId, partnerId: partnerId) { (session, error)

            // your rezolve SDK logic here
        }
    }
}

Handling JWT Expiration & Session Preservation

import RezolveSDK
import Foundation

internal enum Result<T> {
    case success(T)
    case failure(HttpResponse)
}

internal protocol TokenRenewProtocol {
    func renewToken(result: @escaping (Result<RezolveSession>) -> Void)
}

internal class AuthService: NSObject, TokenRenewProtocol {

    func renewToken(result: @escaping (Result<RezolveSession, AuthenticationError>) -> Void) {

        let urlString = ""
        let endpoint = "/api/v1/authentication/register"
        let data = ["username": "user@email.com", "password": "password"]

        guard let url = URL(string: urlString).appendPathComponent(endpoint),
              let request = URLRequest(url: url) else {
            preconditionFailure("Url not created, verify check path and host")
        }

        let device: DeviceProfile  = ... // The device profile 
        let entityId: String =  ... // Entity Id
        let partnerId: String = ... // Partner Id

        let dictionary: [String: String] = [
            "device_id": device.deviceId,
            "make": device.make,
            "os_type": device.osType,
            "os_version": device.osVersion,
            "locale": device.locale
            "entityId": entityId,
            "partnerId": partnerId
        ]

        guard let body = try JSONSerialization.data(withJSONObject: dictionary, options: []) else {
            preconditionFailure("Payload convertion to Data failed")
        }

        request.httpMethod = "POST"
        request.httpBody = body


        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config)
        let task = session.dataTask(with: urlRequest) { data, response, error in
                if let httpResponse = response as? HTTPURLResponse,
                    httpResponse.statusCode == 200 {
                    if let responseData = data,
                       let jsonDictionary = try? JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] {
                        let accessToken = jsonDictionary["access_token"]
                        let publicKey =   jsonDictionary["public_key"]
                        // Securily store received keys
                        // Create new session with received accessToken
                        self.rezolveSdk?.createSession(
                            accessToken: accessToken,
                            entityId: entityId,
                            partnerId: partnerId, 
                            callback: { rezolveSession in
                                // Store new token

                                // Proccess callback
                                callback(.success(rezolveSession))
                        },  errorCallback: { error in
                                // Error handling

                                // Proccess callback
                                callback(.failure(error))
                        })
                       }
                    else {
                        // Handle parsing error
                    }
                }
                else {
                    // Handle error
                }
        }
    }
}

internal final class AddressBookViewController: UIViewController {

    fileprivate(set) var authService: (NSObjectProtocol & TokenRenewProtocol)?

    fileprivate func attemptToGetAddressBook(session: RezolveSession, retryOnFail: Bool) {
        session.addressbookManager.getAll(callback: { addressList in
            // Handle addressList

            }, errorCallback: { httpResponse in
                if retryOnFail && httpResponse.statusCode == 401 {
                    self.authService?.renewToken { result in
                        swith result {
                            case let .success(newSession):
                                // Retries to fetch data from AddressBookManager using the new RezolveSession
                                self.attemptToGetAddressBook(session: newSession, retryOnFail: false)

                            case let .failure(error):
                                // Handle Token Renewal failure
                                debugPrint(error)
                        }
                    }
                }
        })
    }
}

The Login JWT you generate is included in the headers of every SDK transmission. Thus, when your consumer logs out, you can expire the JWT, and the app will cease communication with the Rezolve server. To do this, create a new JWT with an expiration stamp in the past, and supply it to the SDK.

This also means you are required to handle JWT token expiration/renewal if you want a session to continue.

Examples are provided to the right. These are NOT an example of implementing SDK code, but rather an example of implementing session rewnewal with your own authentication server.

To handle session expiration smoothly, without interrupting the user session, Rezolve recommends creating a wrapper around any call that contacts the server. If the server returns an authorization error, you can call your token renewal endpoint and issue a new JWT.

The sample shows two classes, an AuthService class and an AddressBookViewController class.

The AuthService class may, for example, process username/passwords for login, handle registering your users, and handle password resets. It should also handle the JSON Web Tokens to register, create session, and maintain session with Rezolve. The example method renewJwtToken is provided as an example of how to renew a token.

The AddressBookViewController is an example of how you would implement the AddressbookManager.getAll method so as to smoothly handle token expiration and renewal to provide continuity of session.

  1. call the method attemptToGetAddressBook passing in a Rezolve session and the retryOnFail boolean
  2. the method calls the addressbookManager.getAll method
  3. if the call succeeds, proceed as normal, displaying the addressList
  4. if the call results in an error, check if the error is a 401.
  5. If the error is a 401, and the retryOnFail is true, request a new JWT, calling the renewJwtToken method. Then retry the request.
  6. If the error is 401, and retryOnFail is false, we know that we requested a new token and the token provided was expired (indicating end of login session at auth server). Do not retry.
  7. If a different error occurs, log and handle the error.

HTTP Error Responses

403

{
    "type": "Authentication"
    "code": "1"
    "message": "Access is denied."
}

For security reasons, Rezolve masks certain potential issues behind a generic 403 response:

401

{
    "type": "Expired Token"
    "code": "8"
    "message": "Your token has expired, refresh your token and try again."
}

If you receive this response, call your JWT renewal endpoint.

Consumer Profile Management

Once you have established a session, you have access to the consumer's records. These include:

There are no specific flows to consider when managing the customer profile and associated records.

AddressbookManager, FavouriteManager, PhonebookManager and WalletManager support the following CRUD operations: create, update, delete, getAll, get.

ProfileManager supports only update and get.

SSP Engagement Detection

The Self-Serve Portal, or SSP, enables merchants to create Engagements. Engagements provide a way for the merchant to interact with consumers wherever they are - through print, audio/video, geolocation, and more.

Engagements are composed of a Trigger (specially customized media) linked to a Target (the content the merchant wants the consumer to see). Triggers include watermarked images, watermarked audio, geozones, QR Codes, and touch links. Targets include Information Pages, Acts, Products, Categories, and Urls.

This section describes how to capture information from the Trigger when it is detected, and resolve that into a Target.

1. Initialize ScanManager

guard let scanManager = rezolveSession?.getScanManager() else {
    return
}

2. Set up a ProductDelegate

scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)

extension ViewController: ProductDelegate {

    func onStartRecognizeImage() {
        // Suggestion: Show an interstitial loader
    }

    func onFinishRecognizeImage() {
        // Suggestion: Hide an interstitial loader
    }

    func onCategoryResult(merchantId: String, category: RezolveCategory) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onProductResult(product: Product) {
        // See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
    }

    func onError(error: String) {
        // Handle error gracefully
    }
}

3. Add a function to handle SSP Engagements

scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)

extension ViewController: ProductDelegate {

    // Functions from previous example hidden for brevity

    func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
        // Object `engagement` contains the basic structure of a SSP resolved engagement
        let id = engagement.engagementId
        let scanId = engagement.serviceId
        let path = engagement.payloadPath
        let payoffs = engagement.payoffs
        let payload = engagement.rezolveCustomPayload
    }
}

4. Inspect the payload

// Any of these can be `null`, but one should have content
let act = payload.act?.act      // Information for linked Act
let product = payload.product   // Information for linked Product
let category = payload.category // Information for linked Category

The non-null object item corresponds to the target set by the merchant. You must handle all target types. This will be covered in the following section.

SSP Location Triggers

Location Triggers are a powerful SDK feature introduced in 2020 that allows the developer to detect when the consumer has entered a predefined geozone and pop a notification of a product, informational page, or Act. A geozone is defined as a radius of X meters around a specified lat/long point, as set up by a merchant or partner in the Rezolve portal.

When the app is active, the SDK continuously samples the consumer's location, and when the consumer is within the radius of one or more geozones, events fire to alert the system to perform the desired behavior. The SDK intelligently handles areas with a high density of overlapping geozones to prevent spamming the user with notifications. If the consumer is in an area with multiple geozones, the SDK bundles multiple notifications into one alert that contains the messages from all the relevant zones.

Topics

Setup location detection

// Add the following permission descriptions on your Info.plist.
- NSLocationAlwaysUsageDescription
- NSLocationAlwaysAndWhenInUseUsageDescription
- NSLocationWhenInUseUsageDescription

// Enable Background Modes for your app’s target.
- Location Updates
- Background Fetch

This section describes how to set up your project to use the location features of the Rezolve SDK.

Initializing location detection

// On Rezolve class initialization you need to pass your SSP configuration into sspActManagerSettings parameter, which is a class of RezolveSDK.SspActManagerSettings. As an example, it has the following structure.

let sspActManagerSettings = SspActManagerSettings(
    auth0ClientId: "{String}",
    auth0Secret: "{String}",
    auth0Audience: "{String}",
    auth0Endpoint: "{String}",
    sspEndpoint: "{String}",
    sspWidth: "{String}",
    baiduLocationKey: "{String}"
)
// Adding a baiduLocationKey is not mandatory, but will definitely increase tracking accuraccy if you are targeting regions located in China mainland.

Initializing location detection requires setting up the SspActManager with appropriate settings.

Detecting geozones

// Setting up Rezolve SDK with an active SSP Act Manager configuration.
let sdk = Rezolve(apiKey: Config.rezolveApiKey,
    partnerId: Config.partnerId,
    subPartnerId: nil,
    environment: Config.env,
    config: nil,
    sspActManagerSettings: sspActManagerSettings,
    coordinatesConverter: CoordinatesConverter.default)

// Start monitoring for Nearby Engagements.
let ssp = RezolveService.sdk?.createRezolveSsp()
ssp?.nearbyEngagementsManager.startMonitoringForNearbyEngagements()
ssp?.nearbyEngagementsManager.debugNotifications = false
ssp?.nearbyEngagementsManager.delegate = self

// Stop monitoring for Nearby Engagements.
ssp?.nearbyEngagementsManager.stopMonitoringForNearbyEngagements()
ssp?.nearbyEngagementsManager.stopUpdatingDistanceFromNearbyEngagements()
ssp?.nearbyEngagementsManager.resetNotificationSuppressData()
ssp?.nearbyEngagementsManager.delegate = nil

During detection, the user's location is monitored. Detection times vary according to OS settings.

For IOS, the following applies: "When testing your region monitoring code in iOS Simulator or on a device, realize that region events may not happen immediately after a region boundary is crossed. To prevent spurious notifications, iOS doesn't deliver region notifications until certain threshold conditions are met. Specifically, the user's location must cross the region boundary, move away from the boundary by a minimum distance, and remain at that minimum distance for at least 20 seconds before the notifications are reported.

The specific threshold distances are determined by the hardware and the location technologies that are currently available. For example, if Wi-Fi is disabled, region monitoring is significantly less accurate. However, for testing purposes, you can assume that the minimum distance is approximately 200 meters."

Handling detection and notification

// Handle incoming changes regarding Geofence status from the SDK.

extension RezolveGeofence: NearbyEngagementsManagerDelegate {
    func didStartMonitoring(for circularRegions: [CLCircularRegion], coordinate: CLLocationCoordinate2D, radius: Int) {
        print("didStartMonitoring")
    }

    func didEnter(circularRegion: GeofenceData) {
        print("didEnter")
    }

    func didFail(with error: Error) {
        print("didFail -> \(error.localizedDescription)")
    }

    func didUpdateCurrentDistanceFrom(location: CLLocationCoordinate2D, geofences: [GeofenceData], beacons: [BeaconData]) {
        print("didUpdateCurrentDistanceFrom")
    }

    func didReceiveInAppNotification(act: SspResolverAct?) {
        print("didReceiveInAppNotification")
    }
}

When a geofence is detected, notification is potentially shown, as long as it is not within a silent period from a previous detection.

SSP Act Targets

1. Identify the Act payload

let act = payload.act?.act      // Information for linked Act
let product = payload.product   // Information for linked Product
let category = payload.category // Information for linked Category

// Any of the above are not `null`, and a `customUrl` is detected, we need to redirect the user to this linked entity
let actCustomUrl = act?.customUrl
let productCustomUrl = product?.customUrl
let categoryCustomUrl = category?.customUrl

Acts and Information Pages are closely related. An information page simply displays the merchant's message to the consumer. An Act is like a info page that includes a form the user must fill out.

If payload.act?.act is not null, and act?.customUrl IS null, you have an Act.

2. Display the Act

let actId = act?.id
let actPageBuildingBlocks = act?.pageBuildingBlocks
let actIsInformationPage = act?.isInformationPage
let actQuestions = act?.questions

// Iterate through the Act Page Building Blocks and render on screen
for actPageBuildingBlock in actPageBuildingBlocks! {
    print(actPageBuildingBlock)
}

// Iterate through the Act Questions and render on screen
for actQuestion in actQuestions! {
    print(actQuestion)
}

A merchant creates an Act by adding display items (building blocks) and then adding form questions.

Iterate first through the building blocks, and then through the questions, rendering them on screen.

3. Submitting Act answers

let user: User // Current user
let location: CLLocation? // Current user location if available
let sspAct: SspAct // SSP Act to submit 
let page: Page // User answers

func answer(element: Page.Element) -> SspSubmissionAnswer? {
    switch element {
    case .text, .divider, .image, .video:
        return nil
    case .dateField(let dateField):
        guard let value = dateField.value else {
            return nil
        }
        let date = // Map Date to string
        return SspSubmissionAnswer(questionId: dateField.id, answer: date)
    case .select(let select):
        guard let value = select.value else {
            return nil
        }
        return SspSubmissionAnswer(questionId: select.id, answer: String(value.value))
    case .textField(let text):
        guard let value = text.value else {
            return nil
        }
        return SspSubmissionAnswer(questionId: text.id, answer: value)
    }
}

// Map page inputs to submission answers
let answers = page.elements.compactMap(answer(element:))

The example to the right shows an easy way to map all user provided answers into a meaningful object.

3a. How to format the Act submission

// Creating Ssp Act Sumbission model
let submission = SspActSubmission(
    userId: user.id,
    userName: user.username,
    personTitle: user.title,
    firstName: user.firstName,
    lastName: user.lastName,
    email: user.email,
    phone: user.phone,
    scanId: sspAct.scanId,
    latitude: location?.latitude,
    longitude: location?.longitude,
    answers: answers
)

Next you will need to create the basic model for submitting an Act. All of the previously declared details must be preset in the structure, like User, Act, Location, Answers.

3b. Do the submission

// Submission
sspActManager.submitAct(actId: sspAct.id, actSubmission: submission) { (result) in
    switch result {
    case .success(let submission):
        // Act submitted
    case .failure(let error):
        // Handle error gracefully
    }
}

Lastly, execute the final form of the object passed on one of the method’s parameters.

Get an SSP Act By Id

rezolveSession?.sspActManager.getAct(actId: id) { (result) in
    switch result {
    case .success(let act):
        if let pageBlocks = act.pageBuildingBlocks {
            // pageBuildingBlocks can be used directly to render UI, or PageBuilder can be used to map blocks to more useful models (user input, validation)
            do {
                let page = try PageBuilder().build(from: pageBlocks)
                // Use page model to render UI
            } catch {
                // Handle error gracefully
            }

        } else {

        }
        let isInformationPage = act.isInformationPage
        // submission should not be available if isInformationPage == true
    case .failure(let error):
        // Handle error gracefully
    }
}

If you have an Act Id, you can retrieve it using the example code to the right.

SSP Product Targets

func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
    let id = engagement.engagementId
    let scanId = engagement.serviceId
    let path = engagement.payloadPath
    let payoffs = engagement.payoffs
    let payload = engagement.rezolveCustomPayload

    // The logic of parsing, is that you might find a `customUrl` in the structure not being `null`.
    // Hence, we will need to examine it and redirect the user to the linked Web Page, Product or Category
    if let act = payload.act?.act {
        if let customUrl = act.customUrl {
            // Parse act.customUrl
            return
        }
    } else if let product = payload.product?.customUrl {
        if let customUrl = product.customUrl {
            // To Parse product.customUrl 
            // Check if url begins with "https://rzlv.co"
            // Check if url path is in format "/[int]/[int]/[int]/[int]"
            // If both the above are true, you have a valid RezolveTrigger. Use Trigger Manager to look up the product. 
            // If the url does not meet the above rules, it is not a RezolveTrigger, it is a regular url. Redirect the user to the url. 
            return
        }
    } else if let category = payload.category?.customUrl {
        if let customUrl = category.customUrl {
            // Parse category.customUrl
            return
        }
    }
}

To handle a product result, get the product custom URL and test it.

If the url is a RezolveTrigger, similar in format to https://rzlv.co/1/2/3/4, use Trigger Manager to fetch the product. See code sample for test advice.

If the url is NOT a RezolveTrigger, assume it is a regular url, and you should handle it as you see fit. The typical behavior is a simple redirect to the url, to be opened in a browser.

SSP Category Targets

func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
    let id = engagement.engagementId
    let scanId = engagement.serviceId
    let path = engagement.payloadPath
    let payoffs = engagement.payoffs
    let payload = engagement.rezolveCustomPayload

    // The logic of parsing, is that you might find a `customUrl` in the structure not being `null`.
    // Hence, we will need to examine it and redirect the user to the linked Web Page, Product or Category
    if let act = payload.act?.act {
        if let customUrl = act.customUrl {
            // Parse act.customUrl
            return
        }
    } else if let product = payload.product?.customUrl {
        if let customUrl = product.customUrl {
            // Parse product.customUrl 
            return
        }
    } else if let category = payload.category?.customUrl {
        if let customUrl = category.customUrl {
            // To Parse category.customUrl 
            // Check if url begins with "https://rzlv.co"
            // Check if url path is in format "/[int]/[int]/[int]"  (note: one fewer parameter than a product)
            // If both the above are true, you have a valid RezolveTrigger. Use Trigger Manager to look up the category. 
            // If the url does not meet the above rules, it is not a RezolveTrigger, it is a regular url. Redirect the user to the url. 
            return
        }
    }
}

To handle a category result, get the category custom URL and test it.

If the url is a RezolveTrigger, similar in format to https://rzlv.co/1/2/3, use Trigger Manager to fetch the category. See code sample for test advice.

If the url is NOT a RezolveTrigger, assume it is a regular url, and you should handle it as you see fit. The typical behavior is a simple redirect to the url, to be opened in a browser.

SSP URL Targets

let act = payload.act?.act      // Information for linked Act
let product = payload.product   // Information for linked Product
let category = payload.category // Information for linked Category

// Any of the above are not `null`, and a `customUrl` is detected, we need to redirect the user to this linked entity
let actCustomUrl = act?.customUrl
let productCustomUrl = product?.customUrl
let categoryCustomUrl = category?.customUrl

If you find a non-null payload, next check for a non-null customUrl

If a custom URL is found, you redirect the user to the linked entity.

Trigger Manager

Triggers are specially formatted media in the Rezolve system that can point to products, categories, and other resources. Image Engagements and Audio Engagements are a type of trigger, and these are handled by ScanManager. Touch Triggers are handled by TriggerManager. Touch Triggers are essentially URLs that are typically rendered as a touchable link or button onscreen.

Touch Triggers are used by the Background Listening feature to surface items detected from audio watermarks in a Background Listening session.

Touch Triggers could also be used to create a Wishlist feature; ask your sales person for the Wishlist/Favorites Solution Paper.

Trigger Manager provides a way to resolve touch triggers into actionable content, like products or categories.

Touch Engagements, or touch triggers, always have the same format: http://rzlv.co/[partnerId]/[merchantId]/[categoryId]/[productId][(optional...)?ad=[adId]&placement=[placementId]]

To use touch triggers, the partner should watch for urls in their content stream that match this pattern, render them as a touchable link, and then when touched, pass the url to the TriggerManager.resolveTrigger method.

Trigger Manager Example

// Sample URL and `TriggerManager` initialization

let url = URL(string: "http://rzlv.co/1/2/3/8?ad=20&placement=25")

rezolveSession?.triggerManager.resolve(
    url: url!,
    productDelegate: self,
    onRezolveTriggerStart: {},
    onRezolveTriggerEnd: {},
    errorCallback: { error in }
)

extension ViewController: ProductDelegate {

    func onStartRecognizeImage() {
        // Suggestion: Show an interstitial loader
    }

    func onFinishRecognizeImage() {
        // Suggestion: Hide an interstitial loader
    }

    func onCategoryResult(merchantId: String, category: RezolveCategory) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onProductResult(product: Product) {
        // See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
    }

    func onError(error: String) {
        // Handle error gracefully
    }
}

Product Scan, Instant Buy flow


[ View full size ]

The premise of Shoppable Ads is to capture an image scan (usually of an advertisement) using the Scan Manager, resolve it into a product URL, fetch the product info, and enable purchase via saved account information.

In the Instant Buy flow, we purchase the product immediately, without first adding it to the cart.

1. Capture image and get product

// Initialize `ScanManager` based on your RezolveSDK Session

guard let scanManager = rezolveSession?.getScanManager() else {
    return
}

scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)

extension ViewController: ProductDelegate {

    func onStartRecognizeImage() {
        // Suggestion: Show an interstitial loader
    }

    func onFinishRecognizeImage() {
        // Suggestion: Hide an interstitial loader
    }

    func onCategoryResult(merchantId: String, category: RezolveCategory) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onProductResult(product: Product) {
        // See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
    }

    func onError(error: String) {
        // Handle error gracefully
    }
}

First, initialize scanManager, and enable the scan screen using session.startVideoScan(), and capture a watermarked image. The scanManager recognizes the encoded product data, and extracts merchantId, catalogId, and productId from the image, automatically calling getProduct. The scanManager returns a product object.

2. Get shipping and payment options for the product

let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"

rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<PaymentOption, RezolveError>) in
    switch result {
    case .success(let option):
        {
            // For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
            let paymentMethod  = option.supportedPaymentMethods.first!
            let shippingMethod = option.supportedDeliveryMethods.first!
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Call PaymentOptionManager to get shipping and payment options for the current merchant. This tutorial assumes the consumer chose a form of credit card payment, and chose home delivery.

Note: You must repeat this call if the user chooses a different product variant (size, color, etc), changes product quantity, changes shipping choice, or changes payment option.

For more information on what is returned by getProductOptions, see the Background Listening tutorial.

3. Show payment card choices

rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in
    // Handle payment cards
}

Use walletManager.getAll to list the available card choices. If the consumer wishes to buy, they will select a payment card to use, and provide confirmation of ordering intent.

We recommend using a "slide to buy" button to confirm purchase intent, while preserving the maximum ease of use.

4. Create a checkout bundle, checkout the product to get totals, and create an order

let sampleProductCheckoutBundle = CheckoutBundle(
    checkoutProduct: checkoutProduct,
    shippingMethod: deliveryMethod,
    merchantId: merchantId,
    optionId: optionId,
    paymentMethod: paymentMethod,
    paymentRequest: nil,
    phoneId: phoneId,
    location: userLocation
)

rezolveSession?.checkoutManager.checkout(bundle: sampleProductCheckoutBundle) { (result: Result<Price, Error>) in
    switch result {
    case .success(let order):
        {
            print(order.id)
            print(order.finalPrice)

            // ...
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Once you have product information, create a CheckoutProduct object. Then call the SDK CheckoutManagerV2.checkoutProduct method to create an order and get totals. The response order object includes an order id, order total, and price breakdowns.

5. Submit payment for order

let paymentCard = // RezolveSDK.PaymentCard
let cardCVV = "000" // Card CVV

let sampleProductCheckoutBundle = CheckoutBundle(
    checkoutProduct: checkoutProduct,
    shippingMethod: deliveryMethod,
    merchantId: merchantId,
    optionId: optionId,
    paymentMethod: paymentMethod,
    paymentRequest: PaymentRequest(paymentCard: paymentCard, cvv: cardCVV),
    phoneId: phoneId,
    location: userLocation
)

rezolveSession?.checkoutManager.buy(bundle: sampleProductCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
    switch result {
    case .success(let order):
        {
            print(order.id)
            print(order.partnerId)
            print(order.partnerName)

            // ...
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

When the user confirms intent, pass the card choice and the entered CVV value to the createPaymentRequest method. This creates the encrypted paymentRequest object needed for checkout.

In this tutorial, we assume the user chose credit card payment. Note that paymentRequest is actually optional here, and can be null. To determine if it's needed, please check selected SupportedPaymentMethod's type.

Pass a paymentRequest object, checkoutBundleV2 object, the orderId, and an interface or callback to the buyProduct method. The success response will an OrderSummary object. Note that this does not mean the order was confirmed, only that the request was successfully received.

Note that the call to signOrderUpdate shown in the sequence diagram is no longer required.

Product Scan, Cart flow


[ View full size ]

The premise of Shoppable Ads is to capture an image scan (usually of an advertisement) using the Scan Manager, resolve it into a product URL, fetch the product info, and enable purchase via saved account information.

In the Cart Buy flow, one or more products are added to a cart. Each merchant will have a separate cart, and multiple carts can contain products at a time. For this example we are assuming only one cart is active, but you can check for multiple carts using CheckoutManagerV2.getCarts.

1. Capture image and get product

// Initialize `ScanManager` based on your RezolveSDK Session

guard let scanManager = rezolveSession?.getScanManager() else {
    return
}

scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)

extension ViewController: ProductDelegate {

    func onStartRecognizeImage() {
        // Suggestion: Show an interstitial loader
    }

    func onFinishRecognizeImage() {
        // Suggestion: Hide an interstitial loader
    }

    func onCategoryResult(merchantId: String, category: RezolveSDK.Category) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onCategoryProductsResult(merchantId: String, category: RezolveSDK.Category, productsPage: PageResult<DisplayProduct>) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onProductResult(product: Product) {
        // See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
    }

    func onError(error: String) {
        // Handle error gracefully
    }
}

First, initialize scanManager, and enable the scan screen using session.startVideo(), and capture a watermarked image. The scanManager recognizes the encoded product data, and extracts merchantId, catalogId, and productId from the image, automatically calling getProduct. The scanManager returns a product object.

2. Add Product to the Cart

let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"

rezolveSession?.cartManager.createCartWithProduct(sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<CartDetails, RezolveError>) in
    switch result {
    case .success(let cart):
        {
            print(cart.id)
            print(cart.merchantId)

            // ...
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Call CheckoutManagerV2.addProductToCart to add the product to cart.

3. Get shipping and payment options for the cart

let sampleMerchantID = "12"
let sampleCartID = "1"

rezolveSession?.paymentOptionManager.getPaymentOptionsForCartWith(merchantId: sampleMerchantID, cartId: sampleCartID) { (result: Result<[PaymentOption], RezolveError>) in
    switch result {
    case .success(let options):
        {
            // For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
            let paymentMethod  = options.first?.supportedPaymentMethods.first
            let shippingMethod = options.first?.supportedDeliveryMethods.first
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Call PaymentOptionManager to get shipping and payment options for the current merchant. This tutorial assumes the consumer chose a form of credit card payment, and chose home delivery.

Note: You must repeat this call if the user chooses a different product variant (size, color, etc), changes product quantity, changes shipping choice, or changes payment option.

For more information on what is returned by getCartOptions, see the Background Listening tutorial.

4. Show payment card choices

rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in
    // Handle payment cards
}

Use walletManager.getAll to list the available card choices. If the consumer wishes to buy, they will select a payment card to use, and provide confirmation of ordering intent.

We recommend using a "slide to buy" button to confirm purchase intent, while preserving the maximum ease of use.

5. Create a checkout bundle, checkout the cart to get totals, and create an order

let sampleCartCheckoutBundle = CheckoutBundle(
    cartId: cartId,
    shippingMethod: deliveryMethod,
    merchantId: merchantId,
    optionId: optionId,
    paymentMethod: paymentMethod,
    paymentRequest: nil,
    phoneId: phoneId,
    location: userLocation
)

rezolveSession?.checkoutManager.checkout(bundle: sampleCartCheckoutBundle) { (result: Result<Price, Error>) in
    switch result {
    case .success(let order):
        {
            print(order.id)
            print(order.finalPrice)

            // ...
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Create a cart checkout bundle. Then call the SDK CheckoutManagerV2.checkoutCart method (Android) or CheckoutManagerV2.checkout (IOS) method to create an order and get totals. The response order object includes an order id, order total, and price breakdowns.

6. Submit payment for order

let paymentCard = // RezolveSDK.PaymentCard
let cardCVV = "000" // Card CVV

let sampleCartCheckoutBundle = CheckoutBundle(
    cartId: cartId,
    shippingMethod: deliveryMethod,
    merchantId: merchantId,
    optionId: optionId,
    paymentMethod: paymentMethod,
    paymentRequest: PaymentRequest(paymentCard: paymentCard, cvv: cardCVV),
    phoneId: phoneId,
    location: userLocation
)

rezolveSession?.checkoutManager.buy(bundle: sampleCartCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
    switch result {
    case .success(let order):
        {
            print(order.id)
            print(order.partnerId)
            print(order.partnerName)

            // ...
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

When the user confirms intent, pass the card choice and the entered CVV value to the createPaymentRequest method. This creates the encrypted paymentRequest object needed for checkout.

In this tutorial, we assume the user chose credit card payment. Note that paymentRequest is actually optional here, and can be null. To determine if it's needed, please check selected SupportedPaymentMethod's type.

Pass the paymentRequest object, thecheckoutBundleV2 object, the orderId, and an interface or callback to the buyCart method. The success response will be the order id as a string. Note that this does not mean the order was confirmed, only that the request was successfully received.

Note that the call to signOrderUpdate shown in the sequence diagram is no longer required.

Category Scan flow


[ View full size ]

Shoppable Ads can do more than link to a single product, it can link to a category of products in your Rezolve Commerce Engine. Scanning an ad that contains a category link will bring up a list of subcategories and products in that category.

Once a product has been selected, purchasing follows one of the previous examples of Instant Buy Flow or Cart Buy Flow (minus the scan step).

1. Scan a category shoppable ad, get a getCatalog response

// Initialize `ScanManager` based on your RezolveSDK Session

guard let scanManager = rezolveSession?.getScanManager() else {
    return
}

scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)

extension ViewController: ProductDelegate {

    func onStartRecognizeImage() {
        // Suggestion: Show an interstitial loader
    }

    func onFinishRecognizeImage() {
        // Suggestion: Hide an interstitial loader
    }

    func onCategoryResult(merchantId: String, category: RezolveCategory) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onProductResult(product: Product) {
        // See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
    }

    func onError(error: String) {
        // Handle error gracefully
    }
}

First, initialize scanManager, and enable the scan screen using session.startVideo(), and capture a watermarked image. The scanManager returns a Category object, which is then parsed to determine its contents.

2. Repeat for navigation until ready to purchase

Repeat step 1 above as the consumer browses through categories. Once the consumer selects a product and is ready to purchase, the process is the same as in the Instant Buy or Cart Buy flows. Note: a DisplayProduct does not contain full information on a product. You will need to call getProduct to display the detail view of the Product.

Mall flow


[ View full size ]

Mall is the only method for finding products that does not require an ad scan. A consumer enters the mall by clicking on a "Mall" navigation option within the mobile app. The Mall showcases active merchants in an attractive layout that is condusive to casual browsing and subsequent purchasing.

Once a merchant is selected, the consumer shifts into category/product browse mode. Once a product has been selected, purchasing follows one of the previous examples of Instant Buy Flow or Cart Buy Flow (minus the scan step).

1. Get List of Merchants in the mall

rezolveSession?.merchantManager.getMerchants { (result: Result<[Merchant], RezolveError>) in
    switch result {
    case .success(let merchants):
        {
            merchants.forEach {
                // Basic information
                let id = $0.id
                let name = $0.name
                let tagline = $0.tagline
                let contactInformation = $0.contactInformation
                let termsAndConditions = $0.termsAndConditions

                // Assets
                let banner = $0.banner
                let logo = $0.logo
                let bannerThumbs = $0.bannerThumbs
                let logoThumbs = $0.logoThumbs
            }
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

First, initialize MerchantManager, and call GetMerchants, providing an implementation of MerchantCallback as a parameter. This will return an array of Merchant objects. Parse each merchant object to get the id, name, tagline, banner, bannerThumbs, and logoThumbs.

2. Get list of first-level Categories for the selected Merchant

let sampleMerchantID = "12"

rezolveSession?.productManager.getRootCategoryForMerchantWith(id: sampleMerchantID) { (result: Result<RezolveSDK.Category, RezolveError>) in
    switch result {
    case .success(let category):
        {
            // Basic information
            let id = category.id
            let parentId = category.parentId
            let name = category.name
            let hasCategories = category.hasCategories
            let hasProducts = category.hasProducts

            // Assets
            let image = category.image
            let imageThumbs = category.imageThumbs

            // Get subcategories, if any
            if hasCategories {
                category.categories.forEach { subCategory in
                    print(subCategory.id)
                    print(subCategory.parentId)
                    print(subCategory.name)

                    // ...
                }
            }
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Display your subcategories and products as returned by the getCategories call.

For subsequent navigation in categories, use getProductsAndCategories.

3. If the consumer clicks a subcategory, call getProductsAndCategories.

let sampleMerchantID = "12"
let sampleCategoryID: Int = 70

let pageNavigationFilters = PageNavigationFilter(
    productsFilter: PageNavigation(count: 100, pageIndex: 1, sortBy: "product", sort: .ascending),
    categoryFilter: PageNavigation(count: 100, pageIndex: 1, sortBy: "category", sort: .ascending)
)

rezolveSession?.productManager.getPaginatedCategoriesAndProducts(merchantId: sampleMerchantID, categoryId: sampleCategoryID, pageNavigationFilters: pageNavigationFilters) { (result: <Rezolve, RezolveError>) in
    switch result {
    case .success(let category):
        {
            // Basic information
            let id = category.id
            let parentId = category.parentId
            let name = category.name
            let hasCategories = category.hasCategories
            let hasProducts = category.hasProducts

            // Assets
            let image = category.image
            let imageThumbs = category.imageThumbs

            // Get subcategories, if any
            if hasCategories {
                category.categories.forEach { subCategory in
                    print(subCategory.id)
                    print(subCategory.parentId)
                    print(subCategory.name)

                    // ...
                }
            }

            // Get display products, if any
            if hasProducts {
                category.products.forEach { displayProduct in
                    print(displayProduct.id)
                    print(displayProduct.name)
                    print(displayProduct.price)

                    // ...
                }
            }
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

As the consumer navigates the category tree, call getProductsAndCategories to pull a paginated lists of subcategories and products for that category.

4. If the consumer clicks a Product, call getProduct

let sampleMerchantID = "12"
let sampleCategoryID: Int = 70
let sampleProductID: Int = 6
let sampleProduct = Product(id: sampleProductID)

rezolveSession?.productManager.getProductDetails(merchantId: sampleMerchantID, categoryId: sampleCategoryID, product: sampleProduct) { (result: Result<Product, RezolveError>) in
    switch result {
    case .success(let product):
        {
            print(product.id)
            print(product.merchantId)
            print(product.title)
            print(product.subtitle)
            print(product.price)
            print(product.description)

            product.images.forEach {
                print($0)
            }

            product.options.forEach { option in
                print(option.label)
                print(option.code)
                print(option.extraInfo)

                option.values.forEach { optionValue in
                    print(optionValue.value)
                    print(optionValue.label)
                }
            }

            product.optionAvailable.forEach {
                $0.combination.forEach { variant in
                    print(variant.code)
                    print(variant.value)
                    print(variant.id)
                }
            }

            product.customOptions.forEach {
                print($0.isRequire)
                print($0.optionId)
                print($0.sortOrder)
                print($0.title)
                print($0.optionType)

                $0.values.forEach { value in
                    print(value.sortOrder)
                    print(value.title)
                    print(valueId)
                }

                $0.valuesId.forEach { valueId in
                    print(valueId)
                }

                print($0.value)
            }

            print(product.productPlacement)
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

If the consumer clicks a Product, call getProduct to fetch full product information. At this point, the user can either add the product to their cart, click "buy now", or press "back" to navigate to the category view.

Search is available to aid in the presentation and usability of large malls and/or large product catalogs. You can search for merchants, or for products.

let searchData = MerchantSearchData(
    query: nil,
    orderBy: .location,
    order: .ascending,
    deviceInfo: DeviceInfo.current,
    location: RezolveLocation(longitude: -73.9502622, latitude: 40.6726499)
)
let currentPage: Int = 1
let itemsLimit: Int = 50

rezolveSession?.merchantManager.searchForMerchants(using: searchData, page: currentPage, limit: itemsLimit) { (result: Result<MerchantSearchResult, RezolveError>) in
    switch result {
    case .success(let merchantSearchResult):
        {
            // Current state
            let page = merchantSearchResult.page
            let total = merchantSearchResult.total
            let merchants = merchantSearchResult.merchants

            for merchant in merchants {
                // Base information
                let id = merchant.id
                let priority = merchant.priority
                let name = merchant.name
                let tagLine = merchant.tagline
                let distance = merchant.distance

                // Assets
                let banner = merchant.banner
                let bannerThumbs = merchant.bannerThumbs
                let logo = merchant.logo
                let logoThumbs = merchant.logoThumbs

                // Extra information
                let info = merchant.contactInformation
                let infoEmail = info?.email
                let infoName = info?.name
                let infoPhone = info?.phone

                // List of Stores
                if let stores = merchant.stores {
                    for store in stores {
                        let id = store.id
                        let name = store.name
                        let location = store.location
                    }
                }

                // List of Terms & Conditions
                let termsAndConditions = merchant.termsAndConditions

                for item in termsAndConditions {
                    let id = item.id
                    let storeId = item.storeId
                    let name = item.name
                    let content = item.content
                    let checkboxText = item.checkboxText
                }
            }
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Merchant search offers the following features:

Note that to return merchants sorted by distance, each merchant in your ecosystem must configure at least one "Store" in the Merchant Portal, supplying latitude and longitude. Which ever merchant has a store closest to the user will be returned first.

To search for a merchant you must supply the following parameters:

See code samples to the right.

let productSearchData = ProductSearchData(
    query: "book",
    merchantId: "123",
    orderBy: .score,
    order: .descending,
    location: RezolveLocation(longitude: -73.9502622, latitude: 40.6726499),
    includeInResults: .products,
    deviceInfo: DeviceInfo.current
)
let currentPage: Int = 1
let itemsLimit: Int = 50

rezolveSession?.productManager.searchForProducts(using: productSearchData, page: currentPage, limit: itemsLimit, completionHandler: { (result: Result<ProductSearchResult, RezolveError>) in
    switch result {
    case .success(let productSearchResult):
        {
            // Current state
            let page = productSearchResult.page
            let total = productSearchResult.total
            let products = productSearchResult.products

            for product in products {
                // Base information
                let id = product.id
                let merchantId = product.merchantId
                let merchantName = product.merchantName
                let categoryId = product.categoryId
                let categoryName = product.categoryName
                let price = product.price
                let isAct = product.isAct
                let isVirtual = product.isVirtual
                let hasRequiredOptions = product.hasRequiredOptions

                // Assets
                let image = product.image
                let imageThumbs = product.thumbs
            }
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Product search offers the following features:

To search for products you must supply the following parameters:

See code samples to the right.

External Payment

All previous documentation has assumed that payment processing occurs in the Rezolve system. Rezolve can support multiple payment integrations, and often this is the easiest route.

Rezolve also supports "external payments" however, in the case where a developer wishes build their own transaction processing. This section explains how to create an order and then update the payment status, for payment transactions processed outside Rezolve.

Create the checkoutBundle

let paymentMethod.type = "external"

let sampleProductCheckoutBundle = CheckoutBundle(
    checkoutProduct: checkoutProduct,
    shippingMethod: deliveryMethod,
    merchantId: merchantId,
    optionId: optionId,
    paymentMethod: paymentMethod,
    paymentRequest: nil,
    phoneId: phoneId,
    location: userLocation
)

Processing an external payment is similar to processing a regular payment, with a couple key differences that occur when making the checkoutBundle. This example shows a checkout bundle for a single product, but the same changes will apply to a cart bundle.

The first difference is, when making the checkoutBundle, set paymentRequest to nil, and leave it. You will not update this with a payment card later.

Second, if external payment is supported, the PaymentOptions returned from PaymentOptionManager.getPaymentOptionFor(...) or paymentOptionManager.getPaymentOptionsForCartWith(...) will have an option for "external" payment. Selecting this one indicates to the system that payments will be processed externally.

Checkout and Buy


rezolveSession?.checkoutManager.checkout(bundle: sampleProductCheckoutBundle) { (result: Result<Price, Error>) in
    switch result {
    case .success(let order):
        {
            print(order.id)
            print(order.finalPrice)

            // ...
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

// ...

rezolveSession?.checkoutManager.buy(bundle: sampleProductCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
    switch result {
    case .success(let order):
        {
            print(order.id)
            print(order.partnerId)
            print(order.partnerName)

            // ...
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

Then pass the checkoutBundle into checkout as usual, getting your order id and final price.

Finally, call the buy endpoint, again as usual.

Buy Response

{
    "phone_number": "111111111",
    "payment_method": "external",
    "partner_name": "Rezolve Shared Partner",
    "partner_id": "56",
    "order_id": "1501",
    "name": "RCE 3",
    "merchant_id": "79",
    "email": "cgouldthorpe+rce3@gmail.com",
    "data": {
        "callback": {
            "payment_id": "ff04ba41-32f1-4f70-a461-3f63fe9e81b9"
        }
    }
}
rezolveSession?.checkoutManager.buy(bundle: sampleProductCheckoutBundle) { (result) in
    switch result {
    case .success(let order):
        if let data = order.data["callback"]?.value as? [AnyHashable: Any] {
            let paymentId = data["payment_id"] as? String
        }
    case .failure(let error):
        // Handle error gracefully
    }
}

A sample JSON response is shown. The code sample shows how to extract the payment_id from the JSON. You will need this to after the payment transaction is processed.

You have now created an order in the system, but still need to handle payment and update the order with the result.

Handle Payment Externally

How this happens is up to you. Your app will need to display any payment method(s) and securely submit the payment for processing in your system.

Once the payment has succeeded or failed, update the order, as shown below.

Update the Order using Callback

// send header... Content-Type: application/json

POST 'https://core.sbx.eu.rezolve.com/api/v3/external/callback/5cec6b9c-ec38-47c7-97ce-b4872d7b684b'
{
    "action": "approve",
    "reason": null
}

To update the order with the payment result, set header Content-Type: application/json and make a POST to the Rezolve callback endpoint, referencing the payment_id in the url and giving the result of the payment transaction in the body.

The endpoints urls for the EU environment are:

The callback has two fields in the body, the action and the reason.

If payment succeeds, action is approve, and the reason is null.

If payment fails, action is reject and the reason is an optional string, which you can populate with any error code and/or explanatory text your customer might need to make an inquiry.

Callback Response

The callback will respond with one of the following http response codes - there is no body to the response:

Responds codes:

Please note that updating the order status is not instantaneous, and so a 204 may be returned instead of 422 if two requests are made in close proximity.

Click and Collect

Click and Collect enables users to select products online, and pick them up in-store. The consumer may either pay online, or pay in-store.

Please note that your Resolve Commerce Engine must have Pick Up In Store enabled under the Advanced menu, for this option to apply.

The Click and Collect flow is no different from in the Cart Buy or Instant Buy examples, it is only the user-chosen values that change.

1. Choices returned by PaymentOptionManager

let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"

rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<PaymentOption, RezolveError>) in
    switch result {
    case .success(let option):
        {
            // For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
            let paymentMethod  = option.supportedPaymentMethods.first!
            let shippingMethod = option.supportedDeliveryMethods.first!
        }

    case .failure(let error):
        // Handle error gracefully
    }
})

When you call PaymentOptionManager.getProductOptions, it returns a list of your payment and shipment options.

2. Inspecting the returned options

{
  "type": "standard",
  "supported_shipping_methods": [
    {
      "execute": {
        "method_code": "flatrate",
        "manual_payment_label": "Pay on Collection",
        "extension_attributes": null,
        "display_name": "Fixed",
        "carrier_code": "flatrate"
      },
      "details": {
        "store_details": null
      }
    },
    {
      "execute": {
        "method_code": "storepickup",
        "manual_payment_label": "Pay on Collection",
        "extension_attributes": [
          {
            "value": "2",
            "code": "pickup_store"
          }
        ],
        "display_name": "Pickup In Store",
        "carrier_code": "storepickup"
      },
      "details": {
        "store_details": {
          "telephone": "01234456789",
          "pickup_store": 2,
          "opening_times": [

          ],
          "name": "Chao Yang Store",
          "location": {
            "longitude": 116.2573779,
            "latitude": 39.9390628
          },
          "email": "china2@storepickuptesting.com",
          "description": null,
          "address": {
            "street2": "",
            "street1": "14 Dongsuan Huan Beilu",
            "region": "Beijing",
            "post_code": "100026",
            "country": "\u00e4\u00b8\u00ad\u013a\u203a\u02dd",
            "city": "BEIJING"
          }
        }
      }
    },
    {
      "execute": {
        "method_code": "storepickup",
        "manual_payment_label": "Pay on Collection",
        "extension_attributes": [
          {
            "value": "1",
            "code": "pickup_store"
          }
        ],
        "display_name": "Pickup In Store",
        "carrier_code": "storepickup"
      },
      "details": {
        "store_details": {
          "telephone": "01234456789",
          "pickup_store": 1,
          "opening_times": [

          ],
          "name": "China Qingdao Shi Store",
          "location": {
            "longitude": 36.3867744,
            "latitude": 117.6466473
          },
          "email": "chinar@storepickuptesting.com",
          "description": null,
          "address": {
            "street2": "",
            "street1": "63 RENMIN LU",
            "region": "Shandong",
            "post_code": "266033",
            "country": "\u00e4\u00b8\u00ad\u013a\u203a\u02dd",
            "city": "QINGDAO SHI"
          }
        }
      }
    }
  ],
  "supported_payment_methods": [
    {
      "type": "union_pay",
      "data": {
        "supported_types": [

        ],
        "supported_shipping": [
          "storepickup",
          "flatrate"
        ],
        "supported_networks": [

        ],
        "requirements": {
          "rezolve_phone": true
        }
      }
    }
  ],
  "options": {

  },
  "id": "aa665b54-7166-4a53-a275-9b4f693770dc"
}

To the right is a sample of data returned by PaymentOptionsManager.getProductOptions. Look at the supported_shipping_methods node at the top of the file. If the list contains at least one child with execute.method_code: storepickup it means user can buy the product with Click and Collect. Every store where collecting the purchase is available will be listed as a separate item under supported_shipping_methods.

At this point the user should select a payment method. They are listed under the supported_payment_methods node. In some cases, like the example shown, there is one payment method available. In others, there might be more. For example, if SupportedPaymentMethod has type: cash, cash payment may only be allowed with Click and Collect and not in Delivery. To verify that we need SupportedPaymentMethod to be provided as an argument when creating a DeliveryUnit.

3. Creating the Delivery Unit/Delivery Method

// Standard shipping example
let deliveryMethod = CheckoutShippingMethod(type: "flatrate", addressId: address.id)

// Click and Collect example
let deliveryMethod = CheckoutShippingMethod(type: "storepickup", pickupStore: store.pickupStore)

DeliveryMethod has no need of an id for the object creation, so an empty String used to feel the addressId present in the constructor.

See the extension_attributes node for this store id.

"extension_attributes": [ { "value": "2", "code": "pickup_store" } ]

Create the DeliveryMethod as shown at right.

DeliveryMethod is then passed to create the CheckoutBundleV2, which is used to get totals before purchasing the product.

Background Listening

Background Listening enables a mode where your app listens for watermarked audio, and rather than displaying the linked item immediately, instead stores any items it detects (products or categories) in a list. When background listening is stopped, the list is presented to the consumer as a series of Touch Triggers for review and action. As the name "background listening" implies, listening can occur in the background, such as when the consumer is using another app, or when the phone is asleep. Background listening will pause if interrupted by a phone call, and then resume afterwards. Background listening requires that sound be detected through the phone's mic. Watermarked audio playing in another app while headphones are plugged in would not be detected.

The implementation for Android and IOS are somewhat different, so see individual sequence diagrams below.

IOS Background Listening


[ View full size ]

1. Start Background Listening, Listen, and Display Results

// Initialize `ScanManager` based on your RezolveSDK Session

guard let scanManager = rezolveSession?.getScanManager() else {
    return
}

scanManager.autoDetectManagerDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
try? scanManager.startAudioScan()

extension ViewController: AutoDetectManagerDelegate {

    func onAutoDetectStopListening(resolved: [AutoDetectResult]) {
        for item in resolved {
            print(item.description())
        }
    }

    func onAutoDetectError(error: String) {
        // Handle error gracefully
    }
}

Use AutoDetectManagerDelegate (IOS) or AutoDetectManager (Android) to start listening for watermarked audio. When the user stops Background Listening, the results detected (if any) will be returned as an array. Iterate over this array to display detected items as Touch Triggers.

Note: if you want to enable "back" navigation to the item list after the consumer views a list item, you should persist the list in your code. The SDK only returns the list when AutoDetect is stopped; it does not persist the list for you.

2. Display item Consumer selects using Trigger Manager

// Sample URL and `TriggerManager` initialization

let url = URL(string: "http://rzlv.co/1/2/3/8?ad=20&placement=25")

rezolveSession?.triggerManager.resolve(
    url: url!,
    productDelegate: self,
    onRezolveTriggerStart: {},
    onRezolveTriggerEnd: {},
    errorCallback: { error in }
)

extension ViewController: ProductDelegate {

    func onStartRecognizeImage() {
        // Suggestion: Show an interstitial loader
    }

    func onFinishRecognizeImage() {
        // Suggestion: Hide an interstitial loader
    }

    func onCategoryResult(merchantId: String, category: RezolveCategory) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
        // See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
    }

    func onProductResult(product: Product) {
        // See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
    }

    func onError(error: String) {
        // Handle error gracefully
    }
}

Use TriggerManager to display a selected Touch Trigger item. You will have to pass a valid product url into TriggerManager, in the form:

http://rzlv.co/[partnerId]/[merchantId]/[categoryId]/[productId]?ad=[adId]&placement=[placementId]

Get partnerId, merchantId, categoryId, productId, adId and placmentId from the returned list array for that item. If no adId and/or placementId are available, omit the ?ad=[adId]&placement=[placementId] part of the url (note that in this case, no stats will be gathered when this item is displayed or purchased).

Act

NOTE: This section only applies to Acts created in the Rezolve Cloud Engine. For Acts created in the Self Serve Portal, please see SSP Act Targets.

An Act is a type of consumer questionnaire that can be created in the Rezolve Commerce Engine. Acts are used to solicit information and collect consumer answers and contact information. No payment is required to submit an Act.

There is no specific data structure within the SDK that represents an Act; data-wise, it is a particular configuration of product that has no weight and no price. If your application receives a product with no weight and no price, you should display it as an Act. You do this by omitting certain information when displaying the item. You must do this in four places:

Product Detail View

Remove price, quantity, payment details, delivery details, order summary, and the add-to-cart icon. Change the "Instant Buy" text to "Act Now".


[ View full size ]

Post-Purchase Screen

Omit quantity, price, payment details, delivery details, order summary, and pick-up-location map.


[ View full size ]

My Activity List View

Omit the Order Total currency amounts from this screen. Change the order status text to "submitted".


[ View full size ]

My Activity Detail View

Change "Payment Successful" header to "Submitted". Remove "order" text from "Your order ref:". Omit quantity, price, payment details, delivery details, order summary, pick-up-location map, and QR code area.


[ View full size ]

Error Handling

{
  "errors":[
    {
      "type":"<error_type>",
      "code":"<error_code>",
      "message":"<error_message>"
    }
  ]
}

All SDK methods use a uniform error format, as shown at the right. Possible values are:

Type Error Code Message
Authentication 1 Access is denied.
Registration 2 Registration failed.
Bad Request 3 Missing required parameters in request body. Required: ((required_parameters))
Not Found 4 Requested resource not found
Bad Request 5 Incorrect or malformed parameters
Bad Request 6 ((system message))
Service Unavailable 7 Our apologies for the temporary inconvenience. The requested URL generated 503 Service Unavailable error due to overloading or maintenance of the server.

Module Reference Docs

Module reference docs are generated from commented source code. IOS docs are generated using Jazzy (https://github.com/realm/jazzy).

The following link will open in a new window. Close the window to return to this documentation.

IOS Module Documentation

Changelog

All notable changes to the project will be documented in this log.

May 4 2021

April 13, 2021 - 2.0.11.2 release

April 02, 2021 - 2.0.7.1 release

Mar 15, 2021

Mar 10, 2021

Feb 11, 2021

Android 3.1.0 - Jan 27, 2021

Android 3.0.0 - Sept 2, 2020

Android 3.0.0 - July 1, 2020

IOS 2.0.4.1 - May 15, 2020

IOS 1.11.31 - January 28, 2020

IOS 1.11.30 - January 23, 2020

Android 2.3.1, IOS 1.11.28 - November 21, 2019

Cumulative release of several minor version updates.

Android Changes

IOS Changes

Android 2.2.0 - October 10, 2019

This release is Android only.

Android Changes

IOS 1.11.26 - September 24, 2019

This release is IOS only.

IOS Changes

Android v2.1.0, IOS v1.11.25 - June 26, 2019

Documentation Changes

IOS Changes

Android Changes

Android v2.0.1, IOS v1.11.20 - Nov 21, 2018

Documentation Changes

IOS Changes

Android Changes

Android v2.0.0, IOS v1.11.10 - October 11, 2018

Major Documentation Changes

IOS

Added

Changed

Deleted

Android

Added

Changed

Android v1.7.5, IOS v1.7.6.1 - July 9, 2018

Note: This update only changes the IOS version.

Changed

Android v1.7.5, IOS v1.6.5 - May 5, 2018

Added

Changed

Deleted

Android v1.5.37, IOS v1.5.40 - Dec 19 2017

Added

Changed

Deleted