NAV Navbar
Logo
IOS Android/Java Android/Kotlin

Introduction

Note:

Code samples for IOS and Android will appear in this column.

You can switch between IOS and Android by clicking the tabs above.

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 and Android developer. 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.

For more information

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

Getting Started

Supported OS Versions

OS Minimum Version
IOS IOS 9 and up
SWIFT 4 (recommended) or 3
Android 4.2.2 and up

Implement a Crash Reporting Sytem

To provide quality support, we require quality information.

Rezolve advises using Fabric 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.

Download the Rezolve InsideTM 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

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

platform :ios, '10.0'

use_modular_headers!

target 'Sample' do
  pod 'RezolveSDK'
end

 

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

Set up the SDK - Android

The target IDE for Android instructions is Android Studio. 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.

The Android SDK is distributed via the Rezolve Nexus/Maven repository.

Update root gradle.properties

// in /gradle.properties
REZOLVE_SDK_REPOSITORY_URL=https://nexus.rezo.lv/repository/maven-sdk-releases/
REZOLVE_SDK_REPOSITORY_USERNAME=PUT_YOUR_USERNAME_HERE
REZOLVE_SDK_REPOSITORY_PASSWORD=PUT_YOUR_PASSWORD_HERE

Update the gradle.properties file in the root folder of your project and set variables for the repository url, username and repository password. Your Nexus username and password will be assigned to you when you receive your API Key.

Update root build.gradle to add repository

// in /build.gradle
allprojects {
    repositories {
        mavenLocal()
        maven {
            url REZOLVE_SDK_REPOSITORY_URL
            credentials {
                username REZOLVE_SDK_REPOSITORY_USERNAME
                password REZOLVE_SDK_REPOSITORY_PASSWORD
            }
        }
        google()
        jcenter()
    }
}

Add the Rezolve repository to your root build.gradle file, in the allprojects block.

Update :app module build.gradle to add sdk dependency

// in /app/build.gradle
dependencies {
    def rezolveSdkVersion = "3.0.0"
    def rezolveSdkScanVersion = "3.0.0"
    // ...
    implementation "com.rezolve.sdk:core-android:$rezolveSdkVersion"
    implementation "com.rezolve.sdk:payment-android:$rezolveSdkVersion"
    implementation "com.rezolve.sdk:ssp-android:$rezolveSdkVersion"
    implementation "com.rezolve.sdk:ssp-google-geofence:$rezolveSdkVersion"
    implementation "com.rezolve.sdk:resolver:$rezolveSdkVersion"
    implementation "com.rezolve.sdk:scan-android:$rezolveSdkScanVersion"
    // ...
}

Add Rezolve SDK dependency to build.gradle file in your :app module. Set the rezolveSdkVersion and rezolveSdkScanVersion to the version you wish to use. Note it is important to use double quotes as shown around the implementation strings, or $rezolveSdkVersion won't be evaluated. To upgrade to a new release, update rezolveSdkVersion and rezolveSdkScanVersion to the latest version.

Permissions

// Note: the SDK's manifest requests the following permissions:
// On Android 6.0+, you will have to specifically request the last two. 
// See https://developer.android.com/training/permissions/requesting.html

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

// RECORD_AUDIO permission is only needed if you implement the Audio Scan feature.
<uses-permission android:name="android.permission.RECORD_AUDIO" />

// CAMERA permission is only needed if you implement the Image Scan feature.
<uses-permission android:name="android.permission.CAMERA" />

// FOREGROUND_SERVICE permission is only needed if you implement the Background Listening feature.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

// Location permissions are only needed if you implement the Geofence detection feature.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />


SDK Feature Use

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

Android-specific instructions on Managers


//  THE FOLLOWING TWO METHODS ARE EQUIVALENT, but Interface saves effort

//
// Using MANAGER, you must handle the response in a WalletCallback:
//

public class MyActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        rezolveSDK = new RezolveSDK.Builder()
           .setApiKey(API_KEY)
           .setEnv(ENVIRONMENT)
           .setAuthRequestProvider(new PartnerAuthRequestProvider(AuthService.getInstance()))
           .build();

            rezolveSDK.setAuthToken(accessToken);

            rezolveSDK.createSession( accessToken, entityId, partnerId, new RezolveInterface() {


            @Override
            public void onInitializationSuccess(RezolveSession rezolveSession, String 
            entityId, String partnerId) {
                rezolveSession.getWalletManager().getAll(new WalletCallback() {

                    @Override
                    public void onWalletGetAllSuccess(List<PaymentCard> list) {

                    // handle getAll response here
                        for(PaymentCard paymentCard : list) {
                            String cardId = paymentCard.getId();
                            String expiresOn = paymentCard.getExpiresOn();
                            // ...etc
                        }
                    }
                });
            }
        });
    }
}

//
// Using INTERFACE, you can save some development time
//

public class MyActivity extends AppCompatActivity implements WalletInterface {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // .getRezolveSession gets an already created session
        RezolveSDK.getInstance().getRezolveSession()
        .getWalletManager().getAll(this);
    }

    @Override
    public void onWalletGetAllSuccess(List<PaymentCard> list) {
        // handle getAll response here
        for(PaymentCard paymentCard : list) {
            String cardId = paymentCard.getId();
            String expiresOn = paymentCard.getExpiresOn();
            // ...etc
        }
    }
}

In the Android version of the Rezolve InsideTM SDK, each FeatureManager has an equivalent FeatureInterface.

For example, to use WalletManager methods, you can either do so directly, or implement WalletInterface in your current activity for convenience. See Android code at right for comparison.

The Module Reference section exclusively documents using Feature Interfaces, as this is the preferred method.

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.

Minimum Implementation

Topics

As a developer, you may be thinking "There is a lot of documentation here. What is the minimum feature set I need to implement to make a technical demo?"

It depends on what you want to demonstrate, but the typical demonstration of Rezolve's offering looks like:

Scan Engagement   >>>   View Product   >>>   Instant Buy

This section walks you through the minimum implementation required to build a limited, functional, end-to-end demo. Several shortcuts are identified. These shortcuts are not appropriate for an app processing real transactions, but are accessible for a technical demonstration on the Sandbox server. The shortcuts guide you to create a demo with limited UI elements, and only a single user. You are of course free to implement as many of the features as you wish to demonstrate.

1. Authentication

Two things must be in place before the SDK can communicate with the server

  1. You must have an API Key and Partner Id, and be must talking to the correct Environment. This information was provided to you when you signed up for API Access.
  2. You must have a valid JSON Web Token, or JWT. A JWT Secret was provided when you signed up for API Access, and is used to create the JWT.

Examples of both authentication measures are shown under JWT Authentication .

Be aware this is UNSAFE for production, or even pilot use. For any rollout involving real consumers and real purchases, you must do the auth server integration, so JWT tokens are tied properly to login status for a single user, and tokens can be renewed and expired.

2. Profile Building Blocks

The end consumer is represented by a Profile. Attached to this profile are one or more addresses, one or more phone numbers, and one or more payment devices. To create these objects, you must implement:

During the scan and purchase flow, ids for profile, address, phone, and payment device will be needed. You will get these by querying the above managers to get to get the ids of the records you created.

See Consumer Profile Management.

3. The Scan Interface

You must implement ScanManager, and all it's methods.

You will also need ProductManager, to get and parse the retrieved product.

See the example at Product Scan, Instant Buy Flow.

4. Checkout and Payment

Checkout and payment rely upon the following managers.

You must at least stub all the methods of these managers. But you may notice there are some methods specific to cart, and some specific to product. For instant buy, you only need to fully implement the PRODUCT methods.

See the example at Product Scan, Instant Buy Flow.

Conclusion

The above steps outline the absolute shortest route to a fully functional tech demonstration. Please follow them, and embrace the shortcuts provided, to create your tech demo in the minimal time.

But be aware that the Minimum Implementation is suitable for a Technical Demo only. Any application that will be given to consumers and used to make real purchases must fully implement JWT Auth, a proper Consumer UI, and included features omitted from this section, like Mall, Shopping Cart, Multi-Cart, and more.

The Rezolve Reference App is a great example of a full-featured implementation. If haven't used it yet, request access and give it a try.

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://sandbox-api-tw.rzlvtest.co/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
// generate the random id
String deviceId = UUID.randomUUID().toString();

// store the device_id
private static void writeDeviceIdFile(File deviceidfile) throws IOException {
    FileOutputStream out = new FileOutputStream(deviceidfile);
    String id = UUID.randomUUID().toString();
    out.write(id.getBytes());
    out.close();
}

// read the stored device_id
private static String readDeviceIdFile(File deviceidfile) throws IOException {
    RandomAccessFile f = new RandomAccessFile(deviceidfile, "r");
    byte[] bytes = new byte[(int) f.length()];
    f.readFully(bytes);
    f.close();
    return new String(bytes);
}

// supply the random id to the SDK
RezolveSDK.setDeviceIdHeader(deviceId);

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
        }
    }
}
String API_KEY = "your_api_key";
String ENVIRONMENT = "https://sandbox-api-tw.rzlvtest.co/api";
String accessToken = "abc123.abc123.abc123";  // JWT token from auth server
String entityId = "123";    // from auth server
String partnerId = "123";   // from auth server
String deviceId = "wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a"; // from stored device_id, see "Generating the device_id" above

// Use builder to create instance of SDK and set SDK Params
// Pass in an AuthRequestProvider here, to handle expiring JWT tokens
rezolveSDK = new RezolveSDK.Builder()
               .setApiKey(API_KEY)
               .setEnv(ENVIRONMENT)
               .setAuthRequestProvider(new PartnerAuthRequestProvider(AuthService.getInstance()))
               .build();

// Set JWT Auth Token from partner auth server
rezolveSDK.setAuthToken(accessToken);

// Start session, again supplying JWT auth token
rezolveSDK.createSession( accessToken, entityId, partnerId, new RezolveInterface() {

    @Override
    public void onInitializationSuccess(RezolveSession rezolveSession, String entityId, String partnerId) {
        // set device_id so it can be passed in x-header
        RezolveSDK.setDeviceIdHeader(deviceId);

        // use created session to access managers.  Example...
        rezolveSession.getAddressbookManager().get(...);
    }

    @Override
    public void onInitializationFailure() {
        // handle error
    }
});

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)
                        }
                    }
                }
        })
    }
}
// example Partner Auth Request Provider 
// this would handle partner user login against partner server, password reset,
// as well as JWT token renewal
class PartnerAuthRequestProvider implements RezolveSDK.AuthRequestProvider {
    private final AuthService authService;
    RuaAuthRequestProvider(AuthService authService) {
        this. authService = authService;
    }
    @Override
    public RezolveSDK.GetAuthRequest getAuthRequest() {
        if(Looper.myLooper() == Looper.getMainLooper()) {
            throw new IllegalStateException("You can't run this method from main thread");
        }

        //set blocking call as the refresh token callback
        final RefreshTokenCallbackToBlockingCall callback = new RefreshTokenCallbackToBlockingCall();
        authService.refreshAuthToken(callback);

        // ping the partner auth service
        RezolveSDK.GetAuthRequest authRequest = PartnerPingCallbackToBlockingCall.getResult();
        return authRequest;
    }
}

class RefreshTokenCallbackToBlockingCall {
    private RezolveSDK.GetAuthRequest result = null;
    private final CountDownLatch countDownLatch = new CountDownLatch(1);

    // on successful refresh, wait for the ping response 
    public void onRefreshAuthTokenSuccess(@NonNull String authToken) {
        result = RezolveSDK.GetAuthRequest.authorizationHeader(authToken);
        countDownLatch.countDown();
    }

    // getResult is only triggered after a result is received from the partner auth server
    RezolveSDK.GetAuthRequest getResult() {
        try {
            countDownLatch.await();
            return result;
        } catch (InterruptedException e) {
            // handle the exception
        }
    }
}

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.

IOS

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.

Android

The SDK makes every call to the Rezolve server using an http client; if a call to the server results in a "401 token expired" response, the http client will ask for a new token using RezolveSDK.AuthRequestProvider. The Partner Auth Service you passed in to the SDK Builder must handle this JWT renewal.

It should be noted that the Partner Auth Service will typically handle all partner auth needs. Duties may include processesing username/passwords for login, handling registering your users, and handling password resets, in addition to JWT renewal.

The code examples show one way of implementing JWT renewal.

In the class PartnerAuthRequestProvider the Partner Auth Service implements RezolveSDK.AuthRequestProvider, to handle the JWT renewal requirements of the SDK. If the http client receives a "401 token expired", it will call RezolveSDK.GetAuthRequest to either confirm logout or renew the token. The token is renewed, but is only returned if the ping to the partner auth server to check login status suceededs. If the partner auth server says the user is not logged in, the renewed token is not returned, and the user can no longer make requests. If the user is still logged in, the updated JWT is returned.

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.

Detect SSP Engagements - Android

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. Prepare gradle

Implement the SSP module by adding it to your gradle dependencies.

implementation "com.rezolve.sdk:ssp-android:3.1.0"

2. Initialize SspActManager

var authParams: AuthParams = AuthParams(
        AUTH0_CLIENT_ID,
        AUTH0_CLIENT_SECRET,
        AUTH0_API_KEY,
        AUTH0_AUDIENCE,
        AUTH0_ENDPOINT,
        SSP_ENGAGEMENT_ENDPOINT,
        SSP_ACT_ENDPOINT
)

var httpConfig: HttpClientConfig = Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()

var httpClientFactory: HttpClientFactory = Builder()
        .setHttpClientConfig(httpConfig)
        .setAuthParams(authParams)
        .build()

var sspHttpClient: SspHttpClient = httpClientFactory.createHttpClient(SSP_ENDPOINT)

var sspActManager = SspActManager(sspHttpClient)

3. Set up the resolver configuration

The resolver is what decodes the information in the Trigger and looks up the Target

ResolverConfiguration.Builder(rezolveSDK)
    .enableBarcode1dResolver(true)
    .enableCoreResolver(true)
    .enableSspResolver(sspActManager, desiredImageWidth)
    .build(this);

4. Create a scan view.

Note: an example of Scan View can be seen in the Rezolve Sample App code, ScanActivity.java

public class ScanActivity extends AppCompatActivity {

    private static final String TAG = ScanActivity.class.getSimpleName();
    private VideoScanManager videoScanManager = VideoScanManagerProvider.getVideoScanManager();
    private AudioScanManager audioScanManager = AudioScanManagerProvider.getAudioScanManager();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
    }

    @Override
    protected void onDestroy() {
        // ...
    }

    @Override
    protected void onResume() {
        // ...
    }

    @Override
    protected void onPause() {
        // ...
    }
    // ...                                          
}

5. Register listeners for scan results:

When the phone detects trigger media, the listener will pick up the result and deduce what the target is. You must provide handling for all target types. Handling targets is covered in the next section.

ResolverResultListenersRegistry.getInstance().add(resolveResultListener)

private val resolveResultListener: ResolveResultListener = object : ResolveResultListener {
    override fun onProcessingStarted(uuid: UUID) {
        // ...
    }
    override fun onProcessingFinished(uuid: UUID) {
        // ...
    }
    override fun onProcessingUrlTriggerStarted(uuid: UUID, urlTrigger: UrlTrigger) {
            solveRezolveTrigger(urlTrigger.url)
    }
    override fun onContentResult(uuid: UUID, result: ContentResult) {
        if (result is ProductResult) {
            onProductResult(result.product, result.categoryId)      // display product (old method)
        } else if (result is CategoryResult) { 
            onCategoryResult(result.category, result.merchantId)    // display category (old method)
        } else if (result is SspActResult) {
            onSspActResult(result)                                  // display SSP Act or info page - check below
        } else if (result is SspProductResultSspProductResult) {
            solveRezolveTrigger(result.rezolveTrigger)              // display product (new SSP method)  
        } else if (result is SspCategoryResult) {
            solveRezolveTrigger(result.rezolveTrigger)              // display category (new SSP method)  
        }
    }
    override fun onResolverError(uuid: UUID, error: ResolverError) {
        onScanError(error.rezolveError.errorType, error.message)
    }
}

private fun solveRezolveTrigger(url: String) {
    triggerManager.resolveTrigger(url, object : TriggerCallback() {
        override fun onCategoryResult(category: Category, merchantId: String) {
            onCategoryResult(category, merchantId)                      // display category (old method)
        }
        override fun onBadTrigger() {
            // ...
        }
        override fun onProductResult(product: Product, categoryId: String) {
            onProductResult(product, categoryId)                        // display product (old method)
        }
        override fun onScanError(rezolveError: RezolveError, scannedData: ScannedData?) {
            onScanError(rezolveError.errorType, rezolveError.message)
        }
    })
}

Handling SSP Act Targets - Android

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.

Before displaying sspActResult, check the value of the field sspActResult.sspAct.pageBuildingBlocks. If it is not null and is not empty, it means that engagement owner has designed custom layout to present it. Here is how to handle it.

1. Create helper data class that holds both block and answer (if applicable):

data class BlockWrapper(
        val pageBuildingBlock: PageBuildingBlock,
        var answerToDisplay: String? = null
)

2. Create an adapter class to show the custom layout.

class SspActBlockAdapter : ListAdapter<BlockWrapper, SspActBlockAdapter.ViewHolder>(BlockWrapperItemDiff) {

    private lateinit var layoutInflater: LayoutInflater

    lateinit var eventListener: SspActBlockEventListener

    private val blockSelectListener = object : SspBlockSelectClickListener {
        override fun onOptionSelected(blockWrapper: BlockWrapper, option: SelectionOption) {
            eventListener.onSelectBlockOptionSelected(blockWrapper, option)
        }
    }

    private val blockDateListener = object : SspBlockDateFieldListener {
        override fun onDateBlockClick(blockWrapper: BlockWrapper) {
            eventListener.onDateBlockSelected(blockWrapper)
        }
    }

    private val blockTextInputListener = object : SspBlockTextInputListner {
        override fun onTextInputChanged(blockWrapper: BlockWrapper, text: String) {
            eventListener.onTextInputBlockChange(blockWrapper, text)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        if(!::layoutInflater.isInitialized) {
            layoutInflater = LayoutInflater.from(parent.context)
        }

        return when(viewType) {
            VIEW_TYPE_HEADER -> HeaderViewHolder(ItemSspBlockHeaderBinding.inflate(layoutInflater, parent, false))
            VIEW_TYPE_PARAGRAPH -> ParagraphViewHolder(ItemSspBlockParagraphBinding.inflate(layoutInflater, parent, false))
            VIEW_TYPE_DIVIDER -> DividerViewHolder(ItemSspBlockDividerBinding.inflate(layoutInflater, parent, false))
            VIEW_TYPE_IMAGE -> ImageViewHolder(ItemSspBlockImageBinding.inflate(layoutInflater, parent, false))
            VIEW_TYPE_VIDEO -> VideoViewHolder(ItemSspBlockVideoBinding.inflate(layoutInflater, parent, false))
            VIEW_TYPE_DATE_FIELD -> DateFieldViewHolder(ItemSspBlockDateFieldBinding.inflate(layoutInflater, parent, false), blockDateListener)
            VIEW_TYPE_SELECT -> SelectViewHolder(ItemSspBlockSelectBinding.inflate(layoutInflater, parent, false), blockSelectListener)
            VIEW_TYPE_TEXT_INPUT -> TextFieldViewHolder(ItemSspBlockTextInputBinding.inflate(layoutInflater, parent, false), blockTextInputListener)
            else -> throw IllegalArgumentException("Invalid viewType: $viewType")
        }
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        viewHolder.bind(getItem(position), position)
    }

    override fun getItemViewType(position: Int): Int {
        return when(getItem(position).pageBuildingBlock.type) {
            Type.HEADER -> VIEW_TYPE_HEADER
            Type.PARAGRAPH -> VIEW_TYPE_PARAGRAPH
            Type.DIVIDER -> VIEW_TYPE_DIVIDER
            Type.IMAGE -> VIEW_TYPE_IMAGE
            Type.VIDEO -> VIEW_TYPE_VIDEO
            Type.DATE_FIELD -> VIEW_TYPE_DATE_FIELD
            Type.SELECT -> VIEW_TYPE_SELECT
            Type.TEXT_FIELD, Type.UNKNOWN -> VIEW_TYPE_TEXT_INPUT
        }
    }

    abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        protected val context: Context = itemView.context

        abstract fun bind(item: BlockWrapper, position: Int)
    }

