Posted on 4 Comments

Moving The ChatGPT Laravel API Into Composer – Creating My First Laravel 10 Package

I Created A Laravel 10 OpenAI ChatGPT Composer Package

In my last tutorial, I created a Laravel site that featured an OpenAI ChatGPT API. This was very fun to create and while I was thinking of ways to improve it, the idea dawned upon me to make it a Composer package and share it with the world. This took less time than I expected honestly and I already have integrated my package into a few applications (it feels good to composer require your own shit!).

What’s The Benefit?

1. Reusability

I know for a fact that I will be using OpenAI in a majority of my projects going forward, instead of rewriting functionality over and over and over, I’m going to package up the functionality that I know I will need every time.

2. Modularity

Breaking my code into modules allows me to think about my applications from a high-level view. I call it Lego Theory; all of my modules are legos and my app is the lego castle I’m building.

3. Discoverability

Publishing packages directly helps my brand via discoverability. If I produce high-quality, in-demand open-source software then more people will use it, and the more people that use it then the more people know who I am. This helps me when I am doing things like applying for contracts or conference talks.

Creating The Code

Scaffold From Spatie

The wonderful engineers at Spatie have created a package skeleton for Laravel and is what I used as a starting point for my Laravel package. If you are using GitHub you can use this repo as a template or if using the command line enter the following command:

git clone git@github.com:spatie/package-skeleton-laravel.git laravel-open-api

There is a configure.php script that will set all of the placeholder values with the values you provide for your package

php ./configure.php

Now we can get to the nitty gritty.

Listen To Some Hacker Music While You Code

Follow me on Spotify I make Tech Trap music

The Front-Facing Object

After running the configure script you will have a main object that will be renamed, in my case it was called LaravelOpenaiApi.php and it looks like this:

<?php

namespace Mastashake\LaravelOpenaiApi;

use OpenAI\Laravel\Facades\OpenAI;
use Mastashake\LaravelOpenaiApi\Models\Prompt;

class LaravelOpenaiApi
{
  function generateResult(string $type, array $data): Prompt {
    switch ($type) {
      case 'text':
      return $this->generateText($data);
      case 'image':
      return $this->generateImage($data);
    }
  }

  function generateText($data) {
    $result = OpenAI::completions()->create($data);
    return $this->savePrompt($result, $data);
  }

  function generateImage($data) {
    $result = OpenAI::images()->create($data);
    return $this->savePrompt($result, $data);
  }

  private function savePrompt($result, $data): Prompt {
    $prompt = new Prompt([
      'prompt_text' => $data['prompt'],
      'data' => $result
    ]);
    return $prompt;
  }
}

It can generate text and images and save the prompts, it looks at the type provided to determine what resource to generate. It’s all powered by the OpenAI Laravel Facade.

The Migration

The default migration will be edited to use the prompts migration from the Laravel API tutorial, open it up and replace the contents with the following:

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('prompts', function (Blueprint $table) {
            $table->id();
            $table->string('prompt_text');
            $table->json('data');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('prompts');
    }
};

The Model

Create a file called src/Models/Prompt.php and copy the old Prompt code inside

<?php

namespace Mastashake\LaravelOpenaiApi\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Prompt extends Model
{
    use HasFactory;

    protected $guarded = [];
    protected $casts = [
      'data' => 'array'
    ];
}

The Controller

For the controllers, we have to create a BaseController and a PromptController. Create a file called src/Http/Controllers/BaseController.php

<?php

namespace Mastashake\LaravelOpenaiApi\Http\Controllers;

use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

class Controller extends BaseController
{
    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

Now we will create our PromptController and inherit from the BaseController

<?php

namespace Mastashake\LaravelOpenaiApi\Http\Controllers;
use Illuminate\Http\Request;
use Mastashake\LaravelOpenaiApi\LaravelOpenaiApi;
class PromptController extends Controller
{
    //
    function generateResult(Request $request) {

      $ai = new LaravelOpenaiApi();
      $prompt = $ai->generateResult($request->type, $request->except(['type']));
      return response()->json([
        'data' => $prompt
      ]);
    }
}

OpenAI and ChatGPT can generate multiple types of responses, so we want the user to be able to choose which type of resource they want to generate then pass on that data to the underlying engine.

The Route

Create a routes/api.php file to store our api route:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::group(['prefix' => '/api'], function(){
  if(config('openai.use_sanctum') == true){
    Route::middleware(['api','auth:sanctum'])->post(config('openai.api_url'),'Mastashake\LaravelOpenaiApi\Http\Controllers\PromptController@generateResult');
  } else {
    Route::post(config('openai.api_url'),'Mastashake\LaravelOpenaiApi\Http\Controllers\PromptController@generateResult');
  }
});

Depending on the values in the config file (we will get to it in a second calm down) the user may want to use Laravel Sanctum for token-based authenticated requests. In fact, I highly suggest you do if you don’t want your token usage abused, but for development and testing, I suppose it’s fine. I made it this way to make it more robust and extensible.

The Config File

Create a file called config/openai.php that will hold the default config values for the package. This will be published into any application that you install this package in:

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | OpenAI API Key and Organization
    |--------------------------------------------------------------------------
    |
    | Here you may specify your OpenAI API Key and organization. This will be
    | used to authenticate with the OpenAI API - you can find your API key
    | and organization on your OpenAI dashboard, at https://openai.com.
    */

