NAV Navbar
Logo
Java Kotlin

Android SDK Introduction

Note:

Code samples for Java and Kotlin will appear in this column.

You can switch between them 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 Android developers. It is expected that you have built apps before, and know your way around an IDE.

Term Definitions

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

For more information

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

Getting Started

Supported OS Versions

OS Minimum Version
Android 4.2.2 and up

Implement a Crash Reporting Sytem

To provide quality support, we require quality information.

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

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

Get the Sample App project

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

Just fill in your API details and compile. See the README.md for details.

rezolve_sdk_sampleapp_android-20210315.zip

Download the SDK & Get an API Key

Request API Key

Latest release versions:

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

Set up the SDK

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.1.1"
    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" />

The Rezovle SDK requires a variety of permissions, depending which features you use.

SDK Feature Use

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

Managers vs Interface


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

Each FeatureManager has an equivalent FeatureInterface. Using the Interface will save you development time.

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.

JWT Authentication

Topics

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

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

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

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

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

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

Terminology

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

JWT Flow


[ View full size ]

Create the Registration JWT

Requirements

You must possess:

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

JWT Header

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

JWT Payload

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

Signature

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

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

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

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.
eyJyZXpvbHZlX2VudGl0eV9pZCI6IjpOT05FOiIsInBhcnRuZXJfZW50aXR5X2lkIjoiMTIzIiwiZXhwIjoxNTIwODY5NDcwfQ.
5y2e6QpUKcqTNLTv75nO6a6iFPVxrF8YeAH5NTg2ZO9dkub31GEs0N46Hu2IJf1bQ_vC2IOO9Z2N7drmvA_AKg

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

Register a new Rezolve User

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

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

Example, using Sandbox endpoint:

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

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

Logging in a User

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

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

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

JWT Header

Note the addition of the "auth" line.

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

JWT Payload

Note the addition of the device_id.

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

Signature

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

Sign the header and payload with the partner_auth_key.

Generating the device_id

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

String API_KEY = "your_api_key";
String ENVIRONMENT = "https://core.sbx.eu.rezolve.com/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

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

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.

SSP Engagement Detection

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

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

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

1. Prepare gradle

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

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

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

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

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

4. Create a scan view.

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() {
        // ...
    }
    // ...                                          
}

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

5. Register listeners for scan results:

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

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.

SSP Location Triggers

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

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

Topics

Setup location detection

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

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

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

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

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

SSP Act Targets

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:

Get an SSP Act By Id

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.

SSP Product Targets

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

SSP Category Targets

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

SSP URL Targets

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.

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

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


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

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

@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

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


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

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

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

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

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

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

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

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

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

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

External Payment

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

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

Create the checkoutBundle


CheckoutBundleV2.createProductCheckoutBundleV2(
    merchantId, 
    paymentOption.getId(),  
    checkoutProduct, 
    phoneId, 
    supportedPaymentMethod, 
    deliveryUnit
    );

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

If external payment is supported, the PaymentOptions returned from PaymentOptionManager.getProductOptions(...) or getCartOptions(...) will have an option for "external" payment. Selecting this one indicates to the system that payments will be processed externally.

Checkout and Buy


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();

            // set PaymentRequest to null
            PaymentRequest paymentRequest = null;

            // 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();
                }
            });

        }
    });

// ...

// set PaymentRequest as null
PaymentRequest paymentRequest = null;


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


When calling Checkout, set the PaymentRequest to null. Then pass the CheckoutBundleV2 into checkout as usual, getting your order id and final price.

Finally, call the buy endpoint, again setting PaymentRequest as null.

Buy Response

{
    "phone_number": "111111111",
    "payment_method": "external",
    "partner_name": "Rezolve Shared Partner",
    "partner_id": "56",
    "order_id": "1501",
    "name": "RCE 3",
    "merchant_id": "79",
    "email": "cgouldthorpe+rce3@gmail.com",
    "data": {
        "callback": {
            "payment_id": "ff04ba41-32f1-4f70-a461-3f63fe9e81b9"
        }
    }
}
@Override
public void onProductOptionBuySuccess(OrderSummary orderSummary) {
    JSONObject orderData = orderSummary.getData();
    String paymentId = orderData.optJSONObject("callback").optString("payment_id")
}

A sample JSON response is shown. You must extract the payment_id from the JSON for use after the payment transaction is completed.

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

Handle Payment Externally

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

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

Update the Order using Callback

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

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

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

NOTE: It is strongly recommended you make this callback from your server! Mobile callbacks are unreliable and missing one may result in wrong order status in Rezolve systems.

The endpoints urls for the EU environment are:

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

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

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

Callback Response

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

Responds codes:

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

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.

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

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

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

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

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

See the extension_attributes node for this store id.

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

Create the DeliveryUnit as shown at right.

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


[ View full size ]

1. Start Background Listening, Listen, and Display Results

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 AutoDetectManager 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. AutoDetect 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

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

Act

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

An Act is a type of consumer questionnaire that can be created in the Rezolve Cloud 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. Android docs are generated using Doxygen (http://www.doxygen.nl/).

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

Android Module Documentation

Changelog

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

May 4 2021

March 12, 2021 - Update to 3.1.1

Mar 15, 2021

Mar 10, 2021

Feb 11, 2021

Android 3.1.0 - Jan 27, 2021

Android 3.0.0 - Sept 2, 2020

Android 3.0.0 - July 1, 2020

IOS 2.0.4.1 - May 15, 2020

IOS 1.11.31 - January 28, 2020

IOS 1.11.30 - January 23, 2020

Android 2.3.1, IOS 1.11.28 - November 21, 2019

Cumulative release of several minor version updates.

Android Changes

IOS Changes

Android 2.2.0 - October 10, 2019

This release is Android only.

Android Changes

IOS 1.11.26 - September 24, 2019

This release is IOS only.

IOS Changes

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

Documentation Changes

IOS Changes

Android Changes

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

Documentation Changes

IOS Changes

Android Changes

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

Major Documentation Changes

IOS

Added

Changed

Deleted

Android

Added

Changed

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

Note: This update only changes the IOS version.

Changed

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

Added

Changed

Deleted

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

Added

Changed

Deleted