    class HeaderViewHolder(private val binding : ItemSspBlockHeaderBinding) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) {
            binding.blockWrapper = item
        }
    }

    class ParagraphViewHolder(private val binding : ItemSspBlockParagraphBinding) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) {
            binding.blockWrapper = item
        }
    }

    class DividerViewHolder(private val binding : ItemSspBlockDividerBinding) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) { }
    }

    class ImageViewHolder(private val binding : ItemSspBlockImageBinding) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) {
            binding.blockWrapper = item
        }
    }

    class VideoViewHolder(private val binding : ItemSspBlockVideoBinding) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) {
            binding.blockWrapper = item
        }
    }

    class DateFieldViewHolder(private val binding : ItemSspBlockDateFieldBinding, private val listener: SspBlockDateFieldListener) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) {
            binding.blockWrapper = item
            binding.listener = listener
        }
    }

    class SelectViewHolder(private val binding : ItemSspBlockSelectBinding, private val listener: SspBlockSelectClickListener) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) {
            binding.blockWrapper = item
            binding.listener = listener
        }
    }

    class TextFieldViewHolder(private val binding : ItemSspBlockTextInputBinding, private val listener: SspBlockTextInputListner) : ViewHolder(binding.root) {
        override fun bind(item: BlockWrapper, position: Int) {
            binding.blockWrapper = item
            binding.listener = listener
        }
    }

    interface SspBlockDateFieldListener {
        fun onDateBlockClick(blockWrapper: BlockWrapper)
    }

    interface SspBlockSelectClickListener {
        fun onOptionSelected(blockWrapper: BlockWrapper, option: SelectionOption)
    }

    interface SspBlockTextInputListner {
        fun onTextInputChanged(blockWrapper: BlockWrapper, text: String)
    }
}

object BlockWrapperItemDiff : DiffUtil.ItemCallback<BlockWrapper>() {
    override fun areItemsTheSame(p0: BlockWrapper, p1: BlockWrapper): Boolean = p0.pageBuildingBlock.id == p1.pageBuildingBlock.id
    override fun areContentsTheSame(p0: BlockWrapper, p1: BlockWrapper): Boolean = p0 == p1
}

3. List blocks

private lateinit var blockAdapter: SspActBlockAdapter
private var pageBuildingRecycler: RecyclerView? = null

pageBuildingRecycler = binding.pageBuildingRecycler
binding.pageBuildingRecycler.adapter = blockAdapter

pageBuildingRecycler?.setItemViewCacheSize(pageBuildingBlocks.size)
blockAdapter.submitList(pageBuildingBlocks)

In your fragment or activity class submit list of blocks to the adapter:

4. Create layouts to present different types of blocks:

// Date field
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="blockWrapper"
            type="com.rezolve.demo.content.sspact.BlockWrapper" />

        <variable
            name="listener"
            type="com.rezolve.demo.content.sspact.SspActBlockAdapter.SspBlockDateFieldListener" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="@{() -> listener.onDateBlockClick(blockWrapper)}"
        android:layout_margin="@dimen/ssp_block_paragraph_padding">
        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/ssp_block_date_button"
            style="@style/SspBlockBaseStyle.Title"
            android:text="@{blockWrapper.pageBuildingBlock.data.text.concat(blockWrapper.pageBuildingBlock.required ? @string/ssp_block_required_suffix : @string/empty_string)}"
            android:textColor="@{blockWrapper.pageBuildingBlock.required &amp;&amp; blockWrapper.answerToDisplay == null ? @color/ssp_block_field_required : @color/black}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            tools:text="When would you like to visit us?"/>
        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/ssp_block_date_answer_display"
            style="@style/SspBlockBaseStyle.Answer"
            android:text="@{blockWrapper.answerToDisplay == null ? @string/ssp_block_select : blockWrapper.answerToDisplay}"
            app:layout_constraintTop_toBottomOf="@id/ssp_block_date_button"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            tools:text="12-04-2020"/>
        <androidx.appcompat.widget.AppCompatImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="@id/ssp_block_date_answer_display"
            app:layout_constraintBottom_toBottomOf="@id/ssp_block_date_answer_display"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="@dimen/ssp_block_paragraph_padding_small"
            android:src="@drawable/ic_arrow_drop_down_black_24dp"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>


// Divider 
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <View
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:layout_margin="@dimen/ssp_block_paragraph_padding"
        android:background="@color/ssp_block_light_grey"/>
</layout>


// Header
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="blockWrapper"
            type="com.rezolve.demo.content.sspact.BlockWrapper" />
    </data>
    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="@dimen/ssp_block_header_text_size"
        android:paddingTop="10dp"
        android:paddingBottom="10dp"
        android:paddingStart="@dimen/ssp_block_paragraph_padding"
        android:paddingEnd="@dimen/ssp_block_paragraph_padding"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="4dp"
        blockText="@{blockWrapper.pageBuildingBlock}"/>
</layout>


// Image 
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="blockWrapper"
            type="com.rezolve.demo.content.sspact.BlockWrapper" />
    </data>
    <androidx.appcompat.widget.AppCompatImageView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@{blockWrapper.pageBuildingBlock.data.text}"
        android:adjustViewBounds="true"
        android:layout_marginBottom="3dp"
        imageUrl="@{blockWrapper.pageBuildingBlock.data.url}"/>
</layout>


// Paragraph
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="blockWrapper"
            type="com.rezolve.demo.content.sspact.BlockWrapper" />
    </data>
    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="@dimen/ssp_block_standard_text_size"
        android:padding="@dimen/ssp_block_paragraph_padding"
        blockText="@{blockWrapper.pageBuildingBlock}"/>
</layout>


// Select box
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="blockWrapper"
            type="com.rezolve.demo.content.sspact.BlockWrapper" />
        <variable
            name="listener"
            type="com.rezolve.demo.content.sspact.SspActBlockAdapter.SspBlockSelectClickListener" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/ssp_block_paragraph_padding">
        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/ssp_block_select_label"
            style="@style/SspBlockBaseStyle.Title"
            android:text="@{blockWrapper.pageBuildingBlock.data.text.concat(blockWrapper.pageBuildingBlock.required ? @string/ssp_block_required_suffix : @string/empty_string)}"
            android:textColor="@{blockWrapper.pageBuildingBlock.required &amp;&amp; blockWrapper.answerToDisplay == null ? @color/ssp_block_field_required : @color/black}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            tools:text="Pick a color"/>
        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/ssp_block_select_button"
            style="@style/SspBlockBaseStyle.Answer"
            android:text="@{blockWrapper.answerToDisplay == null ? @string/ssp_block_select : blockWrapper.answerToDisplay}"
            app:layout_constraintTop_toBottomOf="@id/ssp_block_select_label"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:onClick="@{(v) -> sspBlockSelectLayout.toggle()}"
            tools:text="Select"/>
        <androidx.appcompat.widget.AppCompatImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="@id/ssp_block_select_button"
            app:layout_constraintBottom_toBottomOf="@id/ssp_block_select_button"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="@dimen/ssp_block_paragraph_padding_small"
            android:src="@drawable/ic_arrow_drop_down_black_24dp"/>
        <net.cachapa.expandablelayout.ExpandableLayout
            android:id="@+id/ssp_block_select_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:el_duration="300"
            app:el_expanded="false"
            app:el_parallax="1"
            app:layout_constraintTop_toBottomOf="@id/ssp_block_select_button">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                selectItems="@{blockWrapper}"
                selectListener="@{listener}"/>
        </net.cachapa.expandablelayout.ExpandableLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>



// Text input
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <import type="android.text.TextUtils"/>

        <variable
            name="blockWrapper"
            type="com.rezolve.demo.content.sspact.BlockWrapper" />

        <variable
            name="listener"
            type="com.rezolve.demo.content.sspact.SspActBlockAdapter.SspBlockTextInputListner" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/ssp_block_paragraph_padding">

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/ssp_block_text_input_label"
            style="@style/SspBlockBaseStyle.Title"
            android:text="@{blockWrapper.pageBuildingBlock.data.text.concat(blockWrapper.pageBuildingBlock.required ? @string/ssp_block_required_suffix : @string/empty_string)}"
            android:textColor="@{blockWrapper.pageBuildingBlock.required &amp;&amp; TextUtils.isEmpty(blockWrapper.answerToDisplay) ? @color/ssp_block_field_required : @color/black}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            tools:text="Special requests"/>

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/ssp_block_text_input_edittext"
            style="@style/SspBlockBaseStyle.Answer"
            label="@{sspBlockTextInputLabel}"
            block="@{blockWrapper}"
            textListener="@{listener}"
            app:layout_constraintTop_toBottomOf="@id/ssp_block_text_input_label"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>


// Video
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="blockWrapper"
            type="com.rezolve.demo.content.sspact.BlockWrapper" />
    </data>
    <WebView
        android:layout_width="match_parent"
        android:layout_height="240dp"
        videoUrl="@{blockWrapper.pageBuildingBlock.data.url}"/>
</layout>

Create layouts to present different types of blocks. An example of each type is provided to the right.

5. Create helper extension methods

@BindingAdapter("blockText")
fun setBlockText(view: AppCompatTextView, value: PageBuildingBlock) {
    view.apply {
        text = value.data.text

        value.style?.let {
            setTextColor(Color.parseColor(it.color))
            setBackgroundColor(Color.parseColor(it.backgroundColor))
            val typeFace = when {
                it.fontWeight == FontWeight.BOLD && it.fontStyle == FontStyle.ITALIC -> Typeface.BOLD_ITALIC
                it.fontWeight == FontWeight.BOLD -> Typeface.BOLD
                it.fontStyle == FontStyle.ITALIC -> Typeface.ITALIC
                else -> Typeface.NORMAL
            }
            setTypeface(null, typeFace)
            gravity = when(it.textAlign) {
                TextAlign.CENTER -> Gravity.CENTER
                TextAlign.RIGHT -> Gravity.RIGHT
                else -> Gravity.LEFT
            }
        }
    }
}

