Introduction
Note:
Code samples for IOS and Android will appear in this column.
You can switch between IOS and Android by clicking the tabs above.
The Rezolve InsideTM SDK is a software development kit that enables mobile app developers to integrate Rezolve’s mobile commerce and engagement capabilities into their new or existing mobile apps.
The Rezolve SDK communicates with the Rezolve online product stack – a set of dashboards and services designed to provide retailers and brands with everything they need to promote and sell via Rezolve, and also provide consumers with a seamless mobile experience.
Reference App
A Rezolve Reference App is available. This app was designed to showcase the unique capabilities of the Rezolve system, and to demonstrate the consumer experience.
To get the app, go here: Rezolve Reference App
Download links, sample media, and instructions are all included.
Capabilities
The Rezolve InsideTM SDK is a full-featured application suite. Capabilities that can be integrated into your application include:
- Enabling Shoppable Ads - Shoppable Ads PDF
- Creating a browsable Mall - Mall PDF
- Supporting Pre-pay account Top Up - Top Up PDF
- Consumer management of topup devices
- Scanning of Rezolve Encoded visual media
- Scanning of Rezolve Encoded audio media
- Consumer account creation, consumer profile, and purchase history
- Wallet management
Intended audience
This document is intended for experienced IOS and Android developer. It is expected that you have built apps before, and know your way around an IDE.
Term Definitions
term | definition |
---|---|
Partner | The Partner is the owner of a mobile application and audience. It is the Partner who wishes to integrate the Rezolve InsideTM SDK capabilities with their app. |
Developer | The Developer is the mobile app developer of the Partner. |
Merchant | A merchant runs an ecommerce site that offers products for sale. Merchants also create Shoppable Ads and link them to products. A merchant may also offer device accounts that need Top Up. |
Consumer | The end user of the Partner's mobile app. The customer who buys merchant products. |
SDK | Refers to the Rezolve InsideTM SDK, unless otherwise specified. |
For more information
For more information on Rezolve, see http://rezolve.com.
Getting Started
Supported OS Versions
OS | Minimum Version |
---|---|
IOS | IOS 9 and up SWIFT 4 (recommended) or 3 |
Android | 4.2.2 and up |
Implement a Crash Reporting Sytem
To provide quality support, we require quality information.
Rezolve advises using Fabric Crashlytics to capture app problems on both IOS and Android.
We will not provide development support if you have not integrated Crashlytics or a similar tool.
Download the Rezolve InsideTM SDK & Get an API Key
Request API Key
Latest release versions:
- Android: 3.1.0
Compatible Android Scan version: 3.0.1
IOS: 2.0.4.1
If this is your first time downloading the SDK, you will be provided with an API Key and the required environment information to begin development.
Set up the SDK - IOS
The target IDE for IOS instructions is XCode. If you use a different IDE you may have to follow a different series of steps, please refer to your IDE documentation to understand how to incorporate third party SDKs into your IDE.
CocoaPods
The Rezolve IOS SDK is distributed through CocoaPods. In order to proceed with installing the SDK, as a prerequisite, first install the latest version of CocoaPods:
sudo gem install cocoapods
Install Rezolve SDK
The SDK can now be installed in your own project by adding the following in Podfile
:
platform :ios, '10.0'
use_modular_headers!
target 'Sample' do
pod 'RezolveSDK'
end
Keep in mind that Sample is just a placeholder, and should be substituted by your own App Target name. Now just run the following command on a Terminal window pointing to the directory of your Podfile:
pod install
Don’t forget to use the .xcworkspace
file to open your project in Xcode, instead of the .xcodeproj
file, from here on out.
Updating the SDK
Every new release of the RezolveSDK can be updated by typing
pod update ResolveSDK
Set up the SDK - Android
The target IDE for Android instructions is Android Studio. If you use a different IDE you may have to follow a different series of steps, please refer to your IDE documentation to understand how to incorporate third party SDKs into your IDE.
The Android SDK is distributed via the Rezolve Nexus/Maven repository.
Update root gradle.properties
// in /gradle.properties
REZOLVE_SDK_REPOSITORY_URL=https://nexus.rezo.lv/repository/maven-sdk-releases/
REZOLVE_SDK_REPOSITORY_USERNAME=PUT_YOUR_USERNAME_HERE
REZOLVE_SDK_REPOSITORY_PASSWORD=PUT_YOUR_PASSWORD_HERE
Update the gradle.properties file in the root folder of your project and set variables for the repository url, username and repository password. Your Nexus username and password will be assigned to you when you receive your API Key.
Update root build.gradle to add repository
// in /build.gradle
allprojects {
repositories {
mavenLocal()
maven {
url REZOLVE_SDK_REPOSITORY_URL
credentials {
username REZOLVE_SDK_REPOSITORY_USERNAME
password REZOLVE_SDK_REPOSITORY_PASSWORD
}
}
google()
jcenter()
}
}
Add the Rezolve repository to your root build.gradle file, in the allprojects
block.
Update :app module build.gradle to add sdk dependency
// in /app/build.gradle
dependencies {
def rezolveSdkVersion = "3.0.0"
def rezolveSdkScanVersion = "3.0.0"
// ...
implementation "com.rezolve.sdk:core-android:$rezolveSdkVersion"
implementation "com.rezolve.sdk:payment-android:$rezolveSdkVersion"
implementation "com.rezolve.sdk:ssp-android:$rezolveSdkVersion"
implementation "com.rezolve.sdk:ssp-google-geofence:$rezolveSdkVersion"
implementation "com.rezolve.sdk:resolver:$rezolveSdkVersion"
implementation "com.rezolve.sdk:scan-android:$rezolveSdkScanVersion"
// ...
}
Add Rezolve SDK dependency to build.gradle file in your :app module. Set the rezolveSdkVersion
and rezolveSdkScanVersion
to the version you wish to use. Note it is important to use double quotes as shown around the implementation strings, or $rezolveSdkVersion
won't be evaluated. To upgrade to a new release, update rezolveSdkVersion
and rezolveSdkScanVersion
to the latest version.
Permissions
// Note: the SDK's manifest requests the following permissions:
// On Android 6.0+, you will have to specifically request the last two.
// See https://developer.android.com/training/permissions/requesting.html
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// RECORD_AUDIO permission is only needed if you implement the Audio Scan feature.
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// CAMERA permission is only needed if you implement the Image Scan feature.
<uses-permission android:name="android.permission.CAMERA" />
// FOREGROUND_SERVICE permission is only needed if you implement the Background Listening feature.
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
// Location permissions are only needed if you implement the Geofence detection feature.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
SDK Feature Use
This section describes the usage of the SDK to build specific feature-related functionalities.
Android-specific instructions on Managers
// THE FOLLOWING TWO METHODS ARE EQUIVALENT, but Interface saves effort
//
// Using MANAGER, you must handle the response in a WalletCallback:
//
public class MyActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
...
rezolveSDK = new RezolveSDK.Builder()
.setApiKey(API_KEY)
.setEnv(ENVIRONMENT)
.setAuthRequestProvider(new PartnerAuthRequestProvider(AuthService.getInstance()))
.build();
rezolveSDK.setAuthToken(accessToken);
rezolveSDK.createSession( accessToken, entityId, partnerId, new RezolveInterface() {
@Override
public void onInitializationSuccess(RezolveSession rezolveSession, String
entityId, String partnerId) {
rezolveSession.getWalletManager().getAll(new WalletCallback() {
@Override
public void onWalletGetAllSuccess(List<PaymentCard> list) {
// handle getAll response here
for(PaymentCard paymentCard : list) {
String cardId = paymentCard.getId();
String expiresOn = paymentCard.getExpiresOn();
// ...etc
}
}
});
}
});
}
}
//
// Using INTERFACE, you can save some development time
//
public class MyActivity extends AppCompatActivity implements WalletInterface {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
// .getRezolveSession gets an already created session
RezolveSDK.getInstance().getRezolveSession()
.getWalletManager().getAll(this);
}
@Override
public void onWalletGetAllSuccess(List<PaymentCard> list) {
// handle getAll response here
for(PaymentCard paymentCard : list) {
String cardId = paymentCard.getId();
String expiresOn = paymentCard.getExpiresOn();
// ...etc
}
}
}
In the Android version of the Rezolve InsideTM SDK, each FeatureManager has an equivalent FeatureInterface.
For example, to use WalletManager
methods, you can either do so directly, or implement WalletInterface
in your current activity for convenience. See Android code at right for comparison.
The Module Reference section exclusively documents using Feature Interfaces, as this is the preferred method.
Automatic thumbnail generation
Example JSON for 1 large image, and 4 associated "thumbnail" images
"image": "https:\/\/s3.amazonaws.com\/1\/27\/Image.png",
"image_thumbs": [
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_400x550.png",
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_800x1100.png",
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_1200x1651.png",
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_1600x2201.png"
],
When large images are uploaded to the merchant portal, the portal automatically generates a series of smaller images, which are referred to as thumbnails even though some are fairly large. The rules for thumbnail generation are as follows:
- Generated images respect the aspect ratio of the original image; their height/width ratio will not be distorted
- If the uploaded image is:
- smaller or equal to 400px maximum dimension (width or height), no resized image is produced.
- larger than 400px maximum dimension, it makes a 400px max dimension image. (referred to as the "thumbnail" image)
- larger than 800px maximum dimension, it makes a 800px max dimension image, and all smaller sizes.
- larger than 1200px maximum dimension, it makes a 1200px max dimension image, and all smaller sizes.
- larger than 1600px maximum dimension, it makes a 1600px max dimension image, and all smaller sizes.
- Additionally, the original upload image always returned, even if gigantic. Consider if you need images larger than 1600x1600 in your app.
- This applies to all merchant uploaded images that are supplied to the SDK.
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:
Utilize an existing authentication server, and adapt it to serve the JWT required by Rezolve. This makes most sense if you already have a community of users with logins.
Utilize Rezolve User Authentication (RUA), our implementation of a JWT-aware auth system. If you do not have an existing auth system, adopting RUA may save development time, as opposed to implementing a new system from the ground up. Your Rezolve sales representative can help you with this decision.
The primary task in implementing user management is to create the JWT that will allow you to bind a Partner user to a Rezolve profile. Through a server-to-server transaction, you can create a new Rezolve User Profile, and get back an identifying entityId
that you should persist in your authentication database, as an addition to your user record. This entityId
will then be used by the Rezolve SDK to identify the user to the Rezolve Server.
A valid JWT is required to initialize the SDK and start a session.
Minimum Implementation
Topics
- 1. Authentication
- 2. Profile Building Blocks
- 3. The Scan Interface
- 4. Checkout and Payment
- Conclusion
As a developer, you may be thinking "There is a lot of documentation here. What is the minimum feature set I need to implement to make a technical demo?"
It depends on what you want to demonstrate, but the typical demonstration of Rezolve's offering looks like:
Scan Engagement >>> View Product >>> Instant Buy
This section walks you through the minimum implementation required to build a limited, functional, end-to-end demo. Several shortcuts are identified. These shortcuts are not appropriate for an app processing real transactions, but are accessible for a technical demonstration on the Sandbox server. The shortcuts guide you to create a demo with limited UI elements, and only a single user. You are of course free to implement as many of the features as you wish to demonstrate.
1. Authentication
Two things must be in place before the SDK can communicate with the server
- You must have an
API Key
andPartner Id
, and be must talking to the correctEnvironment
. This information was provided to you when you signed up for API Access. - You must have a valid
JSON Web Token
, orJWT
. AJWT Secret
was provided when you signed up for API Access, and is used to create theJWT
.
Examples of both authentication measures are shown under JWT Authentication .
Be aware this is UNSAFE for production, or even pilot use. For any rollout involving real consumers and real purchases, you must do the auth server integration, so JWT tokens are tied properly to login status for a single user, and tokens can be renewed and expired.
2. Profile Building Blocks
The end consumer is represented by a Profile. Attached to this profile are one or more addresses, one or more phone numbers, and one or more payment devices. To create these objects, you must implement:
ConsumerProfileManager
- and create a profile recordAddressbookManager
- and create an address recordPhonebookManager
- and create a phone record.WalletManager
- and add a credit card. (we recommend using VISA4111 1111 1111 1111
for testing)
During the scan and purchase flow, ids for profile, address, phone, and payment device will be needed. You will get these by querying the above managers to get to get the ids of the records you created.
See Consumer Profile Management.
3. The Scan Interface
You must implement ScanManager
, and all it's methods.
You will also need ProductManager
, to get and parse the retrieved product.
See the example at Product Scan, Instant Buy Flow.
4. Checkout and Payment
Checkout and payment rely upon the following managers.
PaymentOptionsManager
CheckoutManagerV2
You must at least stub all the methods of these managers. But you may notice there are some methods specific to cart, and some specific to product. For instant buy, you only need to fully implement the PRODUCT methods.
See the example at Product Scan, Instant Buy Flow.
Conclusion
The above steps outline the absolute shortest route to a fully functional tech demonstration. Please follow them, and embrace the shortcuts provided, to create your tech demo in the minimal time.
But be aware that the Minimum Implementation is suitable for a Technical Demo only. Any application that will be given to consumers and used to make real purchases must fully implement JWT Auth, a proper Consumer UI, and included features omitted from this section, like Mall, Shopping Cart, Multi-Cart, and more.
The Rezolve Reference App is a great example of a full-featured implementation. If haven't used it yet, request access and give it a try.
JWT Authentication
Topics
- Terminology
- JWT Flow
- Create the Registration JWT
- Register a new Rezolve User
- Logging in a User
- Handling JWT Expiration & Session Preservation
- HTTP Error Responses
As of release 1.6.0, the Rezolve SDK no longer includes an authentication system. Resultingly, the AuthenticationManager.register
and AuthenticationManager.logout
methods have been deprecated.
Instead, Rezolve is utilizing a server-to-server JWT authentication system, conformant with the https://tools.ietf.org/html/rfc7519 standard. If you are not familar with JSON Web Tokens, the site https://jwt.io/ provides an excellent primer on the use of JWTs, as well as links to various JWT libraries you can utilize.
Rezolve expects the primary authentication of users will happen outside the SDK. Thus, exactly how you implement authentication in your app will depend on your existing auth system. In the server-to-server realm, however, there is only one instance in which your authentication server must interact with the Rezolve server.
After that, whether or not the SDK can talk to Rezolve depends on supplying a valid JWT to the SDK from your auth system.
When the Partner creates a new user on their auth server, or wishes to associate an existing user with Rezolve for the first time, the partner must generate the Registration JWT, and then POST it to the /api/v1/authentication/register
endpoint. The Rezolve server will validate the JWT, create a new user, create the user's public/private key pair, and return to you the Rezolve EntityId.
When a user logs in to your auth system, generate a new Login JWT and supply to CreateSession in the SDK. As long as the JWT is valid, the SDK can talk to Rezolve. A method is suggested below for smoothly handling JWT timeouts without interrupting the SDK session.
Terminology
Term | Definition |
---|---|
partner_id | A numerical id you are assigned by Rezolve. Usually a 2-4 digit integer. |
partner_api_key | The API key you are assigned by Rezolve. 36 characters including dashes. |
partner_auth_key | The Auth key you are assigned by Rezolve. This plays the role of the JWT Secret. The partner_auth_key is typically a ~90 character hash. |
JWT token | A JSON Web Token, consisting of a header, payload, and signature. The header and signature are signed with the parther_auth_key, above. It is used as a bearer token when communicating with the Rezolve server. |
accessToken | In the IOS and Android code samples, the accessToken is the JWT Token you generated. |
deviceId | An id that is randomly generated upon app install and stored. This id is placed in both the JWT payload and x-header sent by the SDK. The Rezolve server checks that these values match to deter request origin spoofing. All calls except Registration calls require this. |
JWT Flow
[ View full size ]
Create the Registration JWT
Requirements
You must possess:
field | description | example |
---|---|---|
partner_id | The numerical id you are assigned by Rezolve | 317 |
partner_api_key | The API key you are assigned by Rezolve | a1b2c3d4-e5f6-g7h8-i9j0-a1b2c3d4e5f6 |
partner_auth_key | The Auth key you are assigned by Rezolve | qwer+4ty ... JYG6XavJg== (approx 90 characters) |
JWT Header
{
"alg": "HS512",
"typ": "JWT"
}
key | value | notes |
---|---|---|
alg | HS512 | algorithm, HMAC SHA-512 |
typ | JWT | type |
JWT Payload
{
"rezolve_entity_id": ":NONE:",
"partner_entity_id": "partner_entity_id",
"exp": 1520869470
}
key | value | notes |
---|---|---|
rezolve_entity_id | :NONE: | use :NONE: when registering |
partner_entity_id | your_user_id | The unique identifier for your user record. This may be a numerical id, or an identifying string such as email address. |
exp | 1520869470 | Expiration, as a unix timestamp integer. Set the expiration value to a small number, now() + 30 minutes or less. |
Signature
HMACSHA512(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
${$partner_auth_key}
)
Sign the header and payload with the partner_auth_key
. It is not necessary to decode the key before using it. Pass the whole value as a string to your library's getBytes
method.
You may need to specify the charset as UTF8.
Example 1, Microsoft: SecretKey(Encoding.UTF8.GetBytes(key));
Example 2, Java: Secret.getBytes("UTF-8");
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.
eyJyZXpvbHZlX2VudGl0eV9pZCI6IjpOT05FOiIsInBhcnRuZXJfZW50aXR5X2lkIjoiMTIzIiwiZXhwIjoxNTIwODY5NDcwfQ.
5y2e6QpUKcqTNLTv75nO6a6iFPVxrF8YeAH5NTg2ZO9dkub31GEs0N46Hu2IJf1bQ_vC2IOO9Z2N7drmvA_AKg
The resulting JWT will look something like this (except without linebreaks); the first third is the header, the second third the payload, and the last third the signature:
Register a new Rezolve User
POST: https://sandbox-api-tw.rzlvtest.co/api/v1/authentication/register
-H content-type: application/json
-H x-rezolve-partner-apikey: your-api-key
-H authorization: Bearer signed-jwt
-d {
"email": "user@example.com"
}
After the JWT is created, POST it to the Rezolve registration endpoint. This will create a Rezolve User, generate a public/private keypair for the user, and return to you the corresponding Entity Id.
Example, using Sandbox endpoint:
{
"entity_id" : "entity123",
"partner_id" : "3"
}
The endpoint will reply with an entity id and the partner id. You should save the Rezolve Entity Id to your authentication database and associate it with the user's record.
Logging in a User
Once a Rezolve User has been registered and an entity_id
obtained, you can log in the user using the instructions below.
For returning users, log them in via your normal method in your auth server, and then follow the instructions below.
Create a new Login JWT
, and use it as the accessToken
in the createSession
method.
JWT Header
Note the addition of the "auth" line.
{
"auth": "v2",
"alg": "HS512",
"typ": "JWT"
}
key | value | notes |
---|---|---|
auth | v2 | auth version to use, login uses v2 |
alg | HS512 | algorithm, HMAC SHA-512 |
typ | JWT | type |
JWT Payload
Note the addition of the device_id
.
{
"rezolve_entity_id": "entity123",
"partner_entity_id": "partner_entity_id",
"exp": 1520869470,
"device_id": "wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a"
}
key | value | notes |
---|---|---|
rezolve_entity_id | your_rezolve_entity_id | use the entity_id you obtained during registration |
partner_entity_id | your_partner_entity_id | set it to the unique identifier for your user record |
exp | 1520869470 | Expiration, as a unix timestamp integer. Set the expiration value to a small number, now() + 30 minutes or less. |
device_id | wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a | An id randomly generated upon app installation and stored. This id is placed in both the JWT payload and x-header sent by the SDK. See below for generation instructions. |
Signature
HMACSHA512(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
${$partner_auth_key}
)
Sign the header and payload with the partner_auth_key
.
Generating the device_id
// iOS uses the iOS "identifierForVendor" string, so there is no need to generate it.
// The iOS SDK pulls this value automatically using `UIDevice.current.identifierForVendor?.uuidString` and supplies it to the SDK for the x-header.
// You will need to use this same call to supply the device_id to your auth server for storage with the user profile.
UIDevice.current.identifierForVendor?.uuidString
// generate the random id
String deviceId = UUID.randomUUID().toString();
// store the device_id
private static void writeDeviceIdFile(File deviceidfile) throws IOException {
FileOutputStream out = new FileOutputStream(deviceidfile);
String id = UUID.randomUUID().toString();
out.write(id.getBytes());
out.close();
}
// read the stored device_id
private static String readDeviceIdFile(File deviceidfile) throws IOException {
RandomAccessFile f = new RandomAccessFile(deviceidfile, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
return new String(bytes);
}
// supply the random id to the SDK
RezolveSDK.setDeviceIdHeader(deviceId);
To generate and set the device_id, see the OS specific examples to the right. On IOS it is handled automatically using identifierForVendor?.uuidString
. Android must manually generate, store, read, and pass the device_id
to the SDK.
Create the Session
In the samples to the right, accessToken
is the Login JWT token you created above.
Note in the Android sample you are also passing in an AuthRequestProvider. This is used for handling expiring JWT sessions, and is explained in the next section.
import UIKit
import RezolveSDK
class SandboxViewController: UIViewController {
private let API_KEY: String = "your_api_key"
private let API_ENVIRONMENT: RezolveEnv = .sandbox
private let accessToken: String = "abc123.abc123.abc123"
private let entityId: String = "123"
private var partnerId: String = "123"
override func viewDidLoad() {
super.viewDidLoad()
let sdk = Rezolve(apiKey: API_KEY,
partnerId: partnerId,
subPartnerId: nil,
environment: API_ENVIRONMENT,
config: nil,
sspActManagerSettings: nil,
coordinatesConverter: .default)
sdk.createSession(accessToken: accessToken, username: "", entityId: entityId, partnerId: partnerId) { (session, error)
// your rezolve SDK logic here
}
}
}
String API_KEY = "your_api_key";
String ENVIRONMENT = "https://sandbox-api-tw.rzlvtest.co/api";
String accessToken = "abc123.abc123.abc123"; // JWT token from auth server
String entityId = "123"; // from auth server
String partnerId = "123"; // from auth server
String deviceId = "wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a"; // from stored device_id, see "Generating the device_id" above
// Use builder to create instance of SDK and set SDK Params
// Pass in an AuthRequestProvider here, to handle expiring JWT tokens
rezolveSDK = new RezolveSDK.Builder()
.setApiKey(API_KEY)
.setEnv(ENVIRONMENT)
.setAuthRequestProvider(new PartnerAuthRequestProvider(AuthService.getInstance()))
.build();
// Set JWT Auth Token from partner auth server
rezolveSDK.setAuthToken(accessToken);
// Start session, again supplying JWT auth token
rezolveSDK.createSession( accessToken, entityId, partnerId, new RezolveInterface() {
@Override
public void onInitializationSuccess(RezolveSession rezolveSession, String entityId, String partnerId) {
// set device_id so it can be passed in x-header
RezolveSDK.setDeviceIdHeader(deviceId);
// use created session to access managers. Example...
rezolveSession.getAddressbookManager().get(...);
}
@Override
public void onInitializationFailure() {
// handle error
}
});
Handling JWT Expiration & Session Preservation
import RezolveSDK
import Foundation
internal enum Result<T> {
case success(T)
case failure(HttpResponse)
}
internal protocol TokenRenewProtocol {
func renewToken(result: @escaping (Result<RezolveSession>) -> Void)
}
internal class AuthService: NSObject, TokenRenewProtocol {
func renewToken(result: @escaping (Result<RezolveSession, AuthenticationError>) -> Void) {
let urlString = ""
let endpoint = "/api/v1/authentication/register"
let data = ["username": "user@email.com", "password": "password"]
guard let url = URL(string: urlString).appendPathComponent(endpoint),
let request = URLRequest(url: url) else {
preconditionFailure("Url not created, verify check path and host")
}
let device: DeviceProfile = ... // The device profile
let entityId: String = ... // Entity Id
let partnerId: String = ... // Partner Id
let dictionary: [String: String] = [
"device_id": device.deviceId,
"make": device.make,
"os_type": device.osType,
"os_version": device.osVersion,
"locale": device.locale
"entityId": entityId,
"partnerId": partnerId
]
guard let body = try JSONSerialization.data(withJSONObject: dictionary, options: []) else {
preconditionFailure("Payload convertion to Data failed")
}
request.httpMethod = "POST"
request.httpBody = body
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) { data, response, error in
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
if let responseData = data,
let jsonDictionary = try? JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] {
let accessToken = jsonDictionary["access_token"]
let publicKey = jsonDictionary["public_key"]
// Securily store received keys
// Create new session with received accessToken
self.rezolveSdk?.createSession(
accessToken: accessToken,
entityId: entityId,
partnerId: partnerId,
callback: { rezolveSession in
// Store new token
// Proccess callback
callback(.success(rezolveSession))
}, errorCallback: { error in
// Error handling
// Proccess callback
callback(.failure(error))
})
}
else {
// Handle parsing error
}
}
else {
// Handle error
}
}
}
}
internal final class AddressBookViewController: UIViewController {
fileprivate(set) var authService: (NSObjectProtocol & TokenRenewProtocol)?
fileprivate func attemptToGetAddressBook(session: RezolveSession, retryOnFail: Bool) {
session.addressbookManager.getAll(callback: { addressList in
// Handle addressList
}, errorCallback: { httpResponse in
if retryOnFail && httpResponse.statusCode == 401 {
self.authService?.renewToken { result in
swith result {
case let .success(newSession):
// Retries to fetch data from AddressBookManager using the new RezolveSession
self.attemptToGetAddressBook(session: newSession, retryOnFail: false)
case let .failure(error):
// Handle Token Renewal failure
debugPrint(error)
}
}
}
})
}
}
// example Partner Auth Request Provider
// this would handle partner user login against partner server, password reset,
// as well as JWT token renewal
class PartnerAuthRequestProvider implements RezolveSDK.AuthRequestProvider {
private final AuthService authService;
RuaAuthRequestProvider(AuthService authService) {
this. authService = authService;
}
@Override
public RezolveSDK.GetAuthRequest getAuthRequest() {
if(Looper.myLooper() == Looper.getMainLooper()) {
throw new IllegalStateException("You can't run this method from main thread");
}
//set blocking call as the refresh token callback
final RefreshTokenCallbackToBlockingCall callback = new RefreshTokenCallbackToBlockingCall();
authService.refreshAuthToken(callback);
// ping the partner auth service
RezolveSDK.GetAuthRequest authRequest = PartnerPingCallbackToBlockingCall.getResult();
return authRequest;
}
}
class RefreshTokenCallbackToBlockingCall {
private RezolveSDK.GetAuthRequest result = null;
private final CountDownLatch countDownLatch = new CountDownLatch(1);
// on successful refresh, wait for the ping response
public void onRefreshAuthTokenSuccess(@NonNull String authToken) {
result = RezolveSDK.GetAuthRequest.authorizationHeader(authToken);
countDownLatch.countDown();
}
// getResult is only triggered after a result is received from the partner auth server
RezolveSDK.GetAuthRequest getResult() {
try {
countDownLatch.await();
return result;
} catch (InterruptedException e) {
// handle the exception
}
}
}
The Login JWT you generate is included in the headers of every SDK transmission. Thus, when your consumer logs out, you can expire the JWT, and the app will cease communication with the Rezolve server. To do this, create a new JWT with an expiration stamp in the past, and supply it to the SDK.
This also means you are required to handle JWT token expiration/renewal if you want a session to continue.
Examples are provided to the right. These are NOT an example of implementing SDK code, but rather an example of implementing session rewnewal with your own authentication server.
IOS
To handle session expiration smoothly, without interrupting the user session, Rezolve recommends creating a wrapper around any call that contacts the server. If the server returns an authorization error, you can call your token renewal endpoint and issue a new JWT.
The sample shows two classes, an AuthService
class and an AddressBookViewController
class.
The AuthService
class may, for example, process username/passwords for login, handle registering your users, and handle password resets. It should also handle the JSON Web Tokens to register, create session, and maintain session with Rezolve. The example method renewJwtToken
is provided as an example of how to renew a token.
The AddressBookViewController
is an example of how you would implement the AddressbookManager.getAll
method so as to smoothly handle token expiration and renewal to provide continuity of session.
- call the method
attemptToGetAddressBook
passing in a Rezolve session and theretryOnFail
boolean - the method calls the
addressbookManager.getAll
method - if the call succeeds, proceed as normal, displaying the
addressList
- if the call results in an error, check if the error is a 401.
- If the error is a 401, and the
retryOnFail
is true, request a new JWT, calling therenewJwtToken
method. Then retry the request. - If the error is 401, and
retryOnFail
is false, we know that we requested a new token and the token provided was expired (indicating end of login session at auth server). Do not retry. - If a different error occurs, log and handle the error.
Android
The SDK makes every call to the Rezolve server using an http client; if a call to the server results in a "401 token expired" response, the http client will ask for a new token using RezolveSDK.AuthRequestProvider
. The Partner Auth Service you passed in to the SDK Builder must handle this JWT renewal.
It should be noted that the Partner Auth Service will typically handle all partner auth needs. Duties may include processesing username/passwords for login, handling registering your users, and handling password resets, in addition to JWT renewal.
The code examples show one way of implementing JWT renewal.
In the class PartnerAuthRequestProvider
the Partner Auth Service implements RezolveSDK.AuthRequestProvider
, to handle the JWT renewal requirements of the SDK. If the http client receives a "401 token expired", it will call RezolveSDK.GetAuthRequest
to either confirm logout or renew the token. The token is renewed, but is only returned if the ping to the partner auth server to check login status suceededs. If the partner auth server says the user is not logged in, the renewed token is not returned, and the user can no longer make requests. If the user is still logged in, the updated JWT is returned.
HTTP Error Responses
403
{
"type": "Authentication"
"code": "1"
"message": "Access is denied."
}
For security reasons, Rezolve masks certain potential issues behind a generic 403 response:
- invalid partner api key
- partner doesn't have an auth key
- token not found
- invalid token signature
- token is missing required fields
- token belongs to another user
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:
- 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. - Favorites - Via the
FavouriteManager
. Reserved for future functionality, "Favorites" are collections of unique devices that can be topped up. A favorite 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
.
ProfileManager supports only update
and get
.
Detect SSP Engagements - Android
The Self-Serve Portal, or SSP, enables merchants to create Engagements. Engagements provide a way for the merchant to interact with consumers wherever they are - through print, audio/video, geolocation, and more.
Engagements are composed of a Trigger (specially customized media) linked to a Target (the content the merchant wants the consumer to see). Triggers include watermarked images, watermarked audio, geozones, QR Codes, and touch links. Targets include Information Pages, Acts, Products, Categories, and Urls.
This section describes how to capture information from the Trigger when it is detected, and resolve that into a Target.
1. Prepare gradle
Implement the SSP module by adding it to your gradle dependencies.
implementation "com.rezolve.sdk:ssp-android:3.1.0"
2. Initialize SspActManager
var authParams: AuthParams = AuthParams(
AUTH0_CLIENT_ID,
AUTH0_CLIENT_SECRET,
AUTH0_API_KEY,
AUTH0_AUDIENCE,
AUTH0_ENDPOINT,
SSP_ENGAGEMENT_ENDPOINT,
SSP_ACT_ENDPOINT
)
var httpConfig: HttpClientConfig = Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
var httpClientFactory: HttpClientFactory = Builder()
.setHttpClientConfig(httpConfig)
.setAuthParams(authParams)
.build()
var sspHttpClient: SspHttpClient = httpClientFactory.createHttpClient(SSP_ENDPOINT)
var sspActManager = SspActManager(sspHttpClient)
3. Set up the resolver configuration
The resolver is what decodes the information in the Trigger and looks up the Target
ResolverConfiguration.Builder(rezolveSDK)
.enableBarcode1dResolver(true)
.enableCoreResolver(true)
.enableSspResolver(sspActManager, desiredImageWidth)
.build(this);
4. Create a scan view.
Note: an example of Scan View can be seen in the Rezolve Sample App code, ScanActivity.java
public class ScanActivity extends AppCompatActivity {
private static final String TAG = ScanActivity.class.getSimpleName();
private VideoScanManager videoScanManager = VideoScanManagerProvider.getVideoScanManager();
private AudioScanManager audioScanManager = AudioScanManagerProvider.getAudioScanManager();
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
}
@Override
protected void onDestroy() {
// ...
}
@Override
protected void onResume() {
// ...
}
@Override
protected void onPause() {
// ...
}
// ...
}
5. Register listeners for scan results:
When the phone detects trigger media, the listener will pick up the result and deduce what the target is. You must provide handling for all target types. Handling targets is covered in the next section.
ResolverResultListenersRegistry.getInstance().add(resolveResultListener)
private val resolveResultListener: ResolveResultListener = object : ResolveResultListener {
override fun onProcessingStarted(uuid: UUID) {
// ...
}
override fun onProcessingFinished(uuid: UUID) {
// ...
}
override fun onProcessingUrlTriggerStarted(uuid: UUID, urlTrigger: UrlTrigger) {
solveRezolveTrigger(urlTrigger.url)
}
override fun onContentResult(uuid: UUID, result: ContentResult) {
if (result is ProductResult) {
onProductResult(result.product, result.categoryId) // display product (old method)
} else if (result is CategoryResult) {
onCategoryResult(result.category, result.merchantId) // display category (old method)
} else if (result is SspActResult) {
onSspActResult(result) // display SSP Act or info page - check below
} else if (result is SspProductResultSspProductResult) {
solveRezolveTrigger(result.rezolveTrigger) // display product (new SSP method)
} else if (result is SspCategoryResult) {
solveRezolveTrigger(result.rezolveTrigger) // display category (new SSP method)
}
}
override fun onResolverError(uuid: UUID, error: ResolverError) {
onScanError(error.rezolveError.errorType, error.message)
}
}
private fun solveRezolveTrigger(url: String) {
triggerManager.resolveTrigger(url, object : TriggerCallback() {
override fun onCategoryResult(category: Category, merchantId: String) {
onCategoryResult(category, merchantId) // display category (old method)
}
override fun onBadTrigger() {
// ...
}
override fun onProductResult(product: Product, categoryId: String) {
onProductResult(product, categoryId) // display product (old method)
}
override fun onScanError(rezolveError: RezolveError, scannedData: ScannedData?) {
onScanError(rezolveError.errorType, rezolveError.message)
}
})
}
Handling SSP Act Targets - Android
Acts and Information Pages are closely related. An information page simply displays the merchant's message to the consumer. An Act is like a info page that includes a form the user must fill out.
Before displaying sspActResult
, check the value of the field sspActResult.sspAct.pageBuildingBlocks
. If it is not null and is not empty, it means that engagement owner has designed custom layout to present it. Here is how to handle it.
1. Create helper data class that holds both block and answer (if applicable):
data class BlockWrapper(
val pageBuildingBlock: PageBuildingBlock,
var answerToDisplay: String? = null
)
2. Create an adapter class to show the custom layout.
class SspActBlockAdapter : ListAdapter<BlockWrapper, SspActBlockAdapter.ViewHolder>(BlockWrapperItemDiff) {
private lateinit var layoutInflater: LayoutInflater
lateinit var eventListener: SspActBlockEventListener
private val blockSelectListener = object : SspBlockSelectClickListener {
override fun onOptionSelected(blockWrapper: BlockWrapper, option: SelectionOption) {
eventListener.onSelectBlockOptionSelected(blockWrapper, option)
}
}
private val blockDateListener = object : SspBlockDateFieldListener {
override fun onDateBlockClick(blockWrapper: BlockWrapper) {
eventListener.onDateBlockSelected(blockWrapper)
}
}
private val blockTextInputListener = object : SspBlockTextInputListner {
override fun onTextInputChanged(blockWrapper: BlockWrapper, text: String) {
eventListener.onTextInputBlockChange(blockWrapper, text)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
if(!::layoutInflater.isInitialized) {
layoutInflater = LayoutInflater.from(parent.context)
}
return when(viewType) {
VIEW_TYPE_HEADER -> HeaderViewHolder(ItemSspBlockHeaderBinding.inflate(layoutInflater, parent, false))
VIEW_TYPE_PARAGRAPH -> ParagraphViewHolder(ItemSspBlockParagraphBinding.inflate(layoutInflater, parent, false))
VIEW_TYPE_DIVIDER -> DividerViewHolder(ItemSspBlockDividerBinding.inflate(layoutInflater, parent, false))
VIEW_TYPE_IMAGE -> ImageViewHolder(ItemSspBlockImageBinding.inflate(layoutInflater, parent, false))
VIEW_TYPE_VIDEO -> VideoViewHolder(ItemSspBlockVideoBinding.inflate(layoutInflater, parent, false))
VIEW_TYPE_DATE_FIELD -> DateFieldViewHolder(ItemSspBlockDateFieldBinding.inflate(layoutInflater, parent, false), blockDateListener)
VIEW_TYPE_SELECT -> SelectViewHolder(ItemSspBlockSelectBinding.inflate(layoutInflater, parent, false), blockSelectListener)
VIEW_TYPE_TEXT_INPUT -> TextFieldViewHolder(ItemSspBlockTextInputBinding.inflate(layoutInflater, parent, false), blockTextInputListener)
else -> throw IllegalArgumentException("Invalid viewType: $viewType")
}
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
viewHolder.bind(getItem(position), position)
}
override fun getItemViewType(position: Int): Int {
return when(getItem(position).pageBuildingBlock.type) {
Type.HEADER -> VIEW_TYPE_HEADER
Type.PARAGRAPH -> VIEW_TYPE_PARAGRAPH
Type.DIVIDER -> VIEW_TYPE_DIVIDER
Type.IMAGE -> VIEW_TYPE_IMAGE
Type.VIDEO -> VIEW_TYPE_VIDEO
Type.DATE_FIELD -> VIEW_TYPE_DATE_FIELD
Type.SELECT -> VIEW_TYPE_SELECT
Type.TEXT_FIELD, Type.UNKNOWN -> VIEW_TYPE_TEXT_INPUT
}
}
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
protected val context: Context = itemView.context
abstract fun bind(item: BlockWrapper, position: Int)
}
class HeaderViewHolder(private val binding : ItemSspBlockHeaderBinding) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) {
binding.blockWrapper = item
}
}
class ParagraphViewHolder(private val binding : ItemSspBlockParagraphBinding) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) {
binding.blockWrapper = item
}
}
class DividerViewHolder(private val binding : ItemSspBlockDividerBinding) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) { }
}
class ImageViewHolder(private val binding : ItemSspBlockImageBinding) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) {
binding.blockWrapper = item
}
}
class VideoViewHolder(private val binding : ItemSspBlockVideoBinding) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) {
binding.blockWrapper = item
}
}
class DateFieldViewHolder(private val binding : ItemSspBlockDateFieldBinding, private val listener: SspBlockDateFieldListener) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) {
binding.blockWrapper = item
binding.listener = listener
}
}
class SelectViewHolder(private val binding : ItemSspBlockSelectBinding, private val listener: SspBlockSelectClickListener) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) {
binding.blockWrapper = item
binding.listener = listener
}
}
class TextFieldViewHolder(private val binding : ItemSspBlockTextInputBinding, private val listener: SspBlockTextInputListner) : ViewHolder(binding.root) {
override fun bind(item: BlockWrapper, position: Int) {
binding.blockWrapper = item
binding.listener = listener
}
}
interface SspBlockDateFieldListener {
fun onDateBlockClick(blockWrapper: BlockWrapper)
}
interface SspBlockSelectClickListener {
fun onOptionSelected(blockWrapper: BlockWrapper, option: SelectionOption)
}
interface SspBlockTextInputListner {
fun onTextInputChanged(blockWrapper: BlockWrapper, text: String)
}
}
object BlockWrapperItemDiff : DiffUtil.ItemCallback<BlockWrapper>() {
override fun areItemsTheSame(p0: BlockWrapper, p1: BlockWrapper): Boolean = p0.pageBuildingBlock.id == p1.pageBuildingBlock.id
override fun areContentsTheSame(p0: BlockWrapper, p1: BlockWrapper): Boolean = p0 == p1
}
3. List blocks
private lateinit var blockAdapter: SspActBlockAdapter
private var pageBuildingRecycler: RecyclerView? = null
pageBuildingRecycler = binding.pageBuildingRecycler
binding.pageBuildingRecycler.adapter = blockAdapter
pageBuildingRecycler?.setItemViewCacheSize(pageBuildingBlocks.size)
blockAdapter.submitList(pageBuildingBlocks)
In your fragment or activity class submit list of blocks to the adapter:
4. Create layouts to present different types of blocks:
// Date field
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="blockWrapper"
type="com.rezolve.demo.content.sspact.BlockWrapper" />
<variable
name="listener"
type="com.rezolve.demo.content.sspact.SspActBlockAdapter.SspBlockDateFieldListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> listener.onDateBlockClick(blockWrapper)}"
android:layout_margin="@dimen/ssp_block_paragraph_padding">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/ssp_block_date_button"
style="@style/SspBlockBaseStyle.Title"
android:text="@{blockWrapper.pageBuildingBlock.data.text.concat(blockWrapper.pageBuildingBlock.required ? @string/ssp_block_required_suffix : @string/empty_string)}"
android:textColor="@{blockWrapper.pageBuildingBlock.required && 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 && 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 && 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.:
SspActQuestionDate
-userAnswer
must be indd/mm/yyyy hh:mm
format.SspActQuestionDropdown
-userAnswer
must equal id of one of providedSspActQuestionValue(s)
.SspActQuestionField
- no validation
You can create an answer for each of these classes using their answer(@NonNull String userAnswer)
method.
6a. Build the Act submission
private fun assembleSspActSubmission(): SspActSubmission {
return SspActSubmission.Builder()
.setUserId(userId)
.setUserName(userName)
.setPhone(userPhone)
.setPersonTitle(userTitle)
.setFirstName(userFirstName)
.setLastName(userLastName)
.setEmail(userEmail)
.setServiceId(sspAct.serviceId)
.setAnswers(listOfSspActAnswers)
.setLocation(rezolveLocation)
.build()
}
When you have var listOfSspActAnswers: List<SspActAnswer>
you can build SspActSubmission
class using the builder tool:
6b. Submit the Act answers
sspActManager.submitAnswer(
sspAct.id,
assembleSspActSubmission(),
object : SspSubmitActDataInterface {
override fun onSubmitActDataSuccess(actSubmissionResponse: SspActSubmissionResponse) {
// handle response
}
override fun onError(error: RezolveError) {
// handle error
}
}
)
Next, you can submit your answer:
Look Up An Act By Id - Android
SspActManager.getResolveResponseFromEngagementId(int imageWidth,
@NonNull String engagementId,
@NonNull final SspFromEngagementInterface sspFromEngagementInterface)
If you have an Act Id, you can retrieve it using the example code to the right.
Handling SSP Product Targets - Android
((SspProductResult)result).getSspProduct().getRezolveTrigger());
When a trigger is resolved (see Step 5 of How To Detect SSP Engagements - Android), one of the possible result ContentResult
types is SspProductResult
. To retrieve the product, extract a RezolveTrigger
using the sample code shown. You can then use TriggerManager to retrieve the product. See TriggerManager.
Handling SSP Category Targets - Android
((SspCategoryResult)result).getSspCategory().getRezolveTrigger());
When a trigger is resolved (see Step 5 of How To Detect SSP Engagements - Android), one of the possible result ContentResult
types is SspCategoryResult
. To retrieve the category, extract a RezolveTrigger
using the sample code shown. You can then use TriggerManager to retrieve the category. See TriggerManager.
Handling SSP URL Targets - Android
if (TriggerManager.isRezolveTrigger(url)) {
// handle url trigger
} else {
// handle custom url
}
The last type of target you need to handle is a custom URL. Custom URLs can be associated with Products or Categories. To detect a custom URL, add a simple if/else clause that checks if the URL you receive is a RezolveTrigger. If not, then it is a custom URL, and you should handle it as you see fit. The typical behavior is to redirect the user to the URL, but the use case is up to you.
Detect SSP Engagements - IOS
The Self-Serve Portal, or SSP, enables merchants to create Engagements. Engagements provide a way for the merchant to interact with consumers wherever they are - through print, audio/video, geolocation, and more.
Engagements are composed of a Trigger (specially customized media) linked to a Target (the content the merchant wants the consumer to see). Triggers include watermarked images, watermarked audio, geozones, QR Codes, and touch links. Targets include Information Pages, Acts, Products, Categories, and Urls.
This section describes how to capture information from the Trigger when it is detected, and resolve that into a Target.
1. Initialize ScanManager
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
2. Set up a ProductDelegate
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
3. Add a function to handle SSP Engagements
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
// Functions from previous example hidden for brevity
func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
// Object `engagement` contains the basic structure of a SSP resolved engagement
let id = engagement.engagementId
let scanId = engagement.serviceId
let path = engagement.payloadPath
let payoffs = engagement.payoffs
let payload = engagement.rezolveCustomPayload
}
}
4. Inspect the payload
// Any of these can be `null`, but one should have content
let act = payload.act?.act // Information for linked Act
let product = payload.product // Information for linked Product
let category = payload.category // Information for linked Category
The non-null object item corresponds to the target set by the merchant. You must handle all target types. This will be covered in the following section.
Handling Act Targets - IOS
1. Identify the Act payload
let act = payload.act?.act // Information for linked Act
let product = payload.product // Information for linked Product
let category = payload.category // Information for linked Category
// Any of the above are not `null`, and a `customUrl` is detected, we need to redirect the user to this linked entity
let actCustomUrl = act?.customUrl
let productCustomUrl = product?.customUrl
let categoryCustomUrl = category?.customUrl
Acts and Information Pages are closely related. An information page simply displays the merchant's message to the consumer. An Act is like a info page that includes a form the user must fill out.
If payload.act?.act
is not null, and act?.customUrl
IS null, you have an Act.
2. Display the Act
let actId = act?.id
let actPageBuildingBlocks = act?.pageBuildingBlocks
let actIsInformationPage = act?.isInformationPage
let actQuestions = act?.questions
// Iterate through the Act Page Building Blocks and render on screen
for actPageBuildingBlock in actPageBuildingBlocks! {
print(actPageBuildingBlock)
}
// Iterate through the Act Questions and render on screen
for actQuestion in actQuestions! {
print(actQuestion)
}
A merchant creates an Act by adding display items (building blocks) and then adding form questions.
Iterate first through the building blocks, and then through the questions, rendering them on screen.
3. Submitting Act answers
let user: User // Current user
let location: CLLocation? // Current user location if available
let sspAct: SspAct // SSP Act to submit
let page: Page // User answers
func answer(element: Page.Element) -> SspSubmissionAnswer? {
switch element {
case .text, .divider, .image, .video:
return nil
case .dateField(let dateField):
guard let value = dateField.value else {
return nil
}
let date = // Map Date to string
return SspSubmissionAnswer(questionId: dateField.id, answer: date)
case .select(let select):
guard let value = select.value else {
return nil
}
return SspSubmissionAnswer(questionId: select.id, answer: String(value.value))
case .textField(let text):
guard let value = text.value else {
return nil
}
return SspSubmissionAnswer(questionId: text.id, answer: value)
}
}
// Map page inputs to submission answers
let answers = page.elements.compactMap(answer(element:))
The example to the right shows an easy way to map all user provided answers into a meaningful object.
3a. How to format the Act submission
// Creating Ssp Act Sumbission model
let submission = SspActSubmission(
userId: user.id,
userName: user.username,
personTitle: user.title,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
phone: user.phone,
scanId: sspAct.scanId,
latitude: location?.latitude,
longitude: location?.longitude,
answers: answers
)
Next you will need to create the basic model for submitting an Act. All of the previously declared details must be preset in the structure, like User, Act, Location, Answers.
3b. Do the submission
// Submission
sspActManager.submitAct(actId: sspAct.id, actSubmission: submission) { (result) in
switch result {
case .success(let submission):
// Act submitted
case .failure(let error):
// Handle error gracefully
}
}
Lastly, execute the final form of the object passed on one of the method’s parameters.
Look Up An Act By Id - IOS
rezolveSession?.sspActManager.getAct(actId: id) { (result) in
switch result {
case .success(let act):
if let pageBlocks = act.pageBuildingBlocks {
// pageBuildingBlocks can be used directly to render UI, or PageBuilder can be used to map blocks to more useful models (user input, validation)
do {
let page = try PageBuilder().build(from: pageBlocks)
// Use page model to render UI
} catch {
// Handle error gracefully
}
} else {
}
let isInformationPage = act.isInformationPage
// submission should not be available if isInformationPage == true
case .failure(let error):
// Handle error gracefully
}
}
If you have an Act Id, you can retrieve it using the example code to the right.
Handling SSP Product Targets - IOS
func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
let id = engagement.engagementId
let scanId = engagement.serviceId
let path = engagement.payloadPath
let payoffs = engagement.payoffs
let payload = engagement.rezolveCustomPayload
// The logic of parsing, is that you might find a `customUrl` in the structure not being `null`.
// Hence, we will need to examine it and redirect the user to the linked Web Page, Product or Category
if let act = payload.act?.act {
if let customUrl = act.customUrl {
// Parse act.customUrl
return
}
} else if let product = payload.product?.customUrl {
if let customUrl = product.customUrl {
// To Parse product.customUrl
// Check if url begins with "https://rzlv.co"
// Check if url path is in format "/[int]/[int]/[int]/[int]"
// If both the above are true, you have a valid RezolveTrigger. Use Trigger Manager to look up the product.
// If the url does not meet the above rules, it is not a RezolveTrigger, it is a regular url. Redirect the user to the url.
return
}
} else if let category = payload.category?.customUrl {
if let customUrl = category.customUrl {
// Parse category.customUrl
return
}
}
}
To handle a product result, get the product custom URL and test it.
If the url is a RezolveTrigger, similar in format to https://rzlv.co/1/2/3/4
, use Trigger Manager to fetch the product. See code sample for test advice.
If the url is NOT a RezolveTrigger, assume it is a regular url, and you should handle it as you see fit. The typical behavior is a simple redirect to the url, to be opened in a browser.
Handling SSP Category Targets - IOS
func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
let id = engagement.engagementId
let scanId = engagement.serviceId
let path = engagement.payloadPath
let payoffs = engagement.payoffs
let payload = engagement.rezolveCustomPayload
// The logic of parsing, is that you might find a `customUrl` in the structure not being `null`.
// Hence, we will need to examine it and redirect the user to the linked Web Page, Product or Category
if let act = payload.act?.act {
if let customUrl = act.customUrl {
// Parse act.customUrl
return
}
} else if let product = payload.product?.customUrl {
if let customUrl = product.customUrl {
// Parse product.customUrl
return
}
} else if let category = payload.category?.customUrl {
if let customUrl = category.customUrl {
// To Parse category.customUrl
// Check if url begins with "https://rzlv.co"
// Check if url path is in format "/[int]/[int]/[int]" (note: one fewer parameter than a product)
// If both the above are true, you have a valid RezolveTrigger. Use Trigger Manager to look up the category.
// If the url does not meet the above rules, it is not a RezolveTrigger, it is a regular url. Redirect the user to the url.
return
}
}
}
To handle a category result, get the category custom URL and test it.
If the url is a RezolveTrigger, similar in format to https://rzlv.co/1/2/3/4
, use Trigger Manager to fetch the category. See code sample for test advice.
If the url is NOT a RezolveTrigger, assume it is a regular url, and you should handle it as you see fit. The typical behavior is a simple redirect to the url, to be opened in a browser.
Handling SSP URL Targets - IOS
let act = payload.act?.act // Information for linked Act
let product = payload.product // Information for linked Product
let category = payload.category // Information for linked Category
// Any of the above are not `null`, and a `customUrl` is detected, we need to redirect the user to this linked entity
let actCustomUrl = act?.customUrl
let productCustomUrl = product?.customUrl
let categoryCustomUrl = category?.customUrl
If you find a non-null payload
, next check for a non-null customUrl
If a custom URL is found, you redirect the user to the linked entity.
Trigger Manager
Triggers are specially formatted media in the Rezolve system that can point to products, categories, and other resources. Image Engagements and Audio Engagements are a type of trigger, and these are handled by ScanManager
. Touch Triggers are handled by TriggerManager
. Touch Triggers are essentially URLs that are typically rendered as a touchable link or button onscreen.
Touch Triggers are used by the Background Listening feature to surface items detected from audio watermarks in a Background Listening session.
Touch Triggers could also be used to create a Wishlist feature; ask your sales person for the Wishlist/Favorites Solution Paper.
Trigger Manager provides a way to resolve touch triggers into actionable content, like products or categories.
Touch Engagements, or touch triggers, always have the same format:
http://rzlv.co/[partnerId]/[merchantId]/[categoryId]/[productId][(optional...)?ad=[adId]&placement=[placementId]]
To use touch triggers, the partner should watch for urls in their content stream that match this pattern, render them as a touchable link, and then when touched, pass the url to the TriggerManager.resolveTrigger method
.
Trigger Manager Example
// Sample URL and `TriggerManager` initialization
let url = URL(string: "http://rzlv.co/1/2/3/8?ad=20&placement=25")
rezolveSession?.triggerManager.resolve(
url: url!,
productDelegate: self,
onRezolveTriggerStart: {},
onRezolveTriggerEnd: {},
errorCallback: { error in }
)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
public class TriggerMgr extends AppCompatActivity implements TriggerInterface {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TriggerManager triggerManager = RezolveSDK.getInstance().getRezolveSession().getTriggerManager();
String triggerUrl = "http://rzlv.co/2/3/13/169?ad=20&placement=25";
triggerManager.resolveTrigger( triggerUrl, this );
// reserved for future functionality:
// triggerManager.getPersistedTriggers();
// triggerManager.listenUrl();
}
@Override
public void onProductResult(Product product) {
// get product info
String product_id = product.getId();
String title = product.getTitle();
String subtitle = product.getSubtitle();
String description = product.getDescription();
List<String> images = product.getImages();
List<String[]> thumbs = product.getImageThumbs();
String merchant_id = product.getMerchantId();
float price = product.getPrice();
List<Option> options = product.getOptions();
List<Variant> optionsAvail = product.getOptionsAvailable();
List<CustomOption> customOptions = product.getCustomOptions();
// iterate to get values of options list
for (Option option : options) {
String optionLabel = option.getLabel();
String extraInfo = option.getExtraInfo();
String optionCode = option.getCode();
List<OptionValue> optionValues = option.getValues();
for(OptionValue optionValue: optionValues){
String value = optionValue.getValue();
String label = optionValue.getLabel();
}
}
// iterate to get values of variant list
for (Variant variant : optionsAvail) {
List<Combination> combinations = variant.getCombinations();
// iterate to get values of composition hashmap
for (Combination combination : combinations ){
String comboValue = combination.getValue();
String comboCode = combination.getCode();
}
}
//iterate to get values of CustomOptions list
for (CustomOption customOption : customOptions){
List<CustomOptionValue> customOptionValues = customOption.getValues();
int customOptionId = customOption.getOptionId();
int customOptionSortOrder = customOption.getSortOrder();
String customOptiontitle = customOption.getTitle();
Boolean customOptionIsRequired = customOption.isRequired();
// iterate to get values of customOptionValues
for( CustomOptionValue customOptionValue : customOptionValues ){
String customOptionValueTitle = customOptionValue.getTitle();
int customOptionValuesSortOrder = customOptionValue.getSortOrder();
String customOptionValueId = customOptionValue.getValueId();
}
}
}
@Override
public void onCategoryResult(Category category, String s) {
String category_id = category.getId();
String parentId = category.getParentId();
String name = category.getName();
Boolean hasCategories = category.hasCategories();
Boolean hasProduct = category.hasProducts();
String image = category.getImage();
List<String> imageThumbs = category.getImageThumbs();
String catParentId = category.getParentId();
List<Category> children = category.getCategories();
PageResult<DisplayProduct> pageResult = category.getProductPageResult();
// get products from pageResult...
Integer count = pageResult.getCount();
Integer total = pageResult.getTotal();
Link[] links = pageResult.getLinks();
List<DisplayProduct> displayProducts = pageResult.getItems();
for (Link link: links){
Integer linkcount = link.getCount();
Integer page = link.getPage();
String sort = link.getSort();
String sortBy = link.getSortBy();
}
for (DisplayProduct displayProduct : displayProducts){
String DPid = displayProduct.getId();
List<String> DPimageThumbs = displayProduct.getImageThumbs();
String DPimage = displayProduct.getImage();
float DPprice = displayProduct.getPrice();
String DPname = displayProduct.getName();
String DPcategoryId = displayProduct.getCategoryId();
}
}
@Override
public void onBadTrigger() {
// called when provided url has bad syntax or is too short
// handle error
}
@Override
public void onError(HttpResponse httpResponse) {
// called if httpError occurs
// handle error... available methods are:
// httpResponse.getResponseJson();
// httpResponse.getErrorList();
// httpResponse.getStatusCode();
// httpResponse.getResponseJson();
}
}
Product Scan, Instant Buy flow
[ View full size ]
The premise of Shoppable Ads is to capture an image scan (usually of an advertisement) using the Scan Manager, resolve it into a product URL, fetch the product info, and enable purchase via saved account information.
In the Instant Buy flow, we purchase the product immediately, without first adding it to the cart.
1. Capture image and get product
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
public class ScanActivity extends AppCompatActivity implements ScanManagerInterface, View.OnClickListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set scan view
setContentView(R.layout.scan_activity);
rezolveScanView = (RezolveScanView)findViewById(R.id.scan_view);
//get scan manager
boolean barcodeenabled = true;
boolean videoenabled = true;
scanManager = RezolveSDK.getInstance().getRezolveSession().getScanManager(this, barcodeenabled, videoenabled );
//start video scan to acquire image
scanManager.startVideoScan(this, rezolveScanView);
}
// capture product result
@Override
public void onProductResult(Product product) {
// get product info
String productId = product.getId();
String title = product.getTitle();
String subtitle = product.getSubtitle();
// ...etc
}
}
First, initialize scanManager
, and enable the scan screen using session.startVideoScan()
, and capture a watermarked image. The scanManager recognizes the encoded product data, and extracts merchantId
, catalogId
, and productId
from the image, automatically calling getProduct
. The scanManager returns a product
object.
2. Get shipping and payment options for the product
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"
rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<PaymentOption, RezolveError>) in
switch result {
case .success(let option):
{
// For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
let paymentMethod = option.supportedPaymentMethods.first!
let shippingMethod = option.supportedDeliveryMethods.first!
}
case .failure(let error):
// Handle error gracefully
}
})
public class PaymentOptionsMgr extends AppCompatActivity implements PaymentOptionInterface {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
PaymentOptionManager pom = RezolveSDK.getInstance().getRezolveSession().getPaymentOptionManager();
String merchantId = "12345";
CheckoutProduct checkoutProduct = new CheckoutProduct();
// get PaymentOptions for a product
pom.getProductOptions(checkoutProduct, merchantId, this);
}
@Override
public void onProductOptionsSuccess(PaymentOption paymentOption) {
String id = paymentOption.getId();
List<CheckoutProduct> checkoutProducts = paymentOption.getCheckoutProducts();
JSONObject paymentOptionOptions = paymentOption.getOptions();
List<Shipping> supportedDeliveryMethods = paymentOption.getSupportedDeliveryMethods();
List<SupportedPaymentMethod> supportedPaymentMethods = paymentOption.getSupportedPaymentMethods();
String type = paymentOption.getType();
for (CheckoutProduct checkoutProduct : checkoutProducts) {
// breakdown checkoutProduct object
float qty = checkoutProduct.getQty();
Placement productPlacement = checkoutProduct.getProductPlacement();
int cpId = checkoutProduct.getId();
List<ConfigurableOption> configurableOptions = checkoutProduct.getConfigurableOptions();
String placementId = productPlacement.getPlacementId();
String adId = productPlacement.getAdId();
for (ConfigurableOption configurableOption : configurableOptions) {
int value = configurableOption.getValue();
String code = configurableOption.getCode();
}
}
for (Shipping shipping : supportedDeliveryMethods) {
ShippingDetails shippingDetails = shipping.getShippingDetails();
ShippingMethod shippingMethod = shipping.getShippingMethod();
}
for (SupportedPaymentMethod supportedPaymentMethod : supportedPaymentMethods ) {
PaymentMethodData paymentMethodData = supportedPaymentMethod.getPaymentMethodData();
String spmType = supportedPaymentMethod.getType();
JSONObject originalDataJson = supportedPaymentMethod.getOriginalDataJson();
JSONObject requirements = paymentMethodData.getRequirements();
List<String> supportedDelivery = paymentMethodData.getSupportedDelivery();
List<String> supportedNetworks = paymentMethodData.getSupportedNetworks();
List<String> supportedTypes = paymentMethodData.getSupportedTypes();
}
}
}
Call PaymentOptionManager
to get shipping and payment options for the current merchant. This tutorial assumes the consumer chose a form of credit card payment, and chose home delivery.
Note: You must repeat this call if the user chooses a different product variant (size, color, etc), changes product quantity, changes shipping choice, or changes payment option.
For more information on what is returned by getProductOptions
, see the Background Listening tutorial.
3. Show payment card choices
rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in
// Handle payment cards
}
rezolveSession.getWalletManager().getAll(new WalletCallback() {
@Override
public void onWalletGetAllSuccess(List<PaymentCard> list) {
// handle getAll response here
}
});
Use walletManager.getAll
to list the available card choices. If the consumer wishes to buy, they will select a payment card to use, and provide confirmation of ordering intent.
We recommend using a "slide to buy" button to confirm purchase intent, while preserving the maximum ease of use.
4. Create a checkout bundle, checkout the product to get totals, and create an order
let sampleProductCheckoutBundle = CheckoutBundle(
checkoutProduct: checkoutProduct,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: nil,
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.checkout(bundle: sampleProductCheckoutBundle) { (result: Result<Price, Error>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.finalPrice)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
@Override
public void onProductResult(Product product) {
// get product info
String product_id = product.getId();
String title = product.getTitle();
String subtitle = product.getSubtitle();
// ... etc
// create a CheckoutProduct object with the product id , and set the quantity
CheckoutProduct checkoutProduct = new CheckoutProduct();
checkoutProduct.setId(Integer.parseInt(product_id));
checkoutProduct.setQty(1);
// use info from productPaymentOption to populate Shipping and PaymentMethod choices...
String productPaymentOptionId = productPaymentOption.getId();
List<SupportedPaymentMethod> supportedPaymentMethods = productPaymentOption.getSupportedPaymentMethods();
List<Shipping> deliveryMethods = productPaymentOption.getSupportedDeliveryMethods();
SupportedPaymentMethod supportedPaymentMethod = supportedPaymentMethods.get(0); // in reality customer chooses an option here
Shipping deliveryMethod = deliveryMethods.get(0); // in reality customer chooses an option here
String phonebookId = "123"; // use real consumer phonebook id
// create the delivery unit
DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, address.getId());
// create a product checkout bundle
CheckoutBundleV2.createProductCheckoutBundleV2(merchantId, paymentOption.getId(), checkoutProduct, phoneId, supportedPaymentMethod, deliveryUnit);
// call the CheckoutProductOption method to get an order object and totals
checkout.checkoutProductOption(productCheckoutBundleV2, new CheckoutV2Callback() {
public void onCheckoutProductSuccess(Order order) {
// get pricing breakdown and final price
List<PriceBreakdown> pricingBreakdown = order.getBreakdowns();
float finalPrice = order.getFinalPrice();
String orderId = order.getOrderId();
// create a paymentRequest object, and then use this with the checkoutProduct object to purchase the item.
PaymentCard paymentCard = new PaymentCard(); // use consumer's chosen payment card
String cvv = "123"; // use actual cvv
PaymentRequest paymentRequest = checkout.createPaymentRequest(paymentCard,cvv);
// buy a single product
checkout.buyProduct(paymentRequest, productCheckoutBundleV2, orderId, new CheckoutV2Callback() {
@Override
public void onProductOptionBuySuccess(OrderSummary orderSummary) {
super.onProductOptionBuySuccess(orderSummary);
//display order summary
String orderId = orderSummary.getOrderId();
JSONObject orderData = orderSummary.getData();
}
});
}
});
}
Once you have product information, create a CheckoutProduct object. Then call the SDK CheckoutManagerV2.checkoutProduct
method to create an order and get totals. The response order object includes an order id, order total, and price breakdowns.
5. Submit payment for order
let paymentCard = // RezolveSDK.PaymentCard
let cardCVV = "000" // Card CVV
let sampleProductCheckoutBundle = CheckoutBundle(
checkoutProduct: checkoutProduct,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: PaymentRequest(paymentCard: paymentCard, cvv: cardCVV),
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.buy(bundle: sampleProductCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.partnerId)
print(order.partnerName)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
// create a paymentRequest object, and then use this with the checkoutBundle object
// create paymentRequest object
PaymentCard paymentCard = new PaymentCard(); // use consumer's chosen payment card from step 3
String cvv = "123"; // use actual cvv
PaymentRequest paymentRequest = checkout.createPaymentRequest(paymentCard,cvv);
// buy a single product
checkoutManagerV2.buyProduct(paymentRequest, productCheckoutBundleV2, orderId, this);
@Override
public void onProductOptionBuySuccess(OrderSummary orderSummary) {
// skip this call, ContactInformation entity is not public
// Merchant.ContactInformation contactInformation = orderSummary.getContactInformation();
JSONObject orderData = orderSummary.getData();
String orderId = orderSummary.getOrderId();
String partnerId = orderSummary.getPartnerId();
String partnerName = orderSummary.getPartnerName();
}
When the user confirms intent, pass the card choice and the entered CVV value to the createPaymentRequest
method. This creates the encrypted paymentRequest
object needed for checkout.
In this tutorial, we assume the user chose credit card payment. Note that paymentRequest
is actually optional here, and can be null. To determine if it's needed, please check selected SupportedPaymentMethod
's type.
Pass a paymentRequest
object, checkoutBundleV2
object, the orderId
, and an interface or callback to the buyProduct
method. The success response will an OrderSummary
object. Note that this does not mean the order was confirmed, only that the request was successfully received.
Note that the call to signOrderUpdate
shown in the sequence diagram is no longer required.
Product Scan, Cart flow
[ View full size ]
The premise of Shoppable Ads is to capture an image scan (usually of an advertisement) using the Scan Manager, resolve it into a product URL, fetch the product info, and enable purchase via saved account information.
In the Cart Buy flow, one or more products are added to a cart. Each merchant will have a separate cart, and multiple carts can contain products at a time. For this example we are assuming only one cart is active, but you can check for multiple carts using CheckoutManagerV2.getCarts
.
1. Capture image and get product
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveSDK.Category) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveSDK.Category, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
public class ScanActivity extends AppCompatActivity implements ScanManagerInterface, View.OnClickListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set scan view
setContentView(R.layout.scan_activity);
rezolveScanView = (RezolveScanView)findViewById(R.id.scan_view);
//get scan manager
scanManager = RezolveSDK.getInstance().getRezolveSession().getScanManager(this, true);
//start video scan to acquire image
scanManager.startVideoScan(this, rezolveScanView);
}
// capture product result
@Override
public void onProductResult(Product product) {
// get product info
String productId = product.getId();
String title = product.getTitle();
String subtitle = product.getSubtitle();
// ...etc
}
}
First, initialize scanManager
, and enable the scan screen using session.startVideo()
, and capture a watermarked image. The scanManager recognizes the encoded product data, and extracts merchantId
, catalogId
, and productId
from the image, automatically calling getProduct
. The scanManager returns a product
object.
2. Add Product to the Cart
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"
rezolveSession?.cartManager.createCartWithProduct(sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<CartDetails, RezolveError>) in
switch result {
case .success(let cart):
{
print(cart.id)
print(cart.merchantId)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
// add product to cart
checkout.addProductToCart(this, checkoutProduct, merchantId, new CheckoutCallback() {
@Override
public void onAddProductsToCartSuccess(CartDetails cartDetails) {
super.onAddProductsToCartSuccess(cartDetails);
String cartId = cartDetails.getId();
String merchantId = "123"; //use actual merchant id
String addressId = "123"; //use actual address id
// immediately call CheckoutCart to get an Order with totals
checkout.addProductToCart( checkoutProduct, merchantId, new CheckoutCallback() {
@Override
public void onAddProductsToCartSuccess(CartDetails cartDetails) {
super.onAddProductsToCartSuccess(cartDetails);
String cartId = cartDetails.getId();
String merchantId = "123"; //use actual merchant id
String addressId = "123"; //use actual address id
}
});
}
});
Call CheckoutManagerV2.addProductToCart
to add the product to cart.
3. Get shipping and payment options for the cart
let sampleMerchantID = "12"
let sampleCartID = "1"
rezolveSession?.paymentOptionManager.getPaymentOptionsForCartWith(merchantId: sampleMerchantID, cartId: sampleCartID) { (result: Result<[PaymentOption], RezolveError>) in
switch result {
case .success(let options):
{
// For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
let paymentMethod = options.first?.supportedPaymentMethods.first
let shippingMethod = options.first?.supportedDeliveryMethods.first
}
case .failure(let error):
// Handle error gracefully
}
})
public class PaymentOptionsMgr extends AppCompatActivity implements PaymentOptionInterface {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
PaymentOptionManager pom = RezolveSDK.getInstance().getRezolveSession().getPaymentOptionManager();
String merchantId = "12345";
String cartId = "12345";
// get PaymentOptions for cart
pom.getCartOptions( merchantId, cartId, this);
}
@Override
public void onCartOptionsSuccess(List<PaymentOption> list) {
for (PaymentOption paymentOption : list ){
String type = paymentOption.getType();
List<SupportedPaymentMethod> supportedPaymentMethods = paymentOption.getSupportedPaymentMethods();
List<Shipping> supportedDeliveryMethods = paymentOption.getSupportedDeliveryMethods();
JSONObject options = paymentOption.getOptions();
String id = paymentOption.getId();
List<CheckoutProduct> checkoutProducts = paymentOption.getCheckoutProducts();
for (SupportedPaymentMethod supportedPaymentMethod : supportedPaymentMethods ) {
PaymentMethodData paymentMethodData = supportedPaymentMethod.getPaymentMethodData();
String spmType = supportedPaymentMethod.getType();
JSONObject originalDataJson = supportedPaymentMethod.getOriginalDataJson();
JSONObject requirements = paymentMethodData.getRequirements();
List<String> supportedDelivery = paymentMethodData.getSupportedDelivery();
List<String> supportedNetworks = paymentMethodData.getSupportedNetworks();
List<String> supportedTypes = paymentMethodData.getSupportedTypes();
}
for (Shipping shipping : supportedDeliveryMethods) {
ShippingDetails shippingDetails = shipping.getShippingDetails();
ShippingMethod shippingMethod = shipping.getShippingMethod();
}
for (CheckoutProduct checkoutProduct : checkoutProducts) {
// breakdown checkoutProduct object
float qty = checkoutProduct.getQty();
Placement productPlacement = checkoutProduct.getProductPlacement();
int cpId = checkoutProduct.getId();
List<ConfigurableOption> configurableOptions = checkoutProduct.getConfigurableOptions();
String placementId = productPlacement.getPlacementId();
String adId = productPlacement.getAdId();
for (ConfigurableOption configurableOption : configurableOptions) {
int value = configurableOption.getValue();
String code = configurableOption.getCode();
}
}
}
}
@Override
public void onFailure(HttpResponse httpResponse) {
// handle error
}
}
// Assuming the user picks traditional shipping.
// Click and Collect options are discussed in a separate tutorial.
// Create DeliveryUnit as follows:
Call PaymentOptionManager
to get shipping and payment options for the current merchant. This tutorial assumes the consumer chose a form of credit card payment, and chose home delivery.
Note: You must repeat this call if the user chooses a different product variant (size, color, etc), changes product quantity, changes shipping choice, or changes payment option.
For more information on what is returned by getCartOptions
, see the Background Listening tutorial.
4. Show payment card choices
rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in
// Handle payment cards
}
rezolveSession.getWalletManager().getAll(new WalletCallback() {
@Override
public void onWalletGetAllSuccess(List<PaymentCard> list) {
// handle getAll response here
}
});
Use walletManager.getAll
to list the available card choices. If the consumer wishes to buy, they will select a payment card to use, and provide confirmation of ordering intent.
We recommend using a "slide to buy" button to confirm purchase intent, while preserving the maximum ease of use.
5. Create a checkout bundle, checkout the cart to get totals, and create an order
let sampleCartCheckoutBundle = CheckoutBundle(
cartId: cartId,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: nil,
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.checkout(bundle: sampleCartCheckoutBundle) { (result: Result<Price, Error>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.finalPrice)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
// create a Cart Checkout Bundle
String phonebookId = "123"; // use real consumer phonebook id
String optionId = "123"; // get this from productPaymentOption.getId();
String cartId = "123";
SupportedPaymentMethod supportedPaymentMethod = new SupportedPaymentMethod(); //get this from productPaymentOption.getSupportedPaymentMethods()
// create the delivery unit
DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, address.getId());
CheckoutBundleV2 checkoutBundleV2 = CheckoutBundleV2.createCartCheckoutBundleV2( merchantId, optionId, cartId, phonebookId, supportedPaymentMethod, deliveryUnit);
// call CheckoutCartOption method to get an order object and totals
checkout.checkoutCartOption(cartCheckoutBundleV2, new CheckoutV2Callback() {
@Override
public void onCartOptionCheckoutSuccess(Order order) {
super.onCartOptionCheckoutSuccess(order);
// get order id
String orderId = order.getOrderId();
// get price breakdowns
List<PriceBreakdown> priceBreakdowns = order.getBreakdowns();
}
});
Create a cart checkout bundle. Then call the SDK CheckoutManagerV2.checkoutCart
method (Android) or CheckoutManagerV2.checkout
(IOS) method to create an order and get totals. The response order object includes an order id, order total, and price breakdowns.
6. Submit payment for order
let paymentCard = // RezolveSDK.PaymentCard
let cardCVV = "000" // Card CVV
let sampleCartCheckoutBundle = CheckoutBundle(
cartId: cartId,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: PaymentRequest(paymentCard: paymentCard, cvv: cardCVV),
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.buy(bundle: sampleCartCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.partnerId)
print(order.partnerName)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
// create a paymentRequest object, and then use this with the checkoutProduct object to purchase the cart.
PaymentCard paymentCard = new PaymentCard(); // use consumer's chosen payment card
String cvv = "123"; // use actual cvv
PaymentRequest paymentRequest = checkout.createPaymentRequest(paymentCard,cvv);
// buy the cart
String orderId = "123"; // get from order.getOrderId();
checkout.buyCart(paymentRequest, cartCheckoutBundleV2, orderId, new CheckoutV2Callback() {
@Override
public void onCartOptionBuySuccess(OrderSummary orderSummary) {
super.onCartOptionBuySuccess(orderSummary);
//display order summary
String orderId = orderSummary.getOrderId();
JSONObject orderData = orderSummary.getData();
}
});
When the user confirms intent, pass the card choice and the entered CVV value to the createPaymentRequest
method. This creates the encrypted paymentRequest
object needed for checkout.
In this tutorial, we assume the user chose credit card payment. Note that paymentRequest
is actually optional here, and can be null. To determine if it's needed, please check selected SupportedPaymentMethod
's type.
Pass the paymentRequest
object, thecheckoutBundleV2
object, the orderId
, and an interface or callback to the buyCart
method. The success response will be the order id
as a string. Note that this does not mean the order was confirmed, only that the request was successfully received.
Note that the call to signOrderUpdate
shown in the sequence diagram is no longer required.
Category Scan flow
[ View full size ]
Shoppable Ads can do more than link to a single product, it can link to a category of products in your Rezolve Commerce Engine. Scanning an ad that contains a category link will bring up a list of subcategories and products in that category.
Once a product has been selected, purchasing follows one of the previous examples of Instant Buy Flow or Cart Buy Flow (minus the scan step).
1. Scan a category shoppable ad, get a getCatalog response
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
public class ScanActivity extends AppCompatActivity implements ScanManagerInterface, View.OnClickListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set scan view
setContentView(R.layout.scan_activity);
rezolveScanView = (RezolveScanView)findViewById(R.id.scan_view);
//get scan manager
scanManager = RezolveSDK.getInstance().getRezolveSession().getScanManager(this, true);
//start video scan to acquire image
scanManager.startVideoScan(this, rezolveScanView);
}
// if scan media contains a category link, onCategoryResult fires and
// returns a Category object. Parse the category object to obtain
// a list of products in that category, and a list of subcategories of that category
@Override
public void onCategoryResult(Category category, String s) {
String category_id = category.getId();
String parentId = category.getParentId();
String name = category.getName();
Boolean hasCategories = category.hasCategories();
Boolean hasProduct = category.hasProducts();
String image = category.getImage();
List<String> imageThumbs = category.getImageThumbs();
String catParentId = category.getParentId();
List<Category> children = category.getCategories();
// get category placement
Placement.CategoryPlacement categoryPlacement = category.getCategoryPlacement();
String categoryAdId = categoryPlacement.getAdId();
String categoryPlacementId = categoryPlacement.getPlacementId();
// optional: get paginated subcategory results
PageResult<Category> categoryPageResult = category.getCategoryPageResult();
// optional: get paginated product results
PageResult<DisplayProduct> pageResult = category.getProductPageResult();
// get products from pageResult...
Integer count = pageResult.getCount();
Integer total = pageResult.getTotal();
Link[] links = pageResult.getLinks();
List<DisplayProduct> displayProducts = pageResult.getItems();
for (Link link: links){
Integer linkcount = link.getCount();
Integer page = link.getPage();
String sort = link.getSort();
String sortBy = link.getSortBy();
}
for (DisplayProduct displayProduct : displayProducts){
String DPid = displayProduct.getId();
List<String> DPimageThumbs = displayProduct.getImageThumbs();
String DPimage = displayProduct.getImage();
float DPprice = displayProduct.getPrice();
String DPname = displayProduct.getName();
String DPcategoryId = displayProduct.getCategoryId();
}
}
}
First, initialize scanManager
, and enable the scan screen using session.startVideo()
, and capture a watermarked image. The scanManager returns a Category
object, which is then parsed to determine its contents.
2. Repeat for navigation until ready to purchase
Repeat step 1 above as the consumer browses through categories. Once the consumer selects a product and is ready to purchase, the process is the same as in the Instant Buy or Cart Buy flows. Note: a DisplayProduct does not contain full information on a product. You will need to call getProduct to display the detail view of the Product.
Mall flow
[ View full size ]
Mall is the only method for finding products that does not require an ad scan. A consumer enters the mall by clicking on a "Mall" navigation option within the mobile app. The Mall showcases active merchants in an attractive layout that is condusive to casual browsing and subsequent purchasing.
Once a merchant is selected, the consumer shifts into category/product browse mode. Once a product has been selected, purchasing follows one of the previous examples of Instant Buy Flow or Cart Buy Flow (minus the scan step).
1. Get List of Merchants in the mall
rezolveSession?.merchantManager.getMerchants { (result: Result<[Merchant], RezolveError>) in
switch result {
case .success(let merchants):
{
merchants.forEach {
// Basic information
let id = $0.id
let name = $0.name
let tagline = $0.tagline
let contactInformation = $0.contactInformation
let termsAndConditions = $0.termsAndConditions
// Assets
let banner = $0.banner
let logo = $0.logo
let bannerThumbs = $0.bannerThumbs
let logoThumbs = $0.logoThumbs
}
}
case .failure(let error):
// Handle error gracefully
}
})
public class Merchants extends AppCompatActivity implements MerchantInterface {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MerchantInterface merchantInterface;
MerchantManager merchantManager = RezolveSDK.getInstance().getRezolveSession().getMerchantManager();
// get merchants
merchantManager.getMerchants(this, this);
}
@Override
public void onGetMerchantsSuccess(List<Merchant> list) {
for(Merchant merchant : list) {
String merchant_id = merchant.getId();
String name = merchant.getName();
String tagline = merchant.getTagline();
String banner = merchant.getBanner();
List<String> bannerThumbs = merchant.getBannerThumbs();
List<String> logoThumbs = merchant.getLogoThumbs();
}
}
}
First, initialize MerchantManager
, and call GetMerchants
, providing an implementation of MerchantCallback as a parameter. This will return an array of Merchant objects. Parse each merchant object to get the id
, name
, tagline
, banner
, bannerThumbs
, and logoThumbs
.
2. Get list of first-level Categories for the selected Merchant
let sampleMerchantID = "12"
rezolveSession?.productManager.getRootCategoryForMerchantWith(id: sampleMerchantID) { (result: Result<RezolveSDK.Category, RezolveError>) in
switch result {
case .success(let category):
{
// Basic information
let id = category.id
let parentId = category.parentId
let name = category.name
let hasCategories = category.hasCategories
let hasProducts = category.hasProducts
// Assets
let image = category.image
let imageThumbs = category.imageThumbs
// Get subcategories, if any
if hasCategories {
category.categories.forEach { subCategory in
print(subCategory.id)
print(subCategory.parentId)
print(subCategory.name)
// ...
}
}
}
case .failure(let error):
// Handle error gracefully
}
})
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ProductInterface productInterface;
ProductManager myProductManager = RezolveSDK.getInstance().getRezolveSession().getProductManager();
// get categories
String merchantId = "123";
myProductManager.getCategories(merchantId,this);
}
@Override
public void onGetCategoriesSuccess(Category category) {
// parse Category object
String category_id = category.getId();
String parentId = category.getParentId();
String name = category.getName();
Boolean hasCategories = category.hasCategories();
Boolean hasProduct = category.hasProducts();
String image = category.getImage();
List<String> imageThumbs = category.getImageThumbs();
String catParentId = category.getParentId();
List<Category> children = category.getCategories();
PageResult<DisplayProduct> pageResult = category.getProductPageResult();
// get products from pageResult...
Integer count = pageResult.getCount();
Integer total = pageResult.getTotal();
Link[] links = pageResult.getLinks();
List<DisplayProduct> displayProducts = pageResult.getItems();
for (Link link: links){
Integer linkcount = link.getCount();
Integer page = link.getPage();
String sort = link.getSort();
String sortBy = link.getSortBy();
}
for (DisplayProduct displayProduct : displayProducts){
String DPid = displayProduct.getId();
List<String> DPimageThumbs = displayProduct.getImageThumbs();
String DPimage = displayProduct.getImage();
float DPprice = displayProduct.getPrice();
String DPname = displayProduct.getName();
String DPcategoryId = displayProduct.getCategoryId();
}
}
Display your subcategories and products as returned by the getCategories
call.
For subsequent navigation in categories, use getProductsAndCategories
.
3. If the consumer clicks a subcategory, call getProductsAndCategories
.
let sampleMerchantID = "12"
let sampleCategoryID: Int = 70
let pageNavigationFilters = PageNavigationFilter(
productsFilter: PageNavigation(count: 100, pageIndex: 1, sortBy: "product", sort: .ascending),
categoryFilter: PageNavigation(count: 100, pageIndex: 1, sortBy: "category", sort: .ascending)
)
rezolveSession?.productManager.getPaginatedCategoriesAndProducts(merchantId: sampleMerchantID, categoryId: sampleCategoryID, pageNavigationFilters: pageNavigationFilters) { (result: <Rezolve, RezolveError>) in
switch result {
case .success(let category):
{
// Basic information
let id = category.id
let parentId = category.parentId
let name = category.name
let hasCategories = category.hasCategories
let hasProducts = category.hasProducts
// Assets
let image = category.image
let imageThumbs = category.imageThumbs
// Get subcategories, if any
if hasCategories {
category.categories.forEach { subCategory in
print(subCategory.id)
print(subCategory.parentId)
print(subCategory.name)
// ...
}
}
// Get display products, if any
if hasProducts {
category.products.forEach { displayProduct in
print(displayProduct.id)
print(displayProduct.name)
print(displayProduct.price)
// ...
}
}
}
case .failure(let error):
// Handle error gracefully
}
})
public class Products2 extends AppCompatActivity implements ProductInterface {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ProductInterface productInterface;
ProductManager myProductManager = RezolveSDK.getInstance().getRezolveSession().getProductManager();
merchantId = "123";
Category category = new Category();
// first pageNavigationFilter is for categories
PageNavigationFilter pageNavigationFilter = new PageNavigationFilter();
pageNavigationFilter.setItemsPerPage(10);
pageNavigationFilter.setPageNumber(1);
pageNavigationFilter.setSortBy("name"); // values: name, price
pageNavigationFilter.setSortDirection("asc"); // values: asc, desc
// second pageNavigationFilter is for products
PageNavigationFilter pageNavigationFilter2 = new PageNavigationFilter();
pageNavigationFilter2.setItemsPerPage(20);
pageNavigationFilter2.setPageNumber(1);
pageNavigationFilter2.setSortBy("price"); // values: name, price
pageNavigationFilter2.setSortDirection("asc"); // values: asc, desc
// get products and categories in one call
myProductManager.getProductsAndCategories(merchantId, category, pageNavigationFilter, pageNavigationFilter2, this);
}
@Override
public void onGetProductsAndCategoriesSuccess(Category category) {
String category_id = category.getId();
String parentId = category.getParentId();
String name = category.getName();
Boolean hasCategories = category.hasCategories();
Boolean hasProduct = category.hasProducts();
String image = category.getImage();
List<String> imageThumbs = category.getImageThumbs();
String catParentId = category.getParentId();
List<Category> children = category.getCategories();
PageResult<DisplayProduct> pageResult = category.getProductPageResult();
// get products from pageResult...
Integer count = pageResult.getCount();
Integer total = pageResult.getTotal();
Link[] links = pageResult.getLinks();
List<DisplayProduct> displayProducts = pageResult.getItems();
for (Link link: links){
Integer linkcount = link.getCount();
Integer page = link.getPage();
String sort = link.getSort();
String sortBy = link.getSortBy();
}
for (DisplayProduct displayProduct : displayProducts){
String DPid = displayProduct.getId();
List<String> DPimageThumbs = displayProduct.getImageThumbs();
String DPimage = displayProduct.getImage();
float DPprice = displayProduct.getPrice();
String DPname = displayProduct.getName();
String DPcategoryId = displayProduct.getCategoryId();
}
}
}
As the consumer navigates the category tree, call getProductsAndCategories
to pull a paginated lists of subcategories and products for that category.
4. If the consumer clicks a Product, call getProduct
let sampleMerchantID = "12"
let sampleCategoryID: Int = 70
let sampleProductID: Int = 6
let sampleProduct = Product(id: sampleProductID)
rezolveSession?.productManager.getProductDetails(merchantId: sampleMerchantID, categoryId: sampleCategoryID, product: sampleProduct) { (result: Result<Product, RezolveError>) in
switch result {
case .success(let product):
{
print(product.id)
print(product.merchantId)
print(product.title)
print(product.subtitle)
print(product.price)
print(product.description)
product.images.forEach {
print($0)
}
product.options.forEach { option in
print(option.label)
print(option.code)
print(option.extraInfo)
option.values.forEach { optionValue in
print(optionValue.value)
print(optionValue.label)
}
}
product.optionAvailable.forEach {
$0.combination.forEach { variant in
print(variant.code)
print(variant.value)
print(variant.id)
}
}
product.customOptions.forEach {
print($0.isRequire)
print($0.optionId)
print($0.sortOrder)
print($0.title)
print($0.optionType)
$0.values.forEach { value in
print(value.sortOrder)
print(value.title)
print(valueId)
}
$0.valuesId.forEach { valueId in
print(valueId)
}
print($0.value)
}
print(product.productPlacement)
}
case .failure(let error):
// Handle error gracefully
}
})
// get a single product using ProductInterface
public class Products extends AppCompatActivity implements ProductInterface {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ProductManager myProductManager = RezolveSDK.getInstance().getRezolveSession().getProductManager();
// get single product
Product product = new Product();
Category category = new Category();
String merchantId = "123";
myProductManager.getProduct(merchantId, category, product, this);
}
@Override
public void onGetProductSuccess(com.rezolve.sdk.model.shop.Product product) {
// get product info
String product_id = product.getId();
String title = product.getTitle();
String subtitle = product.getSubtitle();
String description = product.getDescription();
List<String> images = product.getImages();
List<String[]> thumbs = product.getImageThumbs();
String merchant_id = product.getMerchantId();
float price = product.getPrice();
List<Option> options = product.getOptions();
List<Variant> optionsAvail = product.getOptionsAvailable();
List<CustomOption> customOptions = product.getCustomOptions();
// iterate to get values of options list
for (Option option : options) {
String optionLabel = option.getLabel();
String extraInfo = option.getExtraInfo();
String optionCode = option.getCode();
List<OptionValue> optionValues = option.getValues();
for(OptionValue optionValue: optionValues){
String value = optionValue.getValue();
String label = optionValue.getLabel();
}
}
// iterate to get values of variant list
for (Variant variant : optionsAvail) {
List<Combination> combinations = variant.getCombinations();
// iterate to get values of composition hashmap
for (Combination combination : combinations ){
String comboValue = combination.getValue();
String comboCode = combination.getCode();
}
}
//iterate to get values of CustomOptions list
for (CustomOption customOption : customOptions){
List<CustomOptionValue> customOptionValues = customOption.getValues();
int customOptionId = customOption.getOptionId();
int customOptionSortOrder = customOption.getSortOrder();
String customOptiontitle = customOption.getTitle();
Boolean customOptionIsRequired = customOption.isRequired();
// iterate to get values of customOptionValues
for( CustomOptionValue customOptionValue : customOptionValues ){
String customOptionValueTitle = customOptionValue.getTitle();
int customOptionValuesSortOrder = customOptionValue.getSortOrder();
String customOptionValueId = customOptionValue.getValueId();
}
}
}
}
If the consumer clicks a Product, call getProduct
to fetch full product information. At this point, the user can either add the product to their cart, click "buy now", or press "back" to navigate to the category view.
Merchant and Product Search
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.
Merchant Search
let searchData = MerchantSearchData(
query: nil,
orderBy: .location,
order: .ascending,
deviceInfo: DeviceInfo.current,
location: RezolveLocation(longitude: -73.9502622, latitude: 40.6726499)
)
let currentPage: Int = 1
let itemsLimit: Int = 50
rezolveSession?.merchantManager.searchForMerchants(using: searchData, page: currentPage, limit: itemsLimit) { (result: Result<MerchantSearchResult, RezolveError>) in
switch result {
case .success(let merchantSearchResult):
{
// Current state
let page = merchantSearchResult.page
let total = merchantSearchResult.total
let merchants = merchantSearchResult.merchants
for merchant in merchants {
// Base information
let id = merchant.id
let priority = merchant.priority
let name = merchant.name
let tagLine = merchant.tagline
let distance = merchant.distance
// Assets
let banner = merchant.banner
let bannerThumbs = merchant.bannerThumbs
let logo = merchant.logo
let logoThumbs = merchant.logoThumbs
// Extra information
let info = merchant.contactInformation
let infoEmail = info?.email
let infoName = info?.name
let infoPhone = info?.phone
// List of Stores
if let stores = merchant.stores {
for store in stores {
let id = store.id
let name = store.name
let location = store.location
}
}
// List of Terms & Conditions
let termsAndConditions = merchant.termsAndConditions
for item in termsAndConditions {
let id = item.id
let storeId = item.storeId
let name = item.name
let content = item.content
let checkboxText = item.checkboxText
}
}
}
case .failure(let error):
// Handle error gracefully
}
})
// set up parameters for merchant search
String query = "";
MerchantSearchOrderBy merchantSearchOrderBy = MerchantSearchOrderBy.LOCATION;
SearchDirection searchDirection = SearchDirection.ASC;
Integer offset = 0;
Integer limit = 50;
RezolveLocation rezolveLocation = new RezolveLocation();
rezolveLocation.setLatitude(40.6726499);
rezolveLocation.setLongitude(-73.9502622);
MerchantSearchData merchantSearchData = new MerchantSearchData(query,merchantSearchOrderBy,searchDirection,offset,limit,rezolveLocation);
// set up merchant search interface and handle results
MerchantSearchInterface merchantSearchInterface = new MerchantSearchInterface() {
@Override
public void onSearchMerchantsSuccess(MerchantSearchResult merchantSearchResult) {
int page = merchantSearchResult.getPage();
int total = merchantSearchResult.getTotal();
List<Merchant> merchants = merchantSearchResult.getMerchants();
//iterate over merchants array
for (Merchant merchant : merchants) {
String banner = merchant.getBanner();
List<String> bannerThumbs = merchant.getBannerThumbs();
Double distance = merchant.getDistance();
String id = merchant.getId();
String infoEmail = merchant.getInfoEmail();
String infoName = merchant.getInfoName();
String infoPhone = merchant.getInfoPhone();
String logo = merchant.getLogo();
List<String> logoThumgs = merchant.getLogoThumbs();
String name = merchant.getName();
String partnerId = merchant.getPartnerId();
String partnerName = merchant.getPartnerName();
String priority = merchant.getPriority();
List<DisplayStore> stores = merchant.getStores();
String tagline = merchant.getTagline();
List<TermsAndConditions> termsAndConditionsList = merchant.getTermsAndConditionsList();
// get list of store locations associated with this merchant
for(DisplayStore displayStore:stores){
int storeId = displayStore.getId();
RezolveLocation storeLocation = displayStore.getLocation();
String storeName = displayStore.getName();
}
// get terms of conditions for merchant
for(TermsAndConditions termsAndConditions : termsAndConditionsList){
String termsCheckboxText = termsAndConditions.getCheckboxText();
String termsContent = termsAndConditions.getContent();
String termsId = termsAndConditions.getId();
String termsName = termsAndConditions.getName();
String termsStoreId = termsAndConditions.getStoreId();
}
}
}
@Override
public void onError(@NonNull RezolveError rezolveError) {
// handle error gracefully
}
};
// initiate merchant search
merchantManager.searchMerchants(this,merchantSearchData,merchantSearchInterface);
Merchant search offers the following features:
- The ability to list all merchants, sorted by distance to the end user.
- The ability to enter a merchant name query, and sort results by Score or Distance.
- Ability to order results ascending or descending.
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:
- Query string - Optional. Leave blank to get all merchants. If more than one term is supplied, terms are matched on an AND basis.
- Offset - For pagination, offset is the number of pages of results to skip. Setting this to zero gets the first page of results.
- Limit - For pagination, the number of results to return per page.
- Sort By - Should merchants be sorted by LOCATION or by search SCORE.
- Sort Direction - ASC or DESC
- Location - Optional. The latitude and longitude of the consumer phone. Merchant list cannot be sorted by distance without this parameter.
See code samples to the right.
Product Search
let productSearchData = ProductSearchData(
query: "book",
merchantId: "123",
orderBy: .score,
order: .descending,
location: RezolveLocation(longitude: -73.9502622, latitude: 40.6726499),
includeInResults: .products,
deviceInfo: DeviceInfo.current
)
let currentPage: Int = 1
let itemsLimit: Int = 50
rezolveSession?.productManager.searchForProducts(using: productSearchData, page: currentPage, limit: itemsLimit, completionHandler: { (result: Result<ProductSearchResult, RezolveError>) in
switch result {
case .success(let productSearchResult):
{
// Current state
let page = productSearchResult.page
let total = productSearchResult.total
let products = productSearchResult.products
for product in products {
// Base information
let id = product.id
let merchantId = product.merchantId
let merchantName = product.merchantName
let categoryId = product.categoryId
let categoryName = product.categoryName
let price = product.price
let isAct = product.isAct
let isVirtual = product.isVirtual
let hasRequiredOptions = product.hasRequiredOptions
// Assets
let image = product.image
let imageThumbs = product.thumbs
}
}
case .failure(let error):
// Handle error gracefully
}
})
// set up parameters for product search
String query = "book";
String merchantId2 = "123";
ProductSearchOrderBy productSearchOrderBy = ProductSearchOrderBy.SCORE;
SearchDirection searchDirection = SearchDirection.DESC;
ProductType productType = ProductType.ALL;
Integer offset = 0;
Integer limit = 50;
RezolveLocation rezolveLocation = new RezolveLocation();
rezolveLocation.setLatitude(40.6726499);
rezolveLocation.setLongitude(-73.9502622);
ProductSearchData productSearchData = new ProductSearchData(query,merchantId2,productSearchOrderBy,searchDirection,productType,offset,limit,rezolveLocation);
// set up product search interface and handle results
ProductSearchInterface productSearchInterface = new ProductSearchInterface() {
@Override
public void onSearchProductsSuccess(ProductSearchResult productSearchResult) {
Integer page = productSearchResult.getPage();
Integer total = productSearchResult.getTotal();
List<DisplayProduct> displayProducts = productSearchResult.getDisplayProducts();
// iterate over list of display proucts
for(DisplayProduct displayProduct:displayProducts){
String productId = displayProduct.getId();
String merchantId = displayProduct.getMerchantId();
String categoryId = displayProduct.getCategoryId();
String categoryName = displayProduct.getCategoryName();
String image = displayProduct.getImage();
List<String> imageThumbs = displayProduct.getImageThumbs();
float price = displayProduct.getPrice();
Placement productPlacement = displayProduct.getProductPlacement();
// get product placement details
String adId = productPlacement.getAdId();
String placementId = productPlacement.getPlacementId();
}
}
@Override
public void onError(@NonNull RezolveError rezolveError) {
// handle error gracefully
}
};
// initiate product search
productManager.searchProducts(this,productSearchData, productSearchInterface);
Product search offers the following features:
- Search products across all merchants in your ecosystem
- Search the products of a single merchant (by specifying a merchant id)
- Sort results by search SCORE or PRICE
- Sort direction ASC or DESC
To search for products you must supply the following parameters:
- Query string - Optional. Leaving blank will return all products. If more than one term is supplied, terms are matched on an AND basis.
- Merchant Id - Optional. If supplied, restricts product search to the specified merchant.
- Offset - For pagination, offset is the number of pages to skip. Setting this to zero gets the first page of results.
- Limit - For pagination, the number of results to return per page.
- Sort By - Sort by search SCORE or PRICE
- Sort Direction - ASC or DESC
- Product Type - Can be PRODUCTS, ACTS, or ALL.
- Location - Optional. The latitude and longitude of the consumer phone.
See code samples to the right.
Click and Collect
Click and Collect enables users to select products online, and pick them up in-store. The consumer may either pay online, or pay in-store.
Please note that your Resolve Commerce Engine must have Pick Up In Store enabled under the Advanced menu, for this option to apply.
The Click and Collect flow is no different from in the Cart Buy or Instant Buy examples, it is only the user-chosen values that change.
1. Choices returned by PaymentOptionManager
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"
rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<PaymentOption, RezolveError>) in
switch result {
case .success(let option):
{
// For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
let paymentMethod = option.supportedPaymentMethods.first!
let shippingMethod = option.supportedDeliveryMethods.first!
}
case .failure(let error):
// Handle error gracefully
}
})
paymentOptionManager.getProductOptions(checkoutProduct, merchantId, new PaymentOptionCallback() {
@Override
public void onProductOptionsSuccess(PaymentOption paymentOption) {
setPaymentOption(paymentOption);
}
@Override
public void onError(@NonNull final RezolveError error) {
}
});
When you call PaymentOptionManager.getProductOptions
, it returns a list of your payment and shipment options.
2. Inspecting the returned options
{
"type": "standard",
"supported_shipping_methods": [
{
"execute": {
"method_code": "flatrate",
"manual_payment_label": "Pay on Collection",
"extension_attributes": null,
"display_name": "Fixed",
"carrier_code": "flatrate"
},
"details": {
"store_details": null
}
},
{
"execute": {
"method_code": "storepickup",
"manual_payment_label": "Pay on Collection",
"extension_attributes": [
{
"value": "2",
"code": "pickup_store"
}
],
"display_name": "Pickup In Store",
"carrier_code": "storepickup"
},
"details": {
"store_details": {
"telephone": "01234456789",
"pickup_store": 2,
"opening_times": [
],
"name": "Chao Yang Store",
"location": {
"longitude": 116.2573779,
"latitude": 39.9390628
},
"email": "china2@storepickuptesting.com",
"description": null,
"address": {
"street2": "",
"street1": "14 Dongsuan Huan Beilu",
"region": "Beijing",
"post_code": "100026",
"country": "\u00e4\u00b8\u00ad\u013a\u203a\u02dd",
"city": "BEIJING"
}
}
}
},
{
"execute": {
"method_code": "storepickup",
"manual_payment_label": "Pay on Collection",
"extension_attributes": [
{
"value": "1",
"code": "pickup_store"
}
],
"display_name": "Pickup In Store",
"carrier_code": "storepickup"
},
"details": {
"store_details": {
"telephone": "01234456789",
"pickup_store": 1,
"opening_times": [
],
"name": "China Qingdao Shi Store",
"location": {
"longitude": 36.3867744,
"latitude": 117.6466473
},
"email": "chinar@storepickuptesting.com",
"description": null,
"address": {
"street2": "",
"street1": "63 RENMIN LU",
"region": "Shandong",
"post_code": "266033",
"country": "\u00e4\u00b8\u00ad\u013a\u203a\u02dd",
"city": "QINGDAO SHI"
}
}
}
}
],
"supported_payment_methods": [
{
"type": "union_pay",
"data": {
"supported_types": [
],
"supported_shipping": [
"storepickup",
"flatrate"
],
"supported_networks": [
],
"requirements": {
"rezolve_phone": true
}
}
}
],
"options": {
},
"id": "aa665b54-7166-4a53-a275-9b4f693770dc"
}
To the right is a sample of data returned by PaymentOptionsManager.getProductOptions
. Look at the supported_shipping_methods
node at the top of the file. If the list contains at least one child with execute.method_code: storepickup
it means user can buy the product with Click and Collect. Every store where collecting the purchase is available will be listed as a separate item under supported_shipping_methods
.
At this point the user should select a payment method. They are listed under the supported_payment_methods
node. In some cases, like the example shown, there is one payment method available. In others, there might be more. For example, if SupportedPaymentMethod has type: cash
, cash payment may only be allowed with Click and Collect and not in Delivery. To verify that we need SupportedPaymentMethod
to be provided as an argument when creating a DeliveryUnit
.
3. Creating the Delivery Unit/Delivery Method
// Standard shipping example
let deliveryMethod = CheckoutShippingMethod(type: "flatrate", addressId: address.id)
// Click and Collect example
let deliveryMethod = CheckoutShippingMethod(type: "storepickup", pickupStore: store.pickupStore)
// standard shipping example
DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, address.getId());
// Click and Collect example
DeliveryUnit deliveryUnit = new DeliveryUnit(supportedPaymentMethod, Integer.valueOf(supportedDeliveryMethod.getShippingMethod().getExtensionAttributes().get(0).getValue()));
Android calls this object DeliveryUnit
, IOS uses DeliveryMethod
DeliveryUnit
(Android) is created using the chosen supportedPaymentMethod
and the address id of the pick up store.
DeliveryMethod
(IOS) has no need of an id for the object creation, so an empty String
used to feel the addressId
present in the constructor.
See the extension_attributes
node for this store id.
"extension_attributes": [ { "value": "2", "code": "pickup_store" } ]
Create the DeliveryUnit
/DeliveryMethod
as shown at right.
DeliveryUnit
/DeliveryMethod
is then passed to create the CheckoutBundleV2
, which is used to get totals before purchasing the product.
Background Listening
Background Listening enables a mode where your app listens for watermarked audio, and rather than displaying the linked item immediately, instead stores any items it detects (products or categories) in a list. When background listening is stopped, the list is presented to the consumer as a series of Touch Triggers for review and action. As the name "background listening" implies, listening can occur in the background, such as when the consumer is using another app, or when the phone is asleep. Background listening will pause if interrupted by a phone call, and then resume afterwards. Background listening requires that sound be detected through the phone's mic. Watermarked audio playing in another app while headphones are plugged in would not be detected.
The implementation for Android and IOS are somewhat different, so see individual sequence diagrams below.
IOS Background Listening
[ View full size ]
Android Background Listening
[ View full size ]
1. Start Background Listening, Listen, and Display Results
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.autoDetectManagerDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
try? scanManager.startAudioScan()
extension ViewController: AutoDetectManagerDelegate {
func onAutoDetectStopListening(resolved: [AutoDetectResult]) {
for item in resolved {
print(item.description())
}
}
func onAutoDetectError(error: String) {
// Handle error gracefully
}
}
package com.rezolve.demo.content;
import java.util.List;
import android.support.v7.app.AppCompatActivity;
import com.rezolve.demo.utilities.Constants;
import com.rezolve.sdk.RezolveSDK;
import com.rezolve.sdk.core.interfaces.AutoDetectInterface;
import com.rezolve.sdk.core.managers.AutoDetectManager;
import com.rezolve.sdk.model.shop.Category;
import com.rezolve.sdk.model.shop.Product;
// To use Rezolve Auto Detect, the main Activity of the app must implement AutoDetectInterface
public class AutoDetectSampleActivity extends AppCompatActivity implements AutoDetectInterface {
private AutoDetectManager rezolveAutoDetectManager;
@Override
public void onPause() {
// When app goes background the app should start the service
if (rezolveAutoDetectManager == null) {
// Request RezolveSDK for a instance of the AutoDetectManager
rezolveAutoDetectManager = RezolveSDK.getInstance().getRezolveSession().getAutoDetectManager();
}
// Start the service passing the caller Activity, an optional notification and the AutoDetectInterface Callback.
// Here we don't pass the optional notification, so the RezolveSDK will provide a default Notification for the app.
rezolveAutoDetectManager.startAutoDetectService(this, null, this);
super.onPause();
}
@Override
public void onResume() {
super.onResume();
// When the app is resumed we must stop the service, on stoping the service will return result to the callback method.
if (rezolveAutoDetectManager != null) {
rezolveAutoDetectManager.stopAutoDetectService(this);
}
}
@Override
public void onAutoDetectResults(List list) {
// On the callback method the app gets a heterogeneous list that may contain several types of Objects
int listSize = list.size();
for (int i = 0; i <= listSize; i++) {
Object item = list.get(i);
try {
// The item may be a Product
if (item instanceof Product) {
Product product = (Product) item;
// Or it may be a Pair containing a Category and the merchantId who owns the Category
} else if (item instanceof android.support.v4.util.Pair) {
Category category = (Category) ((android.support.v4.util.Pair) item).first;
String merchantId = (String) ((android.support.v4.util.Pair) item).second;
String categoryId = category.getId();
String categoryName = category.getName();
boolean categoryHasProducts = category.hasProducts();
boolean categoryHasSubCategories = category.hasCategories();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Use AutoDetectManagerDelegate (IOS) or AutoDetectManager (Android) to start listening for watermarked audio. When the user stops Background Listening, the results detected (if any) will be returned as an array. Iterate over this array to display detected items as Touch Triggers.
Note: if you want to enable "back" navigation to the item list after the consumer views a list item, you should persist the list in your code. The SDK only returns the list when AutoDetect is stopped; it does not persist the list for you.
1a. Note that Android offers a Notification Helper
package com.rezolve.demo.content;
import java.util.List;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.support.v7.app.AppCompatActivity;
import com.rezolve.demo.R;
import com.rezolve.demo.utilities.Constants;
import com.rezolve.sdk.RezolveSDK;
import com.rezolve.sdk.core.interfaces.AutoDetectInterface;
import com.rezolve.sdk.core.managers.AutoDetectManager;
import com.rezolve.sdk.model.shop.Category;
import com.rezolve.sdk.model.shop.Product;
import com.rezolve.sdk.notifications.NotificationActionReceivers;
// To use Rezolve Auto Detect, the main Activity of the app must implement AutoDetectInterface
public class AutoDetectSampleActivity2 extends AppCompatActivity implements AutoDetectInterface {
private AutoDetectManager rezolveAutoDetectManager;
@Override
public void onPause() {
// We may create a custom notification to be used with the service
Notification optionalCustomNotification = createOptionalNotification(this);
// When app goes background the app should start the service
if (rezolveAutoDetectManager == null) {
rezolveAutoDetectManager = RezolveSDK.getInstance().getRezolveSession().getAutoDetectManager();
}
// Start the service passing the caller Activity, an optional notification and the AutoDetectInterface Callback.
// Here we send the custom notifiction we have created
rezolveAutoDetectManager.startAutoDetectService(this, optionalCustomNotification, this);
super.onPause();
}
@Override
public void onResume() {
super.onResume();
// When the app is resumed we must stop the service, on stoping the service will return result to the callback method.
if (rezolveAutoDetectManager != null) {
rezolveAutoDetectManager.stopAutoDetectService(this);
}
}
@Override
public void onAutoDetectResults(List list) {
// On the callback method the app gets a heterogeneous list that may contain several types of Objects
int listSize = list.size();
for (int i = 0; i <= listSize; i++) {
Object item = list.get(i);
try {
// The item may be a Product
if (item instanceof Product) {
Product product = (Product) item;
// Or it may be a Pair containing a Category and the merchantId who owns the Category
} else if (item instanceof android.support.v4.util.Pair) {
Category category = (Category) ((android.support.v4.util.Pair) item).first;
String merchantId = (String) ((android.support.v4.util.Pair) item).second;
String categoryId = category.getId();
String categoryName = category.getName();
boolean categoryHasProducts = category.hasProducts();
boolean categoryHasSubCategories = category.hasCategories();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private Notification createOptionalNotification(Context context) {
// Here we create a regular notification to be presented by the service
String CHANNEL_ID = "220518";
int NOTIFICATION_ID = 230518;
int NOTIFICATION_TURN_OFF_REQUEST_CODE = 230519;
String ACTION_TURN_OFF = "action.turn.off";
PendingIntent onNotificationTapIntent = null;
Notification notification;
Notification.Builder notificationBuilder;
NotificationCompat.Builder notificationCompatBuilder;
Intent turnOffIntent = new Intent();
turnOffIntent.setAction(ACTION_TURN_OFF);
turnOffIntent.setClass(this, NotificationActionReceivers.TurnOffBackgroundListenerReceiver.class);
PendingIntent pendingTurnOffIntent = PendingIntent.getBroadcast(this, NOTIFICATION_TURN_OFF_REQUEST_CODE, turnOffIntent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationBuilder = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.default_notification_icon)
.setContentTitle(this.getString(R.string.notification_title))
.setContentText(this.getString(R.string.notification_text))
.setStyle(new Notification.BigTextStyle().bigText(this.getString(R.string.notification_big_text)))
.setAutoCancel(true)
.addAction(R.drawable.spinner, context.getString(R.string.notification_turn_off), pendingTurnOffIntent);
if (onNotificationTapIntent != null) {
notificationBuilder.setContentIntent(onNotificationTapIntent);
}
notificationBuilder.setOngoing(true);
notification = notificationBuilder.build();
} else {
notificationCompatBuilder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_slider_head)
.setContentTitle(context.getString(R.string.notification_title))
.setContentText(context.getString(R.string.notification_text))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_big_text)))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.addAction(R.drawable.spinner, context.getString(R.string.notification_turn_off), pendingTurnOffIntent);
if (onNotificationTapIntent != null) {
notificationCompatBuilder.setContentIntent(onNotificationTapIntent);
}
notificationCompatBuilder.setOngoing(true);
notification = notificationCompatBuilder.build();
}
return notification;
}
}
This helper can be used to display notification to the user that Background Listening is active.
2. Display item Consumer selects using Trigger Manager
// Sample URL and `TriggerManager` initialization
let url = URL(string: "http://rzlv.co/1/2/3/8?ad=20&placement=25")
rezolveSession?.triggerManager.resolve(
url: url!,
productDelegate: self,
onRezolveTriggerStart: {},
onRezolveTriggerEnd: {},
errorCallback: { error in }
)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
rezolveSession.getTriggerManager().resolveTrigger("http://rzlv.co/2/3/13/169?ad=20&placement=25", new TriggerInterface() {
@Override
public void onProductResult(Product product) { // display prouduct result
Log.d("resolveTrigger", "onProductResult: "+product.getTitle());
}
@Override
public void onCategoryResult(Category category, String merchantId) { // display category result
Log.d("resolveTrigger", "onCategoryResult: "+category.getName());
}
@Override
public void onBadTrigger() { // called when provided url has bad syntax or is too short
Log.d("resolveTrigger", "onBadTrigger");
}
@Override
public void onError(HttpResponse httpResponse) {
Log.d("resolveTrigger", "onError: "+httpResponse.getResponseJson());
}
});
Use TriggerManager to display a selected Touch Trigger item. You will have to pass a valid product url into TriggerManager, in the form:
http://rzlv.co/[partnerId]/[merchantId]/[categoryId]/[productId]?ad=[adId]&placement=[placementId]
Get partnerId
, merchantId
, categoryId
, productId
, adId
and placmentId
from the returned list array for that item. If no adId
and/or placementId
are available, omit the ?ad=[adId]&placement=[placementId]
part of the url (note that in this case, no stats will be gathered when this item is displayed or purchased).
Location Triggers
Location Triggers are a powerful SDK feature introduced in 2020 that allows the developer to detect when the consumer has entered a predefined geozone and pop a notification of a product, informational page, or Act. A geozone is defined as a radius of X meters around a specified lat/long point, as set up by a merchant or partner in the Rezolve portal.
When the app is active, the SDK continuously samples the consumer's location, and when the consumer is within the radius of one or more geozones, events fire to alert the system to perform the desired behavior. The SDK intelligently handles areas with a high density of overlapping geozones to prevent spamming the user with notifications. If the consumer is in an area with multiple geozones, the SDK bundles multiple notifications into one alert that contains the messages from all the relevant zones.
Topics
- Setup location detection
- Initializing location detection
- Detecting geozones
- Handling detection and notification
Setup location detection
// Add the following permission descriptions on your Info.plist.
- NSLocationAlwaysUsageDescription
- NSLocationAlwaysAndWhenInUseUsageDescription
- NSLocationWhenInUseUsageDescription
// Enable Background Modes for your app’s target.
- Location Updates
- Background Fetch
// Taken from App.java in the Sample App project
// Set up a Notification Helper
// Notes:
// Geofence and Location Detection service share the same notification
// See geofenceForegroundNotificationProperties to define the look of the notification
// Foreground notification is required for the foreground service.
// This prevents the app from getting killed by the system.
NotificationHelper notificationHelper = new NotificationHelperImpl(this);
Notification notification = notificationHelper.createNotification(
1,
getString(R.string.app_name),
null,
null,
null,
geofenceForegroundNotificationProperties
);
This section describes how to set up your project to use the location features of the Rezolve SDK.
Initializing location detection
// On Rezolve class initialization you need to pass your SSP configuration into sspActManagerSettings parameter, which is a class of RezolveSDK.SspActManagerSettings. As an example, it has the following structure.
let sspActManagerSettings = SspActManagerSettings(
auth0ClientId: "{String}",
auth0Secret: "{String}",
auth0Audience: "{String}",
auth0Endpoint: "{String}",
sspEndpoint: "{String}",
sspWidth: "{String}",
baiduLocationKey: "{String}"
)
// Adding a baiduLocationKey is not mandatory, but will definitely increase tracking accuraccy if you are targeting regions located in China mainland.
// Taken from App.java in the Sample App project
// Create and start Location Provider
// Make sure you have secured the location permission before starting the location provider.
final LocationProviderFused locationProviderFused = LocationProviderFused.create(this, notification);
locationProviderFused.start();
// Next, set up Geofence detection and Resolvers
// Set up SspActManager
SspActManager sspActManager = new SspActManager(httpClient);
// Set up Rezolve Configuration Builder (this also supports the image/audio Scanner function)
new ResolverConfiguration.Builder(rezolveSDK)
.enableBarcode1dResolver(true)
.enableCoreResolver(true)
.enableSspResolver(sspActManager, 400)
.build(this);
// Set up GeofenceMananger
final GeofenceManager geofenceManager = new GeofenceManager.Builder()
.sspActManager(sspActManager)
.engagementsUpdatePolicy(new EngagementsUpdatePolicy.Builder().build())
.notificationChannelPropertiesList(geofenceLocationChannels)
.engagementAlertNotification(geofenceAlertNotificationProperties)
.context(this)
.build();
Initializing location detection requires setting up the SspActManager
with appropriate settings.
Detecting geozones
// Setting up Rezolve SDK with an active SSP Act Manager configuration.
let sdk = Rezolve(apiKey: Config.rezolveApiKey,
partnerId: Config.partnerId,
subPartnerId: nil,
environment: Config.env,
config: nil,
sspActManagerSettings: sspActManagerSettings,
coordinatesConverter: CoordinatesConverter.default)
// Start monitoring for Nearby Engagements.
let ssp = RezolveService.sdk?.createRezolveSsp()
ssp?.nearbyEngagementsManager.startMonitoringForNearbyEngagements()
ssp?.nearbyEngagementsManager.debugNotifications = false
ssp?.nearbyEngagementsManager.delegate = self
// Stop monitoring for Nearby Engagements.
ssp?.nearbyEngagementsManager.stopMonitoringForNearbyEngagements()
ssp?.nearbyEngagementsManager.stopUpdatingDistanceFromNearbyEngagements()
ssp?.nearbyEngagementsManager.resetNotificationSuppressData()
ssp?.nearbyEngagementsManager.delegate = nil
// Taken from App.java in the Sample App project
// Make sure the location provider is started before detection.
// Based on the provided EngagementsUpdatePolicy the GeofenceManager will manage geofence updates
final GeofenceManager geofenceManager = new GeofenceManager.Builder()
.sspActManager(sspActManager)
.engagementsUpdatePolicy(new EngagementsUpdatePolicy.Builder().build())
.notificationChannelPropertiesList(geofenceLocationChannels)
.engagementAlertNotification(geofenceAlertNotificationProperties)
.context(this)
.build();
registerGeofenceListener();
locationProviderFused.start();
geofenceManager.startGeofenceTracking();
private void registerGeofenceListener() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_GEOFENCE_NOTIFICATION_DISPLAYED);
intentFilter.addAction(ACTION_GEOFENCE_NOTIFICATION_SELECTED);
registerReceiver(geofenceBroadcastReceiver, intentFilter);
}
BroadcastReceiver geofenceBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final String sender = intent.getStringExtra(KEY_SENDER_PACKAGE_NAME);
if(!context.getPackageName().equalsIgnoreCase(sender)) {
Log.d(TAG, "Ignoring intent from: " + sender +", expected: " + context.getPackageName());
return;
}
if(action != null) {
switch (action) {
case ACTION_GEOFENCE_NOTIFICATION_DISPLAYED: {
final String name = intent.getStringExtra(KEY_NAME);
final String shortDescription = intent.getStringExtra(KEY_DESCRIPTION_SHORT);
final String actId = intent.getStringExtra(KEY_ACT_ID);
final SspObject sspObject = getSspObjectFromIntent(intent);
Log.d(TAG, action + ": " + name + ", " + shortDescription + ", " + actId + ", " + sspObject);
break;
}
case ACTION_GEOFENCE_NOTIFICATION_SELECTED: {
final SspObject sspObject = getSspObjectFromIntent(intent);
Log.d(TAG, action + ": " + sspObject);
break;
}
}
}
}
@Nullable
private SspObject getSspObjectFromIntent(@NonNull Intent intent) {
SspObject sspObject = null;
if(intent.hasExtra(KEY_SSP_ACT)) {
try {
sspObject = SspAct.jsonToEntity(new JSONObject(intent.getStringExtra(KEY_SSP_ACT)));
} catch (JSONException e) {
e.printStackTrace();
}
} else if(intent.hasExtra(KEY_SSP_CATEGORY)) {
try {
sspObject = SspCategory.jsonToEntity(new JSONObject(intent.getStringExtra(KEY_SSP_CATEGORY)));
} catch (JSONException e) {
e.printStackTrace();
}
} else if(intent.hasExtra(KEY_SSP_PRODUCT)) {
try {
sspObject = SspProduct.jsonToEntity(new JSONObject(intent.getStringExtra(KEY_SSP_PRODUCT)));
} catch (JSONException e) {
e.printStackTrace();
}
}
return sspObject;
}
};
During detection, the user's location is monitored. Detection times vary according to OS settings.
For IOS, the following applies: "When testing your region monitoring code in iOS Simulator or on a device, realize that region events may not happen immediately after a region boundary is crossed. To prevent spurious notifications, iOS doesn't deliver region notifications until certain threshold conditions are met. Specifically, the user's location must cross the region boundary, move away from the boundary by a minimum distance, and remain at that minimum distance for at least 20 seconds before the notifications are reported.
The specific threshold distances are determined by the hardware and the location technologies that are currently available. For example, if Wi-Fi is disabled, region monitoring is significantly less accurate. However, for testing purposes, you can assume that the minimum distance is approximately 200 meters."
Handling detection and notification
// Handle incoming changes regarding Geofence status from the SDK.
extension RezolveGeofence: NearbyEngagementsManagerDelegate {
func didStartMonitoring(for circularRegions: [CLCircularRegion], coordinate: CLLocationCoordinate2D, radius: Int) {
print("didStartMonitoring")
}
func didEnter(circularRegion: GeofenceData) {
print("didEnter")
}
func didFail(with error: Error) {
print("didFail -> \(error.localizedDescription)")
}
func didUpdateCurrentDistanceFrom(location: CLLocationCoordinate2D, geofences: [GeofenceData], beacons: [BeaconData]) {
print("didUpdateCurrentDistanceFrom")
}
func didReceiveInAppNotification(act: SspResolverAct?) {
print("didReceiveInAppNotification")
}
}
// Taken from App.java in the Sample App project
// The SDK sends the following broadcast messages (see code sample in "Detecting Geozones" above for details):
// When the notification is displayed in the notification center, the broadcast action is set to:
ACTION_GEOFENCE_NOTIFICATION_DISPLAYED:
// And when the user taps the notification, the broadcast action is set to:
ACTION_GEOFENCE_NOTIFICATION_SELECTED:
// Notes:
// The SDK shows a notification when it detects the geofence zone.
// The SDK calls the server endpoint to get the object details, and if the object is active
// it will show the notification and send the ACTION_GEOFENCE_NOTIFICATION_DISPLAYED broadcast.
// Use the geofenceBroadcastReceiver to fetch details about the trigger and SspObject.
// The fetched ssp object has to be in an active state for notification to be shown.
// The SDK prevents showing duplicate notifications if the same geofence was detected
// during the silent period defined in the EngagementsUpdatePolicy.
When a geofence is detected, notification is potentially shown, as long as it is not within a silent period from a previous detection.
Act
An Act is a type of consumer questionnaire that can be created in the Rezolve Commerce Engine. Acts are used to solicit information and collect consumer answers and contact information. No payment is required to submit an Act.
There is no specific data structure within the SDK that represents an Act; data-wise, it is a particular configuration of product that has no weight and no price. If your application receives a product with no weight and no price, you should display it as an Act. You do this by omitting certain information when displaying the item. You must do this in four places:
- The Product detail view
- The Post-Purchase screen
- The My Activity list view
- The My Activity detail view
Product Detail View
Remove price, quantity, payment details, delivery details, order summary, and the add-to-cart icon. Change the "Instant Buy" text to "Act Now".
[ View full size ]
Post-Purchase Screen
Omit quantity, price, payment details, delivery details, order summary, and pick-up-location map.
[ View full size ]
My Activity List View
Omit the Order Total currency amounts from this screen. Change the order status text to "submitted".
[ View full size ]
My Activity Detail View
Change "Payment Successful" header to "Submitted". Remove "order" text from "Your order ref:". Omit quantity, price, payment details, delivery details, order summary, pick-up-location map, and QR code area.
[ View full size ]
Error Handling
{
"errors":[
{
"type":"<error_type>",
"code":"<error_code>",
"message":"<error_message>"
}
]
}
All SDK methods use a uniform error format, as shown at the right. Possible values are:
Type | Error Code | Message |
---|---|---|
Authentication | 1 | Access is denied. |
Registration | 2 | Registration failed. |
Bad Request | 3 | Missing required parameters in request body. Required: ((required_parameters)) |
Not Found | 4 | Requested resource not found |
Bad Request | 5 | Incorrect or malformed parameters |
Bad Request | 6 | ((system message)) |
Service Unavailable | 7 | Our apologies for the temporary inconvenience. The requested URL generated 503 Service Unavailable error due to overloading or maintenance of the server. |
Module Reference Docs
Module reference docs are generated from commented source code. IOS docs are generated using Jazzy (https://github.com/realm/jazzy) and Android docs are generated using Doxygen (http://www.doxygen.nl/).
The following links will open in a new window. Close the window to return to this documentation.
IOS Module Documentation
Android Module Documentation
Changelog
All notable changes to the project will be documented in this log.
Feb 11, 2021
- Added documentation on how to detect and handle SSP-based engagements for Android and IOS.
Android 3.1.0 - Jan 27, 2021
- Added AdditionalAttribute list to Order
- Added GeozoneNotificationCallback and stop using BroadcastReceiver to pass to app Display action of notification.
- Added Cloneable to CheckoutProduct
- Added Cloneable to ConfigurableOption and CustomConfigurableOption
- Added engagementId to ScannedData
- Added EventReportInterface
- Added PartnerSettings to EventReportManager
- Added engagementId and engagementName into SspObject
- Added common elements into SspObject.entityToJson implementation
- Added getMerchantId method to SspObject
- Added form builder models.
- Added CurrencyInformation to Merchant
- Added currencyInformation fields getters
- Added user fields (name, location, phone, email) to SspActSubmission
- Added ACCESS_BACKGROUND_LOCATION permission to ssp-google-geofence-detector module
- Added serializable implementation to CheckoutProduct and its fields
- Added check for empty first or last name to CustomerProfileManager.update call
- Added custom payload to OrderDetails
- Added new constructor for (Custom?|ConfigurableOption)
- Added Resolvable.isExclusive()
- Added new enum Resolvable.LOCAL_EXCLUSIVE and Resolvable.SERVER_EXCLUSIVE. In exclusive mode, only the first matching resolver will process the payload.
- Added a ProGuard configuration in the SDK to ensure that app modules that depend on the SDK do not have to manually update their ProGuard files to use our library.
- Added ENTITY_DOESNT_EXIST error type to RezolveError class
- Change: AuthParams class for SSP authentication now requires two extra params: engagement endpoint and act endpoint
- Change: SspActManager now requires SspHttpClient instead of HttpClient.
- Change: LocationProviderFused now only require single argument (context).
- Change: renamed ScanHelper.isBarcodeScan to ScanHelper.is1DBarcodeScan to differentiate from QR scan.
- Change: Allow creating SspActQuestions for PageBuildingBlocks.
- Change: change name of EventReportInterface to SendEventReportCallback
- Change: Set EventReport date when the event is created.
- Fixed crash when an opaque URI is passed to ScannedData.parse() method
- Removed setPartnerId and setDateTime, setOS from EventReport.Builder
- Removed Product references from Order
- Removed geozone notification if detection is handled by any of the callbacks
Android 3.0.0 - Sept 2, 2020
- Changed compatible Android Scan module version to 3.0.0
Android 3.0.0 - July 1, 2020
- Added StoreDetails object to OrderDetails.
- Added SSP module.
- Added guest-login module.
- Added Geozone detection
- RezolveSDK divided into separate modules: core, payment and scan.
- Changed AudioDetectService, now sends merchant_id and is_act flag for product response.
- Changed to improve visibility for javadocs and variables after importing the sdk.
- Changed ScanManagerInterface, no longer extends ErrorInterface. All scan errors will be returned in onScanError callback that now includes ScannedData.
- Changed AudioDetectService intent_extra_merchant_name, changed to intent_extra_merchant_id.
- Changed onProductResult from ScanManagerInterface and TriggerInterface to add categoryId to returned data
- Changed StoreDetails 'pickupStore' field name to 'pickupStoreId'.
- Changed permission requirements: Only one of "android.permission.CAMERA" and "android.permission.RECORD_AUDIO" is now required to initialize ScanManager.
- Changed ScanManager, now requires BarcodeMode enum instead of boolean to set the how the manager should deal with barcodes.
IOS 2.0.4.1 - May 15, 2020
- Added Geofence support
- Added SSP Manager integration
- Added tests for SSP module
- Added support for SSP products
- Complete refactor of the SDK utilizing new Codable protocol and Unit Test implementation
- Updated to
XCode 11.4
- Updated with merged Baidu Geofence framework with internal Geofence algorithm
- Updated unit tests for Geofencing methods
IOS 1.11.31 - January 28, 2020
- Update to invoke
TriggerManager
automatically on 1D Barcode detection.
IOS 1.11.30 - January 23, 2020
- Supressed consecutive
Background Listening
notifications every time the SDK is sent to the background. - Migration to
Xcode 11.3.1
. - Added
EAN13
Symbology for improved 1D Barcode detection.
Android 2.3.1, IOS 1.11.28 - November 21, 2019
Cumulative release of several minor version updates.
Android Changes
- Implement logic in Android SDK to Support 1D Barcode Scanning
- Fix QR code scanning currently recognizes QR code as 1d barcode
- Add a ProGuard configuration in the SDK to ensure that app modules that depend on the SDK do not have to manually update their ProGuard files to use Rezolve library.
IOS Changes
- Updated
AuthenticationManager
public initializer. - Migration to
Xcode 11.2
and SwiftSwift 5.1.2
.
Android 2.2.0 - October 10, 2019
This release is Android only.
Android Changes
- StoreDetails class added to OrderDetails.
- Added isVirtual field to PriceOption
- Added equals, hashCode implementation to PriceOption
- Added doxygen comments for RezolveLocation, StoreAddress and StoreTime.
- isDefault field added to ShippingMethod. Added doxygen comments.
- Constructors of ShippingMethod and ExtensionAttribute are now private. Added missing toString(), equals(Object o) and hashCode() methods for these classes.
- Add logic to support override settings of resolver based on app flavour
- Implement logic to support the new status for order "pre-canceled" which sits between "pending" and "cancelled"/"complete"
- On failed scan, return ScannedData in onScanError callback, if available.
- Added new RezolveError.
- Added flag to CustomerProfile
- Change to avoid reusing ScanManager when requested manager has a different params.
- Fix extra's name for AudioDetectService.
- Add merchant_id to product response.
- Added documentation of Merchant class
- Added documentation of MerchantManager class
- Added documentation of Order class
- Added documentation of OrderHistoryObject
- onProductResult from ScanManagerInterface and TriggerInterface now return categoryId
- Allow ScanManager initialization when only one of required permissions is given
- Pass information if scanned product is an ACT or not.
IOS 1.11.26 - September 24, 2019
This release is IOS only.
- Compiled SDK with latest Xcode 11 & Swift 5.1.
- Removed the need for having the new Mac Catalyst feature enabled
IOS Changes
- Compiled SDK with latest Xcode 11 & Swift 5.1
- Removed the need for having the new Mac Catalyst feature enabled
Android v2.1.0, IOS v1.11.25 - June 26, 2019
Documentation Changes
- Added tutorial for Search
- New install instructions for IOS SDK (CocoaPods)
- Updated module reference docs for both platforms
IOS Changes
- Added new method in Product Manager,
getAdditionalAttributes
- Added new method in Product Manager, allowing searching for products based on various criteria.
- Added new method in Merchant Manager, allowing searching for merchants based on various criteria.
- Added new, optional parameter fullName to Address object.
- Added distance property to Merchant's model.
- Added new property PriceOption to Product object.
- Added option to suspend and resume the process of audio/video capturing during scan.
- Added explicit handling of Act products with new is_act parameter on Product model.
- Added new full_name parameter on Address model to dinstinguish from address_2.
- Added phone_book_id parameter on Address model to allow for uniue Phone Number on each Address model.
- Added getRootCategory endpoint, gets the root category for the selected merchant
- Added getCartProduct added for fetching product information when there is no category
- Added getMerchants endpoint, added optional parameter to fetch only active Merchants, represented as Bool
- Updated getProducts
- Updated PageResult's sorting direction now uses enum instead of string.
- Updated getCategories endpoint
- Misc bug fixes & small optimizations
Android Changes
- Added hashCode, equals implementation for CustomOption, OptionValue
- Added toString implementation to CustomOption, OptionValue models
- Added HttpClientConfig to RezolveSdk.Builder
- Added missing interface to DisplayProduct.
- Added default page navigation filter.
- Added product and category ids.
- Added a getter for scanned objects.
- Added ScanManagerHelper.
- Added .equal() methods for Placement and CheckoutProduct.
- Added hashCode methods for CheckoutProduct and Placement.
- Added missing flags to DisplayProduct.
- Added equals and hashcode methods added for ConfigurableOption and CustomConfigurableOption.
- Add visibility flag to the /merchant call.
- Added CustomAttribute class.
- Added Multiple Exception types catching in HttpClient.enqueueRequest to reduce code duplication
- Added equals, hashCode and toString methods for PaymentCard.
- Added proguard rule for MerchantFilter
- Added merchants search method.
- Added MerchantSearchResult and DisplayStore models.
- Added required fields to DisplayProduct and Merchant. Use SearchMerchantResult and SearchProductResult as wrappers.
- Added TermsAndCondtitions.
- Added merchant_id as a param to SearchData.
- Added ProductSearchData and MerchantSearchData.
- Added toString, eqals and hashcode methods to new and modified classes.
- Added ProductSearchInterface and MerchantSearchInterface.
- Added "total" field to search responses.
- Added "itemsPerPage" to SearchData.
- Added support for too large quantity error
- Added missing return statement in Links.jsonToEntity
- Added factory method to create PageNavigationFilter from Link object
- Added getOkHttpClient to HttpClient
- Added distance field to Merchant
- Changed /options endpoint to v4
- Fixed rder details under my Activity shows QR for Delivery orders
- Changed /category endpoint used by ScanManager to v2.
- Changed item limit back to 30.
- Changed product to be parcelable
- Changed torch support methods to reside in another class.
- Changed all interfaces to extend error interface.
- Changed scan logic to move away from ScanManager.
- Changed image reader to reside in ScanImageHelper class.
- Changed scan audio logic moved to ScanAudioHelper
- Changed RezolveScanResult modification to prevent returning to the user if the engagement has expired.
- Changed clear cache thread priority to background. Make Credentials fields final.
- Changed so allowed to add interceptors to HttpClientConfig.
- Changed OrderDetails status to enum.
- Changed; if scanned image is a QR code, check if it belongs to rezolve.
- Change type of returned scan errors.
- Fixed return error type for scan error.
- Changed new 'include_cart_button_on_listing' flag to be handled by Android SDK
- Changed cart buttons flag by moving to Category.
- Fixed query param name.
- Changed CustomAttribute to AdditionalAttribute
- Fixed bug in HttpClient.enqueueRequest method that could crash the app.
- Changed HttpResponse implementation to throw catched exceptions.
- Changed MerchantFilter enum name to MerchantVisibility.
- Changed override onSearchMerchantsSuccess in MerchantCallback
- Changed do not set param to all when product type is null. This option is not supported yet on the - server side.
- Changed SearchOrderBy by replacing with MerchantSearchOrderBy, ProductSearchOrderBy
- Changed search data param include_in_result -> include_in_results
- Fixed hashCode, equals implementation in (Merchant|Product)SearchData
- Fixed bug in DisplayProduct.jsonToEntity
- Changed include_in_results param to have lowercase values (all, products, acts)
- Changed Link[] to Links object with named fields
- Deprecated old getMerchants method.
- Deprecate old /category methods.
Android v2.0.1, IOS v1.11.20 - Nov 21, 2018
Documentation Changes
- Added tutorial on how to present data for an Act.
- New install instructions for IOS SDK (CocoaPods)
- New install instructions for Android SDK (Nexus/Maven)
IOS Changes
HistoryItemProduct
implementation changed- Scanmanager internal libraries updated
CheckoutBundleV2.deliveryMethod
changed type toDeliveryMethod?
CheckoutBundleV2.paymentMethod
changed type toSupportedPaymentMethod?
createProductCheckoutBundleV2
changed parameter type fromdeliveryMethod
toDeliveryMethod
createProductCheckoutBundleV2
changed parameter type frompaymentMethod
toSupportedPaymentMethod?
createCartCheckoutBundleV2
changed parameter type fromdeliveryMethod
toDeliveryMethod
createCartCheckoutBundleV2
changed parameter type frompaymentMethod
toSupportedPaymentMethod?
Android Changes
- Added abort purchase method to cancel orders without cleaning the cart on QuickPass canceling.
- Fixed wrong base price with certain options
Android v2.0.0, IOS v1.11.10 - October 11, 2018
Major Documentation Changes
- Added tutorial for Trigger Manager
- Added tutorial for Background Listening
- Added tutorial for Click & Collect
- Removed manually created module reference docs and replaced them with module reference docs generated from source.
IOS
Added
- Support for virtual products
x-rezolve-device-id
for all Core callsauthorizationCallback
in DataClient so we can intercept when 401 is thrown by the serverPaymentOptionManager
CheckoutManagerV2
CheckoutBundleV2
Changed
- License file
- Updated
deviceId
payload key for login calls - New
paymentOption
attribute onCheckoutBundle
CheckoutBundle
helper functions have a new parameter:paymentOption
- New attribute
orderId
on Order entity CheckoutManager.checkout(bundle:callback:errorCalback:)
is now supportingpaymentOption
CheckoutManager.buy(bundle:paymentRequest:location:phone:callback:errorCallback:)
is now supportingpaymentOption
- Updated
CheckoutOrder
object with new properties - Updated scan resolver
- Fixed deserialisation method from history transaction.
- Fixed
CartManager.removeCartProduct(merchantId:cart:product:callback:errorCallback:)
removal of products cotaining options or ACT - Fixed
CartManager
: The payment_option key removed from the payload - Fixed
InStorePickupManager
: Thepayment_option
key removed from the payload - Fixed On createSession 401 weren't dispatching properly error to app handle it.
- Fixed
PaymentOptionManager.getProductOptions(checkoutProduct:merchantId:callback:errorCallback:)
updated JSON sent to server - Fixed Regression serialisation on
CustomOption.swift
getScanManager
now accept username, password and env as parameter- Deprecated
ProductManager.getProductsAndCategories
, useProductManager.getProductsAndCategoriesV2
instead. - Deprecagted
ProductManager.getCategories
useProductManager.getPaginatedCategories
instead - Breaking change:
Merchant.contactInformation
type changed from tuple to class - Breaking change: Order have a new property orderId
- Breaking change:
HistoryTransaction
model updated - Breaking change: Change type of "value" member on
CustomOption
fromAny?
to[String]
- Breaking change:
PaymentOptionManager.getProductOptions(checkoutProduct:merchantId:callback:errorCallback:)
updated JSON sent to server
Deleted
- Removed Websocket support from SDK
- Custom Option Array Fix.
Android
Added
- List of
CustomConfigurableOptions
added toOrderProduct
class. CustomOption
fields for user input.getOrdersV2
method inUserActivityManager
NotificationHelper
class.TriggerManager
class.isVirtual
flag for Product.PaymentOptionManager
CheckoutManagerV2
CheckoutBundleV2
DeliveryMethod
- Rezolve Auto Detect Service to listen for ads in backgorund
- Credit Cards may have the number fully edited by users
- Auto Torch for the scan screen. If the device has a light sensor the scan screen is able to turn on the flash light automatically to help on capture process.
- Rezolve Auto Detect Service to listen for ads in backgorund
- Credit Cards may have the number fully edited by users
- Read custom resolver url from shared preferences.
Changed
CustomConfigurableOptions
added toCheckoutProduct
.- Fixed parsing methods for shipping details.
CustomOption
optionId is now an integer.- Fixed a bug on
CustomOption
validator that prevented the validation of dates. - Fixed QR codes scan.
- Fixed the location sent on a buy if the user didn't allow location on the app.
- Fixed several improvements and bugfixes on the video and audio scan process.
- New LICENSE.TXT file with updated use terms.
- Fixed a bug where starting the Rezolve Auto Detect Service before login could crash the SDK.
- Fixed an issue that could crash the SDK if a credit card data was updated without changes on the credit card number.
- Fixed
StoreAddress
model. - Fixed issue when
ScanManager
torch methods were not responsive.
Android v1.7.5, IOS v1.7.6.1 - July 9, 2018
Note: This update only changes the IOS version.
Changed
- Updated IOS version to be compatible with IOS version 9 and onward, instead of 10 and onward
- Updated IOS to be compatible with latest releases of SWIFT (4.x) and XCode (9.x)
Android v1.7.5, IOS v1.6.5 - May 5, 2018
Added
- New section on Generic JWT Authentication, with sequence diagram.
- AuthenticationManager.createSession, v2, added.
MerchantManager
class addedCheckoutBundle
class added (see CheckoutManager class for details)ProductManager.getgetProductsAndCategories
method addedProductManager.getParentCategory
method added (Android Only)ProductManager.getCartProduct
method added (Android Only)
Changed
- Instancing the SDK via
RezolveSDK.getInstance
used to take an enum to specify environment. This is now takes a string.RezolveSDK.getInstance(String API_KEY, String ENVIRONMENT)
. For the Sandbox, ENVIRONMENT should equalsandbox-api-tw.rzlvtest.co
in IOS, andhttps://sandbox-api-tw.rzlvtest.co/api
in Android. Changed in many code samples. - Updated all sequence diagrams for v2 authentication, using JWT.
- Added new SDK download process.
- Product Scan, Instant Buy Flow -
- Updated code samples for
CheckoutManager
changes. - Revised sequence diagram.
- Updated code samples for
- Product Scan, Cart Flow
- Updated code samples for
CheckoutManager
changes. - Updated Category structure.
- Revised sequence diagram.
- Updated code samples for
- Category Scan Flow
- Simplified calls.
- Updated Category structure.
- Revised sequence diagram.
- Mall flow
- Updated
getMerchants
method - Updated Category structure. Simplified subsequent calls
- Revised sequence diagram.
- Updated
AuthenticationManager
class.register
deprecated- v1
.createSession
deprecated - v1
.logout
deprecated - V2
.createSession
has been added, using JWT accessToken, and removing deviceProfile. CreateSession change made to all sequence diagrams.
CheckoutManager
class- Added CheckoutBundle class documentation
- Revised checkout methods and purchase methods to use CheckoutBundles
ProductManager
class
getCategory
now takes category object as a parameter, instead of category id.getProduct
now takes a product object as a parameter, instead of a product id.onGetProductsSuccess
now usesgetItems
instead ofgetProducts
to extract DisplayProducts.- Updated
Category
objects
Deleted
- The enum to represent the target Rezolve environment
RezolveSDK.Env.EvironmentName
is removed. - DeviceProfile object removed from createSession.
- Removed Merchant handling from ProductManager
Android v1.5.37, IOS v1.5.40 - Dec 19 2017
Added
- PhonebookManager Class
- MerchantManager Class
- ProductManager.getCartProduct
- ProductManager.getParentCategory
- ProductManager.getProductsAndCategories
Changed
- Product Scan, Cart Flow example - updated code samples to reflect changes in CheckoutManager, rezolveLocation object
- Product Scan, Cart Flow example - updated sequence diagram and narrative to better describe role of signOrderUpdates call.
- Product Scan, Instant Buy flow example - updated code samples to reflect chnges in CheckoutManager, rezolveLocation object
- Product Scan, Instant Buy flow example - updated sequence diagram and narrative to better describe role of signOrderUpdates call.
- CheckoutManager.buyCart - added phonebookId to method parameters. Removed "Type" from RezolveLocation object.
- CheckoutManager.buyProduct - added phonebookId to method parameters. Removed "Type" from RezolveLocation object.
- CheckoutManager.signOrderUpdates - remove from response:
- Transaction.transactionAmount (Order object)
- Transaction.transactionItems list
- remove Transaction.Timestamp from response
- CheckoutManager.signOrderUpdates - clarified narrative describing socket listener behavior.
- ProductManager.getProduct - change method name from product.getOptionAvailable() product.getOptionsAvailable() (plural)
- UserActivityManager.getOrders request - add "from" and "to" date parameters to request parameters
- UserActivityManager.getOrders response - orderDetails.getItems() now returns a list of OrderProduct objects, instead of HistoryItem objects
- UserActivityManager.getOrders response - remove from the OrderDetails object:
- Type
- RezolveLocation.Type
- UserActivityManager.getOrders response - rename object FinalPrice to Price
- UserActivityManager.getOrders response - add to the OrderDetails object:
- Partner Id
- Billing Address
- Shipping Address
- First Name
- Last Name
- Phone
- Merchant Email
- Merchant Name
- Merchant Phone
Deleted
- none