We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Create an iOS messenger app with push notifications - Part 1: Building the backend

  • Neo Ighodaro
September 20th, 2018
You will need Xcode, Cocoapods and the Laravel CLI installed on your machine. Some knowledge of Swift and PHP will be helpful.

Messenger applications are very popular these days and are the backbone of communication at the moment. We keep in touch using them and occasionally share cat memes with our friends. Meow.

However, one of the most important features of a messenger application is push notifications. These are alerts that are triggered when we are not currently using the application. This lets us know when a new message is received so we can respond.

In this article, we will be building an iOS messenger application with a push notification feature. The application will allow users to chat with each other and when they are not currently in the application, they will receive a push notification telling them there is a new message waiting for them.

Here is a quick look at what we will be creating:

Requirements

To follow along in the tutorial, you need to have the following:

  1. Xcode installed on your machine.
  2. Cocoapods installed on your machine.
  3. Some knowledge of using Xcode and the Swift programming language.
  4. Knowledge of PHP and Laravel.
  5. The latest version of Laravel CLI installed locally.
  6. SQLite installed on your machine. Install here.
  7. A Chatkit instance. Create one here.
  8. A Pusher Beams instance. Create one here.

If you have all the requirements above then let’s proceed.

Setting up your Laravel application

The first thing we want to do is create a backend application. This will be the API that the iOS app will connect to and perform specific tasks like authentication and more.

To get started, open your terminal and run the command below to create a new Laravel project:

    $ laravel new conveyapi
    $ cd conveyapi

This will create a new Laravel project in the current working directory and change to that project directory. When the installation is complete, open the project in your IDE or text editor.

Installing dependencies

Open the composer.json file and add the following dependencies to the file:

    // [...]

    "require": {
        // [...]

        "laravel/passport": "^4.0",
        "pusher/pusher-chatkit-server": "^0.5.4",
        "pusher/pusher-push-notifications": "^1.0"
    },

    // [...]

The dependencies we added are the Laravel Passport library, the Pusher Chatkit SDK and the Pusher Beams SDK. This will make it easy for us to work with both services in any PHP application.

Next, run the following command in your terminal to install the dependencies.

    $ composer update

This will update (or install) the dependencies and make them available to our project.

Creating database migrations

The next thing we want to do is create migrations for the database of the app. Migrations will help us manage our database tables.

In your terminal, run the following command:

    $ php artisan make:migration create_rooms_table
    $ php artisan make:migration create_room_user_table

This will create two additional migrations in our database/migrations directory. Let’s start editing the migrations in this directory.

Open the *_create_users_table.php migration and replace the contents with this:

    <?php // File: ./database/migrations/*_create_users_table.php

    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class CreateUsersTable extends Migration
    {
        public function up()
        {
            Schema::create('users', function (Blueprint $table) {
                $table->increments('id');
                $table->string('chatkit_id')->unique();
                $table->string('name');
                $table->string('email')->unique();
                $table->string('password');
                $table->rememberToken();
                $table->timestamps();
            });
        }

        public function down()
        {
            Schema::dropIfExists('users');
        }
    }

Open the *_create_rooms_table migration and replace the contents with the following:

    <?php // File: ./database/migrations/*_create_rooms_table.php

    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class CreateRoomsTable extends Migration
    {
        public function up()
        {
            Schema::create('rooms', function (Blueprint $table) {
                $table->increments('id');
                $table->integer('chatkit_room_id')->unique();
                $table->string('name');
                $table->boolean('channel')->default(false);
                $table->timestamps();
            });
        }

        public function down()
        {
            Schema::dropIfExists('rooms');
        }
    }

Open the *_create_room_user_table migration and replace the contents with the following:

    <?php // File: ./database/migrations/*_create_room_user_table.php

    use Illuminate\Support\Facades\Schema;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Database\Migrations\Migration;

    class CreateRoomUserTable extends Migration
    {
        public function up()
        {
            Schema::create('room_user', function (Blueprint $table) {
                $table->increments('id');
                $table->unsignedInteger('room_id');
                $table->unsignedInteger('user_id');
                $table->timestamps();
            });
        }

        public function down()
        {
            Schema::dropIfExists('room_user');
        }
    }

That’s all for the migrations. Let’s add a database connection for our application and run the migrations we just created.

Creating and connecting to the database

For our database, we are going to use SQLite, but you can any database you like. You should use a non-file based database like MySQL if in a production environment.