    'api_key' => env('OPENAI_API_KEY'),
    'organization' => env('OPENAI_ORGANIZATION'),
    'api_url' => env('OPENAI_API_URL') !== null ? env('OPENAI_API_URL') : '/generate-result',
    'use_sanctum' => env('OPENAI_USE_SANCTUM') !== null ? env('OPENAI_USE_SANCTUM') == true : false

];
  • The api_key variable is the OpenAI API key
  • The organization variable is the OpenAI organization if one exists
  • The api_url variable is the user-defined URL for the API routes, if one is not defined then use /api/generate-result
    • The use_sanctum variable defines if the API will use auth:sanctum middleware.

The Command

The package includes an artisan command for generating results from the command line. Create a file called src/Commands/LaravelOpenaiApiCommand.php

<?php

namespace Mastashake\LaravelOpenaiApi\Commands;

use Illuminate\Console\Command;
use Mastashake\LaravelOpenaiApi\LaravelOpenaiApi;
class LaravelOpenaiApiCommand extends Command
{
    public $signature = 'laravel-openai-api:generate-result';

    public $description = 'Generate Result';

    public function handle(): int
    {
        $data = [];
        $suffix = null;
        $n = 1;
        $temperature = 1;
        $displayJson = false;
        $max_tokens = 16;
        $type = $this->choice(
            'What are you generating?',
            ['text', 'image'],
            0
        );
        $prompt = $this->ask('Enter the prompt');
        $data['prompt'] = $prompt;

        if($type == 'text') {
          $model = $this->choice(
              'What model do you want to use?',
              ['text-davinci-003', 'text-curie-001', 'text-babbage-001', 'text-ada-001'],
              0
          );
          $data['model'] = $model;
          if ($this->confirm('Do you wish to add a suffix to the generated result?')) {
              //
              $suffix = $this->ask('What is the suffix?');
          }
          $data['suffix'] = $suffix;

          if ($this->confirm('Do you wish to set the max tokens used(defaults to 16)?')) {
            $max_tokens = (int)$this->ask('Max number of tokens to use?');
          }
          $data['max_tokens'] = $max_tokens;

          if ($this->confirm('Change temperature')) {
            $temperature = (float)$this->ask('What is the temperature(0-2)?');
            $data['temperature'] = $temperature;
          }
        }

        if ($this->confirm('Multiple results?')) {
          $n = (int)$this->ask('Number of results?');
          $data['n'] = $n;
        }

        $displayJson = $this->confirm('Display JSON results?');

        $ai = new LaravelOpenaiApi();
        $result = $ai->generateResult($type,$data);

        if ($displayJson) {
          $this->comment($result);
        }
        if($type == 'text') {
          $choices = $result->data['choices'];
          foreach($choices as $choice) {
            $this->comment($choice['text']);
          }
        } else {
          $images = $result->data['data'];
          foreach($images as $image) {
            $this->comment($image['url']);
          }
        }


        return self::SUCCESS;
    }
}

I’m going to add more inputs later, but for now, this is a good starting point to get back data. I tried to make it as verbose as possible, I’m always welcoming PRs if you want to add functionality 🙂

The Service Provider

All Laravel packages must have a service provider, open up the default one in the root directory, in my case it was called LaravelOpenaiApiServiceProvider

<?php

namespace Mastashake\LaravelOpenaiApi;

use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
use Mastashake\LaravelOpenaiApi\Commands\LaravelOpenaiApiCommand;
use Spatie\LaravelPackageTools\Commands\InstallCommand;
class LaravelOpenaiApiServiceProvider extends PackageServiceProvider
{
    public function configurePackage(Package $package): void
    {
        /*
         * This class is a Package Service Provider
         *
         * More info: https://github.com/spatie/laravel-package-tools
         */
        $package
            ->name('laravel-openai-api')
            ->hasConfigFile(['openai'])
            ->hasRoute('api')
            ->hasMigration('create_openai_api_table')
            ->hasCommand(LaravelOpenaiApiCommand::class)
            ->hasInstallCommand(function(InstallCommand $command) {
                $command
                    ->publishConfigFile()
                    ->publishMigrations()
                    ->askToRunMigrations()
                    ->copyAndRegisterServiceProviderInApp()
                    ->askToStarRepoOnGitHub('mastashake08/laravel-openai-api');
                  }
                );
    }
}

The name is the name of our package, next we pass in the config file created above. Of course we have to add our API routes and migrations. Lastly, we add our commands.

Testing It In Laravel Project

composer require mastashake08/laravel-openai-api

You can run that command in any Laravel project, I used it in the Laravel API tutorial I did last week. If you runphp artisan route:list and you will see the API is in your project!

Hey look mom it’s my package!!!

Check The Repo!

This is actually my first-ever Composer package! I would love feedback, stars, and PRs that would go a long way. You can check out the repo here on GitHub. Please let me know in the comments if this tutorial was helpful and share on social media.

Follow Me On Social Media

Follow Me On Youtube!

Follow my YouTube account

Get Your Next Domain Cheap & Support The Channel

I use Namecheap for all of my domains! Whenever I need a cheap solution for a proof-of-concept project I grab a domain name for as little as $1! When you sign up and buy your first domain with Namecheap I get a commission, it’s a great way to get a quality service and support this platform!

