Update: 2026-07-30 02:27:45
This commit is contained in:
@@ -2,83 +2,15 @@
|
||||
// MusadaqLiveActivity.swift
|
||||
// MusadaqLiveActivity
|
||||
//
|
||||
// Created by Hamza Aleghwairyeen on 07/05/2026.
|
||||
// Intentionally empty.
|
||||
//
|
||||
// This file used to hold the stock Xcode widget template (a "😀" static widget
|
||||
// with its own TimelineProvider). It was never added to
|
||||
// MusadaqLiveActivityBundle, so it only ever shipped as dead code — and its
|
||||
// file-scope `Provider` type collided conceptually with the control widget's.
|
||||
//
|
||||
// The real implementation is InvoiceBatchLiveActivity in
|
||||
// MusadaqLiveActivityBundle.swift.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), emoji: "😀")
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [SimpleEntry] = []
|
||||
|
||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0 ..< 5 {
|
||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
// func relevances() async -> WidgetRelevances<Void> {
|
||||
// // Generate a list containing the contexts this widget is relevant in.
|
||||
// }
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let emoji: String
|
||||
}
|
||||
|
||||
struct MusadaqLiveActivityEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Time:")
|
||||
Text(entry.date, style: .time)
|
||||
|
||||
Text("Emoji:")
|
||||
Text(entry.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MusadaqLiveActivity: Widget {
|
||||
let kind: String = "MusadaqLiveActivity"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
MusadaqLiveActivityEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
MusadaqLiveActivityEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("My Widget")
|
||||
.description("This is an example widget.")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
MusadaqLiveActivity()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, emoji: "😀")
|
||||
SimpleEntry(date: .now, emoji: "🤩")
|
||||
}
|
||||
import Foundation
|
||||
|
||||
@@ -10,11 +10,18 @@ import SwiftUI
|
||||
import ActivityKit
|
||||
|
||||
// ─── 1. Data Model ───────────────────────────────────
|
||||
//
|
||||
// Must stay identical to InvoiceBatchAttributes in Runner/AppDelegate.swift and
|
||||
// to the "content-state" keys the server sends in
|
||||
// NotificationService::dispatchLiveActivityUpdate(). A mismatch makes iOS drop
|
||||
// the update silently.
|
||||
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
|
||||
}
|
||||
@@ -27,7 +34,31 @@ struct MusadaqLiveActivityBundle: WidgetBundle {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 3. Widget ───────────────────────────────────────
|
||||
// ─── 3. Copy helpers ─────────────────────────────────
|
||||
// A partially failed batch must not read as a clean success.
|
||||
private func headline(for state: InvoiceBatchAttributes.ContentState) -> String {
|
||||
if state.isDone {
|
||||
if state.failed > 0 && state.current > 0 { return "⚠️ اكتمل مع أخطاء" }
|
||||
if state.failed > 0 { return "❌ فشلت المعالجة" }
|
||||
return "✅ تم بنجاح"
|
||||
}
|
||||
return state.statusText.isEmpty
|
||||
? "مُصادَق — جارٍ الرفع..."
|
||||
: "مُصادَق — \(state.statusText)"
|
||||
}
|
||||
|
||||
private func subtitle(for state: InvoiceBatchAttributes.ContentState, company: String) -> String {
|
||||
var text = "\(state.current) / \(state.total) فاتورة"
|
||||
if state.failed > 0 {
|
||||
text += " — \(state.failed) فاشلة"
|
||||
}
|
||||
if !company.isEmpty {
|
||||
text += " — \(company)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// ─── 4. Widget ───────────────────────────────────────
|
||||
struct InvoiceBatchLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: InvoiceBatchAttributes.self) { context in
|
||||
@@ -40,15 +71,17 @@ struct InvoiceBatchLiveActivity: Widget {
|
||||
.foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263)) // #D4A843
|
||||
.font(.title2)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(context.state.isDone ? "✅ تم الرفع بنجاح" : "مُصادَق — جارٍ الرفع...")
|
||||
Text(headline(for: context.state))
|
||||
.font(.caption.bold())
|
||||
.foregroundColor(.white)
|
||||
ProgressView(
|
||||
value: Double(context.state.current),
|
||||
total: Double(context.state.total)
|
||||
value: Double(context.state.current + context.state.failed),
|
||||
total: Double(max(context.state.total, 1))
|
||||
)
|
||||
.tint(Color(red: 0.831, green: 0.659, blue: 0.263))
|
||||
Text("\(context.state.current) / \(context.state.total) فاتورة — \(context.attributes.companyName)")
|
||||
.tint(context.state.failed > 0
|
||||
? Color(red: 0.96, green: 0.62, blue: 0.04)
|
||||
: Color(red: 0.831, green: 0.659, blue: 0.263))
|
||||
Text(subtitle(for: context.state, company: context.attributes.companyName))
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
@@ -66,8 +99,10 @@ struct InvoiceBatchLiveActivity: Widget {
|
||||
.font(.caption.bold()).foregroundColor(.white)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
ProgressView(value: Double(context.state.current),
|
||||
total: Double(context.state.total))
|
||||
// Divide-by-zero guard: total arrives as 0 if a push lands
|
||||
// before the first image is registered.
|
||||
ProgressView(value: Double(context.state.current + context.state.failed),
|
||||
total: Double(max(context.state.total, 1)))
|
||||
.tint(Color(red: 0.831, green: 0.659, blue: 0.263))
|
||||
}
|
||||
} compactLeading: {
|
||||
|
||||
@@ -2,53 +2,15 @@
|
||||
// MusadaqLiveActivityControl.swift
|
||||
// MusadaqLiveActivity
|
||||
//
|
||||
// Created by Hamza Aleghwairyeen on 07/05/2026.
|
||||
// Intentionally empty.
|
||||
//
|
||||
// This file used to hold the stock Xcode ControlWidget template — a "Start
|
||||
// Timer" toggle whose `perform()` body was an empty comment. It was not listed
|
||||
// in MusadaqLiveActivityBundle, so iOS never surfaced it; had it been surfaced,
|
||||
// it would have shown users a toggle that does nothing.
|
||||
//
|
||||
// Musadaq has no Control Center widget. If one is added later, declare it here
|
||||
// AND add it to MusadaqLiveActivityBundle.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct MusadaqLiveActivityControl: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(
|
||||
kind: "com.example.musadaqApp.MusadaqLiveActivity",
|
||||
provider: Provider()
|
||||
) { value in
|
||||
ControlWidgetToggle(
|
||||
"Start Timer",
|
||||
isOn: value,
|
||||
action: StartTimerIntent()
|
||||
) { isRunning in
|
||||
Label(isRunning ? "On" : "Off", systemImage: "timer")
|
||||
}
|
||||
}
|
||||
.displayName("Timer")
|
||||
.description("A an example control that runs a timer.")
|
||||
}
|
||||
}
|
||||
|
||||
extension MusadaqLiveActivityControl {
|
||||
struct Provider: ControlValueProvider {
|
||||
var previewValue: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func currentValue() async throws -> Bool {
|
||||
let isRunning = true // Check if the timer is running
|
||||
return isRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StartTimerIntent: SetValueIntent {
|
||||
static let title: LocalizedStringResource = "Start a timer"
|
||||
|
||||
@Parameter(title: "Timer is running")
|
||||
var value: Bool
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Start / stop the timer based on `value`.
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
import Foundation
|
||||
|
||||
@@ -46,32 +46,32 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase/CoreOnly (12.12.0):
|
||||
- FirebaseCore (~> 12.12.0)
|
||||
- Firebase/Messaging (12.12.0):
|
||||
- Firebase/CoreOnly (12.13.0):
|
||||
- FirebaseCore (~> 12.13.0)
|
||||
- Firebase/Messaging (12.13.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.12.0)
|
||||
- firebase_core (4.7.0):
|
||||
- Firebase/CoreOnly (= 12.12.0)
|
||||
- FirebaseMessaging (~> 12.13.0)
|
||||
- firebase_core (4.9.0):
|
||||
- Firebase/CoreOnly (= 12.13.0)
|
||||
- Flutter
|
||||
- firebase_messaging (16.2.0):
|
||||
- Firebase/Messaging (= 12.12.0)
|
||||
- firebase_messaging (16.2.2):
|
||||
- Firebase/Messaging (= 12.13.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseCore (12.12.1):
|
||||
- FirebaseCoreInternal (~> 12.12.0)
|
||||
- FirebaseCore (12.13.0):
|
||||
- FirebaseCoreInternal (~> 12.13.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreInternal (12.12.0):
|
||||
- FirebaseCoreInternal (12.13.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseInstallations (12.12.0):
|
||||
- FirebaseCore (~> 12.12.0)
|
||||
- FirebaseInstallations (12.13.0):
|
||||
- FirebaseCore (~> 12.13.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (12.12.0):
|
||||
- FirebaseCore (~> 12.12.0)
|
||||
- FirebaseInstallations (~> 12.12.0)
|
||||
- FirebaseMessaging (12.13.0):
|
||||
- FirebaseCore (~> 12.13.0)
|
||||
- FirebaseInstallations (~> 12.13.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
@@ -84,6 +84,8 @@ PODS:
|
||||
- Mantle
|
||||
- SDWebImage
|
||||
- SDWebImageWebPCoder
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage_darwin (10.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -141,15 +143,12 @@ PODS:
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- ObjectBox (4.4.1)
|
||||
- ObjectBox (5.3.0-beta.4)
|
||||
- objectbox_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- ObjectBox (= 4.4.1)
|
||||
- ObjectBox (= 5.3.0-beta.4)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- printing (1.0.0):
|
||||
@@ -187,13 +186,13 @@ DEPENDENCIES:
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||
- freerasp (from `.symlinks/plugins/freerasp/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- printing (from `.symlinks/plugins/printing/ios`)
|
||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||
@@ -245,6 +244,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_image_compress_common:
|
||||
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_secure_storage_darwin:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
||||
freerasp:
|
||||
@@ -257,8 +258,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/objectbox_flutter_libs/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
printing:
|
||||
@@ -285,28 +284,28 @@ SPEC CHECKSUMS:
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33
|
||||
firebase_core: 9156a152117c843440b0b990c785aa0259bc5447
|
||||
firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269
|
||||
FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284
|
||||
FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9
|
||||
FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e
|
||||
FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31
|
||||
Firebase: 7d62445aeabdaea36f7d372f33052fed9a72514f
|
||||
firebase_core: 0013f886fbd0b4950865551eaab47784424bfdb5
|
||||
firebase_messaging: b875e4088ddd9ecd1834f6c89f6e0a1ffc7d98f0
|
||||
FirebaseCore: 58905958aa00a061397a0fd759ae4b55bddb3576
|
||||
FirebaseCoreInternal: 37bee58388fc6d183f0ab1b32d69ae44f2cf8aad
|
||||
FirebaseInstallations: 134bde50e477628ded76070efdb12d515d53f948
|
||||
FirebaseMessaging: 30564b85d2f81a96f9d312bd23acf8186ff092ae
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||
freerasp: d77275f774facb901f52e9608e5bd34768728363
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
|
||||
objectbox_flutter_libs: 09b1dec1b4cd27bf1a5f9bae7ccaa7e43588bf31
|
||||
ObjectBox: eccb95ea2054c39d81dfa2d4ccc5f1e31187228a
|
||||
objectbox_flutter_libs: ed1510f71602e4a0d3f2a721324e468d066fdbb9
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
@@ -317,7 +316,7 @@ SPEC CHECKSUMS:
|
||||
speech_to_text: 3b313d98516d3d0406cea424782ec25470c59d19
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
|
||||
PODFILE CHECKSUM: a409a572b05f394ce1fca5d08bea69ffac194079
|
||||
|
||||
|
||||
@@ -1,13 +1,234 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@
|
||||
<string>تطبيق مُصادَق قد يحتاج للوصول إلى الموقع الجغرافي لتحسين تجربة المستخدم وتوفير خدمات مخصصة حسب المنطقة.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<!-- remote-notification is required for the silent data pushes that carry
|
||||
batch progress. Without it iOS drops content-available payloads while the
|
||||
app is backgrounded, so progress froze the moment the user left the app. -->
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
<array>
|
||||
<string>group.com.musadaq.app</string>
|
||||
</array>
|
||||
<!-- Required for APNs registration. Without this key the app never receives
|
||||
an APNs device token, so FirebaseMessaging.getToken() fails on iOS and
|
||||
push_token was being stored as null for every iPhone. -->
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Reference in New Issue
Block a user