@BindingAdapter("block", "textListener", "label")
fun setTextListener(view: AppCompatEditText, blockWrapper: BlockWrapper, listener: SspActBlockAdapter.SspBlockTextInputListner, labelView: AppCompatTextView) {
    view.addTextChangedListener(object : TextWatcher {
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
        override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {
            listener.onTextInputChanged(blockWrapper, text.toString())
            labelView.setTextColor(ContextCompat.getColor(view.context, if (blockWrapper.pageBuildingBlock.isRequired && text.isNullOrEmpty()) R.color.ssp_block_field_required else R.color.black))
        }
        override fun afterTextChanged(p0: Editable?) { }
    })
}

@BindingAdapter("imageUrl")
fun setImageUrl(view: AppCompatImageView, url: String?) {
    if (!url.isNullOrEmpty()) {
        Picasso.get().load(url).into(view)
    }
}

@SuppressLint("SetJavaScriptEnabled")
@BindingAdapter("videoUrl")
fun setVideoUrl(view: WebView, url: String) {
    view.settings.apply {
        javaScriptEnabled = true
        loadWithOverviewMode = true
        useWideViewPort = true
    }
    view.loadUrl(url)
}

@BindingAdapter("selectItems", "selectListener")
fun setSelectItems(view: LinearLayout, blockWrapper: BlockWrapper, listener: SspActBlockAdapter.SspBlockSelectClickListener) {
    view.removeAllViews()
    blockWrapper.pageBuildingBlock.data.selectionOptions?.forEach {
        val textView = AppCompatTextView(view.context).apply {
            layoutParams = LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT)
            setPadding(20, 12, 0, 12)
            text = it.description
            setTextColor(ContextCompat.getColor(view.context, R.color.almost_black))
            setOnClickListener { _ ->
                (view.parent as ExpandableLayout).collapse()
                listener.onOptionSelected(blockWrapper, it)
            }
        }
        view.addView(textView)
    }
}

6. Submitting Act answers

SspAct has fields called questions:

private List<SspActQuestion> questions;

If it's not null or empty it means that the merchant would like to collect some informations from users. If SspActQuestion.isRequired() field is true it means that answer submission won't be possible without it.

There are three types of SspActQuestion, and fields answers must pass validation otherwise IllegalArgumentException will be thrown.:

You can create an answer for each of these classes using their answer(@NonNull String userAnswer) method.

6a. Build the Act submission

private fun assembleSspActSubmission(): SspActSubmission {
    return SspActSubmission.Builder()
        .setUserId(userId)
        .setUserName(userName)
        .setPhone(userPhone)
        .setPersonTitle(userTitle)
        .setFirstName(userFirstName)
        .setLastName(userLastName)
        .setEmail(userEmail)
        .setServiceId(sspAct.serviceId)
        .setAnswers(listOfSspActAnswers)
        .setLocation(rezolveLocation)
        .build()
}

When you have var listOfSspActAnswers: List<SspActAnswer> you can build SspActSubmission class using the builder tool:

6b. Submit the Act answers

sspActManager.submitAnswer(
    sspAct.id,
    assembleSspActSubmission(),
    object : SspSubmitActDataInterface {
        override fun onSubmitActDataSuccess(actSubmissionResponse: SspActSubmissionResponse) {
            // handle response
        }
        override fun onError(error: RezolveError) {
            // handle error
        }
    }
)

Next, you can submit your answer:

Look Up An Act By Id - Android

SspActManager.getResolveResponseFromEngagementId(int imageWidth,
       @NonNull String engagementId,
       @NonNull final SspFromEngagementInterface sspFromEngagementInterface)

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

Handling SSP Product Targets - Android

((SspProductResult)result).getSspProduct().getRezolveTrigger());

When a trigger is resolved (see Step 5 of How To Detect SSP Engagements - Android), one of the possible result ContentResult types is SspProductResult. To retrieve the product, extract a RezolveTrigger using the sample code shown. You can then use TriggerManager to retrieve the product. See TriggerManager.

Handling SSP Category Targets - Android

((SspCategoryResult)result).getSspCategory().getRezolveTrigger());

When a trigger is resolved (see Step 5 of How To Detect SSP Engagements - Android), one of the possible result ContentResult types is SspCategoryResult. To retrieve the category, extract a RezolveTrigger using the sample code shown. You can then use TriggerManager to retrieve the category. See TriggerManager.

Handling SSP URL Targets - Android

if (TriggerManager.isRezolveTrigger(url)) {
   // handle url trigger
} else {
   // handle custom url
}

The last type of target you need to handle is a custom URL. Custom URLs can be associated with Products or Categories. To detect a custom URL, add a simple if/else clause that checks if the URL you receive is a RezolveTrigger. If not, then it is a custom URL, and you should handle it as you see fit. The typical behavior is to redirect the user to the URL, but the use case is up to you.

Detect SSP Engagements - IOS

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.

Handling Act Targets - IOS

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.

Look Up An Act By Id - IOS

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.

Handling SSP Product Targets - IOS

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.

Handling SSP Category Targets - IOS

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/4, 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.

Handling SSP URL Targets - IOS

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
    }
}
public class TriggerMgr extends AppCompatActivity implements TriggerInterface {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TriggerManager triggerManager = RezolveSDK.getInstance().getRezolveSession().getTriggerManager();

        String triggerUrl = "http://rzlv.co/2/3/13/169?ad=20&placement=25";

        triggerManager.resolveTrigger( triggerUrl, this );

        // reserved for future functionality:
        // triggerManager.getPersistedTriggers();
        // triggerManager.listenUrl();
    }


    @Override
    public void onProductResult(Product product) {
// get product info
        String product_id = product.getId();
        String title = product.getTitle();
        String subtitle = product.getSubtitle();
        String description = product.getDescription();
        List<String> images = product.getImages();
        List<String[]> thumbs = product.getImageThumbs();
        String merchant_id = product.getMerchantId();
        float price = product.getPrice();
        List<Option> options = product.getOptions();
        List<Variant> optionsAvail = product.getOptionsAvailable();
        List<CustomOption> customOptions = product.getCustomOptions();


        // iterate to get values of options list
        for (Option option : options) {
            String optionLabel = option.getLabel();
            String extraInfo = option.getExtraInfo();
            String optionCode = option.getCode();
            List<OptionValue> optionValues = option.getValues();

            for(OptionValue optionValue: optionValues){
                String value = optionValue.getValue();
                String label = optionValue.getLabel();
            }
        }

        // iterate to get values of variant list
        for (Variant variant : optionsAvail) {
            List<Combination> combinations = variant.getCombinations();
            // iterate to get values of composition hashmap
            for (Combination combination : combinations ){
                String comboValue = combination.getValue();
                String comboCode = combination.getCode();
            }
        }

        //iterate to get values of CustomOptions list
        for (CustomOption customOption : customOptions){
            List<CustomOptionValue> customOptionValues = customOption.getValues();
            int customOptionId = customOption.getOptionId();
            int customOptionSortOrder = customOption.getSortOrder();
            String customOptiontitle = customOption.getTitle();
            Boolean customOptionIsRequired = customOption.isRequired();

            // iterate to get values of customOptionValues
            for( CustomOptionValue customOptionValue : customOptionValues ){
                String customOptionValueTitle = customOptionValue.getTitle();
                int customOptionValuesSortOrder = customOptionValue.getSortOrder();
                String customOptionValueId = customOptionValue.getValueId();
            }
        }
    }

    @Override
    public void onCategoryResult(Category category, String s) {
        String category_id = category.getId();
        String parentId = category.getParentId();
        String name = category.getName();
        Boolean hasCategories = category.hasCategories();
        Boolean hasProduct = category.hasProducts();
        String image = category.getImage();
        List<String> imageThumbs = category.getImageThumbs();
        String catParentId = category.getParentId();
        List<Category> children = category.getCategories();
        PageResult<DisplayProduct> pageResult = category.getProductPageResult();

        // get products from pageResult...
        Integer count = pageResult.getCount();
        Integer total = pageResult.getTotal();
        Link[] links = pageResult.getLinks();
        List<DisplayProduct> displayProducts = pageResult.getItems();

        for (Link link: links){
            Integer linkcount = link.getCount();
            Integer page = link.getPage();
            String sort = link.getSort();
            String sortBy = link.getSortBy();
        }

        for (DisplayProduct displayProduct : displayProducts){
            String DPid = displayProduct.getId();
            List<String> DPimageThumbs = displayProduct.getImageThumbs();
            String DPimage = displayProduct.getImage();
            float DPprice = displayProduct.getPrice();
            String DPname = displayProduct.getName();
            String DPcategoryId = displayProduct.getCategoryId();
        }
    }

    @Override
    public void onBadTrigger() {
        // called when provided url has bad syntax or is too short
        // handle error
    }

    @Override
    public void onError(HttpResponse httpResponse) {
        // called if httpError occurs
        // handle error... available methods are:
        // httpResponse.getResponseJson();
        // httpResponse.getErrorList();
        // httpResponse.getStatusCode();
        // httpResponse.getResponseJson();
    }
}

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
    }
}

public class ScanActivity extends AppCompatActivity implements ScanManagerInterface, View.OnClickListener {


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // set scan view
        setContentView(R.layout.scan_activity);
        rezolveScanView = (RezolveScanView)findViewById(R.id.scan_view);

        //get scan manager
        boolean barcodeenabled = true;
        boolean videoenabled = true;
        scanManager = RezolveSDK.getInstance().getRezolveSession().getScanManager(this, barcodeenabled, videoenabled );                                     

        //start video scan to acquire image
        scanManager.startVideoScan(this, rezolveScanView);
    }


    // capture product result
    @Override
    public void onProductResult(Product product) {

        // get product info
        String productId = product.getId();
        String title = product.getTitle();
        String subtitle = product.getSubtitle();

        // ...etc
    }
}

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
    }
})
public class PaymentOptionsMgr extends AppCompatActivity implements PaymentOptionInterface {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        PaymentOptionManager pom = RezolveSDK.getInstance().getRezolveSession().getPaymentOptionManager();

