iOS

The following section takes you through using the App Messaging Foundation iOS SDK which can be found in Github here:

Installing the SDK

compile 'com.comapi:foundation:1.3.0'
<dependency>
    <groupId>com.comapi</groupId>
    <artifactId>foundation</artifactId>
    <version>1.3.0</version>
</dependency>

Then

  • initialise SDK with API Space id and authentication token provider
  • start session when you obtain token from your authentication provider
  • listen for real time events, and push messages
  • call message and profile services

Initialise

To initialise the SDK, you will need a few pre-requisites ...

  1. Setup the Push / App Messaging configuration is Engagement Cloud, see the instructions here for more information on setting up App Messaging.

  2. Your API Space Id for your configuration, see the instructions here for more information

  3. An authentication provider that can generate a JWT that matches the authorisation scheme configured for your API Space.

The Auth Challenge function you pass when initializing the SDK needs to generate and return a JWT via the answerAuthenticationChallenge method.

There are two ways of creating a JWT for users, you can:

Use the Session API to create a JWT

This is a convenient way to create an anonymous user JWT, simply call the Session API to generate an anonymous JWT. You can pass a user id, which is unique per user or device, and a nonce to ensure the JWT is the one you requested.

Self Issued JWT

With this option you must create a valid JWT matching the settings you entered into the App Messaging configuration; you have total control of the user id and claims. There are plenty of code examples of how to create JWT online, and a lot of good information and code libraries can be found at the JWT home page.

The JWT

The following parameters are needed when generating a JWT for the SDK and they are:

  • Issuer
  • Audience
  • Shared Secret
  • ID Claim

A cryptographic nonce is used as part of the authentication flow. This is passed into the authChallenge (options.nonce) and needs to be added as a claim in the generated JWT.

These parameters can be defined and obtained from within the portal in the Push channel profile settings area. See the Push / App Messaging setup pages for more information.

The code snippet shows how to initialize the SDK passing your authenticationDelegate:

// create a config object with your api-space-id and an object conforming to CMPAuthenticationDelegate protocol;
CMPComapiConfig *config = [[[[CMPComapiConfig builder] 
                                    setApiSpaceID:@"<API_SPACE_ID>"]
                                    setAuthDelegate:<CMPAuthenticationDelegate_Conforming_Object>]                                      
                    <CMPAuthenticationDelegate_Conforming_Object>build];


CMPComapiClient *client = [CMPComapi initialiseWithConfig:config];
// we can use the client object now
// create a config object with your api-space-id and an object conforming to AuthenticationDelegate protocol;
let config = ComapiConfig.builder()
            .setAuthDelegate(<CMPAuthenticationDelegate_Conforming_Object>)
            .setApiSpaceID("API_SPACE_ID")
            .build()

client = Comapi.initialise(with: config)
guard let client = Comapi.initialise(with: config) else { 
    // initializing error
    return
}
// we can use the client object now

In order for the client to be able to start a session, the config's authenticationDelegate object must conform to the protocol's method:

NSString *id = <Portal's authentication tab ID claim value>;
NSString *issuer = <Portal's authentication tab issuer value>;
NSString *audience = <Portal's authentication tab audience value>;
NSString *secret = <Portal's authentication tab secret value>;
  
- (void)client:(CMPComapiClient *)client didReceiveAuthenticationChallenge:(CMPAuthenticationChallenge *)challenge completion:(void (^)(NSString * _Nullable))continueWithToken {
    // request a JWT token from your provider (backend server)
    // example call
    [YourProviderServer getTokenForNonce:challenge.nonce id:id issuer:issuer audience:audience secret:secret completion:^(NSString * token, NSError * error) {
            // call continueWithToken block with generated token
        if (token && !error) {
            continueWithToken(token);
        }
    }];
}
let id: String = <Portal's authentication tab ID claim value>
let issuer: String = <Portal's authentication tab issuer value>
let audience: String = <Portal's authentication tab audience value>
let secret: String = <Portal's authentication tab secret value>

