import Foundation import Capacitor // Reads the files the Share Extension staged into the App Group container, so the web app can pick a // conversation and send them. The extension and the app are separate processes; the App Group's shared // container is the only place both can read/write, and it is NOT one of @capacitor/filesystem's known // directories — hence this small bridge. // // getPending() → { items: [ {kind, name, path, uri, mime, size} | {kind:"text", text} ] } // `uri` is a file:// URL the web layer turns into a fetchable source with Capacitor.convertFileSrc, // so the existing upload path can read the bytes without base64 marshalling. // clear() removes the manifest and every staged file, once the app has taken them. @objc(ShareInboxPlugin) public class ShareInboxPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "ShareInboxPlugin" public let jsName = "ShareInbox" public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod(name: "getPending", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setAuth", returnType: CAPPluginReturnPromise) ] private let appGroup = "group.com.bizgaze.connect" // The web app hands the extension a bearer token + API base (via /api/share/token) so the extension can // list chats, upload and send on its own — no app-open needed. Stored in the App Group's shared // UserDefaults, which the extension reads directly. Passing an empty token clears it (e.g. on logout). @objc func setAuth(_ call: CAPPluginCall) { let token = call.getString("token") ?? "" let base = call.getString("base") ?? "" if let d = UserDefaults(suiteName: appGroup) { if token.isEmpty { d.removeObject(forKey: "bzc_token"); d.removeObject(forKey: "bzc_base") } else { d.set(token, forKey: "bzc_token"); d.set(base, forKey: "bzc_base") } } call.resolve(["ok": true]) } @objc func getPending(_ call: CAPPluginCall) { guard let dir = sharedDir() else { return call.resolve(["items": []]) } let manifest = dir.appendingPathComponent("manifest.json") guard let data = try? Data(contentsOf: manifest), let records = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return call.resolve(["items": []]) } var items: [[String: Any]] = [] for r in records { let kind = r["kind"] as? String ?? "file" if kind == "text" { if let text = r["text"] as? String { items.append(["kind": "text", "text": text]) } continue } // A file record is only usable if its staged copy is still on disk. guard let path = r["path"] as? String, FileManager.default.fileExists(atPath: path) else { continue } let url = URL(fileURLWithPath: path) items.append([ "kind": "file", "name": r["name"] as? String ?? url.lastPathComponent, "path": path, "uri": url.absoluteString, "mime": r["mime"] as? String ?? "application/octet-stream", "size": r["size"] as? Int64 ?? (r["size"] as? Int ?? 0) ]) } call.resolve(["items": items]) } @objc func clear(_ call: CAPPluginCall) { if let dir = sharedDir() { let fm = FileManager.default if let entries = try? fm.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil) { for e in entries { try? fm.removeItem(at: e) } } } call.resolve() } private func sharedDir() -> URL? { guard let base = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { return nil } return base.appendingPathComponent("Shared", isDirectory: true) } }