        String merchantId = "12345";
        CheckoutProduct checkoutProduct = new CheckoutProduct();

        // get PaymentOptions for a product
        pom.getProductOptions(checkoutProduct, merchantId, this);

    }


    @Override
    public void onProductOptionsSuccess(PaymentOption paymentOption) {
        String id = paymentOption.getId();
        List<CheckoutProduct> checkoutProducts = paymentOption.getCheckoutProducts();
        JSONObject paymentOptionOptions = paymentOption.getOptions();
        List<Shipping> supportedDeliveryMethods =  paymentOption.getSupportedDeliveryMethods();
        List<SupportedPaymentMethod> supportedPaymentMethods = paymentOption.getSupportedPaymentMethods();
        String type = paymentOption.getType();

        for (CheckoutProduct checkoutProduct : checkoutProducts) {
            // breakdown checkoutProduct object
            float qty = checkoutProduct.getQty();
            Placement productPlacement = checkoutProduct.getProductPlacement();
            int cpId  = checkoutProduct.getId();
            List<ConfigurableOption> configurableOptions = checkoutProduct.getConfigurableOptions();

            String placementId = productPlacement.getPlacementId();
            String adId = productPlacement.getAdId();

            for (ConfigurableOption configurableOption : configurableOptions) {
                int value = configurableOption.getValue();
                String code = configurableOption.getCode();
            }
        }

        for (Shipping shipping : supportedDeliveryMethods) {
            ShippingDetails shippingDetails = shipping.getShippingDetails();
            ShippingMethod shippingMethod = shipping.getShippingMethod();
        }

        for (SupportedPaymentMethod supportedPaymentMethod : supportedPaymentMethods ) {
            PaymentMethodData paymentMethodData = supportedPaymentMethod.getPaymentMethodData();
            String spmType = supportedPaymentMethod.getType();
            JSONObject originalDataJson = supportedPaymentMethod.getOriginalDataJson();

            JSONObject requirements = paymentMethodData.getRequirements();
            List<String> supportedDelivery = paymentMethodData.getSupportedDelivery();
            List<String> supportedNetworks = paymentMethodData.getSupportedNetworks();
            List<String> supportedTypes = paymentMethodData.getSupportedTypes();
        }
    }
}

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
}

rezolveSession.getWalletManager().getAll(new WalletCallback() {

    @Override
    public void onWalletGetAllSuccess(List<PaymentCard> list) {
        // handle getAll response here
    }

});

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
    }
})
@Override
public void onProductResult(Product product) {
    // get product info
    String product_id = product.getId();
    String title = product.getTitle();
    String subtitle = product.getSubtitle();
    // ... etc


    // create a CheckoutProduct object with the product id , and set the quantity
    CheckoutProduct checkoutProduct = new CheckoutProduct();
    checkoutProduct.setId(Integer.parseInt(product_id));
    checkoutProduct.setQty(1);

   // use info from productPaymentOption to populate Shipping and PaymentMethod choices...
    String productPaymentOptionId = productPaymentOption.getId();
    List<SupportedPaymentMethod> supportedPaymentMethods = productPaymentOption.getSupportedPaymentMethods();
    List<Shipping> deliveryMethods = productPaymentOption.getSupportedDeliveryMethods();
    SupportedPaymentMethod supportedPaymentMethod = supportedPaymentMethods.get(0);  // in reality customer chooses an option here
    Shipping deliveryMethod = deliveryMethods.get(0); // in reality customer chooses an option here
    String phonebookId = "123"; // use real consumer phonebook id

    // create the delivery unit
    DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, address.getId()); 

    // create a product checkout bundle           
    CheckoutBundleV2.createProductCheckoutBundleV2(merchantId, paymentOption.getId(), checkoutProduct, phoneId, supportedPaymentMethod, deliveryUnit);

    // call the CheckoutProductOption method to get an order object and totals
    checkout.checkoutProductOption(productCheckoutBundleV2, new CheckoutV2Callback() {
        public void onCheckoutProductSuccess(Order order) {
            // get pricing breakdown and final price
            List<PriceBreakdown> pricingBreakdown = order.getBreakdowns();
            float finalPrice = order.getFinalPrice();
            String orderId = order.getOrderId();

            // create a paymentRequest object, and then use this with the checkoutProduct object to purchase the item.
            PaymentCard paymentCard = new PaymentCard(); // use consumer's chosen payment card
            String cvv = "123"; // use actual cvv
            PaymentRequest paymentRequest = checkout.createPaymentRequest(paymentCard,cvv);

            // buy a single product
            checkout.buyProduct(paymentRequest, productCheckoutBundleV2, orderId, new CheckoutV2Callback() {
                @Override
                public void onProductOptionBuySuccess(OrderSummary orderSummary) {
                    super.onProductOptionBuySuccess(orderSummary);
                    //display order summary
                    String orderId = orderSummary.getOrderId();
                    JSONObject orderData = orderSummary.getData();
                }
            });

        }
    });
}

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
    }
})
// create a paymentRequest object, and then use this with the checkoutBundle object 

// create paymentRequest object
PaymentCard paymentCard = new PaymentCard(); // use consumer's chosen payment card from step 3
String cvv = "123"; // use actual cvv
PaymentRequest paymentRequest = checkout.createPaymentRequest(paymentCard,cvv);


// buy a single product
checkoutManagerV2.buyProduct(paymentRequest, productCheckoutBundleV2, orderId, this);

@Override
public void onProductOptionBuySuccess(OrderSummary orderSummary) {
    // skip this call, ContactInformation entity is not public
    // Merchant.ContactInformation contactInformation = orderSummary.getContactInformation();
    JSONObject orderData = orderSummary.getData();
    String orderId = orderSummary.getOrderId();
    String partnerId = orderSummary.getPartnerId();
    String partnerName = orderSummary.getPartnerName();
}

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
    }
}

public class ScanActivity extends AppCompatActivity implements ScanManagerInterface, View.OnClickListener {


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // set scan view
        setContentView(R.layout.scan_activity);
        rezolveScanView = (RezolveScanView)findViewById(R.id.scan_view);

        //get scan manager
        scanManager = RezolveSDK.getInstance().getRezolveSession().getScanManager(this, true);

          //start video scan to acquire image
          scanManager.startVideoScan(this, rezolveScanView);
    }


    // capture product result
    @Override
    public void onProductResult(Product product) {

        // get product info
        String productId = product.getId();
        String title = product.getTitle();
        String subtitle = product.getSubtitle();

        // ...etc
    }
}

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
    }
})
// add product to cart
checkout.addProductToCart(this, checkoutProduct, merchantId, new CheckoutCallback() {
    @Override
    public void onAddProductsToCartSuccess(CartDetails cartDetails) {
        super.onAddProductsToCartSuccess(cartDetails);

        String cartId = cartDetails.getId();
        String merchantId = "123"; //use actual merchant id
        String addressId = "123";  //use actual address id

        // immediately call CheckoutCart to get an Order with totals
        checkout.addProductToCart( checkoutProduct, merchantId, new CheckoutCallback() {
            @Override
            public void onAddProductsToCartSuccess(CartDetails cartDetails) {
                super.onAddProductsToCartSuccess(cartDetails);

                String cartId = cartDetails.getId();
                String merchantId = "123"; //use actual merchant id
                String addressId = "123";  //use actual address id
            }
        });
    }
});

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
    }
})
public class PaymentOptionsMgr extends AppCompatActivity implements PaymentOptionInterface {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        PaymentOptionManager pom = RezolveSDK.getInstance().getRezolveSession().getPaymentOptionManager();

        String merchantId = "12345";
        String cartId = "12345";

        // get PaymentOptions for cart
        pom.getCartOptions( merchantId, cartId, this);
    }

    @Override
    public void onCartOptionsSuccess(List<PaymentOption> list) {
        for (PaymentOption paymentOption : list ){
            String type = paymentOption.getType();
            List<SupportedPaymentMethod> supportedPaymentMethods = paymentOption.getSupportedPaymentMethods();
            List<Shipping> supportedDeliveryMethods = paymentOption.getSupportedDeliveryMethods();
            JSONObject options = paymentOption.getOptions();
            String id = paymentOption.getId();
            List<CheckoutProduct> checkoutProducts = paymentOption.getCheckoutProducts();

            for (SupportedPaymentMethod supportedPaymentMethod : supportedPaymentMethods ) {
                PaymentMethodData paymentMethodData = supportedPaymentMethod.getPaymentMethodData();
                String spmType = supportedPaymentMethod.getType();
                JSONObject originalDataJson = supportedPaymentMethod.getOriginalDataJson();

                JSONObject requirements = paymentMethodData.getRequirements();
                List<String> supportedDelivery = paymentMethodData.getSupportedDelivery();
                List<String> supportedNetworks = paymentMethodData.getSupportedNetworks();
                List<String> supportedTypes = paymentMethodData.getSupportedTypes();
            }

            for (Shipping shipping : supportedDeliveryMethods) {
                ShippingDetails shippingDetails = shipping.getShippingDetails();
                ShippingMethod shippingMethod = shipping.getShippingMethod();
            }

            for (CheckoutProduct checkoutProduct : checkoutProducts) {
                // breakdown checkoutProduct object
                float qty = checkoutProduct.getQty();
                Placement productPlacement = checkoutProduct.getProductPlacement();
                int cpId  = checkoutProduct.getId();
                List<ConfigurableOption> configurableOptions = checkoutProduct.getConfigurableOptions();

                String placementId = productPlacement.getPlacementId();
                String adId = productPlacement.getAdId();

                for (ConfigurableOption configurableOption : configurableOptions) {
                    int value = configurableOption.getValue();
                    String code = configurableOption.getCode();
                }
            }
        }
    }

    @Override
    public void onFailure(HttpResponse httpResponse) {
        // handle error
    }
}