func client(_ client: ComapiClient, didReceive challenge:               AuthenticationChallenge, completion continueWithToken: @escaping (String?) -> Void) {
        // request a JWT token from your provider (backend server)
    // example call
    YourProviderServer.getToken(nonce: challenge.nonce, id: id, issuer: issuer,          audience: audience, completion: { token: String?, error: Error? in
            // call continueWithToken block with generated token
        if (token != nil && error == nil) {
            continueWithToken(token!);
        }
    })
}

The JWT token needs to include claims you configured in the push channel configuration in Engagement Cloud, see the instructions here for more information on setting up App Messaging.

Here's an example implementation of a token generator in Objective-C and Swift using JWT

#import "CMPAuthenticationManager.h"
#import <JWT/JWT.h>

@implementation CMPAuthenticationManager

+ (NSString *)generateTokenForNonce:(NSString *)nonce profileID:(NSString *)profileID issuer:(NSString *)issuer audience:(NSString *)audience secret:(NSString *)secret {
    NSDate *now = [NSDate date];
    NSDate *exp = [NSCalendar.currentCalendar dateByAddingUnit:NSCalendarUnitDay value:30 toDate:now options:0];
    
    NSDictionary *headers = @{@"typ" : @"JWT"};
    NSDictionary *payload = @{@"nonce" : nonce,
                               @"sub" : profileID,
                               @"iss" : issuer,
                               @"aud" : audience,
                               @"iat" : [NSNumber numberWithDouble:now.timeIntervalSince1970],
                               @"exp" : [NSNumber numberWithDouble:exp.timeIntervalSince1970]};
    
    NSData *secretData = [secret dataUsingEncoding:NSUTF8StringEncoding];
    id<JWTAlgorithm> algorithm = [JWTAlgorithmFactory algorithmByName:@"HS256"];
    
    NSString *token = [JWTBuilder encodePayload:payload].headers(headers).secretData(secretData).algorithm(algorithm).encode;
    return token;
}

@end
  
/* Note that this should preferably be generated by your backend, the app should only retreive the token through an HTTP call */
import JWT

class JWTokenGenerator {
    
    struct AuthHeaders {
        static let HeaderType = "JWT"
    }
    
    static func generate(tokenFor nonce: String, profileId: String, issuer: String, audience: String, secret: String) -> String {
        let now = Date()
        let exp = Calendar.current.date(byAdding: .day, value: 30, to: now)!
        
        let base64SecretKey = secret.data(using: .utf8)!
        
        let headers = ["typ" : NSString.init(string: AuthHeaders.HeaderType)] as [AnyHashable : Any]
        
        let claims = ["nonce" : NSString.init(string: nonce),
                      "sub" : NSString.init(string: profileId),
                      "iss" : NSString.init(string: issuer),
                      "aud" : NSString.init(string: audience),
                      "iat" : NSNumber(value: now.timeIntervalSince1970),
                      "exp" : NSNumber(value: exp.timeIntervalSince1970)] as [AnyHashable : Any]
        
        let algorithm = JWTAlgorithmFactory.algorithm(byName: "HS256")
        
        let e = JWTBuilder.encodePayload(claims)!
        
        let h = e.headers(headers)!
        let s = h.secretData(base64SecretKey)!
        let b = s.algorithm(algorithm)!
        let token = b.encode

        return token!
    }
}

/* Note that this should preferably be generated by your backend, the app should only retreive the token through an HTTP call */

Retrieving the client

The client can be retrieved either as a separate object using:

CMPComapiClient *client = [CMPComapi initialiseWithConfig:config];
// client instance ready to use
guard let client = Comapi.initialise(with: config) else {
        // initialisation error
    return
}
// client instance ready to use

or as a singleton:

[CMPComapi initialiseSharedInstanceWithConfig:config];

CMPComapiClient *client = [Comapi shared];
// shared client ready to use
Comapi.initialiseSharedInstance(with: config)

guard let client = Comapi.shared else {
        // initialisation error
    return
}
// shared client ready to use

Start a session