Get Your Next Domain Cheap
CLICK HERE

Become A Sponsor

Open-source work is free to use but it is not free to develop. If you enjoy my content and would like to see more please consider becoming a sponsor on Github or Patreon! Not only do you support me but you are funding tech programs for at risk youth in Louisville, Kentucky.

Join The Newsletter

By joining the newsletter, you get first access to all of my blogs, events, and other brand-related content delivered directly to your inbox. It’s 100% free and you can opt out at any time!

Check The Shop

You can also consider visiting the official #CodeLife shop! I have my own clothing/accessory line for techies as well as courses designed by me covering a range of software engineering topics.

Posted on Leave a comment

Creating A Laravel ChatGPT API

laravel + chatgpt

What Is ChatGPT?

ChatGPT is a generative AI software created by OpenAI from the official Wikipedia page

ChatGPT (Chat Generative Pre-trained Transformer[2]) is a chatbot developed by OpenAI and launched in November 2022. It is built on top of OpenAI’s GPT-3 family of large language models and has been fine-tuned (an approach to transfer learning) using both supervised and reinforcement learning techniques.

https://en.wikipedia.org/wiki/ChatGPT

Since its launch, it has taken the world by storm. People are losing their minds over the impending AI overlords destroying society and making humanity its slave 😂

Seriously though we are talking BILLIONS of weights across MILLIONS of neurons I geek out every time I think about it. I keep getting asked what my thoughts are on ChatGPT and instead of repeating myself for the 99934394398th time, I decided to write a blog and do a Laravel code tutorial to show developers just HOW EASY it is to use this OpenAI software.

Use Cases For ChatGPT

Asset Generation

With ChatGPT you can generate images This was first made popular with the DALL-E project. AI-generated art is on the rise and there are many opportunities to be had. Images aren’t the end though, with ChatGPT you can generate any asset. Think about how this will affect gaming! You can generate 3D assets, audio assets, texture assets, and more.

Code Generation

In my opinion, this is where ChatGPT shines. The Codex project allows you to use ChatGPT to generate code, and the results are scarily amazing. If you are a solo developer you can leverage the power of artificial intelligence to speed run through proof of concepts. I have seen videos of people programming whole apps with ChatGPT

Text Generation

Using ChatGPT you can generate text. Many companies are integrating ChatGPT to create contextual accurate text responses. One of my favorite integrations of this is the Twitter bot ChatGPTBot. However, some people are scared of this technology such as the Rabbi who used AI to create a sermon. I personally think e-commerce will be dominated by AI-driven product descriptions.

The Sky Is The Limit

Microsoft has already integrated ChatGPT into Bing and Google is working on a rival application called Bard. The beautiful thing about this technology is that it is only limited to our imagination. We don’t even know the full scope of what ChatGPT is capable of. This is a perfect storm for people to get first movers advantage on this gold rush that is coming over the next decade.

How I Plan On Using ChatGPT

Mobisnacks

Download Mobisnacks!

I have integrated ChatGPT into MobiSnacks to create product descriptions for chefs. The chefs can put in keywords and ChatGPT spits out 3 descriptions for the chefs to use as a starting point. The next step is to use ChatGPT to generate contextual ads for the platform and for the chefs as an additional service.

GPT Audiobook

GPT Audiobook logo

I created a proof of concept called GPT Audiobook. It uses ChatGPT to create audiobooks and spits them out as SSML documents for text-to-speech software to read. I’m currently creating an Android and iOS app to go with the web app. In the future, I plan on adding rich structured data snippets to display the books on Google and other search engines. Even the logo for GPT Audiobook was MADE WITH CHATGPT!

The Laravel ChatGPT API

Overview

The Laravel API will be very simple: one route, one model, and one controller. The model will be called Prompt a prompt will have two fields, prompt_text and data. The controller will have one method called generateResult that will use the OpenAI SDK to communicate with ChatGPT and generate the result. Finally, there will be a POST route called /generate-result which saves the model and returns the JSON.

Listen To Some Hacker Music While You Code

Follow me on Spotify I make Tech Trap music

Creating The Application

For this tutorial, I am using a Mac with docker. To start open up the tutorial and create a new Laravel application

curl -s "https://laravel.build/laravel-chat-gpt-api" | bash

Afterward cd into the application and add the OpenAI Laravel package, which will power our ChatGPT logic.

composer require openai-php/laravel

This is the only composer requirement we will need for this tutorial. Now we need to do our configuration for OpenAI.

Configuring The OpenAI SDK

The OpenAI Laravel package comes with some config files that need to be published before we get started coding. In your terminal paste the following command

php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider"

This will create a config/openai.php configuration file in your project, which you can modify to your needs using environment variables: You need to retrieve your OpenAI developer key from here and paste it in your .env file.

OPENAI_API_KEY=sk-...

Ok, that’s it for the SDK configuration.

Database & Model

The Prompt model will have a prompt_text field that will hold the text entered by the user. It will also have a data json field that holds the result from OpenAI. Let’s create the model and the migration all in one:

./vendor/bin/sail artisan make:model -m Prompt

Open up the created migration and paste in the following:

<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('prompts', function (Blueprint $table) {
            $table->id();
            $table->string('prompt_text');
            $table->json('data');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('prompts');
    }
};
 

