Posted on Leave a comment

Create A WebRTC Google Meet Clone In Vue.js Pt. 1

Google Meet Clone Written In Vue

In this tutorial series, we will be building a WebRTC Google Meet clone using Vue.js. All of the source code is free and available on Github. If you found this tutorial to be helpful and want to help keep this site free for others consider becoming a patron! The application will allow you to join a room by ID. Anyone who joins that room @ that ID will instantly join the call. In this first iteration, we can share voice, video, and screens!

Setting Up The Vue Application

Let’s go ahead and create the Vue application and add our dependency for WebRTC vue-webrtc. This dependency adds all of the functionality we need in a simple web component!

vue create google-meet-clone; cd google-meet-clone; npm install --save vue-webrtc

All of the functionality is built in the App.vue page (for now) let’s open it up and add the following:

web rtc google meet clone!
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <vue-webrtc width="100%" :roomId="roomId" ref="webrtc" v-on:share-started="shareStarted"  v-on:share-stopped="leftRoom" v-on:left-room="leftRoom" v-on:joined-room="joinedRoom"/>
    <input v-model="roomId" placeholder="Enter room ID"/>
    <button @click="toggleRoom">{{hasJoined ? 'Leave Room' : 'Join Room'}}</button>
    <button @click="screenShare" v-if="hasJoined">Screen Share</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      roomId: 'roomId',
      hasJoined: false,
      userStream: null
    }
  },
  mounted () {},
  methods: {
    async toggleRoom () {
      try {
        if(this.hasJoined) {
          this.$refs.webrtc.leave()
          this.hasJoined = false
        } else {
          await this.$refs.webrtc.join()
          this.userStream = this.$refs.webrtc.videoList[0].stream
          this.hasJoined = true
        }
      } catch (e) {
        console.log(e)
      }

    },
    screenShare () {
      this.$refs.webrtc.shareScreen()
    },
    joinedRoom (streamId) {
      console.log(streamId)
    },
    shareStarted (streamId) {
      console.log(streamId)
    },
    leftRoom (streamId) {
      console.log(streamId)
    }
  }
}
</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>

The screen has a text field for putting the roomID which will be used by the vue-webrtc component to connect to a room. We have some events we listen to, which we will do more with in later tutorials. For now there are two buttons, one for connecting /leaving the room and one for sharing your screen. This is it! The package handles everything else and you can test it out here. In the next series we will implement recording functionality so everyone can download the meetings! If you enjoyed this please like and share this blog and subscribe to my YouTube page! In the meantime while you wait check out my screen recorder app tutorial!

Posted on Leave a comment

Adding Livestreaming To The Screen Recorder Using The YouTube Data V3 API

Adding YouTube API To The Screen Recorder

So I wanted to add some more functionality to the app that would separate it from the competition (check it out here). At first, I was going to add YouTube functionality where the user could upload the video straight to Youtube. My brother who is a streamer brought up that unless I added editing capabilities to it, there wasn’t much need for that functionality. Instead, I should stream to YouTube. This made much more sense, even in my case I usually stream myself coding from the desktop but instead of downloading cumbersome software, I can do it straight in the browser! For this, I decided to use Laravel Socialite with a YouTube provider, while on the client-side creating a YouTube class with the various functions needed to interact with the API.

Connect To Youtube!

Extending The Laravel Microservice

The Laravel part is pretty simple first we add the Socialite and Youtube Provider packages.

composer require laravel/socialite socialiteproviders/youtube

Now we have to edit the app/Providers/EventServiceProvider.php file

<?php

namespace App\Providers;

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        // ... other providers
        \SocialiteProviders\YouTube\YouTubeExtendSocialite::class.'@handle',
      ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

Next we need to set the .env file and add the client secret, recorder URL and redirect URL

YOUTUBE_CLIENT_ID=
YOUTUBE_CLIENT_SECRET=
YOUTUBE_REDIRECT_URI="${APP_URL}/api/callback/youtube"
RECORDER_URL=

If you have worked with Laravel Socialite in the past then all of this is familiar. Finally we need to edit our routes/api.php file and add our two API routes for interacting with Youtube.

Route::get('/login/youtube', function (Request $request) {
  return Socialite::driver('youtube')->scopes(['https://www.googleapis.com/auth/youtube', 'https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.readonly'])->stateless()->redirect();
});

Route::get('/callback/youtube', function (Request $request) {
  $user = Socialite::driver('youtube')->stateless()->user();
  return redirect(env('RECORDER_URL').'/#/success?token='.$user->token);
});

The callback function redirects us to the web app and the reason for this will become clear next.

The Client Side

On the web app we need to create a Youtube class that will call all of the functions needed for interacting with the API. Not everything is implemented now and will be as the tutorial goes on. Create a new file src/classes/Youtube.js

export default class Youtube {
  constructor (token) {
    this.token = token
    this.broadcasts = []
  }
  async uploadVideo () {}
  async createNewLiveStream () {
    try {
    const broadcast = await this.createBroadcast()
    const livestream = await this.makeRequest('https://www.googleapis.com/youtube/v3/liveStreams?part=cdn&part=snippet', 'POST', {
      "snippet": {
        "title": "Getting Started With Screen Recorder"
      },
      "cdn": {
        "frameRate": "variable",
        "ingestionType": "dash",
        "resolution": "variable"
      }
    })
      console.log([broadcast, livestream])
      const bind = await this.bindBroadCast(broadcast.id, livestream.id)
      console.log(bind)
    } catch (e) {
      console.log(e)
    }
  }
  async getBroadcasts () {
    try {
      const res = await fetch('https://www.googleapis.com/youtube/v3/liveBroadcasts?broadcastStatus=all', {
        headers: {
          'Authorization': `Bearer ${this.token}`
        }
      })
      const results = await res.json()
      this.broadcasts = results.items
      console.log(this.broadcasts)
      return results
    } catch (e) {
      console.log(e)
    }
  }
  async createBroadcast () {
    try {
      const res = await this.makeRequest('https://youtube.googleapis.com/youtube/v3/liveBroadcasts?part=contentDetails&part=snippet&part=status','POST',{
      "snippet": {
        "scheduledStartTime": new Date(Date.now()).toISOString(),
        "title": "Getting Started With Screen Recorder"
      },
      "contentDetails": {
        "enableDvr": true,
        "enableAutoStart": true,
        "enableAutoStop": true
      },
      "status": {
        "privacyStatus": "unlisted",
      }
    })
    return res
    } catch (e) {
      console.log(e)
    }
  }
  async bindBroadCast (broadcastId, streamId) {
    const url = `https://www.googleapis.com/youtube/v3/liveBroadcasts/bind?id=${broadcastId}&part=snippet&streamId=${streamId}`
    try {
      const res = await this.makeRequest(url, 'POST', {})
      return res
    } catch (e) {
      console.log(e)
    }
  }
  async endBroadcast() {}
  async makeRequest(url, method, data) {
    try {
      const res = await fetch(url, {
        method: method, // *GET, POST, PUT, DELETE, etc.
        mode: 'cors', // no-cors, *cors, same-origin
        cache: 'no-cache',
        headers: {
          Authorization: `Bearer ${this.token}`
        },
        body: JSON.stringify(data)
      })
      const ret = await res.json()
      return ret
    } catch (e) {
      alert('There was an error with the request! Please try agin later.')
    }

  }
}

All of these methods are from the Live and Broadcasts APIs now we will grab the token and init our class! To do this we will create a button that when pressed will open up a new window call the Socialite endpoint, grab the token, close the window, and set the class. First we will create a vuex file and add it to the application open src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import Youtube from '../classes/Youtube'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    yt: {}
  },
  mutations: {
    setYouTube (state, token) {
      state.yt = new Youtube(token)
    },
    streamToYouTube (state) {
      return state.yt.createNewLiveStream()
    },
    getBroadcasts (state) {
      return state.yt.getBroadcasts()
    },
    createBroadcast (state) {
      return state.yt.createNewLiveStream()
    }
  },
  actions: {
    setYouTube (context, token) {
      console.log(token)
      context.commit('setYouTube', token)
    },
    streamToYouTube (context) {
      context.commit('streamToYouTube')
    },
    getBroadcasts (context) {
      return context.commit('getBroadcasts')
    },
    createBroadcast (context) {
      context.commit('createBroadcast')
    }
  },
  getters: {
    getYoutube (state) {
      return state.yt
    }
  },
  modules: {
  }
})