// Assuming the user picks traditional shipping. 
// Click and Collect options are discussed in a separate tutorial.
// Create DeliveryUnit as follows:  


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
}
rezolveSession.getWalletManager().getAll(new WalletCallback() {

    @Override
    public void onWalletGetAllSuccess(List<PaymentCard> list) {
        // handle getAll response here
    }
});

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
String phonebookId = "123"; // use real consumer phonebook id
String optionId = "123"; // get this from productPaymentOption.getId();
String cartId = "123";
SupportedPaymentMethod supportedPaymentMethod = new SupportedPaymentMethod(); //get this from productPaymentOption.getSupportedPaymentMethods()

// create the delivery unit
DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, address.getId()); 
CheckoutBundleV2 checkoutBundleV2 = CheckoutBundleV2.createCartCheckoutBundleV2( merchantId, optionId, cartId, phonebookId, supportedPaymentMethod, deliveryUnit);

// call CheckoutCartOption method to get an order object and totals
checkout.checkoutCartOption(cartCheckoutBundleV2, new CheckoutV2Callback() {
    @Override
    public void onCartOptionCheckoutSuccess(Order order) {
        super.onCartOptionCheckoutSuccess(order);
        // get order id
        String orderId = order.getOrderId();
        // get price breakdowns
        List<PriceBreakdown> priceBreakdowns = order.getBreakdowns();
    }
});

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
    }
})
    // create a paymentRequest object, and then use this with the checkoutProduct object to purchase the cart.
    PaymentCard paymentCard = new PaymentCard(); // use consumer's chosen payment card
    String cvv = "123"; // use actual cvv
    PaymentRequest paymentRequest = checkout.createPaymentRequest(paymentCard,cvv);

    // buy the cart
    String orderId = "123";  // get from order.getOrderId();
    checkout.buyCart(paymentRequest, cartCheckoutBundleV2, orderId, new CheckoutV2Callback() {
        @Override
        public void onCartOptionBuySuccess(OrderSummary orderSummary) {
            super.onCartOptionBuySuccess(orderSummary);
            //display order summary
            String orderId = orderSummary.getOrderId();
            JSONObject orderData = orderSummary.getData();
        }
    });

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
    }
}
public class ScanActivity extends AppCompatActivity implements ScanManagerInterface, View.OnClickListener {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // set scan view
        setContentView(R.layout.scan_activity);
        rezolveScanView = (RezolveScanView)findViewById(R.id.scan_view);

        //get scan manager
        scanManager = RezolveSDK.getInstance().getRezolveSession().getScanManager(this, true);

          //start video scan to acquire image
          scanManager.startVideoScan(this, rezolveScanView);
    }


    // if scan media contains a category link, onCategoryResult fires and
    // returns a Category object. Parse the category object to obtain 
    // a list of products in that category, and a list of subcategories of that category
    @Override
    public void onCategoryResult(Category category, String s) {
        String category_id = category.getId();
        String parentId = category.getParentId();
        String name = category.getName();
        Boolean hasCategories = category.hasCategories();
        Boolean hasProduct = category.hasProducts();
        String image = category.getImage();
        List<String> imageThumbs = category.getImageThumbs();
        String catParentId = category.getParentId();
        List<Category> children = category.getCategories();

        // get category placement
        Placement.CategoryPlacement categoryPlacement = category.getCategoryPlacement();
        String categoryAdId = categoryPlacement.getAdId();
        String categoryPlacementId = categoryPlacement.getPlacementId();

        // optional:  get paginated subcategory results
        PageResult<Category> categoryPageResult = category.getCategoryPageResult();

        // optional: get paginated product results
        PageResult<DisplayProduct> pageResult = category.getProductPageResult();

        // get products from pageResult...
        Integer count = pageResult.getCount();
        Integer total = pageResult.getTotal();
        Link[] links = pageResult.getLinks();
        List<DisplayProduct> displayProducts = pageResult.getItems();

        for (Link link: links){
            Integer linkcount = link.getCount();
            Integer page = link.getPage();
            String sort = link.getSort();
            String sortBy = link.getSortBy();
        }

        for (DisplayProduct displayProduct : displayProducts){
            String DPid = displayProduct.getId();
            List<String> DPimageThumbs = displayProduct.getImageThumbs();
            String DPimage = displayProduct.getImage();
            float DPprice = displayProduct.getPrice();
            String DPname = displayProduct.getName();
            String DPcategoryId = displayProduct.getCategoryId();
        }
    }
}

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
    }
})
public class Merchants extends AppCompatActivity implements MerchantInterface {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MerchantInterface merchantInterface;

        MerchantManager merchantManager = RezolveSDK.getInstance().getRezolveSession().getMerchantManager();

        // get merchants
        merchantManager.getMerchants(this, this);
    }

    @Override
    public void onGetMerchantsSuccess(List<Merchant> list) {
        for(Merchant merchant : list) {
            String merchant_id = merchant.getId();
            String name = merchant.getName();
            String tagline = merchant.getTagline();
            String banner = merchant.getBanner();
            List<String> bannerThumbs = merchant.getBannerThumbs();
            List<String> logoThumbs = merchant.getLogoThumbs();
        }
    }
}

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
    }
})
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ProductInterface productInterface;

    ProductManager myProductManager = RezolveSDK.getInstance().getRezolveSession().getProductManager();

    // get categories
    String merchantId = "123";
    myProductManager.getCategories(merchantId,this);
}

@Override
public void onGetCategoriesSuccess(Category category) {
    // parse Category object 
    String category_id = category.getId();
    String parentId = category.getParentId();
    String name = category.getName();
    Boolean hasCategories = category.hasCategories();
    Boolean hasProduct = category.hasProducts();
    String image = category.getImage();
    List<String> imageThumbs = category.getImageThumbs();
    String catParentId = category.getParentId();
    List<Category> children = category.getCategories();
    PageResult<DisplayProduct> pageResult = category.getProductPageResult();

    // get products from pageResult...
    Integer count = pageResult.getCount();
    Integer total = pageResult.getTotal();
    Link[] links = pageResult.getLinks();
    List<DisplayProduct> displayProducts = pageResult.getItems();

    for (Link link: links){
        Integer linkcount = link.getCount();
        Integer page = link.getPage();
        String sort = link.getSort();
        String sortBy = link.getSortBy();
    }

    for (DisplayProduct displayProduct : displayProducts){
        String DPid = displayProduct.getId();
        List<String> DPimageThumbs = displayProduct.getImageThumbs();
        String DPimage = displayProduct.getImage();
        float DPprice = displayProduct.getPrice();
        String DPname = displayProduct.getName();
        String DPcategoryId = displayProduct.getCategoryId();
    }
}

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
    }
})
public class Products2 extends AppCompatActivity implements ProductInterface {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ProductInterface productInterface;

        ProductManager myProductManager = RezolveSDK.getInstance().getRezolveSession().getProductManager();

        merchantId = "123";
        Category category = new Category();

        // first pageNavigationFilter is for categories
        PageNavigationFilter pageNavigationFilter = new PageNavigationFilter();
        pageNavigationFilter.setItemsPerPage(10);
        pageNavigationFilter.setPageNumber(1);
        pageNavigationFilter.setSortBy("name");   // values: name, price
        pageNavigationFilter.setSortDirection("asc");  // values: asc, desc
        // second pageNavigationFilter is for products
        PageNavigationFilter pageNavigationFilter2 = new PageNavigationFilter();
        pageNavigationFilter2.setItemsPerPage(20);
        pageNavigationFilter2.setPageNumber(1);
        pageNavigationFilter2.setSortBy("price");   // values: name, price
        pageNavigationFilter2.setSortDirection("asc");  // values: asc, desc

        // get products and categories in one call
        myProductManager.getProductsAndCategories(merchantId, category, pageNavigationFilter, pageNavigationFilter2, this);
    }

    @Override
    public void onGetProductsAndCategoriesSuccess(Category category) {
        String category_id = category.getId();
        String parentId = category.getParentId();
        String name = category.getName();
        Boolean hasCategories = category.hasCategories();
        Boolean hasProduct = category.hasProducts();
        String image = category.getImage();
        List<String> imageThumbs = category.getImageThumbs();
        String catParentId = category.getParentId();
        List<Category> children = category.getCategories();
        PageResult<DisplayProduct> pageResult = category.getProductPageResult();

        // get products from pageResult...
        Integer count = pageResult.getCount();
        Integer total = pageResult.getTotal();
        Link[] links = pageResult.getLinks();
        List<DisplayProduct> displayProducts = pageResult.getItems();

        for (Link link: links){
            Integer linkcount = link.getCount();
            Integer page = link.getPage();
            String sort = link.getSort();
            String sortBy = link.getSortBy();
        }

        for (DisplayProduct displayProduct : displayProducts){
            String DPid = displayProduct.getId();
            List<String> DPimageThumbs = displayProduct.getImageThumbs();
            String DPimage = displayProduct.getImage();
            float DPprice = displayProduct.getPrice();
            String DPname = displayProduct.getName();
            String DPcategoryId = displayProduct.getCategoryId();
        }
    }
}

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
    }
})
// get a single product using ProductInterface
public class Products extends AppCompatActivity implements ProductInterface {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ProductManager myProductManager = RezolveSDK.getInstance().getRezolveSession().getProductManager();

        // get single product
        Product product = new Product();
        Category category = new Category();
        String merchantId = "123";