Create a database file at database/database.sqlite. Next, open your .env file and replace the following lines:

    DB_CONNECTION=mysql
    DB_DATABASE=homestead
    DB_USERNAME=username
    DB_PASSWORD=password

with

    DB_CONNECTION=sqlite
    DB_DATABASE=/full/path/to/database/database.sqlite

That is all for our database setup. Run the migrations to create the database tables for our application and seed it:

    $ php artisan migrate

When we run migrate, it will run all the migrations available to the application including the Laravel passport migrations. Speaking of Laravel Passport, let’s set it up.

Setting up Laravel Passport

To start integrating Laravel Passport to our app, add the Laravel\Passport\HasApiTokens trait to your App\User model as seen below:

    <?php // File: ./app/User.php
    namespace App;

    use Laravel\Passport\HasApiTokens;
    use Illuminate\Notifications\Notifiable;
    use Illuminate\Foundation\Auth\User as Authenticatable;

    class User extends Authenticatable
    {
        use HasApiTokens, Notifiable;

        protected $fillable = ['name', 'email', 'password', 'chatkit_id'];

        protected $hidden = ['password', 'remember_token'];
    }

Note that we also added a chatkit_id property to the fillable array.

Next, we need to register the Passport routes to our application. Open the app/providers/AuthServiceProvider.php file and in the boot method add this call:

    \Laravel\Passport\Passport::routes();

Finally, open the config/auth.php file and change the driver option of the api authentication guard to passport from token. This forces your application to use Passport’s TokenGuard when authenticating incoming API requests:

    // File: ./config/auth.php

    'guards' => [
        // [...]

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],    
    ],

    // [...]

To finish the installation, run the command below:

    $ php artisan passport:install

This will generate encryption keys for generating tokens and also create two clients for your usage. We will be using only the password grant client though. The output of our command will look similar to this:

    Encryption keys generated successfully.
    Personal access client created successfully.
    Client ID: 1
    Client Secret: N6GH0MyTCIBW89g7BkzZwc6Q3gcPU16p91G7LDLv
    Password grant client created successfully.
    Client ID: 2
    Client Secret: nneBZLH70o0Ez9rtpOYCBOzbarrcYpDVLCjnUTdn

💡 Please note the details (Client ID and Client Secret) for the password grant client. You’ll need the credentials in our iOS application when we need to generate tokens to make calls to our API.

To learn more about creating OAuth servers using Laravel and Passport, read this article.

Creating the Eloquent models

Let’s create and modify the models so our application can use them. All our models are stored in the app directory.

Open the User.php model and add the following methods to the class:

    // File: ./app/User.php
    // This will automatically encrypt the password when it is set
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }

    // This is a reference to the room's relationship for this model
    public function rooms()
    {
        return $this->belongsToMany(Room::class);
    }

Next, run the following command to create additional models:

    $ php artisan make:model Room
    $ php artisan make:model RoomUser

Open the Room model and replace the content with the following:

    <?php // File: ./app/Room.php

    namespace App;

    use Illuminate\Database\Eloquent\Model;

    class Room extends Model
    {
        protected $fillable = [
            'name',
            'channel',
            'chatkit_room_id',
        ];

        protected $casts = [
            'channel' => 'boolean',
            'chatkit_room_id' => 'integer',
        ];

        public function getNameAttribute(): string
        {
            $isChannel = (bool) $this->attributes['channel'];

            if ($isChannel === true) {
                return $this->attributes['name'];
            }

            return $this->users()
                ->where('user_id', '!=', request()->user()->id)
                ->get()
                ->implode('name', ', ');
        }

        public function users()
        {
            return $this->belongsToMany(User::class);
        }
    }

Above we have the Room model with standard properties we already know with Laravel. However, we have the getNameAttribute Eloquent accessor that we use to set the name attribute of the room.

In this method, we check to see if the room is a channel and if it is we return the name of the room without modifying it. However, if it is a direct message room, we return a comma-separated name of participants as the name of the room.

We don’t need to modify the RoomUser model.

Setting up Pusher Chatkit

The next thing we want to do is set up Pusher Chatkit. Since we already installed the library using Composer, we will just create a model and a service provider so our Laravel application has an instance of our Chatkit library handy always.

