I've used Vue-cli 3 to create a Vue app and I've been trying to incorporate FCM into it. However, I've been working on it for two days and I still cannot get it working.
First, here's my
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase- app.js');
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js');
var config = {
messagingSenderId: "69625964474"
};
firebase.initializeApp(config);
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function (payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload)
// Customize notification here
const notificationTitle = 'Background Message Title';
const notificationOptions = {
body: 'Background Message body.',
icon: '/firebase-logo.png'
}
return self.registration.showNotification(notificationTitle, notificationOptions)
});
```
One solution that sorta works is I moved this file into the public folder and register it in App.vue using
const registration = await navigator.serviceWorker.register(`${process.env.BASE_URL}firebase-messaging-sw.js`)
messaging.useServiceWorker(registration)
However, then I'll be having two service workers (the other one from Vue itself).
I tried to modify vue.config.js instead trying to work with Workbox by adding the following config:
module.exports = {
pwa: {
name: 'My App',
themeColor: '#4DBA87',
msTileColor: '#000000',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
// swSrc is required in InjectManifest mode.
swSrc: 'public/firebase-messaging-sw.js'
// ...other Workbox options...
}
}
}
And then register it again in App.vue:
const registration = await navigator.serviceWorker.register(`${process.env.BASE_URL}service-worker.js`)
messaging.useServiceWorker(registration)
Then I got the following error instead:
If you are confused by the files I mentioned or how the directory of my project looks like, what I did was simply creating a PWA using vue-cli 3. And I left most of the structure untouched.
And I set up firebase in main.js:
import firebase from '#firebase/app'
Vue.config.productionTip = false
const config = {
apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
authDomain: process.env.VUE_APP_AUTH_DOMAIN,
databaseURL: process.env.VUE_APP_DATABASE_URL,
projectId: process.env.VUE_APP_PROJECT_ID,
storageBucket: process.env.VUE_APP_STORAGE_BUCKET,
messagingSenderId: process.env.VUE_APP_MESSAGING_SENDER_ID
}
firebase.initializeApp(config)
Then in App.vue:
import firebase from '#firebase/app'
import '#firebase/messaging'
const messaging = firebase.messaging()
messaging.usePublicVapidKey('PUBLIC_KEY')
The service worker is by default disabled in development mode, so running it in development will cause an HTML error page to be returned, this is the reason you are getting text/html error
You can find detailed explanation here LINK
Related
Introduction
Hello everyone,
I am trying to implement azure active directory B2c into my nuxt 3 application. Because #nuxtjs/auth-next is not yet working for nuxt 3, I am trying to make my own composable that makes use of the #azure/msal-browser npm package.
The reason I am writing this article is because it is not working. The code I created can be seen below:
Error:
Terminal
[nitro] [dev] [unhandledRejection] BrowserAuthError: non_browser_environment: Login and token requests are not supported in non-browser environments. 21:07:32
at BrowserAuthError.AuthError [as constructor]
Browser console
PerformanceClient.ts:100 Uncaught (in promise) TypeError: this.startPerformanceMeasurement is not a function
at PerformanceClient2.startMeasurement
Code:
file: /composables/useAuth.js
import * as msal from '#azure/msal-browser'
let state = {
applicationInstance: null,
}
export const useAuth = () => {
//config auth
const msalConfig = {
auth: {
clientId: '',
authority: '',
knownAuthorities: [``],
redirectUri: '',
knownAuthorities: ['']
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
}
state.applicationInstance = new msal.PublicClientApplication(msalConfig);
return {
signIn
}
}
const signIn = () => {
//handle redirect
state.applicationInstance
.addEventCallback(event => {
if(event.type == "msal:loginSuccess" && event.payload.account)
{
const account = event.payload.account
state.applicationInstance.setActiveAccount(account)
console.log(account)
}
})
//handle auth redirect
state.applicationInstance
.handleRedirectPromise()
.then(() => {
const account = state.applicationInstance.getActiveAccount()
if(!account) {
const requestParams = {
scopes: ['openid', 'offline_access', 'User.Read'],
}
state.applicationInstance.loginRedirect(requestParams)
}
})
}
file: index.vue
<script setup>
const auth = useAuth();
auth.signIn()
</script>
You need to make sure that you try to login only in the browser because Nuxt runs also server side.
You can check if you are client side with process.client or process.server for server side.
<script setup>
if (process.client) {
const auth = useAuth();
auth.signIn() // Try to sign in but only on client.
}
</script>
NuxtJS/VueJS: How to know if page was rendered on client-side only?
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getMessaging } from "firebase/messaging";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyA0dAgRPZLlgULgnOp2MH_3xtVbWPs03Cg",
authDomain: "u-63f99.firebaseapp.com",
projectId: "u-63f99",
storageBucket: "u-63f99.appspot.com",
messagingSenderId: "884933400988",
appId: "1:884933400988:web:0b7e0e37b0f23da236d059",
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const fireStore = getFirestore(app);
const messaging = getMessaging(app);
export { auth, fireStore, messaging };
i'm getting this error, i don't know why messaging service is missing while i installed firebase SDK
your problem is probably because you're testing this on localhost (http).
if you look at the console logs you might notice beside that error something like:
code: 'messaging/unsupported-browser'
and cloud messaging only support https connections so you'd have to make your localhost run on https somehow to get it to work :)
The problem: I can't have my service worker access my .env variables
The only solution I found was storing the variables into a generated js files, I am using Vue PWA for this.
Vue Config
module.exports = {
pwa: {
name: 'Fintask App',
themeColor: '#3aa9ff',
msTileColor: '#3aa9ff',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
// swSrc is required in InjectManifest mode.
swSrc: 'src/service-worker.js',
// ...other Workbox options...
}
}
};
My service worker config
importScripts('swenv.js'); // this generates an error: Uncaught ReferenceError: Cannot access 'process' before initialization
workbox.setConfig({
debug: false,
});
workbox.precaching.precacheAndRoute([]);
workbox.routing.registerRoute(
new RegExp(`${process.env.VUE_APP_API_ROOT_URL}/organization/(.*)`),
workbox.strategies.networkFirst({
cacheName: 'organization',
}),
);
my swEnvBuild that should generate my swenv.js
//swEnvBuild.js - script that is separate from webpack
require('dotenv').config();
const fs = require('fs');
fs.writeFileSync("public/swenv.js",
`const process = {
env: {
VUE_APP_API_ROOT_URL: process.env.VUE_APP_API_ROOT_URL
}
}`);
my swenv.js
const process = {
env: {
VUE_APP_API_ROOT_URL: process.env.VUE_APP_API_ROOT_URL
}
}
This is still not working for me, I did find different solutions based on this method, I just wish there was a better way
My output
swenv.js:3 Uncaught ReferenceError: Cannot access 'process' before initialization
at swenv.js:3
at service-worker.js:3
Error during service worker registration: TypeError: Failed to register a ServiceWorker for scope ('http://localhost/') with script ('http://localhost/service-worker.js'): ServiceWorker script evaluation failed
Don't know why I get this error, this result was generated in my apache server
Given the example official Nuxt end-to-end test example using Ava:
import test from 'ava'
import { Nuxt, Builder } from 'nuxt'
import { resolve } from 'path'
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:4000
test.before('Init Nuxt.js', async t => {
const rootDir = resolve(__dirname, '..')
let config = {}
try { config = require(resolve(rootDir, 'nuxt.config.js')) } catch (e) {}
config.rootDir = rootDir // project folder
config.dev = false // production build
config.mode = 'universal' // Isomorphic application
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(4000, 'localhost')
})
// Example of testing only generated html
test('Route / exits and render HTML', async t => {
let context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('<h1 class="red">Hello world!</h1>'))
})
// Close the Nuxt server
test.after('Closing server', t => {
nuxt.close()
})
How can you use Nuxt or Builder to configure/access the applications Vuex store? The example Vuex store would look like:
import Vuex from "vuex";
const createStore = () => {
return new Vuex.Store({
state: () => ({
todo: null
}),
mutations: {
receiveTodo(state, todo) {
state.todo = todo;
}
},
actions: {
async nuxtServerInit({ commit }, { app }) {
console.log(app);
const todo = await app.$axios.$get(
"https://jsonplaceholder.typicode.com/todos/1"
);
commit("receiveTodo", todo);
}
}
});
};
export default createStore;
Currently trying to run the provided Ava test, leads to an error attempting to access #nuxtjs/axios method $get:
TypeError {
message: 'Cannot read property \'$get\' of undefined',
}
I'd be able to mock $get and even $axios available on app in Vuex store method nuxtServerInit, I just need to understand how to access app in the test configuration.
Thank you for any help you can provide.
Just encountered this and after digging so many tutorial, I pieced together a solution.
You have essentially import your vuex store into Nuxt when using it programmatically. This is done by:
Importing Nuxt's config file
Adding to the config to turn off everything else but enable store
Load the Nuxt instance and continue your tests
Here's a working code (assuming your ava and dependencies are set up)
// For more info on why this works, check this aweomse guide by this post in getting this working
// https://medium.com/#brandonaaskov/how-to-test-nuxt-stores-with-jest-9a5d55d54b28
import test from 'ava'
import jsdom from 'jsdom'
import { Nuxt, Builder } from 'nuxt'
import nuxtConfig from '../nuxt.config' // your nuxt.config
// these boolean switches turn off the build for all but the store
const resetConfig = {
loading: false,
loadingIndicator: false,
fetch: {
client: false,
server: false
},
features: {
store: true,
layouts: false,
meta: false,
middleware: false,
transitions: false,
deprecations: false,
validate: false,
asyncData: false,
fetch: false,
clientOnline: false,
clientPrefetch: false,
clientUseUrl: false,
componentAliases: false,
componentClientOnly: false
},
build: {
indicator: false,
terser: false
}
}
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:5000 BEFORE running your tests. We are combining our config file with our resetConfig using Object.assign into an empty object {}
test.before('Init Nuxt.js', async (t) => {
t.timeout(600000)
const config = Object.assign({}, nuxtConfig, resetConfig, {
srcDir: nuxtConfig.srcDir, // don't worry if its not in your nuxt.config file. it has a default
ignore: ['**/components/**/*', '**/layouts/**/*', '**/pages/**/*']
})
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(5000, 'localhost')
})
// Then run our tests using the nuxt we defined initially
test.serial('Route / exists and renders correct HTML', async (t) => {
t.timeout(600000) // Sometimes nuxt's response is slow. We increase the timeont to give it time to render
const context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('preload'))
// t.true(true)
})
test.serial('Route / exits and renders title', async (t) => {
t.timeout(600000)
const { html } = await nuxt.renderRoute('/', {})
const { JSDOM } = jsdom // this was the only way i could get JSDOM to work. normal import threw a functione error
const { document } = (new JSDOM(html)).window
t.true(document.title !== null && document.title !== undefined) // simple test to check if site has a title
})
Doing this should work. HOWEVER, You may still get some errors
✖ Timed out while running tests. If you get this you're mostly out of luck. I thought the problem was with Ava given that it didn't give a descriptive error (and removing any Nuxt method seemed to fix it), but so far even with the above snippet sometimes it works and sometimes it doesn't.
My best guess at this time is that there is a delay on Nuxt's side using either renderRouter or renderAndGetWindow that ava doesn't wait for, but on trying any of these methods ava almost immediately "times out" despite the t.timeout being explicitly set for each test. So far my research has lead me to checking the timeout for renderAndGetWindow (if it exists, but the docs doesn't indicate such).
That's all i've got.
I was following this article here (which is not complete unfortunately) in attempt to learn how to friend Ionic 3 based PWA and Firebase Cloud Messaging: Push Notifications with FCM
What I did:
as advised in the article added FCM libraries into service-worker.js:
'use strict';
importScripts('./build/sw-toolbox.js');
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-messaging');
firebase.initializeApp({
// get this from Firebase console, Cloud messaging section
'messagingSenderId': '47286327412'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler((payload) => {
console.log('Received background message ', payload);
// here you can override some options describing what's in the message;
// however, the actual content will come from the service sending messages
const notificationOptions = {
icon: '/assets/img/appicon.png'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});
self.toolbox.options.cache = {
name: 'ionic-cache'
};
// pre-cache our key assets
self.toolbox.precache(
[
'./build/main.js',
'./build/vendor.js',
'./build/main.css',
'./build/polyfills.js',
'index.html',
'manifest.json'
]
);
// dynamically cache any other local assets
self.toolbox.router.any('/*', self.toolbox.cacheFirst);
// for any other requests go to the network, cache,
// and then only use that cached resource if your user goes offline
self.toolbox.router.default = self.toolbox.networkFirst;
Then created Firebase Messaging based provider here:
import { Injectable } from "#angular/core";
import * as firebase from 'firebase';
import { Storage } from '#ionic/storage';
#Injectable()
export class FirebaseMessagingProvider {
private messaging: firebase.messaging.Messaging;
private unsubscribeOnTokenRefresh = () => {};
constructor(
private storage: Storage
) {
this.messaging = firebase.messaging();
}
public enableNotifications() {
console.log('Requesting permission...');
return this.messaging.requestPermission().then(() => {
console.log('Permission granted');
// token might change - we need to listen for changes to it and update it
this.setupOnTokenRefresh();
return this.updateToken();
});
}
public disableNotifications() {
this.unsubscribeOnTokenRefresh();
this.unsubscribeOnTokenRefresh = () => {};
return this.storage.set('fcmToken','').then();
}
private updateToken() {
return this.messaging.getToken().then((currentToken) => {
if (currentToken) {
// we've got the token from Firebase, now let's store it in the database
return this.storage.set('fcmToken', currentToken);
} else {
console.log('No Instance ID token available. Request permission to generate one.');
}
});
}
private setupOnTokenRefresh(): void {
this.unsubscribeOnTokenRefresh = this.messaging.onTokenRefresh(() => {
console.log("Token refreshed");
this.storage.set('fcmToken','').then(() => { this.updateToken(); });
});
}
}
And now during app initialization I call enableNotifications() and get error that says that default service worker is not found (404):
A bad HTTP response code (404) was received when fetching the script.
:8100/firebase-messaging-sw.js Failed to load resource: net::ERR_INVALID_RESPONSE
If I move service-worker.js firebase related stuff into default service worker in WWW folder - I get general error from Firebase (Error, failed to register service worker).
QUESTIONS:
- is there a fresh guide on Ionic 3's PWA & FCM?
- at high level what is the difference in registering service workers in Ionic 3 vs Angular? I did watch the tutorial about Angular but can't figure how to do the same in Ionic 3.
UPDATE: the below is valid as of today (02/12/2018) and most likely will be less relevant once AngularFire2 supports messaging module. So take the below with that assumption...
OK I researched and finally made it work on my Ionic 3 PWA, so I am posting solution here:
Prerequisites:
I created ionic blank app (just a home page)
installed angularfire2 and firebase ("angularfire2": "5.0.0-rc.4","firebase": "4.9.1") using npm install, I used specifically 5.0.0-rc.4" cause I had stability issues with latest one;(
created config (filename environment.ts in src folder):
export const firebaseConfig = {
apiKey: "Your Stuff Here from FB",
authDomain: "YOURAPPNAME.firebaseapp.com",
databaseURL: "https://YOURAPPNAME.firebaseio.com",
projectId: "YOURAPPNAME",
storageBucket: "YOURAPPNAME.appspot.com",
messagingSenderId: "FROMFIREBASECONEOLE"
};
I modified app.module.ts to add firebase and angularfire2 this way:
...
import { AngularFireModule } from 'angularfire2';
import 'firebase/messaging'; // only import firebase messaging or as needed;
import { firebaseConfig } from '../environment';
import { FirebaseMessagingProvider } from '../providers/firebase-messaging';
...
#NgModule({
declarations: [
MyApp,
HomePage
],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
AngularFireModule.initializeApp(firebaseConfig),
IonicStorageModule.forRoot()
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
HomePage
],
providers: [
FirebaseMessagingProvider,
StatusBar,
SplashScreen,
{provide: ErrorHandler, useClass: IonicErrorHandler}
]
})
export class AppModule {}
Here we also import our provider whose code is below:
in providers folder I created firebase-messaging.ts like this:
import { Injectable } from "#angular/core";
import { FirebaseApp } from 'angularfire2';
// I am importing simple ionic storage (local one), in prod this should be remote storage of some sort.
import { Storage } from '#ionic/storage';
#Injectable()
export class FirebaseMessagingProvider {
private messaging;
private unsubscribeOnTokenRefresh = () => {};
constructor(
private storage: Storage,
private app: FirebaseApp
) {
this.messaging = app.messaging();
navigator.serviceWorker.register('service-worker.js').then((registration) => {
this.messaging.useServiceWorker(registration);
//this.disableNotifications()
this.enableNotifications();
});
}
public enableNotifications() {
console.log('Requesting permission...');
return this.messaging.requestPermission().then(() => {
console.log('Permission granted');
// token might change - we need to listen for changes to it and update it
this.setupOnTokenRefresh();
return this.updateToken();
});
}
public disableNotifications() {
this.unsubscribeOnTokenRefresh();
this.unsubscribeOnTokenRefresh = () => {};
return this.storage.set('fcmToken','').then();
}
private updateToken() {
return this.messaging.getToken().then((currentToken) => {
if (currentToken) {
// we've got the token from Firebase, now let's store it in the database
console.log(currentToken)
return this.storage.set('fcmToken', currentToken);
} else {
console.log('No Instance ID token available. Request permission to generate one.');
}
});
}
private setupOnTokenRefresh(): void {
this.unsubscribeOnTokenRefresh = this.messaging.onTokenRefresh(() => {
console.log("Token refreshed");
this.storage.set('fcmToken','').then(() => { this.updateToken(); });
});
}
}
Please note I init the firebase app and then in constructor we register ionic's default service worker (service-worker.js) that contains the following right after whatever is there by default:
service-worker.js:
// firebase messaging part:
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.9.0/firebase-messaging.js');
firebase.initializeApp({
// get this from Firebase console, Cloud messaging section
'messagingSenderId': 'YOURIDFROMYOURFIREBASECONSOLE'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload) {
console.log('Received background message ', payload);
// here you can override some options describing what's in the message;
// however, the actual content will come from the Webtask
const notificationOptions = {
icon: '/assets/images/logo-128.png'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});
At this point you also need to make sure you enabled your app as PWA, there is a good guide from Josh Morony and today there was a video stream on youtube that covers it. In TLDR you need to uncomment this in your index.html:
index.html in src uncomment:
<!-- un-comment this code to enable service worker -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(() => console.log('service worker installed'))
.catch(err => console.error('Error', err));
}
</script>
OK almost the last thing - your manifest.json (in src) should have exact line:
"gcm_sender_id": "103953800507"
This concludes initial stuff on the client. Please note I didn't implement yet anything to handle notifications while user is in app itself, think for now it just handles when a message is sent from a server while your tab is not in focus (that is what I tested).
Now you want to go to your firebase console and obtain server key (click setting gear icon, then see cloud messaging section there). Copy server key. Also run the client (ionic serve and capture your local token (i just console.logged it). Now try sending yourself the message using a POST method. ( I did it with Postman)
// method: "POST",
//url: "https://fcm.googleapis.com/fcm/send",
// get the key from Firebase console
headers: { Authorization: `key=${fcmServerKey}` },
json: {
"notification": {
"title": "Message title",
"body": "Message body",
"click_action": "URL to your app?"
},
// userData is where your client stored the FCM token for the given user
// it should be read from the database
"to": userData.fcmRegistrationKey
}
So by doing all this I was able to reliable send myself a message WHILE the app was in background. I am yet to handle foreground but this SO question is about how to init default service worker and marry it with FCM.
I hope this will help some learners in future.
I have successfully implemented the process and got success response on API calls. But no notification popup coming on my browser. Any idea?
api: https://fcm.googleapis.com/fcm/send
response got:
{"multicast_id":6904414188195222649,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1545375125056264%e609af1cf9fd7ecd"}]}
cheth the attached url of my console: