Join us in San Francisco on the 29/30th of October for two days of developer workshops and technical talks

How to Make and Receive In App calls with the Nexmo Client SDK on Android

In this tutorial you learn how to use the Nexmo Client SDK for Android, in order to perform an in-app (IP to IP) voice call.

You create a simple app that can make a call and receive a call.

The app will have two buttons, which log in different users: Jane or Joe. After logging in, Jane and Joe are able to place a call and perform actions such as answer, reject or hangup.

Prerequisites

  1. Create a Nexmo Application.

  2. Create two users on that Nexmo Application: one with the name Jane and the other with the name Joe.

The starter project

Clone or download the GitHub repository on either Kotlin orJava. On that repository you'll find two apps:

  • GetStartedCalls-Start - if you want to follow along and add the code with this tutorials
  • GetStartedCalls-Complete - if you want to look at the final result

Open the NexmoHelper class.

  1. Make sure that enabledFeatures array contains Features.IN_APP_to_IN_APP. You can remove the rest, if you haven't completed their tutorials yet.

  2. Replace the user IDs and tokens:

String USER_ID_JANE = "USR-XXX"; //TODO: replace with the UserId you generated for Jane
String USER_ID_JOE = "USR-XXX"; //TODO: replace with the UserId you generated for Joe
String JWT_JANE = "PLACEHOLDER";//TODO: replace with the JWT you generated for Jane
String JWT_JOE = "PLACEHOLDER"; //TODO: replace with the JWT you generated for Joe
val USER_ID_JANE = "USR-XXX"; //TODO("replace with the UserId you generated for Jane")
val USER_ID_JOE = "USR-XXX"; //TODO("replace with the UserId you generated for Joe")
val JWT_JANE = "PLACEHOLDER"; //TODO("replace with the JWT you generated for Jane")
val JWT_JOE = "PLACEHOLDER"; //TODO("replace with the JWT you generated for Joe")

Login

To start using the Nexmo Client SDK you need to log in to NexmoClient, using a JWT user token.

In production apps, your server would authenticate the user, and would return a correctly configured JWT to your app.

For testing and getting started purposes, you can use the Nexmo CLI to generate JWTs.

Open LoginActivity. It already has two button handlers: onLoginJaneClick(...) and onLoginJoeClick(...). Each calls the loginToSdk(...) method, with a different parameter - the corresponding jwt.

When the login is successful, the logged in NexmoUser returns. For convenience, save a reference to NexmoUser on NexmoHelper, and then, start MainActivity.

Complete the loginToSdk() method implementation:

void loginToSdk(String token) {
    NexmoClient.get().login(token, new NexmoRequestListener<NexmoUser>() {

        @Override
        public void onError(NexmoApiError nexmoApiError) {}

        @Override
        public void onSuccess(NexmoUser user) {
            NexmoHelper.user = user;

            Intent intent = new Intent(getBaseContext(), MainActivity.class);
            startActivity(intent);
            finish();
        }
    });
}
fun loginToSdk(token: String) {
        NexmoClient.get().login(token, object : NexmoRequestListener<NexmoUser> {

            override fun onError(nexmoApiError: NexmoApiError) {
                notifyError(nexmoApiError)
            }

            override fun onSuccess(user: NexmoUser?) {
                currentUser = user

                val intent = Intent(baseContext, MainActivity::class.java)
                startActivity(intent)
                finish()
            }
        })
    }
}

At this point you should already be able to run the app and see that you can login successfully with the SDK.

Start a Call

You can now make a simple In-App call. If you logged in as Jane, you can call Joe, and if you logged in as Joe you can call Jane.

Open MainActivity and complete the prepared onInAppCallClick() handler:

public void onInAppCallClick(View view) {
    String callee = getOtherUserId();
    NexmoClient.get().call(callee, NexmoCallHandler.IN_APP, callListener);
}

String getOtherUserId() {
    return NexmoHelper.user.getName().equals(NexmoHelper.USER_NAME_JANE) ? NexmoHelper.USER_ID_JOE : NexmoHelper.USER_ID_JANE;
}
fun onInAppCallClick(view: View) {
        val callee = otherUserName
        NexmoClient.get().call(otherUserName, NexmoCallHandler.IN_APP, callListener)
}

val otherUserName: String
    get() = if (user?.name == USER_NAME_JANE) USER_NAME_JOE else USER_NAME_JANE

When the call creation is successful, save the NexmoCall on NexmoHelper, for convenience, and start OnCallActivity:

