Create a cryptocurrency tracking app with push notifications using Swift and Laravel - Part 1: The backend

Introduction

Introduction

Cryptocurrency has been and is still one of the biggest trends this year. With currencies like Bitcoin reaching record highs and new companies creating tokens and offerings, it’s showing just how much potential cryptocurrencies have. However, cryptocurrency prices are erratic and can fall or climb at a moments notice, so it’s always a good idea to keep tabs on the changes.

In this article, we will be building an application that keeps tabs on changes to the crypto market. The application will focus on BTC and ETH and will allow users of the application to set minimum and maximum amounts when they would like to be notified about the coins current price. The application will be built using Swift, Laravel, Pusher Channels, and Pusher Beams.

Prerequisites

To follow along you need the following requirements:

What we will be building

We will start out by building the backend of the application using Laravel. Then we will build the iOS application using Swift. If you want to test the push notifications then you will need to run the application on a live device.

How the client application will work

For the client app, the iOS application, we will create a simple list that will display the available currencies and the current prices to the dollar. Whenever the price of the cryptocurrency changes, we will trigger an event using Pusher Channels so the prices are updated.

From the application, you will be able to set a minimum and maximum price change when you want to be alerted. For instance, you can configure the application to send a push notification to the application when the price of one Etherium (ETH) goes below $500. You can also configure the application to receive a notification when the price of Bitcoin goes above $5000.

How the backend application will work

For the backend application, we will be using Laravel and we will create endpoints that allow a user update the settings and load the settings for a device. The API will be responsible for checking the current prices of the cryptocurrency and sending both a Channels update and a Beams notification when the price changes.

However, because the prices don’t change very predictably, we will be simulating the currency changes so we can preview the application in action. We will also be using task scheduling in Laravel to trigger the checks for the current currency prices.

In a production environment we will set the scheduler as a cronjob, but because we are in development, we will manually run the command to trigger price changes.

How the application will look

When we are done with the application, here's how the application will look:

ios-cryptocurrency-part-1-demo

Let’s get started.

Setting up Pusher Beams and Channels

Setting up Pusher Channels

Log in to your Pusher dashboard. If you don’t have an account, create one. Your dashboard should look like this:

ios-cryptocurrency-part-1-keys

Create a new Channels app. You can easily do this by clicking the big Create new Channels app card at the bottom right. When you create a new app, you are provided with keys. Keep them safe as you will soon need them.

Setting up Pusher Beams

Next, log in to the new Pusher dashboard, in here we will create a Pusher Beams instance. You should sign up if you don’t have an account yet. Click on the Beams button on the sidebar then click Create, this will launch a pop up to Create a new Beams instance. Name it cryptoalat.

ios-cryptocurrency-part-1-new-beams

As soon as you create the instance, you will be presented with a quickstart guide. Select the IOS quickstart and follow through the wizard.

ios-cryptocurrency-part-1-beams-quickstart

When you are done creating the Beams application, you will be provided with an instance ID and a secret key, we will need these later.

Setting up your backend application

In your terminal, run the command below to create a new Laravel project:

    $ laravel new cryptoapi

This command will create a new Laravel project and install all the required Laravel dependencies.

Next, let’s install some of the project specific dependencies. Open the composer.json file and in the require property, add the following dependencies:

1// File: composer.json
2    "require": {
3        [...]
4        
5        "neo/pusher-beams": "^1.0",
6        "pusher/pusher-php-server": "~3.0"
7    },

Now run the command below to install these dependencies.

    $ composer update

When the installation is complete, open the project in a text editor of your choice. Visual Studio Code is pretty nice.

Setting up our Pusher Beams library

The first thing we want to do is set up the Pusher Beams library we just pulled in using composer. To set up, open the .env file and add the following keys:

1PUSHER_BEAMS_SECRET_KEY="PUSHER_BEAMS_SECRET_KEY"
2    PUSHER_BEAMS_INSTANCE_ID="PUSHER_BEAMS_INSTANCE_ID"

You should replace the PUSHER_BEAMS_* placeholders with the keys you got when setting up your Beams application.

Next, open the config/broadcasting.php file and scroll until you see the connections key. In there, you’ll have the pusher settings, add the following to the pusher configuration:

1'pusher' => [
2        // [...]
3    
4        'beams' => [
5            'secret_key' => env('PUSHER_BEAMS_SECRET_KEY'),
6            'instance_id' => env('PUSHER_BEAMS_INSTANCE_ID'),
7        ],
8    ],

Setting up our Pusher Channels library

The next step is to set up Pusher Channels. Laravel comes with native support for Pusher Channels so we do not need to do much to set it up.

Open the .env file and update the following keys below:

1BROADCAST_DRIVER=pusher
2    
3    // [...]
4    
5    PUSHER_APP_ID="PUSHER_APP_ID"
6    PUSHER_APP_KEY="PUSHER_APP_KEY"
7    PUSHER_APP_SECRET="PUSHER_APP_SECRET"
8    PUSHER_APP_CLUSTER="PUSHER_APP_CLUSTER"

Above you set the BROADCAST_DRIVER to pusher and then for the other PUSHER_APP_* keys, replace the placeholders with the keys gotten from your Pusher dashboard. That’s all we need to do to set up Pusher Channels for this application.

Building the backend application

Now that we have set up all the dependencies, we can start building the application. We will start by creating the routes. However, instead of creating controllers to hook into the routes, we will be adding the logic directly to the routes.

Setting up the database, migration, and model

Since we will be working with a database, we need to set up the database we are going to be working with. To make things easy we will be using SQLite. Create an empty database.sqlite file in the database directory.

Open the .env file and replace:

1DB_CONNECTION=mysql
2    DB_HOST=127.0.0.1
3    DB_PORT=3306
4    DB_DATABASE=homestead
5    DB_USERNAME=homestead
6    DB_PASSWORD=secret

With

1DB_CONNECTION=sqlite
2    DB_DATABASE=/full/path/to/your/database.sqlite

Next, let’s create a migration for the devices table. We will use this table to store devices and their notification settings. This will help us know what devices to send push notifications to.

Run the command below to create the migration and model:

    $ php artisan make:model Device -m

The -m flag will instruct artisan to create a migration alongside the model.

This command will generate two files, the migration file in the database/migrations and the model in the app directory. Let’s edit the migration file first.

Open the *_create_devices_table.php migration file in the database/migrations directory and replace the contents with the following:

1<?php
2    
3    use Illuminate\Support\Facades\Schema;
4    use Illuminate\Database\Schema\Blueprint;
5    use Illuminate\Database\Migrations\Migration;
6    
7    class CreateDevicesTable extends Migration
8    {
9        /**
10         * Run the migrations.
11         *
12         * @return void
13         */
14        public function up()
15        {
16            Schema::create('devices', function (Blueprint $table) {
17                $table->increments('id');
18                $table->string('uuid')->unique();
19                $table->float('btc_min_notify')->default(0);
20                $table->float('btc_max_notify')->default(0);
21                $table->float('eth_min_notify')->default(0);
22                $table->float('eth_max_notify')->default(0);
23            });
24        }
25        
26        /**
27         * Reverse the migrations.
28         *
29         * @return void
30         */
31        public function down()
32        {
33            Schema::dropIfExists('devices');
34        }
35    }

In the up method, we have defined the structure of the devices table. We have the uuid field which will be a unique string for each device registered. We have two btc_notify fields which are there to save the minimum and maximum prices of BTC at which point the device should be notified. Same applies to the* eth_*_notify fields.

To run the migration, run the command below:

    $ php artisan migrate

Open the app/Device.php model and replace the contents with the code below:

1<?php
2    namespace App;
3    
4    use Illuminate\Database\Eloquent\Model;
5    use Illuminate\Notifications\Notifiable;
6    
7    class Device extends Model
8    {
9        use Notifiable;
10    
11        public $timestamps = false;
12        
13        protected $fillable = [
14            'uuid', 
15            'btc_min_notify', 
16            'btc_max_notify', 
17            'eth_min_notify', 
18            'eth_max_notify',
19        ];
20        
21        protected $cast = [
22            'btc_min_notify' => 'float',
23            'btc_max_notify' => 'float',
24            'eth_min_notify' => 'float',
25            'eth_max_notify' => 'float'
26        ];
27        
28        public function scopeAffected($query, string $currency, $currentPrice)
29        {
30            return $query->where(function ($q) use ($currency, $currentPrice) {
31                $q->where("${currency}_min_notify", '>', 0)
32                  ->where("${currency}_min_notify", '>', $currentPrice);
33            })->orWhere(function ($q) use ($currency, $currentPrice) {
34                $q->where("${currency}_max_notify", '>', 0)
35                  ->where("${currency}_max_notify", '<', $currentPrice);
36            });
37        }
38    }

In the model above, we have set the $timestamps property to false to make sure that Eloquent does not try to update the created_at and updated_at fields, which is the normal behavior.

We also have the scopeAffected method which is an example of an Eloquent scope. We use this to get the affected devices after a price change has occurred on a currency. So if, for instance, BTC’s price drops, this method will check the devices and the settings to see the devices that need to be notified of this change.

Local scopes allow you to define common sets of constraints that you may easily re-use throughout your application. For example, you may need to frequently retrieve all users that are considered "popular". To define a scope, prefix an Eloquent model method with scope. - Laravel documentation.

