From 38a849d8a1d05eebe5cd1cb94e3e18d800d499ca Mon Sep 17 00:00:00 2001
From: Adrien MOREL <adrien.morel2.etu@univ-lille.fr>
Date: Fri, 23 Dec 2022 15:10:13 +0100
Subject: [PATCH] Notifications + Background refresh for notifications done

---
 .gitlab-ci.yml                                |   1 +
 README.md                                     |  13 +-
 android/app/build.gradle                      |   8 +-
 android/app/src/main/AndroidManifest.xml      |   2 +
 android/build.gradle                          |  10 +-
 ios/Podfile.lock                              |  18 ++
 ios/Runner/AppDelegate.swift                  |   3 +
 ios/Runner/Base.lproj/Main.storyboard         |  13 +-
 ios/Runner/Info.plist                         |  22 +-
 .../background_tasks_utils.dart               |  81 ++++++
 lib/Notifications/custom_notifications.dart   |  30 +++
 lib/bloc/watchlist_bloc.dart                  |   2 -
 lib/main.dart                                 | 254 ++++++++++--------
 macos/Flutter/GeneratedPluginRegistrant.swift |   2 +
 pubspec.lock                                  |  91 +++++++
 pubspec.yaml                                  |   3 +
 16 files changed, 417 insertions(+), 136 deletions(-)
 create mode 100644 lib/BackgroundTasks/background_tasks_utils.dart
 create mode 100644 lib/Notifications/custom_notifications.dart

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c7a2981..5a60922 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,4 @@
+# Docker container with flutter installed to run on Univ-Lille's servers
 image: cirrusci/flutter
 
 stages:
diff --git a/README.md b/README.md
index 6265751..7810922 100644
--- a/README.md
+++ b/README.md
@@ -4,13 +4,8 @@ A new Flutter project.
 
 ## Getting Started
 
-This project is a starting point for a Flutter application.
+## Testing
 
-A few resources to get you started if this is your first Flutter project:
-
-- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
-
-For help getting started with Flutter development, view the
-[online documentation](https://docs.flutter.dev/), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+```bash
+adb shell cmd jobscheduler run -f com.example.is_eat_safe 999
+```
diff --git a/android/app/build.gradle b/android/app/build.gradle
index e1b778d..1bcf9ac 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -26,10 +26,11 @@ apply plugin: 'kotlin-android'
 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 
 android {
-    compileSdkVersion flutter.compileSdkVersion
+    compileSdkVersion 33
     ndkVersion flutter.ndkVersion
 
     compileOptions {
+        coreLibraryDesugaringEnabled true
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
@@ -48,6 +49,7 @@ android {
         // You can update the following values to match your application needs.
         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
         minSdkVersion 18
+        multiDexEnabled true
         targetSdkVersion flutter.targetSdkVersion
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
@@ -67,5 +69,9 @@ flutter {
 }
 
 dependencies {
+    implementation 'androidx.window:window:1.0.0'
+    implementation 'androidx.window:window-java:1.0.0'
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+    implementation 'androidx.multidex:multidex:2.0.1'
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 }
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fed62f6..0578b5b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,7 +1,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.example.is_eat_safe">
     <uses-permission android:name="android.permission.INTERNET"/>
    <application
+        tools:replace="android:label"
         android:label="is_eat_safe"
         android:name="${applicationName}"
         android:icon="@mipmap/ic_launcher">
diff --git a/android/build.gradle b/android/build.gradle
index 83ae220..2b14f9e 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,12 +1,17 @@
 buildscript {
     ext.kotlin_version = '1.6.10'
+    ext {
+        compileSdkVersion = 33                // or latest
+        targetSdkVersion = 33                // or latest
+        appCompatVersion = "1.4.2"           // or latest
+    }
     repositories {
         google()
         mavenCentral()
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:7.1.2'
+        classpath 'com.android.tools.build:gradle:7.1.3'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }
@@ -15,6 +20,9 @@ allprojects {
     repositories {
         google()
         mavenCentral()
+        maven{
+            url "${project(':background_fetch').projectDir}/libs"
+        }
     }
 }
 
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 0fe9ad2..9d9e36d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,20 +1,29 @@
 PODS:
+  - background_fetch (1.1.2):
+    - Flutter
   - barcode_scan2 (0.0.1):
     - Flutter
     - MTBBarcodeScanner
     - SwiftProtobuf
   - Flutter (1.0.0)
+  - flutter_background_service_ios (0.0.3):
+    - Flutter
   - flutter_barcode_scanner (2.0.0):
     - Flutter
+  - flutter_local_notifications (0.0.1):
+    - Flutter
   - MTBBarcodeScanner (5.0.11)
   - shared_preferences_ios (0.0.1):
     - Flutter
   - SwiftProtobuf (1.20.3)
 
 DEPENDENCIES:
+  - background_fetch (from `.symlinks/plugins/background_fetch/ios`)
   - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`)
   - Flutter (from `Flutter`)