NexmoRequestListener<NexmoCall> callListener = new NexmoRequestListener<NexmoCall>() {
    @Override
    public void onError(NexmoApiError nexmoApiError) { }

    @Override
    public void onSuccess(NexmoCall call) {
        NexmoHelper.currentCall = call;

        Intent intent = new Intent(MainActivity.this, OnCallActivity.class);
        startActivity(intent);
    }
};
var callListener = object: NexmoRequestListener<NexmoCall> {
    override fun onError(nexmoApiError: NexmoApiError) {
        notifyError(nexmoApiError)
    }

    override fun onSuccess(call: NexmoCall) {
        currentCall = call

        val intent = Intent(this@MainActivity, OnCallActivity::class.java)
        startActivity(intent)
    }
}

You can also start a call with customized logic using an NCCO, by choosing NexmoCallHandler.SERVER as the CallHandler:

NexmoCient.get().call(callee, NexmoCallHandler.SERVER, listener)
NexmoCient.get().call(callee, NexmoCallHandler.SERVER, listener)

This allows you to start a PSTN phone call, by adding a phone number to the callees list.

Register for incoming events

When Jane calls Joe, Joe should be notified. Joe can then answer the call. Joe should register to incoming events, and implement onIncomingCall(). Whenever Joe is called, onIncomingCall() is invoked with the incoming Call object.

For simplicity in this example, you will accept incoming calls only on MainActivity. Open MainActivity and create the NexmoIncomingCallListener to save the reference to the incoming call on NexmoHelper, and start IncomingCallActivity:

NexmoIncomingCallListener incomingCallListener = new NexmoIncomingCallListener() {
    @Override
    public void onIncomingCall(NexmoCall call) {

        NexmoHelper.currentCall = call;
        startActivity(new Intent(MainActivity.this, IncomingCallActivity.class));
    }
};
val incomingCallListener = NexmoIncomingCallListener { call ->
    currentCall = call
    startActivity(Intent(this@MainActivity, IncomingCallActivity::class.java))
}

You need to register and unregister the listener in onCreate() and onDestroy():

@Override
protected void onStart() {
    super.onStart()
    NexmoClient.get().addIncomingCallListener(incomingCallListener);
}

@Override
protected void onStop() {
    NexmoClient.get().removeIncomingCallListeners();
    super.onStop();
}
override fun onStart() {
    super.onStart()
    NexmoClient.get().addIncomingCallListener(incomingCallListener)
}

override fun onStop() {
    NexmoClient.get().removeIncomingCallListeners()
    super.onStop()
}

Answer a call

Once a incoming call is received, it can be answered. Open IncomingCallActivity, and complete the prepared onAnswer() button handler, to start OnCallActivity after a successful answer:

public void onAnswer(View view) {
    NexmoHelper.currentCall.answer(new NexmoRequestListener<NexmoCall>() {
        @Override
        public void onError(NexmoApiError nexmoApiError) { }

        @Override
        public void onSuccess(NexmoCall call) {
            startActivity(new Intent(IncomingCallActivity.this, OnCallActivity.class));
            finish();
        }
    });
}
fun onAnswer(view: View) {
    currentCall?.answer(object : NexmoRequestListener<NexmoCall> {
        override fun onError(nexmoApiError: NexmoApiError) {}

        override fun onSuccess(call: NexmoCall) {
            startActivity(Intent(this@IncomingCallActivity, OnCallActivity::class.java))
            finish()
        }
    })
}

Hangup

The onHangup() handler allows to reject the call. Complete the implementation in IncomingCallActivity to finish the activity:

public void onHangup(View view) {
    NexmoHelper.currentCall.hangup(new NexmoRequestListener<NexmoCall>() {
        @Override
        public void onError(NexmoApiError nexmoApiError) { }

        @Override
        public void onSuccess(NexmoCall call) {
            finish();
        }
    });
}
currentCall?.hangup(object : NexmoRequestListener<NexmoCall> {
    override fun onError(nexmoApiError: NexmoApiError) {}

    override fun onSuccess(call: NexmoCall) {
        startActivity(Intent(this@IncomingCallActivity, OnCallActivity::class.java))
        finish()
    }
})

Register to call status

To be aware of the call status, for example, if one of the members answers or hangs up, you should register to CallEvents. The FinishOnCallEnd is a NexmoCallEventListener that finishes the current activity if the call is completed or canceled.

Register to its instance, to address the use cases mentioned previously.

