Skip to content

Commit

Permalink
3 Add push notifications (#7)
Browse files Browse the repository at this point in the history
* Implement receiving notifications

* Add sending notifications

* Change notification description and icon
  • Loading branch information
Gojodzojo authored Jun 7, 2022
1 parent c302a5d commit e3d78e9
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 28 deletions.
3 changes: 2 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"assets": [
"src/favicon.ico",
"src/assets",
"src/manifest.webmanifest"
"src/manifest.webmanifest",
"src/firebase-messaging-sw.js"
],
"styles": [
"src/styles.scss"
Expand Down
48 changes: 40 additions & 8 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import * as functions from "firebase-functions";

// // Start writing Firebase Functions
// // https://firebase.google.com/docs/functions/typescript
//
export const helloWorld = functions.https.onRequest((request, response) => {
functions.logger.info("Hello logs!", {structuredData: true});
response.send("Hello from Firebase!");
import {pubsub} from "firebase-functions";
import * as admin from "firebase-admin";
import {MessagingPayload} from "firebase-admin/lib/messaging/messaging-api";

const app = admin.initializeApp();
const firestore = app.firestore();
const messaging = app.messaging();

export const sendPush = pubsub.schedule("every 1 minutes").onRun(async () => {
const users = await firestore.collection("users").listDocuments();
users.forEach(async (user) => {
const plants = await user.collection("plants").get();
plants.forEach(async (plant) => {
const plantData = plant.data();

const timeFormatOptions: Intl.DateTimeFormatOptions = {
timeZone: plantData.timezone,
timeStyle: "short",
};
// eslint-disable-next-line
const timeInPlantTimezone = new Intl.DateTimeFormat("PL", timeFormatOptions).format();

if (timeInPlantTimezone === plantData.waterTime) {
const payload: MessagingPayload = {
notification: {
title: `It's time to water ${plantData.name}`,
body: plantData.description,
image: plantData.imageUrl ? plantData.imageUrl : "https://watering-reminder.web.app/assets/images/default_plant_image.png",
icon: "https://watering-reminder.web.app/assets/icons/icon-144x144.png",
},
};

const fcmTokens = await user.collection("fcmTokens").get();
fcmTokens.forEach(async (token) => {
const tokenData = token.data() as { fcmToken: string };
await messaging.sendToDevice(tokenData.fcmToken, payload);
});
}
});
});
});
6 changes: 3 additions & 3 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ import { connectAuthEmulator } from '@firebase/auth'
provideMessaging(() => getMessaging()),
provideAuth(() => {
const auth = getAuth()
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true })
if(!environment.production) connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true })
return auth
}),
provideFirestore(() => {
const firestore = getFirestore()
connectFirestoreEmulator(firestore, 'localhost', 8080)
if(!environment.production) connectFirestoreEmulator(firestore, 'localhost', 8080)
enableIndexedDbPersistence(firestore)
return firestore
}),
provideStorage(() => {
const storage = getStorage()
connectStorageEmulator(storage, 'localhost', 9199)
if(!environment.production) connectStorageEmulator(storage, 'localhost', 9199)
return storage
}),
],
Expand Down
38 changes: 23 additions & 15 deletions src/app/pages/dashboard/dashboard.component.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
<p>dashboard works!</p>
<button (click)="logout()">Logout</button>
<div *ngIf="notificationsService.areNotificationsAllowed; then notificationsAllowedBlock else notificationsNotAllowedBlock"></div>

<div *ngIf="isLoading; then loadingBlock else loadedBlock"></div>

<ng-template #loadingBlock>
<p>Loading</p>
<ng-template #notificationsNotAllowedBlock>
<p>You have to allow push notifications to use this app.</p>
</ng-template>

<ng-template #loadedBlock>
<div *ngIf="plantsService.plantsAreEmpty$ | async; then plantsEmptyBlock else plantsNotEmptyBlock"></div>
<ng-template #notificationsAllowedBlock>
<p>dashboard works!</p>
<button (click)="logout()">Logout</button><br>

<div *ngIf="isLoading; then loadingBlock else loadedBlock"></div>

<ng-template #plantsEmptyBlock>
<p>You have no plants.</p>
<button routerLink="/add-plant">Add your first plant</button>
<ng-template #loadingBlock>
<p>Loading</p>
</ng-template>