+  - flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
   - flutter_barcode_scanner (from `.symlinks/plugins/flutter_barcode_scanner/ios`)
+  - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
   - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
 
 SPEC REPOS:
@@ -23,19 +32,28 @@ SPEC REPOS:
     - SwiftProtobuf
 
 EXTERNAL SOURCES:
+  background_fetch:
+    :path: ".symlinks/plugins/background_fetch/ios"
   barcode_scan2:
     :path: ".symlinks/plugins/barcode_scan2/ios"
   Flutter:
     :path: Flutter
+  flutter_background_service_ios:
+    :path: ".symlinks/plugins/flutter_background_service_ios/ios"
   flutter_barcode_scanner:
     :path: ".symlinks/plugins/flutter_barcode_scanner/ios"
+  flutter_local_notifications:
+    :path: ".symlinks/plugins/flutter_local_notifications/ios"
   shared_preferences_ios:
     :path: ".symlinks/plugins/shared_preferences_ios/ios"
 
 SPEC CHECKSUMS:
+  background_fetch: aaf8fc9d1da7f04e5373aabd4bb239704a5be6eb
   barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0
   Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+  flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
   flutter_barcode_scanner: 7a1144744c28dc0c57a8de7218ffe5ec59a9e4bf
+  flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
   MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
   shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
   SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 70693e4..c17fa4f 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -8,6 +8,9 @@ import Flutter
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
     GeneratedPluginRegistrant.register(with: self)
+    if #available(iOS 10.0, *) {
+  UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
+    }
     return super.application(application, didFinishLaunchingWithOptions: launchOptions)
   }
 }
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
index f3c2851..08dbc27 100644
--- a/ios/Runner/Base.lproj/Main.storyboard
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -1,8 +1,10 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <device id="retina6_12" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
         <!--Flutter View Controller-->
@@ -14,13 +16,14 @@
                         <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
                     </layoutGuides>
                     <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
-                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                     </view>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
             </objects>
+            <point key="canvasLocation" x="-26" y="-76"/>
         </scene>
     </scenes>
 </document>
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 78a3e86..4a99181 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -2,8 +2,13 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-    <key>NSCameraUsageDescription</key>
-    <string>Autorisez l'accès à la caméra pour pouvoir scanner un produit.</string>
+	<key>BGTaskSchedulerPermittedIdentifiers</key>
+	<array>
+		<string>com.transistorsoft.fetch</string>
+		<string>com.transistorsoft.customtask</string>
+	</array>
+	<key>CADisableMinimumFrameDurationOnPhone</key>
+	<true/>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 	<key>CFBundleDisplayName</key>
@@ -26,6 +31,15 @@
 	<string>$(FLUTTER_BUILD_NUMBER)</string>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