        myProductManager.getProduct(merchantId, category, product, this);
    }

    @Override
    public void onGetProductSuccess(com.rezolve.sdk.model.shop.Product product) {
        // get product info
        String product_id = product.getId();
        String title = product.getTitle();
        String subtitle = product.getSubtitle();
        String description = product.getDescription();
        List<String> images = product.getImages();
        List<String[]> thumbs = product.getImageThumbs();
        String merchant_id = product.getMerchantId();
        float price = product.getPrice();
        List<Option> options = product.getOptions();
        List<Variant> optionsAvail = product.getOptionsAvailable();
        List<CustomOption> customOptions = product.getCustomOptions();


        // iterate to get values of options list
        for (Option option : options) {
            String optionLabel = option.getLabel();
            String extraInfo = option.getExtraInfo();
            String optionCode = option.getCode();
            List<OptionValue> optionValues = option.getValues();

            for(OptionValue optionValue: optionValues){
                String value = optionValue.getValue();
                String label = optionValue.getLabel();
            }
        }

        // iterate to get values of variant list
        for (Variant variant : optionsAvail) {
            List<Combination> combinations = variant.getCombinations();
            // iterate to get values of composition hashmap
            for (Combination combination : combinations ){
                String comboValue = combination.getValue();
                String comboCode = combination.getCode();
            }
        }

        //iterate to get values of CustomOptions list
        for (CustomOption customOption : customOptions){
            List<CustomOptionValue> customOptionValues = customOption.getValues();
            int customOptionId = customOption.getOptionId();
            int customOptionSortOrder = customOption.getSortOrder();
            String customOptiontitle = customOption.getTitle();
            Boolean customOptionIsRequired = customOption.isRequired();

            // iterate to get values of customOptionValues
            for( CustomOptionValue customOptionValue : customOptionValues ){
                String customOptionValueTitle = customOptionValue.getTitle();
                int customOptionValuesSortOrder = customOptionValue.getSortOrder();
                String customOptionValueId = customOptionValue.getValueId();
            }
        }

    }
}

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
    }
})
// set up parameters for merchant search
String query = "";
MerchantSearchOrderBy merchantSearchOrderBy = MerchantSearchOrderBy.LOCATION;
SearchDirection searchDirection = SearchDirection.ASC;
Integer offset = 0;
Integer limit = 50;
RezolveLocation rezolveLocation = new RezolveLocation();
rezolveLocation.setLatitude(40.6726499);
rezolveLocation.setLongitude(-73.9502622);
MerchantSearchData merchantSearchData = new MerchantSearchData(query,merchantSearchOrderBy,searchDirection,offset,limit,rezolveLocation);

// set up merchant search interface and handle results
MerchantSearchInterface merchantSearchInterface = new MerchantSearchInterface() {
    @Override
    public void onSearchMerchantsSuccess(MerchantSearchResult merchantSearchResult) {
        int page = merchantSearchResult.getPage();
        int total = merchantSearchResult.getTotal();
        List<Merchant> merchants = merchantSearchResult.getMerchants();

        //iterate over merchants array
        for (Merchant merchant : merchants) {
            String banner = merchant.getBanner();
            List<String> bannerThumbs = merchant.getBannerThumbs();
            Double distance = merchant.getDistance();
            String id = merchant.getId();
            String infoEmail = merchant.getInfoEmail();
            String infoName = merchant.getInfoName();
            String infoPhone = merchant.getInfoPhone();
            String logo = merchant.getLogo();
            List<String> logoThumgs = merchant.getLogoThumbs();
            String name = merchant.getName();
            String partnerId = merchant.getPartnerId();
            String partnerName = merchant.getPartnerName();
            String priority = merchant.getPriority();
            List<DisplayStore> stores = merchant.getStores();
            String tagline = merchant.getTagline();
            List<TermsAndConditions> termsAndConditionsList = merchant.getTermsAndConditionsList();

            // get list of store locations associated with this merchant
            for(DisplayStore displayStore:stores){
                 int storeId = displayStore.getId();
                 RezolveLocation storeLocation = displayStore.getLocation();
                 String storeName = displayStore.getName();
            }

            // get terms of conditions for merchant
            for(TermsAndConditions termsAndConditions : termsAndConditionsList){
                String termsCheckboxText = termsAndConditions.getCheckboxText();
                String termsContent = termsAndConditions.getContent();
                String termsId = termsAndConditions.getId();
                String termsName = termsAndConditions.getName();
                String termsStoreId = termsAndConditions.getStoreId();
            }
        }
    }

    @Override
    public void onError(@NonNull RezolveError rezolveError) {
        // handle error gracefully
    }
};

// initiate merchant search
merchantManager.searchMerchants(this,merchantSearchData,merchantSearchInterface);

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
    }
})
// set up parameters for product search
String query = "book";
String merchantId2 = "123";
ProductSearchOrderBy productSearchOrderBy = ProductSearchOrderBy.SCORE;
SearchDirection searchDirection = SearchDirection.DESC;
ProductType productType = ProductType.ALL;
Integer offset = 0;
Integer limit = 50;
RezolveLocation rezolveLocation = new RezolveLocation();
rezolveLocation.setLatitude(40.6726499);
rezolveLocation.setLongitude(-73.9502622);
ProductSearchData productSearchData = new ProductSearchData(query,merchantId2,productSearchOrderBy,searchDirection,productType,offset,limit,rezolveLocation);

// set up product search interface and handle results
ProductSearchInterface productSearchInterface = new ProductSearchInterface() {
    @Override
    public void onSearchProductsSuccess(ProductSearchResult productSearchResult) {
        Integer page = productSearchResult.getPage();
        Integer total = productSearchResult.getTotal();
        List<DisplayProduct> displayProducts = productSearchResult.getDisplayProducts();

        // iterate over list of display proucts
        for(DisplayProduct displayProduct:displayProducts){
            String productId = displayProduct.getId();
            String merchantId = displayProduct.getMerchantId();
            String categoryId = displayProduct.getCategoryId();
            String categoryName = displayProduct.getCategoryName();
            String image = displayProduct.getImage();
            List<String> imageThumbs = displayProduct.getImageThumbs();
            float price = displayProduct.getPrice();
            Placement productPlacement = displayProduct.getProductPlacement();
            // get product placement details
            String adId = productPlacement.getAdId();
            String placementId = productPlacement.getPlacementId();
        }
    }

    @Override
    public void onError(@NonNull RezolveError rezolveError) {
        // handle error gracefully
    }
};

// initiate product search
productManager.searchProducts(this,productSearchData, productSearchInterface);

Product search offers the following features:

To search for products you must supply the following parameters:

See code samples to the right.

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
    }
})
paymentOptionManager.getProductOptions(checkoutProduct, merchantId, new PaymentOptionCallback() {
    @Override
    public void onProductOptionsSuccess(PaymentOption paymentOption) {
        setPaymentOption(paymentOption);
    }
    @Override
    public void onError(@NonNull final RezolveError error) {
    }
});

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)
// standard shipping example
DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, address.getId());

// Click and Collect example
DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, Integer.valueOf(supportedDeliveryMethod.getShippingMethod().getExtensionAttributes().get(0).getValue()));

Android calls this object DeliveryUnit, IOS uses DeliveryMethod

DeliveryUnit (Android) is created using the chosen supportedPaymentMethod and the address id of the pick up store.

DeliveryMethod (IOS) 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 DeliveryUnit/DeliveryMethod as shown at right.

DeliveryUnit/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 ]

Android 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
    }
}
package com.rezolve.demo.content;

import java.util.List;
import android.support.v7.app.AppCompatActivity;
import com.rezolve.demo.utilities.Constants;
import com.rezolve.sdk.RezolveSDK;
import com.rezolve.sdk.core.interfaces.AutoDetectInterface;
import com.rezolve.sdk.core.managers.AutoDetectManager;
import com.rezolve.sdk.model.shop.Category;
import com.rezolve.sdk.model.shop.Product;

// To use Rezolve Auto Detect, the main Activity of the app must implement AutoDetectInterface
public class AutoDetectSampleActivity extends AppCompatActivity implements AutoDetectInterface {
    private AutoDetectManager rezolveAutoDetectManager;

    @Override
    public void onPause() {
        // When app goes background the app should start the service
        if (rezolveAutoDetectManager == null) {
            // Request RezolveSDK for a instance of the AutoDetectManager
            rezolveAutoDetectManager = RezolveSDK.getInstance().getRezolveSession().getAutoDetectManager();
        }
        // Start the service passing the caller Activity, an optional notification and the AutoDetectInterface Callback.
        // Here we don't pass the optional notification, so the RezolveSDK will provide a default Notification for the app.
        rezolveAutoDetectManager.startAutoDetectService(this, null, this);
        super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
        // When the app is resumed we must stop the service, on stoping the service will return result to the callback method.
        if (rezolveAutoDetectManager != null) {
            rezolveAutoDetectManager.stopAutoDetectService(this);
        }
    }