Next open up the Prompt model and paste the following:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Prompt extends Model
{
    use HasFactory;

    protected $guarded = [];
    protected $casts = [
      'data' => 'array'
    ];
}

Next, let’s move on to creating the controller.

Creating The Controller

Generate the PromptController in the terminal:

 ./vendor/bin/sail artisan make:controller PromptController 

Open it up and let’s create our generateResult function:

<?php

namespace App\Http\Controllers;
use OpenAI\Laravel\Facades\OpenAI;
use App\Models\Prompt;
use Illuminate\Http\Request;

class PromptController extends Controller
{
    //
    function generateResult(Request $request) {

      $result = OpenAI::completions()->create($request->all());
      $prompt = new Prompt([
        'prompt_text' => $request->prompt,
        'data' => $result
      ]);
      return response()->json($prompt);
    }
}

So what’s going on here? We import the OpenAI SDK and we simply pass the $request to the completions API. If you need a reference you can check the OpenAI API reference. We then create a new prompt model pass in the text and pass in the resulting data. The last thing to do is create the route and we are done!

Creating The API Route

Open up the routes/api.php routes file and update it to call the PromptController@generateResult function

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::post('/generate-result','App\Http\Controllers\PromptController@generateResult');

Now we are done, make sure you plug in your API key and make a test request! Here is how we can test with cURL:


curl -X POST http://localhost/api/generate-result -H "Content-Type: application/json" --data-binary @- <<DATA
{
  "model": "text-davinci-003",
   "prompt" : "PHP is"
}
DATA

Next Steps

The next step that I want to do with this project is to create it as a Laravel package so developers can put an OpenAI ChatGPT API in their backends easily. Afterward, I would like to add functionality for issuing tokens and possibly even a monetization module powered by Stripe and Laravel Cashier. Please leave comments on this article and let me know what you would like to see and I will build it! You can see the GitHub repository here.

**UPDATE** I Created The Laravel Composer Package

Shortly after writing this tutorial, I went ahead and created a Composer package for the Laravel OpenAI ChatGPT API. If you want to implement this functionality from the tutorial and more then please check it out! I’m actively looking for PRs from fellow developers! I can’t wait to see how you all use and integrate this package into your web applications and business services!

You can install the package using the following command:

composer require mastashake08/laravel-openai-api 

Afterward you can publish the migrations and config files with the following commands:

php artisan vendor:publish --tag="openai-api-migrations"
php artisan migrate
php artisan vendor:publish --tag="openai-api-config"

Finally, start to use it in your code! You can access the object directly, via the included API routes, or with the interactive Artisan CLI command.

Via Code

$laravelOpenaiApi = new Mastashake\LaravelOpenaiApi();
echo $laravelOpenaiApi->generateResult($type, $data);

Via Artisan

php artisan laravel-openai-api:generate-result

Via API

You set the OPENAI_API_URL in the .env file if a value is not set then it defaults to /api/generate-result

/api/generate-result POST {openai_data}

The data object requires a type property that is either set to text or image. Depending on which type then provide the JSON referenced in the OpenAI API Reference

Text Example

{
  "type": "text",
  "prompt": "Rust is",
  "n": 1,
  "model": "text-davinci-003",
  "max_tokens": 16
}

Image Example

{
  "type": "image",
  "prompt": "A cute baby sea otter",
  "n": 1,
  "size": "1024x1024"
}

I’m going to continue to work on both this package and the demo tutorial and will update you all on the progress for sure. Thank you for taking time to read this tutorial, if you found it helpful please leave a comment and a like!

Follow Me On Social Media

Follow Me On Youtube!

Follow my YouTube account

Get Your Next Domain Cheap & Support The Channel

I use Namecheap for all of my domains! Whenever I need a cheap solution for a proof-of-concept project I grab a domain name for as little as $1! When you sign up and buy your first domain with Namecheap I get a commission, it’s a great way to get a quality service and support this platform!

Get Your Next Domain Cheap
CLICK HERE

Become A Sponsor

Open-source work is free to use but it is not free to develop. If you enjoy my content and would like to see more please consider becoming a sponsor on Github or Patreon! Not only do you support me but you are funding tech programs for at risk youth in Louisville, Kentucky.

Join The Newsletter

By joining the newsletter, you get first access to all of my blogs, events, and other brand-related content delivered directly to your inbox. It’s 100% free and you can opt out at any time!

Check The Shop

You can also consider visiting the official #CodeLife shop! I have my own clothing/accessory line for techies as well as courses designed by me covering a range of software engineering topics.

Posted on Leave a comment

Creating A Screen Recorder and Email Microservice With Vue.js + MediaRecorder API and Laravel PHP Framework

Recording Your Screen With Vue.js and MediaRecorder API

Last year I wrote a screen recording progressive web app with Vue.js and the MediaRecorder API. This was a simple app that allowed you to record your current screen and after screen sharing, a file would be created with the File API and downloaded to your system. Well I decided to update it this week and add email functionality. The reason? I needed to send a screen recording to a client and figured might as well add the functionality in the app and save time; as opposed to downloading the file then opening Gmail, then sending the email. Here is a video for the first part.

Screen recorder part 1

Adding The Email Service