Create a new file in the app directory, Chatkit.php, and paste in the following:

    <?php // File: ./app/Chatkit.php

    namespace App;
    use Chatkit\Chatkit as PusherChatkit;

    class Chatkit extends PusherChatkit
    {
        public function getUserJoinableRooms(array $options)
        {
            return $this->getUserRooms([
                'joinable' => true,
                'user_id' => $options['user_id'] ?? null
            ]);
        }

        public function rooms($include_private = false)
        {
            $ch = $this->createCurl(
                $this->api_settings,
                '/rooms/',
                $this->getServerToken(),
                'GET',
                [],
                ['include_private' => $include_private]
            );

            return $this->execCurl($ch);
        }
    }

In the class above, we have extended the Pusher Chatkit SDK’s main class Chatkit\Chatkit. We have some new methods that help us achieve functionality that is not yet available to the SDK.

Next, open app/Providers/AppServiceProvider.php and paste the following code inside the register method:

    // File: ./app/Providers/AppServiceProvider.php
    $this->app->bind(\App\Chatkit::class, function () {
        return new \App\Chatkit([
            'key' => config('services.chatkit.secret'),
            'instance_locator' => config('services.chatkit.instanceLocator'),
        ]);
    });

This will register the App\Chatkit class into Laravel’s IoC container and make sure every time Laravel attempts to resolve the class using IoC, the class will return an instantiated and configured instance of App\Chatkit. This will be useful when we inject the class into our controllers later.

Open services.php in the config directory and add this configuration to the configuration array:

    // File: ./config/services.php
    'chatkit' => [
        'instanceLocator' => env('CHATKIT_INSTANCE_LOCATOR'),
        'secret' => env('CHATKIT_SECRET_KEY'),
    ],

Now, open your .env file and update your Pusher Chatkit application keys.

    CHATKIT_INSTANCE_LOCATOR="PASTE_CHATKIT_INSTANCE_LOCATOR_HERE"
    CHATKIT_SECRET_KEY="PASTE_CHATKIT_SECRET_KEY_HERE"

If you hadn’t created a Pusher Chatkit instance before now, you should head over to the Pusher Chatkit dashboard and create an instance.

Setting up Pusher Beams

Finally, before we start creating controllers and routes, let’s set up Pusher Beams. Open the AppServiceProvider and in the register method, add the following code:

    // File: ./app/Providers/AppServiceProvider.php
    $this->app->singleton('push_notifications', function () {
        return new \Pusher\PushNotifications\PushNotifications([
            'instanceId' => env('PUSHER_PN_INSTANCE_ID'),
            'secretKey' => env('PUSHER_PN_SECRET_KEY'),
        ]);
    });

Now that we have registered a singleton, open the .env file and update your Pusher Beams app keys:

    PUSHER_PN_INSTANCE_ID="PASTE_BEAMS_INSTANCE_ID_HERE"
    PUSHER_PN_SECRET_KEY="PASTE_BEAMS_SECRET_KEY_HERE"

If you hadn’t created a Pusher Beams instance before now, you should head over to the Pusher Beams dashboard and create an instance. Note though, that to create a Pusher Beams instance, you need an APNs token, launch Xcode and create a new Single application project named Convey before continuing with the Pusher Beams wizard.

Creating our application endpoints

Now that we have done all the necessary setting up, let’s start creating our endpoints.

Login endpoint

We do not have to create the endpoint for handling login as Laravel Passport handles that for us. When we registered the Laravel Passport routes in AuthServiceProvider we registered some routes to our application.

The one we are interested in is /oauth/token. When you send a POST request to the /oauth/token endpoint with the request body: username, password, grant_type, client_id and client_secret, we will get a token back for the user.

Signup endpoint

To create the signup endpoint, first open the routes/api.php file and replace the contents with the following:

    <?php // File: ./routes/api.php

    Route::group(['middleware' => 'auth:api'], function () {
        Route::post('/chatkit/token', 'ChatkitController@getToken');
        Route::post('/rooms/sent_message', 'ChatkitController@sentMessage');
        Route::get('/rooms/joinable', 'ChatkitController@getJoinableRooms');
        Route::post('/rooms/add', 'ChatkitController@addToRoom');
        Route::get('/rooms', 'ChatkitController@getJoinedRooms');
        Route::post('/rooms', 'ChatkitController@createRoom');
    });

    Route::post('/users/signup', 'UserController@create');

Above, we have defined all the routes the application will need to function. Now, let’s create the handlers in our controllers.

In your terminal, run the following command to create a UserController:

    $ php artisan make:controller UserController

