Smart Engage - Android SDK

Android - Getting Started

Requirements

  • The minimum supported Android SDK version is 21 (Android 5.0 Lollipop).

Open Source SDK Sample app

The most convenient way to get started with SDK integration is to get familiar with our SDK Sample app hosted right here on Github. This is a complete Android Studio project that produces a working application, showcasing the majority of the features described in this documentation.

Remember to fill-in your API details in the gradle.properties file before attempting to compile and run the SDK Sample app.

SDK Setup

The The SDK is distributed through Nexus repository manager. In order to gain access please get in touch with your Smart Engage contact. When you have Nexus url, username and password, you can add them in your top level build.gradle file:

allprojects {
    repositories {
        mavenLocal()
        maven {
            url SDK_REPOSITORY_URL
            credentials {
                username SDK_REPOSITORY_USERNAME
                password SDK_REPOSITORY_PASSWORD
            }
        }
        google()
        jcenter()
    }
}

Now you can declare SDK dependencies in your application module's build.gradle:

dependencies {
    def rezolveSdkVersion = "3.2.1"
    def rezolveSdkScanVersion = "3.2.1"
    // ...

    // core SDK, required for any other functionality
    implementation "com.rezolve.sdk:core-android:$rezolveSdkVersion"

    // required to conduct payments via SDK
    implementation "com.rezolve.sdk:payment-android:$rezolveSdkVersion"

    // required to handle RXP engagements, including Acts and Info Pages
    implementation "com.rezolve.sdk:ssp-android:$rezolveSdkVersion"

    // optional module with ready-to-go implementation of geofence detector based on Google API.
    implementation "com.rezolve.sdk:ssp-google-geofence:$rezolveSdkVersion"

    // required to process image and audio engagements
    implementation "com.rezolve.sdk:resolver:$rezolveSdkVersion"

    // required to provide image and audio scan capabilities
    implementation "com.rezolve.sdk:scan-android:$rezolveSdkScanVersion"

    // optional module that provides image and audio scan capabilities for devices using x86 architecture.
    implementation "com.rezolve.sdk:scan-android-x86:$rezolveSdkScanVersion"
    // ...
}

JWT Authentication

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

Smart Engage 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 Smart Engage server.

After that, whether or not the SDK can talk to Smart Engage 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 Smart Engage for the first time, the partner must generate the Registration JWT, and then POST it to the /api/v1/authentication/register endpoint. The Smart Engage server will validate the JWT, create a new user, create the user's public/private key pair, and return to you the 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 Smart Engage. 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 Smart Engage. Usually a 2-4 digit integer.
partner_api_key The API key you are assigned by Smart Engage. 36 characters including dashes.
partner_auth_key The Auth key you are assigned by Smart Engage. 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 Smart Engage 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 Smart Engage server checks that these values match to deter request origin spoofing. All calls except Registration calls require this.

JWT Flow

JWT Flow Chart ???might want to host the document elsewhere???

Create the Registration JWT

Note: For testing via cURL or Postman without a server-side JWT library, you can use https://jwt.io/#debugger to generate valid JWT tokens.

Use https://www.epochconverter.com/ to generate the token expiration in Epoch time.

Click here for screenshots showing registration using jwt.io and Postman.? ??might want to host elsewhere???

Requirements

You must possess:

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

JWT Header

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

JWT Payload

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.
{
    "rezolve_entity_id": ":NONE:",
    "partner_entity_id": "partner_entity_id",
    "exp": 1520869470
}

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

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:

eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.
eyJyZXpvbHZlX2VudGl0eV9pZCI6IjpOT05FOiIsInBhcnRuZXJfZW50aXR5X2lkIjoiMTIzIiwiZXhwIjoxNTIwODY5NDcwfQ.
5y2e6QpUKcqTNLTv75nO6a6iFPVxrF8YeAH5NTg2ZO9dkub31GEs0N46Hu2IJf1bQ_vC2IOO9Z2N7drmvA_AKg

Register a new Smart Engage User

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

Example, using Sandbox endpoint:

POST: https://core.sbx.eu.rezolve.com/api/v2/authentication/register
-H content-type: application/json
-H x-rezolve-partner-apikey: your-api-key
-H authorization: Bearer signed-jwt
-d {
  "name": "John Doe",
  "last_name": "Doe",
  "first_name": "John",
  "email": "user@domain.com"
}

Responses:

201 OK
{
  "partner_id": "4",
  "entity_id": "d89d-d34fd-fddf45g8xc7-x8c7fddg"
}

400 Bad Request
{
  "errors": [
    {
      "type": "Bad Request",
      "message": "Missing required parameters in request body. Required: [\"field1\", \"field2\"]",
      "code": "6"
    }
  ]
}

422 Unprocessable entity
{
  "errors": [
    {
      "type": "Bad Request",
      "message": "Incorrect or malformed parameters",
      "code": "5"
    }
  ]
}

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

Logging in a User

Once a Smart Engage 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.

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

JWT Payload

Note the addition of the device_id.

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.
{
    "rezolve_entity_id": "entity123",
    "partner_entity_id": "partner_entity_id",
    "exp": 1520869470, 
    "device_id": "wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a"
}

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

Core SDK Integration

Base Smart Engage SDK integration requires core module dependency:

dependencies {
    def rezolveSdkVersion = "3.2.0"
    implementation "com.rezolve.sdk:core-android:$rezolveSdkVersion"
    // ...
}

In the samples to the right, accessToken is the Login JWT token you created in previous 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" section from "JWT Authentication".

// Use builder to create instance of SDK and set SDK Params
// Pass in an AuthRequestProvider here, to handle expiring JWT tokens
RezolveSDK 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

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

Example is provided below. It is NOT an example of implementing SDK code, but rather an example of implementing session renewal with your own authentication server.

The SDK makes every call to the Smart Engage 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 processing username/passwords for login, handling registering your users, and handling password resets, in addition to JWT renewal.

The code example 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 succeeds. 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.

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

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

Customer Profile Management

Once you have established a session, you can access it through RezolveSDK, and from the session you can access set of Managers, responsible for backend communication, for example:

RezolveSdk.peekInstance().getRezolveSession().getAddressbookManager();

Keep in mind that sdk instance and session can be null if they haven't been initiatilized.

If the session was correctly initialized you now have access to the consumer's records. These include:

  • Consumer Profile - Via the ConsumerProfileManager. Name, email, and device profile (phone info) for the consumer
  • Address Book - Via the AddressbookManager. A collection of postal addresses, to be used for ship-to and bill-to purposes.
  • Phone Book - Via the PhonebookManager. A collection of phone numbers associated with the profile.
  • Favourites - Via the FavouriteManager. Reserved for future functionality, "Favourites" are collections of unique devices that can be topped up. A favourite can represent a mobile phone, a tollway transponder, or other device/account.
  • Wallet - Via the WalletManager. Wallet lets you store credit card info securely, and lets the consumer maintain the list of cards. There can be multiple cards.

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.

