iOS SDK guide

Requirements

  • iOS 12.0+

Retrieve company credentials

  1. Sign into the CCAI Platform Portal using Admin credentials

  2. Go to Settings > Developer Settings

  3. Under Company Key and Secret Code, note Company Key and Company Secret Code.

Getting started

Installation

Download the Example App

  1. Download the iOS Example App.

  2. Navigate to the folder and install dependencies using CocoaPods. ```sh $ pod install --project-directory=ExampleApp ```

  3. To quickly configure project settings, run a shell script. ```sh $ ./setup.sh ```

Or you can edit manually using the steps below.

Open ExampleApp.xcworkspace

Replace UJETCompanyKey, UJETCompanySecret, and UJETSubdomain value in Info.plist from your Admin Portal > Settings > Developer Settings (UJETSubdomain will be found in the URL, ex. https://your_sub_domain.ujet.co/settings/developer-setting)

Integrate to your project

Swift package manager
  1. Add the https://customer:glpat-TwLqiBEKX54hzDyCgK_s@gitlab.com/ujet/ujet-ios-sdk-sp.git as a Swift Package.

  2. In your Build Settings, put -ObjC on Other Linker Flags.

  3. As of the latest release of Xcode (currently 13.2), there is a known issue with consuming binary frameworks distributed via Swift Package Manager. The current workaround to this issue is to add a Run Script Phase to the Build Phases of your Xcode project. This Run Script Phase should come after the Embed Frameworks build phase. This new Run Script Phase should contain the following code:

    find "${CODESIGNING_FOLDER_PATH}" -name '*.framework' -print0 | while read -d $'\0' framework
    do
    codesign --force --deep --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --preserve-metadata=identifier,entitlements --timestamp=none "${framework}"
    done
    
CocoaPods
  1. Add the following line to the Podfile:

    pod 'UJET', :podspec => 'https://sdk.ujet.co/ios/x.y.z/CCAI Platform.podspec' #specific version x.y.z

  2. Run pod install. If the iOS SDK has been integrated previously, run pod update CCAI Platform instead

Carthage

We recommend using a dependency manager or manual integration because a CCAI Platform dependency does not currently support Carthage.

1. Add the following lines:

binary "https://sdk.ujet.co/ios/UJETKit.json

binary "https://sdk.ujet.co/ios/UJETFoundationKit.json

binary https://raw.githubusercontent.com/twilio/twilio-voice-ios/Releases/twilio-voice-ios.json

Manual Integration

This is not supported: https://github.com/twilio/conversations-ios/issues/12

binary https://raw.githubusercontent.com/twilio/conversations-ios/master/twilio-convo-ios.json

1. Run carthage bootstrap --use-xcframeworks (or carthage update --use-xcframeworks (if you're updating dependencies)

  1. Download UJETKit.xcframework, UJETFoundationKit.xcframework, UJETChatRedKit.xcframework, UJETChatBlueKit.xcframework, UJETTwilioCallKit.xcframework, and all dependencies TwilioVoice.xcframework and TwilioConversationsClient.xcframework.

  2. Add the UJETKit.xcframework to your target by dragging it into the Frameworks, Libraries, and Embedded Content section.

  3. Repeat Steps 2 and 3 on all dependencies from Step 1.

  4. In your _Build Settings_, put -ObjC on Other Linker Flags

  5. Add libc++.tbd as a dependency in Linked Frameworks section of target.

If you are going to build the SDK manually with the example project use the steps in the section below.

Build the SDK manually with the example project

Follow these steps in order:

  1. Download all frameworks including UJETKit.xcframework and other dependencies.

  2. Create folder CCAI Platform on the project root and unzip all frameworks.

  3. Select Objc-Manual or Swift-Manual target and build.

Import framework

Objective-C Project

@import UJETKit;

Swift Project

```swiftimport
UJETimport UJETKit
```

Initialize SDK

Initialize CCAI Platform with UJET_COMPANY_KEY and UJET_SUBDOMAIN

In application:didFinishLaunchingWithOptions: method:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // Initialize CCAI Platform
    [CCAI Platform initialize:UJET_COMPANY_KEY subdomain:UJET_SUBDOMAIN delegate:self];

    // YOUR CODE

    return YES;
}

You can change log level from verbose to error. The default log level is UjetLogLevelInfo

[CCAI Platform setLogLevel:UjetLogLevelVerbose];

End-user authentication

Access the iOS SDK through the iOS app.

In order to make sure that the end user is authenticated, we are introducing the JWT signing mechanism.

The iOS SDK will ask to sign the payload when authentication is needed. If the signing is successful, the application exchanges the signed JWT to the end user auth token. The success or failure block must be called before the delegate returns.

For anonymous user (identifier = nil), the application will create a UUID for the user. If at a later time the user is authenticated with an identifier, the application will attempt to merge the two users based on the UUID.

In UJETObject.h from example project :

@import UJETKit;

@interface UJETObject : NSObject <UJETDelegate>

Implement signPayload: payloadType: success: failure: delegate method.

- (void)signPayload:(NSDictionary *)payload payloadType:(UjetPayloadType)payloadType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure {
  if (payloadType == UjetPayloadAuthToken) {
    [self signAuthTokenInLocal:payload success:success failure:failure];
  }
}

- (void)signAuthTokenInLocal:(NSDictionary *)payload success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure {
    NSMutableDictionary *payloadData = [payload mutableCopy];

    NSDictionary *userData = [[NSUserDefaults standardUserDefaults] objectForKey:@"user-data"];
    [payloadData addEntriesFromDictionary:userData];
    payloadData[@"iat"] = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; // required
    payloadData[@"exp"] = [NSNumber numberWithDouble:([[NSDate date] timeIntervalSince1970] + 600)]; // required

    NSString *signedToken = [self encodeJWT:payloadData];

    if (signedToken.length > 0) {
        success(signedToken);

    } else {
        NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Failed to sign token" };
        NSError *error = [NSError errorWithDomain:@"ExampleApp" code:0 userInfo:userInfo];
        failure(error);
    }
}

- (NSString *)encodeJWT:(NSDictionary *)payload {
    id<JWTAlgorithm> algorithm = [JWTAlgorithmHSBase algorithm384];
    NSString *secret = NSBundle.mainBundle.infoDictionary[@"UJETCompanySecret"];
    return [JWTBuilder encodePayload:payload].secret().algorithm(algorithm).encode;
}

We strongly recommend to sign the payload from your application server, not in the client.

This example uses local signing for testing purpose. See signDataInRemote: success: failure: in UJETObject.m file.

Setup Push Notifications

The application sends push notifications to request Smart Actions like verification and photo, as well as reporting an incoming call. The application requires two different type of certificate (VoIP and APNs) to be saved in the Admin Portal.

Prepare VoIP Services Certificate

This document is a good reference about Apple's VoIP push notification.

  1. Create and download the VoIP certificate from the Apple developer site.

  2. Double-click the certificate to add it to Keychain

  3. Start the Keychain Access application on your Mac

  4. Pick the My Certificates category in the left hand sidebar

  5. Right-click VoIP Services: your.app.id certificate

  6. In the popup menu choose Export...

  7. Save it as cert.p12 without protecting it with a password by leaving the password blank

  8. Run the following command in terminal

    openssl s_client -connect gateway.push.apple.com:2195 -cert cert.pem -debug -showcert
    
  9. The upper part of cert.pem is the certificate and the lower part is the private key

  10. Check that your certificate is working with Apple's push notification server

    openssl s_client -connect gateway.push.apple.com:2195 -cert cert.pem -debug -showcerts
    

    When successful, it should return:

    ---
    New, TLSv1/SSLv3, Cipher is AES256-SHA
    Server public key is 2048 bit
    Secure Renegotiation IS supported
    Compression: NONE
    Expansion: NONE
    SSL-Session:
        Protocol  : TLSv1
        Cipher    : AES256-SHA
        Session-ID:
        Session-ID-ctx:
        Master-Key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        Key-Arg   : None
        Start Time: 1475785489
        Timeout   : 300 (sec)
        Verify return code: 0 (ok)
    ---
    
  11. Sign in to the Admin Portal with admin credentials, and go to 'Settings > Developer Settings > Mobile App'

  12. Fill the certificate in the 'VoIP Services Certificate' section, and save. Be sure to contain boundaries (-----BEGIN ...----- and -----END ...-----) for both certificate and private key

  13. Check the Sandbox checkbox if you are running an app with a development provisioning profile such as debugging in Xcode. If your app is archived for Ad hoc or App store and is using a distribution provisioning profile, then leave the Sandbox checkbox unchecked.

Prepare Apple Push Notification service SSL

Almost as above but uses Apple Push Notification service SSL (Sandbox & Production) certificate. You can refer this document to create the certificate.

Integrating Push Notification

In AppDelegate.m :

@import PushKit;

@interface AppDelegate() <PKPushRegistryDelegate>
In application:didFinishLaunchingWithOptions: method:
// Initialize CCAI Platform
[CCAI Platform initialize:UJET_COMPANY_KEY subdomain:UJET_SUBDOMAIN delegate:self];

//  Register for VoIP notifications on launch.
PKPushRegistry *voipRegistry = [[PKPushRegistry alloc] initWithQueue: dispatch_get_main_queue()];
voipRegistry.delegate = self;
voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];

Add the following delegate methods in implementing UIApplicationDelegate protocol file:

Please print your device token to test push notifications.

// PKPushRegistryDelegate

- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
  [CCAI Platform updatePushToken:credentials.token type:UjetPushTypeVoIP];
}

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
  if (payload.dictionaryPayload[@"ujet"]) {
    [CCAI Platform receivedNotification:payload.dictionaryPayload completion:completion];
  } else {
    completion();
  }
}