Initialisation through RxComapi class gives you access to client object used for any further communication with the SDK. See initialisation for details.

In order to communicate with App Messaging services you need to start session in SDK. This is needed only once per user log in, after that SDK will be re-authenticating session until you end session.

Start

To create or log in user to App Messaging services use:

client.service().session().startSession(new Callback<Session>(){/* implement */});
rxClient.service().session().startSession()
   .subscribe(new Observer<Session>(){/* implement */});

This will ask ComapiAuthenticator (provided when initialising) for a JWT token then SDK will create session server side for profile id obtained from the token. Any subsequent call to services will use same authentication details.

Stop

To close currently opened session use

client.service().session().endSession(
   new Callback<ComapiResult<Void>>(){/* implement */});
rxClient.service().session().endSession()
   .subscribe(new Observer<ComapiResult<Void>>(){/* implement */});

Push messages

In order to use SDK built-in support for the Push Notifications, you need to configure your app to support Apple's Push Notifications, as you would normally do, and then you need to pass the deviceToken which your application has registered with a call to setPushToken in the SDK e.g:

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSMutableString *token = [NSMutableString string];
    
    const char *data = [deviceToken bytes];
    for (NSUInteger i = 0; i < [deviceToken length]; i++) {
        [token appendFormat:@"%.2hhx", data[i]];
    }
  
    if (token) {
        [client setPushToken:token completion:^(CMPResult<NSNumber *> * result) {
            BOOL success = [result.object boolValue];
            if (result.error || !success) {
                // error occurred
            } else {
                // configuration successful
            }
        }];
    }
    
    // rest of you push notification code
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
    client.set(pushToken: token, completion: { (success, error) in
        if error != nil || !success {
                        // error
        } else {
                        // successfully configured push notifications
        }
    })
    
    // rest of you push notification code
}

Displaying push notifications when the app is in the foreground

Push notifications are displayed only while the app is in the background. These notification are sent to the notification center and launch your app when users tap them.

If you want to display the push notification message while the app is in the foreground or get access to the push notification to process the data, such as deep links, do one of the following, depending on the iOS version that your app is running on:

For iOS 9

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    if (application.applicationState == UIApplicationStateActive) {
        
    }
}

For iOS 10 and above

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    
    completionHandler();
}

Listen for events

WebSocket

Whenever ComapiClient is configured and authenticated, a new session is started which opens a WebSocket connection. This allows for a two-way real-time communication between the client and the server. For more information on WebSocket visit the official RFC site. When open, the socket provides live updates for the profile and conversations which you are a participant of. You can subscribe for these updates to update your views and models.

Add event listener

Register the delegate for the incoming events with the CMPComapiClient object. The delegate should conform to the CMPEventListener protocol.

[client addEventDelegate:self];
client.add(self)

To receive the event implement following method from CMPEventListener protocol:

- (void)client:(CMPComapiClient *)client didReceiveEvent:(CMPEvent *)event {
    switch (event.type) {
        case CMPEventTypeConversationMessageSent:
                // message sent event received
            break;
        case CMPEventTypeConversationParticipantTypingOff:
                // participant typing off event received
            break;
        case CMPEventTypeConversationParticipantTyping:
                // participant typing event received
            break;
        case CMPEventTypeConversationMessageRead:
                // message read event received
            break;
        // case .other events:
        default:
                // unknown event type
            break;
    }
}
func client(_ client: ComapiClient, didReceive event: Event) {
        switch event.type {
        case .conversationMessageSent:
            // message sent event received
        case .conversationParticipantTypingOff:
            // participantTypingOff event received
        case .conversationParticipantTyping:
            // participantTyping event received
        case .conversationMessageRead:
                // message read event received
        // case .other events:
        
        default:
            // unknown event type
        }
}

Please note that you can register multiple listeners in your code, to handle events in different parts of your application. The events are broadcasted to all registered delegates.

[client removeEventDelegate:self];
client.remove(self)

Available events

Event type

Event subtype

profile

update

Sent when a user's profile is updated.

