feat(share): "Biz Connect" in the iOS share sheet — share a photo/file into a chat
Adds the reverse direction: share FROM Photos/Files/Safari INTO a Biz Connect conversation. An app can only appear in the iOS share sheet as an app-extension target, so this is real native work, not a web change. Pieces: - mobile/ios-share/ShareViewController.swift: a UI-less Share Extension. It stages the shared items into the App Group container and opens bizconnect://share. It deliberately does NOT reimplement the chat picker — that lives in the app, which already has the chat list, search and upload progress. Appends to the manifest (never overwrites), so sharing twice before opening the app loses nothing. - mobile/scripts/add-share-extension.rb: injects the extension target into the Capacitor-generated Xcode project on every CI build (Codemagic checks out fresh), using the xcodeproj gem that ships with CocoaPods. Embeds it, sets the bundle id <app>.share, and MERGES the App Group into the app's entitlements rather than clobbering them (push's aps-environment must survive). Idempotent. - mobile/plugins/share-inbox: getPending()/clear() to read that manifest — the App Group container isn't one of Filesystem's known directories, so it needs a bridge. - home.html: on bizconnect://share (and every resume, and cold-launch), read the inbox and show a "Send to…" picker over the chat list; chosen files run the SAME upload + /api/messages send as an in-app attachment. Reuses convertFileSrc to read the staged bytes with no base64 marshalling. - ios-patch.sh registers the bizconnect URL scheme; codemagic.yaml fetches a profile for the .share bundle id too. One-time manual gate (CI cannot toggle App capabilities): the App Group group.com.bizgaze.connect must be created and enabled on both App IDs in the Apple portal — documented in mobile/IOS_SETUP.md. Without it the two processes can't see each other's files and sharing silently no-ops; everything else still works. Validated cross-file: pod-name/jsName/method wiring for all three plugins, App Group id identical in all 4 files, URL scheme consistent across extension/plist/web, entitlement-merge preserves push. Needs a new iOS build (new targets + plugins). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
# Pod name MUST be 'ShareInbox' (PascalCase of 'share-inbox') — same rule as the other plugins, or
|
||||
# pod install fails with "No podspec found for `ShareInbox`".
|
||||
s.name = 'ShareInbox'
|
||||
s.version = package['version']
|
||||
s.summary = package['description']
|
||||
s.license = package['license']
|
||||
s.homepage = 'https://bizgaze.com'
|
||||
s.author = 'BizGaze'
|
||||
s.source = { :git => 'https://bizgaze.com/share-inbox.git', :tag => s.version.to_s }
|
||||
s.source_files = 'ios/Sources/**/*.{swift,h,m}'
|
||||
s.ios.deployment_target = '14.0'
|
||||
s.dependency 'Capacitor'
|
||||
s.swift_version = '5.1'
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
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)
|
||||
]
|
||||
|
||||
private let appGroup = "group.com.bizgaze.connect"
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "share-inbox",
|
||||
"version": "1.0.0",
|
||||
"description": "Read files handed to Biz Connect from the iOS share sheet (via the App Group)",
|
||||
"main": "dist/plugin.cjs.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"author": "BizGaze",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist/",
|
||||
"ios/",
|
||||
"ShareInbox.podspec"
|
||||
],
|
||||
"capacitor": {
|
||||
"ios": {
|
||||
"src": "ios"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/core": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^7.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user