We will use this scope later in our application when we need to know what devices to send push notifications to.

Creating the routes

Open the routes/api.php file and replace the contents of the file with the following code:

1// File: routes/api.php
2    <?php
3    
4    use App\Device;
5    use Illuminate\Http\Request;

Next, let’s add the first route. Append the code below to the routes file:

1// File: routes/api.php
2    Route::get('/settings', function (Request $request) {
3        return Device::whereUuid($request->query('u'))->firstOrFail()['settings'];
4    });

In the route above, we are returning the settings for the device supplied in the u query parameter. This means if a registered device hits the /settings endpoint and passes the device UUID through the u parameter, the settings for that device will be returned.

Next, in the same routes file, paste the following at the bottom of the file:

1Route::post('/settings', function (Request $request) {
2        $settings = $request->validate([
3            'btc_min_notify' => 'int|min:0',
4            'btc_max_notify' => 'int|min:0',
5            'eth_min_notify' => 'int|min:0',
6            'eth_max_notify' => 'int|min:0',
7        ]);
8    
9        $settings = array_filter($settings, function ($value) { return $value > 0; });
10    
11        $device = Device::firstOrNew(['uuid' => $request->query('u')]);
12        $device->fill($settings);
13        $saved = $device->save();
14        
15        return response()->json([
16            'status' => $saved ? 'success' : 'failure'
17        ], $saved ? 200 : 400);
18    });

Above, we have defined the route for the POST /settings route. This route saves settings to the database. It will create a new entry if the setting does not already exist or will update the existing one if it does.

That’s all for the routes.

Creating the jobs, events, and notifiers

Next, we need to create the Laravel job that will run at intervals to check if there is a change in the currency price.

Run the command below to create a new Laravel job:

    $ php artisan make:job CheckPrices

This will create a new CheckPrices class in the app directory. Open that class and replace the contents with the following:

1<?php
2    
3    namespace App\Jobs;
4    
5    use App\Device;
6    use Illuminate\Bus\Queueable;
7    use Illuminate\Queue\SerializesModels;
8    use Illuminate\Queue\InteractsWithQueue;
9    use Illuminate\Contracts\Queue\ShouldQueue;
10    use Illuminate\Foundation\Bus\Dispatchable;
11    use App\Events\CurrencyUpdated;
12    use App\Notifications\CoinPriceChanged;
13    
14    class CheckPrices implements ShouldQueue
15    {
16        use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
17        
18        protected $supportedCurrencies = ['ETH', 'BTC'];
19    
20        /**
21         * Execute the job.
22         *
23         * @return void
24         */
25        public function handle()
26        {
27            $payload = $this->getPricesForSupportedCurrencies();
28    
29            if (!empty($payload)) {
30                $this->triggerPusherUpdate($payload);
31                $this->triggerPossiblePushNotification($payload);
32            }
33        }
34        
35        private function triggerPusherUpdate($payload)
36        {
37            event(new CurrencyUpdated($payload));
38        }
39        
40        private function triggerPossiblePushNotification($payload)
41        {
42            foreach ($this->supportedCurrencies as $currency) {
43                $currentPrice = $payload[$currency]['current'];
44                
45                $currency = strtolower($currency);
46    
47                foreach (Device::affected($currency, $currentPrice)->get() as $device) {
48                    $device->notify(new CoinPriceChanged($currency, $device, $payload));
49                }
50            }
51        }
52        
53        public function getPricesForSupportedCurrencies(): array
54        {
55            $payload = [];
56    
57            foreach ($this->supportedCurrencies as $currency) {
58                if (config('app.debug') === true) {
59                    $response = [
60                        $currency => [
61                            'USD' => (float) rand(100, 15000)
62                        ]
63                    ];
64                } else {
65                    $url = "https://min-api.cryptocompare.com/data/pricehistorical?fsym={$currency}&tsyms=USD&ts={$timestamp}";
66                    
67                    $response = json_decode(file_get_contents($url), true);
68                }
69    
70                if (json_last_error() === JSON_ERROR_NONE) {
71                    $currentPrice = $response[$currency]['USD'];
72    
73                    $previousPrice = cache()->get("PRICE_${currency}", false);
74    
75                    if ($previousPrice == false or $previousPrice !== $currentPrice) {
76                        $payload[$currency] = [
77                            'current' => $currentPrice,
78                            'previous' => $previousPrice,
79                        ];
80                    }
81    
82                    cache()->put("PRICE_${currency}", $currentPrice, (24 * 60 * 60));
83                }
84            }
85    
86            return $payload;
87        }
88    }

In the class above, we implement the ShouldQueue interface. This makes it so that the job can and will be queued. In a production server, queueing jobs makes your application faster as it queues jobs that might take a while to execute for later execution.