    @Override
    public void onAutoDetectResults(List list) {
        // On the callback method the app gets a heterogeneous list that may contain several types of Objects
        int listSize = list.size();
        for (int i = 0; i <= listSize; i++) {
            Object item = list.get(i);
            try {
                // The item may be a Product
                if (item instanceof Product) {
                    Product product = (Product) item;
                    // Or it may be a Pair containing a Category and the merchantId who owns the Category
                } else if (item instanceof android.support.v4.util.Pair) {
                    Category category = (Category) ((android.support.v4.util.Pair) item).first;
                    String merchantId = (String) ((android.support.v4.util.Pair) item).second;

                    String categoryId = category.getId();
                    String categoryName = category.getName();
                    boolean categoryHasProducts = category.hasProducts();
                    boolean categoryHasSubCategories = category.hasCategories();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

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.

1a. Note that Android offers a Notification Helper

package com.rezolve.demo.content;

import java.util.List;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.support.v7.app.AppCompatActivity;
import com.rezolve.demo.R;
import com.rezolve.demo.utilities.Constants;
import com.rezolve.sdk.RezolveSDK;
import com.rezolve.sdk.core.interfaces.AutoDetectInterface;
import com.rezolve.sdk.core.managers.AutoDetectManager;
import com.rezolve.sdk.model.shop.Category;
import com.rezolve.sdk.model.shop.Product;
import com.rezolve.sdk.notifications.NotificationActionReceivers;

// To use Rezolve Auto Detect, the main Activity of the app must implement AutoDetectInterface
public class AutoDetectSampleActivity2 extends AppCompatActivity implements AutoDetectInterface {
    private AutoDetectManager rezolveAutoDetectManager;

    @Override
    public void onPause() {
        // We may create a custom notification to be used with the service
        Notification optionalCustomNotification = createOptionalNotification(this);

        // When app goes background the app should start the service
        if (rezolveAutoDetectManager == null) {
            rezolveAutoDetectManager = RezolveSDK.getInstance().getRezolveSession().getAutoDetectManager();
        }
        // Start the service passing the caller Activity, an optional notification and the AutoDetectInterface Callback.
        // Here we send the custom notifiction we have created
        rezolveAutoDetectManager.startAutoDetectService(this, optionalCustomNotification, this);
        super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
        // When the app is resumed we must stop the service, on stoping the service will return result to the callback method.
        if (rezolveAutoDetectManager != null) {
            rezolveAutoDetectManager.stopAutoDetectService(this);
        }
    }

    @Override
    public void onAutoDetectResults(List list) {
        // On the callback method the app gets a heterogeneous list that may contain several types of Objects
        int listSize = list.size();
        for (int i = 0; i <= listSize; i++) {
            Object item = list.get(i);
            try {
                // The item may be a Product
                if (item instanceof Product) {
                    Product product = (Product) item;
                    // Or it may be a Pair containing a Category and the merchantId who owns the Category
                } else if (item instanceof android.support.v4.util.Pair) {
                    Category category = (Category) ((android.support.v4.util.Pair) item).first;
                    String merchantId = (String) ((android.support.v4.util.Pair) item).second;

                    String categoryId = category.getId();
                    String categoryName = category.getName();
                    boolean categoryHasProducts = category.hasProducts();
                    boolean categoryHasSubCategories = category.hasCategories();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private Notification createOptionalNotification(Context context) {
        // Here we create a regular notification to be presented by the service
        String CHANNEL_ID = "220518";
        int NOTIFICATION_ID = 230518;
        int NOTIFICATION_TURN_OFF_REQUEST_CODE = 230519;
        String ACTION_TURN_OFF = "action.turn.off";

        PendingIntent onNotificationTapIntent = null;
        Notification notification;
        Notification.Builder notificationBuilder;
        NotificationCompat.Builder notificationCompatBuilder;

        Intent turnOffIntent = new Intent();

        turnOffIntent.setAction(ACTION_TURN_OFF);
        turnOffIntent.setClass(this, NotificationActionReceivers.TurnOffBackgroundListenerReceiver.class);
        PendingIntent pendingTurnOffIntent = PendingIntent.getBroadcast(this, NOTIFICATION_TURN_OFF_REQUEST_CODE, turnOffIntent, PendingIntent.FLAG_CANCEL_CURRENT);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationBuilder = new Notification.Builder(this, CHANNEL_ID)
                    .setSmallIcon(R.drawable.default_notification_icon)
                    .setContentTitle(this.getString(R.string.notification_title))
                    .setContentText(this.getString(R.string.notification_text))
                    .setStyle(new Notification.BigTextStyle().bigText(this.getString(R.string.notification_big_text)))
                    .setAutoCancel(true)
                    .addAction(R.drawable.spinner, context.getString(R.string.notification_turn_off), pendingTurnOffIntent);

            if (onNotificationTapIntent != null) {
                notificationBuilder.setContentIntent(onNotificationTapIntent);
            }

            notificationBuilder.setOngoing(true);
            notification = notificationBuilder.build();
        } else {
            notificationCompatBuilder = new NotificationCompat.Builder(context, CHANNEL_ID)
                    .setSmallIcon(R.drawable.ic_slider_head)
                    .setContentTitle(context.getString(R.string.notification_title))
                    .setContentText(context.getString(R.string.notification_text))
                    .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_big_text)))
                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                    .setAutoCancel(true)
                    .addAction(R.drawable.spinner, context.getString(R.string.notification_turn_off), pendingTurnOffIntent);

            if (onNotificationTapIntent != null) {
                notificationCompatBuilder.setContentIntent(onNotificationTapIntent);
            }

            notificationCompatBuilder.setOngoing(true);
            notification = notificationCompatBuilder.build();
        }
        return notification;
    }
}

This helper can be used to display notification to the user that Background Listening is active.

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
    }
}
rezolveSession.getTriggerManager().resolveTrigger("http://rzlv.co/2/3/13/169?ad=20&placement=25", new TriggerInterface() {
    @Override
    public void onProductResult(Product product) {  // display prouduct result
        Log.d("resolveTrigger", "onProductResult: "+product.getTitle());
    }
    @Override
    public void onCategoryResult(Category category, String merchantId) {  // display category result
        Log.d("resolveTrigger", "onCategoryResult: "+category.getName());
    }
    @Override
    public void onBadTrigger() { // called when provided url has bad syntax or is too short
        Log.d("resolveTrigger", "onBadTrigger");
    }
    @Override
    public void onError(HttpResponse httpResponse) {
        Log.d("resolveTrigger", "onError: "+httpResponse.getResponseJson());
    }
});

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

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
// Taken from App.java in the Sample App project  

// Set up a Notification Helper

// Notes:
// Geofence and Location Detection service share the same notification
// See geofenceForegroundNotificationProperties to define the look of the notification
// Foreground notification is required for the foreground service. 
// This prevents the app from getting killed by the system. 

NotificationHelper notificationHelper = new NotificationHelperImpl(this);
Notification notification = notificationHelper.createNotification(
    1,
    getString(R.string.app_name),
    null,
    null,
    null,
    geofenceForegroundNotificationProperties
 );

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.
// Taken from App.java in the Sample App project 

// Create and start Location Provider
// Make sure you have secured the location permission before starting the location provider.
final LocationProviderFused locationProviderFused = LocationProviderFused.create(this, notification);
locationProviderFused.start();

// Next, set up Geofence detection and Resolvers 

// Set up SspActManager 
SspActManager sspActManager = new SspActManager(httpClient);

// Set up Rezolve Configuration Builder (this also supports the image/audio Scanner function)
new ResolverConfiguration.Builder(rezolveSDK)
    .enableBarcode1dResolver(true)
    .enableCoreResolver(true)
    .enableSspResolver(sspActManager, 400)
    .build(this);

// Set up GeofenceMananger
final GeofenceManager geofenceManager = new GeofenceManager.Builder()
    .sspActManager(sspActManager)
    .engagementsUpdatePolicy(new EngagementsUpdatePolicy.Builder().build())
    .notificationChannelPropertiesList(geofenceLocationChannels)
    .engagementAlertNotification(geofenceAlertNotificationProperties)
    .context(this)
    .build();

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
// Taken from App.java in the Sample App project 

// Make sure the location provider is started before detection.
// Based on the provided EngagementsUpdatePolicy  the GeofenceManager will manage geofence updates

final GeofenceManager geofenceManager = new GeofenceManager.Builder()
                .sspActManager(sspActManager)
                .engagementsUpdatePolicy(new EngagementsUpdatePolicy.Builder().build())
                .notificationChannelPropertiesList(geofenceLocationChannels)
                .engagementAlertNotification(geofenceAlertNotificationProperties)
                .context(this)
                .build();
registerGeofenceListener();
locationProviderFused.start();
geofenceManager.startGeofenceTracking();
private void registerGeofenceListener() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ACTION_GEOFENCE_NOTIFICATION_DISPLAYED);
        intentFilter.addAction(ACTION_GEOFENCE_NOTIFICATION_SELECTED);
        registerReceiver(geofenceBroadcastReceiver, intentFilter);
    }
    BroadcastReceiver geofenceBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            final String sender = intent.getStringExtra(KEY_SENDER_PACKAGE_NAME);
            if(!context.getPackageName().equalsIgnoreCase(sender)) {
                Log.d(TAG, "Ignoring intent from: " + sender +", expected: " + context.getPackageName());
                return;
            }
            if(action != null) {
                switch (action) {
                    case ACTION_GEOFENCE_NOTIFICATION_DISPLAYED: {
                        final String name = intent.getStringExtra(KEY_NAME);
                        final String shortDescription = intent.getStringExtra(KEY_DESCRIPTION_SHORT);
                        final String actId = intent.getStringExtra(KEY_ACT_ID);
                        final SspObject sspObject = getSspObjectFromIntent(intent);
                        Log.d(TAG, action + ": " + name + ", " + shortDescription + ", " + actId + ", " + sspObject);
                        break;
                    }
                    case ACTION_GEOFENCE_NOTIFICATION_SELECTED: {
                        final SspObject sspObject = getSspObjectFromIntent(intent);
                        Log.d(TAG, action + ": " + sspObject);
                        break;
                    }
                }
            }
        }
        @Nullable
        private SspObject getSspObjectFromIntent(@NonNull Intent intent) {
            SspObject sspObject = null;
            if(intent.hasExtra(KEY_SSP_ACT)) {
                try {
                    sspObject = SspAct.jsonToEntity(new JSONObject(intent.getStringExtra(KEY_SSP_ACT)));
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            } else if(intent.hasExtra(KEY_SSP_CATEGORY)) {
                try {
                    sspObject = SspCategory.jsonToEntity(new JSONObject(intent.getStringExtra(KEY_SSP_CATEGORY)));
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            } else if(intent.hasExtra(KEY_SSP_PRODUCT)) {
                try {
                    sspObject = SspProduct.jsonToEntity(new JSONObject(intent.getStringExtra(KEY_SSP_PRODUCT)));
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            return sspObject;
        }
    };

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")
    }
}
// Taken from App.java in the Sample App project 

// The SDK sends the following broadcast messages (see code sample in "Detecting Geozones" above for details):

// When the notification is displayed in the notification center, the broadcast action is set to: 
ACTION_GEOFENCE_NOTIFICATION_DISPLAYED:

// And when the user taps the notification, the broadcast action is set to: 
ACTION_GEOFENCE_NOTIFICATION_SELECTED:

// Notes:
// The SDK shows a notification when it detects the geofence zone. 
// The SDK calls the server endpoint to get the object details, and if the object is active
//   it will show the notification and send the ACTION_GEOFENCE_NOTIFICATION_DISPLAYED broadcast.
// Use the geofenceBroadcastReceiver to fetch details about the trigger and SspObject.
// The fetched ssp object has to be in an active state for notification to be shown. 
// The SDK prevents showing duplicate notifications if the same geofence was detected 
//   during the silent period defined in the EngagementsUpdatePolicy.

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

Act

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) and Android docs are generated using Doxygen (http://www.doxygen.nl/).

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

IOS Module Documentation

Android Module Documentation

Changelog

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

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