// UIApplicationDelegate

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  [CCAI Platform updatePushToken:deviceToken type:UjetPushTypeAPN];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  if (userInfo[@"ujet"]) {
    [CCAI Platform receivedNotification:userInfo completion:nil];
  }
}

// UserNotificationsDelegate overrides [UIApplicationDelegate didReceiveRemoteNotification:]

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    NSDictionary *userInfo = notification.request.content.userInfo;

   if (userInfo[@"ujet"] != nil) {
       [CCAI Platform receivedNotification:userInfo completion:nil];
   }
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;

    if (userInfo[@"ujet"] != nil) {
        [CCAI Platform receivedNotification:userInfo completion:nil];
    }
}

Enable Push Notifications Capability

  1. Select your target and open Capabilities tab

  2. Turn on the switch of Push Notifications

Test Push Notifications

Push notification debug section

In the admin portal, navigate to Settings > Developer Settings. On this page, find the section titled "Push Notification Debug":

Copy and paste the device token in the right text area and select the right Mobile App. Note: Simulators doesn't support push notification tests.

Get the device token

An example device token string looks like this:

7db0bc0044c8a203ed87cdab86a597a2c43bf16d82dae70e8d560e88253364b7

Push notifications are usually set in the class which conforms to UIApplicationDelegate or PKPushRegistryDelegate protocol. At some point, the device token is available to you. You can print it out before passing it to the iOS SDK. To get your device token, use the code snippet below.