CustomerProfileManager supports only update and get.

CustomerProfile customerProfile = new CustomerProfile();
customerProfile.setEmail("user@example.com");
customerProfile.setTitle("Mr.");
customerProfile.setFirstName("John");
customerProfile.setLastName("Doe");

String currentLocale = Locale.getDefault().getLanguage() + "_" + Locale.getDefault().getCountry();
DeviceProfile device = new DeviceProfile(deviceId, Build.MANUFACTURER, currentLocale);
List deviceProfiles = new ArrayList<>();
deviceProfiles.add(deviceProfile);

customerProfile.setDevices(deviceProfiles);
customerProfile.setDateCreated(String.valueOf(System.currentTimeMillis()));
customerProfile.setDateUpdated(String.valueOf(System.currentTimeMillis()));
customerProfile.setLocale(Locale.getDefault().getCountry());

rezolveSession.getCustomerProfileManager().update(customerProfile, new CustomerProfileCallback() {
            @Override
            public void onUpdateSuccess(CustomerProfile customerProfile) {
                // handle success response
            }

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

Even though updating user data is not required to access the engagements, we need certain data to process the product purchase. In most standard usecase it is updated CustomerProfile and at least one Phone, Address and PaymentCard.


Adding phones, delivery and payment methods

Adding payment cards and delivery addresses is part of the payment flow. It requires payment module dependency:

dependencies {
    def rezolveSdkVersion = "3.2.0"
    implementation "com.rezolve.sdk:payment-android:$rezolveSdkVersion"
    // ...
}

As mentioned in the previous section after session was established you can start to create, update get and delete various items. There's a direct dependency between some of them. To create PaymentCard one of the required fields is addressId. To get addressId you first need to create an Address, but to do that you need phoneId, which is returned after phone object is created.

Phone myPhone = new Phone();
myPhone.setName("User's phone");
myPhone.setPhone("+447400123456");

RezolveSDK.peekInstance().getRezolveSession().getPhonebookManager().create(myPhone, new PhonebookCallback() {
            @Override
            public void onPhonebookCreateSuccess(Phone phone) {
                String phoneId = phone.getId();
            }

            @Override
            public void onError(@NonNull RezolveError error) {
                
            }
});
Address myAddress = new Address.Builder()
            .line1("10 Downing Street")
            .line2("optional line")
            .zip("SW1A 2AA")
            .city("London")
            .country("United Kingdom")
            .phoneId(phoneId)
            .fullName("Boris Johnson")
            .shortName("My Work Address")
            .build();
                
RezolveSDK.peekInstance().getRezolveSession().getAddressbookManager().create(myAddress, new AddressbookCallback() {
            @Override
            public void onAddressbookCreateSuccess(Address address) {
                String addressId = address.getId();
            }

            @Override
            public void onError(@NonNull RezolveError error) {
                        
            }
});
String pan = "4000221111111111";

PaymentCard myCard = new PaymentCard.Builder()
            .nameOnCard("John Doe")
            .addressId(addressId)
            .pan(pan)
            .brand("VISA")
            .shortName(pan.substring(pan.length() - 4))
            .validFrom("0122") // MMYY
            .expiresOn("1225") // MMYY
            .type("credit")
            .build();
                        
RezolveSDK.peekInstance().getRezolveSession().getWalletManager().create(myCard, new WalletCallback() {
            @Override
            public void onWalletCreateSuccess(PaymentCard paymentCard) {
                                
            }

            @Override
            public void onError(@NonNull RezolveError error) {
                                
            }
});

Mall tree. Getting merchants, categories and products.

Prerequisites

To be able to navigate Mall you need to finish [integration and initialization]???Link to core integration Github repo??? of core sdk module.

Getting merchants

First step is to create Mall is to get list of Merchants:

MerchantManager merchantManager = RezolveSDK.peekInstance().getRezolveSession().getMerchantManager();
        
merchantManager.getMerchants(context, MerchantManager.MerchantVisibility.VISIBLE, new MerchantCallback() {
    @Override
    public void onGetMerchantsSuccess(List merchants) {

                // you can now access list of Merchants
                Merchant merchant = merchants.get(0);
                String name = merchant.getName();
                List bannerThumbs = merchant.getBannerThumbs();
                String currencyCode = merchant.getCurrencyCode();
                String currencySymbol = merchant.getCurrencySymbol();
    }

    @Override
    public void onError(@NonNull RezolveError error) {
                
    }
});

Sometimes, when the Merchant is no longer active it might be hidden. If user has bought something from such merchant you might still need it's details to give your user better experience. In such case you can use MerchantManager.MerchantVisibility.HIDDEN or ALL.

Merchant object contains bunch of parameters which can be used to show it to the user. For reference please visit our SDK Doxygen documentation.

Search merchants

As an alternative you can get Merchant object by using the search feature:

RezolveLocation rezolveLocation = new RezolveLocation(latitude, longitude);

MerchantSearchData merchantSearchData = new MerchantSearchData(
        "search phrase",
        MerchantSearchOrderBy.LOCATION,
        SearchDirection.ASC,
        page,
        itemsPerPage,
        rezolveLocation
);

merchantManager.searchMerchants(context, merchantSearchData, new MerchantSearchInterface() {
            @Override
            public void onSearchMerchantsSuccess(MerchantSearchResult merchantSearchResult) {
                List merchants = merchantSearchResult.getMerchants()
            }

            @Override
            public void onError(@NonNull RezolveError rezolveError) {

            }
        });

Getting categories

Next step is to get selected Merchant's root category. Since results are paginated you need to define your PageNavigationFilter for both categories and products returned from this endpoint:

ProductManager productManager = RezolveSDK.peekInstance().getRezolveSession().getProductManager();

PageNavigationFilter pageNavigationCategoryFilter= new PageNavigationFilter();
pageNavigationFilter.setSortBy(PageNavigationFilter.SortBy.NAME);
pageNavigationFilter.setSortDirection(PageNavigationFilter.SortDirection.ASC);
pageNavigationFilter.setItemsPerPage(PageNavigationFilter.ItemLimit.LIMIT_30);
pageNavigationFilter.setPageNumber(4);

PageNavigationFilter pageNavigationProductFilter = PageNavigationFilter.getDefault(-1); // you can also use default filter like this

productManager.getProductsAndCategories(
    merchant.getId(),
    null,
    pageNavigationCategoryFilter,
    pageNavigationProductFilter, 
    new ProductCallback() {
        @Override
        public void onGetProductsAndCategoriesSuccess(Category category) {
             Category parentCategory = category;
             List childCategories = category.getCategoryPageResult().getItems();
             List childProducts = category.getProductPageResult().getItems();
        }

        @Override
        public void onError(@NonNull RezolveError error) {
                                
        }
    }
);

To get to the next level of category tree you need to make the same call, but replace second argument with selected category.getId().

Getting products

To get Product details use ProductManager.getProduct(...) method:

productManager.getProduct(
    this,
    merchant.getId(),
    category.getId(),
    selectedProduct.getId(),
    new ProductCallback() {
        public void onGetProductSuccess(Product product) {
            String title = product.getTitle();
            float price = product.getPrice();
        }

        public void onError(@NonNull RezolveError error) {
        
        }
    }
);

Similar as with Merchants, you can use ProductManager.searchProducts(@NonNull final Context context, @NonNull ProductSearchData searchData, @NonNull final ProductSearchInterface productSearchInterface) to search products by key phrase.


How to make a purchase?

Prerequisites

Buying products or carts requires payment module dependency:

dependencies {
    def rezolveSdkVersion = "3.2.0"
    implementation "com.rezolve.sdk:payment-android:$rezolveSdkVersion"
    // ...
}

Simple product

There are couple of steps required before we can checkout a Product. In the most simple form we create a CheckoutProduct object which allows us to make a request for payment and delivery options and then checkout and buy:

CheckoutProduct checkoutProduct = new CheckoutProduct();
checkoutProduct.setId(product.getId());
checkoutProduct.setQty(quantity);

Complex Product

Product might have two types of options: configurable and custom.

Configurable options

Configurable options like size or color are pre-determined, based on RCE Product setup. These options might affect price.

List

The payload for configurable options might look like this:

"options": [
        {
            "code": "color",
            "extra_info": "",
            "label": "Color",
            "values": [
                {
                    "label": "red",
                    "value": "227"
                },
                {
                    "label": "blue",
                    "value": "228"
                }
            ]
        }
    ]

Which means you should present option label "Color" to the user with possible values to select: "red" or "blue". If user selects "red" you should add it to the CheckoutProduct using:

checkoutProduct.addConfigurableOption(code, value);

so in the example above it means:

checkoutProduct.addConfigurableOption("color", 227);

If Product has more than one configurable option, for example "size" and "color" it gets a little bit more complicated. For example, if you have created product "shoes" with sizes: "7 UK" and "8 UK" and colors: "red" and "blue" in reality you have 4 different products:

  • Shoes 7 UK, red
  • Shoes 8 UK, blue
  • Shoes 7 UK, red
  • Shoes 8 UK, blue

These combinations can be looked up in product.getOptionsAvailable();.

Each of these variants can have different price. Prices for each variant are provided in product.getPriceOptions();.

Custom options

Custom options don't affect the price. There are 3 types:

  • drop_down - user should select single value from possible values, similar to configurable product
  • date - user should provide a date in "YYYY-MM-DD HH:mm:ss" format, for example "2022-02-28 00:00:00"
  • field - user can type their own text

When user has selected custom option, you should provide it to the CheckoutProduct object:

CustomConfigurableOption cco = new CustomConfigurableOption();
cco.setOptionId(customOption.getOptionId());
cco.setValue(selectedValue);

checkoutProduct.addCustomConfigurableOption(product, cco);

Payment and delivery options

Once you have your CheckoutProduct it's time to get delivery and payment methods.

RezolveSDK.peekInstance().getRezolveSession().getPaymentOptionManager().getProductOptions(checkoutProduct, merchantId, new PaymentOptionCallback() {
            @Override
            public void onProductOptionsSuccess(PaymentOption paymentOption) {
                List supportedDeliveryMethods = paymentOption.getSupportedDeliveryMethods();
                List supportedPaymentMethods = paymentOption.getSupportedPaymentMethods();
            }

            @Override
            public void onError(@NonNull RezolveError error) {
                
            }
});

First, let's talk about payment methods.

SupportedPaymentMethod supportedPaymentMethod = supportedPaymentMethods.get(index);
String type = supportedPaymentMethod.getType();

type parameter describes payment provider configured for certain Merchant. For some, multiple types are available. Some virtual products might be available for free and some stores might allow paying in cash with store pickup. In most basic scenario it will be simple credit card payment, but it can also contain external payment providers.

When user has selected payment method you can display related delivery methods. Some methods are not possible with certain methods of payment. List of available delivery options for payment method are listed here:

List supportedDelivery = supportedPaymentMethod.getPaymentMethodData().getSupportedDelivery(); // 'flatrate' for delivery to certain address. 'storepickup' for pickup in store.

Sometimes, even though certain delivery method is enabled for payment provider, it might be possible for a Merchant to execute it. To check what delivery methods are available for a Merchant you can check SupportedDeliveryMethod's carrier codes:

SupportedDeliveryMethod supportedDeliveryMethod = supportedDeliveryMethods.get(index);
ShippingMethod shippingMethod = supportedDeliveryMethod.getShippingMethod();
String carrierCode = shippingMethod.getCarrierCode(); // 'flatrate' or 'storepickup'

If user has selected flatrate delivery they should now select one of their previously created addresses. With that you can create DeliveryUnit object required for checkout:

DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, addressId);

If user has selected storepickup delivery you should display list of available stores, where user can pick up their order. Storedetails can be found here:

StoreDetails storeDetails = supportedDeliveryMethod.getShippingDetails().getStoreDetails();

If user has selected a store, you should create DeliveryUnit with store id:

DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, Integer.valueOf(shippingMethod.getExtensionAttributes().get(0).getValue()));

