Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

223 lines
7.8 KiB
Swift

import Capacitor
import FamilyControls
import Foundation
import UIKit
@objc(ElizaAppBlockerPlugin)
public class ElizaAppBlockerPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "ElizaAppBlockerPlugin"
public let jsName = "ElizaAppBlocker"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getInstalledApps", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "selectApps", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "blockApps", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "unblockApps", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise),
]
@objc public override func checkPermissions(_ call: CAPPluginCall) {
call.resolve(buildPermissionResult())
}
@objc public override func requestPermissions(_ call: CAPPluginCall) {
if #available(iOS 16.0, *) {
Task { @MainActor in
var reasonOverride: String?
do {
try await AuthorizationCenter.shared.requestAuthorization(for: .individual)
} catch {
reasonOverride = error.localizedDescription
}
call.resolve(buildPermissionResult(reasonOverride: reasonOverride))
}
return
}
AuthorizationCenter.shared.requestAuthorization { result in
let reasonOverride: String?
switch result {
case .success:
reasonOverride = nil
case .failure(let error):
reasonOverride = error.localizedDescription
}
call.resolve(self.buildPermissionResult(reasonOverride: reasonOverride))
}
}
@objc func getInstalledApps(_ call: CAPPluginCall) {
call.resolve([
"apps": [],
])
}
@objc func selectApps(_ call: CAPPluginCall) {
guard let presenter = bridge?.viewController else {
call.reject("Could not present the iPhone app picker.")
return
}
Task { @MainActor in
FamilyActivityPickerBridge.present(from: presenter) { tokens, cancelled in
let apps = tokens.enumerated().compactMap { index, token -> JSObject? in
guard let tokenData = AppBlockerShared.serializeToken(token) else {
return nil
}
var object = JSObject()
object["packageName"] = ""
object["displayName"] = "Selected App \(index + 1)"
object["tokenData"] = tokenData
return object
}
call.resolve([
"apps": JSArray(apps),
"cancelled": cancelled,
])
}
}
}
@objc func blockApps(_ call: CAPPluginCall) {
if AuthorizationCenter.shared.authorizationStatus != .approved {
call.resolve([
"success": false,
"endsAt": NSNull(),
"error": nullable(permissionReason(for: AuthorizationCenter.shared.authorizationStatus)),
"blockedCount": 0,
])
return
}
let durationMinutes = call.getInt("durationMinutes")
if let durationMinutes, durationMinutes > 0 {
call.resolve([
"success": false,
"endsAt": NSNull(),
"error": "Timed iPhone app blocking requires a DeviceActivity extension. Omit durationMinutes for an indefinite block, then call unblockApps when the block should end.",
"blockedCount": 0,
])
return
}
let tokenDataArray = (call.getArray("appTokens") ?? []).compactMap { $0 as? String }
let tokens = AppBlockerShared.deserializeTokens(tokenDataArray)
guard !tokens.isEmpty else {
call.resolve([
"success": false,
"endsAt": NSNull(),
"error": "Select at least one iPhone app to block.",
"blockedCount": 0,
])
return
}
AppBlockerShared.startBlock(tokens: tokens)
call.resolve([
"success": true,
"endsAt": NSNull(),
"blockedCount": tokens.count,
])
}
@objc func unblockApps(_ call: CAPPluginCall) {
AppBlockerShared.stopBlock()
call.resolve([
"success": true,
])
}
@objc func getStatus(_ call: CAPPluginCall) {
let state = AppBlockerShared.loadState()
let permission = buildPermissionResult()
let permissionStatus = permission["status"] as? String ?? "not-determined"
let reason = permission["reason"] as? String
let blockedCount = state?.tokenDataArray.count ?? 0
let active = permissionStatus == "granted" && blockedCount > 0
call.resolve([
"status": active ? "active" : "inactive",
"available": true,
"active": active,
"platform": "ios",
"engine": "family-controls",
"capabilities": appBlockerCapabilities(),
"blockedCount": blockedCount,
"blockedPackageNames": [],
"endsAt": nullable(AppBlockerShared.endsAtString(for: state)),
"permissionStatus": permissionStatus,
"canRequest": permission["canRequest"] as? Bool ?? false,
"canOpenSettings": permission["canOpenSettings"] as? Bool ?? true,
"settingsTarget": nullable(permission["settingsTarget"]),
"reason": nullable(reason),
])
}
private func buildPermissionResult(reasonOverride: String? = nil) -> [String: Any] {
let status = AuthorizationCenter.shared.authorizationStatus
let mappedStatus: String
let canRequest: Bool
let settingsTarget: Any
switch status {
case .approved:
mappedStatus = "granted"
canRequest = false
settingsTarget = NSNull()
case .notDetermined:
mappedStatus = "not-determined"
canRequest = true
settingsTarget = "screenTime"
default:
mappedStatus = "denied"
canRequest = true
settingsTarget = "screenTime"
}
var result: [String: Any] = [
"status": mappedStatus,
"canRequest": canRequest,
"canOpenSettings": mappedStatus != "granted",
"settingsTarget": settingsTarget,
"engine": "family-controls",
"capabilities": appBlockerCapabilities(),
]
if let reason = reasonOverride ?? permissionReason(for: status) {
result["reason"] = reason
}
return result
}
private func appBlockerCapabilities() -> [String: Any] {
[
"canSelectApps": true,
"canBlockApps": true,
"canScheduleTimedBlocks": false,
"canUnblockEarly": true,
"requiresFamilyControls": true,
"requiresUsageAccess": false,
"requiresOverlay": false,
]
}
private func permissionReason(for status: AuthorizationStatus) -> String? {
switch status {
case .approved:
return nil
case .notDetermined:
return "Authorize Family Controls before Eliza can choose and shield apps on this iPhone."
default:
return "Family Controls access is currently denied for this app. Re-run authorization on this iPhone developer build."
}
}
private func nullable(_ value: Any?) -> Any {
value ?? NSNull()
}
}