Swift
func tokenFromData(data: Data) -> String {
  return data.map { String(format: "%02x", $0) }.joined()
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  print("apns token: ", tokenFromData(data: deviceToken))
  ...
}

func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
  print("voip token: ", tokenFromData(data: credentials.token))
  ...
}
Obj-C
- (NSString *)tokenFromData:(NSData *)data {
  const char *d = data.bytes;
  NSMutableString *token = [NSMutableString string];

  for (NSUInteger i = 0; i < data.length; i++) {
    [token appendFormat:@"%02.2hhX", d[i]];
  }

  return [[token copy] lowercaseString];
}

- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
  NSLog(@"voip token: %@", [self tokenFromData:credentials.token]);
  ...
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  NSLog(@"apns token: %@", [self tokenFromData:deviceToken]);
}
Result

Once you have entered the certificate PEM file and the device token, click the button.

The result will show a message Push notification successfully configured if the test push notification was successfully delivered.

Push notification is not 100% guaranteed to be delivered, depending on the device's network connection.

Project Configurations

Project Configuration

Capabilities

In target settings, turn on below capabilities:

  • Push Notifications

  • Background Modes (check below items)

  • Audio and AirPlay

  • Voice over IP

Info.plist

To protect user privacy, any iOS app linked on or after iOS 10.0 which accesses any of the device's microphones, photo library, and camera, must declare the intent to do so. Include the keys below with a string value in your app's Info.plist file and provide a purpose string for this key. If your app attempts to access any of the device's microphones, photo library and camera without a corresponding purpose string, the app exits.

  • NSMicrophoneUsageDescription: Allows access to the microphone for calling and talking to support/troubleshooting teams, and for sending videos with sound related to product inquiries.

  • NSCameraUsageDescription: Allows access to the camera for customer to take and send photos related to their customer support inquiry

  • NSPhotoLibraryUsageDescription: Allows access for customer to send photos related to their customer support inquiry

  • NSFaceIDUsageDescription: Allows access to verification using Face ID

Start the iOS SDK

Add the following line where you want to start the iOS SDK:

[CCAI Platform startWithOptions:nil];

You can also start the iOS SDK from a specific point in the menu with this key using a Direct Access Point:

UJETStartOptions *option = [[UJETStartOptions alloc] initWithMenuKey:@"MENU_KEY"];

[CCAI Platform startWithOptions:option];

The menuKey can be created with a Direct Access Point in the Admin Portal using admin credentials > Settings > Queue > Select any queue from the menu structure > Check Create direct access point > Enter key in the text form > Save

Clear cache from local if user data has been updated

We're caching auth token in the Keychain to re-use and make less frequent requests to sign payload from the host app. The SDK will use it until expired or revoked through clearUserData call. The host app is in charge of revoking this cache whenever user related data has changed or updated such as a sign out event.

[CCAI Platform clearUserData];

Check for existing session before starting UJET

Before starting a session, check to see if there isn't a current session. This is especially important when the userId has changed.

[CCAI Platform getStatus];

If there is an existing session, we should prompt the user to resume the session or cancel the action:

if ([CCAI Platform getStatus] != UjetStatusNone) {
  // Display alert to cancel login or resume existing session
}

Customization

There are several options for the SDK theme listed in UJETGlobalTheme.h.

Set your theme after [CCAI Platform initialize] for example,

UJETGlobalTheme *theme = [UJETGlobalTheme new];

theme.font = [UIFont fontWithName:@"OpenSans" size: 16.0f];

theme.lightFont = [UIFont fontWithName:@"OpenSans-Light" size: 16.0f];

theme.boldFont = [UIFont fontWithName:@"OpenSans-Bold" size: 16.0f];

theme.tintColor = [UIColor colorWithRed:0.243 green:0.663 blue:0.965 alpha:1.00];

[CCAI Platform setGlobalTheme:theme];

The company name is retrieved from Admin Portal > Settings > Support Center Details > Display Name.