On both OnCallActivity and IncomingCallActivity, add:

NexmoCallEventListener callEventListener = new FinishOnCallEnd(this);

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    NexmoHelper.currentCall.addCallEventListener(callEventListener);
}


@Override
protected void onDestroy() {
    NexmoHelper.currentCall.removeCallEventListener(callEventListener);
    super.onDestroy();
}
var callEventListener = FinishOnCallEnd(this)

override fun onCreate(savedInstanceState: Bundle?) {
    currentCall?.addCallEventListener(callEventListener)
}


override fun onDestroy() {
    currentCall?.removeCallEventListener(callEventListener)
    super.onDestroy()
}

Handle permissions

For devices running Android 6.0 (API level 23) and higher, creation and operation of calls requires requesting runtime permissions. To simplify the implementation in this tutorial, BaseActivity checks the permissions in every Activity's onStart() and onStop().

To read more about the permissions required, see the setup tutorial.

Conclusion

Congratulations! You have implemented your first In-App Voice application with Nexmo Client SDK for Android.

You can now run the app on two devices, log in as two different users, and start an in-app voice call between the users.

How to Make and Receive In App Calls with the Nexmo Client SDK on iOS

In this tutorial you learn how to use the Nexmo Client SDK for iOS, in order to perform an in-app (IP to IP) voice call.

You will create a simple app to make a call and receive a call.

The app will have two buttons, which log in different users: Jane or Joe. After logging in, Jane and Joe are able to place a call and perform actions such as answer, reject or hangup.

Prerequisites

Starter Project

Clone this Github project.

Using the Github project you cloned, in the Start folder, open GettingStarted.xcworkspace. Then, within XCode:

  1. Open Constants.swift file and replace the user uuids and jwts:

        var uuid: String {
            switch self {
            case .jane:
                return "" //TODO: swap with Jane's userId
            case .joe:
                return "" //TODO: swap with Joe's userId
            }
        }
    
        var jwt: String {
            switch self {
            case .jane:
                return "" //TODO: swap with a token for Jane
            case .joe:
                return "" //TODO: swap with a token for Joe
            }
        }
    
  2. From the App-to-App group, open AppToAppCallViewController.swift file and make sure the following lines exist:

  • import NexmoClient - imports the sdk
  • let client = NXMClient.shared - the NexmoClient shrared instance
  • var call: NXMCall? - property for the call instance

Clone this Github project.

Using the Github project you cloned, in the Start folder, open GettingStarted.xcworkspace. Then, within XCode:

  1. Open User.h file and replace the user IDs and tokens:
#define kJaneName @"Jane"

#define kJaneUUID @"" //TODO: swap with a user uuid for Jane

#define kJaneJWT @"" //TODO: swap with a token for Jane



#define kJoeName @"Joe"

#define kJoeUUID @"" //TODO: swap with a userId for Joe

#define kJoeJWT @"" //TODO: swap with a token for Joe


  1. From the App-to-App group, open AppToAppCallViewController.m file and make sure the following lines exist:
  • #import <NexmoClient/NexmoClient.h> - imports the sdk
  • @property NXMClient *client; - property for the client instance
  • @property NXMCall *call; - property for the call instance

Login

Using the Nexmo Client SDK should start with logging in to NexmoClient, using a user's jwt.

In production apps, your server would authenticate the user, and would return a correctly configured JWT to your app.

For testing and getting started purposes, you can use the Nexmo CLI to generate JWTs.

Inside AppToAppCallViewController, explore the setup methods that were written for you on viewDidLoad.

Now locate the following line //MARK: Setup Nexmo Client and complete the setupNexmoClient method implementation:

func setupNexmoClient() {
    client.setDelegate(self)
    client.login(withAuthToken: user.jwt)
}

Notice that self is set to be the delegate for NXMClient. Do not forget to adopt the NXMClientDelegate protocol and implement the required methods.

Add the required protocol adoption declaration to the class extension located towards the end of the AppToAppCallViewController.swift file:

extension AppToAppCallViewController: NXMClientDelegate {
    ...
}

The client(_:didChanged:reason:) methods of the NXMClientDelegate protocol indicates if the login was successful and you can start using the SDK.

Add the following methods under the //MARK:- NXMClientDelegate line.

func client(_ client: NXMClient, didChange status: NXMConnectionStatus, reason: NXMConnectionStatusReason) {
    updateInterface()
}
func client(_ client: NXMClient, didReceiveError error: Error) {
    updateInterface()
}