We create a universal yt object in the state that represents our Youtube class and we will call the methods. Don’t forget to add the plugin

vue add vuex

Routing

The Youtube API use case requires us to provide a privacy policy so we need to add vue-router and make some new components for the pages.

vue add router

Now create a new file src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/privacy',
    name: 'Privacy',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/Privacy.vue')
  },
  {
    path: '/terms',
    name: 'TOS',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/Terms.vue')
  },
  {
    path: '/success',
    name: 'Success',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/Success.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router

The About, Terms and Privacy pages are simply templates with text in them showing the various content needed and for sake of brevity I won’t show those contents as there is no javascript. The Success page however is very important and is responsible for grabbing the Youtube token from the Laravel callback. Let’s explore it src/views/Success.vue

<template>
  <div class="Success">
    <img alt="Screen Record Pro" src="../assets/logo.svg" class="animate-fade-slow object-contain h-80 w-full">
    <h2 class="text-sm tracking-wide font-medium text-gray-500 uppercase">Youtube Connected!</h2>
    <p class="text-base font-light leading-relaxed mt-0 mb-4 text-gray-800">
    Thank you for authenticating with Screen Record Pro! This window will close automatically
    </p>
  </div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
  name: 'Success',
  mounted () {
    window.localStorage.setItem('youtube_key', this.$route.query.token)
    window.opener.postMessage({youtube_token: this.$route.query.token}, '*')
    window.close()
  },
  computed: {
    ...mapGetters(['getYoutube'])
  },
  methods : {
    ...mapActions(['setYouTube'])
  }
}
</script>