+	<key>NSCameraUsageDescription</key>
+	<string>Autorisez l'accès à la caméra pour pouvoir scanner un produit.</string>
+	<key>UIApplicationSupportsIndirectInputEvents</key>
+	<true/>
+	<key>UIBackgroundModes</key>
+	<array>
+		<string>fetch</string>
+		<string>processing</string>
+	</array>
 	<key>UILaunchStoryboardName</key>
 	<string>LaunchScreen</string>
 	<key>UIMainStoryboardFile</key>
@@ -45,9 +59,5 @@
 	</array>
 	<key>UIViewControllerBasedStatusBarAppearance</key>
 	<false/>
-	<key>CADisableMinimumFrameDurationOnPhone</key>
-	<true/>
-	<key>UIApplicationSupportsIndirectInputEvents</key>
-	<true/>
 </dict>
 </plist>
diff --git a/lib/BackgroundTasks/background_tasks_utils.dart b/lib/BackgroundTasks/background_tasks_utils.dart
new file mode 100644
index 0000000..6926744
--- /dev/null
+++ b/lib/BackgroundTasks/background_tasks_utils.dart
@@ -0,0 +1,81 @@
+import 'dart:ffi';
+
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:background_fetch/background_fetch.dart';
+import 'package:is_eat_safe/Notifications/custom_notifications.dart';
+import 'package:is_eat_safe/models/watchlist_item.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import '../api_utils.dart';
+
+class BackgroundUtils{
+
+  @pragma('vm:entry-point')
+  static void backgroundFetchHeadlessTask(HeadlessTask task) async {
+  String taskId = task.taskId;
+  bool isTimeout = task.timeout;
+  if (isTimeout) {
+    // This task has exceeded its allowed running-time.
+    // You must stop what you're doing and immediately .finish(taskId)
+    print("[BackgroundFetch] Headless task timed-out: $taskId");
+    BackgroundFetch.finish(taskId);
+    return;
+  }
+  print('[BackgroundFetch] Headless event received.');
+  onHeadlessBackgroundFetch();
+  BackgroundFetch.finish(taskId);
+  }
+
+  static Future<void> configureBackgroundFetch(SharedPreferences prefs, FlutterLocalNotificationsPlugin fln) async {
+    int status = await BackgroundFetch.configure(BackgroundFetchConfig(
+        minimumFetchInterval: 15,
+        requiredNetworkType: NetworkType.ANY
+    ), (String taskId) async {  // <-- Event callback.
+      // This is the fetch-event callback.
+      print("[BackgroundFetch] taskId: $taskId");
+
+      // Use a switch statement to route task-handling.
+      switch (taskId) {
+        case 'com.transistorsoft.customtask':
+          print("Received custom task");
+          break;
+        default:
+          print("Default fetch task");
+      }
+      onBackgroundFetch(prefs,fln);
+      // Finish, providing received taskId.
+      BackgroundFetch.finish(taskId);
+    }, (String taskId) async {  // <-- Event timeout callback
+      // This task has exceeded its allowed running-time.  You must stop what you're doing and immediately .finish(taskId)
+      print("[BackgroundFetch] TIMEOUT taskId: $taskId");
+      BackgroundFetch.finish(taskId);
+    });
+
+// Step 2:  Schedule a custom "oneshot" task "com.transistorsoft.customtask" to execute 5000ms from now.
+    BackgroundFetch.scheduleTask(TaskConfig(
+        taskId: "com.transistorsoft.customtask",
+        delay: 5000  // <-- milliseconds
+    ));
+  }
+
+
+  static Future<void> onBackgroundFetch(SharedPreferences prefs, FlutterLocalNotificationsPlugin fln) async{
+    final List<String> items = prefs.getStringList('watchlist_items') ?? [];
+    //CustomNotifications.showBigTextNotification(title: "Danger", body: "Un article de votre liste a été rapplé", fln: fln);
+    for (var e in items) {
+      WatchlistItem item = await ApiUtils.fetchWLItem(e);
+      if(item.isRecalled){
+        CustomNotifications.showBigTextNotification(title: "Danger", body: "Un article de votre liste a été rappelé", fln: fln);
+        return;
+      }
+    }
+  }
+
+  static Future<void> onHeadlessBackgroundFetch() async {
+    final prefs = await SharedPreferences.getInstance();
+    FlutterLocalNotificationsPlugin fln = FlutterLocalNotificationsPlugin();
+    CustomNotifications.initialize(fln);
+    onBackgroundFetch(prefs,fln);
+  }
+
+}
\ No newline at end of file
diff --git a/lib/Notifications/custom_notifications.dart b/lib/Notifications/custom_notifications.dart
new file mode 100644
index 0000000..3e4525e
--- /dev/null
+++ b/lib/Notifications/custom_notifications.dart
@@ -0,0 +1,30 @@
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+class CustomNotifications{
+
+  static Future initialize(FlutterLocalNotificationsPlugin fln) async {
+    var androidInit = const AndroidInitializationSettings('mipmap/ic_launcher');
+    var iosInit = const DarwinInitializationSettings();
+    var initSettings = InitializationSettings(
+        android: androidInit,
+        iOS: iosInit);
+    await fln.initialize(initSettings);
+  }
+
+  static Future showBigTextNotification({required String title, required String body, required FlutterLocalNotificationsPlugin fln
+  } ) async {
+    AndroidNotificationDetails androidPlatformChannelSpecifics =
+    const AndroidNotificationDetails(
+      'watchlist_warning',
+      'iseatsafe_channel',
+      importance: Importance.max,
+      priority: Priority.high,
+    );
+
+    var not = NotificationDetails(android: androidPlatformChannelSpecifics,
+        iOS: const DarwinNotificationDetails()
+    );
+    await fln.show(0, title, body, not);
+  }
+
+}
\ No newline at end of file
diff --git a/lib/bloc/watchlist_bloc.dart b/lib/bloc/watchlist_bloc.dart
index 3447426..af705e9 100644
--- a/lib/bloc/watchlist_bloc.dart
+++ b/lib/bloc/watchlist_bloc.dart
@@ -40,8 +40,6 @@ class WatchlistBloc extends Bloc<WatchlistEvent, WatchlistState> {
     try {
       var it = await ApiUtils.fetchWLItem(toBeAddedItemId);
 
-      //TODO Api call to get real data
-
       if (state is WatchlistInitial) {
         var tmp = <WatchlistItem>[];
         tmp.add(it);
diff --git a/lib/main.dart b/lib/main.dart
index 9b65659..41e92db 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,5 +1,11 @@
+import 'dart:async';
+import 'dart:io' show Platform;
+import 'dart:ui';
+
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:is_eat_safe/BackgroundTasks/background_tasks_utils.dart';
+import 'package:is_eat_safe/Notifications/custom_notifications.dart';
 import 'package:is_eat_safe/api_utils.dart';
 import 'package:is_eat_safe/bloc/watchlist_bloc.dart';
 import 'package:is_eat_safe/models/watchlist_item.dart';
@@ -8,6 +14,8 @@ import 'package:is_eat_safe/views/rappel_listview.dart';
 import 'package:barcode_scan2/barcode_scan2.dart';
 import 'package:is_eat_safe/views/watchlist_view.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:background_fetch/background_fetch.dart';
 
 import 'bloc/produit_bloc.dart';
 
@@ -15,6 +23,11 @@ late WatchlistBloc _watchlistBloc;
 
 late ProduitBloc _produitBloc;
 
+final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
+
+
+
+
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
 
@@ -35,6 +48,15 @@ void main() async {
     theme: ThemeData(primarySwatch: Colors.orange),
     debugShowCheckedModeBanner: false,
     home: const MyApp(),));
+
+
+  //Fetch data from api and compares it to local data, if data matches emit local notification
+  //See https://pub.dev/packages/background_fetch for more info
+  if(Platform.isAndroid) {
+    BackgroundFetch.registerHeadlessTask(BackgroundUtils.backgroundFetchHeadlessTask);
+  }
+  CustomNotifications.initialize(flutterLocalNotificationsPlugin);
+  BackgroundUtils.configureBackgroundFetch(prefs, flutterLocalNotificationsPlugin);
 }
 
 class MyApp extends StatefulWidget {
@@ -56,120 +78,126 @@ class _MyAppState extends State<MyApp> {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
-        appBar: AppBar(
-          title: _searchBoolean && _index == 0 ? _searchTextField() : const Text('IsEatSafe'),
-          actions: [
-            //add
-            if (_index == 0)
-              !_searchBoolean
-                  ?  IconButton(
-                      icon: const Icon(Icons.search),
-                      onPressed: () {
-                        setState(() {
-                          _searchBoolean = true;
-                        });
-                      })
-                  : IconButton(
-                      icon: const Icon(Icons.clear),
-                      onPressed: () {
-                        setState(() {
-                          _produitBloc.add(ProduitRefreshed());
-                          _searchBoolean = false;
-                        });
-                      }),
-          ],
-          toolbarHeight: 40,
-        ),
-        body: Stack(
-          children: <Widget>[
-            Offstage(
-              offstage: _index != 0,
-              child: TickerMode(
-                enabled: _index == 0,
-                child: BlocProvider<ProduitBloc>(
-                    create: (context) => _produitBloc..add(ProduitFetched()),
-                    child: const RappelListView()),
-              ),
+      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
+      appBar: AppBar(
+        title: _searchBoolean && _index == 0 ? _searchTextField() : const Text(
+            'IsEatSafe'),
+        actions: [
+          //add
+          if (_index == 0)
+            !_searchBoolean
+                ? IconButton(
+                icon: const Icon(Icons.search),
+                onPressed: () {
+                  setState(() {
+                    _searchBoolean = true;
+                  });
+                })
+                : IconButton(
+                icon: const Icon(Icons.clear),
+                onPressed: () {
+                  setState(() {
+                    _produitBloc.add(ProduitRefreshed());
+                    _searchBoolean = false;
+                  });
+                }),
+        ],
+        toolbarHeight: 40,
+      ),
+      body: Stack(
+        children: <Widget>[
+          Offstage(
+            offstage: _index != 0,
+            child: TickerMode(
+              enabled: _index == 0,
+              child: BlocProvider<ProduitBloc>(
+                  create: (context) => _produitBloc..add(ProduitFetched()),
+                  child: const RappelListView()),
             ),
-            Offstage(
-              offstage: _index != 1,
-              child: TickerMode(
-                enabled: _index == 1,
-                child: BlocProvider<WatchlistBloc>(
-                    create: (context) => _watchlistBloc,
-                    child: const WatchListView()),
-              ),
+          ),
+          Offstage(
+            offstage: _index != 1,
+            child: TickerMode(
+              enabled: _index == 1,
+              child: BlocProvider<WatchlistBloc>(
+                  create: (context) => _watchlistBloc,
+                  child: const WatchListView()),
             ),
-          ],
+          ),
+        ],
+      ),
+      floatingActionButton: _index == 0
+          ? FloatingActionButton(
+        backgroundColor: Colors.black,
+        onPressed: () async {
+          var result = await BarcodeScanner.scan();
+          if (await ApiUtils.isItemRecalled(result.rawContent)) {
+            ApiUtils.getOneRecalledProduct(result.rawContent).then((value) =>
+                Navigator.push(context, MaterialPageRoute(
+                    builder: (context) => DetailledProductView(value))));
+          } else {
+            showDialog(
+              context: context,
+              builder: (_) => _productIsOkDialog(),
+              barrierDismissible: true,
+            );
+          }
+        },
+        child: const Icon(
+          Icons.qr_code_scanner,
+          color: Colors.white,
         ),
-        floatingActionButton: _index == 0
-            ? FloatingActionButton(
-                backgroundColor: Colors.black,
-                onPressed: () async {
-                  var result = await BarcodeScanner.scan();
-                  if(await ApiUtils.isItemRecalled(result.rawContent)){
-                    ApiUtils.getOneRecalledProduct(result.rawContent).then((value) => Navigator.push(context, MaterialPageRoute(builder: (context) => DetailledProductView(value))));
-                  }else {
-                    showDialog(
-                      context: context,
-                        builder: (_) => _productIsOkDialog(),
-                      barrierDismissible: true,
-                    );
-                  }
-                },
-                child: const Icon(
-                  Icons.qr_code_scanner,
-                  color: Colors.white,
-                ),
-              )
-            : FloatingActionButton(
-                backgroundColor: Colors.black,
-                onPressed: () async {
-                  var result = await BarcodeScanner.scan();
-                  _watchlistBloc.add(ElementToBeAdded(result.rawContent));
-                },
-                child: const Icon(
-                  Icons.add,
-                  color: Colors.white,
-                ),
+      )
+          : FloatingActionButton(
+        backgroundColor: Colors.black,
+        onPressed: () async {
+          var result = await BarcodeScanner.scan();
+          _watchlistBloc.add(ElementToBeAdded(result.rawContent));
+        },
+        child: const Icon(
+          Icons.add,
+          color: Colors.white,
+        ),
+      ),
+      bottomNavigationBar: BottomAppBar(
+        shape: const CircularNotchedRectangle(),
+        elevation: 1,
+        notchMargin: 5,
+        color: Colors.orange,
+        child: BottomNavigationBar(
+          elevation: 0,
+          showSelectedLabels: false,
+          showUnselectedLabels: false,
+          backgroundColor: Theme
+              .of(context)
+              .primaryColor
+              .withAlpha(0),
+          fixedColor: Colors.white,
+          currentIndex: _index,
+          onTap: (int index) {
+            setState(() {
+              _index = index;
+            });
+          },
+          items: const [
+            BottomNavigationBarItem(
+              icon: Padding(
+                padding: EdgeInsets.fromLTRB(0, 0, 25, 0),
+                child: Icon(Icons.list),
               ),
-        bottomNavigationBar: BottomAppBar(
-          shape: const CircularNotchedRectangle(),
-          elevation: 1,
-          notchMargin: 5,
-          color: Colors.orange,
-          child: BottomNavigationBar(
-            elevation: 0,
-            showSelectedLabels: false,
-            showUnselectedLabels: false,
-            backgroundColor: Theme.of(context).primaryColor.withAlpha(0),
-            fixedColor: Colors.white,
-            currentIndex: _index,
-            onTap: (int index) {
-              setState(() {
-                _index = index;
-              });
-            },
-            items: const [
-              BottomNavigationBarItem(
-                icon: Padding(
-                  padding: EdgeInsets.fromLTRB(0, 0, 25, 0),
-                  child: Icon(Icons.list),
-                ),
-                label: "",
+              label: "",
+            ),
+            BottomNavigationBarItem(
+              icon: Padding(
+                padding: EdgeInsets.fromLTRB(25, 0, 0, 0),
+                child: Icon(Icons.add_alarm_outlined),
               ),
-              BottomNavigationBarItem(
-                icon: Padding(
-                  padding: EdgeInsets.fromLTRB(25, 0, 0, 0),
-                  child: Icon(Icons.add_alarm_outlined),
-                ),
-                label: "",
-              )
-            ],
-          ),
+              label: "",
+            )
+          ],
         ),
-      );
+      ),
+    );
   }
 
 
@@ -192,10 +220,10 @@ class _MyAppState extends State<MyApp> {
       decoration: const InputDecoration(
         //Style of TextField
         enabledBorder: UnderlineInputBorder(
-            //Default TextField border
+          //Default TextField border
             borderSide: BorderSide(color: Colors.white)),
         focusedBorder: UnderlineInputBorder(
-            //Borders when a TextField is in focus
+          //Borders when a TextField is in focus
             borderSide: BorderSide(color: Colors.white)),
         hintText: 'Search', //Text that is displayed when nothing is entered.
         hintStyle: TextStyle(
@@ -208,10 +236,11 @@ class _MyAppState extends State<MyApp> {
   }
 
 
-  Widget _productIsOkDialog(){
+  Widget _productIsOkDialog() {
     return AlertDialog(
       title: const Text("Produit OK"),
-      content: const Text("Selon nos informations ce produit ne fait pas l'objet d'un rappel"),
+      content: const Text(
+          "Selon nos informations ce produit ne fait pas l'objet d'un rappel"),
       actions: <Widget>[
         TextButton(
           child: const Text('Bien reçu!'),
@@ -223,3 +252,4 @@ class _MyAppState extends State<MyApp> {
     );
   }
 }
+
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 287b6a9..57a36ee 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,8 +5,10 @@
 import FlutterMacOS
 import Foundation
 
+import flutter_local_notifications
 import shared_preferences_macos
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
 }
diff --git a/pubspec.lock b/pubspec.lock
index 4cea9d2..1ec893a 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,6 +1,13 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  args:
+    dependency: transitive
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.1"
   async:
     dependency: transitive
     description:
@@ -8,6 +15,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.9.0"
+  background_fetch:
+    dependency: "direct main"
+    description:
+      name: background_fetch
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.2"
   barcode_scan2:
     dependency: "direct main"
     description:
@@ -57,6 +71,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.5"
+  dbus:
+    dependency: transitive
+    description:
+      name: dbus
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.8"
   equatable:
     dependency: "direct main"
     description:
@@ -97,6 +118,34 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_background_service:
+    dependency: "direct main"
+    description:
+      name: flutter_background_service
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.4.6"
+  flutter_background_service_android:
+    dependency: transitive
+    description:
+      name: flutter_background_service_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.3"
+  flutter_background_service_ios:
+    dependency: transitive
+    description:
+      name: flutter_background_service_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.4.0"
+  flutter_background_service_platform_interface:
+    dependency: transitive
+    description:
+      name: flutter_background_service_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
   flutter_barcode_scanner:
     dependency: "direct main"
     description:
@@ -118,6 +167,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.0.1"
+  flutter_local_notifications:
+    dependency: "direct main"
+    description:
+      name: flutter_local_notifications
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "13.0.0"
+  flutter_local_notifications_linux:
+    dependency: transitive
+    description:
+      name: flutter_local_notifications_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  flutter_local_notifications_platform_interface:
+    dependency: transitive
+    description:
+      name: flutter_local_notifications_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.0"
   flutter_plugin_android_lifecycle:
     dependency: transitive
     description:
@@ -219,6 +289,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.3"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.0"
   platform:
     dependency: transitive
     description:
@@ -357,6 +434,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.4.12"
+  timezone:
+    dependency: transitive
+    description:
+      name: timezone
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.9.0"
   typed_data:
     dependency: transitive
     description:
@@ -385,6 +469,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.2.0+2"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.0"
 sdks:
   dart: ">=2.18.1 <3.0.0"
   flutter: ">=3.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index d510f6b..778f8f8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -48,6 +48,9 @@ dependencies:
   cupertino_icons: ^1.0.2
   barcode_scan2: ^4.2.1
   shared_preferences: ^2.0.15
+  flutter_background_service: ^2.4.6
+  flutter_local_notifications: ^13.0.0
+  background_fetch: ^1.1.2
 
 dev_dependencies:
   flutter_test:
-- 
GitLab