You can set the logo image instead of the company name like this:

theme.companyImage = [UIImage imageNamed:@"logo"];

The image will be resized to fit on the area if it is too large.

Strings

You can also customize strings by overriding the value. For example, put this key/value on your Localizable.strings:

"ujet_greeting_title" = "Title";

"ujet_greeting_description" = "Description";

Available customizable strings are listed in ujet.strings file.

Specify dark mode for font legibility

You can specify a tint of the color you want for dark mode to better legibility of fonts.

@property (nonatomic, strong) UIColor *tintColorForDarkMode;

If you don't set the property then UJETGlobalTheme.tintColor will be used for dark mode. We recommend setting this property if your app supports dark mode. Please read this Apple article on how to pick the right tint color for dark mode.

Other appearances

You can customize other appearances such as font size and background color

theme.supportTitleLabelFontSize = 30;
theme.supportDescriptionLabelFontSize = 20;
theme.supportPickerViewFontSize = 30;
theme.staticFontSizeInSupportPickerView = YES;

theme.backgroundColor = UIColor.darkGrayColor;
theme.backgroundColorForDarkMode = UIColor.lightGrayColor;

CallKit

On iOS 10.0 and greater, CallKit is enabled for all calls.

With CallKit, it shows an in-app call coming in with the call screen and shows the call in the phone's call history.

To start a new CCAI Platform support session from call history, add the following block to your AppDelegate.m:

AppDelegate.m:
- (BOOL)application:(UIApplication *)app continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler {
    if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]) {
        // Open app from Call history
        [CCAI Platform startWithOptions:nil];
    }

    return YES;
}

CallKit allows a 40x40 icon to be displayed on the lock screen when receiving a call while the device is locked. Place an image your xcassets named 'icon-call-kit'.

Configure SDK

You can set several options before starting the SDK.

Please take a look at the UJETGlobalOptions class for details.

UJETGlobalOptions *options = [UJETGlobalOptions new];

options.fallbackPhoneNumber = @"+18001112222";

options.preferredLanguage = @"en";

[CCAI Platform setGlobalOptions:options];

PSTN Fallback

We provide PSTN fallback for several situations:

  • Mobile network is offline.

  • The application backend is not reachable.

  • VoIP is not available

    • The network condition is not good enough to connect. See UJETGlobalOptions.pstnFallbackSensitivity property for details.

    • A failure has occurred during connection due to firewall configuration or provider issue.

We recommend setting your company IVR number in UJETGlobalOptions.fallbackPhoneNumber. The recommended format is + followed by country code and phone number. eg. +18001112222

PSTN Fallback Sensitivity

You can adjust the sensitivity level of checking network condition to PSTN fallback.

@property (nonatomic, assign) float pstnFallbackSensitivity;

The value must be in the range of 0.0 to 1.0. If set to 1, the call will always connect via PSTN rather than VoIP. The maximum latency and the minimum bandwidth threshold are 10000ms and 10KB/s respectively for the value of 0. For example, a value of 0.5 means a minimum latency and bandwidth is 5000ms and 15KB/s, respectively. This value can be configured in the Admin Portal > Settings > Developer Settings > Mobile Apps > Fallback phone number threshold. The default value is 0.85.

Ignore dark mode

You can ignore the dark mode in CCAI Platform SDK specifically with this property

@property (nonatomic, assign) BOOL ignoreDarkMode;

If you've already set UIUserInterfaceStyle as Light on your app's Info.plist to opt out of dark mode entirely then you can ignore this property.

Preferred language

The CCAI Platform SDK will use the following priority order to determine the preferred language.

  1. Language selected from the splash screen within the app.

  2. Default language selected from UJETGlobalOptions. You can set the default language with preferredLanguage property. The supported language codes can be found in the UJETGlobalOptions.h file.

  3. Device language selected in the device (from Settings > General > Language & Region) will be used, when it is supported by the app.

  4. Closest dialect of device language will be used when the application does not support the device language but supports its closest parent dialect. For example, if the user selected Spanish Cuba as the language in the device and the app does not support Spanish Cuba but supports parent dialect Spanish, then Spanish language will be used.

  5. English will be used if the device language is not supported by the app.

Configure external deflection link icons

You can customize the icon in the external deflection link channel by uploading icon into asset catalog of your app and ensure to use the same icon name while creating external deflection link in Settings > Chat > External Deflection Links > View links > Add Deflection Link in the Admin Portal. If the icon name in the Admin Portal does not match with the icon uploaded into the app then the SDK will use the default icon. You can refer to this link on how to add images on asset catalog.

Send custom data to CRM

You can send custom data to the CRM ticket.

There are two methods to send custom data:

  1. Secure method: predefined data signing with JWT

  2. Non-secure method: predefined data with plain JSON (Not recommended)

Using the secure method to send custom data

You have to implement signing method. First, you can put your custom data on client side, and send to your server to sign it. On your server you can add additional data by defined form and sign with your company.secret and return it by JWT.