Once the page mountes we use localstorage API to set the youtube_key to the token query parameter. This parameter is set when the redirect is called in the /callback/youtube API endpoint. This window will be a popup window, and we need to send a message to the window that opened this window (make sense?). For this we use the window.opener.postMessage() function. We will listen for this message on the home screen and set the youtube object. Now that we have made our router and vuex object we need to redo the main.js and set our Vue object with them. open up main.js

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"
import router from './router'
import store from './store'
Vue.use(VueGtag, {
  config: { id: "UA-xxxxxxx" }
});

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({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

Lastly we need to open the src/views/Home.vue file and edit our application. When it mounts we need to set a listener for message and call the setYoutube method. If the localstorage is already set then we don’t show the button for connecting. If the user is connected then they click a button and it creates a live stream.

<template>
  <div id="app">
    <img alt="Screen Record Pro" src="../assets/logo.svg" class="animate-fade-slow object-contain h-80 w-full">
    <h2 class="text-sm tracking-wide font-medium text-gray-500 uppercase">Free Online Screen Recorder</h2>
    <p class="text-base font-light leading-relaxed mt-0 mb-4 text-gray-800">
    Free online screen recorder by J Computer Solutions LLC that allows you to
    record your screen including microphone audio and save the file to your desktop.
    No download required, use this progressive web app in the browser!
    J Computer Solutions LLC provides the #1 free online screen capture software! Due to current
    browser limitations, this software can only be used on desktop. Please ensure you are on a Windows, MacOS or Linux
    computer using Chrome, Firefox or Safari!
    </p>
    <h1 class="text-3xl font-large text-gray-500 uppercase">To Date We Have Processed: <strong class="animate-pulse text-3xl font-large text-red-500">{{bytes_processed}}</strong> bytes worth of video data!</h1>
    <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 mb-5">
  <t-button v-on:click="connectToYoutube" v-if="!youtube_ready"> Connect To YouTube πŸ“Ί</t-button>
</div>
<div class="mt-5 mb-5">
  <t-button v-on:click="getStream" v-if="!isRecording" v-show="canRecord" class="ml-10"> Start Recording πŸŽ₯</t-button>
    <div v-else>
      <t-button v-on:click="streamToYouTube" @click="createBroadcast" v-if="youtube_ready">Stream To Youtube πŸ“Ί</t-button>

      <t-button v-on:click="stopStream"> Stop Screen Recording ❌ </t-button>
      </div>
    <t-button v-on:click="download" v-if="fileReady" class="ml-10"> Download Recording 🎬</t-button>
    <t-button  v-on:click="$refs.modal.show()" autoPictureInPicture="true" v-if="fileReady" class="ml-10"> Email Recording πŸ“§</t-button>
</div>
<div class="mt-5" v-show="fileReady">
  <video class="center" height="500px"  controls  id="video" ></video>
</div>
<Adsense
  data-ad-client="ca-pub-7023023584987784"
  data-ad-slot="8876566362">
</Adsense>
<footer>
  <cookie-law theme="base"></cookie-law>
</footer>
  </div>
</template>

<script>
 import CookieLaw from 'vue-cookie-law'
 import { mapGetters, mapActions } from 'vuex'
export default {
  name: 'Home',
  components: { CookieLaw },
  data() {
    return {
      youtube_ready: false,
      canRecord: true,
      isRecording: false,
      options: {
        audioBitsPerSecond: 128000,
        videoBitsPerSecond: 2500000,
        mimeType: 'video/webm; codecs=vp9'
      },
      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',
      bytes_processed: 0,
    }
  },
  methods: {
    ...mapActions(['setYouTube', 'streamToYouTube', 'getBroadcasts', 'createBroadcast']),
    async connectToYoutube () {
      window.open(`${this.url}/api/login/youtube`, "YouTube Login", 'width=800, height=600');
    },
    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.$gtag.event('email-file-data', {
          'name': this.file.name,
          'size': this.file.size,
          'email': this.sendEmail
        })
      this.$refs.modal.hide()
      this.showNotification()
      } catch (err) {
        alert(err.message)
      }
    },
    async uploadFileData () {
      try {
        const fd = new FormData();
        fd.append('video', this.file)
        await fetch(`${this.url}/api/upload-file-data`, {
          method: 'post',
          body: fd
        })
        this.$gtag.event('upload-file-data', {
          'name': this.file.name,
          'size': this.file.size
        })
      } catch (e) {
        this.$gtag.exception('application-error', e)
      }
    },
    setFile (){
      this.file = new Blob(this.recordedChunks, {
        type: "video/webm; codecs=vp9"
      });
      this.$gtag.event('file-set', {
        'event_category' : 'Files',
        'event_label' : 'File Set'
      })
      const newObjectUrl = URL.createObjectURL( this.file );
      const videoEl = document.getElementById('video')
      // URLs created by `URL.createObjectURL` always use the `blob:` URI scheme: https://w3c.github.io/FileAPI/#dfn-createObjectURL
      const oldObjectUrl = videoEl.src;
      if( oldObjectUrl && oldObjectUrl.startsWith('blob:') ) {
          // It is very important to revoke the previous ObjectURL to prevent memory leaks. Un-set the `src` first.
          // See https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL

          videoEl.src = ''; // <-- Un-set the src property *before* revoking the object URL.
          URL.revokeObjectURL( oldObjectUrl );
      }

      // Then set the new URL:
      videoEl.src = newObjectUrl;

      // And load it:
      videoEl.load();
      this.$gtag.event('file-loaded', {
        'event_category' : 'Files',
        'event_label' : 'File Loaded'
      })
      videoEl.onloadedmetadata = () => {
        this.uploadFileData()
        this.getBytes()
      }
      videoEl.onPlay = () => {
        this.$gtag.event('file-played', {
          'event_category' : 'Files',
          'event_label' : 'File Played'
        })
      }

      this.fileReady = true
    },
    download: function(){
    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()
    this.$gtag.event('file-downloaded', {
      'event_category' : 'Files',
      'event_label' : 'File Downloaded'
    })
    },
    showNotification: function() {
      this.$gtag.event('notification-shown', {})
      var img = '/logo.png';
      var text = 'If you enjoyed this product consider donating!';
      navigator.serviceWorker.getRegistration().then(function(reg) {
        reg.showNotification('Screen Record Pro', { 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 {
        // ...
      }
    },
    async registerPeriodicNewsCheck () {
      const registration = await navigator.serviceWorker.ready;
      try {
        await registration.periodicSync.register('get-latest-stats', {
          minInterval: 24 * 60 * 60 * 1000,
        });
      } catch (e) {
        this.$gtag.exception('application-error', e)
      }
    },
    stopStream: function() {
      this.mediaRecorder.stop()
      this.mediaRecorder = null
      this.stream.getTracks()
      .forEach(track => track.stop())
      this.stream = null
      this.$gtag.event('stream-stop', {
        'event_category' : 'Streams',
        'event_label' : 'Stream Stopped'
      })
    },
    getStream: async function() {
    try {
        this.stream =  await navigator.mediaDevices.getDisplayMedia(this.displayOptions);
        this.stream.getVideoTracks()[0].onended = () => { // Click on browser UI stop sharing button
          this.stream.getTracks()
          .forEach(track => track.stop())
        };
        const audioStream = await navigator.mediaDevices.getUserMedia({audio: true}).catch(e => {throw e});
        const audioTrack = audioStream.getAudioTracks();
        // add audio track
        this.stream.addTrack(audioTrack[0])
        this.mediaRecorder = new MediaRecorder(this.stream)
        this.mediaRecorder.ondataavailable = this.handleDataAvailable;
        this.mediaRecorder.start();
        this.isRecording = true
        this.$gtag.event('stream-start', {
          'event_category' : 'Streams',
          'event_label' : 'Stream Started'
        })
      } catch(e) {
        this.isRecording = false
        this.$gtag.exception('application-error', e)
      }
    },
    async getBytes () {
      const result = await fetch(`${this.url}/api/get-stats`)
      this.bytes_processed = await result.json()
    },
    skipDownloadUseCache () {
      this.bytes_processed = localStorage.bytes_processed
    }

  },
  mounted() {
    const ctx = this
    window.addEventListener("message", function (e) {
      if (typeof e.data.youtube_token !== 'undefined') {
        console.log(e.data.youtube_token)
        ctx.setYouTube(e.data.youtube_token)
        ctx.youtube_ready = true
      }
    })
    this.$gtag.pageview("/");
    const ua = navigator.userAgent;
    if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua) || /Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)) {
        alert('You must be on desktop to use this application!')
        this.canRecord = false
        this.$gtag.exception('mobile-device-attempt', {})
    }
    let that = this
    if (Notification.permission !== 'denied' || Notification.permission === "default") {
      try {
        Notification.requestPermission().then(function(result) {
          that.$gtag.event('accepted-notifications', {
            'event_category' : 'Notifications',
            'event_label' : 'Notification accepted'
          })
          console.log(result)
        });
      } catch (error) {
          // Safari doesn't return a promise for requestPermissions and it
          // throws a TypeError. It takes a callback as the first argument
          // instead.
          if (error instanceof TypeError) {
              Notification.requestPermission((result) => {
                that.$gtag.event('accepted-notifications', {
                  'event_category' : 'Notifications',
                  'event_label' : 'Notification accepted'
                })
                console.log(result)
              });
          } else {
            this.$gtag.exception('notification-error', error)
            throw error;
          }
      }

    }
  },
  computed: {
    ...mapGetters(['getYoutube'])
  },
  async created () {
    try {
      if(localStorage.youtube_key != null) {
        this.setYouTube(localStorage.youtube_key)
        console.log(this.getBroadcasts())
        this.youtube_ready = true
      }
      const registration = await navigator.serviceWorker.ready
      const tags = await registration.periodicSync.getTags()
      navigator.serviceWorker.addEventListener('message', event => {
        this.bytes_processed = event.data
      });
      if (tags.includes('get-latest-stats')) {
          // this.skipDownloadUseCache()
      } else {
        this.getBytes()
      }
    } catch (e) {
      this.$gtag.exception('application-error', e)
      this.getBytes()
    }
  }
}
</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;
}
:picture-in-picture {
  box-shadow: 0 0 0 5px red;
  height: 500px;
  width: 500px;
}
</style>
OAuth Screen
We can now stream using the Youtube live api
Now we can create a live stream!

