mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
236016ed8a
* fix(ios): probe remote-hosted system modals (AccessorySetupKit picker) when the springboard mirror yields no hittable actions * fix(ios): fail closed on host state, guard dismissal re-query, unit-test probe routing Addresses review on #1232: - Gate the remote-host probe to a foreground host (RemoteHostedSystemModalPolicy.isEligibleHostState); background/unknown hosts fail closed instead of substituting an unrelated action tree. - Wrap the alert-resolution fallback query in safeElementsQuery so a dismissed remote host raising kAXErrorServerNotFound is absorbed. - Extract routing/gating into RemoteHostedSystemModalPolicy and add simulator-free unit tests under AGENT_DEVICE_RUNNER_UNIT_TESTS. * refactor(ios): centralize blocking system modal resolution * fix(ios): bound alert dismissal rechecks * feat(ios): enable alerts on physical devices * fix(ios): bound alert system modal resolution * test(ios): add AccessorySetupKit picker fixture * fix(ios): validate remote-hosted system modal interactions * chore: keep pnpm checks non-interactive * fix(ios): share alert command deadline --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
113 lines
3.1 KiB
Swift
113 lines
3.1 KiB
Swift
import AccessorySetupKit
|
|
import CoreBluetooth
|
|
import ExpoModulesCore
|
|
import UIKit
|
|
|
|
public final class AccessorySetupLabModule: Module {
|
|
public func definition() -> ModuleDefinition {
|
|
Name("AccessorySetupLab")
|
|
|
|
AsyncFunction("showPickerAsync") { (promise: Promise) in
|
|
guard #available(iOS 18.0, *) else {
|
|
promise.reject(
|
|
Exception(
|
|
name: "UnsupportedOperation",
|
|
description: "AccessorySetupKit requires iOS 18 or later."
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
AccessorySetupController.shared.showPicker(promise: promise)
|
|
}.runOnQueue(.main)
|
|
}
|
|
}
|
|
|
|
@available(iOS 18.0, *)
|
|
private final class AccessorySetupController {
|
|
static let shared = AccessorySetupController()
|
|
|
|
private let session = ASAccessorySession()
|
|
private var isActivated = false
|
|
private var isPickerPresented = false
|
|
|
|
func showPicker(promise: Promise) {
|
|
guard !isPickerPresented else {
|
|
promise.reject(
|
|
Exception(
|
|
name: "PickerAlreadyPresented",
|
|
description: "The accessory picker is already presented."
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
guard
|
|
let serviceUuid = firstInfoPlistString(forKey: "NSAccessorySetupBluetoothServices")
|
|
else {
|
|
promise.reject(
|
|
Exception(
|
|
name: "MissingAccessoryService",
|
|
description:
|
|
"The development client is missing its AccessorySetupKit test service configuration."
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
let descriptor = ASDiscoveryDescriptor()
|
|
descriptor.bluetoothServiceUUID = CBUUID(string: serviceUuid)
|
|
descriptor.bluetoothNameSubstring = firstInfoPlistString(
|
|
forKey: "NSAccessorySetupBluetoothNames"
|
|
)
|
|
|
|
let productImage = UIImage(
|
|
systemName: "dot.radiowaves.left.and.right",
|
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: 64, weight: .regular)
|
|
) ?? UIImage()
|
|
let displayItem = ASPickerDisplayItem(
|
|
name: descriptor.bluetoothNameSubstring ?? "Test accessory",
|
|
productImage: productImage,
|
|
descriptor: descriptor
|
|
)
|
|
|
|
if #available(iOS 26.0, *) {
|
|
let settings = ASPickerDisplaySettings.default
|
|
settings.discoveryTimeout = .short
|
|
session.pickerDisplaySettings = settings
|
|
}
|
|
|
|
let presentPicker = { [weak self] in
|
|
guard let self else { return }
|
|
|
|
session.showPicker(for: [displayItem]) { [weak self] error in
|
|
self?.isPickerPresented = false
|
|
if let error {
|
|
promise.reject(
|
|
Exception(name: "AccessoryPickerFailed", description: error.localizedDescription)
|
|
)
|
|
} else {
|
|
promise.resolve(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
isPickerPresented = true
|
|
if isActivated {
|
|
presentPicker()
|
|
return
|
|
}
|
|
|
|
session.activate(on: .main) { [weak self] event in
|
|
guard let self, event.eventType == .activated else { return }
|
|
isActivated = true
|
|
presentPicker()
|
|
}
|
|
}
|
|
|
|
private func firstInfoPlistString(forKey key: String) -> String? {
|
|
let values = Bundle.main.object(forInfoDictionaryKey: key) as? [String]
|
|
return values?.first(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })
|
|
}
|
|
}
|