- (void)signPayload:(NSDictionary *)payload payloadType:(UjetPayloadType)payloadType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
{
    if (payloadType == UjetPayloadCustomData) {
      // sign custom data using UJET_COMPANY_SECRET on your server.

      NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
      NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];

      NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] init];
      mutableRequest.URL = [NSURL URLWithString:@"https://your.company.com/api/ujet/sign/custom_data"];
      mutableRequest.HTTPMethod = @"POST";
      NSError *error;

      // Make client's custom data
      UJETCustomData *customData = [[UJETCustomData alloc] init];
      [customData set:@"name" label:@"Name" stringValue:@"USER_NAME"];
      [customData set:@"os_version" label:@"OS Version" stringValue:[[UIDevice currentDevice] systemVersion]];
      [customData set:@"model" label:@"Model number" numberValue:[NSNumber numberWithInteger:1234]];
      [customData set:@"temperature" label:@"Temperature" numberValue:[NSNumber numberWithFloat:70.5]];
      [customData set:@"purchase_date" label:@"Purchase Date" dateValue:[NSDate date]];
      [customData set:@"dashboard_url" label:@"Dashboard" urlValue:[NSURL URLWithString:@"http://internal.dashboard.com/1234"]];

      NSDictionary *data = @{@"custom_data": [customData getData]};
      mutableRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
      NSURLSessionDataTask *task = [session dataTaskWithRequest:mutableRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
          if(error) {
              failure(error);
          }
          else {
              NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
              success(json[@"jwt"]);
          }
      }];

      [task resume];
    }
}

Using insecure method to send custom data

This method is not recommended as it creates a potential vulnerability which could open your application to a man-in-the-middle attack. If you choose to use this method, we are not responsible for the security exposure and potential damage which may occur. We encourage you to use the secure method described above to send custom data in your application. Or you can just start the iOS SDK with UJETCustomData instance. In this case, signPayload delegate for UJETPayloadCustomData should just call success(nil);

- (void)signPayload:(NSDictionary *)payload payloadType:(UjetPayloadType)payloadType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure {
    if (payloadType == UjetPayloadCustomData) {
      success(nil);
    }
}
UJETStartOptions *options = [UJETStartOptions new];
options.unsignedCustomData = customData;

[CCAI Platform startWithOptions:options];

Using unsigned custom data to send external chat transcript

You can send the chat transcript to the SDK when it's started with unsigned custom data by calling setExternalChatTransfer: or setExternalChatTransferWithDictionary: method to set JSON data with NSString or NSDictionary, respectively.

UJETCustomData *customData = [UJETCustomData new];
[customData setExternalChatTransfer:jsonString];

UJETStartOptions *options = [UJETStartOptions new];
options.unsignedCustomData = customData;

[CCAI Platform startWithOptions:options];

JSON Format:

  • greeting_override: string

  • agent: dictionary

    • name: string

    • avatar: string [url of agent avatar, optional]

  • transcript: array

    • sender: string ["end_user" or "agent"]

    • timestamp: string [ie "2021-03-15 12:00:00Z"]

    • content: array

      • type: string [one of text, media]

      • text: string [required for text type]

      • media: dictionary [required for media type]

        • type: string [one of image, video]

        • url: string [public url pointing at media file]

    { "greeting_override": "Please hold while we connect you with a human agent.", "agent": { "name": "Name", "avatar": "avatar url" }, "transcript": [ { "sender": "agent", "timestamp": "2021-03-15 12:00:15Z", "content": [ { "type": "text", "text": "Suggestions shown:\n\n* Help with batch or delivery\n* Help with metrics or order feedback\n* Help with Instant Cashout" } ] }, { "sender": "end_user", "timestamp": "2021-03-15 12:00:16Z", "content": [ { "type": "text", "text": "Help with batch or delivery" } ] } ] }

You can use markdown on the text type. Supported syntax are italic, bold, bullet list, hyperlink, and underline(--text--).

Example of custom data

JSON encoded to JWT

The JSON should include iat and exp to validate JWT. And the object of custom data is value of custom_data key.

{
  "iat" : 1537399656,
  "exp" : 1537400256,
  "custom_data" : {
    "location" : {
      "label" : "Location",
      "value" : "1000 Stockton St, San Francisco, CA, United States",
      "type" : "string"
    },
    "dashboard_url" : {
      "label" : "Dashboard URL",
      "value" : "http:\/\/(company_name)\/dashboard\/device_user_ID",
      "type" : "url"
    },
    "contact_date" : {
      "label" : "Contact Date",
      "value" : 1537399655992,
      "type" : "date"
    },
    "membership_number" : {
      "label" : "Membership Number",
      "value" : 62303,
      "type" : "number"
    },
    "model" : {
      "label" : "Model",
      "value" : "iPhone",
      "type" : "string"
    },
    "os_version" : {
      "label" : "OS Version",
      "value" : "12.0",
      "type" : "string"
    },
    "last_transaction_id" : {
      "label" : "Last Transaction ID",
      "value" : "243324DE-01A1-4F71-BABC-3572B77AC487",
      "type" : "string"
    },
    "battery" : {
      "label" : "Battery",
      "value" : "-100%",
      "type" : "string"
    },
    "bluetooth" : {
      "label" : "Bluetooth",
      "value" : "Bluetooth not supported",
      "type" : "string"
    },
    "wifi" : {
      "label" : "Wi-Fi",
      "value" : "Wi-Fi not connected",
      "type" : "string"
    }
  }
}