In the generated controller, app/Http/Controllers, replace the contents with the following:

    <?php // File: ./app/Http/Controllers/UserController.php

    namespace App\Http\Controllers;

    use App\User;
    use App\Chatkit;
    use Illuminate\Http\Request;

    class UserController extends Controller
    {
        public function create(Request $request, Chatkit $chatkit)
        {
            $data = $request->validate([
                'name'     => 'required|string|max:255',
                'password' => 'required|string|min:6',
                'email'    => 'required|string|email|max:255|unique:users',
            ]);

            $data['chatkit_id'] = str_slug($data['email'], '_');

            $response = $chatkit->createUser([
                'id'   => $data['chatkit_id'],
                'name' => $data['name']
            ]);

            if ($response['status'] !== 201) {
                return response()->json(['status' => 'error'], 400);
            }

            return response()->json(User::create($data));
        }
    }

Above, we have a single method, create, in the UserController. This method first validates the submitted data, then creates a new Chatkit user, then finally creates a new user in the database.

Chatkit token endpoint

The next endpoint we want to create is the Chatkit token endpoint. This endpoint will simply generate a Chatkit token that the user can use to make authenticated requests directly to Chatkit from the iOS application.

In the terminal, run the following command to create a ChatkitController:

    $ php artisan make:controller ChatkitController

Next, in the generated controller, replace the contents with the following:

    <?php // File: ./app/Http/Controllers/ChatkitController.php

    namespace App\Http\Controllers;

    use App\User;
    use App\Room;
    use App\Chatkit;
    use Illuminate\Http\Request;

    class ChatkitController extends Controller
    {
        protected $chatkit;

        public function __construct(Chatkit $chatkit)
        {
            $this->chatkit = $chatkit;
        }

        public function getToken()
        {
            $user = auth()->user();

            $response = (array)$this->chatkit->authenticate([
                'user_id' => $user->chatkit_id
            ]);

            $data = array_merge($response['body'], [
                'user' => $user->toArray(),
                'user_id' => $user->id,
                'chatkit_id' => $user->chatkit_id
            ]);

            return response()->json($data, $response['status']);
        }
    }

Above, we have the ChatkitController and we have a method called getToken. This method simply fetches the token for the current user and returns it as a response to the client. It also returns the associated user.

Rooms endpoint

The next set of endpoints we will create will be related to the rooms. We will create endpoints to create a room, join a room, get rooms already joined, and get rooms that the user can join.

Open the ChatkitController and paste the following:

    // File: ./app/Http/Controllers/ChatkitController.php

    public function getJoinedRooms(Request $request)
    {
        $rooms = $request->user()
            ->rooms()
            ->without('users')
            ->orderBy('channel', 'ASC')
            ->get()
            ->toArray();

        return response()->json($rooms);
    }

    public function getJoinableRooms()
    {
        $rooms = (array)$this->chatkit->rooms()['body'] ?? [];

        return response()->json($rooms);
    }

    public function createRoom(Request $request)
    {
        $me = $request->user();

        $data = $request->validate([
            'email' => "required|exists:users|not_in:{$me->email}"
        ]);

        $friend = User::whereEmail($data['email'])->first();

        $room_name = str_random();

        $chatkitRoom = $this->chatkit->createRoom([
            'private' => true,
            'name' => $room_name,
            'creator_id' => $request->user()->chatkit_id,
            'user_ids' => [$me->chatkit_id, $friend->chatkit_id]
        ]);

        $room = Room::create([
            'channel' => false,
            'name' => $room_name,
            'chatkit_room_id' => $chatkitRoom['body']['id']
        ]);

        $room->users()->saveMany([$me, $friend]);

        return response()->json(
            array_merge($room->toArray(), ['name' => $friend->name])
        );
    }

    public function addToRoom(Request $request)
    {   
        $data = $request->validate([
            'name' => 'required',
            'room_id' => 'required',
        ]);

        $me = $request->user();

        $response = $this->chatkit->addUsersToRoom([
            'room_id' => $data['room_id'], 
            'user_ids' => [$me->chatkit_id]
        ]);

        if ($response['status'] == 204) {
            $room = Room::firstOrCreate(
                ['chatkit_room_id' => $data['room_id']],
                ['channel' => true, 'name' => $data['name']]
            );

            if ($room->users()->whereUserId($me->id)->count() === 0) {
                $room->users()->save($me);
            }
        }

        return response()->json([], $response['status']);
    }

Above we have the following methods:

  • getJoinedRooms - get the rooms that have been joined by the user.
  • getJoinableRooms - get the rooms that can be joined by the user.
  • createRoom - creates a new private room between two users so they can message each other.
  • addToRoom - adds the current user to a room.