Inside AppToAppCallViewController, explore the setup methods that were written for you on viewDidLoad.

Now locate the following line //MARK: Setup Nexmo Client and complete the setupNexmoClient method implementation:

- (void)setupNexmoClient {
    self.client = [NXMClient shared];
    [self.client setDelegate:self];
    [self.client loginWithAuthToken:self.user.jwt];;
}

Notice that self is set to be the delegate for NXMClient. Do not forget to adopt the NXMClientDelegate protocol and implement the required methods.

Locate the //MARK:- NXMClientDelegate line and add the required protocol methods:

- (void)client:(nonnull NXMClient *)client didChangeConnectionStatus:(NXMConnectionStatus)status reason:(NXMConnectionStatusReason)reason {
    [self updateInterface];
}

- (void)client:(nonnull NXMClient *)client didReceiveError:(nonnull NSError *)error {
    [self updateInterface];
}

At this point you should already be able to run the app and see that you can login successfully with the SDK.

Start a Call

You can now make a simple In-App call. If you logged in as Jane, you can call Joe, and if you logged in as Joe you can call Jane - the call button changes its text accordingly.

Call Jane/Joe button press is already connected to the AppToAppCallViewController.

Implement the call: method to start a call.

@IBAction func call(_ sender: Any) {
    // start a new call (if one doesn't already exists)
    guard let call = call else {
        startCall()
        return
    }
    // or end an existing call
    end(call: call)
}

private func startCall() {

}
private func end(call: NXMCall) {

}

If a call is already in progress, taping the button will end it.

Let's implement startCall - it will start the call, and also update the visual elements so that Jane or Joe know the call is in progress:

private func startCall() {
    callStatus = .initiated
    client.call(user.callee.rawValue, callHandler: .inApp) { [weak self] (error, call) in
        guard let self = self else { return }
        // Handle create call failure
        guard let call = call else {
            if let error = error {
                // Handle create call failure
                print("✆  ‼️ call not created: \(error.localizedDescription)")
            } else {
                // Handle unexpected create call failure
                print("✆  ‼️ call not created: unknown error")
            }
            self.callStatus = .error
            self.call = nil
            self.updateInterface()
            return
        }

        // Handle call created successfully.
        // callDelegate's  statusChanged: will be invoked with needed updates.
        call.setDelegate(self)
        self.call = call
        self.updateInterface()
    }
    updateInterface()
    }

Call Other button press is already connected to the AppToAppCallViewController.

Implement the call: method to start a call. It will start the call, and also update the UIViews so that Jane or Joe know the call is in progress:

- (IBAction)call:(id)sender {
    if (self.call != nil || self.callStatus == CallStatusInitiated) {
        [self endCall];
    } else {
        [self startCall];
    }
}

- (void)startCall {
}

- (void)endCall {
}

If a call is already in progress, taping the button will end it.

Let's implement startCall - it will start the call, and also update the visual elements so that Jane or Joe know the call is in progress:

- (void)startCall {
    self.callStatus = CallStatusInitiated;
    __weak AppToAppCallViewController *weakSelf = self;
    [self.client call:[self callee].name callHandler:NXMCallHandlerInApp completionHandler:^(NSError * _Nullable error, NXMCall * _Nullable call) {
        if(error) {
            NSLog(@"✆  ‼️ call not created: %@", error);
            weakSelf.call = nil;
            [weakSelf updateInterface];
            return;
        }
        NSLog(@"✆  call: %@", call);
        weakSelf.call = call;
        [weakSelf.call setDelegate:self];
        [weakSelf updateInterface];
    }];
}

Call Handler

Note the second parameter in the client?.call method above - while NXMCallHandler.inApp is useful for simple calls, you can also start a call with customized logic using a NCCO, by choosing NXMCallHandler.server as the callHandler.

client?.call(callee, callHandler: .server) { [weak self] (error, call) in
    ...
}

Note the second parameter in the client?.call method above - while NXMCallHandlerInApp is useful for simple calls, you can also start a call with customized logic using a NCCO, by choosing NXMCallHandlerServer as the callHandler.

[self.nexmoClient call:callee callHandler:NXMCallHandlerServer completion:^(NSError * _Nullable error, NXMCall * _Nullable call) {
    ...
}];

This also allows you to start a PSTN phone call, by adding a phone number as the callee.

Call Delegate

Note that, when a call is placed successfully, we're setting self as the delegate for it.