Each data is similar to JSON object format and should contain the key, value, type, and label.

key is unique identifier for the data. label is display name on CRM's page type is type of the value.

  • string

    • JSON string
  • number

    • integer, float
  • date

    • UTC Unix timestamp format with 13 digits. (contains milliseconds)
  • url

    • HTTP url format
CRM example

Location

Use CoreLocation framework. For more details please refer AppDelegate.m

Device OS Version
[customData set:@"os_version" label:@"OS Version" stringValue:[[UIDevice currentDevice] systemVersion]];

Customize Flow

Disconnect CCAI Platform for handling Host app events

// CCAI Platform is connected
...
// An event has come
[CCAI Platform disconnect:^{
  // Handle an event
}];

Postpone CCAI Platform incoming call or chat

Implement a delegate method for handling incoming events
- (BOOL)shouldConnectUjetIncoming:(NSString *)identifier forType:(UjetIncomingType)type {
  if (weDoingSomething) {
    // save identifier and type
    return NO; // postpone
  } else {
    return YES;
  }
}
Connect postponed event
[CCAI Platform connect:identifier forType:UjetIncomingTypeCall];

Setup Deep link

This enables agents on PSTN calls to use smart actions by SMS for both when an end user has or does not have the app.

Go to Settings > Operation Management > Enable Send SMS to Download App in the CCAI Platform Admin Portal

You can set App URL with your web page (i.e, https://your-company.com/support) after configuring Universal Link or custom URL scheme. You can select either way.

Implement delegate method to handle deep link

The universal link and custom URL look like https://your-company.com/support?call_id=xxx&nonce=yyy and your-company://support?call_id=xxx&nonce=yyy respectively. Put one of your links without query parameters under App URL in Admin Portal. For example, put your-company://support if using a custom URL scheme. In the delegate method, make sure to only call [CCAI Platform start] when the URL paths and parameters in the universal link or custom URL are specific for CCAI Platform.

- (BOOL)application:(UIApplication *)app continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler {
  ...
  if ([NSUserActivityTypeBrowsingWeb isEqualToString:userActivity.activityType]) {
    NSURL *url = userActivity.webpageURL;

    NSArray *availableSchema = @[
                                 @"your-company",   // custom URL scheme
                                 @"https"           // universal link
                                 ];

    NSArray *availableHostAndPath = @[
                                      @"ujet",                  // custom URL scheme
                                      @"your-comany.com/ujet"   // universal link
                                      ];

    if (![availableSchema containsObject:url.scheme]) {
      return NO;
    }

    NSString *hostAndPath = [NSString stringWithFormat:@"%@%@", url.host, url.path];
    if (![availableHostAndPath containsObject:hostAndPath]) {
      return NO;
    }

    // your-company://ujet?call_id={call_id}&nonce={nonce}
    // https://your-company.com/ujet?call_id={call_id}&nonce={nonce}
    NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
                                                resolvingAgainstBaseURL:NO];
    NSArray *queryItems = urlComponents.queryItems;
    NSString *callId = [self valueForKey:@"call_id" fromQueryItems:queryItems];
    // validate call id
    if (![self isValidCallId:callId]) {
      return NO;
    }
    NSString *nonce = [self valueForKey:@"nonce" fromQueryItems:queryItems];

    UJETStartOptions *options = [[UJETStartOptions alloc] initWithCallId:callId nonce:nonce];

    [CCAI Platform startWithOptions:options];
  }
  ...
}

Please see the example codes from UJETObject+DeepLink file.

Observe CCAI Platform event

We post the following events through NSNotificationCenter.defaultCenter. You can listen to them and customize your flow depending on your use case, e.g., custom keyboard layout.

  • UJETEventEmailDidClick

    • Queue Menu Data
  • UJETEventEmailDidSubmit

    • Queue Menu Data

    • has_attachment: (NSNumber) @YES, @NO

  • UJETEventSessionViewDidAppear

    • type: @"call", @"chat"

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionViewDidDisappear

    • type: @"call", @"chat"

    • timestamp: (NSString) ISO 8601

  • UJETEventSessionDidCreate

    • Session Data
  • UJETEventSessionDidEnd

    • Session Data

    • agent_name: (NSString) null if agent didn't join

    • duration: (NSNumber) only for call

    • ended_by: (NSString)

      • type=call: @"agent", @"end_user"

      • type=chat: @"agent", @"end_user", @"timeout", @"dismissed"

  • UJETEventSdkDidTerminate

Event Data

Meta Data
  • application: @"iOS"

  • app_id: (NSString) bundle identifier

  • app_version: (NSString)

  • company: (NSString) subdomain

  • device_model: (NSString)

  • device_version: (NSString)

  • sdk_version: (NSString)

  • timestamp: (NSString) ISO 8601

