feat(share-ext): confirmation card instead of a confusing blank flash

A Share Extension opening its host app is unsupported on modern iOS (restricted
~iOS 14), so the programmatic bizconnect://share open is silently blocked and the
user just saw a blank flash back to Photos — looking broken even though the files
staged fine.

The extension now shows a small native card after staging: "✓ Ready to send — Open
Biz Connect to choose a chat", with an "Open Biz Connect" button (user-initiated
open has the best chance of working) and a Done button. It still attempts the
auto-open first. Either way the app collects the staged files when next opened, so
the manual path that already works is unchanged — this just removes the "did it
even work?" confusion.

Renamed the local `staged` array to `collected` to free `staged` for the state
flag. Balance + selectors checked. NEEDS A NEW iOS BUILD (native change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 16:24:15 +05:30
parent 0c3487fdb0
commit 2ec0a0c0cd
+107 -12
View File
@@ -5,19 +5,35 @@ import UniformTypeIdentifiers
// An app CANNOT appear there any other way: the share sheet only lists app extensions, so this has to be a // An app CANNOT appear there any other way: the share sheet only lists app extensions, so this has to be a
// second target inside the app bundle, with its own bundle id and provisioning profile. // second target inside the app bundle, with its own bundle id and provisioning profile.
// //
// It deliberately has NO UI. Picking the conversation happens in the app, where the chat list, search and // The picker itself lives in the APP, not here the chat list, search and upload progress already exist
// upload progress already exist re-implementing all of that natively here would be a second, divergent // there, and re-implementing them natively would be a second, divergent copy (and the extension can't
// copy of the app. So this extension only does the part that must happen inside it: // reach the web session where contacts live). So this extension:
// 1. copy the shared items into the App Group container (the only storage both processes can see), and // 1. copies the shared items into the App Group container (the only storage both processes can see),
// 2. bounce the user into the app, which picks them up and shows "Send to". // 2. tries to open the app (bizconnect://share), and
// If step 2 is blocked, the files still sit staged and are collected the next time the app is opened // 3. shows a small confirmation card because a Share Extension opening its host app is unsupported on
// nothing is lost, the hand-off is just deferred. // modern iOS and often silently blocked, so the card gives the user a clear "Open Biz Connect" tap
// and states the files are ready. Either way the app collects the staged files when next opened, so
// nothing is ever lost.
class ShareViewController: UIViewController { class ShareViewController: UIViewController {
private let appGroup = "group.com.bizgaze.connect" private let appGroup = "group.com.bizgaze.connect"
private let urlScheme = "bizconnect" private let urlScheme = "bizconnect"
private let maxItems = 20 private let maxItems = 20
// Minimal confirmation card (built in code no storyboard).
private let card = UIView()
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let openButton = UIButton(type: .system)
private let doneButton = UIButton(type: .system)
private var staged = false
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.35)
buildCard()
}
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
handleShare() handleShare()
@@ -31,25 +47,104 @@ class ShareViewController: UIViewController {
.prefix(maxItems) .prefix(maxItems)
guard !providers.isEmpty else { return finish() } guard !providers.isEmpty else { return finish() }
var staged: [[String: Any]] = [] var collected: [[String: Any]] = []
let lock = NSLock() let lock = NSLock()
let group = DispatchGroup() let group = DispatchGroup()
for provider in providers { for provider in providers {
group.enter() group.enter()
load(provider) { record in load(provider) { record in
if let record = record { lock.lock(); staged.append(record); lock.unlock() } if let record = record { lock.lock(); collected.append(record); lock.unlock() }
group.leave() group.leave()
} }
} }
group.notify(queue: .main) { [weak self] in group.notify(queue: .main) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if !staged.isEmpty { self.appendToManifest(staged) } if !collected.isEmpty { self.appendToManifest(collected) }
self.openHostApp() self.staged = !collected.isEmpty
self.finish() self.openHostApp() // best-effort; usually blocked, hence the card below
self.showReady(count: collected.count)
} }
} }
// MARK: - Confirmation UI
private func buildCard() {
card.backgroundColor = .systemBackground
card.layer.cornerRadius = 16
card.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(card)
titleLabel.text = "Preparing…"
titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
titleLabel.textColor = .label
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 0
subtitleLabel.text = ""
subtitleLabel.font = .systemFont(ofSize: 13)
subtitleLabel.textColor = .secondaryLabel
subtitleLabel.textAlignment = .center
subtitleLabel.numberOfLines = 0
openButton.setTitle("Open Biz Connect", for: .normal)
openButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
openButton.setTitleColor(.white, for: .normal)
openButton.backgroundColor = UIColor(red: 0.12, green: 0.23, blue: 0.45, alpha: 1) // brand navy
openButton.layer.cornerRadius = 10
openButton.addTarget(self, action: #selector(openTapped), for: .touchUpInside)
openButton.isHidden = true
doneButton.setTitle("Done", for: .normal)
doneButton.titleLabel?.font = .systemFont(ofSize: 15)
doneButton.addTarget(self, action: #selector(doneTapped), for: .touchUpInside)
doneButton.isHidden = true
let stack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel, openButton, doneButton])
stack.axis = .vertical
stack.spacing = 12
stack.alignment = .fill
stack.setCustomSpacing(6, after: titleLabel)
stack.setCustomSpacing(18, after: subtitleLabel)
stack.translatesAutoresizingMaskIntoConstraints = false
card.addSubview(stack)
NSLayoutConstraint.activate([
card.centerXAnchor.constraint(equalTo: view.centerXAnchor),
card.centerYAnchor.constraint(equalTo: view.centerYAnchor),
card.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 32),
card.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -32),
card.widthAnchor.constraint(lessThanOrEqualToConstant: 340),
stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 22),
stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -18),
stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 22),
stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -22),
openButton.heightAnchor.constraint(equalToConstant: 46)
])
}
private func showReady(count: Int) {
if !staged {
titleLabel.text = "Couldnt prepare the file"
subtitleLabel.text = "Please try sharing again."
} else {
titleLabel.text = "✓ Ready to send"
let noun = count == 1 ? "item" : "\(count) items"
subtitleLabel.text = "Open Biz Connect to choose a chat and send your \(noun)."
openButton.isHidden = false
}
doneButton.isHidden = false
}
@objc private func openTapped() {
openHostApp()
finish()
}
@objc private func doneTapped() {
finish()
}
/// Resolve one attachment to a staged file (or a text/link record). /// Resolve one attachment to a staged file (or a text/link record).
private func load(_ provider: NSItemProvider, completion: @escaping ([String: Any]?) -> Void) { private func load(_ provider: NSItemProvider, completion: @escaping ([String: Any]?) -> Void) {
// Most specific first; .item is the catch-all for arbitrary documents. // Most specific first; .item is the catch-all for arbitrary documents.