We have four methods in this class. The first one is the handle method. This one is called automatically when the job is executed. In this method, we fetch the prices for the available currencies and then check if the price has changed. If it has, we publish a Pusher Channel event and then check if there are any devices that need to be notified based on the user’s settings. If there are any, we send a push notification to that device.

We have the triggerPusherUpdate method which triggers a CurrencyUpdated event. We will create this event in the next section. We also have a triggerPossiblePushNotification method which gets the list of devices which should be notified of the currency change and then notifies the user using the CoinPriceChanged class, which we will create in the next section.

Lastly, we have the getPricesForSupportedCurrencies method which just fetches the current price of a currency. In this method, we have a debug mode that simulates the current price of a currency.

To make sure this class we just created is scheduled properly, open the app/Console/Kernel.php file and in the schedule method, add the following code to the schedule method:

    $schedule->job(new \App\Jobs\CheckPrices)->everyMinute();

Now every time we run the command php artisan schedule:run all the jobs in this schedule method will be run. Normally, in a production environment, we will need to add the schedule command as a cronjob, however, we will run this command manually.

The next thing to do will be to create the notifiers and events. In your terminal, run the following commands:

1$ php artisan make:event CurrencyUpdated
2    $ php artisan make:notification CoinPriceChanged

This will create a class in the Events and Notifications directories.

In the event class, CurrencyUpdated paste the following code:

1<?php
2    
3    namespace App\Events;
4    
5    use Illuminate\Broadcasting\Channel;
6    use Illuminate\Queue\SerializesModels;
7    use Illuminate\Foundation\Events\Dispatchable;
8    use Illuminate\Broadcasting\InteractsWithSockets;
9    use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
10    
11    class CurrencyUpdated implements ShouldBroadcast
12    {
13        use Dispatchable, InteractsWithSockets, SerializesModels;
14    
15        public $payload;
16        
17        public function __construct($payload)
18        {
19            $this->payload = $payload;
20        }
21    
22        public function broadcastOn()
23        {
24            return new Channel('currency-update');
25        }
26        
27        public function broadcastAs()
28        {
29            return 'currency.updated';
30        }
31    }

In the event class above, we have the broadcastOn method that specifies the Pusher channel we want to broadcast an event on. We also have the broadcastAs method which specifies the name of the event we want to broadcast to the channel.

In the CoinPriceChanged notification class, replace the contents with the following code:

1<?php
2    
3    namespace App\Notifications;
4    
5    use App\Device;
6    use Illuminate\Bus\Queueable;
7    use Neo\PusherBeams\PusherBeams;
8    use Neo\PusherBeams\PusherMessage;
9    use Illuminate\Notifications\Notification;
10    
11    class CoinPriceChanged extends Notification
12    {
13        use Queueable;
14        
15        private $currency;
16        private $device;
17        private $payload;
18        
19        public function __construct(string $currency, Device $device, array $payload)
20        {
21            $this->currency = $currency;
22            $this->device = $device;
23            $this->payload = $payload;
24        }
25        
26        public function via($notifiable)
27        {
28            return [PusherBeams::class];
29        }
30        
31        public function toPushNotification($notifiable)
32        {
33            $currentPrice = $this->payload[strtoupper($this->currency)]['current'];
34            
35            $previousPrice = $this->payload[strtoupper($this->currency)]['current'];
36    
37            $direction = $currentPrice > $previousPrice ? 'climbed' : 'dropped';
38    
39            $currentPriceFormatted = number_format($currentPrice);
40    
41            return PusherMessage::create()
42                    ->iOS()
43                    ->sound('success')
44                    ->title("Price of {$this->currency} has {$direction}")
45                    ->body("The price of {$this->currency} has {$direction} and is now \${$currentPriceFormatted}");
46        }
47        
48        public function pushNotificationInterest()
49        {
50            $uuid = strtolower(str_replace('-', '_', $this->device->uuid));
51            
52            return "{$uuid}_{$this->currency}_changed";
53        }
54    }

In the class above we have the toPushNotification class which prepares the push notification using the Pusher Beams library. We also have the pushNotificationInterest method which sets the name for the interest of the push notification depending on the currency and device ID.

That’s all for the backend, now just run the command below to start the server:

    $ php artisan serve

This will start a PHP server with our application running. Also if you need to manually trigger a currency change, run the command below:

    $ php artisan schedule:run

Now that we are done with the backend, we can create the application using Swift and Xcode.

Conclusion

In this part of the article, we have created the backend for our cryptocurrency alert application. In the next part, we will be seeing how we can create the application that will consume the API we just created in this part.

The source code to this application is available on GitHub.