IOS SDK Introduction
Note:
Code samples will appear in this column.
The Rezolve InsideTM SDK is a software development kit that enables mobile app developers to integrate Rezolve’s mobile commerce and engagement capabilities into their new or existing mobile apps.
The Rezolve SDK communicates with the Rezolve online product stack – a set of dashboards and services designed to provide retailers and brands with everything they need to promote and sell via Rezolve, and also provide consumers with a seamless mobile experience.
Reference App
A Rezolve Reference App is available. This app was designed to showcase the unique capabilities of the Rezolve system, and to demonstrate the consumer experience.
To get the app, go here: Rezolve Reference App
Download links, sample media, and instructions are all included.
Capabilities
The Rezolve InsideTM SDK is a full-featured application suite. Capabilities that can be integrated into your application include:
- 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
- Detection of Rezolve Encoded audio media
- Detection of Geozones
- Detection of QR Codes
- Presentation of target media, like Products, Categories, Acts, Info Pages, and External URLs
- Consumer account creation, consumer profile, and purchase history
- Wallet management
Intended audience
This document is intended for experienced IOS developers. It is expected that you have built apps before, and know your way around an IDE.
Term Definitions
term | definition |
---|---|
Partner | The Partner is the owner of a mobile application and audience. It is the Partner who wishes to integrate the Rezolve InsideTM SDK capabilities with their app. |
Developer | The Developer is the mobile app developer of the Partner. |
Merchant | A merchant runs an ecommerce site that offers products for sale. Merchants also create Shoppable Ads and link them to products. A merchant may also offer device accounts that need Top Up. |
Consumer | The end user of the Partner's mobile app. The customer who buys merchant products. |
SDK | Refers to the Rezolve InsideTM SDK, unless otherwise specified. |
RCE | Rezolve Cloud Engine, the online Rezolve application for managing your categories and products. |
SSP | Self-Serve Portal, the online Rezolve portal for creating engagements, and linking them to target offerings. |
For more information
For more information on Rezolve, see http://rezolve.com.
Getting Started
Supported OS Versions
OS | Minimum Version |
---|---|
IOS | IOS 12 and up SWIFT 5 |
Implement a Crash Reporting Sytem
To provide quality support, we require quality information.
Rezolve advises using Firebase Crashlytics to capture app problems on both IOS and Android.
We will not provide development support if you have not integrated Crashlytics or a similar tool.
Get the Sample App project
The best way to get started with SDK integration is to download our Sample App project. This is a complete Xcode project that produces a working application, showcasing the features described in this documentation.
Just fill in your API details in Config.swift
and compile.
rezolve_sdk_sampleapp_ios-20210401.zip
Download the SDK & Get an API Key
Latest release versions:
- IOS: 2.0.11.2
If this is your first time downloading the SDK, you will be provided with an API Key and the required environment information to begin development.
Set up the SDK - IOS
The target IDE for IOS instructions is XCode. If you use a different IDE you may have to follow a different series of steps, please refer to your IDE documentation to understand how to incorporate third party SDKs into your IDE.
CocoaPods
The Rezolve IOS SDK is distributed through CocoaPods. In order to proceed with installing the SDK, as a prerequisite, first install the latest version of CocoaPods:
sudo gem install cocoapods
Install Rezolve SDK
platform :ios, '12.0'
use_modular_headers!
target 'Sample' do
pod 'RezolveSDK'
end
The SDK can now be installed in your own project by adding the following in Podfile
:
Keep in mind that Sample is just a placeholder, and should be substituted by your own App Target name. Now just run the following command on a Terminal window pointing to the directory of your Podfile:
pod install
Don’t forget to use the .xcworkspace
file to open your project in Xcode, instead of the .xcodeproj
file, from here on out.
Updating the SDK
Every new release of the RezolveSDK can be updated by typing
pod update ResolveSDK
SDK Feature Use
This section describes the usage of the SDK to build specific feature-related functionalities.
Automatic thumbnail generation
Example JSON for 1 large image, and 4 associated "thumbnail" images
"image": "https:\/\/s3.amazonaws.com\/1\/27\/Image.png",
"image_thumbs": [
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_400x550.png",
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_800x1100.png",
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_1200x1651.png",
"https:\/\/s3.amazonaws.com\/1\/27\/Image_thumb_1600x2201.png"
],
When large images are uploaded to the merchant portal, the portal automatically generates a series of smaller images, which are referred to as thumbnails even though some are fairly large. The rules for thumbnail generation are as follows:
- 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.
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://core.sbx.eu.rezolve.com/api/v1/authentication/register
-H content-type: application/json
-H x-rezolve-partner-apikey: your-api-key
-H authorization: Bearer signed-jwt
-d {
"email": "user@example.com"
}
After the JWT is created, POST it to the Rezolve registration endpoint. This will create a Rezolve User, generate a public/private keypair for the user, and return to you the corresponding Entity Id.
Example, using Sandbox endpoint:
{
"entity_id" : "entity123",
"partner_id" : "3"
}
The endpoint will reply with an entity id and the partner id. You should save the Rezolve Entity Id to your authentication database and associate it with the user's record.
Logging in a User
Once a Rezolve User has been registered and an entity_id
obtained, you can log in the user using the instructions below.
For returning users, log them in via your normal method in your auth server, and then follow the instructions below.
Create a new Login JWT
, and use it as the accessToken
in the createSession
method.
JWT Header
Note the addition of the "auth" line.
{
"auth": "v2",
"alg": "HS512",
"typ": "JWT"
}
key | value | notes |
---|---|---|
auth | v2 | auth version to use, login uses v2 |
alg | HS512 | algorithm, HMAC SHA-512 |
typ | JWT | type |
JWT Payload
Note the addition of the device_id
.
{
"rezolve_entity_id": "entity123",
"partner_entity_id": "partner_entity_id",
"exp": 1520869470,
"device_id": "wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a"
}
key | value | notes |
---|---|---|
rezolve_entity_id | your_rezolve_entity_id | use the entity_id you obtained during registration |
partner_entity_id | your_partner_entity_id | set it to the unique identifier for your user record |
exp | 1520869470 | Expiration, as a unix timestamp integer. Set the expiration value to a small number, now() + 30 minutes or less. |
device_id | wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a | An id randomly generated upon app installation and stored. This id is placed in both the JWT payload and x-header sent by the SDK. See below for generation instructions. |
Signature
HMACSHA512(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
${$partner_auth_key}
)
Sign the header and payload with the partner_auth_key
.
Generating the device_id
// iOS uses the iOS "identifierForVendor" string, so there is no need to generate it.
// The iOS SDK pulls this value automatically using `UIDevice.current.identifierForVendor?.uuidString` and supplies it to the SDK for the x-header.
// You will need to use this same call to supply the device_id to your auth server for storage with the user profile.
UIDevice.current.identifierForVendor?.uuidString
To generate and set the device_id, see the OS specific examples to the right. On IOS it is handled automatically using identifierForVendor?.uuidString
. Android must manually generate, store, read, and pass the device_id
to the SDK.
Create the Session
In the samples to the right, accessToken
is the Login JWT token you created above.
Note in the Android sample you are also passing in an AuthRequestProvider. This is used for handling expiring JWT sessions, and is explained in the next section.
import UIKit
import RezolveSDK
class SandboxViewController: UIViewController {
private let API_KEY: String = "your_api_key"
private let API_ENVIRONMENT: RezolveEnv = .sandbox
private let accessToken: String = "abc123.abc123.abc123"
private let entityId: String = "123"
private var partnerId: String = "123"
override func viewDidLoad() {
super.viewDidLoad()
let sdk = Rezolve(apiKey: API_KEY,
partnerId: partnerId,
subPartnerId: nil,
environment: API_ENVIRONMENT,
config: nil,
sspActManagerSettings: nil,
coordinatesConverter: .default)
sdk.createSession(accessToken: accessToken, username: "", entityId: entityId, partnerId: partnerId) { (session, error)
// your rezolve SDK logic here
}
}
}
Handling JWT Expiration & Session Preservation
import RezolveSDK
import Foundation
internal enum Result<T> {
case success(T)
case failure(HttpResponse)
}
internal protocol TokenRenewProtocol {
func renewToken(result: @escaping (Result<RezolveSession>) -> Void)
}
internal class AuthService: NSObject, TokenRenewProtocol {
func renewToken(result: @escaping (Result<RezolveSession, AuthenticationError>) -> Void) {
let urlString = ""
let endpoint = "/api/v1/authentication/register"
let data = ["username": "user@email.com", "password": "password"]
guard let url = URL(string: urlString).appendPathComponent(endpoint),
let request = URLRequest(url: url) else {
preconditionFailure("Url not created, verify check path and host")
}
let device: DeviceProfile = ... // The device profile
let entityId: String = ... // Entity Id
let partnerId: String = ... // Partner Id
let dictionary: [String: String] = [
"device_id": device.deviceId,
"make": device.make,
"os_type": device.osType,
"os_version": device.osVersion,
"locale": device.locale
"entityId": entityId,
"partnerId": partnerId
]
guard let body = try JSONSerialization.data(withJSONObject: dictionary, options: []) else {
preconditionFailure("Payload convertion to Data failed")
}
request.httpMethod = "POST"
request.httpBody = body
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) { data, response, error in
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
if let responseData = data,
let jsonDictionary = try? JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] {
let accessToken = jsonDictionary["access_token"]
let publicKey = jsonDictionary["public_key"]
// Securily store received keys
// Create new session with received accessToken
self.rezolveSdk?.createSession(
accessToken: accessToken,
entityId: entityId,
partnerId: partnerId,
callback: { rezolveSession in
// Store new token
// Proccess callback
callback(.success(rezolveSession))
}, errorCallback: { error in
// Error handling
// Proccess callback
callback(.failure(error))
})
}
else {
// Handle parsing error
}
}
else {
// Handle error
}
}
}
}
internal final class AddressBookViewController: UIViewController {
fileprivate(set) var authService: (NSObjectProtocol & TokenRenewProtocol)?
fileprivate func attemptToGetAddressBook(session: RezolveSession, retryOnFail: Bool) {
session.addressbookManager.getAll(callback: { addressList in
// Handle addressList
}, errorCallback: { httpResponse in
if retryOnFail && httpResponse.statusCode == 401 {
self.authService?.renewToken { result in
swith result {
case let .success(newSession):
// Retries to fetch data from AddressBookManager using the new RezolveSession
self.attemptToGetAddressBook(session: newSession, retryOnFail: false)
case let .failure(error):
// Handle Token Renewal failure
debugPrint(error)
}
}
}
})
}
}
The Login JWT you generate is included in the headers of every SDK transmission. Thus, when your consumer logs out, you can expire the JWT, and the app will cease communication with the Rezolve server. To do this, create a new JWT with an expiration stamp in the past, and supply it to the SDK.
This also means you are required to handle JWT token expiration/renewal if you want a session to continue.
Examples are provided to the right. These are NOT an example of implementing SDK code, but rather an example of implementing session rewnewal with your own authentication server.
To handle session expiration smoothly, without interrupting the user session, Rezolve recommends creating a wrapper around any call that contacts the server. If the server returns an authorization error, you can call your token renewal endpoint and issue a new JWT.
The sample shows two classes, an AuthService
class and an AddressBookViewController
class.
The AuthService
class may, for example, process username/passwords for login, handle registering your users, and handle password resets. It should also handle the JSON Web Tokens to register, create session, and maintain session with Rezolve. The example method renewJwtToken
is provided as an example of how to renew a token.
The AddressBookViewController
is an example of how you would implement the AddressbookManager.getAll
method so as to smoothly handle token expiration and renewal to provide continuity of session.
- 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.
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
.
SSP Engagement Detection
The Self-Serve Portal, or SSP, enables merchants to create Engagements. Engagements provide a way for the merchant to interact with consumers wherever they are - through print, audio/video, geolocation, and more.
Engagements are composed of a Trigger (specially customized media) linked to a Target (the content the merchant wants the consumer to see). Triggers include watermarked images, watermarked audio, geozones, QR Codes, and touch links. Targets include Information Pages, Acts, Products, Categories, and Urls.
This section describes how to capture information from the Trigger when it is detected, and resolve that into a Target.
1. Initialize ScanManager
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
2. Set up a ProductDelegate
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
3. Add a function to handle SSP Engagements
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
// Functions from previous example hidden for brevity
func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
// Object `engagement` contains the basic structure of a SSP resolved engagement
let id = engagement.engagementId
let scanId = engagement.serviceId
let path = engagement.payloadPath
let payoffs = engagement.payoffs
let payload = engagement.rezolveCustomPayload
}
}
4. Inspect the payload
// Any of these can be `null`, but one should have content
let act = payload.act?.act // Information for linked Act
let product = payload.product // Information for linked Product
let category = payload.category // Information for linked Category
The non-null object item corresponds to the target set by the merchant. You must handle all target types. This will be covered in the following section.
SSP Location Triggers
Location Triggers are a powerful SDK feature introduced in 2020 that allows the developer to detect when the consumer has entered a predefined geozone and pop a notification of a product, informational page, or Act. A geozone is defined as a radius of X meters around a specified lat/long point, as set up by a merchant or partner in the Rezolve portal.
When the app is active, the SDK continuously samples the consumer's location, and when the consumer is within the radius of one or more geozones, events fire to alert the system to perform the desired behavior. The SDK intelligently handles areas with a high density of overlapping geozones to prevent spamming the user with notifications. If the consumer is in an area with multiple geozones, the SDK bundles multiple notifications into one alert that contains the messages from all the relevant zones.
Topics
- Setup location detection
- 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
This section describes how to set up your project to use the location features of the Rezolve SDK.
Initializing location detection
// On Rezolve class initialization you need to pass your SSP configuration into sspActManagerSettings parameter, which is a class of RezolveSDK.SspActManagerSettings. As an example, it has the following structure.
let sspActManagerSettings = SspActManagerSettings(
auth0ClientId: "{String}",
auth0Secret: "{String}",
auth0Audience: "{String}",
auth0Endpoint: "{String}",
sspEndpoint: "{String}",
sspWidth: "{String}",
baiduLocationKey: "{String}"
)
// Adding a baiduLocationKey is not mandatory, but will definitely increase tracking accuraccy if you are targeting regions located in China mainland.
Initializing location detection requires setting up the SspActManager
with appropriate settings.
Detecting geozones
// Setting up Rezolve SDK with an active SSP Act Manager configuration.
let sdk = Rezolve(apiKey: Config.rezolveApiKey,
partnerId: Config.partnerId,
subPartnerId: nil,
environment: Config.env,
config: nil,
sspActManagerSettings: sspActManagerSettings,
coordinatesConverter: CoordinatesConverter.default)
// Start monitoring for Nearby Engagements.
let ssp = RezolveService.sdk?.createRezolveSsp()
ssp?.nearbyEngagementsManager.startMonitoringForNearbyEngagements()
ssp?.nearbyEngagementsManager.debugNotifications = false
ssp?.nearbyEngagementsManager.delegate = self
// Stop monitoring for Nearby Engagements.
ssp?.nearbyEngagementsManager.stopMonitoringForNearbyEngagements()
ssp?.nearbyEngagementsManager.stopUpdatingDistanceFromNearbyEngagements()
ssp?.nearbyEngagementsManager.resetNotificationSuppressData()
ssp?.nearbyEngagementsManager.delegate = nil
During detection, the user's location is monitored. Detection times vary according to OS settings.
For IOS, the following applies: "When testing your region monitoring code in iOS Simulator or on a device, realize that region events may not happen immediately after a region boundary is crossed. To prevent spurious notifications, iOS doesn't deliver region notifications until certain threshold conditions are met. Specifically, the user's location must cross the region boundary, move away from the boundary by a minimum distance, and remain at that minimum distance for at least 20 seconds before the notifications are reported.
The specific threshold distances are determined by the hardware and the location technologies that are currently available. For example, if Wi-Fi is disabled, region monitoring is significantly less accurate. However, for testing purposes, you can assume that the minimum distance is approximately 200 meters."
Handling detection and notification
// Handle incoming changes regarding Geofence status from the SDK.
extension RezolveGeofence: NearbyEngagementsManagerDelegate {
func didStartMonitoring(for circularRegions: [CLCircularRegion], coordinate: CLLocationCoordinate2D, radius: Int) {
print("didStartMonitoring")
}
func didEnter(circularRegion: GeofenceData) {
print("didEnter")
}
func didFail(with error: Error) {
print("didFail -> \(error.localizedDescription)")
}
func didUpdateCurrentDistanceFrom(location: CLLocationCoordinate2D, geofences: [GeofenceData], beacons: [BeaconData]) {
print("didUpdateCurrentDistanceFrom")
}
func didReceiveInAppNotification(act: SspResolverAct?) {
print("didReceiveInAppNotification")
}
}
When a geofence is detected, notification is potentially shown, as long as it is not within a silent period from a previous detection.
SSP Act Targets
1. Identify the Act payload
let act = payload.act?.act // Information for linked Act
let product = payload.product // Information for linked Product
let category = payload.category // Information for linked Category
// Any of the above are not `null`, and a `customUrl` is detected, we need to redirect the user to this linked entity
let actCustomUrl = act?.customUrl
let productCustomUrl = product?.customUrl
let categoryCustomUrl = category?.customUrl
Acts and Information Pages are closely related. An information page simply displays the merchant's message to the consumer. An Act is like a info page that includes a form the user must fill out.
If payload.act?.act
is not null, and act?.customUrl
IS null, you have an Act.
2. Display the Act
let actId = act?.id
let actPageBuildingBlocks = act?.pageBuildingBlocks
let actIsInformationPage = act?.isInformationPage
let actQuestions = act?.questions
// Iterate through the Act Page Building Blocks and render on screen
for actPageBuildingBlock in actPageBuildingBlocks! {
print(actPageBuildingBlock)
}
// Iterate through the Act Questions and render on screen
for actQuestion in actQuestions! {
print(actQuestion)
}
A merchant creates an Act by adding display items (building blocks) and then adding form questions.
Iterate first through the building blocks, and then through the questions, rendering them on screen.
3. Submitting Act answers
let user: User // Current user
let location: CLLocation? // Current user location if available
let sspAct: SspAct // SSP Act to submit
let page: Page // User answers
func answer(element: Page.Element) -> SspSubmissionAnswer? {
switch element {
case .text, .divider, .image, .video:
return nil
case .dateField(let dateField):
guard let value = dateField.value else {
return nil
}
let date = // Map Date to string
return SspSubmissionAnswer(questionId: dateField.id, answer: date)
case .select(let select):
guard let value = select.value else {
return nil
}
return SspSubmissionAnswer(questionId: select.id, answer: String(value.value))
case .textField(let text):
guard let value = text.value else {
return nil
}
return SspSubmissionAnswer(questionId: text.id, answer: value)
}
}
// Map page inputs to submission answers
let answers = page.elements.compactMap(answer(element:))
The example to the right shows an easy way to map all user provided answers into a meaningful object.
3a. How to format the Act submission
// Creating Ssp Act Sumbission model
let submission = SspActSubmission(
userId: user.id,
userName: user.username,
personTitle: user.title,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
phone: user.phone,
scanId: sspAct.scanId,
latitude: location?.latitude,
longitude: location?.longitude,
answers: answers
)
Next you will need to create the basic model for submitting an Act. All of the previously declared details must be preset in the structure, like User, Act, Location, Answers.
3b. Do the submission
// Submission
sspActManager.submitAct(actId: sspAct.id, actSubmission: submission) { (result) in
switch result {
case .success(let submission):
// Act submitted
case .failure(let error):
// Handle error gracefully
}
}
Lastly, execute the final form of the object passed on one of the method’s parameters.
Get an SSP Act By Id
rezolveSession?.sspActManager.getAct(actId: id) { (result) in
switch result {
case .success(let act):
if let pageBlocks = act.pageBuildingBlocks {
// pageBuildingBlocks can be used directly to render UI, or PageBuilder can be used to map blocks to more useful models (user input, validation)
do {
let page = try PageBuilder().build(from: pageBlocks)
// Use page model to render UI
} catch {
// Handle error gracefully
}
} else {
}
let isInformationPage = act.isInformationPage
// submission should not be available if isInformationPage == true
case .failure(let error):
// Handle error gracefully
}
}
If you have an Act Id, you can retrieve it using the example code to the right.
SSP Product Targets
func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
let id = engagement.engagementId
let scanId = engagement.serviceId
let path = engagement.payloadPath
let payoffs = engagement.payoffs
let payload = engagement.rezolveCustomPayload
// The logic of parsing, is that you might find a `customUrl` in the structure not being `null`.
// Hence, we will need to examine it and redirect the user to the linked Web Page, Product or Category
if let act = payload.act?.act {
if let customUrl = act.customUrl {
// Parse act.customUrl
return
}
} else if let product = payload.product?.customUrl {
if let customUrl = product.customUrl {
// To Parse product.customUrl
// Check if url begins with "https://rzlv.co"
// Check if url path is in format "/[int]/[int]/[int]/[int]"
// If both the above are true, you have a valid RezolveTrigger. Use Trigger Manager to look up the product.
// If the url does not meet the above rules, it is not a RezolveTrigger, it is a regular url. Redirect the user to the url.
return
}
} else if let category = payload.category?.customUrl {
if let customUrl = category.customUrl {
// Parse category.customUrl
return
}
}
}
To handle a product result, get the product custom URL and test it.
If the url is a RezolveTrigger, similar in format to https://rzlv.co/1/2/3/4
, use Trigger Manager to fetch the product. See code sample for test advice.
If the url is NOT a RezolveTrigger, assume it is a regular url, and you should handle it as you see fit. The typical behavior is a simple redirect to the url, to be opened in a browser.
SSP Category Targets
func onSspEngagementResult(engagement: ResolverEngagement, eventType: RezolveEventReport.RezolveEventReportType) {
let id = engagement.engagementId
let scanId = engagement.serviceId
let path = engagement.payloadPath
let payoffs = engagement.payoffs
let payload = engagement.rezolveCustomPayload
// The logic of parsing, is that you might find a `customUrl` in the structure not being `null`.
// Hence, we will need to examine it and redirect the user to the linked Web Page, Product or Category
if let act = payload.act?.act {
if let customUrl = act.customUrl {
// Parse act.customUrl
return
}
} else if let product = payload.product?.customUrl {
if let customUrl = product.customUrl {
// Parse product.customUrl
return
}
} else if let category = payload.category?.customUrl {
if let customUrl = category.customUrl {
// To Parse category.customUrl
// Check if url begins with "https://rzlv.co"
// Check if url path is in format "/[int]/[int]/[int]" (note: one fewer parameter than a product)
// If both the above are true, you have a valid RezolveTrigger. Use Trigger Manager to look up the category.
// If the url does not meet the above rules, it is not a RezolveTrigger, it is a regular url. Redirect the user to the url.
return
}
}
}
To handle a category result, get the category custom URL and test it.
If the url is a RezolveTrigger, similar in format to https://rzlv.co/1/2/3
, use Trigger Manager to fetch the category. See code sample for test advice.
If the url is NOT a RezolveTrigger, assume it is a regular url, and you should handle it as you see fit. The typical behavior is a simple redirect to the url, to be opened in a browser.
SSP URL Targets
let act = payload.act?.act // Information for linked Act
let product = payload.product // Information for linked Product
let category = payload.category // Information for linked Category
// Any of the above are not `null`, and a `customUrl` is detected, we need to redirect the user to this linked entity
let actCustomUrl = act?.customUrl
let productCustomUrl = product?.customUrl
let categoryCustomUrl = category?.customUrl
If you find a non-null payload
, next check for a non-null customUrl
If a custom URL is found, you redirect the user to the linked entity.
Trigger Manager
Triggers are specially formatted media in the Rezolve system that can point to products, categories, and other resources. Image Engagements and Audio Engagements are a type of trigger, and these are handled by ScanManager
. Touch Triggers are handled by TriggerManager
. Touch Triggers are essentially URLs that are typically rendered as a touchable link or button onscreen.
Touch Triggers are used by the Background Listening feature to surface items detected from audio watermarks in a Background Listening session.
Touch Triggers could also be used to create a Wishlist feature; ask your sales person for the Wishlist/Favorites Solution Paper.
Trigger Manager provides a way to resolve touch triggers into actionable content, like products or categories.
Touch Engagements, or touch triggers, always have the same format:
http://rzlv.co/[partnerId]/[merchantId]/[categoryId]/[productId][(optional...)?ad=[adId]&placement=[placementId]]
To use touch triggers, the partner should watch for urls in their content stream that match this pattern, render them as a touchable link, and then when touched, pass the url to the TriggerManager.resolveTrigger method
.
Trigger Manager Example
// Sample URL and `TriggerManager` initialization
let url = URL(string: "http://rzlv.co/1/2/3/8?ad=20&placement=25")
rezolveSession?.triggerManager.resolve(
url: url!,
productDelegate: self,
onRezolveTriggerStart: {},
onRezolveTriggerEnd: {},
errorCallback: { error in }
)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
Product Scan, Instant Buy flow
[ View full size ]
The premise of Shoppable Ads is to capture an image scan (usually of an advertisement) using the Scan Manager, resolve it into a product URL, fetch the product info, and enable purchase via saved account information.
In the Instant Buy flow, we purchase the product immediately, without first adding it to the cart.
1. Capture image and get product
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
First, initialize scanManager
, and enable the scan screen using session.startVideoScan()
, and capture a watermarked image. The scanManager recognizes the encoded product data, and extracts merchantId
, catalogId
, and productId
from the image, automatically calling getProduct
. The scanManager returns a product
object.
2. Get shipping and payment options for the product
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"
rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<PaymentOption, RezolveError>) in
switch result {
case .success(let option):
{
// For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
let paymentMethod = option.supportedPaymentMethods.first!
let shippingMethod = option.supportedDeliveryMethods.first!
}
case .failure(let error):
// Handle error gracefully
}
})
Call PaymentOptionManager
to get shipping and payment options for the current merchant. This tutorial assumes the consumer chose a form of credit card payment, and chose home delivery.
Note: You must repeat this call if the user chooses a different product variant (size, color, etc), changes product quantity, changes shipping choice, or changes payment option.
For more information on what is returned by getProductOptions
, see the Background Listening tutorial.
3. Show payment card choices
rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in
// Handle payment cards
}
Use walletManager.getAll
to list the available card choices. If the consumer wishes to buy, they will select a payment card to use, and provide confirmation of ordering intent.
We recommend using a "slide to buy" button to confirm purchase intent, while preserving the maximum ease of use.
4. Create a checkout bundle, checkout the product to get totals, and create an order
let sampleProductCheckoutBundle = CheckoutBundle(
checkoutProduct: checkoutProduct,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: nil,
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.checkout(bundle: sampleProductCheckoutBundle) { (result: Result<Price, Error>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.finalPrice)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
Once you have product information, create a CheckoutProduct object. Then call the SDK CheckoutManagerV2.checkoutProduct
method to create an order and get totals. The response order object includes an order id, order total, and price breakdowns.
5. Submit payment for order
let paymentCard = // RezolveSDK.PaymentCard
let cardCVV = "000" // Card CVV
let sampleProductCheckoutBundle = CheckoutBundle(
checkoutProduct: checkoutProduct,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: PaymentRequest(paymentCard: paymentCard, cvv: cardCVV),
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.buy(bundle: sampleProductCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.partnerId)
print(order.partnerName)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
When the user confirms intent, pass the card choice and the entered CVV value to the createPaymentRequest
method. This creates the encrypted paymentRequest
object needed for checkout.
In this tutorial, we assume the user chose credit card payment. Note that paymentRequest
is actually optional here, and can be null. To determine if it's needed, please check selected SupportedPaymentMethod
's type.
Pass a paymentRequest
object, checkoutBundleV2
object, the orderId
, and an interface or callback to the buyProduct
method. The success response will an OrderSummary
object. Note that this does not mean the order was confirmed, only that the request was successfully received.
Note that the call to signOrderUpdate
shown in the sequence diagram is no longer required.
Product Scan, Cart flow
[ View full size ]
The premise of Shoppable Ads is to capture an image scan (usually of an advertisement) using the Scan Manager, resolve it into a product URL, fetch the product info, and enable purchase via saved account information.
In the Cart Buy flow, one or more products are added to a cart. Each merchant will have a separate cart, and multiple carts can contain products at a time. For this example we are assuming only one cart is active, but you can check for multiple carts using CheckoutManagerV2.getCarts
.
1. Capture image and get product
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveSDK.Category) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveSDK.Category, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
First, initialize scanManager
, and enable the scan screen using session.startVideo()
, and capture a watermarked image. The scanManager recognizes the encoded product data, and extracts merchantId
, catalogId
, and productId
from the image, automatically calling getProduct
. The scanManager returns a product
object.
2. Add Product to the Cart
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"
rezolveSession?.cartManager.createCartWithProduct(sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<CartDetails, RezolveError>) in
switch result {
case .success(let cart):
{
print(cart.id)
print(cart.merchantId)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
Call CheckoutManagerV2.addProductToCart
to add the product to cart.
3. Get shipping and payment options for the cart
let sampleMerchantID = "12"
let sampleCartID = "1"
rezolveSession?.paymentOptionManager.getPaymentOptionsForCartWith(merchantId: sampleMerchantID, cartId: sampleCartID) { (result: Result<[PaymentOption], RezolveError>) in
switch result {
case .success(let options):
{
// For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
let paymentMethod = options.first?.supportedPaymentMethods.first
let shippingMethod = options.first?.supportedDeliveryMethods.first
}
case .failure(let error):
// Handle error gracefully
}
})
Call PaymentOptionManager
to get shipping and payment options for the current merchant. This tutorial assumes the consumer chose a form of credit card payment, and chose home delivery.
Note: You must repeat this call if the user chooses a different product variant (size, color, etc), changes product quantity, changes shipping choice, or changes payment option.
For more information on what is returned by getCartOptions
, see the Background Listening tutorial.
4. Show payment card choices
rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in
// Handle payment cards
}
Use walletManager.getAll
to list the available card choices. If the consumer wishes to buy, they will select a payment card to use, and provide confirmation of ordering intent.
We recommend using a "slide to buy" button to confirm purchase intent, while preserving the maximum ease of use.
5. Create a checkout bundle, checkout the cart to get totals, and create an order
let sampleCartCheckoutBundle = CheckoutBundle(
cartId: cartId,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: nil,
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.checkout(bundle: sampleCartCheckoutBundle) { (result: Result<Price, Error>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.finalPrice)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
Create a cart checkout bundle. Then call the SDK CheckoutManagerV2.checkoutCart
method (Android) or CheckoutManagerV2.checkout
(IOS) method to create an order and get totals. The response order object includes an order id, order total, and price breakdowns.
6. Submit payment for order
let paymentCard = // RezolveSDK.PaymentCard
let cardCVV = "000" // Card CVV
let sampleCartCheckoutBundle = CheckoutBundle(
cartId: cartId,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: PaymentRequest(paymentCard: paymentCard, cvv: cardCVV),
phoneId: phoneId,
location: userLocation
)
rezolveSession?.checkoutManager.buy(bundle: sampleCartCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.partnerId)
print(order.partnerName)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
When the user confirms intent, pass the card choice and the entered CVV value to the createPaymentRequest
method. This creates the encrypted paymentRequest
object needed for checkout.
In this tutorial, we assume the user chose credit card payment. Note that paymentRequest
is actually optional here, and can be null. To determine if it's needed, please check selected SupportedPaymentMethod
's type.
Pass the paymentRequest
object, thecheckoutBundleV2
object, the orderId
, and an interface or callback to the buyCart
method. The success response will be the order id
as a string. Note that this does not mean the order was confirmed, only that the request was successfully received.
Note that the call to signOrderUpdate
shown in the sequence diagram is no longer required.
Category Scan flow
[ View full size ]
Shoppable Ads can do more than link to a single product, it can link to a category of products in your Rezolve Commerce Engine. Scanning an ad that contains a category link will bring up a list of subcategories and products in that category.
Once a product has been selected, purchasing follows one of the previous examples of Instant Buy Flow or Cart Buy Flow (minus the scan step).
1. Scan a category shoppable ad, get a getCatalog response
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.productResultDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
First, initialize scanManager
, and enable the scan screen using session.startVideo()
, and capture a watermarked image. The scanManager returns a Category
object, which is then parsed to determine its contents.
2. Repeat for navigation until ready to purchase
Repeat step 1 above as the consumer browses through categories. Once the consumer selects a product and is ready to purchase, the process is the same as in the Instant Buy or Cart Buy flows. Note: a DisplayProduct does not contain full information on a product. You will need to call getProduct to display the detail view of the Product.
Mall flow
[ View full size ]
Mall is the only method for finding products that does not require an ad scan. A consumer enters the mall by clicking on a "Mall" navigation option within the mobile app. The Mall showcases active merchants in an attractive layout that is condusive to casual browsing and subsequent purchasing.
Once a merchant is selected, the consumer shifts into category/product browse mode. Once a product has been selected, purchasing follows one of the previous examples of Instant Buy Flow or Cart Buy Flow (minus the scan step).
1. Get List of Merchants in the mall
rezolveSession?.merchantManager.getMerchants { (result: Result<[Merchant], RezolveError>) in
switch result {
case .success(let merchants):
{
merchants.forEach {
// Basic information
let id = $0.id
let name = $0.name
let tagline = $0.tagline
let contactInformation = $0.contactInformation
let termsAndConditions = $0.termsAndConditions
// Assets
let banner = $0.banner
let logo = $0.logo
let bannerThumbs = $0.bannerThumbs
let logoThumbs = $0.logoThumbs
}
}
case .failure(let error):
// Handle error gracefully
}
})
First, initialize MerchantManager
, and call GetMerchants
, providing an implementation of MerchantCallback as a parameter. This will return an array of Merchant objects. Parse each merchant object to get the id
, name
, tagline
, banner
, bannerThumbs
, and logoThumbs
.
2. Get list of first-level Categories for the selected Merchant
let sampleMerchantID = "12"
rezolveSession?.productManager.getRootCategoryForMerchantWith(id: sampleMerchantID) { (result: Result<RezolveSDK.Category, RezolveError>) in
switch result {
case .success(let category):
{
// Basic information
let id = category.id
let parentId = category.parentId
let name = category.name
let hasCategories = category.hasCategories
let hasProducts = category.hasProducts
// Assets
let image = category.image
let imageThumbs = category.imageThumbs
// Get subcategories, if any
if hasCategories {
category.categories.forEach { subCategory in
print(subCategory.id)
print(subCategory.parentId)
print(subCategory.name)
// ...
}
}
}
case .failure(let error):
// Handle error gracefully
}
})
Display your subcategories and products as returned by the getCategories
call.
For subsequent navigation in categories, use getProductsAndCategories
.
3. If the consumer clicks a subcategory, call getProductsAndCategories
.
let sampleMerchantID = "12"
let sampleCategoryID: Int = 70
let pageNavigationFilters = PageNavigationFilter(
productsFilter: PageNavigation(count: 100, pageIndex: 1, sortBy: "product", sort: .ascending),
categoryFilter: PageNavigation(count: 100, pageIndex: 1, sortBy: "category", sort: .ascending)
)
rezolveSession?.productManager.getPaginatedCategoriesAndProducts(merchantId: sampleMerchantID, categoryId: sampleCategoryID, pageNavigationFilters: pageNavigationFilters) { (result: <Rezolve, RezolveError>) in
switch result {
case .success(let category):
{
// Basic information
let id = category.id
let parentId = category.parentId
let name = category.name
let hasCategories = category.hasCategories
let hasProducts = category.hasProducts
// Assets
let image = category.image
let imageThumbs = category.imageThumbs
// Get subcategories, if any
if hasCategories {
category.categories.forEach { subCategory in
print(subCategory.id)
print(subCategory.parentId)
print(subCategory.name)
// ...
}
}
// Get display products, if any
if hasProducts {
category.products.forEach { displayProduct in
print(displayProduct.id)
print(displayProduct.name)
print(displayProduct.price)
// ...
}
}
}
case .failure(let error):
// Handle error gracefully
}
})
As the consumer navigates the category tree, call getProductsAndCategories
to pull a paginated lists of subcategories and products for that category.
4. If the consumer clicks a Product, call getProduct
let sampleMerchantID = "12"
let sampleCategoryID: Int = 70
let sampleProductID: Int = 6
let sampleProduct = Product(id: sampleProductID)
rezolveSession?.productManager.getProductDetails(merchantId: sampleMerchantID, categoryId: sampleCategoryID, product: sampleProduct) { (result: Result<Product, RezolveError>) in
switch result {
case .success(let product):
{
print(product.id)
print(product.merchantId)
print(product.title)
print(product.subtitle)
print(product.price)
print(product.description)
product.images.forEach {
print($0)
}
product.options.forEach { option in
print(option.label)
print(option.code)
print(option.extraInfo)
option.values.forEach { optionValue in
print(optionValue.value)
print(optionValue.label)
}
}
product.optionAvailable.forEach {
$0.combination.forEach { variant in
print(variant.code)
print(variant.value)
print(variant.id)
}
}
product.customOptions.forEach {
print($0.isRequire)
print($0.optionId)
print($0.sortOrder)
print($0.title)
print($0.optionType)
$0.values.forEach { value in
print(value.sortOrder)
print(value.title)
print(valueId)
}
$0.valuesId.forEach { valueId in
print(valueId)
}
print($0.value)
}
print(product.productPlacement)
}
case .failure(let error):
// Handle error gracefully
}
})
If the consumer clicks a Product, call getProduct
to fetch full product information. At this point, the user can either add the product to their cart, click "buy now", or press "back" to navigate to the category view.
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
}
})
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
}
})
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.
External Payment
All previous documentation has assumed that payment processing occurs in the Rezolve system. Rezolve can support multiple payment integrations, and often this is the easiest route.
Rezolve also supports "external payments" however, in the case where a developer wishes build their own transaction processing. This section explains how to create an order and then update the payment status, for payment transactions processed outside Rezolve.
Create the checkoutBundle
let paymentMethod.type = "external"
let sampleProductCheckoutBundle = CheckoutBundle(
checkoutProduct: checkoutProduct,
shippingMethod: deliveryMethod,
merchantId: merchantId,
optionId: optionId,
paymentMethod: paymentMethod,
paymentRequest: nil,
phoneId: phoneId,
location: userLocation
)
Processing an external payment is similar to processing a regular payment, with a couple key differences that occur when making the checkoutBundle
. This example shows a checkout bundle for a single product, but the same changes will apply to a cart bundle.
The first difference is, when making the checkoutBundle
, set paymentRequest
to nil
, and leave it. You will not update this with a payment card later.
Second, if external payment is supported, the PaymentOption
s returned from PaymentOptionManager.getPaymentOptionFor(...)
or paymentOptionManager.getPaymentOptionsForCartWith(...)
will have an option for "external" payment. Selecting this one indicates to the system that payments will be processed externally.
Checkout and Buy
rezolveSession?.checkoutManager.checkout(bundle: sampleProductCheckoutBundle) { (result: Result<Price, Error>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.finalPrice)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
// ...
rezolveSession?.checkoutManager.buy(bundle: sampleProductCheckoutBundle) { (result: Result<CheckoutOrder, RezolveError>) in
switch result {
case .success(let order):
{
print(order.id)
print(order.partnerId)
print(order.partnerName)
// ...
}
case .failure(let error):
// Handle error gracefully
}
})
Then pass the checkoutBundle
into checkout as usual, getting your order id and final price.
Finally, call the buy endpoint, again as usual.
Buy Response
{
"phone_number": "111111111",
"payment_method": "external",
"partner_name": "Rezolve Shared Partner",
"partner_id": "56",
"order_id": "1501",
"name": "RCE 3",
"merchant_id": "79",
"email": "cgouldthorpe+rce3@gmail.com",
"data": {
"callback": {
"payment_id": "ff04ba41-32f1-4f70-a461-3f63fe9e81b9"
}
}
}
rezolveSession?.checkoutManager.buy(bundle: sampleProductCheckoutBundle) { (result) in
switch result {
case .success(let order):
if let data = order.data["callback"]?.value as? [AnyHashable: Any] {
let paymentId = data["payment_id"] as? String
}
case .failure(let error):
// Handle error gracefully
}
}
A sample JSON response is shown. The code sample shows how to extract the payment_id
from the JSON. You will need this to after the payment transaction is processed.
You have now created an order in the system, but still need to handle payment and update the order with the result.
Handle Payment Externally
How this happens is up to you. Your app will need to display any payment method(s) and securely submit the payment for processing in your system.
Once the payment has succeeded or failed, update the order, as shown below.
Update the Order using Callback
// send header... Content-Type: application/json
POST 'https://core.sbx.eu.rezolve.com/api/v3/external/callback/5cec6b9c-ec38-47c7-97ce-b4872d7b684b'
{
"action": "approve",
"reason": null
}
To update the order with the payment result, set header Content-Type: application/json
and make a POST
to the Rezolve callback endpoint, referencing the payment_id
in the url and giving the result of the payment transaction in the body.
The endpoints urls for the EU environment are:
- Sandbox:
https://core.sbx.eu.rezolve.com/api/v3/external/callback/[payment_id]
- Production:
https://core.eu.rezolve.com/api/v3/external/callback/[payment_id]
The callback has two fields in the body, the action
and the reason
.
If payment succeeds, action
is approve
, and the reason
is null.
If payment fails, action
is reject
and the reason
is an optional string, which you can populate with any error code and/or explanatory text your customer might need to make an inquiry.
Callback Response
The callback will respond with one of the following http response codes - there is no body to the response:
Responds codes:
204
- (No Content) when the payment is successfully updated400
- (Bad Request) when the provided request is invalid422
- (Unprocessable Entity) when the payment was already approved/rejected500
- (Internal Server Error) some internal error happened
Please note that updating the order status is not instantaneous, and so a 204
may be returned instead of 422
if two requests are made in close proximity.
Click and Collect
Click and Collect enables users to select products online, and pick them up in-store. The consumer may either pay online, or pay in-store.
Please note that your Resolve Commerce Engine must have Pick Up In Store enabled under the Advanced menu, for this option to apply.
The Click and Collect flow is no different from in the Cart Buy or Instant Buy examples, it is only the user-chosen values that change.
1. Choices returned by PaymentOptionManager
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product)
let sampleMerchantID = "12"
rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result<PaymentOption, RezolveError>) in
switch result {
case .success(let option):
{
// For this example we assume the user chooses the first option. In reality, we should display all options and provide the ability to choose.
let paymentMethod = option.supportedPaymentMethods.first!
let shippingMethod = option.supportedDeliveryMethods.first!
}
case .failure(let error):
// Handle error gracefully
}
})
When you call PaymentOptionManager.getProductOptions
, it returns a list of your payment and shipment options.
2. Inspecting the returned options
{
"type": "standard",
"supported_shipping_methods": [
{
"execute": {
"method_code": "flatrate",
"manual_payment_label": "Pay on Collection",
"extension_attributes": null,
"display_name": "Fixed",
"carrier_code": "flatrate"
},
"details": {
"store_details": null
}
},
{
"execute": {
"method_code": "storepickup",
"manual_payment_label": "Pay on Collection",
"extension_attributes": [
{
"value": "2",
"code": "pickup_store"
}
],
"display_name": "Pickup In Store",
"carrier_code": "storepickup"
},
"details": {
"store_details": {
"telephone": "01234456789",
"pickup_store": 2,
"opening_times": [
],
"name": "Chao Yang Store",
"location": {
"longitude": 116.2573779,
"latitude": 39.9390628
},
"email": "china2@storepickuptesting.com",
"description": null,
"address": {
"street2": "",
"street1": "14 Dongsuan Huan Beilu",
"region": "Beijing",
"post_code": "100026",
"country": "\u00e4\u00b8\u00ad\u013a\u203a\u02dd",
"city": "BEIJING"
}
}
}
},
{
"execute": {
"method_code": "storepickup",
"manual_payment_label": "Pay on Collection",
"extension_attributes": [
{
"value": "1",
"code": "pickup_store"
}
],
"display_name": "Pickup In Store",
"carrier_code": "storepickup"
},
"details": {
"store_details": {
"telephone": "01234456789",
"pickup_store": 1,
"opening_times": [
],
"name": "China Qingdao Shi Store",
"location": {
"longitude": 36.3867744,
"latitude": 117.6466473
},
"email": "chinar@storepickuptesting.com",
"description": null,
"address": {
"street2": "",
"street1": "63 RENMIN LU",
"region": "Shandong",
"post_code": "266033",
"country": "\u00e4\u00b8\u00ad\u013a\u203a\u02dd",
"city": "QINGDAO SHI"
}
}
}
}
],
"supported_payment_methods": [
{
"type": "union_pay",
"data": {
"supported_types": [
],
"supported_shipping": [
"storepickup",
"flatrate"
],
"supported_networks": [
],
"requirements": {
"rezolve_phone": true
}
}
}
],
"options": {
},
"id": "aa665b54-7166-4a53-a275-9b4f693770dc"
}
To the right is a sample of data returned by PaymentOptionsManager.getProductOptions
. Look at the supported_shipping_methods
node at the top of the file. If the list contains at least one child with execute.method_code: storepickup
it means user can buy the product with Click and Collect. Every store where collecting the purchase is available will be listed as a separate item under supported_shipping_methods
.
At this point the user should select a payment method. They are listed under the supported_payment_methods
node. In some cases, like the example shown, there is one payment method available. In others, there might be more. For example, if SupportedPaymentMethod has type: cash
, cash payment may only be allowed with Click and Collect and not in Delivery. To verify that we need SupportedPaymentMethod
to be provided as an argument when creating a DeliveryUnit
.
3. Creating the Delivery Unit/Delivery Method
// Standard shipping example
let deliveryMethod = CheckoutShippingMethod(type: "flatrate", addressId: address.id)
// Click and Collect example
let deliveryMethod = CheckoutShippingMethod(type: "storepickup", pickupStore: store.pickupStore)
DeliveryMethod
has no need of an id for the object creation, so an empty String
used to feel the addressId
present in the constructor.
See the extension_attributes
node for this store id.
"extension_attributes": [ { "value": "2", "code": "pickup_store" } ]
Create the DeliveryMethod
as shown at right.
DeliveryMethod
is then passed to create the CheckoutBundleV2
, which is used to get totals before purchasing the product.
Background Listening
Background Listening enables a mode where your app listens for watermarked audio, and rather than displaying the linked item immediately, instead stores any items it detects (products or categories) in a list. When background listening is stopped, the list is presented to the consumer as a series of Touch Triggers for review and action. As the name "background listening" implies, listening can occur in the background, such as when the consumer is using another app, or when the phone is asleep. Background listening will pause if interrupted by a phone call, and then resume afterwards. Background listening requires that sound be detected through the phone's mic. Watermarked audio playing in another app while headphones are plugged in would not be detected.
The implementation for Android and IOS are somewhat different, so see individual sequence diagrams below.
IOS Background Listening
[ View full size ]
1. Start Background Listening, Listen, and Display Results
// Initialize `ScanManager` based on your RezolveSDK Session
guard let scanManager = rezolveSession?.getScanManager() else {
return
}
scanManager.autoDetectManagerDelegate = self
try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
try? scanManager.startAudioScan()
extension ViewController: AutoDetectManagerDelegate {
func onAutoDetectStopListening(resolved: [AutoDetectResult]) {
for item in resolved {
print(item.description())
}
}
func onAutoDetectError(error: String) {
// Handle error gracefully
}
}
Use AutoDetectManagerDelegate (IOS) or AutoDetectManager (Android) to start listening for watermarked audio. When the user stops Background Listening, the results detected (if any) will be returned as an array. Iterate over this array to display detected items as Touch Triggers.
Note: if you want to enable "back" navigation to the item list after the consumer views a list item, you should persist the list in your code. The SDK only returns the list when AutoDetect is stopped; it does not persist the list for you.
2. Display item Consumer selects using Trigger Manager
// Sample URL and `TriggerManager` initialization
let url = URL(string: "http://rzlv.co/1/2/3/8?ad=20&placement=25")
rezolveSession?.triggerManager.resolve(
url: url!,
productDelegate: self,
onRezolveTriggerStart: {},
onRezolveTriggerEnd: {},
errorCallback: { error in }
)
extension ViewController: ProductDelegate {
func onStartRecognizeImage() {
// Suggestion: Show an interstitial loader
}
func onFinishRecognizeImage() {
// Suggestion: Hide an interstitial loader
}
func onCategoryResult(merchantId: String, category: RezolveCategory) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onCategoryProductsResult(merchantId: String, category: RezolveCategory, productsPage: PageResult<DisplayProduct>) {
// See "Mall" section "3. If the consumer clicks a subcategory, call `getProductsAndCategories`"
}
func onProductResult(product: Product) {
// See "Mall" section "4. If the consumer clicks a Product, call `getProduct`"
}
func onError(error: String) {
// Handle error gracefully
}
}
Use TriggerManager to display a selected Touch Trigger item. You will have to pass a valid product url into TriggerManager, in the form:
http://rzlv.co/[partnerId]/[merchantId]/[categoryId]/[productId]?ad=[adId]&placement=[placementId]
Get partnerId
, merchantId
, categoryId
, productId
, adId
and placmentId
from the returned list array for that item. If no adId
and/or placementId
are available, omit the ?ad=[adId]&placement=[placementId]
part of the url (note that in this case, no stats will be gathered when this item is displayed or purchased).
Act
NOTE: This section only applies to Acts created in the Rezolve Cloud Engine. For Acts created in the Self Serve Portal, please see SSP Act Targets.
An Act is a type of consumer questionnaire that can be created in the Rezolve Commerce Engine. Acts are used to solicit information and collect consumer answers and contact information. No payment is required to submit an Act.
There is no specific data structure within the SDK that represents an Act; data-wise, it is a particular configuration of product that has no weight and no price. If your application receives a product with no weight and no price, you should display it as an Act. You do this by omitting certain information when displaying the item. You must do this in four places:
- 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).
The following link will open in a new window. Close the window to return to this documentation.
IOS Module Documentation
Changelog
All notable changes to the project will be documented in this log.
May 4 2021
- Add how-to directions for external payment.
April 13, 2021 - 2.0.11.2 release
- Added ability to silence Local Notifications sent from the
NearbyEngagementsManager
- Added
engagementId
toResolverEngagement
object - Added additional attributes to
price
object - Update to Handle and discard duplicate Location Engagements that point to the same
relatedItemId
- Added modular SSP page builder elements and data structures.
April 02, 2021 - 2.0.7.1 release
- Updated to SDK 2.0.7.1
- Updated sample app link to latest.
Mar 15, 2021
- Updated JWT Auth server url
- Updated link to Crashlytics
- Updated supported OS and IDE versions
Mar 10, 2021
- Split Android and IOS documentation into separate codebases. Reorganzied menu.
- Updated IOS Module Reference docs, auto-generated from code comments
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