We'll now adopt the NXMCallDelegate as an extension on AppToAppCallViewController, under the //MARK:- NXMCallDelegate line:

extension AppToAppCallViewController: NXMCallDelegate {

}

Copy the following NXMCallDelegate methods inside the extension:

func call(_ call: NXMCall, didUpdate callMember: NXMCallMember, with status: NXMCallMemberStatus) {

    // call error
    if call.otherCallMembers.contains(callMember), [NXMCallMemberStatus.failed, NXMCallMemberStatus.busy, NXMCallMemberStatus.timeout].contains(callMember.status) {
        self.callStatus = .rejected
        self.call?.hangup()
        self.call = nil
    }

    // call rejected
    if call.otherCallMembers.contains(callMember), callMember.status == .rejected {
        self.callStatus = .rejected
        self.call?.hangup()
        self.call = nil
    }

    // call ended
    if call.otherCallMembers.contains(callMember), callMember.status == .completed {
        self.callStatus = .completed
        self.call?.hangup()
        self.call = nil
    }

    updateInterface()  
}

func call(_ call: NXMCall, didUpdate callMember: NXMCallMember, isMuted muted: Bool) {
    updateInterface()
}

func call(_ call: NXMCall, didReceive error: Error) {
    updateInterface()
}

The call(_:didUpdate:with:) method notifies on changes that happen to members on the call.

As with NXMClient, NXMCall also has a delegate. Add the required protocol methods under the //MARK:- NXMCallDelegate line:

- (void)call:(nonnull NXMCall *)call didUpdate:(nonnull NXMCallMember *)callMember withStatus:(NXMCallMemberStatus)status {
    NSLog(@"✆  🤙 Call Status update | member: %@ | status: %@", callMember.user.displayName, callMemberStatusDescriptionFor(status));

    // call completed
    if (status == NXMCallMemberStatusCanceled || status == NXMCallMemberStatusCompleted) {
        self.callStatus = CallStatusCompleted;
        [self.call hangup];
        self.call = nil;
    }

    // call error
    if ( (call.myCallMember.memberId != callMember.memberId) && (status == NXMCallMemberStatusFailed || status == NXMCallMemberStatusBusy)) {
        self.callStatus = CallStatusError;
        [self.call hangup];
        self.call = nil;
    }

    // call rejected
    if ( (call.myCallMember.memberId != callMember.memberId) && (status == NXMCallMemberStatusRejected)) {
        self.callStatus = CallStatusRejected;
        [self.call hangup];
        self.call = nil;
    }

    [self updateInterface];
}

- (void)call:(nonnull NXMCall *)call didUpdate:(nonnull NXMCallMember *)callMember isMuted:(BOOL)muted {
    [self updateInterface];
}

- (void)call:(nonnull NXMCall *)call didReceive:(nonnull NSError *)error {
    NSLog(@"✆  ‼️ call error: %@", [error localizedDescription]);
    [self updateInterface];
}

You can build the project now and make an outgoing call. Next you implement how to receive an incoming call.

Receive incoming call

When Jane calls Joe, Joe should be notified, so that he can decide on answering or rejecting the call.

This is done by implementing the optional client(_:didReceive:) method which is declared in the NXMClientDelegate.

Go back to the //MARK: NXMClientDelegate line and add the `incomingCall:' method

func client(_ client: NXMClient, didReceive call: NXMCall) {
    print("✆  📲 Incoming Call: \(call)")
    DispatchQueue.main.async { [weak self] in
        self?.displayIncomingCallAlert(call: call)
    }
}

func displayIncomingCallAlert(call: NXMCall) {
    let names: [String] = call.otherCallMembers.compactMap({ participant -> String? in
        return (participant as? NXMCallMember)?.user.name
    })
    let alert = UIAlertController(title: "Incoming call from", message: names.joined(separator: ", "), preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Answer", style: .default, handler: { _ in
        self.answer(call: call)
    }))
    alert.addAction(UIAlertAction(title: "Reject", style: .default, handler: { _ in
        self.reject(call: call)
    }))
    self.present(alert, animated: true, completion: nil)
}

Go back to the //MARK: NXMClientDelegate line and add the `client:didReceiveCall:' method:

- (void)client:(nonnull NXMClient *)client didReceiveCall:(nonnull NXMCall *)call {
    NSLog(@"✆  📲 Incoming Call: %@", call);
    [self displayIncomingCallAlert:call];
}


- (void)displayIncomingCallAlert:(NXMCall *)call {
    if(![NSThread isMainThread]){
        dispatch_async(dispatch_get_main_queue(), ^{
            [self displayIncomingCallAlert:call];
        });
        return;
    }

    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Incoming Call" message: [self callee].name preferredStyle:UIAlertControllerStyleAlert];

    __weak AppToAppCallViewController *weakSelf = self;
    UIAlertAction* answerAction = [UIAlertAction actionWithTitle:@"Answer" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [weakSelf didPressAnswerIncomingCall:call];
    }];

    UIAlertAction* rejectAction = [UIAlertAction actionWithTitle:@"Reject" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [weakSelf didPressRejectIncomingCall:call];
    }];

    [alertController addAction:answerAction];
    [alertController addAction:rejectAction];
    [self presentViewController:alertController animated:YES completion:nil];
}

This method takes as a parameter an NXMCall object with which you can answer or reject the call. An alert was implemented for you, to allow the user to choose whether to answer or reject the call.

Answer a call

Under the //MARK: Incoming call - Accept, implement this method to answer the incoming call:

private func answer(call: NXMCall) {
    self.call = call
    call.answer { [weak self] error in
        if let error = error {
            print("✆  ‼️ error answering call: \(error.localizedDescription)")
        }
        self?.updateInterface()
    }
}

Under the //MARK: Incoming call - Accept, implement this method to answer the incoming call:

- (void)didPressAnswerIncomingCall:(NXMCall *)call {
    self.call = nil;
    self.callStatus = CallStatusInitiated;
    [self updateInterface];

    __weak AppToAppCallViewController *weakSelf = self;
    self.call = call;
    [call answer:^(NSError * _Nullable error) {
        if(error) {
            NSLog(@"✆  ‼️ error answering call: %@", error.localizedDescription);
            [weakSelf displayAlertWithTitle:@"Answer Call" andMessage:@"Error answering call"];
            weakSelf.call = nil;
            [weakSelf updateInterface];
            return;
        }
        [weakSelf.call setDelegate:self];
        NSLog(@"✆  🤙 call answered");
        [weakSelf updateInterface];
    }];
}

answer: accepts a single completionHandler parameter to indicate if an error occurred in the process.

Reject a call

Under the //MARK: Incoming call - Reject, implement this method to reject the incoming call:

private func reject(call: NXMCall) {
    call.reject { [weak self] error in
        if let error = error {
            print("✆  ‼️ error declining call: \(error.localizedDescription)")
        }
        self?.updateInterface()
    }
}

Under the //MARK: Incoming call - Reject, implement this method to reject the incoming call:

- (void)didPressRejectIncomingCall:(NXMCall *)call {
    self.call = nil;

    __weak AppToAppCallViewController *weakSelf = self;
    [call reject:^(NSError * _Nullable error) {
        if(error) {
            NSLog(@"✆  ‼️ error rejecting call: %@", error.localizedDescription);
            [weakSelf displayAlertWithTitle:@"Reject Call" andMessage:@"Error rejecting call"];
            return;
        }
        NSLog(@"✆  🤙 call rejected");
        [weakSelf updateInterface];
    }];
}

reject: accepts a single completionHandler parameter to indicate if an error occurred in the process.

Hangup a call

Once Jane or Joe presses the 'End Call' button, it is time to hangup the call. Implement the private end: method and call hangup for call.

private func end(call: NXMCall) {
    call.hangup()
    callStatus = .completed
    self.call = nil
    updateInterface()
}

Updates for callMember statuses are received in call(_:didUpdate:with:) as part of the NXMCallDelegate as you have seen before.

The existing implementation is already handling call hangup.

Once Jane or Joe presses the End Call button, it is time to hangup the call. Implement endCall method and call hangup for call.

- (void)endCall {
    [self.call hangup];
    [self updateInterface];
}

Updates for callMember statuses are received in call:didUpdate:withStatus: as part of the NXMCallDelegate as you have seen before.

The existing implementation is already handling call hangup.

Handle permissions

For the call to happen, Audio Permissions are required. In the appDelegate of the sample project, you can find an implementation for the permissions request in application:didFinishLaunchingWithOptions.

To read more about the permissions required, see the setup tutorial.

Conclusion

You have implemented your first In App Voice application with the Nexmo Client SDK for iOS.

Run the app on two simulators and see that you can call, answer, reject and hangup.

If possible, test on two phones using your developer signing and provisioning facility.