From 2ec0a0c0cd0c7a8a7cc8330e847fc3897e54078a Mon Sep 17 00:00:00 2001 From: sravan Date: Fri, 24 Jul 2026 16:24:15 +0530 Subject: [PATCH] feat(share-ext): confirmation card instead of a confusing blank flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mobile/ios-share/ShareViewController.swift | 119 ++++++++++++++++++--- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/mobile/ios-share/ShareViewController.swift b/mobile/ios-share/ShareViewController.swift index bed4b52..d7eb9bb 100644 --- a/mobile/ios-share/ShareViewController.swift +++ b/mobile/ios-share/ShareViewController.swift @@ -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 // 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 -// upload progress already exist — re-implementing all of that natively here would be a second, divergent -// copy of the app. So this extension only does the part that must happen inside it: -// 1. copy the shared items into the App Group container (the only storage both processes can see), and -// 2. bounce the user into the app, which picks them up and shows "Send to…". -// If step 2 is blocked, the files still sit staged and are collected the next time the app is opened — -// nothing is lost, the hand-off is just deferred. +// The picker itself lives in the APP, not here — the chat list, search and upload progress already exist +// there, and re-implementing them natively would be a second, divergent copy (and the extension can't +// reach the web session where contacts live). So this extension: +// 1. copies the shared items into the App Group container (the only storage both processes can see), +// 2. tries to open the app (bizconnect://share), and +// 3. shows a small confirmation card — because a Share Extension opening its host app is unsupported on +// 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 { private let appGroup = "group.com.bizgaze.connect" private let urlScheme = "bizconnect" 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) { super.viewDidAppear(animated) handleShare() @@ -31,25 +47,104 @@ class ShareViewController: UIViewController { .prefix(maxItems) guard !providers.isEmpty else { return finish() } - var staged: [[String: Any]] = [] + var collected: [[String: Any]] = [] let lock = NSLock() let group = DispatchGroup() for provider in providers { group.enter() 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.notify(queue: .main) { [weak self] in guard let self = self else { return } - if !staged.isEmpty { self.appendToManifest(staged) } - self.openHostApp() - self.finish() + if !collected.isEmpty { self.appendToManifest(collected) } + self.staged = !collected.isEmpty + 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 = "Couldn’t 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). private func load(_ provider: NSItemProvider, completion: @escaping ([String: Any]?) -> Void) { // Most specific first; .item is the catch-all for arbitrary documents.