conversation

participantAdded

Sent when a participant is added to a conversation. When a conversation is created, this event will also fire with the owner's profileId.

conversation

participantUpdated

Sent when a participant role is updated in a conversation.

conversation

participantRemoved

Sent when a participant is removed from a conversation.

conversation

delete

Sent when a conversation is deleted.

conversation

update

Sent when a conversation details were updated.

conversation

undelete

Sent when a conversation is restored.

conversation

create

Sent when new conversation was created.

conversation

participantTyping

Sent when one of the participants is typing new message

conversation

participantTypingOff

Sent when the other participant stopped typing

conversationMessage

delivered

Sent when one of participants updated the message status to 'delivered'.

conversationMessage

read

Sent when one of participants updated the message status to 'read'.

conversationMessage

sent

Sent when a new message appeared in a conversation. This event will also be delivered to the message sender.

For all events you can access id, apiSpace and name and context properties.

For other events, there are other properties you can access, as follows:

profile.profileId

Conversation

Below are listed properties specific for the conversation event subtypes.

conversation.conversationId
conversation.payload.profileId
conversation.payload.role
conversation.conversationId
conversation.payload.profileId
conversation.payload.role
conversation.conversationId
conversation.payload.profileId
conversation.payload.role
conversation.conversationId
conversation.payload.date
conversation.profileId
conversation.payload.roles
conversation.payload.isPublic
conversation.payload.participants
conversation.conversationId
conversation.payload.description
conversation.payload.roles
conversation.profileId
conversation.payload.roles
conversation.payload.isPublic
conversation.payload.participants
conversation.account
conversation.payload.profileId
covenrsation.payload.conversationId
conversation.account
conversation.payload.profileId
covenrsation.payload.conversationId

ConversationMessage

conversationMessage.messageId
conversationMessage.conversationId
conversationMessage.profileId
conversationMessage.timestamp
conversationMessage.messageId
conversationMessage.conversationId
conversationMessage.profileId
conversationMessage.timestamp
conversationMessage.messageId
conversationMessage.metadata
conversationMessage.parts
conversationMessage.alert

Client APIs

You can use CMPComapi client instance obtained in Initialisation to access SDK APIs.

Session

Get the current profileID:

NSString *profileID = [client profileID];
let profileId = client.profileID

Check if session is successfully created:

BOOL isSuccessfullyCreated = [client isSessionSuccessfullyCreated];
let isSessionSuccesfullyCreated = client.isSessionSuccessfullyCreated

Services

Accessing services is done by calling methods from the proper group:

client.services.profile
client.services.session
client.services.messaging
client.services.profile
client.services.session
client.services.messaging

Most services use CMPResult object as a return value in completion blocks:

@interface CMPResult<__covariant T> : NSObject

@property (nonatomic, nullable) T object;
@property (nonatomic, nullable) NSError *error;
@property (nonatomic, nullable) NSString *eTag;
@property (nonatomic) NSInteger code;

- (instancetype)init NS_UNAVAILABLE;

- (instancetype)initWithObject:(nullable T)object error:(nullable NSError *)error eTag:(nullable NSString *)eTag code:(NSInteger)code;

@end
open class Result<T> : NSObject where T : AnyObject {

    open var object: T?
    open var error: Error?
    open var eTag: String?
    open var code: Int

    public init(object: T?, error: Error?, eTag: String?, code: Int)
}

You can get the ETag value from it, as well as an HTTP status code and potential error.

Profile service

Profile

Get profile:

[client.services.profile getProfileForProfileID:@"<PROFILE-ID>" completion:^(CMPResult<CMPProfile *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.profile.getProfile(profileID: "") { (result) in
    if let err = result.error {
        // error occurred
    } else if let obj = result.object {
        // success
    }
}

Query profiles with query builder:

[client.services.profile queryProfilesWithQueryElements:@[] completion:^(CMPResult<NSArray<CMPProfile *> *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.profile.queryProfiles(queryElements: [.init(key: "email", element: .contains, value: "new")]) { (result) in
    if let err = result.error {
        // error occurred
    } else if let obj = result.object {
        // success
    }
}

Patch profile:

[client.services.profile patchProfileForProfileID:@"<PROFILE-ID>" attributes:@{@"email" : @"[email protected]"} eTag:nil completion:^(CMPResult<CMPProfile *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.profile.patchProfile(profileID: "profileId", attributes: ["email" : "[email protected]"], eTag: nil) { (result) in
    if let err = result.error {
        // error occurred
    } else if let obj = result.object {
        // success
    }
}

Update profile:

[client.services.profile updateProfileForProfileID:@"<PROFILE-ID>" attributes:@{@"email" : @"[email protected]"} eTag:nil completion:^(CMPResult<CMPProfile *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.profile.updateProfile(profileID: "profileId", attributes: ["email" : "[email protected]"], eTag: nil) { (result) in
    if let err = result.error {
        // error occurred
    } else if let obj = result.object {
        // success
    }
}

Messaging service

Conversation

Create conversation:

CMPRoleAttributes *ownerAttributes = [[CMPRoleAttributes alloc] initWithCanSend:YES canAddParticipants:YES canRemoveParticipants:YES];

    CMPRoleAttributes *participantAttributes = [[CMPRoleAttributes alloc] initWithCanSend:YES canAddParticipants:NO canRemoveParticipants:NO];

    CMPRoles *roles = [[CMPRoles alloc] initWithOwnerAttributes:ownerAttributes participantAttributes:participantAttributes];

CMPConversationParticipant *owner = [[CMPConversationParticipant alloc] initWithID:profileID role:@"owner"];

CMPNewConversation *newConversation = [[CMPNewConversation alloc] initWithID:@"{id}" name:@"{name}" description:@"{description}" roles:roles participants:@[owner] isPublic:@(NO)];

[client.services.messaging addConversationWithConversation:weakSelf.conversation completion:^(CMPResult<CMPConversation *> *result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
let participant = ConversationParticipant(id: "{participantId}", role: .participant)
let ownerAttributes = RoleAttributes(canSend: true, canAddParticipants: true, canRemoveParticipants: true)
let participantAttributes = RoleAttributes(canSend: true, canAddParticipants: false, canRemoveParticipants: false)
let roles = Roles(ownerAttributes: ownerAttributes, participantAttributes: participantAttributes)
let newConversation = NewConversation(id: "{id}", name: "{name}", description: "{description}", roles: roles, participants: [participant], isPublic: NSNumber.init(booleanLiteral: false))

client.services.messaging.addConversation(conversation: newConversation) { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object {
        // success
    }
}

Query all conversations:

[_client.services.messaging getConversationsWithProfileID:_client.profileID isPublic:YES completion:^(CMPResult<NSArray<CMPConversation *> *> * _Nonnull) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }              
}];
client.services.messaging.getConversations(profileID: client.profileID!, isPublic: true) { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object?.compactMap({ $0 as? Profile }) {
        // success
    }
}

Query specific conversation:

[client.services.messaging getConversationWithConversationID:@"{id}" completion:^(CMPResult<CMPConversation *> *result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.messaging.getConversation(conversationID: "{id}") { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object {
        // success
    }
}

Update conversation:

CMPConversationUpdate *update = [[CMPConversationUpdate alloc] initWithID:@"{id}" name:@"{name}" description:@"{description}" roles:roles isPublic:@(NO)];

[client.services.messaging updateConversationWithConversationID:@"{id}" conversation:update eTag:nil completion:^(CMPResult<CMPConversation *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
let ownerAttributes = RoleAttributes(canSend: true, canAddParticipants: true, canRemoveParticipants: true)
let participantAttributes = RoleAttributes(canSend: true, canAddParticipants: false, canRemoveParticipants: false)
let roles = Roles(ownerAttributes: ownerAttributes, participantAttributes: participantAttributes)
let update = ConversationUpdate(id: "{id}", name: "{name}", description: "{description}", roles: roles, isPublic: NSNumber.init(booleanLiteral: false))
        
client.services.messaging.updateConversation(conversationID: "{id}", conversation: update, eTag: nil) { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object {
        // success
    }
}

Delete conversation:

[client.services.messaging deleteConversationWithConversationID:@"{id}" eTag:nil completion:^(CMPResult<NSNumber *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.messaging.deleteConversation(conversationID: "{id}", eTag: nil) { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object?.boolValue, obj == true {
        // success
    }
}

Participants

Get participants:

[client.services.messaging getParticipantsWithConversationID:@"{id}" completion:^(CMPResult<NSArray<CMPConversationParticipant *> *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.messaging.getParticipants(conversationID: "{id}") { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object?.compactMap({ $0 as ConversationParticipant }) {
        // success
    }
}

Add participants:

NSArray<CMPConversationParticipant *> *participants = @[[[CMPConversationParticipant alloc] initWithID:@"@{id1}" role:@"{role}"], [[CMPConversationParticipant alloc] initWithID:@"@{id2}" role:@"{role}"]];

[client.services.messaging addParticipantsWithConversationID:@"{id}" participants:participants completion:^(CMPResult<NSNumber *> * result) {
    if (result.error) {
            // error occurred
    } else {
        // success
    }
}];
let participant = ConversationParticipant(id: "{participantId}", role: .participant)
        
client.services.messaging.addParticipants(conversationID: "{id}", participants: []) { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object?.boolValue, obj == true {
        // success
    }
}

Remove participants:

[client.services.messaging removeParticipantsWithConversationID:@"{id}" participants:participants completion:^(CMPResult<NSNumber *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
let participant = ConversationParticipant(id: "{participantId}", role: .participant)
        
client.services.messaging.removeParticipants(conversationID: "{id}", participants: [participant]) { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object?.boolValue, obj == true {
        // success
    }
}

Messages

Query messages:

[client.services.messaging getMessagesWithConversationID:@"{id}" limit:100 from:0 completion:^(CMPResult<CMPGetMessagesResult *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.messaging.getMessages(conversationID: "{id}") { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object {
        // success
    }
}

Create message:

CMPMessagePart *part = [[CMPMessagePart alloc] initWithName:@"{name}" type:@"{type}" url:nil data:@"{data}" size:@(123)];

CMPMessageAlert *alert = [[CMPMessageAlert alloc] initWithPlatforms:[[CMPMessageAlertPlatforms alloc] initWithApns:@{} fcm:@{}]];
    
CMPSendableMessage *message = [[CMPSendableMessage alloc] initWithMetadata:@{} parts:@[part] alert:alert];

[client.services.messaging sendMessage:message toConversationWithID:@"{id}" completion:^(CMPResult<CMPSendMessagesResult *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
let text = "some message"
let part = MessagePart(name: "part1", type: "plain/text", url: nil, data: text, size: NSNumber(integerLiteral: text.count))
let msg = SendableMessage(metadata: ["custom field" : "custom value"], parts: [part], alert: nil)
        
client.services.messaging.send(message: msg, conversationID: "{id}") { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object {
        // success
    }
}

Update messages status':

[client.services.messaging updateStatusForMessagesWithIDs:@[@"{id1}", @"{id2}"] status:@"read" conversationID:@"{id}" timestamp:[NSDate date] completion:^(CMPResult<NSNumber *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.messaging.updateStatus(messageIDs: ["{id1}", "{id2}"], status: .delivered, conversationID: "{id}", timestamp: Date()) { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object?.boolValue, obj == true {
        // success
    }
}

Events

Query events:

[client.services.messaging queryEventsWithConversationID:@"{id}" limit:0 from:100 completion:^(CMPResult<NSArray<CMPEvent *> *> * result) {
    if (result.error) {
        // error occurred
    } else {
        // success
    }
}];
client.services.messaging.queryEvents(conversationID: "{id}") { (result) in
    if let err = result.error {
        // error
    } else if let obj = result.object?.compactMap({ $0 as? Event }) {
        // success
    }
}