The sent message endpoint

This endpoint will be responsible for sending push notifications to devices when a message has been successfully posted to a room. In other words, every time a message is posted to a room, we will hit this endpoint to send push notifications to all users in the room.

Open the ChatkitController and add the following method to the file:

    // File: ./app/Http/Controllers/ChatkitController.php
    public function sentMessage(Request $request)
    {
        $data = $request->validate([
            'message' => 'required|string',
            'chatkit_room_id' => 'required|exists:rooms',
        ]);

        $interest = (string) $data['chatkit_room_id'];

        $title = "New message from {$request->user()->name}";

        $room = Room::whereChatkitRoomId($data['chatkit_room_id'])->first();

        $response = (array) app('push_notifications')->publish([$interest], [
            'apns' => [
                'aps' => [
                    'alert' => [
                        'title' => $title,
                        'body' => $data['message'],
                    ],
                    'mutable-content' => 0,
                    'category' => 'pusher',
                    'sound' => 'default'
                ],
                'data' => ['room' => $room],
            ],
        ]);

        return response()->json($response);
    }

Above, we have the method that sends push notifications using Pusher Beams. We use the room ID as the interest to push to. This will mean that, when we are joining a room from the client, we will subscribe to the interest which will be the Chatkit room ID.

Quickly setting up the application

Before we run the application, let’s add a function that will quickly set up everything we need. It will create the accounts necessary both on the API and on Chatkit. It will also create a General room for the accounts to use.

Open the routes/api.php and paste the following code at the bottom of the file:

    // File: ./routes/api.php
    Route::get('/setup', function (App\Chatkit $chatkit) {
        $users = collect([]);

        foreach (['Neo', 'Lena'] as $name) {
            $email = strtolower("{$name}@convey.test");
            $chatkit_id = str_slug($email, '_');

            $resp = $chatkit->createUser(['id' => $chatkit_id, 'name' => $name]);

            if ($resp['status'] === 201) {
                $users->push(
                    App\User::create([
                        'name' => $name,
                        'email' => $email,
                        'password' => 'secret',
                        'chatkit_id' => $chatkit_id,
                    ])
                );
            }
        }

        $resp = $chatkit->createRoom([
            'creator_id' => $users->first()->chatkit_id,
            'name' => 'General',
            'private' => false,
            'user_ids' => $users->pluck('chatkit_id')->toArray()
        ]);

        if ($resp['status'] === 201) {
            $room = App\Room::firstOrCreate(['name' => 'General'], [
                'channel' => true,
                'chatkit_room_id' => $resp['body']['id'],
            ]);

            foreach ($users as $user) {
                if ($room->users()->whereUserId($user->id)->count() === 0) {
                    $room->users()->save($user);
                }
            }
        }

        return [
            'status' => 'complete',
            'rooms' => $resp,
            'users' => $users->toArray(),
        ];
    });

Now let’s go on to run the application.

Running the application

At this point, we have finished creating the backend application. The next thing to do will be running the application. To do so, run the following command on your terminal:

    $ php artisan serve

Now, if you visit http://localhost:8000 you should see the Laravel welcome page. To complete the set up of the application, visit http://localhost:8000/api/setup once.

The accounts created are:

  • Neo - email: neo@convey.test and password: secret.
  • Lena - email: lena@convey.test and password: secret.

A General room is also created and both users are added to it. If you want to create new rooms, you can do so using the Pusher instance inspector in the dashboard.

Testing the application with push notifications

If you want to test the application with push notifications also, you cannot do so using the localhost address as you will have to run the application on a physical device.

To tunnel your localhost address to a web-accessible URL, you will need to download and install ngrok on your machine. When you have ngrok installed, make sure your Artisan server is still running then in another terminal window, run the following command:

    $ ngrok http 8000 # Or /path/to/ngrok http 8000 if you don't set an alias for it

This will create the tunnel to your localhost and give you a web-accessible URL like we see above. Use the HTTPS URL as the API base URL in the iOS application we will create in the next part.

Conclusion

In this part of the tutorial, we have been able to create the backend for our application. This backend will also be responsible for sending out push notifications anytime a message is sent in a chat room. In the next part, we will create the iOS messenger application that will connect to this backend.

The source code for this application is available on GitHub.

Clone the project repository
  • Chat
  • iOS
  • Laravel
  • Beams
  • PHP
  • Social
  • Social Interactions
  • Swift
  • Chatkit
  • Beams

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.