// // MusadaqLiveActivityBundle.swift // MusadaqLiveActivity // // Created by Hamza Aleghwairyeen on 07/05/2026. // import WidgetKit 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 } // ─── 2. Bundle ─────────────────────────────────────── @main struct MusadaqLiveActivityBundle: WidgetBundle { var body: some Widget { InvoiceBatchLiveActivity() } } // ─── 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 // Lock Screen UI ZStack { Color(red: 0.043, green: 0.098, blue: 0.161) // #0B1929 HStack(spacing: 12) { Image(systemName: context.state.isDone ? "checkmark.doc.fill" : "arrow.up.doc.fill") .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263)) // #D4A843 .font(.title2) VStack(alignment: .leading, spacing: 4) { Text(headline(for: context.state)) .font(.caption.bold()) .foregroundColor(.white) ProgressView( value: Double(context.state.current + context.state.failed), total: Double(max(context.state.total, 1)) ) .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) } } .padding() } } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Image(systemName: "arrow.up.doc.fill") .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263)) } DynamicIslandExpandedRegion(.trailing) { Text("\(context.state.current)/\(context.state.total)") .font(.caption.bold()).foregroundColor(.white) } DynamicIslandExpandedRegion(.bottom) { // 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: { Image(systemName: "arrow.up.doc.fill") .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263)) .font(.caption) } compactTrailing: { Text("\(context.state.current)/\(context.state.total)") .font(.caption2.bold()).foregroundColor(.white) } minimal: { Image(systemName: "arrow.up.doc.fill") .foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263)) } } } }