twonly-app/ios/Runner/AppDelegate.swift
2026-08-30 09:25:15 +02:00

258 lines
8.8 KiB
Swift

import CryptoKit
import Flutter
import Foundation
import UIKit
import UserNotifications
import flutter_sharing_intent
import workmanager_apple
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") {
VideoCompressionChannel.register(with: registrar.messenger())
}
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
// Background tasks call AppEnvironment.init() too, so this engine needs
// the runtime storage channel just as much as the implicit one.
RuntimeStorageChannel.register(with: registry)
}
WorkmanagerPlugin.registerPeriodicTask(
withIdentifier: "eu.twonly.periodic_task",
frequency: NSNumber(value: 20 * 60)
)
WorkmanagerPlugin.registerBGProcessingTask(
withIdentifier: "eu.twonly.processing_task"
)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
if sharingIntent.hasSameSchemePrefix(url: url) {
return sharingIntent.application(app, open: url, options: options)
}
// Proceed url handling for other Flutter libraries like app_links
return super.application(app, open: url, options: options)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
RuntimeStorageChannel.register(with: engineBridge.pluginRegistry)
NativeNotificationChannel.register(with: engineBridge.pluginRegistry)
}
override func userNotificationCenter(
_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
NSLog(
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
response.notification.request.content.userInfo)
super.userNotificationCenter(
center, didReceive: response, withCompletionHandler: completionHandler)
}
override func userNotificationCenter(
_ center: UNUserNotificationCenter, willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
NSLog("userNotificationCenter:willPresent")
completionHandler([.alert, .sound])
}
}
/// Withdraws only the native notifications whose message IDs Dart reports as
/// opened, and keeps the app icon badge in step with them. The notification
/// service extension's final alert keeps APNs' request identifier, so both the
/// identifier and our `notification_id` user-info field have to be considered.
class NativeNotificationChannel {
private static let channelName = "eu.twonly/notificationTap"
private static let notificationIdsKey = "notification_ids"
private static let badgeCountKey = "badge_count"
static func register(with registry: FlutterPluginRegistry) {
guard let registrar = registry.registrar(forPlugin: "TwonlyNativeNotifications") else {
return
}
let channel = FlutterMethodChannel(
name: channelName,
binaryMessenger: registrar.messenger()
)
channel.setMethodCallHandler { call, result in
switch call.method {
case "cancelNotifications":
guard
let arguments = call.arguments as? [String: Any],
let values = arguments[notificationIdsKey] as? [String]
else {
result(
FlutterError(
code: "invalid_notification_ids",
message: "notification_ids must be a list of strings",
details: nil
))
return
}
removeNotifications(Set(values), completion: result)
case "setBadgeCount":
guard
let arguments = call.arguments as? [String: Any],
let count = arguments[badgeCountKey] as? Int
else {
result(
FlutterError(
code: "invalid_badge_count",
message: "badge_count must be an integer",
details: nil
))
return
}
setBadgeCount(count, completion: result)
default:
result(FlutterMethodNotImplemented)
}
}
}
/// iOS only takes an app icon badge from a notification payload, so a badge
/// set by the notification service extension survives until the app itself
/// overwrites it. Dart pushes the pending event count here whenever the
/// notification outbox changes and on every resume.
private static func setBadgeCount(_ count: Int, completion: @escaping FlutterResult) {
let badge = max(0, count)
guard #available(iOS 16.0, *) else {
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = badge
completion(nil)
}
return
}
UNUserNotificationCenter.current().setBadgeCount(badge) { error in
DispatchQueue.main.async {
if let error {
completion(
FlutterError(
code: "badge_count_failed",
message: error.localizedDescription,
details: nil
))
} else {
completion(nil)
}
}
}
}
private static func removeNotifications(
_ notificationIds: Set<String>,
completion: @escaping FlutterResult
) {
guard !notificationIds.isEmpty else {
completion(nil)
return
}
let center = UNUserNotificationCenter.current()
center.getDeliveredNotifications { delivered in
let requestIds = delivered.compactMap { notification -> String? in
let request = notification.request
let notificationId = request.content.userInfo["notification_id"] as? String
return notificationIds.contains(request.identifier)
|| notificationId.map(notificationIds.contains) == true
? request.identifier
: nil
}
center.removeDeliveredNotifications(withIdentifiers: requestIds)
center.getPendingNotificationRequests { pending in
let pendingIds = pending.compactMap { request -> String? in
let notificationId = request.content.userInfo["notification_id"] as? String
return notificationIds.contains(request.identifier)
|| notificationId.map(notificationIds.contains) == true
? request.identifier
: nil
}
center.removePendingNotificationRequests(withIdentifiers: pendingIds)
DispatchQueue.main.async {
completion(nil)
}
}
}
}
}
/// Hands Dart the App Group container shared between the app and its
/// extensions. Must be registered on every Flutter engine that runs
/// `AppEnvironment.init()`, not just the implicit one.
class RuntimeStorageChannel {
private static let appGroupIdentifier = "group.eu.twonly.runtime"
static func register(with registry: FlutterPluginRegistry) {
guard let registrar = registry.registrar(forPlugin: "TwonlyRuntimeStorage") else {
return
}
register(with: registrar.messenger())
}
static func register(with messenger: FlutterBinaryMessenger) {
let channel = FlutterMethodChannel(
name: "eu.twonly/runtime_storage",
binaryMessenger: messenger
)
channel.setMethodCallHandler { call, result in
guard call.method == "runtimeSupportDirectory" else {
result(FlutterMethodNotImplemented)
return
}
guard
let container = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupIdentifier
)
else {
result(
FlutterError(
code: "runtime_app_group_unavailable",
message: "Could not open \(appGroupIdentifier)",
details: nil
))
return
}
let runtimeDirectory = container.appendingPathComponent("runtime", isDirectory: true)
do {
try FileManager.default.createDirectory(
at: runtimeDirectory,
withIntermediateDirectories: true
)
try (runtimeDirectory as NSURL).setResourceValue(
URLFileProtection.completeUntilFirstUserAuthentication,
forKey: .fileProtectionKey
)
result(runtimeDirectory.path)
} catch {
result(
FlutterError(
code: "runtime_directory_failed",
message: error.localizedDescription,
details: nil
))
}
}
}
}