Queue Menu Data
  • Meta Data

  • menu_id: NSString

  • menu_key: NSString, nullable

  • menu_name: NSString

  • menu_path : NSString

Session Data
  • Queue Menu Data

  • session_id: NSString

  • type: @"call", @"chat"

  • end_user_identifier: NSString

UI and string customization

There are several options for the SDK theme listed in UJETGlobalTheme.h`.

Set your theme after `[UJET initialize] for example:

UJETGlobalTheme *theme = [UJETGlobalTheme new];

theme.font = [UIFont fontWithName:@"OpenSans" size: 16.0f];

theme.lightFont = [UIFont fontWithName:@"OpenSans-Light" size: 16.0f];

theme.boldFont = [UIFont fontWithName:@"OpenSans-Bold" size: 16.0f];

theme.tintColor = [UIColor colorWithRed:0.243 green:0.663 blue:0.965 alpha:1.00];

[UJET setGlobalTheme:theme];

The company name is retrieved from Admin Portal > Settings > Support Center Details > Display Name.

You can also set the logo image instead of the company name. The image will be resized to fit on the area if it is too large:

theme.companyImage = [UIImage imageNamed:@"logo"];

Strings

You can also customize strings by overriding the value. For example, put this key/value on your Localizable.strings: ``` ujet_greeting_title = "Title";

"ujet_greeting_description" = "Description"; ```

Available customizable strings are listed in ujet.strings file.

Specify dark mode for font legibility

You can specify a tint of the color you want for dark mode to make fonts more legible.

@property (nonatomic, strong) UIColor *tintColorForDarkMode;

If you don't set the property, UJETGlobalTheme.tintColor` will be used for dark mode. We recommend setting this property if your app supports dark mode. The following external documentation provides insights on how to pick the right tint color for dark mode:

You can control the visibility of the status bar with this property:

@property (nonatomic, assign) BOOL hideStatusBar;

Chat theme

To customize the chat screen, you have the option to use a JSON string or each theme class.

For reference, you can check out the example app and uncomment the customizeChatTheme method.

    guard let file = Bundle.main.path(forResource: "chat-theme-custom", ofType: "json") else { return }
    let json = try String.init(contentsOfFile: file, encoding: .utf8)

    let chatTheme = UJETChatTheme.init(jsonString: json)

    let quickReplyTheme = UJETChatQuickReplyButtonTheme()
    quickReplyTheme.style = .individual
    quickReplyTheme.alignment = .right
    quickReplyTheme.backgroundColor = UJETColorRef(assetName: "white_color")
    quickReplyTheme.backgroundColorForHighlightedState = UJETColorRef(assetName: "quick_reply_color")
    quickReplyTheme.textColor = UJETColorRef(assetName: "quick_reply_color")
    quickReplyTheme.textColorForHighlightedState = UJETColorRef(assetName: "white_color")

    let fontTheme = UJETFontTheme()
    fontTheme.family = "Arial Rounded MT Bold"
    fontTheme.size = 14
    quickReplyTheme.font = fontTheme

    chatTheme?.quickReplyButtonTheme = quickReplyTheme

    let globalTheme = UJETGlobalTheme()
    globalTheme.chatTheme = chatTheme
    globalTheme.defaultAgentImage = UIImage(named: "agent_avatar_image")
    globalTheme.font = UIFont(name: "Arial Rounded MT Bold", size: 14)

    UJET.setGlobalTheme(globalTheme)
}

Other appearances

You can customize other appearances such as font size and background color:

theme.supportDescriptionLabelFontSize = 20;
theme.supportPickerViewFontSize = 30;
theme.staticFontSizeInSupportPickerView = YES;

theme.backgroundColor = UIColor.darkGrayColor;
theme.backgroundColorForDarkMode = UIColor.lightGrayColor;

Content cards

You can add customization for content cards together with chat customization. You can do this either using the json file (see content_card property) or by using the UJETChatContentCardTheme class.

func customizeChatTheme() throws {
    guard let file = Bundle.main.path(forResource: "chat-theme-custom", ofType: "json") else { return }
    let json = try String.init(contentsOfFile: file, encoding: .utf8)

    let chatTheme = UJETChatTheme.init(jsonString: json)

    let contentCardTheme = UJETChatContentCardTheme()
    contentCardTheme.backgroundColor = UJETColorRef(assetName: "agent_message_background_color")
    contentCardTheme.cornerRadius = 16

    let contentCardFontTheme = UJETFontTheme()
    contentCardFontTheme.family = "Arial Rounded MT Bold"
    contentCardFontTheme.size = 18
    contentCardTheme.font = contentCardFontTheme
  
    let contentCardBorder = UJETBorderTheme()
    contentCardBorder.width =  1
    contentCardBorder.color = UJETColorRef(assetName: "agent_message_border_color")
    contentCardTheme.border = contentCardBorder

    let contentCardFontTheme = UJETFontTheme()
    contentCardFontTheme.family = "Arial Rounded MT Bold"
    contentCardFontTheme.size = 18
    contentCardTheme.font = contentCardFontTheme

    // The font family is inherited from the contentCardFontTheme
    let subtitle = UJETFontTheme()
    subtitle.size = 12
    contentCardTheme.subtitle = subtitle

    // The font family is inherited from the contentCardFontTheme
    let bodyFont = UJETFontTheme()
    bodyFont.size = 10
    contentCardTheme.body = bodyFont

    theme.chatTheme?.contentCard = contentCardTheme

    let globalTheme = UJETGlobalTheme()
    globalTheme.chatTheme = chatTheme
    globalTheme.defaultAgentImage = UIImage(named: "agent_avatar_image")
    globalTheme.font = UIFont(name: "Arial Rounded MT Bold", size: 14)

    UJET.setGlobalTheme(globalTheme)
}

