Files
musadaq-saas/musadaq-app/ios/Runner/AppDelegate.swift
T

235 lines
7.5 KiB
Swift

import ActivityKit
import Flutter
import UIKit
// ─────────────────────────────────────────────────────────────────────────────
// Live Activity attributes.
//
// This MUST stay byte-for-byte compatible with InvoiceBatchAttributes in
// MusadaqLiveActivity/MusadaqLiveActivityBundle.swift. ActivityKit matches the
// app's activity to the widget's UI by attributes type name and shape, and the
// two targets compile separately, so the declaration is intentionally duplicated
// rather than shared. Change one, change the other.
// ─────────────────────────────────────────────────────────────────────────────
struct InvoiceBatchAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var current: Int
var total: Int
var isDone: Bool
var failed: Int = 0
var statusText: String = ""
}
var companyName: String
}
@main
@objc class AppDelegate: FlutterAppDelegate {
/// The Live Activity currently on screen, if any.
private var currentActivity: Any?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
if let controller = window?.rootViewController as? FlutterViewController {
registerLiveActivityChannel(with: controller.binaryMessenger)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// MARK: - Flutter channel
private func registerLiveActivityChannel(with messenger: FlutterBinaryMessenger) {
let channel = FlutterMethodChannel(
name: "com.musadaq.liveactivity",
binaryMessenger: messenger
)
channel.setMethodCallHandler { [weak self] call, result in
guard let self else {
result(nil)
return
}
let args = call.arguments as? [String: Any] ?? [:]
switch call.method {
case "start":
self.startActivity(args: args, result: result)
case "update":
self.updateActivity(args: args, result: result)
case "end":
self.endActivity(args: args, result: result)
case "isSupported":
result(self.areActivitiesEnabled())
default:
result(FlutterMethodNotImplemented)
}
}
}
// MARK: - ActivityKit
private func areActivitiesEnabled() -> Bool {
if #available(iOS 16.2, *) {
return ActivityAuthorizationInfo().areActivitiesEnabled
}
return false
}
/// Starts a Live Activity and returns its ActivityKit push token (hex) so the
/// server can update it remotely while the app is closed.
private func startActivity(args: [String: Any], result: @escaping FlutterResult) {
guard #available(iOS 16.2, *) else {
result(nil)
return
}
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
// The user disabled Live Activities for this app. Not an error.
result(nil)
return
}
// Never stack activities: replace any previous one.
endCurrentActivityImmediately()
let companyName = args["companyName"] as? String ?? ""
let total = args["total"] as? Int ?? 1
let current = args["current"] as? Int ?? 0
let attributes = InvoiceBatchAttributes(companyName: companyName)
let state = InvoiceBatchAttributes.ContentState(
current: current,
total: max(total, 1),
isDone: false,
failed: 0,
statusText: "جارٍ الرفع"
)
do {
let activity = try Activity<InvoiceBatchAttributes>.request(
attributes: attributes,
content: .init(state: state, staleDate: Date().addingTimeInterval(30 * 60)),
pushType: .token
)
self.currentActivity = activity
// The push token arrives asynchronously, so hand the result back from the
// first token update rather than blocking here.
var didReturn = false
let lock = NSLock()
Task {
for await tokenData in activity.pushTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
lock.lock()
let shouldReturn = !didReturn
didReturn = true
lock.unlock()
if shouldReturn {
DispatchQueue.main.async { result(token) }
} else {
// Token rotated mid-activity: tell Flutter so it can re-register.
DispatchQueue.main.async {
if let controller = self.window?.rootViewController as? FlutterViewController {
FlutterMethodChannel(
name: "com.musadaq.liveactivity",
binaryMessenger: controller.binaryMessenger
).invokeMethod("onPushTokenChanged", arguments: token)
}
}
}
}
}
// Don't leave Flutter awaiting forever if no token ever arrives.
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
lock.lock()
let shouldReturn = !didReturn
didReturn = true
lock.unlock()
if shouldReturn { result(nil) }
}
} catch {
NSLog("[LiveActivity] request failed: \(error.localizedDescription)")
result(FlutterError(
code: "START_FAILED",
message: error.localizedDescription,
details: nil
))
}
}
private func updateActivity(args: [String: Any], result: @escaping FlutterResult) {
guard #available(iOS 16.2, *),
let activity = currentActivity as? Activity<InvoiceBatchAttributes> else {
result(nil)
return
}
let current = args["current"] as? Int ?? 0
let total = max(args["total"] as? Int ?? 1, 1)
let failed = args["failed"] as? Int ?? 0
let isDone = args["isDone"] as? Bool ?? false
let statusText = args["statusText"] as? String ?? ""
let state = InvoiceBatchAttributes.ContentState(
current: current,
total: total,
isDone: isDone,
failed: failed,
statusText: statusText
)
Task {
await activity.update(.init(state: state, staleDate: Date().addingTimeInterval(30 * 60)))
DispatchQueue.main.async { result(nil) }
}
}
private func endActivity(args: [String: Any], result: @escaping FlutterResult) {
guard #available(iOS 16.2, *),
let activity = currentActivity as? Activity<InvoiceBatchAttributes> else {
currentActivity = nil
result(nil)
return
}
let finalText = args["finalText"] as? String ?? "اكتمل"
let finalState = InvoiceBatchAttributes.ContentState(
current: activity.content.state.total,
total: activity.content.state.total,
isDone: true,
failed: activity.content.state.failed,
statusText: finalText
)
currentActivity = nil
Task {
// Leave the finished state visible briefly instead of vanishing instantly.
await activity.end(
.init(state: finalState, staleDate: nil),
dismissalPolicy: .after(Date().addingTimeInterval(10))
)
DispatchQueue.main.async { result(nil) }
}
}
private func endCurrentActivityImmediately() {
guard #available(iOS 16.2, *),
let activity = currentActivity as? Activity<InvoiceBatchAttributes> else {
return
}
currentActivity = nil
Task { await activity.end(nil, dismissalPolicy: .immediate) }
}
}