Obviously, you all know I love Laravel! I decided to create a Laravel 8 API microservice with a single post route that takes the video file and email address and sends a notification to said email address. I then had to edit the Vue application to make a network call to the microservice when the user wants to email the file.

Screen recorder part 2

Getting To The Code!

Let’s start off with the Vue.js application. Create a new application in your terminal

vue create screen-recorder

The first thing we are going to do is add our dependencies, which in this case is vue-tailwind for ease of working with TailwindCSS, gtag for working with Google Analytics ( I like to know where my users are coming from), Google Adsense ( a brother gotta eat) and vue-script2.

cd screen-recorder; npm install --save vue-tailwind vue-script2 vue-gtag vue-google-adsense

After installing the dependencies, head over to main.js and let’s setup the application

import Vue from 'vue'
import App from './App.vue'
import VueTailwind from 'vue-tailwind'
import Ads from 'vue-google-adsense'
import VueGtag from "vue-gtag";
import "tailwindcss/tailwind.css"
Vue.use(VueGtag, {
  config: { id: "your google analytics id" }
});

Vue.use(require('vue-script2'))

Vue.use(Ads.Adsense)
const settings = {
  TInput: {
    classes: 'form-input border-2 text-gray-700',
    variants: {
      error: 'form-input border-2 border-red-300 bg-red-100',
      // ... Infinite variants
    }
  },
TButton: {
    classes: 'rounded-lg border block inline-flex items-center justify-center block px-4 py-2 transition duration-100 ease-in-out focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none focus:ring-opacity-50 disabled:opacity-50 disabled:cursor-not-allowed',
    variants: {
      secondary: 'rounded-lg border block inline-flex items-center justify-center bg-purple-500 border-purple-500 hover:bg-purple-600 hover:border-purple-600',
    }
  },
  TAlert: {
    classes: {
      wrapper: 'rounded bg-blue-100 p-4 flex text-sm border-l-4 border-blue-500',
      body: 'flex-grow text-blue-700',
      close: 'text-blue-700 hover:text-blue-500 hover:bg-blue-200 ml-4 rounded',
      closeIcon: 'h-5 w-5 fill-current'
    },
    variants: {
      danger: {
        wrapper: 'rounded bg-red-100 p-4 flex text-sm border-l-4 border-red-500',
        body: 'flex-grow text-red-700',
        close: 'text-red-700 hover:text-red-500 hover:bg-red-200 ml-4 rounded'
      },
      // ... Infinite variants
    }
  },
  // ... The rest of the components
}

Vue.use(VueTailwind, settings)
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

This file basically bootstraps the application with all the Google stuff and the Tailwind CSS packaging. Now let’s open up the App.vue and replace with the following:

<template>
  <div id="app">
    <img alt="J Computer Solutions Logo" src="./assets/logo.png" class="object-contain h-48 w-full">
    <p>
    Record your screen and save the file as a video.
    Perfect for screen recording for clients. Completely client side app and is installable as a PWA!
    </p>
    <p>
    Currently full system audio is only available in Windows and Chrome OS.
    In Linux and MacOS only chrome tabs are shared.
    </p>
    <t-modal
      header="Email Recording"
      ref="modal"
    >
  <t-input v-model="sendEmail" placeholder="Email Address" name="send-email" />
  <template v-slot:footer>
    <div class="flex justify-between">
      <t-button type="button" @click="$refs.modal.hide()">
        Cancel
      </t-button>
      <t-button type="button" @click="emailFile">
        Send File
      </t-button>
    </div>
  </template>
</t-modal>
<div class="mt-5">
    <t-button v-on:click="getStream" v-if="!isRecording"> Start Recording 🎥</t-button>
    <t-button v-on:click="stopStream" v-else> Stop Screen Recording ❌ </t-button>
    <t-button v-on:click="download" v-if="fileReady" class="ml-10"> Download Recording 🎬</t-button>
    <t-button  v-on:click="$refs.modal.show()" v-if="fileReady" class="ml-10"> Email Recording 📧</t-button>
</div>
    <br>
    <Adsense
      data-ad-client="ca-pub-xxxxxxxxxx"
      data-ad-slot="xxxxxxx">
    </Adsense>
  </div>
</template>

<script>

export default {
  name: 'App',
  data() {
    return {
      isRecording: false,
      options: {
        audioBitsPerSecond: 128000,
        videoBitsPerSecond: 2500000,
        mimeType: 'video/webm'
      },
      displayOptions: {
      video: {
        cursor: "always"
      },
      audio: {
          echoCancellation: true,
          noiseSuppression: true,
          sampleRate: 44100
        }
      },
      mediaRecorder: {},
      stream: {},
      recordedChunks: [],
      file: null,
      fileReady: false,
      sendEmail: '',
      url: 'https://screen-recorder-micro.jcompsolu.com'
    }
  },
  methods: {
    async emailFile () {
      try {
        const fd = new FormData();
        fd.append('video', this.file)
        fd.append('email', this.sendEmail)
        await fetch(`${this.url}/api/email-file`, {
          method: 'post',
          body: fd
        })
      this.$refs.modal.hide()
      this.showNotification()
      } catch (err) {
        alert(err.message)
      }
    },
    setFile (){
      this.file = new Blob(this.recordedChunks, {
        type: "video/webm"
      });
      this.fileReady = true
    },
    download: function(){
      this.$gtag.event('download-stream', {})


    var url = URL.createObjectURL(this.file);
    var a = document.createElement("a");
    document.body.appendChild(a);
    a.style = "display: none";
    a.href = url;
    var d = new Date();
    var n = d.toUTCString();
    a.download = n+".webm";
    a.click();
    window.URL.revokeObjectURL(url);
    this.recordedChunks = []
    this.showNotification()
    },
    showNotification: function() {
      var img = '/logo.png';
      var text = 'If you enjoyed this product consider donating!';
      navigator.serviceWorker.getRegistration().then(function(reg) {
        reg.showNotification('Screen Recorder', { body: text, icon: img, requireInteraction: true,
        actions: [
            {action: 'donate', title: 'Donate',icon: 'logo.png'},
            {action: 'close', title: 'Close',icon: 'logo.png'}
            ]
              });
      });
    },
    handleDataAvailable: function(event) {
      if (event.data.size > 0) {
        this.recordedChunks.push(event.data);
        this.isRecording = false
        this.setFile()
      } else {
        // ...
      }
    },
    stopStream: function() {
      this.$gtag.event('stream-stop', {})
      this.mediaRecorder.stop()
      this.mediaRecorder = null
      this.stream.getTracks()
      .forEach(track => track.stop())

    },
    getStream: async function() {
    try {
        this.stream =  await navigator.mediaDevices.getDisplayMedia(this.displayOptions);
        this.mediaRecorder = new MediaRecorder(this.stream, this.options);
        this.mediaRecorder.ondataavailable = this.handleDataAvailable;
        this.mediaRecorder.start();
        this.isRecording = true
        this.$gtag.event('stream-start', {})
      } catch(err) {
        this.isRecording = false
        this.$gtag.event('stream-stop', {})
        alert(err);
      }
    }
  },
  mounted() {

    let that = this
    Notification.requestPermission().then(function(result) {
      that.$gtag.event('accepted-notifications', { result: result })
    });
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Laravel API

Start off by creating a new Laravel application. My setup uses Docker and MacOS

curl -s "https://laravel.build/screen-recorder-api" | bash

The first thing we want to do is create our File model and migration. The File model will hold the name, mime_type and size of the file along with the email where the file is to be sent. Note! We are NOT storing the file, simply passing it through to the email.

cd screen-recorder-api; ./vendor/bin/sail up -d; ./vendor/bin/sail artisan make:model -m File

Open up the app/Models/File.php file and replace the contents with the following:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
class File extends Model
{
    use HasFactory, Notifiable;
    public $guarded = [];
}

Now open up the migration file and edit it to be the following:

<?php

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

class CreateFilesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('files', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email');
            $table->string('size');
            $table->string('mime_type');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('files');
    }
}

Now let’s create a new notification called SendFile. This notification will send an email with the file attached to it to the user. Let’s create the notification and fill out the contents!

./vendor/bin/sail artisan make:migration SendFile
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class SendFile extends Notification
{
    use Queueable;
    public $file;
    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct($file)
    {
        //
        $this->file = $file;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        return (new MailMessage)
                    ->line('Your Screen Recording')
                    ->line('Thank you for using our application!')
                    ->attach($this->file, ['as' => 'jcompsolu-screen-record.webm', 'mime' => 'video/webm']);
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}

You will notice we set the file in the constructor then attach it using the attach() method on the MailMessage object. Now that is done let’s create the API route, and send our notifications! Open up routes/api.php and edit it to be so:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Models\File;
use App\Notifications\SendFile;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Route::post('/email-file', function (Request $request) {
  $uploadedFile = $request->video;
  $file = File::Create([
    'name' => $uploadedFile->getClientOriginalName(),
    'mime_type' => $uploadedFile->getClientMimeType(),
    'size' => $uploadedFile->getSize(),
    'email' => $request->email
  ]);
  $file->notify(new SendFile($uploadedFile));
  return response()->json($file);
});

When you upload a file in Laravel it is an instance of UploadedFile class and has several file related methods associated with it! Using these methods we can get the name, size and mimetype of the uploaded file! After setting the model and saving in the database we send a notification with the uploaded file! Test it yourself here!

Conclusion

The vast majority of the apps I create and monetize, start off as an app that I use myself to make my life or work easier! This is the basis of #CodeLife and is the reason I was able to retire early for a few years. If this tutorial helped you please consider subscribing to my Youtube channel and subscribing to the blog and leave a comment if you want me to add new functionality!

Posted on Leave a comment

Adding Github Actions To Your Laravel Application

Github Actions Make CI/CD A Breeze

I will be the first to admit that even though I have an adequate DevOps and programming knowledge base; I have been reluctant to embrace best practices such as testing and employing continuous integration. I finally got over it once I realized that I was wasting time and losing money by NOT doing these things. Since I use Github for over 90% of my projects I decided to give Github Actions a try.

GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. Make code reviews, branch management, and issue triaging work the way you want.

Github actions allows for ultimate flexibility for continuous integration and deployment.

With Github Actions you can choose when deployments run based on triggers such as: push, merge, comments and more. In this example I will show you how to add a simple .yml file that will run your Laravel tests and push to Laravel Forge upon successful tests.

Enabling Github Actions In Your Project

The first thing that must be done in any project to use Github Actions is to add the .github/workflows/ci.yml file this file will house the YAML script that will run on your deployments. In my example the code looks like this

on: push
name: CI
jobs:
  phpunit:
    runs-on: ubuntu-latest
    container:
      image: kirschbaumdevelopment/laravel-test-runner:7.3
    services:
      mysql:
        image: mysql:5.7
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: test
        ports:
          - 33306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
    steps:
    - uses: actions/checkout@v1
      with:
        fetch-depth: 1
    - name: Install composer dependencies
      run: |
        composer install --no-scripts
    - name: Prepare Laravel Application
      run: |
        cp .env.ci .env
        php artisan key:generate
    - name: Run Testsuite
      run: vendor/bin/phpunit tests/
    - name: Deploy to Laravel Forge
      run: curl ${{ secrets.FORGE_DEPLOYMENT_WEBHOOK }}

The first line “on” defines the trigger that will run the script, in this case it is whenever we push to our repo. Next we give it a “name” which in this example we call CI. Afterwards we define “jobs” that will run when the Action is called, in this example we will run phpunit on a linux machine running the latest ubuntu OS running a laravel 7 docker image. Next we define the services, we only need to use MySQL so we bring in that image. Lastly we define the steps :
– checkout the latest push
– install the Composer dependencies
– copy the .env.ci to the .env and generate the encrypted application key
– run the tests
– deploy to Laravel Forge

# database
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=test
DB_USERNAME=root
DB_PASSWORD=password

Above is the .env.ci file. In the last step we use secrets. Secrets are a way for you to put sensitive information like keys in your repository settings without putting them in source control.

After each push now it will run the tests and if they pass will deploy to Laravel Forge. Watch my YouTube video for a more in depth explanation!

Posted on Leave a comment

Setting Up A CI Laravel Pipeline Using Bitbucket

Continuous Integration

Is a must in today’s software architecture. Setting up a solid CI/CD pipeline will save you countless hours and money immediately and down the line.

There are plenty of tools that can accomplish your CI/CD goals from Jenkins, to CircleCI however in today’s tutorial I will be showing you how to accomplish this using Bitbucket pipelines.

Editing The YAML File

Bitbucket’s pipeline system uses yml files to spin up docker instances and run your scripts. This is an example file that runs composer, generates keys, runs migrations, installs passport and runs the tests. It also only runs when deploying to master branch and sets up a mysql instance and sets the environment keys.

# This is a sample build configuration for PHP.
# Check our guides at https://confluence.atlassian.com/x/e8YWN for more examples.
# Only use spaces to indent your .yml configuration.
# -----
# You can specify a custom docker image from Docker Hub as your build environment.
image: lorisleiva/laravel-docker
pipelines:
  branches:
    master:
      - step:
          caches:
            - composer
          script:
            - composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
            - cp .env.example .env
            - php artisan key:generate
            - php artisan migrate
            - php artisan passport:install
            - vendor/bin/phpunit
          services:
            - mysql
      - step:
          name: Deploy to prod
          deployment: production
          # trigger: manual  # Uncomment to make this a manual deployment.
          script:
            - echo "Deploying to prod environment"
            - curl -X GET https://forge.laravel.com/servers/148653/sites/653797/deploy/http?token=4Q8e1kFPmFsHbxOBek7jcqikAvGVTbiX50tsPUPK
definitions:
    services:
      mysql:
        image: mysql:5.7
        variables:
          MYSQL_DATABASE: 'ben'
          MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
          MYSQL_USER: 'homestead'
          MYSQL_PASSWORD: 'secret'

Why Is Continuous Integration Important?

I did an article about why continuous integration is important to the solo developer and I suggest everyone take a few moments and read that article. It outlines why having a solid CI/CD plan in your architecture, however the TL;DR is that is saves you time, money, effort and stress by spending the little bit of time up front to ensure you have tested and tried code before you ship to production.

Posted on Leave a comment

Create An Encryption Based Anonymous Messenger

Secure Your Messages With Laravel Encryption

Encryption is the best tool in this fight for your right to privacy. Now more than anytime in history privacy is of the essence. In today’s tutorial I will show you how to create a simple messenger service in Laravel and Vue.js; however they will be password protected and encrypted therefore the receiver must know the password beforehand to read the message. All code can be found on my Github repo.

Setting Up The Backend

The application itself has only one model and that is the Message model. It has 3 properties: content, email and passphrase. The content stores the encrypted message. The email is the email address of who is receiving the message. Finally the passphrase is the password that protects the message from being opened by anybody.
php artisan make:model -m Message
Open the Message.php file and make it Notifiable and change the $fillable


<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
use Notifiable;
//
public $fillable = ['content','passphrase','email'];
}

 
Next open up the migration file that was created with the model and add the following:


<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateMessagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('messages', function (Blueprint $table) {
$table->increments('id');
$table->longText('content');
$table->string('passphrase');
$table->string('email');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('messages');
}
}

Next create the controller. This controller will be RESTful with an extra method for decrypting the messages.


php artisan make:controller --resource MessageController

Open the file and fill in the methods store(), show() and decrypt()


<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Message;
use App\Notifications\MessageCreated;
use Illuminate\Contracts\Encryption\DecryptException;
class MessageController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
$message = Message::Create([
'content' => encrypt($request->content),
'passphrase' => encrypt($request->password),
'email' => $request->email
]);
$message->notify(new MessageCreated($message));
return response()->json($message);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
$message = Message::findOrFail($id);
return view('message')->with([
'message' => $message
]);
}
public function decrypt(Request $request, $id){
try{
$message = Message::findOrFail($id);
if($request->password == decrypt($message->passphrase)){
$message = decrypt($message->content);
$with = [
'message' => $message
];
return response()->json($with);
}
}
catch (DecryptException $e){
return response()->json($e);
}
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

 
As you can see there is a call to a notification that has not been created yet so create it


php artisan make:notification MessageCreated

 
Open that up and replace it with the following which will call the toMail method and alert the recipient they have a new message to view


<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class MessageCreated extends Notification
{
use Queueable;
public $message;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(\App\Message $message)
{
//
$this->message = $message;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->line('You have a new encrypted message.')
->line('You should have been given the passphrase')
->action('Decrypt and Read Now!', url('/message/'.$this->message->id))
->line('Thank you for using our application!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

Lastly the api and web routes need to be updated
In web.php


<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('home');
});
Route::get('/home', 'HomeController@index')->name('home');
Route::get('/message/{id}','MessageController@show');

In api.php


<?php
use Illuminate\Http\Request;
/* |-------------------------------------------------------------------------- | API Routes Fr */
Route::middleware('auth:api')->get('/user', function (Request $request) { return $request->user(); });
Route::resource('/message','MessageController');
Route::post('/decrypt-message/{id}','MessageController@decrypt');

Lastly let’s create the view files. As a shortcut I scaffold authentication even though we aren’t using it to get the bootstrap layouts and the home.blade.php file. Copy the home.blade.php file to another file called message.blade.php.
In home.blade.php


@extends('layouts.app')
@section('content')
<send-message></send-message>
@endsection

 
In message.blade.php


@extends('layouts.app')
@section('content')
<read-message v-bind:message="{{$message}}"></read-message>
@endsection

That’s it, the back end is complete!
 

Front End

The Vue.js side of things is pretty simple there are two components a MessageSend and a MessageRead. Create a file in your resources/assets/js/components folder called MessageSend.vue and MessageRead.vue.
 
In MessageSend.vue


<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card card-default">
<div class="card-header">Send Encrypted Message</div>
<div class="card-body">
<div class="form-group">
<input type="email" placeholder="Enter email address" class="form-control" v-model="message.email">
</div>
<div class="form-group">
<input type="password" placeholder="Enter passphrase" class="form-control" v-model="message.password">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Enter Message" v-model="message.content"></textarea>
</div>
<div class="form-group">
<button class="btn btn-sm btn-primary" v-on:click="send()">Send Message</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
},
data() {
return {
message: {}
}
},
methods: {
send: function(){
axios.post('/api/message',this.message).then(data =>{
console.log(data);
alert('Message Sent!');
}).catch(err => {
console.log(err);
})
}
}
}
</script>

 
In MessageRead.vue