Checkout

To checkout product you need to create CheckoutBundleV2 object:

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

CheckoutManagerV2 checkoutManagerV2 = RezolveSDK.peekInstance().getRezolveSession().getCheckoutManagerV2();

checkoutManagerV2.checkoutProductOption(checkoutBundleV2, new CheckoutV2Callback() {
            @Override
            public void onProductOptionCheckoutSuccess(Order order) {
                String orderId = order.getOrderId();
                float finalPrice = order.getFinalPrice();
                List priceBreakdowns = order.getBreakdowns();
            }

            @Override
            public void onError(@NonNull RezolveError error) {
                
            }
});

Order object will contain final price and price breakdown, for example:

"price_breakdown": [
        {
            "amount": 30,
            "type": "unit"
        },
        {
            "amount": 10,
            "type": "shipping"
        },
        {
            "amount": 0,
            "type": "tax"
        },
        {
            "amount": 0,
            "type": "discount"
        }
    ]

Buy

If user decided to pay with credit card, create payment request. PaymentCardId was assigned to each payment card created earlier. Cvv security code will be encrypted before sending it to Rezolve backend. If user has selected other payment method, the PaymentRequest should be null.

PaymentRequest paymentRequest = checkoutManagerV2.createPaymentRequest(paymentCardId, cvv);

checkoutManagerV2.buyProduct(
                paymentRequest,
                checkoutBundleV2,
                orderId,
                rezolveLocation,
                new CheckoutV2Callback() {
                    @Override
                    public void onProductOptionBuySuccess(OrderSummary orderSummary) {
                        String orderId = orderSummary.getOrderId();
                    }

                    @Override
                    public void onError(@NonNull RezolveError error) {
                        
                    }
                }
);

At this point the request to purchase a product was placed. But, since some of the payment methods are asynchonous, some require more interaction from the user (confirmation of 3ds payments, payments via external systems, etc) we advise to check the transaction status by requesting latest orders with UserActivityManager.

Add product to cart

checkoutManagerV2.addProductToCart(checkoutProduct, merchantId, new AddProductToCartInterface() {
            @Override
            public void onAddProductsToCartSuccess(@NonNull CartDetails cartDetails) {
                
            }

            @Override
            public void onError(@NonNull RezolveError rezolveError) {

            }
        });

Get cart product

Each Merchant will have separate cart. You can get list of carts with getCarts method:

checkoutManagerV2.getCarts(new GetCartsInterface() {
            @Override
            public void onGetCartsSuccess(@NonNull List list) {
                CartDetails cartDetails = list.get(index);
                String merchantId = cartDetails.getMerchantId();
                List checkoutProducts = cartDetails.getProducts();
            }

            @Override
            public void onError(@NonNull RezolveError rezolveError) {

            }
        });

If you want to get details of a product from the cart you can use ProductManager.getCartProduct() method:

RezolveSDK.peekInstance().getRezolveSession().getProductManager().getCartProduct(
                merchantId,
                checkoutProduct.getId(),
                new ProductCallback() {
                    @Override
                    public void onGetProductSuccess(Product product) {
                        
                    }

                    @Override
                    public void onError(@NonNull RezolveError error) {
                        
                    }
                }
);

Checkout cart

Cart checkout is very similar to product checkout, but instead of CheckoutProduct you need to provide cartId. First get payment and delivery options for your cart, then select relevant options and checkout cart:

RezolveSDK.peekInstance().getRezolveSession().getPaymentOptionManager().getCartOptions(
                merchantId,
                cartId,
                new PaymentOptionCallback() {
                    @Override
                    public void onCartOptionsSuccess(List paymentOptions) {
                        PaymentOption paymentOption = paymentOptions.get(0);
                    }

                    @Override
                    public void onError(@NonNull RezolveError error) {
                        
                    }
                }
        );

CheckoutBundleV2 checkoutBundleV2 = CheckoutBundleV2.createCartCheckoutBundleV2(
                merchantId,
                paymentOption.getId(),
                cartId,
                phoneId,
                supportedPaymentMethod,
                deliveryUnit
);

checkoutManagerV2.checkoutCartOption(checkoutBundleV2, new CheckoutV2Callback() {
            @Override
            public void onCartOptionCheckoutSuccess(Order order) {
                String orderId = order.getOrderId();
            }

            @Override
            public void onError(@NonNull RezolveError error) {
                
            }
        });

Buy cart

checkoutManagerV2.buyCart(
                paymentRequest,
                checkoutBundleV2,
                orderId,
                rezolveLocation,
                new CheckoutV2Callback() {
                    @Override
                    public void onProductOptionBuySuccess(OrderSummary orderSummary) {
                        
                    }

                    @Override
                    public void onError(@NonNull RezolveError error) {
                        
                    }
                }
);

Abort purchase

If payment method selected by the user requires additional steps, user might fail to complete the purchase. When you're notified of such event, use abortPurchase method to let Rezolve systems know that the payment has failed.

checkoutManagerV2.abortPurchase(
                orderId,
                new CheckoutV2Callback() {
                    @Override
                    public void onPurchaseAbortSuccess() {
                        
                    }

                    @Override
                    public void onError(@NonNull RezolveError error) {
                        
                    }
                }
);

Scan module integration

Prerequisites

To integrate scan module with rest of Smart Engage systems, first you need to complete SspActManager integration

int desiredImageWidthInPixels = 400;

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

This will allow to "translate" result of a scan into a meaningful ContentResult.

Dependencies

These dependencies are required by the Scan module. They should be added in a project build.gradle:

dependencies {
    def sdkScanVersion = "3.0.1"
    implementation "com.rezolve.sdk:scan-android:$sdkScanVersion"
    implementation "com.rezolve.sdk:scan-android-x86:$sdkScanVersion" // optional module, required for support of x86 devices
}

Permissions

The module uses device camera and audio. These permission are required for the Scan module usage.

    
    

Please consult official Android docummentation for details about implementing runtime permissions.

Implementation

RezolveScanView view component allows to show camera view:


RezolveScanView view component allows to show camera view:

RezolveScanView scanView = findViewById(R.id.rezolve_scan_view);

AudioScanManager and VideoScanManager instances allow to manipulate and use the functionality of the Scan module. AudioScanManagerProvider.getAudioScanManager() returns an AudioScanManager instance, and VideoScanManagerProvider.getVideoScanManager() returns a VideoScanManager instance. Those managers are needed to manage the module and handle data:

private AudioScanManager audioScanManager = AudioScanManagerProvider.getAudioScanManager();
private VideoScanManager videoScanManager = VideoScanManagerProvider.getVideoScanManager();

Scan module usage

