- Get Started
- Guides
- Integrations
- References
- API Reference
- Basic Payment
- Forex
- Authentication
- Card Account
- Apple Pay
- Virtual Account
- Bank Account
- Token Account
- Customer
- Billing Address
- Merchant Billing Address
- Shipping Address
- Merchant Shipping Address
- Corporate
- Sender
- Recipient
- Merchant
- Marketplace & Cart
- Airline
- Lodging
- Passenger
- Tokenization
- Recurring Migration
- 3D Secure
- Custom Parameters
- Async Payments
- Webhook notifications
- Job
- Risk
- Point of Sale
- Response Parameters
- Card On File
- Chargeback
- Result Codes
- Payment Methods
- Transaction Flows
- Regression Testing
- Data Retention Policy
- Release Notes
- API Reference
- Support
ios - Getting Started
Requirements
The minimum supported iOS version currently is iOS 12 which is available in more than 98% of devices in circulation globally. The target IDE for interacting with the SDK is Xcode 12 onwards.
- macOS 11.x (or later)
- Xcode 12.x with Swift 5.x (or later)
- Latest version of CocoaPods
Open Source SDK Sample app
The most convenient way to get started with SDK integration is to get familiar with our SDK Sample app hosted right here on GitHub. This is a complete Xcode project that produces a working application, showcasing the majority of the features described in this documentation.
Remember to fill-in your API details in the file Config.swift before attempting to compile and run the SDK Sample app.
SDK Setup
The 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
Now, you are ready to include the SDK in the Podfile
of your own project. Keep in mind that Sample
is just a placeholder, and should be substituted by your own App Target name:
platform :ios, '12.0' use_modular_headers! target 'Sample' do pod 'RezolveSDK' end
Don’t forget to use the .xcworkspace
file to open your project in Xcode, instead of the .xcodeproj
file, from here on out. As a final step, in order to download the actual SDK binary locally, please execute the following in terminal:
pod install
We encourage you to update your workspace with every new release of the SDK by using
pod update RezolveSDK
.JWT Authentication
Overview
Smart Engage is utilizing a server-to-server JWT authentication system, conformant with the RFC7519 standard. If you are not familar with JSON Web Tokens, the site JWT.io provides an excellent primer on the use of JWTs, as well as links to various JWT libraries you can utilize.
When the Partner creates a new user on their auth server, or wishes to associate an existing user with Smart Engage for the first time, the partner must generate the Registration JWT, and then POST it to the /api/v1/authentication/register
endpoint. The Smart Engage server will validate the JWT, create a new user, create the user's public/private key pair, and return to you the Smart Engage EntityId.
When a user logs in to your auth system, generate a new Login JWT and supply to CreateSession
in the SDK. As long as the JWT is valid, the SDK can talk to Smart Engage. A method is suggested below for smoothly handling JWT timeouts without interrupting the SDK session.
Take a look at the following diagram in order to understand the sequence of the login/registration logic: JWT Flow Chart???might want to host the document elsewhere???
Terminology
Term | Definition |
---|---|
partner_id | A numerical ID you are assigned by Smart Engage. Usually a 2-4 digit integer. |
partner_api_key | The API Key you are assigned by Smart Engage. 36 characters including dashes. |
partner_auth_key | The Auth Key you are assigned by Smart Engage. This plays the role of the JWT Secret. The partner_auth_key is typically a ~90 character hash. |
JWT token | A JSON Web Token, consisting of a header, payload, and signature. The header and signature are signed with the parther_auth_key, above. It is used as a bearer token when communicating with the Smart Engage server. |
accessToken | In the iOS and Android code samples, the accessToken is the JWT Token you generated. |
deviceId | An ID that is randomly generated upon app install and stored. This id is placed in both the JWT payload and x-header sent by the SDK. The Smart Engage server checks that these values match to deter request origin spoofing. All calls except Registration calls require this. |
Create the Registration JWT
For testing via cURL or Postman without a server-side JWT library, you can use https://jwt.io/#debugger to generate valid JWT tokens.
Use https://www.epochconverter.com/ to generate the token expiration in Epoch time.
Click here for screenshots showing registration using jwt.io and Postman. ???might want to host elsewhere???
Requirements
You must possess:
Field | Description | Example |
---|---|---|
partner_id | The numerical ID you are assigned by Smart Engage | 317 |
partner_api_key | The API Key you are assigned by Smart Engage | a1b2c3d4-e5f6-g7h8-i9j0-a1b2c3d4e5f6 |
partner_auth_key | The Auth Key you are assigned by Smart Engage | qwer+4ty ... JYG6XavJg== (approx 90 characters) |
JWT Header
Key | Value | Notes |
---|---|---|
alg | HS512 | algorithm, HMAC SHA-512 |
typ | JWT | type |
{ "alg": "HS512", "typ": "JWT" }
JWT Payload
Key | Value | Notes |
---|---|---|
rezolve_entity_id | :NONE: | use :NONE: when registering |
partner_entity_id | your_user_id | The unique identifier for your user record. This may be a numerical ID, or an identifying string such as email address. |
exp | 1520869470 | Expiration, as a unix timestamp integer. Set the expiration value to a small number, now() + 30 minutes or less. |
{ "rezolve_entity_id": ":NONE:", "partner_entity_id": "partner_entity_id", "exp": 1520869470 }
Signature
HMACSHA512( base64UrlEncode(header) + "." + base64UrlEncode(payload), ${$partner_auth_key} )
Sign the header and payload with the partner_auth_key
. It is not necessary to decode the key before using it. Pass the whole value as a string to your library's getBytes
method.
You may need to specify the charset as UTF8.
- Example 1, Microsoft:
SecretKey(Encoding.UTF8.GetBytes(key));
- Example 2, Java:
Secret.getBytes("UTF-8");
The resulting JWT will look something like this (except without linebreaks); the first third is the header, the second third the payload, and the last third the signature:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9. eyJyZXpvbHZlX2VudGl0eV9pZCI6IjpOT05FOiIsInBhcnRuZXJfZW50aXR5X2lkIjoiMTIzIiwiZXhwIjoxNTIwODY5NDcwfQ. 5y2e6QpUKcqTNLTv75nO6a6iFPVxrF8YeAH5NTg2ZO9dkub31GEs0N46Hu2IJf1bQ_vC2IOO9Z2N7drmvA_AKg
Register a new Smart Engage User
After the JWT is created, POST it to the authentication/register
endpoint. This will create a Smart Engage User, generate a public/private keypair for the user, and return to you the corresponding Entity ID.
Example, using Sandbox endpoint:
POST: https://core.sbx.eu.rezolve.com/api/v2/authentication/register -H content-type: application/json -H x-rezolve-partner-apikey: your-api-key -H authorization: Bearer signed-jwt -d { "name": "John Doe", "last_name": "Doe", "first_name": "John", "email": "user@domain.com" }
Responses:
201 OK { "partner_id": "4", "entity_id": "d89d-d34fd-fddf45g8xc7-x8c7fddg" }
400 Bad Request { "errors": [ { "type": "Bad Request", "message": "Missing required parameters in request body. Required: [\"field1\", \"field2\"]", "code": "6" } ] }
422 Unprocessable entity { "errors": [ { "type": "Bad Request", "message": "Incorrect or malformed parameters", "code": "5" } ] }
The endpoint will reply with an Entity ID and the Partner ID. You should save the Entity ID to your authentication database and associate it with the user's record.
Logging in a User
Once a Smart Engage User has been registered and an entity_id
obtained, you can log in the user using the instructions below. For returning users, log them in via your normal method in your auth server, and then follow the instructions below. Create a new Login JWT
, and use it as the accessToken
in the createSession
method.
JWT Header
Note the addition of the "auth" line.
Key | Value | Notes |
---|---|---|
auth | v2 | auth version to use, login uses v2 |
alg | HS512 | algorithm, HMAC SHA-512 |
typ | JWT | type |
{ "auth": "v2", "alg": "HS512", "typ": "JWT" }
JWT Payload
Note the addition of the device_id.
Key | Value | Notes |
---|---|---|
rezolve_entity_id | your_rezolve_entity_id | use the entity_id you obtained during registration |
partner_entity_id | your_partner_entity_id | set it to the unique identifier for your user record |
exp | 1520869470 | Expiration, as a unix timestamp integer. Set the expiration value to a small number, now() + 30 minutes or less. |
device_id | wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a | An ID randomly generated upon app installation and stored. This ID is placed in both the JWT payload and x-header sent by the SDK. See below for generation instructions. |
{ "rezolve_entity_id": "entity123", "partner_entity_id": "partner_entity_id", "exp": 1520869470, "device_id": "wlkCDA2Hy/CfMqVAShslBAR/0sAiuRIUm5jOg0a" }
Signature
HMACSHA512( base64UrlEncode(header) + "." + base64UrlEncode(payload), ${$partner_auth_key} )
Sign the header and payload with the partner_auth_key
.
Generating the device_id
iOS uses the iOS "identifierForVendor" string, where the SDK supplies this 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
Mall Flow & Merchants
A consumer can enter the Mall by clicking on a related navigation option within the mobile app. The Mall showcases active merchants in an attractive layout that is conducive to casual browsing and subsequent purchasing.
Once a Merchant is selected, the consumer shifts into Product/Category browsing mode. Once a product has been selected, purchasing follows one of the previous examples of Instant Buy Flow or Cart Buy Flow.
Get list of Merchants in the Mall
All SDK related Managers like merchantManager
are already initialised for the developer. In order to get a list of Merchants, proceed by calling getMerchants
, providing an implementation of result
as a parameter. This will return an array of Merchant
objects. Parse each Merchant
object separately to get the id
, name
, tagline
, banner
, logo
, and all other parameters that are useful in your implementation.
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 termsAndConditions = $0.termsAndConditions // Assets let banner = $0.banner let logo = $0.logo let bannerThumbs = $0.bannerThumbs let logoThumbs = $0.logoThumbs } } case .failure(let error): // Error handling } })
Get list of Merchant's top-level Categories
Display your subcategories and products as returned by the getRootCategoryForMerchantWith
call. For subsequent navigation in categories, use getPaginatedCategoriesAndProducts
.
let sampleMerchantId = "12" rezolveSession?.productManager.getRootCategoryForMerchantWith(id: sampleMerchantId) { (result: Result) 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?.items.forEach { subCategory in print(subCategory.id) print(subCategory.parentId) print(subCategory.name) // ... } } case .failure(let error): // Error handling } })
Accessing a Subcategory
As the consumer navigates the down the category tree, call getPaginatedCategoriesAndProducts
to pull a paginated list of subcategories and products for that category.
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: Result) 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?.items.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): // Error handling } })
Merchant & 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
Merchant search offers the following features:
- List all Merchants sorted by Distance.
- Lookup by Merchant name, and sort results by Distance.
- Order results Ascending or Descending.
n order to get Merchants sorted by Distance, each Merchant in your ecosystem must configure at least one "Store" in the Merchant Portal, supplying longitude and latitude.
Search for a merchant requires the following parameters:
- Query string - Optional. Leave it blank to get all Merchants. If more than one term is supplied, terms are matched on an AND basis.
- Order By - Should merchants be sorted by
.location
. - Order -
.ascending
or.descending
. - Location - Optional. The
latitude
andlongitude
of the consumer phone. Merchant list cannot be sorted by distance without this parameter. - 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.
let searchData = MerchantSearchData( query: nil, orderBy: .location, order: .ascending, deviceInfo: DeviceInfo.current, location: Location(longitude: -73.9502622, latitude: 40.6726499) ) let pageOffset: Int = 1 let pageLimit: Int = 50
Now we are ready to combine our desired Query structure along with the actual API request:
rezolveSession?.merchantManager.searchForMerchants(using: searchData, page: pageOffset, limit: pageLimit) { (result: Result) in switch result { case .success(let result): // Current state print(result.merchants) print(result.page) print(result.total) result.merchants.forEach { // Base information print($0.id) print($0.name) print($0.tagline) print($0.priority) print($0.distance) // Assets print($0.banner) print($0.bannerThumbs) print($0.logo) print($0.logoThumbs) // List of Stores if let stores = $0.stores { stores.forEach { print($0.id) print($0.name) print($0.location) } } // List of Terms & Conditions $0.termsAndConditions.forEach { print($0.id) print($0.storeId) print($0.name) print($0.content) print($0.checkboxText) } } case .failure(let error): // Error handling } }
Product Search
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
- Order results Ascending or Descending.
To search for products you must supply the following parameters:
- Query string - Optional. Leave it blank to get 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.
- Product Type - Can be
.products
,.acts
, or.all
. - Order By - Should products be sorted by
.price
. - Order -
.ascending
or.descending
. - Location - Optional. The
latitude
andlongitude
of the consumer phone. - 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.
let searchData = ProductSearchData( query: "book", merchantId: "123", orderBy: .price, order: .descending, location: Location(longitude: -73.9502622, latitude: 40.6726499), includeInResults: .products, deviceInfo: DeviceInfo.current ) let pageOffset: Int = 1 let pageLimit: Int = 50
Now we are ready to combine our desired Query structure along with the actual API request:
rezolveSession?.productManager.searchForProducts(using: searchData, page: pageOffset, limit: pageLimit, completionHandler: { (result: Result) in switch result { case .success(let result): // Current state print(result.products) print(result.page) print(result.total) result.products.forEach { // Base information print($0.id) print($0.merchantId) print($0.merchantName) print($0.categoryId) print($0.categoryName) print($0.price) print($0.isAct) print($0.isVirtual) print($0.hasRequiredOptions) // Assets print($0.image) print($0.imageThumbnails) } case .failure(let error): // Error handling } })
Presenting a Product
Accessing a Product
When the consumer taps on a Product, call getProductDetails
to fetch the product's information tree. At this point, the user can either add the product to their cart, tap "Buy now", or press "Back" to return to the Category view.
let sampleMerchantID = "12" let sampleCategoryID: Int = 70 let sampleProductID = "6" let sampleProduct = Product(id: sampleProductID) rezolveSession?.productManager.getProductDetails(merchantId: sampleMerchantID, categoryId: sampleCategoryID, product: sampleProduct) { (result: Result) 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.availableOptions?.forEach { $0.combination.forEach { variant in print(variant.code) print(variant.value) } } 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(value.valueId) } $0.valuesId.forEach { valueId in print(valueId) } print($0.value) } print(product.productPlacement) } case .failure(let error): // Error handling } })
Payment & Delivery
A necessary feature for any online shopping platform is management of the user's payment methods and their preferred delivery addresses. Payment cards are the default payment method supported by The SDK.
Managing Payment methods
All payment method management is handled by the rezolveSession
's walletManager
in the form of PaymentCard
instances. A reminder, just like all other managers the walletManager
is already initialised for the developer.
In order to add a payment method to the user's payment methods, proceed by calling walletManager
's create
, providing the payment method in the PaymentCard
format as a parameter. Address
must be created and its id must be assigned to PaymentCard
before creating.
rezolveSession?.walletManager.create(paymentCard: cardToSave, completionHandler: { [weak self] (result: Result) in switch result { case .success(let card): // Handle success case .failure(let error): // Error handling } })
In order to get a list of user's payment methods, proceed by calling walletManager
's getAll
.
rezolveSession?.walletManager.getAll(completionHandler: { [weak self] (result: Result<[PaymentCard], RezolveError>) in switch result { case .success(let cards): // Handle success case .failure(let error): // Error handling } })
In order to get a specific payment method, proceed by calling walletManager
's get
, providing the payment method's id
as a parameter.
let cardId = "1" rezolveSession?.walletManager.get(id: cardId, completionHandler: { [weak self] (result: Result) in switch result { case .success(let card): // Handle success case .failure(let error): // Error handling } })
In order to update a specific payment method, proceed by calling walletManager
's update
, providing the updated payment method in the PaymentCard
format as a parameter.
rezolveSession?.walletManager.update(paymentCard: cardToUpdate, completionHandler: { [weak self] (result: Result) in switch result { case .success(let card): // Handle success case .failure(let error): // Error handling } })
In order to delete a specific payment method, proceed by calling walletManager
's delete
, providing the payment method to delete in the PaymentCard
format as a parameter.
rezolveSession?.walletManager.delete(paymentCard: cardToDelete, completionHandler: { [weak self] (result: Result) in switch result { case .success(): // Handle success case .failure(let error): // Error handling } })
Managing Delivery addresses
All address management is handled by the rezolveSession
's addressbookManager
in the form of Address
instances. A reminder, just like all other managers the addressbookManager
is already initialised for the developer.
In order to add a user address to the user's address book, proceed by calling addressbookManager
's create
, providing the address in the Address
format as a parameter.
rezolveSession?.addressbookManager.create(address: address, completionHandler: { [weak self] (result: Result< Address, RezolveError>) in switch result { case .success(let address): // Handle success case .failure(let error): // Error handling } })
In order to get a list of user's addresses, proceed by calling addressbookManager
's getAll
.
rezolveSession?.addressbookManager.getAll(completionHandler: { [weak self] (result: Result<[Address], RezolveError>) in switch result { case .success(let addresses): // Handle success case .failure(let error): // Error handling } })
In order to get a specific address, proceed by calling addressbookManager
's get
, providing the address's id
as a parameter.
let addressId = "42" rezolveSession?.addressbookManager.get(id: addressId, completionHandler: { [weak self] (result: Result) in switch result { case .success(let address): // Handle success } case .failure(let error): // Error handling } })
In order to update a specific address, proceed by calling addressbookManager
's update
, providing the updated address in the Address
format as a parameter.
rezolveSession?.addressbookManager.update(address: addressToUpdate, completionHandler: { [weak self] (result: Result) in switch result { case .success(let address): // Handle success case .failure(let error): // Error handling } })
In order to delete an address, proceed by calling addressbookManager
's delete
, providing the address to delete in the Address
format as a parameter.
Warning: Address should not be used by credit card at the time of deleting
rezolveSession?.addressbookManager.delete(address: addressToDelete, completionHandler: { [weak self] (result: Result) in switch result { case .success(): // Handle success case .failure(let error): // Error handling } })
Managing Phone numbers
All phone number management is handled by the rezolveSession
's phonebookManager
in the form of Phone
instances. A reminder, just like all other managers the phonebookManager
is already initialised for the developer.
In order to add a phone number to the user's phone book, proceed by calling phonebookManager
's create
, providing the address in the Phone
format as a parameter.
rezolveSession?.phonebookManager.create(phoneToCreate: phone, completionHandler: { [weak self] (result: Result) in switch result { case .success(let phone): // Handle success case .failure(let error): // Error handling } })
In order to get a list of user's phone numbers, proceed by calling phonebookManager
's getAll
.
In order to update a specific phone number, proceed by calling phonebookManager
's update
, providing the updated phone number in the Phone
format as a parameter.
rezolveSession?.phonebookManager.update(phone: phoneToUpdate, completionHandler: { [weak self] (result: Result) in switch result { case .success(let phone): // Handle success case .failure(let error): // Error handling } })
In order to delete a phone number, proceed by calling phonebookManager
's delete
, providing the phone number to delete in the Phone
format as a parameter.
rezolveSession?.phonebookManager.delete(phone: phoneToDelete, completionHandler: { [weak self] (result: Result) in switch result { case .success(): // Handle success case .failure(let error): // Error handling } })
Instant Buy Flow
Available Payment & Shipping options
Call paymentOptionManager
to get Payment & Shipping options for the current Merchant. This tutorial assumes the consumer has chosen a form of Credit Card payment, and that the Product will be delivered to an Address.
You must repeat this SDK method if the user alters the product Variant (size, colour, etc), changes Product Quantity, changes Shipping choice, or changes Payment option.
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product) let sampleMerchantID = "12" rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result) in switch result { case .success(let option): { // For this example we will assume that the user chooses the first option. Instead, we should display all available options and provide the ability to choose from a User Interface. let paymentMethod = option.supportedPaymentMethods.first! let shippingMethod = option.supportedShippingMethods.first! } case .failure(let error): // Error handling } })
Showing Payment Card choices
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.
rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in // Handle payment cards }
Creating a Checkout Bundle & Order
Once you have Product information, create a CheckoutProduct
object. Then call the SDK's CheckoutManagerV2.checkoutProduct
method to create an Order and get Totals. The response Order object includes an Order ID, Order Total, and Price breakdowns.
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) in switch result { case .success(let order): { print(order.id) print(order.finalPrice) // ... } case .failure(let error): // Error handling } })
Finalising the Order & Buying
When the user confirms intent, pass the Credit Card choice and the entered CVV value to the createPaymentRequest
method. This creates an encrypted paymentRequest
object needed for Checkout.
In this tutorial, we assume the user has chosen 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.
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) in switch result { case .success(let order): { print(order.id) print(order.partnerId) print(order.partnerName) // ... } case .failure(let error): // Error handling } })
Cart Buy Flow
Add Product to the Cart
Call CheckoutManagerV2.addProductToCart
to add the Product to Cart.
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product) let sampleMerchantID = "12" rezolveSession?.cartManager.createCartWithProduct(sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result) in switch result { case .success(let cart): { print(cart.id) print(cart.merchantId) // ... } case .failure(let error): // Handle error gracefully } })
Available Payment & Shipping options
Call paymentOptionManager
to get Payment & Shipping options for the current Merchant. This tutorial assumes the consumer has chosen a form of Credit Card payment, and that the Product will be delivered to an Address.
You must repeat this SDK method if the user alters the product Variant (size, colour, etc), changes Product Quantity, changes Shipping choice, or changes Payment option.
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 will assume that the user chooses the first option. Instead, we should display all available options and provide the ability to choose from a User Interface. let paymentMethod = options.first?.supportedPaymentMethods.first let shippingMethod = options.first?.supportedShippingMethods.first } case .failure(let error): // Error handling } })
Showing Payment Card choices
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.
rezolveSession?.walletManager.getAll { (result: Result<[PaymentCard], RezolveError>) in // Handle payment cards }
Creating a Checkout Bundle & Order
Once you have Product information, create a CheckoutProduct
object. Then call the SDK's CheckoutManagerV2.checkoutProduct
method to create an Order and get Totals. The response Order object includes an Order ID, Order Total, and Price breakdowns.
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) in switch result { case .success(let order): { print(order.id) print(order.finalPrice) // ... } case .failure(let error): // Error handling } })
Finalising the Order & Buying
When the user confirms intent, pass the Credit Card choice and the entered CVV value to the createPaymentRequest
method. This creates an encrypted paymentRequest
object needed for Checkout.
In this tutorial, we assume the user has chosen 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.
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) in switch result { case .success(let order): { print(order.id) print(order.partnerId) print(order.partnerName) // ... } case .failure(let error): // Error handling } })
Click & 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. 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.
Please note that the commerce platfrom must have Pick Up In Store enabled under the Advanced menu, for this option to apply.
Choices returned by PaymentOptionManager
When you call PaymentOptionManager.getProductOptions
, it returns a list of your Payment and Shipment options.
let sampleCheckoutProduct = createCheckoutProductWithVariant(product: product) let sampleMerchantID = "12" rezolveSession?.paymentOptionManager.getPaymentOptionFor(checkoutProduct: sampleCheckoutProduct, merchantId: sampleMerchantID) { (result: Result) in switch result { case .success(let option): { // For this example we will assume that the user chooses the first option. Instead, we should display all available options and provide the ability to choose from a User Interface. let paymentMethod = option.supportedPaymentMethods.first! let shippingMethod = option.supportedDeliveryMethods.first! } case .failure(let error): // Error handling } })
Creating the Delivery Unit/Delivery Method
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 follows:
// Standard Shipping example let deliveryMethod = CheckoutShippingMethod(type: "flatrate", addressId: address.id) // Click & Collect example let deliveryMethod = CheckoutShippingMethod(type: "storepickup", pickupStore: store.pickupStore)
DeliveryMethod
is then passed to create the CheckoutBundleV2
, which is used to get totals before purchasing the product.
Understnading Acts
Getting and posting of acts is handled by rezolveSession
's sspActManager
in the form of SspAct
instances. A reminder, just like all other managers the sspActManager
is already initialised for the developer.
Acts, save for historic items which will be covered at the end of this page, cannot be fetched in bulk. In order to get an act, proceed by calling sspActManager
's getAct
method. Required parameters are the act's id and an optional parameter describing the act's image width. If no image exists this parameter is ignored by the developer.
rezolveSession?.sspActManager.getAct(actId: actId, completionHandler: { [weak self] (result: Result) in switch result { case .success(let act): // Handle success // Basic information let scanId = act.scanId let id = act.id let dateCreated = act.dateCreated let dateUpdated = act.dateUpdated let status = act.status let name = act.name let shortDescription = act.shortDescription let longDescription = act.longDescription let merchantId = act.merchantId let ownerId = act.ownerId let images = act.images let questions = act.questions // Is buy act. let isBuy = act.isBuy // Is information act. let isInformationPage = act.isInformationPage // Act custom URL (used to resolve product/category using TriggerManager.resolve(url:)). let customUrl = act.customUrl // Form builder data. If an act is displayed through custom elements those elements will be available here. let pageBuildingBlocks: [PageElement]? } case .failure(let error): // Error handling } })
In order to post an act proceed by calling sspActManager
's submitAct
method, providing the act's id and act data in SspActSubmission
format as parameters. Please note that any answers to relevant act questions are a part of the act data in SspSubmissionAnswer
format.
rezolveSession?.sspActManager.submitAct(actId: actId, actSubmission: sspActSubmission, completionHandler: { [weak self] (result: Result< SspActSubmission, RezolveError>) in switch result { case .success(let actSubmission): // Handle success } case .failure(let error): // Error handling } })
In order to fetch historical act submissions by the user proceed by calling sspActManager
's submittedActs
method, while providing the entity id as a parameter. The request will return a SspActSubmissionBundle
instance, while the submitted acts are the data
property of this object.
Please note that answers in the act data are in SspSubmissionAnswer
format. Meaning the text of the question is unavailable until the developer fetches the act. To get the question texts, the developer has to call the above-mentioned getAct
with an adequate act id.
rezolveSession?.sspActManager.submittedActs(entityId: entityId, completionHandler: { [weak self] (result: Result< SspActSubmissionBundle, RezolveError>) in switch result { case .success(let bundle): let submittedActs = bundle.data // Handle success submittedActs.forEach { // User information let userId = $0.userId let entityId = $0.entityId let userName = $0.userName let personTitle = $0.personTitle let firstName = $0.firstName let lastName = $0.lastName let email = $0.email let phone = $0.phone // Submitted act information let scanId = $0.scanId let latitude = $0.latitude let longitude = $0.longitude let answers = $0.answers let actName = $0.actName let hasCoupons = $0.hasCoupons } } case .failure(let error): // Error handling } })
Location Triggers
One of the most powerful tools you can take advantage of with Smart Engage is Location Triggers. It keeps a reference of the user's location, and through a decision making mechanism it sends unobtrusive Push Notifications at the right time, thus enticing customers into an action when it is deemed proper.
Initialising
The entry point of Location Triggers is run once, in order to inform the backend about the user's essential information.
let checkInPayload = RXPCheckIn( applicationId: APP_BUNDLE, version: APP_VERSION, os: "IOS", pushToken: APP_PUSH_TOKEN, pushTokenType: tokenType, apiKey: REZOLVE_API_KEY ) rezolveSession?.rxpManager.requestCheckIn(checkInPayload: checkInPayload, completionHandler: { (result: Result) in switch result { case .success(let success): print("[RXP] Access token success -> \(success.sessionAccessToken)") case .failure(let error): // Error handling } })
The
pushTokenType
can be either "APN_DEBUG"
or "APN_RELEASE"
. This decision is based on whether you are running the application in Xcode's debugging mode, or from the AppStore/TestFlight.Getting available Interest Tags
With the current user already in check-in
state, as seen in the previous section, we can now fetch the state of the user's Interest Tags. An array of RXPTag
models will be provided as a response to this request, indicating the following parameters for each item:
-
tagId
A unique identifier for this item -
name
Descriptive name of the Interest Tag -
state
Indicates whether the Tag is"UNDEFINED"
,"ENABLED"
or"DISABLED"
for this user
rezolveSession?.rxpManager.getAllTags(completionHandler: { (result: Result<[RXPTag], RezolveError>) in switch result { case .success(let models): print("[RXP] Tags success -> \(models)") case .failure(let error): // Error handling } })
Updating Interest Tags
The defining step that subscribes your audience into your established Location Triggers is updating the user's chosen Interest Tags, as seen on the following example:
let tags = RXPSelectedTagList(tags: [ RXPSelectedTag(tagId: 1, enabled: true), ... ... ]) rezolveSession?.rxpManager.updateUserTags(tagsPayload: tags, completionHandler: { (result: Result<[RXPTag], RezolveError>) in switch result { case .success(let models): print("[RXP] Tags update success -> \(models)") case .failure(let error): // Error handling } })
Invoking Location Tracking
After the user has identified to the backend service, and his/her Interest Tags are set, the last step would be to engage Location Tracking. Following, is an example of a class that would handle startMonitoring
, stopMonitoring
the user's location at will:
class RezolveLocationService { var rezolveSsp: RezolveSsp? private var nearbyEngagementsManager: NearbyEngagementsManager? { return rezolveSsp?.nearbyEngagementsManager } init() { // Ask permission to show Push Notifications } func startMonitoring() { nearbyEngagementsManager?.startMonitoringForNearbyEngagements() nearbyEngagementsManager?.delegate = self } func stopMonitoring() { nearbyEngagementsManager?.stopMonitoringForNearbyEngagements() nearbyEngagementsManager?.stopUpdatingDistanceFromNearbyEngagements() nearbyEngagementsManager?.resetNotificationSuppressData() nearbyEngagementsManager?.delegate = nil } }
You can initialise this class at the point where the SDK's rezolveSdk?.createSession
is finished:
extension RezolveLocationService: NearbyEngagementsManagerDelegate { func didStartMonitoring(for circularRegions: [CLCircularRegion], coordinate: CLLocationCoordinate2D, radius: Int) { // Informs that the SDK has started monitoring regions } func didEnter(circularRegion: GeofenceData) { // The user has entered in region with data `circularRegion` } func didExit(circularRegion: GeofenceData) { // The user has left region with data `circularRegion` } func didUpdateCurrentDistanceFrom(location: CLLocationCoordinate2D, geofences: [GeofenceData], beacons: [BeaconData]) { // The user updated their current distance from regions } func didReceiveInAppNotification(engagement: EngagementNotification) { // Whilst Push Notifications are not allowed, this can function as a fallback } func didFail(with error: Error) { // Error handling } }
Scanning Advertisements
One of the most engaging features of Smart Engage is transforming plain advertisements into Shoppable Ads. The user can now use Smart Engage to scan an advertisement that is enriched with Smart Engage's watermark, in order to get presented with one of the following:
- Merchant landing page
- Subcategories
- Promoted Product
- Interactive Act or Promotion
- Website
Prerequisites
You will need to add the NSCameraUsageDescription
permission into the Info.plist
file of your application. A typical explanation of why your Smart Engage-enabled application is asking for this permission would be Allow camera usage for scanning visual engagements
. Feel free to add more detail to it, if conditions demand it.
Invoking the ScanManager
Create a designated ViewController
to handle the Camera Scan integration. Initialise scanManager
, and enable the scan screen using session.startVideoScan()
.
guard let scanManager = rezolveSession?.getScanManager() else { return } scanManager.productResultDelegate = self try? scanManager.startVideoScan(scanCameraView: self.view as! ScanCameraView, rectOfInterest: .frame)
When an image is scanned, the delegation methods of ProductScan
will contain all information regarding the engagement that was resolved.
extension ViewController: ProductDelegate { func onStartRecognizeImage() { // Suggestion: Show an interstitial loader } func onFinishRecognizeImage() { // Suggestion: Hide loader } func onProductResult(product: Product) { // The user has scanned an image that resolved into a `Product` } func onCategoryResult(merchantId: String, category: Rezolve.Category) { // The user has scanned an image that resolved into a `Rezolve.Category` } func onSspEngagementResult(engagement: ResolverEngagement) { // The user has scanned an image that resolved into a `ResolverEngagement` } func onError(error: String) { // Error handling } func onCategoryProductsResult(merchantId: String, category: Category, productsPage: PageResult) { //The user has scanned an image that resolved into RezolveSDK.PageResult } func onInactiveEngagement(payload: RezolveCustomPayload) { // Called when eangagement is inactive } }
Handling Scan results
After the delegation methods have received their designated data objects, refer to the following documentation sections for more information:
- Learn how to present a
Product
- Present a
Rezolve.Category
- Handle the contents of
ResolverEngagement
User Activity
In order to present information about user's activities throughout the app, you will need to use the Activity Manager. It provides insights on purchase history, shipping method, delivery address, total product pricing, and other useful details. Your search criteria depends entirely on providing a Date object, so you can get history items based on specific timeframes.
User Activity Manager
The following features are available:
- Get all available orders.
- Get transaction history items for a time period.
- Get transaction history items with the provided identifier.
- Post a history item.
All SDK related Managers like userActivityManager are already initialised for the developer.
Get orders from server
To fetch the list of orders from the server, call the getOrders function.
- Parameters:
- date: Optional date. Defaults to the date/time the method is called.
- completionHandler: A closure to be called when the task is completed.
Result will be an array of HistoryTransactionDetails
in case of success, or an error if it failed.
rezolveSession?.userActivityManager.getOrders() { (result: Result<[HistoryTransactionDetails], RezolveError>) in switch result { case .success(let orders): { orders.forEach { // Order's information let status = $0.status let shippingMethod = $0.shippingMethod let shippingAddress = $0.shippingAddress let storeDetails = $0.storeDetails let price = $0.price let paymentMethod = $0.paymentMethod let partnerId = $0.partnerId let orderId = $0.orderId let location = $0.location let items = $0.items // Merchant's information let merchantPhone = $0.merchantPhone let merchantName = $0.merchantName let merchantId = $0.merchantId let merchantEmail = $0.merchantEmail let merchantPhone = $0.merchantPhone let merchantName = $0.merchantName // Client's information let firstName = $0.firstName let lastName = $0.lastName let phone = $0.phone let email = $0.email let billingAddress = $0.billingAddress // Extra information let additionalAttributes = $0.additionalAttributes } } case .failure(let error): // Error handling } })
Description of the fields for the HistoryTransactionDetails model:
- Parameters:
- timestamp: Order placement timestamp.
- status: Order's status.
- shippingMethod: Order's shipping method.
- shippingAddress: Order's shipping address. *Optional
- storeDetails: Order's store details. *Optional
- price: Order's price.
- phone: Client's phone.
- paymentMethod: Payment method used at order placement.
- partnerId: Partner's Id.
- orderId: Order's id.
- merchantPhone: Merchant's phone.
- merchantName: Merchant's name.
- merchantId: Merchant's id.
- merchantEmail: Merchant's email.
- lastName: Client's last name. *Optional
- merchantId: Products being part of an order.
- location: Optional order placement location. *Optional
- firstName: Client's first name. *Optional
- email: Client's email. *Optional
- billingAddress: Client's billing address. *Optional
- additionalAttributes: Additional details. *Optional
Get transaction history items for a time period
To fetch a list of historical transactions from the server, call the getHistoryItems function.
- Parameters:
- from: Optional initial date.
- to: Optional end date.
- completionHandler: A closure to be called when the task is completed.
Result will be an array of HistoryItem
in case of success, or an error if it failed.
rezolveSession?.userActivityManager.getHistoryItems() { (result: Result<[HistoryItem], RezolveError>) in switch result { case .success(let orders): { orders.forEach { // Information let id = $0.id let catalogName = $0.catalogName let productName = $0.productName let merchantName = $0.merchantName let type = $0.type let partnerId = $0.partnerId let merchantId = $0.merchantId let entityId = $0.entityId let catalogId = $0.catalogId let productId = $0.productId let location = $0.location } } case .failure(let error): // Error handling } })
Description of the fields for the HistoryItem model:
- Parameters:
- id: The history item identifier.
- catalogName: The catalog name. *Optional
- productName: The product name. *Optional
- merchantName: The merchant name.
- type: The type.
- partnerId: The partner identifier.
- merchantId: The merchant identifier.
- entityId: The entity identifier. *Optional
- catalogId: The catalog identifier.
- productId: The product identifier. *Optional
- location: The location.
Get transaction history items with the provided identifier
To fetch a list of historical transactions from the server, call the getHistoryItems function.
- Parameters:
- id: The scan identifier.
- completionHandler: A closure to be called when the task is completed.
Result will be an array of HistoryItem
in case of success, or an error if it failed.
rezolveSession?.userActivityManager.getHistoryItems() { (result: Result<[HistoryItem], RezolveError>) in switch result { case .success(let orders): { orders.forEach { // Information let id = $0.id let catalogName = $0.catalogName let productName = $0.productName let merchantName = $0.merchantName let type = $0.type let partnerId = $0.partnerId let merchantId = $0.merchantId let entityId = $0.entityId let catalogId = $0.catalogId let productId = $0.productId let location = $0.location } } case .failure(let error): // Error handling } })
Description of the fields for the HistoryItem model:
- Parameters:
- id: The history item identifier.
- catalogName: The catalog name. *Optional
- productName: The product name. *Optional
- merchantName: The merchant name.
- type: The type.
- partnerId: The partner identifier.
- merchantId: The merchant identifier.
- entityId: The entity identifier. *Optional
- catalogId: The catalog identifier.
- productId: The product identifier. *Optional
- location: The location.
Post a history item
To post a history item to server, call the postHistoryItem function.
- Parameters:
- historyItem: The history item to be posted.
- completionHandler: A closure to be called when the task is completed.
Result will be an array of HistoryItem
in case of success, or an error if it failed.
let historyItem = HistoryItem(...) rezolveSession?.userActivityManager.postHistoryItem(historyItem) { (result: Result) in switch result { case .success(let historyItem): { // Information let id = historyItem.id let catalogName = historyItem.catalogName let productName = historyItem.productName let merchantName = historyItem.merchantName let type = historyItem.type let partnerId = historyItem.partnerId let merchantId = historyItem.merchantId let entityId = historyItem.entityId let catalogId = historyItem.catalogId let productId = historyItem.productId let location = historyItem.location } case .failure(let error): // Error handling } })
Description of the fields for the HistoryItem model:
- Parameters:
- id: The history item identifier.
- catalogName: The catalog name. *Optional
- productName: The product name. *Optional
- merchantName: The merchant name.
- type: The type.
- partnerId: The partner identifier.
- merchantId: The merchant identifier.
- entityId: The entity identifier. *Optional
- catalogId: The catalog identifier.
- productId: The product identifier. *Optional
- location: The location.
Create a guest user
In some applications, it's useful to have a guest User object to pass around even before the (human) user has registered or logged in. Normally, you want this guest user to persist as long as the session persists.
The registration of the guest
Our approach is to create a guest user object in the database and store its data in session.
Result will be an object GuestUser
in case of success, or an error if it failed.
struct GuestUser: Codable { let username: String let email: String? let phone: String? let sdkEntity: String let sdkPartner: String } func guestRegister(success: @escaping (String) -> Void, error: @escaping (Error) -> Void) { let headers = [ "x-rezolve-partner-apikey": REZOLVE_API_KEY ] let request = Alamofire.request( "...register/guest...", method: .post, parameters: [:], encoding: JSONEncoding.default, headers: headers ) request.responseJSON { response in guard response.result.isSuccess else { error(.failedRegistrationResponse) return } guard let data = response.data, let guestUserInfo = try? JSONDecoder().decode(GuestUser.self, from: data) else { error(.failedRegistrationDataDecoding) return } success(guestUserInfo.sdkEntity) } }
The Log in of the guest
When (and if) the guest registers, we log in the guest user. We need the sdkEntity from the previous query.
Result will be an object GuestUser
in case of success, or an error if it failed.
struct GuestUser: Codable { let username: String let email: String? let phone: String? let sdkEntity: String let sdkPartner: String } struct GuestUserCredentials: Codable { let entityId: String let deviceId: String } func guestLogin(entityId: String, success: @escaping (GuestUser) -> Void, error: @escaping (Error) -> Void) { var data: [String: String]? { if let data = try? JSONEncoder().encode( GuestUserCredentials( entityId: entityId, deviceId: Device().id ) ) { return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: String] } return nil } let headers = [ "x-rezolve-partner-apikey": REZOLVE_API_KEY ] let request = Alamofire.request( "...login/guest...", method: .post, parameters: data, encoding: JSONEncoding.default, headers: headers ) request.responseJSON { response in guard response.result.isSuccess else { error(.failedLoginResponse) return } guard let data = response.data, let guestUserInfo = try? JSONDecoder().decode(GuestUser.self, from: data) else { error(.failedLoginDataDecoding) return } // Auth information let jwt = response.response?.allHeaderFields["Authorization"] as? String let refreshToken = response.response?.allHeaderFields["Refresh-Token"] as? String success(guestUserInfo) } }
Pairing a guest with a logged-in user.
To connect a guest to a logged-in user, we need to send information to the API such as the device ID number. Guests can also create their own accounts by registering themselves on the special page. The action steps will be the same in this case.
func signIn(successCallback: @escaping (String?, String?) -> Void, errorCallback: @escaping (Error?, Int?) -> Void) { let data: [String: String] = { return [ "username": "user", "password": "password", "deviceId": Device().id ] }() let headers = [ "x-rezolve-partner-apikey": REZOLVE_API_KEY ] let request = Alamofire.request(.../credentials/login..., method: .post, parameters: data, encoding: JSONEncoding.default, headers: headers) request.responseJSON { requestResult in if(requestResult.response?.statusCode == 200) { // Auth information let jwt = response.response?.allHeaderFields["Authorization"] as? String let refreshToken = response.response?.allHeaderFields["Refresh-Token"] as? String if let json = requestResult.result.value as? [String: Any] { let entityId: String? = json["sdkEntity"] as? String let partnerId: String? = json["sdkPartner"] as? String successCallback(entityId, partnerId) } } else { errorCallback(requestResult.error, requestResult.response?.statusCode) } } }