diff --git a/angular.json b/angular.json index 211a8b4..699fef8 100644 --- a/angular.json +++ b/angular.json @@ -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" diff --git a/functions/src/index.ts b/functions/src/index.ts index 283db56..97c44a6 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -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); + }); + } + }); + }); }); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ecb3ac3..0c6cebe 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 }), ], diff --git a/src/app/pages/dashboard/dashboard.component.html b/src/app/pages/dashboard/dashboard.component.html index 2298edd..3933618 100644 --- a/src/app/pages/dashboard/dashboard.component.html +++ b/src/app/pages/dashboard/dashboard.component.html @@ -1,23 +1,31 @@ -

dashboard works!

- +
-
- - -

Loading

+ +

You have to allow push notifications to use this app.

- -
+ +

dashboard works!

+
+ +
- -

You have no plants.

- + +

Loading

- - - - + +
+ + +

You have no plants.

+ +
+ + + + + +
\ No newline at end of file diff --git a/src/app/pages/dashboard/dashboard.component.ts b/src/app/pages/dashboard/dashboard.component.ts index 4cf46d7..103ea8e 100644 --- a/src/app/pages/dashboard/dashboard.component.ts +++ b/src/app/pages/dashboard/dashboard.component.ts @@ -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({ @@ -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']) } diff --git a/src/app/services/notifiications/notifications.service.spec.ts b/src/app/services/notifiications/notifications.service.spec.ts new file mode 100644 index 0000000..c939c4c --- /dev/null +++ b/src/app/services/notifiications/notifications.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/services/notifiications/notifications.service.ts b/src/app/services/notifiications/notifications.service.ts new file mode 100644 index 0000000..8c63505 --- /dev/null +++ b/src/app/services/notifiications/notifications.service.ts @@ -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 + } +} diff --git a/src/firebase-messaging-sw.js b/src/firebase-messaging-sw.js new file mode 100644 index 0000000..7d000a9 --- /dev/null +++ b/src/firebase-messaging-sw.js @@ -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(); \ No newline at end of file