We created the stream but now we need to send our packets via MPEG-DASH! In the next series, we create the dash service and send our packets to Youtube for ingestion! Be sure to like and share this article and subscribe to my Youtube channel! Also, be sure to check out the source code for the API and the PWA! Lastly, join the discord and connect with software engineers and entrepreneurs alike!

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

Building A Twitter Discord Bot In Node.JS

Discord Is Awesome, So Is Twitter

I have a discord server and I have a pretty decent sized Twitter following. I wanted to add a bot that followed my tweets and sent them as messages in my #twitter channel on my server. I refuse to pay for software and I have a hyper distrust of other’s software so I wrote my own. In this node.js script I have one file main.js that uses 3 dependencies: twit (for Twitter streaming API), Discord.js (for well….Discord) and dotenv (to load environment variables). The script is ~30 lines long and is available on my Github.

Create a Discord Bot using the Twitter Streaming API

Main.js

require('dotenv').config()
const Twit = require('twit')
const Discord = require('discord.js');
const client = new Discord.Client();
var T = new Twit({
  consumer_key:         process.env.TWITTER_CONSUMER_KEY,
  consumer_secret:      process.env.TWITTER_CONSUMER_SECRET,
  access_token:         process.env.TWITTER_ACCESS_TOKEN,
  access_token_secret:  process.env.TWITTER_ACCESS_TOKEN_SECRET,
  timeout_ms:           60*1000,  // optional HTTP request timeout to apply to all requests.
  strictSSL:            true,     // optional - requires SSL certificates to be valid.
})
client.login(process.env.DISCORD_TOKEN);
client.once('ready', () => {
  var stream = T.stream('statuses/filter', { follow: [process.env.TWITTER_USER_ID] })
  stream.on('tweet', function (tweet) {
    //...
    var url = "https://twitter.com/" + tweet.user.screen_name + "/status/" + tweet.id_str;
    try {
        let channel = client.channels.fetch(process.env.DISCORD_CHANNEL_ID).then(channel => {
          channel.send(url)
        }).catch(err => {
          console.log(err)
        })
    } catch (error) {
            console.error(error);
    }
  })
})