Setup Co-browse

If you want to use Co-browse feature, then integrate UJETCobrowseKit.xcframework.

CocoaPods: Add the following subspec to your app target

       ruby
    target 'MyApp' do
      pod 'UJET'
      pod 'UJET/Cobrowse'
    end

Carthage

Add the following line on the Cartfile:

binary "https://sdk.ujet.co/ios/UJETKit.json"

SwiftPM: Select UJET and UJETCobrowse products and add to your app target

And set UJETGlobalOptions.cobrowseKey property.

swift
let options = UJETGlobal
Options()options.cobrowseKey = cobrowseKey

CCAI Platform.setGlobalOptions(options)

Full Device Screen Sharing (optional)

Full device screen sharing allows your support agents to view screens from applications outside of your own. This is often useful where support agents need to check the state of system settings, or need to see the user navigate between multiple applications. If you do not want this feature, you may skip this section.

Broadcast Extension

The feature requires adding a Broadcast Extension.

  1. Open your Xcode project

  2. Navigate to File > Target

  3. Pick Broadcast Upload Extension

  4. Enter a Name for the target

  5. Uncheck Include UI Extension

  6. Create the target, noting its bundle ID

  7. Change the target SDK of your Broadcast Extension to iOS 12.0 or higher

Integrate SDK

CocoaPods: Add the following subspec to your extenion target

       ruby
    target 'MyApp' do
      pod 'UJET'
      pod 'UJET/Cobrowse'
    end
    target 'MyAppExtension' do
      pod 'UJET/CobrowseExtension'
    end

SwiftPM: Select UJETCobrowseExtension product and add to your extension target

Set up Keychain Sharing

Your app and the app extension you created above need to share some secrets via the iOS Keychain. They do this using their own Keychain group so they are isolated from the rest of your apps Keychain.

In both your app target and your extension target add a Keychain Sharing entitlement for the io.cobrowse keychain group.

Add the bundle ID to your plist

Take the bundle ID of the extension you created above, and add the following entry in your apps Info.plist (Note: not in the extensions Info.plist), replacing the bundle ID below with your own:

xml
<key>CBIOBroadcastExtension</key>
<string>your.app.extension.bundle.ID.here</string>
Implement the extension

Xcode will have added SampleHandler.m and SampleHandler.h (or SampleHander.swift) files as part of the target you created earlier. Replace the content of the files with the following:

Swift

swift
import CobrowseIOAppExtension
class SampleHandler: CobrowseIOReplayKitExtension {
}

ObjC

objc// SampleHandler.h
@import CobrowseIOAppExtension;
@interface SampleHandler : CobrowseIOReplayKitExtension
@end// SampleHandler.m
#import "SampleHandler.h"
@implementation SampleHandler
@end
Build and run your app

You're now ready to build and run your app. The full device capability is only available on physical devices, it will not work in the iOS Simulator.

Troubleshooting

App Submission Rejection

App submission is rejected due to the inclusion of CallKit framework in China territory

If your app is rejected by Apple for this reason, then just leave a comment as the system is designed to deactivate the CallKit framework for China region on VoIP call. This is effective as of SDK version 0.31.1.

SDK size is too large

When SDK size is too large and hard to track on GitHub

In this article, they offer two choices. We recommend to use Git lfs.

If you are not using Bitcode then stripping bitcode from binary can be an another option. Run this command under UJETKit.xcframework folder.

xcrun bitcode_strip -r CCAI Platform -o CCAI Platform

dyld: Library not loaded error

Add @executable_path/Frameworks on the Runpath Search Paths from Target > Build Settings > Linking

App submission on iTunes Connect

Apple might ask the following question while in review process because of enabled Voice over IP background mode:

Can users receive VoIP calls in your app?

Respond Yes to the question.

Alert Notification is unavailable when starting SDK

Check the following:

  • Use real device not simulator

  • Enable Push notifications and Background Modes > Voice over IP capability

If those things do not help, then try to build with distribution provisioning profile (Ad-hoc or Apple Store)

Test push notification against your test app

Prepare your VoIP certificate and device's device token

On your Admin Portal, there is push notification debug section in Settings > Developer Settings menu.

If you already set the certificate for APNS, you don't have to put your certificate again.

Input your certificate (optional) and check whether sandbox or not (optional) and input your test app's push notification device token.