In my last YouTube video, I was asked to implement Google Drive upload functionality for saving screen recordings. I thought this was a marvelous idea and immediately got to work! We already added OAuth login via Google and Laravel in the last tutorial to interact with the Youtube Data v3 API, so with a few simple backend tweaks, we can add Google Drive as well!
Steps To Accomplish
The functionality I want to add to this is going to be just uploading to Google Drive, with no editing or listing. Keep things simple! This is going to require the following steps
Adding Google Drive scopes to Laravel Socialite
Create a function to upload file to Google API endpoint
Pretty easy if I do say so myself. Let’s get started with the backend.
Adding Google Drive Scopes To Laravel Socialite
We already added scopes for YouTube in the last tutorial so thankfully not a whole lot of work is needed to add Google Drive scopes. Open up your routes/api.php file and update the scopes array to include the new scopes needed to interact with Google Drive
Make sure you enable the API in the Google cloud console! Now we head over to the frontend Vue application and let’s add our markup and functions.
Open the Home.vue and we are going to add a button in our list of actions for uploading to Google Drive
<t-button v-on:click="uploadToDrive" v-if="uploadReady" class="ml-10">Upload To Drive 🗄️</t-button>
In the methods add a function called uploadToDrive() inside put the following
async uploadToDrive () {
let metadata = {
'name': 'Screen Recorder Pro - ' + new Date(), // Filename at Google Drive
'mimeType': 'application/zip', // mimeType at Google Drive
}
let form = new FormData();
form.append('metadata', new Blob([JSON.stringify(metadata)], {type: 'application/json'}));
form.append('file', this.file);
await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache',
headers: {
'Content-Length': this.file.length,
Authorization: `Bearer ${this.yt_token}`
},
body: form
})
alert('Video uploaded to Google Drive!')
}
Inside this function we create an HTTP POST request to the Google Drive endpoint for uploading files. We pass a FormData object that contains some metadata about the object and the actual file itself. After the file is uploaded the user is alerted that their video is stored!
Screen Recorder Pro Google Drive upload confirmation
What’s Next?
Next, we will add cloud storage you will be able to share with Amazon S3 and WebShare API! Finally we will add monetization and this project will be wrapped up! If you enjoyed this please give the app a try at https://recorder.jcompsolu.com
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:
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!
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.
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
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.
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
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
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
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 ScreenNow 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!
I love listening to podcasts and online radio. I used to run an online station a fews ago called 90K Radio and I was hooked in the community ever since. Keeping on track with my PWA binge I thought it would be a cool to write a progressive web app that can take in any stream URL and play it. Not only that but I want to be able to control the audio using the native audio commands on Android, iOS and desktop. There is this awesome javascript API called the Media Session API. The Media Session API allows you to customize media notifications and how to handle them. It allows you to control your media without having to be on that specific webpage. This allows for things such as background playing ( a must need feature for online radio PWA). You can even do things like set album artwork and other metadata and have custom handlers for events such as skipping tracks and pausing/playing.
What Are The Benefits?
Primarily I built this because a PWA will load faster. Also no tracking, I don’t have to worry about Google or anyone else tracking my activity, I can just listen in peace. By using the media session API I can listen in the background whilst doing other things which most likely will be every time I use the app. Lastly it’s just an awesome feeling to use your own software 😅.
The Vue Application
I created the vue application using the standard Vue CLI and added Vuetify so that it has some basic responsive styling. The app has one component called Radio.vue which holds all of the logic. The application has some preset radio stations that I can click as well as a text field where I can put in any URL of their choosing for play. It also grabs an RSS feed for a few of my favorite podcasts so I can quickly listen. Everything is done client side including the RSS XML parsing! You can view the live version here and clone the repo here.
Let’s Get Coding
As I stated above, I created a new Vue application using the vue-cli and added vuetify using the vue add vuetify command. For brevity I will skip that part and only talk about the Radio.vue component which holds all of the logic. This component will grab the preset stations and turn those into buttons. The favorited podcast RSS feeds it will grab, parse the XML and play said podcast. There is a URL text input that the user can manually put an audio stream URL in. Finally I set the Media Session metadata to show the cover art and info of whatever is playing and if I don’t have it show a default image, artist and album.
This was a fun and easy PWA to make and I will turn it into an Android application to put on the Google Play Store (learn how with my PWA to APK course). Some features I will add will include:
save favorites locally using indexDB
create queue that can be skipped
download podcast episodes
Have everything play via the WebAudio API and add visualizations.
Two days ago I put out Relaxing Sounds. RS is a android(soon to be iOS) productivity application that plays various white noise sounds like thunderstorms and ocean waves. I wrote this app using Nativescript and Vue.js in a matter of 3 hours. Now keep in mind:
This is a simple app.
I just wanted to get my hands dirty with the core framework and have something to show for it.
I integrated admob and created a free and a pro version with plans to update the application to include a relaxing sounds livestream for 24 hr continuous new calming content. I was blown away by how fast I was able to transition from the DOM to Nativescript XML. If you don’t know anything about Nativescript then check out my blog post about it.
How Did I Write It So Fast?
Well once I understood the Nativescript system (which in itself only took a couple hours to understand and master) it was just a simple matter of how well do I know Vue.js? That’s the beauty of Nativescript! It abstracts the headache of interacting with the native hardware so your mind can stay in a purely web development mode. If you can write a PWA with Vue.js then you can write a truly native mobile application with Vue.js there really isn’t any excuse. Couple Nativescript with Vue.js and Laravel for your backend API and you can be a one man (or woman) code army.
Why Did I Write Relaxing Sounds?
I have always been a fan of using calming distracting audio; Whether it be while I code, sleep, meditate or study. I have downloaded apps in the past that have provided that functionality but for one reason or another I deleted them. So to remedy the problem I just wrote my own application.
What Are My Future Plans For RS?
Really after my next release to add the livestream I don’t plan on giving it much attention development wise. I will study the analytics and create a marketing campaign if the data says it is worth it. Otherwise it is just another notch on my project belt and I will continue to use for my own personal enjoyment. If you want to support me then please download my application and rate it with comments! If you really enjoy it then I ask that you purchase the pro version for $1.99.
Nativescript is a new javascript framework that allows you to create TRULY native mobile apps using the V8 engine and JavascriptCore engine for Android and iOS respectively. Using Nativescript you can create the mobile app of your dreams using the web technologies you already know and love.
How Did You Hear About It?
It’s funny actually. Last year I was reading documentation on Laravel’s website and saw an ad for Nativescript. It intrigued me so I decided to click the ad and instantly was glad I did. Although the project looked very promising, the Vue.js integration wasn’t complete yet and I wasn’t interested in going back to Angular so I left it alone. A few months later nativescript-vue was released and I came running back.
What Are The Advantages To Using Nativescript?
Well the first one is that you actually get native performance. This isn’t like Ionic or Cordova where your web app is hosted in a WebView running inside an app container. This is actual transpiled code from web technologies to mobile technologies.
Low learning curve. Since you can develop using Vue and all Vue extensions there is no extra learning curve as far as syntax is concerned. Just setup the environment and you are ready to turn your web applications into native mobile applications. If you want an intro to Vue.js check out my video course.
Extensive marketplace. The Nativescript ecosystem allows developers to create packages and are managed by npm. Easily extend the functionality of your mobile applications.
It’s FREE. Nativescript is free and open source to use for free and commercial use and has an Apache 2 license.
Want A More In Depth Explanation?
Watch my Youtube video and learn more about the power of Nativescript and even start on an example mobile application! Don’t forget to like and share the video and subscribe to my channel!
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.
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
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 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'
});
In this segment we explore the stripe product and sku apis. This allows us to do inventory management inside of our app. For the full source code go here.
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!
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.
We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept All”, you consent to the use of ALL the cookies. However, you may visit "Cookie Settings" to provide a controlled consent.
This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.