<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card card-default">
<div class="card-header">Read Encrypted Message</div>
<div class="card-body">
<div class="form-group">
<input type="password" placeholder="Enter passphrase" class="form-control" v-model="password">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Enter Message" v-model="msg.content"></textarea>
</div>
<div class="form-group">
<button class="btn btn-sm btn-primary" v-on:click="decrypt()">Decrypt Message</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
},
data() {
return {
password: '',
msg : this.message
}
},
props: ['message'],
created(){
},
methods: {
decrypt: function(){
var that = this;
axios.post('/api/decrypt-message/'+this.message.id,{password:this.password}).then(data =>{
that.message.content = data.data.message;
}).catch(err => {
console.log(err);
})
}
}
}
</script>

In your resources/assets/js/app.js file don’t forget to add your components


/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
window.Vue = require('vue');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('send-message', require('./components/MessageSend.vue'));
Vue.component('read-message', require('./components/MessageRead.vue'));
const app = new Vue({
el: '#app'
});

 

Posted on Leave a comment

Create A Point Of Sales System With Laravel, Vue and Stripe – Part 4

Stripe Subscriptions

In part 4 we add recurring billing to our point of sales system by utilizing Stipe Subscriptions. Subscriptions in conjunction with our customers created in part 3 allows for easy tracking of expenses and opens the way for residual income. Next up we add inventory management to the system! For the source code click here and don’t forget to subscribe to the YouTube channel!

Posted on 1 Comment

Create A Point Of Sales System With Laravel, Vue and Stripe – Part 3

Stripe Customers

In the previous segment we set up the Stripe webhooks so that Stripe can interact with our system asynchronously. Today we implement Stripe customers in our platform. For all source code check here. In the next post we introduce recurring billing.

 

Posted on 1 Comment

Create A Point Of Sales System With Laravel, Vue and Stripe – Part 2

Stripe Webhooks

In this second part of the tutorial (here is part one) I show you how to work with Stripe webhooks, create a manifest.json file and implement push notifications. The full source code is listed below here. An important part of any point of sales system, is alerting you when you have successful transactions among other things. Stripe has a wonderful webhook system that allows us to interact with the system with events. A full list of events can be found here if you want to expand the source code. In part 3 I will show you how to add customers to your POS platform and what can be done by doing so. Don’t forget to like the video and subscribe to my Youtube channel!


#30days30sites #100daysofcode #301daysofcode