<ng-template #plantsNotEmptyBlock>
<button routerLink="/add-plant">Add a plant</button>
<app-plants-list-element *ngFor="let plant of plantsService.plants$ | async" [plant]="plant">
</app-plants-list-element>
<ng-template #loadedBlock>
<div *ngIf="plantsService.plantsAreEmpty$ | async; then plantsEmptyBlock else plantsNotEmptyBlock"></div>

<ng-template #plantsEmptyBlock>
<p>You have no plants.</p>
<button routerLink="/add-plant">Add your first plant</button>
</ng-template>

<ng-template #plantsNotEmptyBlock>
<button routerLink="/add-plant">Add a plant</button>
<app-plants-list-element *ngFor="let plant of plantsService.plants$ | async" [plant]="plant">
</app-plants-list-element>
</ng-template>
</ng-template>
</ng-template>
10 changes: 9 additions & 1 deletion src/app/pages/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Component } from '@angular/core'
import { Messaging, getToken, } from '@angular/fire/messaging'
import { Router } from '@angular/router'
import { AuthService } from 'src/app/services/auth/auth.service'
import { NotificationsService } from 'src/app/services/notifiications/notifications.service'
import { PlantsService } from 'src/app/services/plants/plants.service'

@Component({
Expand All @@ -10,12 +12,18 @@ import { PlantsService } from 'src/app/services/plants/plants.service'
})
export class DashboardComponent {
isLoading = true


constructor(public auth: AuthService, public router: Router, public plantsService: PlantsService) {
constructor(public auth: AuthService, public router: Router, public plantsService: PlantsService, public notificationsService: NotificationsService) {
this.plantsService.plants$.subscribe(() => this.isLoading = false)

if(!this.notificationsService.areNotificationsAllowed) {
this.notificationsService.registerToken()
}
}

async logout() {
await this.notificationsService.unregisterToken()
await this.auth.logout()
await this.router.navigate(['/login'])
}
Expand Down
16 changes: 16 additions & 0 deletions src/app/services/notifiications/notifications.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { NotificationsService } from './notifications.service';

describe('NotificationsService', () => {
let service: NotificationsService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NotificationsService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
41 changes: 41 additions & 0 deletions src/app/services/notifiications/notifications.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { addDoc, collection, deleteDoc, doc, Firestore, setDoc } from '@angular/fire/firestore'
import { Messaging } from '@angular/fire/messaging'
import { getToken } from '@firebase/messaging'
import { lastValueFrom, take } from 'rxjs'
import { AuthService } from '../auth/auth.service'

@Injectable({
providedIn: 'root'
})
export class NotificationsService {
areNotificationsAllowed = Notification.permission === 'granted'

constructor(private messaging: Messaging, private auth: AuthService, private firestore: Firestore) { }

async registerToken() {
const user = await lastValueFrom(this.auth.user$.pipe(take(1)))

if (!user) {
throw new Error("User is not logged in")
}

const fcmToken = await getToken(this.messaging)
const d = doc(this.firestore, "users", user.uid, "fcmTokens", fcmToken)
await setDoc(d, { fcmToken })
this.areNotificationsAllowed = true
}

async unregisterToken() {
const user = await lastValueFrom(this.auth.user$.pipe(take(1)))

if (!user) {
throw new Error("User is not logged in")
}

const fcmToken = await getToken(this.messaging)
const d = doc(this.firestore, "users", user.uid, "fcmTokens", fcmToken)
await deleteDoc(d)
this.areNotificationsAllowed = false
}
}
20 changes: 20 additions & 0 deletions src/firebase-messaging-sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
importScripts('https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.10.0/firebase-messaging.js');

// Initialize the Firebase app in the service worker by passing in
// your app's Firebase config object.
// https://firebase.google.com/docs/web/setup#config-object
firebase.initializeApp({
apiKey: 'api-key',
authDomain: 'project-id.firebaseapp.com',
databaseURL: 'https://project-id.firebaseio.com',
projectId: 'project-id',
storageBucket: 'project-id.appspot.com',
messagingSenderId: 'sender-id',
appId: 'app-id',
measurementId: 'G-measurement-id',
});

// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();

0 comments on commit e3d78e9

Please sign in to comment.