First, create a listener for scan callbacks:

    private final ResolveResultListener resolveResultListener = new ResolveResultListener() {
        @Override
        public void onProcessingStarted(@NonNull UUID uuid) {
            Log.d(TAG, "onProcessingStarted: " + uuid);
            processingStarted();
        }

        @Override
        public void onProcessingFinished(@NonNull UUID uuid) {
            Log.d(TAG, "onProcessingFinished: " + uuid);
            processingFinished();
        }

        @Override
        public void onProcessingUrlTriggerStarted(@NonNull UUID uuid, @NonNull UrlTrigger urlTrigger) {
            Log.d(TAG, "onProcessingUrlTriggerStarted: " + uuid + " url: " + urlTrigger.getUrl());
        }

        @Override
        public void onContentResult(@NonNull UUID uuid, @NonNull ContentResult result) {
            Log.d(TAG, "onContentResult: " + result);
            if(result instanceof ProductResult) {
                ProductResult productResult = (ProductResult) result;
                onProductResult(productResult.getProduct(), productResult.getCategoryId());
            } else if(result instanceof CategoryResult) {
                CategoryResult categoryResult = (CategoryResult) result;
                onCategoryResult(categoryResult.getCategory(), categoryResult.getMerchantId());
            } else if(result instanceof SspActResult) {
                SspActResult act = (SspActResult) result;
                if (act.sspAct.getPageBuildingBlocks() != null && !act.sspAct.getPageBuildingBlocks().isEmpty()) {
                    onSspActResult(act);
                }
            } else if(result instanceof SspProductResult) {

            } else if(result instanceof SspCategoryResult) {

            }
        }

        @Override
        public void onResolverError(@NonNull UUID uuid, @NonNull ResolverError resolverError) {
            if(resolverError instanceof  ResolverError.Error) {
                ResolverError.Error error = (ResolverError.Error) resolverError;
                onScanError(error.rezolveError.getErrorType(), error.message);
            }
        }
    };

Create image reader when your fragment or activity is created:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scan);

        videoScanManager.createImageReader();
    }

It is recommended to check the state of required permissions each time before start of scanning in case user has revoked permissions.

If RECORD_AUDIO permission was given you can initialize audio scan. If CAMERA permission was given you can initialize video scan.

    @Override
    protected void onResume() {
        super.onResume();
        ResolverResultListenersRegistry.getInstance().add(resolveResultListener);

        String[] scannerPermissions = {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
        Permissions.check(this, scannerPermissions, null, null, new PermissionHandler() {
            @Override
            public void onGranted() {
                audioScanManager.clearCache();
                audioScanManager.startAudioScan();

                videoScanManager.clearCache(); // clears cached scan results
                videoScanManager.startCamera(); // starts camera preview
                videoScanManager.attachReader(); // adds watermark detection
            }
        });
    }

When your fragment or activity is paused, stop scan managers and remove the listener from registry to avoid memory leaks. When it goes into onDestroy state, destroy the image reader:

    @Override
    protected void onPause() {
        super.onPause();
        videoScanManager.detachReader();
        audioScanManager.stopAudioScan();
        audioScanManager.destroy();
        ResolverResultListenersRegistry.getInstance().remove(resolveResultListener);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        videoScanManager.destroyImageReader();
    }

It is also popular to implement Smart Engage Scan module in "scan on demand" mode. In this case you should add/remove watermark reader on user's demand:

button.setOnTouchListener(new View.OnTouchListener() {
   @Override
   public boolean onTouch(View v, MotionEvent event) {
      switch (event.getAction()) {
         case MotionEvent.ACTION_DOWN:
            // video scan started
            videoScanManager.clearCache();
            videoScanManager.attachReader();
            return true;

         case MotionEvent.ACTION_OUTSIDE:
         case MotionEvent.ACTION_UP:
            // video scan stopped
            videoScanManager.detachReader();
            return true;
      }
         
      return false;
   }
});

Torch

VideoScanManager also provides some helper methods to operate torch:

videoScanManager.isTorchSupported(); // true if torch is supported on a device
videoScanManager.isTorchOn(); // true if torch is currently turned on
videoScanManager.setTorch(boolean isOn); // turns the torch on/off

Editing In The Area feature

Overview

In the area allows the user to see nearby location engagements. To use the feature, you need to enable location tracking on your device.

How it works

RxpSdk

RxpSdk implementation requires additional sdk module added to build.gradle dependencies:

dependencies {
    implementation "com.rezolve.sdk:rxp-android:$rezolveSdkVersion"
}

For details on RxpSdk integration [click here](??? Link to RXP section of ACI Github???)

Use this request to get list of nearby engagements:

APIResult result = RXPClientProvider.client.getMyArea(
    float latitude, 
    float longitude, 
    long distance, 
    CoordinateSystem coordinateSystem, 
    MyAreaFilter filter, // MyAreaFilter.ALL for all nearby engagements, MyAreaFilter.MY for engagements matching user's interests
    long limit, 
    long offset
);
if(result instanceof APIResult.Success){
    SliceOfMyAreaResponse successResult = ((APIResult.Success) result).getResult();
    for(MyAreaResponse myAreaResponse : successResult.getData()){
        String content = myAreaResponse.getContent();
        long distance = myAreaResponse.getDistance();
        String engagementId = myAreaResponse.getEngagementId();
        long id = myAreaResponse.getId();
        Coordinates location = myAreaResponse.getLocation();
        String thumbnailUrl = myAreaResponse.getThumbnailUrl();
        String title = myAreaResponse.getTitle();
    }
} else {
    //Handle error response
}

From there, you need to build a screen that shows list of results and redirects user to an engagement page when user interacts with an item from the list.


Understanding Acts

SspActManager

SspActManager is responsible for getting Acts by Act ID or Engagement ID. It is also used to submit answers to the Acts and getting historical submissions. To implement it you need ssp-android module dependency. If you're not using new RxpSdk you will also need legacy methods provided in old-ssp-android module.

dependencies {
   implementation "com.rezolve.sdk:ssp-android:$rezolveSdkVersion"
   implementation "com.rezolve.sdk:old-ssp-android:$rezolveSdkVersion"
}

old-ssp-android module provides extension of SspActManager class so if make sure you import it from correct package (com.rezolve.sdk.ssp.managers.SspActManager vs com.rezolve.sdk.old_ssp.managers.SspActManager). This extended manager provides getSspGeofenceEngagements method that allows to fetch nearby engagements.

To initialize SspActManager:

        AuthParams authParams = new AuthParams(
                                    AUTH0_CLIENT_ID,
                                    AUTH0_CLIENT_SECRET,
                                    AUTH0_API_KEY,
                                    AUTH0_AUDIENCE,
                                    AUTH0_ENDPOINT,
                                    SSP_ENGAGEMENT_ENDPOINT,
                                    SSP_ACT_ENDPOINT
                            );

        HttpClientConfig httpConfig = new HttpClientConfig.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .build();

        HttpClientFactory httpClientFactory = new HttpClientFactory.Builder()
                .setHttpClientConfig(httpConfig)
                .setAuthParams(authParams)
                .build();

        SspHttpClient sspHttpClient = httpClientFactory.createHttpClient(SSP_ENDPOINT);

        SspActManager sspActManager = new SspActManager(sspHttpClient, rezolveSDK);

Getting Act

Getting acts is handled by sspActManager in the form of SspAct instances.

Acts cannot be fetched in bulk. In order to get an act, proceed by calling sspActManager's getAct method. Required parameters are the act's id, parameter describing the act's image width and SspGetActInterface which is a callback to getAct method.

sspActManager.getAct(actId, 400, new SspGetActInterface() {
    @Override
    public void onGetActSuccess(SspAct sspAct) {
        Log.d(TAG, "SspAct: " + sspAct.entityToJson());
    }

    @Override
    public void onError(@NonNull RezolveError rezolveError) {
        // Error handling
    }
});

Presenting Act

Before displaying sspAct, check the value of the field sspAct.getPageBuildingBlocks(). 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):
    class BlockWrapper {
        private final PageBuildingBlock pageBuildingBlock;
        private String answerToDisplay;
    
        public BlockWrapper(PageBuildingBlock pageBuildingBlock, String answerToDisplay){
            this.pageBuildingBlock = pageBuildingBlock;
            this.answerToDisplay = answerToDisplay;
        }
    
        public PageBuildingBlock getPageBuildingBlock() {
            return pageBuildingBlock;
        }
    
        public String getAnswerToDisplay() {
            return answerToDisplay;
        }
    
        public void setAnswerToDisplay(String answerToDisplay) {
            this.answerToDisplay = answerToDisplay;
        }
    }
    
  2. Create an adapter class to show the custom layout.
  3. List blocks In your fragment or activity class submit list of blocks to the adapter.
  4. Create layouts to present different types of blocks: PageBuildingBlock.getType() all available types:
Type.HEADER
Type.PARAGRAPH
Type.DIVIDER
Type.IMAGE
Type.VIDEO
Type.DATE_FIELD
Type.SELECT
Type.TEXT_FIELD
Type.COUPON
Type.UNKNOWN

Act submission

In order to post an act proceed by calling sspActManager's submitAnswer method, providing the act's id and act data in SspActSubmission format as parameters. Please note that any answers to relevant act questions are a part of the act data in SspActAnswer format. To create SspActSubmission use SspActSubmission.Builder.

SspActSubmission sspActSubmission = new SspActSubmission.Builder()
        .setUserId(String)
        .setUserName(String)
        .setEntityId(String)
        .setServiceId(String)
        .setAnswers(List)
        .setPersonTitle(String)
        .setFirstName(String)
        .setLastName(String)
        .setEmail(String)
        .setPhone(String)
        .setLocation(RezolveLocation)
        .build();

sspActManager.submitAnswer(actId, sspActSubmission, new SspSubmitActDataInterface() {
    @Override
    public void onSubmitActDataSuccess(SspActSubmissionResponse sspActSubmissionResponse) {
        // Handle success
    }

    @Override
    public void onError(@NonNull RezolveError rezolveError) {
        // Error handling
    }
});

Act submission history

In order to fetch historical act submissions by the user proceed by calling sspActManager's getActSubmissions method, while providing the entity id as a parameter. The request will return a ActSubmissionHistoryObject instance, while the submitted acts are available by calling getActs method on this object.

Please note that answers in the act data are in SspActAnswer format. Meaning the text of the question is unavailable until the developer fetches the act. To get the question texts, the developer has to call the above-mentioned getAct with an adequate act id.

sspActManager.getActSubmissions(entityId, new SspGetActSubmissionInterface() {
    @Override
    public void onGetSspActSubmissionsSuccess(ActSubmissionHistoryObject actSubmissionHistoryObject) {
        actSubmissionHistoryObject.getActs();
    }

    @Override
    public void onError(@NonNull RezolveError rezolveError) {
        // Error handling
    }
});

Rxp module integration

RxpSdk

RxpSdk is a common SDK client which allows android developers to use following features:

  • authentication handling
  • updating access token
  • manage and handle geofence engagements
  • monitoring location updates
  • configuration of push notifications
  • and others

Prerequisites

Smart Triggers uses Firebase Cloud Messaging to send push notifications to users' devices.

Consumer of RxpSdk needs to provide Firebase Server Key in order to let Smart Engage systems send push notifications. Please get in touch with your Smart Engage contact in order to do so.

Dependencies

These dependencies are required by the Rxp module. They should be added in a project build.gradle:

dependencies {
   def rezolveSdk = "2616-bitplaces-5452b67" // TODO: Update it with proper, tagged version after we do the official release of new sdk.
   implementation "com.rezolve.sdk:rxp-android:$rezolveSdk"
}

Rxp manager helper classes and interfaces

Below the table describes Rxp interfaces are required to handle its state and features:

Interface or class name Description
Authenticator OkHttp3 interface which allows to update access token in case request fails with "token expired" error. Performs either preemptive authentication before connecting to a proxy server, or reactive authentication after receiving a challenge from either an origin web server or proxy server.
NotificationProperties Notification properties wrapper class which describes notification-related fields: channelId, smallIcon, color, priority, defaults, vibrationPattern, sound, autoCancel.
PushNotificationProvider Allows to manage and configure push notifications.
NotificationHelper Helper class to provide better control of notifications.
SspGeofenceDetector Abstraction class for geofence detector.
SspActManager A manager class that handles Rxp backend communication.

Key variables

Variable name Description
APP_CONTEXT Android application context
SDK_API_KEY SDK Api Key
SMART_TRIGGERS_ENDPOINT Backend URL address which is used for networking communication

Rxp SDK initialization

RxpSdk should be initialized in Android application class onCreate() method.

@Override 
public void onCreate() {
    super.onCreate();
    initRxpSdk()
}

fun initRxpSdk() {

        val geofenceEngagementAlerts = NotificationProperties(
            geofenceAlertsNotificationChannelId,               // channel id
            R.mipmap.ic_launcher,                              // small icon
            ContextCompat.getColor(APP_CONTEXT, R.color.blue), // color
            NotificationCompat.PRIORITY_HIGH,                  // priority
            Notification.DEFAULT_ALL,                          // notification options. The value should be one or more of the following fields combined
                                                               //    with bitwise-or: Notification.DEFAULT_SOUND, Notification.DEFAULT_VIBRATE, Notification.DEFAULT_LIGHTS.
                                                               //    For all default values, use Notification.DEFAULT_ALL.
            longArrayOf(1000, 1000, 1000, 1000, 1000),         // vibration pattern
            Settings.System.DEFAULT_NOTIFICATION_URI,          // sound
            true                                               // auto cancel
        )

        // you can find PushNotificationProvider example in the next section

        val pushNotificationProvider: PushNotificationProvider = object : PushNotificationProvider {
                override fun resetCache() {

                }

                override val messages: MutableSharedFlow
                    get() = yourMessagesFlow

                override val pushToken: MutableSharedFlow
                    get() = yourPushTokenFlow
            }

        PushNotificationDIProvider.pushNotificationProvider = pushNotificationProvider

        val authenticator = object : Authenticator {
            override fun authenticate(route: Route?, response: Response): Request? {
                // your implementation of okhttp3 Authenticator that refreshes expired Rezolve Core SDK access token. Check section below for an example.
            }
        }

        val tokenHolder: TokenHolder = object : TokenHolder {
            // implement methods
        }

        val accessTokenFlowable = tokenHolder.observeAccessTokenAsFlow()

        val notificationHelper = object : NotificationHelper {
            // implement methods
        }

        val rxpSdk = RxpSdk.Builder(APP_CONTEXT)
            .authenticator(authenticator)
            .accessTokenFlowable(accessTokenFlowable)
            .notificationAlerts(geofenceEngagementAlerts)
            .pushNotificationProvider(pushNotificationProvider)
            .notificationHelper(notificationHelper) // you can use NotificationHelperImpl(APP_CONTEXT) or provide your own implementation
            .apiKey(SDK_API_KEY)
            .endpoint(SMART_TRIGGERS_ENDPOINT)
            .geofenceDetector(geofenceDetector)
            .sspActManager(sspActManager)
            .build()

        RxpSdkProvider.sdk = rxpSdk
    }

PushNotificationProvider

object FCMManagerProvider {
    lateinit var manager: FCMManager
}

Initialize FCMManager in Android application class onCreate() method.

class FCMManager constructor(context: Context) : PushNotificationProvider {

    private val _token = MutableSharedFlow(
        replay = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    private val _messages = MutableSharedFlow(
        replay = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    override val pushToken: Flow
        get() = _token.distinctUntilChanged()

    override val messages: Flow
        get() = _messages.distinctUntilChanged()

    init {
        FirebaseApp.initializeApp(context)
        FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
            if (!task.isSuccessful) {
                Log.e(TAG, "Fetching FCM registration token failed", task.exception)
                _token.tryEmit(PushToken.None)
                return@OnCompleteListener
            }
            val token = task.result
            if( token != null ){
                _token.tryEmit(PushToken.FCM(token))
            } else {
                Log.e(TAG, "FCM token is null")
                _token.tryEmit(PushToken.None)
                return@OnCompleteListener
            }
        })
    }

    fun updateToken(newToken: String) {
        _token.tryEmit(PushToken.FCM(newToken))
    }

    fun onMessageReceived(message: RemoteMessage) {
        println("$TAG.onMessageReceived: $message")
        _messages.tryEmit(message.toPushMessage())
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    override fun resetCache(){
        _messages.resetReplayCache()
    }

    companion object {
        const val TAG = "FCM_Manager"
    }
}

class RezolveFirebaseMessagingService : FirebaseMessagingService() {

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        println("RezolveFirebaseMessagingService.onMessageReceived: $message")
        FCMManagerProvider.manager.onMessageReceived(message)
    }

    override fun onNewToken(token: String) {
        Log.d(TAG, "Refreshed token: $token")
        FCMManagerProvider.manager.updateToken(token)
    }

    companion object {
        const val TAG = "RezolveFMS"
    }
}

private fun RemoteMessage.toPushMessage() = PushMessage(this.data)

Don't forget to register messaging service in your manifest:

    

        
            
                
            
        

    

Authenticator

        class RezolveAuthenticator(private val authRequestProvider: RezolveSDK.AuthRequestProvider) : Authenticator {
            override fun authenticate(route: Route?, response: Response): Request? {
                if (responseCount(response) > 1) {
                    return null // If it've already been tried, give up.
                }
                val getAuthRequest: RezolveSDK.GetAuthRequest = authRequestProvider.authRequest
                if (getAuthRequest.isSuccessful) {
                    val headersMap: MutableMap? = getAuthRequest.headersMap
                    val builder = response.request.newBuilder()
                    if (headersMap != null) {
                        for ((key, value) in headersMap) {
                            builder.header(key, value)
                        }
                    }
                    return builder.build()
                }
                return null
            }

            private fun responseCount(response: Response?): Int {
                return if (response == null) 0 else 1 + responseCount(response.priorResponse)
            }
        }

SspGeofenceDetector

There are various providers of geofence detection. It's up to you to decide which one best suits your needs. The default implementation of SspGeofenceDetector, based on Google Geofencing Client was provided in ssp-google-geofence module.

dependencies {
   implementation "com.rezolve.sdk:ssp-google-geofence:$rezolveSdkVersion"
}

If you choose to use it, use provided Builder class to create an instance:

val geofenceDetector = GoogleGeofenceDetector.Builder()
                .transitionTypes(GoogleGeofenceDetector.TRANSITION_TYPE_ENTER or GoogleGeofenceDetector.TRANSITION_TYPE_EXIT)
                .build(context)

Location Triggers

One of the most powerful tools you can take advantage of with Smart Engage SDK is Location Triggers. It keeps a reference of the user's location, and through a decision making mechanism it sends unobtrusive Push Notifications at the right time, thus enticing customers into an action when it is deemed proper.

Initialising

The entry point of Location Triggers is run once, in order to inform the backend about the user's essential information - it is done automatically after RxpSDK is initialized. You can run checkIn yourself by using

APIResult checkInApiResult = RXPClientProvider.client.checkIn(pushToken);
if (checkInApiResult instanceof APIResult.Success){
    //handle successful check in
    String accessToken = ((APIResult.Success) checkInApiResult).getResult();
} else {
    //handle error
}

Getting available Interest Tags

With the current user already in check-in state, as seen in the previous section, we can now fetch the state of the user's Interest Tags. An array of RXPTag models will be provided as a response to this request, indicating the following parameters for each item:

  • tagId A unique identifier for this item
  • name Descriptive name of the Interest Tag
  • state Indicates whether the Tag is "UNDEFINED", "ENABLED" or "DISABLED" for this user Tags are accessible in two ways:
  • By TagsListWorker with RXPDatabase (currently only with Kotlin support - WIP: adding getting tags from db as LiveData)
    RxpSdkProvider.sdk.runTagsListWorkerOnce();
    val tags = RXPDatabaseProvider.database.tagDAO().getTags();
    
  • By executing RXPClientInterface.listTags()
APIResult> listTagsApiResult = RXPClientProvider.client.listTags();
if (listTagsApiResult instanceof APIResult.Success){
    List tags = ((APIResult.Success>) listTagsApiResult).getResult();
} else {
    //handle error
}

Updating Interest Tags

