// // AppWidgetLiveActivity.swift // AppWidget // // Created by Cloud Solutions on 15/12/2025. // import ActivityKit import WidgetKit import SwiftUI struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable { public typealias LiveDeliveryData = ContentState // don't forget to add this line, otherwise, live activity will not display it. public struct ContentState: Codable, Hashable { } var id = UUID() } let sharedDefault = UserDefaults(suiteName: "group.alhabib.patientApp")! struct AppWidgetAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { // Dynamic stateful properties about your activity go here! var emoji: String } // Fixed non-changing properties about your activity go here! var name: String } extension Color { static let redColorText = Color(hex: "#ED1C2B") static let yellowColor = Color(hex: "#FFAF15") static let successColor = Color(hex: "#18C273") static let inactiveColor = Color(hex: "#ED1C2B") static let textColor = Color(hex: "#A2A2A2") static let colorWhite = Color(hex: "#FFFFFF") // Helper initializer for hex colors init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) let a, r, g, b: UInt64 switch hex.count { case 3: // RGB (12-bit) (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) case 6: // RGB (24-bit) (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) case 8: // ARGB (32-bit) (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: (a, r, g, b) = (255, 0, 0, 0) } self.init( .sRGB, red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255, opacity: Double(a) / 255 ) } } // MARK: - Widget Configuration based on nextStepBackground struct WidgetColorConfig { let nextStepBackground: Color let nextStepTextColor: Color let containerBorderColor: Color static func config(for value: Int) -> WidgetColorConfig { switch value { case 0: return WidgetColorConfig( nextStepBackground: .inactiveColor, nextStepTextColor: .textColor, containerBorderColor: .yellowColor ) case 1: return WidgetColorConfig( nextStepBackground: .redColorText, nextStepTextColor: .colorWhite, containerBorderColor: .redColorText ) case 2: return WidgetColorConfig( nextStepBackground: .successColor, nextStepTextColor: .colorWhite, containerBorderColor: .successColor ) default: return WidgetColorConfig( nextStepBackground: .inactiveColor, nextStepTextColor: .textColor, containerBorderColor: .yellowColor ) } } } // MARK: - Widget Data Model struct NotificationWidgetData { let currentTicket: String let currentlyServingHeader: String let currentlyServing: String let status: String let nextStep: String let imageName: String let colorConfig: WidgetColorConfig } struct NotificationWidgetView: View { private let data: NotificationWidgetData init(data: NotificationWidgetData) { self.data = data } var body : some View{ // ZStack(){ body2 // .frame(width: .infinity, height: 400 ) // .padding(.top, 340) // .cornerRadius(20) // }.background(data.colorConfig.containerBorderColor) // .cornerRadius(20) } var body2: some View { VStack(alignment: .leading, spacing: 0) { // Top section with current ticket and image HStack(alignment: .top, spacing: 0) { // Current ticket value Text(data.currentTicket) .font(.custom("Poppins-Medium", size: 28)) .fontWeight(.bold) .foregroundColor(.black) .padding(.trailing, 10) .frame(maxWidth: .infinity, alignment: .leading) Image(data.imageName) .resizable() .scaledToFit() .frame(width: 34, height: 34) } .padding(.top, 16) .padding(.horizontal, 16) // Currently Serving Header Text(data.currentlyServingHeader) .font(.custom("Poppins-Medium", size: 14)) .fontWeight(.bold) .foregroundColor(.black) .padding(.trailing, 12) .padding(.bottom, 10) .padding(.horizontal, 16) // Currently Serving section with status HStack(alignment: .center, spacing: 0) { // Currently serving text Text(data.currentlyServing) .font(.custom("Poppins-Medium", size: 16)) .fontWeight(.bold) .foregroundColor(.black) .frame(maxWidth: .infinity, alignment: .leading) // Status badge Text(data.status) .font(.custom("Poppins-Medium", size: 12)) .foregroundColor(data.colorConfig.nextStepTextColor) .padding(.horizontal, 12) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.inactiveColor.opacity(0.2)) ) } .padding(.horizontal, 16) Spacer() // Next Step badge Text(data.nextStep) .font(.custom("Poppins-Medium", size: 12)) .foregroundColor(data.colorConfig.nextStepTextColor) .padding(.horizontal, 12) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 8) .fill(data.colorConfig.nextStepBackground) ) .padding(.horizontal, 16) .padding(.bottom, 16) } .background(Color.black.opacity(0.01)) .cornerRadius(20) // .border(width: 10, edges: [.top, .bottom], color: data.colorConfig.containerBorderColor) // .border(width: 2, edges: [.leading, .trailing], color: data.colorConfig.containerBorderColor) // .overlay( // RoundedRectangle(cornerRadius: 20) // .stroke(data.colorConfig.containerBorderColor, lineWidth: 20) // ) } } extension View { func border(width: CGFloat, edges: [Edge], color: Color) -> some View { overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color)) .cornerRadius(20) } } struct EdgeBorder: Shape { var width: CGFloat var edges: [Edge] func path(in rect: CGRect) -> Path { edges.map { edge -> Path in switch edge { case .top: return Path(.init(x: rect.minX, y: rect.minY, width: rect.width, height: width)) case .bottom: return Path(.init(x: rect.minX, y: rect.maxY - width, width: rect.width, height: width)) case .leading: return Path(.init(x: rect.minX, y: rect.minY, width: width, height: rect.height)) case .trailing: return Path(.init(x: rect.maxX - width, y: rect.minY, width: width, height: rect.height)) } }.reduce(into: Path()) { $0.addPath($1) } } } // MARK: - Main Widget View //working widget but the border is not appearing // struct NotificationWidgetView: View { // private let data: NotificationWidgetData // // init(data: NotificationWidgetData) { // self.data = data // } // // var body: some View { // VStack(alignment: .leading, spacing: 0) { // // VStack(alignment: .leading, spacing: 0) { // // Top section with current ticket and image // HStack(alignment: .top, spacing: 0) { // // Current ticket value // Text(data.currentTicket) // .font(.custom("Poppins-Medium", size: 28)) // .fontWeight(.bold) // .foregroundColor(.black) // .padding(.trailing, 10) // // .frame(maxWidth: .infinity, alignment: .leading) // // Image(data.imageName) // .resizable() // .scaledToFit() // .frame(width: 34, height: 34) // } // // // Currently Serving Header // Text(data.currentlyServingHeader) // .font(.custom("Poppins-Medium", size: 14)) // .fontWeight(.bold) // .foregroundColor(.black) // .padding(.trailing, 12) // .padding(.bottom, 10) // // // Currently Serving section with status // HStack(alignment: .center, spacing: 0) { // // Currently serving text // Text(data.currentlyServing) // .font(.custom("Poppins-Medium", size: 16)) // .fontWeight(.bold) // .foregroundColor(.black) // .frame(maxWidth: .infinity, alignment: .leading) // // // Status badge // Text(data.status) // .font(.custom("Poppins-Medium", size: 12)) // .foregroundColor(data.colorConfig.nextStepTextColor) // .padding(.horizontal, 12) // .padding(.vertical, 8) // .background( // RoundedRectangle(cornerRadius: 8) // .fill(Color.inactiveColor.opacity(0.2)) // ) // } // // // Next Step badge // Text(data.nextStep) // .font(.custom("Poppins-Medium", size: 12)) // .foregroundColor(data.colorConfig.nextStepTextColor) // .padding(.horizontal, 12) // .padding(.vertical, 8) // .background( // RoundedRectangle(cornerRadius: 8) // .fill(data.colorConfig.nextStepBackground) // ) // // } // // // }.padding() // .background( RoundedRectangle(cornerRadius: 20) // // .fill(Color.white) // ) // // .border(data.colorConfig.containerBorderColor, width: 2) // // .overlay( // RoundedRectangle(cornerRadius: 20) // .stroke(data.colorConfig.containerBorderColor, lineWidth: 1) // // RoundedRectangle(cornerRadius: 12, style: .continuous) // // .stroke(data.colorConfig.containerBorderColor, lineWidth: 5) // ) // // // .padding(12) // } // } // MARK: - Main Widget View // struct NotificationWidgetView: View { // private let data: NotificationWidgetData // // init(data: NotificationWidgetData) { // self.data = data // } // // var body: some View { // // Use a ZStack as the base to layer the background and content // ZStack { // // Layer 1: Glass effect background with border on all sides // RoundedRectangle(cornerRadius: 12, style: .continuous) // .strokeBorder(data.colorConfig.containerBorderColor, lineWidth: 3) // .background( // RoundedRectangle(cornerRadius: 12, style: .continuous) // // .background( // RoundedRectangle(cornerRadius: 12, style: .continuous) // .fill(Color.white) // ) // ) // .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 4) // // // // Layer 2: Your content, padded from the edges // VStack(alignment: .leading, spacing: 0) { // // Top section with current ticket and image // HStack(alignment: .top, spacing: 0) { // Text(data.currentTicket) // .font(.custom("Poppins-Medium", size: 28)) // .fontWeight(.bold) // .foregroundColor(.black) // .padding(.trailing, 10) // // Spacer() // Pushes the image to the trailing edge // // Image(systemName: data.imageName) // .resizable() // .scaledToFit() // .frame(width: 24, height: 24) // .foregroundColor(.gray) // } // // // Currently Serving Header // Text(data.currentlyServingHeader) // .font(.custom("Poppins-Medium", size: 14)) // .fontWeight(.bold) // .foregroundColor(.black) // .padding(.top, 4) // Adjusted padding // .padding(.bottom, 10) // // // Currently Serving section with status // HStack(alignment: .center, spacing: 0) { // Text(data.currentlyServing) // .font(.custom("Poppins-Medium", size: 16)) // .fontWeight(.bold) // .foregroundColor(.black) // // Spacer() // Pushes status to the trailing edge // // Text(data.status) // .font(.custom("Poppins-Medium", size: 12)) // .foregroundColor(data.colorConfig.nextStepTextColor) // .padding(.horizontal, 12) // .padding(.vertical, 8) // .background( // RoundedRectangle(cornerRadius: 8) // .fill(Color.inactiveColor.opacity(0.2)) // ) // } // // // Next Step badge // Text(data.nextStep) // .font(.custom("Poppins-Medium", size: 12)) // .foregroundColor(data.colorConfig.nextStepTextColor) // .padding(.horizontal, 12) // .padding(.vertical, 8) // .background( // RoundedRectangle(cornerRadius: 8) // .fill(data.colorConfig.nextStepBackground) // ) // .padding(.top, 10) // // // **CRITICAL FIX**: Pushes all content to the top // // and makes the VStack fill the available height. // Spacer(minLength: 0) // } // .padding(15) // Apply padding to the content VStack instead // } // } // } // MARK: - Live Activity Widget struct AppWidgetLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in // Extract parameters from Flutter let title = sharedDefault.string(forKey: context.attributes.prefixedKey("title")) ?? "test" let contentText = sharedDefault.string(forKey: context.attributes.prefixedKey("contentText")) ?? "test" let currentTicket = sharedDefault.string(forKey: context.attributes.prefixedKey("currentTicket")) ?? "test" let status = sharedDefault.string(forKey: context.attributes.prefixedKey("status")) ?? "test" let currentlyServing = sharedDefault.string(forKey: context.attributes.prefixedKey("currentlyServing")) ?? "test" let nextStep = sharedDefault.string(forKey: context.attributes.prefixedKey("nextStep")) ?? "test" let nextStepBackground = sharedDefault.integer(forKey: context.attributes.prefixedKey("nextStepBackground")) // Get color configuration based on nextStepBackground let colorConfig = WidgetColorConfig.config(for: nextStepBackground) // Create widget data let widgetData = NotificationWidgetData( currentTicket: currentTicket, currentlyServingHeader: "Currently Serving:", currentlyServing: currentlyServing, status: status, nextStep: nextStep, imageName:"hmg", colorConfig: colorConfig ) // Lock screen/banner UI NotificationWidgetView(data: widgetData) .activityBackgroundTint(Color.clear) .activitySystemActionForegroundColor(Color.black) } dynamicIsland: { context in let currentTicket = sharedDefault.string(forKey: context.attributes.prefixedKey("currentTicket")) ?? "" let status = sharedDefault.string(forKey: context.attributes.prefixedKey("status")) ?? "" let currentlyServing = sharedDefault.string(forKey: context.attributes.prefixedKey("currentlyServing")) ?? "" let nextStep = sharedDefault.string(forKey: context.attributes.prefixedKey("nextStep")) ?? "" let nextStepBackground = sharedDefault.integer(forKey: context.attributes.prefixedKey("nextStepBackground")) let colorConfig = WidgetColorConfig.config(for: nextStepBackground) return DynamicIsland { // Expanded UI DynamicIslandExpandedRegion(.leading) { VStack(alignment: .leading) { Text(currentTicket) .font(.system(size: 24, weight: .bold)) .foregroundColor(.primary) } } DynamicIslandExpandedRegion(.trailing) { Image("hmg") .resizable() .scaledToFit() .frame(width: 34, height: 34) .foregroundColor(.gray) } DynamicIslandExpandedRegion(.center) { VStack(spacing: 4) { Text("Currently Serving:") .font(.caption2) .foregroundColor(.secondary) Text(currentlyServing) .font(.caption) .fontWeight(.semibold) } } DynamicIslandExpandedRegion(.bottom) { HStack { Text(status) .font(.caption2) .padding(.horizontal, 8) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: 6) .fill(colorConfig.nextStepBackground.opacity(0.2)) ) Spacer() Text(nextStep) .font(.caption2) .foregroundColor(colorConfig.nextStepTextColor) .padding(.horizontal, 8) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: 6) .fill(colorConfig.nextStepBackground) ) } .padding(.horizontal) } } compactLeading: { // Compact leading view Text(currentTicket) .font(.caption2) .fontWeight(.bold) } compactTrailing: { // Compact trailing view Image("hmg") .resizable() .scaledToFit() .frame(width: 12, height: 12) .foregroundColor(colorConfig.containerBorderColor).padding(4) } minimal: { // Minimal view HStack{ Text(currentTicket) .font(.caption2) .fontWeight(.bold) Spacer() Image("hmg") .resizable() .scaledToFit() .frame(width: 4, height: 4) .foregroundColor(colorConfig.containerBorderColor) }.padding(2) } .contentMargins(.all, 20,for: .expanded) } } } extension LiveActivitiesAppAttributes { func prefixedKey(_ key: String) -> String { return "\(id)_\(key)" } } // MARK: - Preview for Testing struct NotificationWidgetView_Previews: PreviewProvider { static var previews: some View { Group { // Preview with nextStepBackground = 0 (Yellow border) NotificationWidgetView( data: NotificationWidgetData( currentTicket: "42", currentlyServingHeader: "Currently Serving:", currentlyServing: "Customer #15", status: "Pending", nextStep: "Next: Prepare Order", imageName: "bell.fill", colorConfig: .config(for: 0) ) ) .previewDisplayName("Yellow Border (0)") // Preview with nextStepBackground = 1 (Red border) NotificationWidgetView( data: NotificationWidgetData( currentTicket: "89", currentlyServingHeader: "Currently Serving:", currentlyServing: "Customer #67", status: "Active", nextStep: "Next: Quality Check", imageName: "bell.fill", colorConfig: .config(for: 1) ) ) .previewDisplayName("Red Border (1)") // Preview with nextStepBackground = 2 (Green border) NotificationWidgetView( data: NotificationWidgetData( currentTicket: "125", currentlyServingHeader: "Currently Serving:", currentlyServing: "Customer #98", status: "Completed", nextStep: "Next: Ready for Pickup", imageName: "bell.fill", colorConfig: .config(for: 2) ) ) .previewDisplayName("Green Border (2)") } .padding() .previewLayout(.sizeThatFits) } }