.env file to load variables.

TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
TWITTER_USER_ID=
DISCORD_TOKEN=
DISCORD_CHANNEL_ID=

Now all you have to do is run it using the following command

node main.js

Don’t forget to get your application keys from the Discord developer portal and the Twitter developer portal. I made a Youtube video showing the process in detail. Want to start making money and living a #CodeLife? Join the group today!

Posted on Leave a comment

401KApps & The Future Of Investing

401KApps Is Almost Ready

Those of you who follow my Twitter might remember my thread about 401KApps

Ricky Mason and I put on a 3 month case study with 6 participants to buy some apps and gather the data. Needless to say the model worked so we went straight to creating the prototype.

I Don’t Follow You On Twitter WTH Is 401KApps

My apologies everyone! Let me back up. 401KApps is a new partnership I am working on with Ricky that is a private investment group that buys, holds and sells apps. The apps are bought from the owners (we only buy apps that have a current positive cash flow) and we enhance the applications, increase the marketing and promotion and share the profits. Unlike stocks, bonds, rental property where you get paid once a month, apps make money from day one. Using the app, you can invest in investment pools that are categorized. Each pool will hold 10-15 apps. After the vesting period you can choose to receive your payouts on a daily, weekly, month or yearly basis!

How Is Investing In Apps Smart?

Like I said apps make money daily. In addition to traditional investments such as stocks, bonds, real estate and precious metals I believe it is good to have an investment vehicle that gives you consistent stable cash flow. At one point in my personal life I was living completely off of my app income, which freed my other income to be used how I wish. The apps that we are buying are already complete and have a positive cash flow, by applying the power of group economics the collective can generate a sizable amount of free flowing cash to help enrich their lives.

What Is The Minimum Amount To Invest?

The minimum to invest in an app pool will be $20 with a maximum investment of $10,000. Each investment pool will raise between $30,000 and $250,000. This will allow investors to diversify their exposure to the markets and give newer investors with less capital the opportunity to get into the space.

When Will The App Be Released?

You can currently sign up for the waiting list at https://401k-apps.com but the beta MVP will be available before the end of the year so look for a Xmas gift πŸ˜‰

Want To Learn How To Code In The Meantime?

Another way to invest into apps is to write one yourself! If you are wanting to learn how to code check out my video course “Intro To Web Development” In it you will learn the basics of web development and get started learning the skills needed to create rock star web applications! Perhaps you are already a coder and want to get started in test driven design? If so check out my course Test Driven Design W/ Laravel and Vue.js. Into web dev but want to switch over to mobile development? Check out Nativescript with my course Nativescript, Vue & Laravel SAAS Master Class

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