The defining step that subscribes your audience into your established Location Triggers is updating the user's chosen Interest Tags, as seen on the following example. Any of this methods calls should trigger TagsUpdateWorker which is uploading Interest Tags from DB to API:

List tags;
RXPDatabaseProvider.database.tagDAO().updateTagsAndSetAllTagsWithStateUndefinedToDisabled(tags);
State state;
RXPDatabaseProvider.database.tagDAO().updateAllTagsToState(state);
State withState;
State toState;
RXPDatabaseProvider.database.tagDAO().updateAllTagsWithStateToState(withState, toState)

or by executing RXPClientInterface.updateTags()

List tagsToUpdate;
APIResult> updateTagsApiResult = RXPClientProvider.client.updateTags(tagsToUpdate);
if (updateTagsApiResult instanceof APIResult.Success){
    //handle successful update
} else {
    //handle error
}

Invoking Location Tracking

After the user has identified to the backend service, and their Interest Tags are set, the last step would be to engage Location Tracking. During RxpSdk initialization periodic and onLocationChanged TrackingWorker are set up. You can modify its behaviour by modifing Checker.shouldProcessLocationUpdate method and supplying it in CheckerProvider. It needs to be set after RxpSdk initialization. You can also send your own updateTracking request

float latitude;
float longitude;
int radius;
APIResult updateTrackingApiResult = RXPClientProvider.client.updateTracking(
    latitude,
    longitude,
    radius,
    CoordinateSystem.WGS84
);
if (updateTrackingApiResult instanceof APIResult.Success){
    //handle successful update
} else {
    //handle error
}

User Activity

In order to present information about user's activities throughout the app, you will need to use the Activity Manager. It provides insights on purchase history, shipping method, delivery address, total product pricing, and other useful details. Your search criteria depends entirely on providing a Date object, so you can get history items based on specific timeframes.

User Activity Manager

The following features are available:

Get transaction history items for a time period.

After the session has been established SDK related Managers like UserActivityManager are already initialised for the developer and available at RezolveSession.getUserActivityManager()

Get transaction history items for a time period

To fetch the list of orders from the server, call the getOrdersV3 function.

  • Parameters
    • userActivityInterface: A callback when the task is completed.
    • from: Optional date in yyyy-MM-dd format for getting data from that date.
    • to: Optional date in yyyy-MM-dd format for getting data to that date.

Result will be an OrderHistoryObject in case of success, or an error if it failed. Use method getOrders() from OrderHistoryObject to get order details in format of List<OrderDetails>.

userActivityManager.getOrdersV3(new UserActivityCallback() {
    @Override
    public void onGetOrdersSuccess(OrderHistoryObject orderHistoryObject) {
        orderHistoryObject.getOrders();
    }

    @Override
    public void onError(@NonNull RezolveError error) {
        // Error handling
    }
}, from, to);

OrderDetails fields:

  • timestamp: Order placement timestamp
  • status: Order's status. Available order statuses:
    • COMPLETED
    • SUBMITTED
    • CANCELED
    • PROCESSING
    • PENDING
    • PRE_CANCELED
    • UNKNOWN
  • shippingMethod: Order's shipping method.
  • shippingAddressDetails: Order's shipping address.
  • pickupAddressDetails: Order's pickup address.
  • storeDetails: Order's store details.
  • price: Order's price.
  • phone: Client's phone.
  • partnerId: Partner's Id.
  • orderId: Order's id.
  • merchantPhone: Merchant's phone.
  • merchantName: Merchant's name.
  • merchantId: Merchant's id.
  • merchantEmail: Merchant's email.
  • firstName: Client's first name.
  • lastName: Client's last name.
  • items:
  • email: Client's email.
  • billingAddressDetails: Client's billing address.
  • paymentMethod: Payment method used at order placement.
  • customPayload: Additional data.

User can browse the app/use endpoints from Smart Engage API without fully signing up and signing in. To achieve that, guest register and login are used. For all the other API calls entityId(sdkEntity) is needed. It can be acquired by registering guest user which will return the sdkEntity in response

Guest User Registration

To register a guest user you can call this endpoint:

POST: https://core.sbx.eu.rezolve.com/api/v2/authentication/register/guest
-H content-type: application/json
-d {
}

Responses:

200 OK
{
  "id": 100,
  "username": "ab0ed6e0-1022-4c47-83e9-d69af0311540",
  "firstName": null,
  "lastName": null,
  "email": "",
  "phone": "",
  "language": "en",
  "sdkEntity": "ab0ed6e0-1022-4c47-83e9-d69af0311540",
  "sdkPartner": "2",
  "createdAt": "2022-04-13T12:26:48.418",
  "updatedAt": "2022-04-13T12:26:48.418",
  "enabled": true,
  "partnerId": "2",
  "timesLoggedIn": null,
  "roles": [
    {
      "role": "ROLE_USER"
    }
  ],
  "guest": true,
  "accountNonExpired": true,
  "accountNonLocked": true,
  "credentialsNonExpired": true,
  "timezone": "GMT"
}

Guest User Login

Apart from registering also login is needed to get authorization token which is returned on succesful login. To do so you need to call this endpoint:

POST: https://core.sbx.eu.rezolve.com/api/v2/authentication/login/guest
-H content-type: application/json
-H authorization: Bearer signed-jwt
-d {
    "deviceId": deviceId,
    "entityId": entityId
}

Responses:

200 OK
{
  "id": 100,
  "username": "ab0ed6e0-1022-4c47-83e9-d69af0311540",
  "firstName": null,
  "lastName": null,
  "email": "",
  "phone": "",
  "language": "en",
  "sdkEntity": "ab0ed6e0-1022-4c47-83e9-d69af0311540",
  "sdkPartner": "2",
  "createdAt": "2022-04-13T12:26:48.418",
  "updatedAt": "2022-04-13T12:26:48.418",
  "enabled": true,
  "partnerId": "2",
  "timesLoggedIn": 1,
  "roles": [
    {
      "role": "ROLE_USER"
    }
  ],
  "guest": true,
  "accountNonExpired": true,
  "credentialsNonExpired": true,
  "accountNonLocked": true,
  "timezone": "GMT"
}

Authorization token is returned in response header - "Authorization"

Upgrade to standard user

When user wants to register as normal user you should upgrade the guest user to normal by calling this endpoint with the entityId returned by guest register:

PUT: https://core.sbx.eu.rezolve.com/api/v2/authentication/register/guest
-H content-type: application/json
-H authorization: Bearer signed-jwt
-d {
    "phone": phone,
    "entityId": entityId,
    "email": email
}

Responses:

201 OK
{
  "partner_id": "4",
  "entity_id": "d89d-d34fd-fddf45g8xc7-x8c7fddg"
}