Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions xpeapp_ios/XpeApp/XpeApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
0E87AC2F2C660DBB0079877D /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E87AC2E2C660DBB0079877D /* HeaderView.swift */; };
0E87AC332C660DDF0079877D /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E87AC322C660DDF0079877D /* SidebarView.swift */; };
0EB50D232CDB676F004E8C24 /* xpeho_ui in Frameworks */ = {isa = PBXBuildFile; productRef = 0EB50D222CDB676F004E8C24 /* xpeho_ui */; };
0ED13AB52D118E4900F9AE3E /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75E0C262D11822D000DC4AA /* LocalNotificationManager.swift */; };
0ED13AB62D118E4C00F9AE3E /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75E0C272D118589000DC4AA /* BackgroundTaskManager.swift */; };
0EEE15042C9C56A70066CCB7 /* UserRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EEE15032C9C56A70066CCB7 /* UserRepositoryTests.swift */; };
0EEE15082C9D56790066CCB7 /* FeatureModelEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EEE15072C9D56790066CCB7 /* FeatureModelEntityTests.swift */; };
0EEE150A2C9D58130066CCB7 /* NewsletterModelEntityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EEE15092C9D58130066CCB7 /* NewsletterModelEntityTests.swift */; };
Expand Down Expand Up @@ -184,6 +186,8 @@
BDACED0A2C10A690007B5262 /* NewsletterPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsletterPageView.swift; sourceTree = "<group>"; };
BDC8A4242C18765A00D14C35 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
BDE1B5152C183AE200E8513F /* DebugPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugPageView.swift; sourceTree = "<group>"; };
D75E0C262D11822D000DC4AA /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
D75E0C272D118589000DC4AA /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -343,6 +347,7 @@
0EF0F8E32C91E14900AF5966 /* Presentation */ = {
isa = PBXGroup;
children = (
D75E0C2F2D118C64000DC4AA /* Notifications */,
0E6277A52C94813400F26E55 /* Shared */,
0EF0F8EA2C91E1C300AF5966 /* ViewModel */,
0EF0F8E92C91E1BE00AF5966 /* View */,
Expand Down Expand Up @@ -514,6 +519,15 @@
path = XpeAppTests;
sourceTree = "<group>";
};
D75E0C2F2D118C64000DC4AA /* Notifications */ = {
isa = PBXGroup;
children = (
D75E0C272D118589000DC4AA /* BackgroundTaskManager.swift */,
D75E0C262D11822D000DC4AA /* LocalNotificationManager.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -636,9 +650,11 @@
0EF0F9002C931EE100AF5966 /* HomePageViewModel.swift in Sources */,
0E7EBD5E2C6CFF8700C95482 /* CampaignFormView.swift in Sources */,
0EFC0FEA2CF9D17800F9480D /* DisabledFeaturePlaceHolderView.swift in Sources */,
0ED13AB52D118E4900F9AE3E /* LocalNotificationManager.swift in Sources */,
0E260DBD2CD8D79E00F8EF0C /* NoContentPlaceHolderView.swift in Sources */,
0E6277AA2C94897100F26E55 /* NewsletterCardView.swift in Sources */,
0E6ACF802C8710B100735E1E /* FeatureRepositoryImpl.swift in Sources */,
0ED13AB62D118E4C00F9AE3E /* BackgroundTaskManager.swift in Sources */,
0E6277A72C9484D900F26E55 /* NewsletterPageViewModel.swift in Sources */,
0EF0F9022C931F2D00AF5966 /* LoginManager.swift in Sources */,
0EF0F9042C932F1000AF5966 /* NewsletterEntity.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class NewsletterRepositoryImpl: NewsletterRepository {
self.dataSource = dataSource
}

func getNewsletters() async -> [NewsletterEntity]? {
func getNewsletters() async -> [NewsletterEntity]? {
// Fetch data
guard let newsletters = await dataSource.fetchAllNewsletters() else {
debugPrint("Failed call to fetchAllNewsletters in getNewsletters")
Expand Down
6 changes: 5 additions & 1 deletion xpeapp_ios/XpeApp/XpeApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>xpeapp.notifications_check</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// BackgroundTaskManager.swift
// XpeApp
//
// Created by Théo Lebègue on 17/12/2024.
//

import Foundation
import BackgroundTasks
import SwiftUI

class BackgroundTaskManager {

static let instance = BackgroundTaskManager()

let taskId = "xpeapp.notifications_check" // Define task id
private let startDate = Date().addingTimeInterval(60) // Schedule the next task in 1 minute
private let localNotificationsManager = LocalNotificationsManager.instance

private init() {}

func registerBackgroundTask() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: self.taskId, using: nil) { task in
guard let task = task as? BGProcessingTask else { return }
self.handleBackgroundTask(task: task)
}
self.submitBackgroundTask()
}

private func submitBackgroundTask() {
// Manual test : e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"xpeapp.notifications_check"]
// check if there is a pending task request or not
BGTaskScheduler.shared.getPendingTaskRequests { request in
guard request.isEmpty else {
debugPrint("A BGTask is already scheduled")
return
}
debugPrint("Try scheduling a new BGTask")
// Create a new background task request
let request = BGProcessingTaskRequest(identifier: self.taskId)
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
request.earliestBeginDate = self.startDate

do {
// Schedule the background task
try BGTaskScheduler.shared.submit(request)
} catch {
debugPrint("Unable to schedule background task: \(error.localizedDescription)")
}
}
}

private func handleBackgroundTask(task: BGProcessingTask) {
let count = UserDefaults.standard.integer(forKey: "task_run_count")
UserDefaults.standard.set(count+1, forKey: "task_run_count")

Task {
// Send newsletter notif if needed
await checkNewNewsletter()

// Mark the task as completed
task.setTaskCompleted(success: true)
}
}

private func checkNewNewsletter() async {
// Étape 1: Récupérer la dernière newsletter depuis l'API
let obtainedLastNewsletter = await NewsletterRepositoryImpl.instance.getLastNewsletter()

// Étape 2: Vérifier si une newsletter est enregistrée localement via le UserDefaults
let lastSavedPDFURL = UserDefaults.standard.string(forKey: "lastNewsletterPDFURL")

// Étape 3: Comparer la newsletter locale avec la nouvelle
if let lastSavedPDFURL = lastSavedPDFURL {
if lastSavedPDFURL == obtainedLastNewsletter?.pdfUrl {
debugPrint("Aucune nouvelle newsletter détectée.")
// Étape 4: Envoyer une notification
localNotificationsManager.scheduleNotification(
title: "Info",
message: "Aucune nouvelle newsletter !",
secondsFromNow: 1
)
} else {
// Étape 4: Nouvelle newsletter détectée -> Mettre à jour le stockage local
UserDefaults.standard.set(obtainedLastNewsletter?.pdfUrl, forKey: "lastNewsletterPDFURL")

// Étape 5: Envoyer une notification
localNotificationsManager.scheduleNotification(
title: "Nouvelle newsletter !",
message: "Restez informé avec notre nouvelle newsletter !",
secondsFromNow: 1
)
debugPrint("Nouvelle newsletter détectée et notification envoyée.")
}


} else {
debugPrint("Sauvegarde d'une newsletter pour référence !")
// Étape 4: Nouvelle newsletter détectée -> Mettre à jour le stockage local
UserDefaults.standard.set(obtainedLastNewsletter?.pdfUrl, forKey: "lastNewsletterPDFURL")
// Étape 5: Envoyer une notification
localNotificationsManager.scheduleNotification(
title: "Info",
message: "Sauvegarde d'une newsletter pour référence !",
secondsFromNow: 1
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// NotificationManager.swift
// XpeApp
//
// Created by Théo Lebègue on 17/12/2024.
//

import Foundation
import SwiftUI

class LocalNotificationsManager {

static let instance = LocalNotificationsManager()

private init(){}

func scheduleNotification(title: String, message: String, secondsFromNow: TimeInterval) {
// Create notification content
let content = UNMutableNotificationContent()
content.title = title
content.body = message
content.sound = .default

// Create a trigger for the notification
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: secondsFromNow, repeats: false)

// Create a request with a unique identifier
let request = UNNotificationRequest(identifier: "checkNotification", content: content, trigger: trigger)

// Add the request to the notification center
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
debugPrint("Error scheduling notification: \(error.localizedDescription)")
}
}
}

}
40 changes: 16 additions & 24 deletions xpeapp_ios/XpeApp/XpeApp/XpeAppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import SwiftData
import FirebaseCore
import FirebaseAuth
import FirebaseFirestore
import FirebaseMessaging
import xpeho_ui

@main
Expand All @@ -36,16 +35,26 @@ struct XpeAppApp: App {
// Note(Loucas): Firebase method swizzling has been disabled. If it becomes necessary in the future,
// we can enable it through setting the FirebaseAppDelegateProxyEnabled boolean to 'NO' in Info.plist
class XpeAppAppDelegate: NSObject, UIApplicationDelegate {

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
// Initialize Firebase
FirebaseApp.configure()
// Set Firebase Messaging delegate
Messaging.messaging().delegate = self
// Request notification permissions
registerForPushNotifications(application: application)

// Init background manager
let backgroundTaskManager = BackgroundTaskManager.instance
// Register background task
backgroundTaskManager.registerBackgroundTask()

// Monitoring count of BGTask
let count = UserDefaults.standard.integer(forKey: "task_run_count")
debugPrint("Since last execution of the app, BGTask ran \(count) times")
UserDefaults.standard.set(0, forKey: "task_run_count")

return true
}

Expand All @@ -61,35 +70,18 @@ class XpeAppAppDelegate: NSObject, UIApplicationDelegate {
}

extension XpeAppAppDelegate: UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print(error)
}


func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
//self.sendDeviceTokenToServer(data: deviceToken)
Messaging.messaging().setAPNSToken(deviceToken, type: .unknown)
}

private func registerForPushNotifications(application: UIApplication) {
UNUserNotificationCenter.current().delegate = self

let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions
) { (granted, error) in
guard granted else {return}
DispatchQueue.main.async{
application.registerForRemoteNotifications()
guard granted else {
debugPrint("Permission denied for local notifications")
return
}
debugPrint("Permission granted for local notifications")
}
}
}

extension XpeAppAppDelegate: MessagingDelegate {

func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
//print("Firebase registration token: \(String(describing: fcmToken))")
}
}