From db4af0ef6a4c58edf438b47227a04f9827ef88d6 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Thu, 22 Oct 2026 18:37:28 -0400 Subject: [PATCH 1/2] feat: add App Intents and device control widget --- .gitignore | 4 + app-intents/AppIntentsSetup.swift | 21 + app-intents/AppShortcuts.swift | 43 ++ app-intents/DeviceActionIntents.swift | 105 ++++ app-intents/DeviceEntity.swift | 59 +++ app-intents/DeviceQuery.swift | 25 + app.json | 66 ++- package-lock.json | 469 +++--------------- package.json | 3 +- plugins/WakeDeviceControl.swift | 143 ++++++ plugins/withWakeControl.js | 61 +++ src/context/AuthContext.tsx | 2 + src/services/widgetSync.ts | 121 +++-- src/widgets/DeviceControl.tsx | 181 +++++++ targets/widget/AppIntent.swift | 82 --- .../AppIcon.appiconset/App-Icon-20x20@1x.png | Bin 340 -> 0 bytes .../AppIcon.appiconset/App-Icon-20x20@2x.png | Bin 703 -> 0 bytes .../AppIcon.appiconset/App-Icon-20x20@3x.png | Bin 1037 -> 0 bytes .../AppIcon.appiconset/App-Icon-29x29@1x.png | Bin 503 -> 0 bytes .../AppIcon.appiconset/App-Icon-29x29@2x.png | Bin 963 -> 0 bytes .../AppIcon.appiconset/App-Icon-29x29@3x.png | Bin 1528 -> 0 bytes .../AppIcon.appiconset/App-Icon-40x40@1x.png | Bin 703 -> 0 bytes .../AppIcon.appiconset/App-Icon-40x40@2x.png | Bin 1396 -> 0 bytes .../AppIcon.appiconset/App-Icon-40x40@3x.png | Bin 2156 -> 0 bytes .../AppIcon.appiconset/App-Icon-60x60@2x.png | Bin 2156 -> 0 bytes .../AppIcon.appiconset/App-Icon-60x60@3x.png | Bin 3409 -> 0 bytes .../AppIcon.appiconset/App-Icon-76x76@1x.png | Bin 1344 -> 0 bytes .../AppIcon.appiconset/App-Icon-76x76@2x.png | Bin 2827 -> 0 bytes .../App-Icon-83.5x83.5@2x.png | Bin 3156 -> 0 bytes .../AppIcon.appiconset/Contents.json | 122 ----- .../AppIcon.appiconset/ItunesArtwork@2x.png | Bin 38852 -> 0 bytes targets/widget/Assets.xcassets/Contents.json | 6 - targets/widget/Info.plist | 11 - targets/widget/WidgetControl.swift | 93 ---- targets/widget/expo-target.config.js | 10 - targets/widget/generated.entitlements | 10 - targets/widget/index.swift | 11 - .../remotewol-ios.icon/Assets/Image.png | Bin 14535 -> 0 bytes .../remotewol-ios.icon/Assets/display 2.png | Bin 10298 -> 0 bytes .../remotewol-ios.icon/Assets/display 3.png | Bin 11115 -> 0 bytes .../remotewol-ios.icon/Assets/display.png | Bin 10632 -> 0 bytes targets/widget/remotewol-ios.icon/icon.json | 66 --- targets/widget/widgets.swift | 208 -------- 43 files changed, 852 insertions(+), 1070 deletions(-) create mode 100644 app-intents/AppIntentsSetup.swift create mode 100644 app-intents/AppShortcuts.swift create mode 100644 app-intents/DeviceActionIntents.swift create mode 100644 app-intents/DeviceEntity.swift create mode 100644 app-intents/DeviceQuery.swift create mode 100644 plugins/WakeDeviceControl.swift create mode 100644 plugins/withWakeControl.js create mode 100644 src/widgets/DeviceControl.tsx delete mode 100644 targets/widget/AppIntent.swift delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 targets/widget/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png delete mode 100644 targets/widget/Assets.xcassets/Contents.json delete mode 100644 targets/widget/Info.plist delete mode 100644 targets/widget/WidgetControl.swift delete mode 100644 targets/widget/expo-target.config.js delete mode 100644 targets/widget/generated.entitlements delete mode 100644 targets/widget/index.swift delete mode 100644 targets/widget/remotewol-ios.icon/Assets/Image.png delete mode 100644 targets/widget/remotewol-ios.icon/Assets/display 2.png delete mode 100644 targets/widget/remotewol-ios.icon/Assets/display 3.png delete mode 100644 targets/widget/remotewol-ios.icon/Assets/display.png delete mode 100644 targets/widget/remotewol-ios.icon/icon.json delete mode 100644 targets/widget/widgets.swift diff --git a/.gitignore b/.gitignore index f8c6c2e..42b3fda 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,7 @@ app-example # generated native folders /ios /android +# @generated expo skills start +.agents/skills/expo-app-intents +.claude/skills/expo-app-intents +# @generated expo skills end diff --git a/app-intents/AppIntentsSetup.swift b/app-intents/AppIntentsSetup.swift new file mode 100644 index 0000000..3211183 --- /dev/null +++ b/app-intents/AppIntentsSetup.swift @@ -0,0 +1,21 @@ +internal import ExpoAppIntents +internal import ExpoModulesCore + +/// Registered Expo inline module that wires app-target App Intents code to expo-app-intents. +/// Do not change the name of this class. +final class AppIntentsSetup: Module { + public func definition() -> ExpoModulesCore.ModuleDefinition { + Name("AppIntentsSetup") + + OnCreate { + if #available(iOS 18.0, *) { + Task { + await AppIntentDispatcher.shared.setShortcutsRefreshHandler { + AppShortcuts.updateAppShortcutParameters() + } + AppShortcuts.updateAppShortcutParameters() + } + } + } + } +} diff --git a/app-intents/AppShortcuts.swift b/app-intents/AppShortcuts.swift new file mode 100644 index 0000000..b1f8d79 --- /dev/null +++ b/app-intents/AppShortcuts.swift @@ -0,0 +1,43 @@ +import AppIntents + +@available(iOS 18.0, *) +struct AppShortcuts: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: WakeDeviceIntent(), + phrases: [ + "Wake a device in \(.applicationName)", + "Wake \(\.$device) in \(.applicationName)", + ], + shortTitle: "Wake Device", + systemImageName: "bolt.fill" + ) + AppShortcut( + intent: SleepDeviceIntent(), + phrases: [ + "Sleep a device in \(.applicationName)", + "Sleep \(\.$device) in \(.applicationName)", + ], + shortTitle: "Sleep Device", + systemImageName: "moon.fill" + ) + AppShortcut( + intent: RestartDeviceIntent(), + phrases: [ + "Restart a device in \(.applicationName)", + "Restart \(\.$device) in \(.applicationName)", + ], + shortTitle: "Restart Device", + systemImageName: "arrow.clockwise" + ) + AppShortcut( + intent: ShutdownDeviceIntent(), + phrases: [ + "Shut down a device in \(.applicationName)", + "Shut down \(\.$device) in \(.applicationName)", + ], + shortTitle: "Shut Down Device", + systemImageName: "power" + ) + } +} diff --git a/app-intents/DeviceActionIntents.swift b/app-intents/DeviceActionIntents.swift new file mode 100644 index 0000000..4637b57 --- /dev/null +++ b/app-intents/DeviceActionIntents.swift @@ -0,0 +1,105 @@ +import AppIntents + +enum DeviceActionPath: String { + case wake + case sleep + case restart + case shutdown +} + +enum DeviceActionIntentError: LocalizedError { + case invalidDevice + + var errorDescription: String? { + "That device can't be opened." + } +} + +func deviceActionURL(_ action: DeviceActionPath, deviceId: String) throws -> URL { + let encoded = deviceId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? deviceId + guard !encoded.isEmpty, + let url = URL(string: "remotewol-upsnap://action/\(action.rawValue)/\(encoded)") else { + throw DeviceActionIntentError.invalidDevice + } + return url +} + +@available(iOS 18.0, *) +struct WakeDeviceIntent: AppIntent { + static let title: LocalizedStringResource = "Wake Device" + static let description = IntentDescription("Wakes the selected device using Wake-on-LAN.") + static let openAppWhenRun = true + + @Parameter(title: "Device", requestValueDialog: "Which device should be woken?") + var device: DeviceEntity + + static var parameterSummary: some ParameterSummary { + Summary("Wake \(\.$device)") + } + + @MainActor + func perform() async throws -> some IntentResult & OpensIntent { + let url = try deviceActionURL(.wake, deviceId: device.id) + return .result(opensIntent: OpenURLIntent(url)) + } +} + +@available(iOS 18.0, *) +struct SleepDeviceIntent: AppIntent { + static let title: LocalizedStringResource = "Sleep Device" + static let description = IntentDescription("Sends the selected device to sleep.") + static let openAppWhenRun = true + + @Parameter(title: "Device", requestValueDialog: "Which device should sleep?") + var device: DeviceEntity + + static var parameterSummary: some ParameterSummary { + Summary("Sleep \(\.$device)") + } + + @MainActor + func perform() async throws -> some IntentResult & OpensIntent { + let url = try deviceActionURL(.sleep, deviceId: device.id) + return .result(opensIntent: OpenURLIntent(url)) + } +} + +@available(iOS 18.0, *) +struct RestartDeviceIntent: AppIntent { + static let title: LocalizedStringResource = "Restart Device" + static let description = IntentDescription("Restarts the selected device.") + static let openAppWhenRun = true + + @Parameter(title: "Device", requestValueDialog: "Which device should restart?") + var device: DeviceEntity + + static var parameterSummary: some ParameterSummary { + Summary("Restart \(\.$device)") + } + + @MainActor + func perform() async throws -> some IntentResult & OpensIntent { + let url = try deviceActionURL(.restart, deviceId: device.id) + return .result(opensIntent: OpenURLIntent(url)) + } +} + +@available(iOS 18.0, *) +struct ShutdownDeviceIntent: AppIntent { + static let title: LocalizedStringResource = "Shut Down Device" + static let description = IntentDescription("Shuts down the selected device.") + static let openAppWhenRun = true + + @Parameter(title: "Device", requestValueDialog: "Which device should shut down?") + var device: DeviceEntity + + static var parameterSummary: some ParameterSummary { + Summary("Shut down \(\.$device)") + } + + @MainActor + func perform() async throws -> some IntentResult & OpensIntent { + let url = try deviceActionURL(.shutdown, deviceId: device.id) + return .result(opensIntent: OpenURLIntent(url)) + } +} diff --git a/app-intents/DeviceEntity.swift b/app-intents/DeviceEntity.swift new file mode 100644 index 0000000..655acee --- /dev/null +++ b/app-intents/DeviceEntity.swift @@ -0,0 +1,59 @@ +import AppIntents +internal import ExpoAppIntents +import Foundation + +private func normalizeDeviceText(_ value: String) -> String { + value + .folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) + .trimmingCharacters(in: .whitespacesAndNewlines) +} + +struct DeviceEntity: AppEntity { + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Device" + static let defaultQuery = DeviceQuery() + + var id: String + @Property(title: "Name") + var name: String + var subtitle: String? + var synonyms: [String] + + var displayRepresentation: DisplayRepresentation { + if let subtitle { + if #available(iOS 17.0, *) { + return DisplayRepresentation( + title: "\(name)", + subtitle: "\(subtitle)", + synonyms: synonyms.map { LocalizedStringResource(stringLiteral: $0) } + ) + } + return DisplayRepresentation(title: "\(name)", subtitle: "\(subtitle)") + } + + if #available(iOS 17.0, *) { + return DisplayRepresentation( + title: "\(name)", + synonyms: synonyms.map { LocalizedStringResource(stringLiteral: $0) } + ) + } + return DisplayRepresentation(title: "\(name)") + } + + init(record: AppIntentEntityRecord) { + self.id = record.id + self.subtitle = record.subtitle + self.synonyms = record.synonyms + self.name = record.title + } + + func matches(_ searchText: String) -> Bool { + let normalizedSearch = normalizeDeviceText(searchText) + guard !normalizedSearch.isEmpty else { + return false + } + + return ([name, id] + synonyms).map(normalizeDeviceText).contains { term in + normalizedSearch == term || normalizedSearch.contains(term) || term.contains(normalizedSearch) + } + } +} diff --git a/app-intents/DeviceQuery.swift b/app-intents/DeviceQuery.swift new file mode 100644 index 0000000..d9d868b --- /dev/null +++ b/app-intents/DeviceQuery.swift @@ -0,0 +1,25 @@ +import AppIntents +internal import ExpoAppIntents + +/// Resolves devices from the catalog published by `syncDevicesToWidget`. +struct DeviceQuery: EntityStringQuery { + func entities(for identifiers: [String]) async throws -> [DeviceEntity] { + try await AppIntentEntityStore.shared + .entities(ofKind: "device", matching: identifiers) + .map(DeviceEntity.init(record:)) + } + + func suggestedEntities() async throws -> [DeviceEntity] { + try await AppIntentEntityStore.shared + .entities(ofKind: "device") + .map(DeviceEntity.init(record:)) + } + + func entities(matching string: String) async throws -> [DeviceEntity] { + let search = string.trimmingCharacters(in: .whitespacesAndNewlines) + guard !search.isEmpty else { + return [] + } + return try await suggestedEntities().filter { $0.matches(search) } + } +} diff --git a/app.json b/app.json index 9c9fc67..1eb3275 100644 --- a/app.json +++ b/app.json @@ -14,7 +14,6 @@ "appleTeamId": "8S7C654DQ4", "entitlements": { "com.apple.security.application-groups": [ - "group.abunchofknowitalls.remotewol-upsnap", "group.abunchofknowitalls.remotewol-upsnap" ] }, @@ -50,35 +49,60 @@ } } ], - "@bacons/apple-targets", + "./plugins/withWakeControl.js", + [ + "expo-widgets", + { + "bundleIdentifier": "com.abunchofknowitalls.remotewol-upsnap.widget", + "groupIdentifier": "group.abunchofknowitalls.remotewol-upsnap", + "widgets": [ + { + "name": "DeviceControl", + "displayName": "Device Control", + "description": "Control your devices with wake, sleep, restart, and shutdown actions.", + "ios": { + "supportedFamilies": ["systemSmall", "systemMedium"], + "initialLayout": "./src/widgets/DeviceControl.tsx", + "configuration": { + "title": "Device Control", + "description": "Select a device to control from your home screen.", + "parameters": { + "device": { + "title": "Device", + "type": "enum", + "default": "none", + "dynamic": true, + "values": [ + { + "name": "Select a Device", + "value": "none" + } + ] + } + } + } + } + } + ] + } + ], "expo-font", "expo-image", - "expo-web-browser" + "expo-web-browser", + "expo-app-intents" ], "experiments": { "typedRoutes": true, - "reactCompiler": true + "reactCompiler": true, + "inlineModules": { + "watchedDirectories": [ + "app-intents" + ] + } }, "extra": { "router": {}, "eas": { - "build": { - "experimental": { - "ios": { - "appExtensions": [ - { - "bundleIdentifier": "com.abunchofknowitalls.remotewol-upsnap.widget", - "targetName": "widget", - "entitlements": { - "com.apple.security.application-groups": [ - "group.abunchofknowitalls.remotewol-upsnap" - ] - } - } - ] - } - } - }, "projectId": "b2c99399-8470-4a78-9253-492056fc1c35" } } diff --git a/package-lock.json b/package-lock.json index 8537106..5aee394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,12 @@ "name": "remote-wol", "version": "1.0.0", "dependencies": { - "@bacons/apple-targets": "^3.0.6", "@expo/ui": "~58.0.21", "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "^2.2.0", "burnt": "^0.13.0", "expo": "^58.0.16", + "expo-app-intents": "~0.5.10", "expo-constants": "~58.0.12", "expo-dev-client": "~58.0.14", "expo-font": "~58.0.9", @@ -27,6 +27,7 @@ "expo-symbols": "~58.0.4", "expo-system-ui": "~58.0.6", "expo-web-browser": "~58.0.6", + "expo-widgets": "~58.0.21", "react": "19.3.0", "react-dom": "19.3.0", "react-native": "0.88.0", @@ -1137,29 +1138,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bacons/apple-targets": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@bacons/apple-targets/-/apple-targets-3.0.7.tgz", - "integrity": "sha512-RpzofKGvll8BPrvgf1Xj5raoHffJRW2qfWl7Q9F/25eo6lZeRa9R5NRIZCpUYoVnsL7whCQV9NIp0/ai4O+Ejw==", - "license": "MIT", - "dependencies": { - "@bacons/xcode": "1.0.0-alpha.27", - "@react-native/normalize-colors": "^0.79.2", - "debug": "^4.3.4", - "glob": "^10.4.2" - } - }, - "node_modules/@bacons/xcode": { - "version": "1.0.0-alpha.27", - "resolved": "https://registry.npmjs.org/@bacons/xcode/-/xcode-1.0.0-alpha.27.tgz", - "integrity": "sha512-WlemTiwpwwVH8IMvg4VBsyxGUuFWUfzeWKzxhU+dRxd8MHvnX7F9PlWCf7T0ZXmtjbl39iEgQB+/Tf3wuGJbyg==", - "license": "MIT", - "dependencies": { - "@expo/plist": "^0.0.18", - "debug": "^4.3.4", - "uuid": "^8.3.2" - } - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -2165,17 +2143,6 @@ "integrity": "sha512-KtLrtAvpShq16l9lTxd2fZSjULxcGxXrJTHA31iUVqrc+Af7aTAJW4ZB3yQBgi/w+kSb6BB/CG9RXI0ohbMm6g==", "license": "MIT" }, - "node_modules/@expo/plist": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.0.18.tgz", - "integrity": "sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "~0.7.0", - "base64-js": "^1.2.3", - "xmlbuilder": "^14.0.0" - } - }, "node_modules/@expo/prebuild-config": { "version": "58.0.14", "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-58.0.14.tgz", @@ -2380,23 +2347,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -2671,16 +2621,6 @@ "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@radix-ui/primitive": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz", @@ -3251,12 +3191,6 @@ "node": "^22.13.0 || ^24.3.0 || >= 26.0.0" } }, - "node_modules/@react-native/normalize-colors": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.7.tgz", - "integrity": "sha512-RrvewhdanEWhlyrHNWGXGZCc6MY0JGpNgRzA8y6OomDz0JmlnlIsbBHbNpPnIrt9Jh2KaV10KTscD1Ry8xU9gQ==", - "license": "MIT" - }, "node_modules/@react-native/virtualized-lists": { "version": "0.88.0", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.88.0.tgz", @@ -4035,16 +3969,6 @@ "win32" ] }, - "node_modules/@xmldom/xmldom": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", - "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", - "deprecated": "this version has critical issues, please update to the latest version", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/2g": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/2g/-/2g-1.0.1.tgz", @@ -4554,6 +4478,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -5388,12 +5313,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5406,12 +5325,6 @@ "integrity": "sha512-9qpCBlXPy5v7n1Q7WTO5rW2StI4WNQhl96HpgXRZLPlNoXAD0LbsQUflovQYAO44Yia073ISusqohncvGSMbGA==", "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -6169,6 +6082,34 @@ } } }, + "node_modules/expo-app-intents": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/expo-app-intents/-/expo-app-intents-0.5.10.tgz", + "integrity": "sha512-PNxAbIaNjLDeajnVJGClDXnB1CFkGScuAYcEORFsmaLmABMjlxq/jNC708cgWVyDgX2XUvDBXifq2rqvQAsPmQ==", + "license": "MIT", + "dependencies": { + "@expo/ui": "^58.0.21", + "commander": "^12.1.0", + "prompts": "^2.4.2" + }, + "bin": { + "expo-app-intents": "bin/expo-app-intents.js" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-app-intents/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/expo-asset": { "version": "58.0.17", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-58.0.17.tgz", @@ -6604,6 +6545,50 @@ "react-native": "*" } }, + "node_modules/expo-widgets": { + "version": "58.0.21", + "resolved": "https://registry.npmjs.org/expo-widgets/-/expo-widgets-58.0.21.tgz", + "integrity": "sha512-0DiTq5jxNgiX7KPcu8GewlvDMxLcrcT4Q2gVyBTg67hzrC8KS4Du6ZbOCYflvaVrdqN8Mhhwtnrl4UFydoYK7Q==", + "license": "MIT", + "dependencies": { + "@expo/plist": "^0.10.1", + "@expo/ui": "~58.0.21" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-widgets/node_modules/@expo/plist": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.10.1.tgz", + "integrity": "sha512-nuNOqj+r/hGJbZ1Ax3jUpZaOB8UAMHUrISALJEUKaCieTflYj7224EqFWmMpriCGk9wXS1gH+9GGVnSj0tWzuQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + } + }, + "node_modules/expo-widgets/node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/expo-widgets/node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/expo/node_modules/@expo/cli": { "version": "58.1.13", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-58.1.13.tgz", @@ -7215,22 +7200,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -7390,27 +7359,6 @@ "node": ">=6" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7424,30 +7372,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -8274,21 +8198,6 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -9928,12 +9837,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9993,28 +9896,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -11112,18 +10993,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/simple-plist": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", @@ -11259,56 +11128,6 @@ "node": ">= 0.10.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", @@ -11408,46 +11227,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", - "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -11977,16 +11756,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/validate-npm-package-name": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", @@ -12177,85 +11946,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/ws": { "version": "7.5.13", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", @@ -12322,15 +12012,6 @@ "node": ">=4.0" } }, - "node_modules/xmlbuilder": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", - "integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==", - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 717e3e2..5440d31 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,12 @@ "lint": "expo lint" }, "dependencies": { - "@bacons/apple-targets": "^3.0.6", "@expo/ui": "~58.0.21", "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "^2.2.0", "burnt": "^0.13.0", "expo": "^58.0.16", + "expo-app-intents": "~0.5.10", "expo-constants": "~58.0.12", "expo-dev-client": "~58.0.14", "expo-font": "~58.0.9", @@ -30,6 +30,7 @@ "expo-symbols": "~58.0.4", "expo-system-ui": "~58.0.6", "expo-web-browser": "~58.0.6", + "expo-widgets": "~58.0.21", "react": "19.3.0", "react-dom": "19.3.0", "react-native": "0.88.0", diff --git a/plugins/WakeDeviceControl.swift b/plugins/WakeDeviceControl.swift new file mode 100644 index 0000000..7896032 --- /dev/null +++ b/plugins/WakeDeviceControl.swift @@ -0,0 +1,143 @@ +// jumpstart-wake-control +// Control Center wake button. Device choices are the home-screen widget's dynamic +// configuration, which JavaScript updates when the device list loads. + +@available(iOS 18.0, *) +struct WakeDeviceControl: ControlWidget { + static let kind: String = "wakecontrol" + + var body: some ControlWidgetConfiguration { + AppIntentControlConfiguration( + kind: Self.kind, + provider: WakeDeviceControlProvider() + ) { value in + ControlWidgetButton(action: WakeControlIntent(deviceId: value.deviceId, deviceName: value.deviceName)) { + Label(value.deviceName, systemImage: "bolt.fill") + } + } + .displayName("Wake Device") + .description("Quickly wake a device from Control Center.") + } +} + +@available(iOS 18.0, *) +extension WakeDeviceControl { + struct Value { + var deviceId: String + var deviceName: String + } +} + +@available(iOS 18.0, *) +struct WakeDeviceControlProvider: AppIntentControlValueProvider { + func previewValue(configuration: WakeControlConfiguration) -> WakeDeviceControl.Value { + WakeDeviceControl.Value( + deviceId: configuration.device ?? "", + deviceName: deviceName(for: configuration.device) + ) + } + + func currentValue(configuration: WakeControlConfiguration) async throws -> WakeDeviceControl.Value { + WakeDeviceControl.Value( + deviceId: configuration.device ?? "", + deviceName: deviceName(for: configuration.device) + ) + } + + private func deviceName(for deviceId: String?) -> String { + guard let deviceId, !deviceId.isEmpty, deviceId != "none" else { + return "Select Device" + } + + let options = widgetConfigurationOptions( + name: "DeviceControl", + parameter: "device", + fallback: [] + ) + return options.first { $0.value == deviceId }?.name ?? "Select Device" + } +} + +@available(iOS 18.0, *) +struct WakeControlConfiguration: ControlConfigurationIntent { + static let title: LocalizedStringResource = "Device to Wake" + + @Parameter(title: "Device", optionsProvider: WakeControlDeviceOptionsProvider()) + var device: String? +} + +@available(iOS 18.0, *) +struct WakeControlDeviceOptionsProvider: DynamicOptionsProvider { + func results() async throws -> IntentItemCollection { + let options = widgetConfigurationOptions( + name: "DeviceControl", + parameter: "device", + fallback: [ + WidgetConfigurationOption(name: "Select a Device", value: "none") + ] + ) + + return IntentItemCollection(sections: [ + IntentItemSection(items: options.map { option in + IntentItem( + option.value, + title: LocalizedStringResource(stringLiteral: option.name), + subtitle: option.subtitle.map { LocalizedStringResource(stringLiteral: $0) } + ) + }) + ]) + } + + func defaultResult() async -> String? { + let options = widgetConfigurationOptions( + name: "DeviceControl", + parameter: "device", + fallback: [] + ) + return options.first { $0.value != "none" }?.value ?? options.first?.value + } +} + +@available(iOS 18.0, *) +struct WakeControlIntent: AppIntent { + static let title: LocalizedStringResource = "Wake Device" + static let description = IntentDescription("Wakes the selected device using Wake-on-LAN.") + static let openAppWhenRun: Bool = true + static let isDiscoverable: Bool = false + + @Parameter(title: "Device ID") + var deviceId: String + + @Parameter(title: "Device Name") + var deviceName: String + + init() { + self.deviceId = "" + self.deviceName = "" + } + + init(deviceId: String, deviceName: String) { + self.deviceId = deviceId + self.deviceName = deviceName + } + + func perform() async throws -> some IntentResult & OpensIntent { + let encoded = deviceId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? deviceId + if !encoded.isEmpty, encoded != "none", let url = URL(string: "remotewol-upsnap://action/wake/\(encoded)") { + return .result(opensIntent: OpenURLIntent(url)) + } + + guard let fallback = URL(string: "remotewol-upsnap://") else { + throw WakeControlIntentError.invalidDevice + } + return .result(opensIntent: OpenURLIntent(fallback)) + } +} + +enum WakeControlIntentError: LocalizedError { + case invalidDevice + + var errorDescription: String? { + "Select a device to wake." + } +} diff --git a/plugins/withWakeControl.js b/plugins/withWakeControl.js new file mode 100644 index 0000000..cbc7556 --- /dev/null +++ b/plugins/withWakeControl.js @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); +const { withDangerousMod } = require('expo/config-plugins'); + +const MARKER = 'jumpstart-wake-control'; + +/** + * Adds the Control Center wake control to the extension expo-widgets generates. + * This plugin must be listed before expo-widgets so it runs after that extension is written. + */ +function withWakeControl(config) { + return withDangerousMod(config, [ + 'ios', + async config => { + const indexPath = path.join( + config.modRequest.platformProjectRoot, + 'ExpoWidgetsTarget', + 'index.swift' + ); + + if (!fs.existsSync(indexPath)) { + throw new Error( + 'expo-widgets has not generated ios/ExpoWidgetsTarget/index.swift yet. List ./plugins/withWakeControl before expo-widgets in the plugins array.' + ); + } + + const source = fs.readFileSync(indexPath, 'utf8'); + if (source.includes(MARKER)) { + return config; + } + + if (!source.includes('WidgetLiveActivity()')) { + throw new Error( + 'Could not find WidgetLiveActivity() in ios/ExpoWidgetsTarget/index.swift.' + ); + } + + const controlPath = path.join( + config.modRequest.projectRoot, + 'plugins', + 'WakeDeviceControl.swift' + ); + const controlSource = fs.readFileSync(controlPath, 'utf8'); + let next = source; + + if (!next.includes('import AppIntents')) { + next = next.replace('import SwiftUI\n', 'import SwiftUI\nimport AppIntents\n'); + } + + next = next.replace( + /^([ \t]*)WidgetLiveActivity\(\)/m, + `$1if #available(iOS 18.0, *) {\n$1 WakeDeviceControl()\n$1}\n$1WidgetLiveActivity()` + ); + next = `${next.trimEnd()}\n\n${controlSource.trim()}\n`; + fs.writeFileSync(indexPath, next); + return config; + }, + ]); +} + +module.exports = withWakeControl; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 8cab4ec..be15a6b 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import api from '../services/api'; +import { syncDevicesToWidget } from '../services/widgetSync'; import { AuthResponse, User } from '../types'; interface AuthContextType { @@ -103,6 +104,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const logout = async () => { try { await clearSession(); + syncDevicesToWidget([]); } catch (error) { console.error('Failed to logout', error); } diff --git a/src/services/widgetSync.ts b/src/services/widgetSync.ts index 6b79a15..64bd810 100644 --- a/src/services/widgetSync.ts +++ b/src/services/widgetSync.ts @@ -1,64 +1,115 @@ -import { ExtensionStorage } from '@bacons/apple-targets'; +import { setEntityCatalogAsync, isAvailable as appIntentsAvailable } from 'expo-app-intents'; +import type { AppIntentEntity } from 'expo-app-intents'; import { Platform } from 'react-native'; import { Device } from '../types'; +import DeviceControlWidget, { type WidgetDevice } from '../widgets/DeviceControl'; -// Create a storage object with the App Group -const storage = - Platform.OS === 'ios' - ? new ExtensionStorage('group.abunchofknowitalls.remotewol-upsnap') - : null; +const DEVICE_KIND = 'device'; -interface WidgetDevice { - id: string; - name: string; - mac: string; - ip: string; - canSleep: boolean; - canShutdown: boolean; - status: string; +let pendingCatalog: AppIntentEntity[] | null = null; +let catalogWrite: Promise | null = null; + +function toWidgetDevice(device: Device): WidgetDevice | null { + const id = device.id.trim(); + if (!id) { + return null; + } + + return { + id, + name: device.name.trim() || device.mac.trim() || 'Device', + mac: device.mac, + ip: device.ip, + canSleep: device.sol_enabled, + canShutdown: device.shutdown_cmd !== '', + status: device.status || 'unknown', + }; +} + +function toCatalogEntity(device: WidgetDevice): AppIntentEntity { + const synonyms = [device.mac, device.ip].filter(value => { + const trimmed = value.trim(); + return trimmed.length > 0 && trimmed !== device.name; + }); + + return { + id: device.id, + title: device.name, + subtitle: device.mac.trim() || undefined, + synonyms, + }; +} + +function publishDeviceCatalog(entities: AppIntentEntity[]): void { + if (!appIntentsAvailable()) { + return; + } + + pendingCatalog = entities; + if (catalogWrite) { + return; + } + + catalogWrite = drainDeviceCatalog(); +} + +async function drainDeviceCatalog(): Promise { + while (pendingCatalog) { + const next = pendingCatalog; + pendingCatalog = null; + try { + await setEntityCatalogAsync(DEVICE_KIND, next); + } catch (error) { + console.warn('Could not publish the device catalog.', error); + break; + } + } + + catalogWrite = null; + if (pendingCatalog) { + catalogWrite = drainDeviceCatalog(); + } } /** - * Syncs device data to the iOS widget via App Group shared storage. - * This allows the widget to display device information and create action deep links. + * Syncs device data to the iOS widget and Siri device catalog. + * Called after the device list loads. */ export function syncDevicesToWidget(devices: Device[]): void { - if (!storage) { + if (Platform.OS !== 'ios') { return; } try { - // Transform devices to the format expected by the widget - const widgetDevices: WidgetDevice[] = devices.map(device => ({ - id: device.id, - name: device.name, - mac: device.mac, - ip: device.ip, - canSleep: device.sol_enabled, - canShutdown: device.shutdown_cmd !== "", - status: device.status || 'unknown', - })); + const widgetDevices = devices + .map(toWidgetDevice) + .filter((device): device is WidgetDevice => device !== null); - const devicesJson = JSON.stringify(widgetDevices); - storage.set('devices', devicesJson); - - // Refresh widgets to pick up the new data - ExtensionStorage.reloadWidget(); + DeviceControlWidget.setConfigurationParameterEnum( + 'device', + widgetDevices.map(device => ({ + name: device.name, + value: device.id, + subtitle: device.mac.trim() || undefined, + })) + ); + DeviceControlWidget.updateSnapshot({ devices: widgetDevices }); + publishDeviceCatalog(widgetDevices.map(toCatalogEntity)); } catch (error) { console.error('Failed to sync devices to widget:', error); } } /** - * Refreshes all widget timelines to pick up new data. + * Refreshes widget timelines so they pick up the latest snapshot. */ export function refreshWidgets(): void { - if (!storage) { + if (Platform.OS !== 'ios') { return; } try { - ExtensionStorage.reloadWidget(); + DeviceControlWidget.reload(); } catch (error) { console.error('Failed to refresh widgets:', error); } diff --git a/src/widgets/DeviceControl.tsx b/src/widgets/DeviceControl.tsx new file mode 100644 index 0000000..53fc696 --- /dev/null +++ b/src/widgets/DeviceControl.tsx @@ -0,0 +1,181 @@ +import { HStack, Image, Link, Spacer, Text, VStack } from '@expo/ui/swift-ui'; +import { + background, + containerBackground, + font, + foregroundStyle, + frame, + lineLimit, + padding, + shapes, +} from '@expo/ui/swift-ui/modifiers'; +import { createWidget, type WidgetEnvironment } from 'expo-widgets'; +import type { SFSymbol } from 'sf-symbols-typescript'; + +export type WidgetDevice = { + id: string; + name: string; + mac: string; + ip: string; + canSleep: boolean; + canShutdown: boolean; + status: string; +}; + +type DeviceControlProps = { + devices: WidgetDevice[]; +}; + +type DeviceControlConfiguration = { + device?: string | null; +}; + +type DeviceAction = { + id: 'wake' | 'sleep' | 'restart' | 'shutdown'; + symbol: SFSymbol; + color: string; + enabled: boolean; +}; + +const DeviceControlWidgetView = ( + props: DeviceControlProps, + environment: WidgetEnvironment +) => { + 'widget'; + const rootModifiers = [ + containerBackground({ type: 'hierarchical', style: 'quaternary' }, 'widget'), + padding({ top: 4, bottom: 4 }), + ]; + const devices = Array.isArray(props.devices) ? props.devices : []; + const selectedId = environment.configuration?.device; + const device = devices.find(candidate => candidate.id === selectedId); + + if (!device || !selectedId || selectedId === 'none') { + return ( + + + + Select a Device + + + Long press to configure + + + ); + } + + const isOnline = device.status === 'online'; + const isOffline = device.status === 'offline'; + const actions: DeviceAction[] = [ + { id: 'wake', symbol: 'bolt.fill', color: '#4CAF50', enabled: isOffline }, + { + id: 'sleep', + symbol: 'moon.fill', + color: '#FF9800', + enabled: isOnline && Boolean(device.canSleep), + }, + { + id: 'restart', + symbol: 'arrow.clockwise', + color: '#2196F3', + enabled: isOnline && Boolean(device.canShutdown), + }, + { + id: 'shutdown', + symbol: 'power', + color: '#F44336', + enabled: isOnline && Boolean(device.canShutdown), + }, + ]; + + const buttons = actions.map(action => { + const tint = action.enabled ? action.color : '#8E8E93'; + const button = ( + + + + ); + + if (!action.enabled) { + return {button}; + } + + return ( + + {button} + + ); + }); + + const header = ( + + {device.name} + + {device.mac} + + + ); + + if (environment.widgetFamily === 'systemSmall') { + return ( + + {header} + + + + {buttons[0]} + {buttons[1]} + + + {buttons[2]} + {buttons[3]} + + + + ); + } + + return ( + + {header} + + {buttons} + + ); +}; + +const DeviceControlWidget = createWidget( + 'DeviceControl', + DeviceControlWidgetView, + { devices: [] } +); + +export default DeviceControlWidget; diff --git a/targets/widget/AppIntent.swift b/targets/widget/AppIntent.swift deleted file mode 100644 index c6708a8..0000000 --- a/targets/widget/AppIntent.swift +++ /dev/null @@ -1,82 +0,0 @@ -import WidgetKit -import AppIntents - -// MARK: - Shared Data Access - -struct SharedDeviceData { - static let appGroupIdentifier = "group.abunchofknowitalls.remotewol-upsnap" - - static var sharedDefaults: UserDefaults? { - UserDefaults(suiteName: appGroupIdentifier) - } - - static func getDevices() -> [DeviceInfo] { - guard let defaults = sharedDefaults, - let jsonString = defaults.string(forKey: "devices"), - let data = jsonString.data(using: .utf8), - let devices = try? JSONDecoder().decode([DeviceInfo].self, from: data) else { - return [] - } - return devices - } - - static func getDevice(id: String) -> DeviceInfo? { - return getDevices().first { $0.id == id } - } -} - -// MARK: - Device Model - -struct DeviceInfo: Codable, Hashable, Identifiable { - let id: String - let name: String - let mac: String - let ip: String - let canSleep: Bool - let canShutdown: Bool - let status: String -} - -// MARK: - Device Entity for AppIntents - -struct DeviceEntity: AppEntity { - static var typeDisplayRepresentation: TypeDisplayRepresentation = "Device" - static var defaultQuery = DeviceQuery() - - var id: String - var name: String - var mac: String - - var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: "\(name)", subtitle: "\(mac)") - } -} - -struct DeviceQuery: EntityQuery { - func entities(for identifiers: [String]) async throws -> [DeviceEntity] { - let devices = SharedDeviceData.getDevices() - return devices - .filter { identifiers.contains($0.id) } - .map { DeviceEntity(id: $0.id, name: $0.name, mac: $0.mac) } - } - - func suggestedEntities() async throws -> [DeviceEntity] { - let devices = SharedDeviceData.getDevices() - return devices.map { DeviceEntity(id: $0.id, name: $0.name, mac: $0.mac) } - } - - func defaultResult() async -> DeviceEntity? { - let devices = SharedDeviceData.getDevices() - return devices.first.map { DeviceEntity(id: $0.id, name: $0.name, mac: $0.mac) } - } -} - -// MARK: - Widget Configuration Intent - -struct ConfigurationAppIntent: WidgetConfigurationIntent { - static var title: LocalizedStringResource { "Device Control" } - static var description: IntentDescription { "Select a device to control from your home screen." } - - @Parameter(title: "Device") - var device: DeviceEntity? -} diff --git a/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png b/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png deleted file mode 100644 index 8ebf0723d35f08f0ac7a7ff5c3c06290c764e57b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 340 zcmV-a0jvIrP);Uc~%4t)&b5RM)}=ok%d&VJz15p*sa`h%0A zb%LG;KLbJspZ(#3)oPXJx#xLln%?hsOd*)UFiaFhOecyWmSr)8V9I8*SuU68sH$oh z2Br{@#p&N3UpU+*_#R$QWBuSJ~ z06`FxWm#2~@B08O%aUaoLj*&I!$B0qs;VxR3p!yKmSrgj!s&Fv5JAskvDj|608G$ThM-tV`r>x!bFhoHx{ZI0uv*X!+eLr;<r1*X#ZH z`MJ5dffWQ+cs!nbK93ID+uJ^$4^|LZk;!DdUN1UGl2j-ZUDzCc&`0yc`S$ z!Hs}hu~>9EonR0}u~aI78v&nCC^SDm56Uo%csxEgH>c}5sM*=s$;nCZA>dOkmkA-D zDwRsLT0K8MR}=-5APD7hSr7zpAmA`PJspikK|MV^$+8U7&d$!`<0Gi4si~Qn8E_!z zg%EOhcnHccjJ373udgqdzQ4a$R#ptd0CjM1;BYw53qddOczj}F0@T^r*~P^Le!053 zIz2rFH9kJRu&{t$2)YtQaesdwRJ~r`+}uQmt*x#1_jgcxdwc!;{pgCItEHu-v9U2w zySuy3&(G*ktJSjEEU1x@k=4~zbVa~6FffqIv|%QXti2k&~-hLNVHlln1+UiHa0fEM!?{9yJcAhRjE`|RYg~trYVX7 zDwRqN4i18WfFTlz_W002ovPDHLkV1l`vL@59O diff --git a/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png b/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png deleted file mode 100644 index 61f57d11070b74b49c636493313fe49a1378c167..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1037 zcmV+o1oHcdP)9!PSZNPi4hs2GEKe@PM#?M65(lI%~R?r(>^r2GwDwXO#h(Ku(5fSO>>5-9< z0Q`Rc+1Xhj5P+fx6qTHuyuH1hn3#w^uCA`i%E~S;FQFU;%3)b{b#*l}GZSCL#l;mB z70u7j2Lb^ogn@E8IyzcfS`b99*T1~H92^`#AruPH>2w^&;TyfZz47t!PzZ%WhK7dX z;^Od)=;-LNu`wuwLJ6stLc&d$z;5-5}~K0eN}EPg~Ob-7%G5XeR$o6%^j zuC9gz|9paM6!H*4TrL+O1k$+OZiZnX4~0CnwY7P9d63LtFf=wcLLLekSe6|b8NuId zZf>rxuj6k92M42~q96kU87vly*=$CTgM)+8($dAn#e#x@q@*N>#m2@yK0Y2E9wJ;6 z;nHfg9LFID&+}%p`Qzh55QL_triX_I1aUYVI-L&TVi2yrzP|GEas;tjt-HIs_{8(P z&*!VDsezbAqlt)!*xK4cSSZ4x*X!+eJA&-(?aj{4;){iag^i631Zi(?Pe@2WSSZ56 zaa>GH48&euURqjO1VMn9#bV)k9%34e#_4n-EEJ(jNl9sLZbp#d;bE`Wiy+t6*A9mR zK^htw3k zp`ZvwadB};NeRU6?(TYedLTi6fB((R4aD;E^D8PU5DJP=jE|2KLLk=N-F<(54+(gl zw_2?T;&eI*A&`thGLy-ao0|)<{r&xgg#}2qva+(Vu>rBHtgO1aI!Hz#4Z|=aBO?%d ze}C`n>QC+_V#vqdWtXB*4CDnmm$OE=H}AU623S;KYx6Dgaj1Q3=R$o zf`Csv&$qX?3xWU{1VQNP>Uw&5!XEi9enC+NgCQd$11gnDrBbO>s{gy+d4fu!yImmr00000NkvXX Hu0mjf&ST?J diff --git a/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png b/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png deleted file mode 100644 index 97f681b66690c24d3dabd344e127b6c09012771d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 503 zcmVn_{rK%~Qg{ zudcrJ(>cD=ck+7S>+26t_yw(2+w1joIvvmRKA&&9-Qp7&excj#x?C=`T5U3!EEY?@ z-^V8epEw*2kH>=_ZnxX%bm9|&wxz zKp;V>R2B*a2xhZcyWIxR>2#*kDFnG(j%8U$Adrwsr8F81fZOdB2n6o;JAlXI5ex>e z*DC;(N|niEAc5c&tyY^zBp_%uo6F@AKUS+%wOWNB5{c;bdb~pLNuTn@zb~ z#9sHHk%i~P1VGa?L}U8dWM4 zfLJUR3*)oO(_1QL{# zl+4b~g4*=-bY*2FBp{GrVq&7ErUn@DKmvkXX*8OtsVQWLL?SIM zElo{L=jZ3hV6|GcS}k%#kgNImd9_*%F+mVmmi_qnh{xkB%O(;Dh^bVnrKKh0iXbNq z4Gm_q85z7@@5#vteuTqej^mJFU|^uJu@N~T$O*@BN~IEFS65dH3kyiIyu2I^hasj= zC>9qNkrRUKwYRtT^z=Y1nM_(NmUujlG+$p|7K`Qk`x|1Not*}Q0ofzSp4;ta7zSc~ zzyI*?5E;(S&U`)}GVnalFbuLpkfn}}4!vFvvHSb`xw$#~Vqsz7`uZATZEbBvqY+sm z$Oa*V=XqqX+wD(JPx!^l%ge;XL@Jen7{_r6g#y_i$VPvEe{*v)#P;|1x3{0;a z)oQg!lS-u~CnrBYKS3>#NZ4$)WHN~~8jZ$gvw;o)1+`l3a5x|q3H7LQB=GzFM@L8aBNmJ8?(TvD0flHZ>U28sLlA_a zp&>yKAVD&j92prA1OY#Ip1--d0fhq4IX*s)Mx#YVMI6WN?d?Ib$H&J=BvM{p?(_Lp lS63mE$z(E_OeXt(_!kXaHkj`en8^SD002ovPDHLkV1gxHw%q^# diff --git a/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png b/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png deleted file mode 100644 index 1833711f8c6d9ab342f75be7a74dbb90847a5247..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1528 zcmVgn@#4j%O`9;s zr%#^_9y~ZPF@enDky(V0?(Xj0yLV$gYPDLc)pmDx3xa?Q;*nXkwY4^z4RjkeY=}f6 zZ{NN}1`%Y?s#UAJUN08oa5yv?4Kj!zgIq3`TCK)na&mIKUN16;AX7GP-hAfF8T^^! z$B*yXvj>?%kSRW&Po+}f&+t5d<;oR~`zS^|`sZkQon| zH8wU@RaHT(6)RRKmCEPOpF>Ura>~ogYiw*pdQ2wM+O=yTCjvQnJf57K9HeLY^5t!9 zZIBazYzhkt4F&@ilS-wqnCj~4EnBugHaukG_xnpqNp6}C z1tFxcu<+5NN00}BJajr;Wo0E6)YsS7-roN6=g+%$?{;-{VL|2P<)x*ikOzSbc%E-> zZ&xT3pc@+-Ga8Nfo5f-Y27{m@gxt7sL#b3k1_UxFFE1}ED+66JnY36elarJ9+sw?2 z$z+~%@#Dw%f3IJ^zJLEd7Sz_(rc$XOH3F&C z)YNR;#=!t*)!9gQN(glb4t0 za5z9WH8o|mT2rYM7G$^EL!l7ptXAuqHESRp0_oJ%)fE&JfbR0;%M%k5Sj>+fKkRlp z=vJ;=X|vfN9Ri80Teq&Rt`2m8K;XfH2lz8RJv~pJJOQ22Xe=%+hC~P?Vz=9Ka&kbC zNF>Z=b1W9apGhPVR;x7{je4F+M)-^ZBrJH*enj{P{B|c%E-=Zss@+X(LFRPN%D^tOVV~ zix;P+rm%F8NW^S53xWWOii(Q8d-o!31nJ>8&g=DNWo3b4U|^uXzaL_~eEG7crw0^- z5Rb=02tj%X(onTP(-6qv)L>N0@6@lUq3xP4T_yRck1jp?K~Y>>eB#6jh(#a@&+{IShvPWRXL@?tY&NG-DM+fJq2c@Y@1SUEYEmkd z5QRXLjT<*=wOUYkz1~nL1gT9;O*tG6P!tsvZQHgDq7aBOJ39-C4@87>63V|r0Q0VK|ub3kii(R;IAs&xI9)cj8KYu&EHy1!MnXIj?9U2;f{NnL=Fc>^`?3hBK01ys`j~+cb zHZ}&aIHV^(Ki_ONuV25ux3~A%vuDVlva+&Mr%r{#;X8NkghC-O48t%C!!QiPFbu;m e48t)0F@FIev-R1(ZZCZR0000r1*X#ZH z`MJ5dffWQ+cs!nbK93ID+uJ^$4^|LZk;!DdUN1UGl2j-ZUDzCc&`0yc`S$ z!Hs}hu~>9EonR0}u~aI78v&nCC^SDm56Uo%csxEgH>c}5sM*=s$;nCZA>dOkmkA-D zDwRsLT0K8MR}=-5APD7hSr7zpAmA`PJspikK|MV^$+8U7&d$!`<0Gi4si~Qn8E_!z zg%EOhcnHccjJ373udgqdzQ4a$R#ptd0CjM1;BYw53qddOczj}F0@T^r*~P^Le!053 zIz2rFH9kJRu&{t$2)YtQaesdwRJ~r`+}uQmt*x#1_jgcxdwc!;{pgCItEHu-v9U2w zySuy3&(G*ktJSjEEU1x@k=4~zbVa~6FffqIv|%QXti2k&~-hLNVHlln1+UiHa0fEM!?{9yJcAhRjE`|RYg~trYVX7 zDwRqN4i18WfFTlz_W002ovPDHLkV1l`vL@59O diff --git a/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png b/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png deleted file mode 100644 index 0b4cd5789f6450ec95e477092afce1da5afa9342..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1396 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 diff --git a/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png b/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png deleted file mode 100644 index 4656986349901abf31946d3bf3076a69e56d8950..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1344 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 E1I&t2Z2$lO diff --git a/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png b/targets/widget/Assets.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png deleted file mode 100644 index 4d4bedd6b26187edcf68f7c7bf32b9f970b9ba8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3156 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 d(YxAy?^c8hi{+m zaVV>E?7dScdQtrCA2z~0^|Iy zarf}BuPqrq@T{S9g zvtc1j^)TL(%Owt(T3MD7wyggQPATzkVWDWWYY0Ye-MTehtC7ZvC*-(S^x8{Zb9a}L zl5%%q4`r=+qum4Ekw=H^wq><-fHF>!NI`>xz;@;R{D{V zk%tZ)n({Cdw(4=Gb8lL6Q(bt6dGheASEXZVkG<5& z>s6p9$`$%`e7s|9cDOZV`})A~@o~JMW9iP&XH!$o*J`F^@wEpG2;#~GNv_kG?f62x z+K|hUrAloPEAs1)@$qFQno;pTQCUlR#u+b2PVC%VX-P@4f}gLS>Hq&Qfm$z092%U} z(_g)L^XAp7S1(??c>P+tKo7I$@`)N0vFmpQXIIym`KiHMn|ZAz;!J7t1GP7?TsD%1 zUE_0$%mm#t1E09Q!C55w#k+U!X2-g&^jB3rc_J$>pWJBq^!?wR1}$sTqdH~SgDoZE z?25V82a>bTnD^C$)3-+ip?RN+g~FVcmUbb}&~s&J?n>X29hi}y1rA4#(&Xe0@Hbk% z;-$;T$jHlo_c2lO?%z(-1dHR3&Fy=_l=uUWo7vjh`k1=8xtW{uCp6pT4!s+jTZ~W@ zNYWg3DRY%^(AUlH+8SY;zmM>bG+R#${6o1}tHdvUo+;`IpCKMch%{pJPKV5RuxPqnp*`CXSwd+@b8@U@!9XjBaW_Vee@U%q^~E8oP# z*f=>Zu5rjESn2rX+$i?j_cLsJN;+lOSnwPh$#_)(jdpvf1obTA{Ncqa%$Cr3g4o9w zrxAAd?q7L%%im(Fe-7N-{H&p&ebkjkl{|fZa#*9CbdbmdPPcRXTwO;KZCW3A zJXndJE3`asY%@16@AXwx6B~$ujhMa3Pxmw8)YDZ|R2<(up4z-Jxj9zM=*94K*dU8> zm>IRr=8;y`B!ltxm$+x=A##Lq)K28cDBV9{lZE$7}{wHw5cHiaclv{OX=}`mw#91xuQhUbx zs{K|525Q`TH8b}R{$18s*3*cv4!hZ>fg3L58yo5A{VjS*c&~?CsLBZ?+14IjHlpV8 zA$+&sKjsA+wu>2-xmcN*Jut|%%Uzw{OuMVZ-!|^fEAKr&GkTtU1FTVgL*ZYl0$O6R z`6kuNeb*;KLqpTsZR=>1D*;yeYuBy~=T+$I?cKJe%MHVFVQ!4W9eSobo@k345EV77 z^g2vsh-?#RKK?b)*@Z18@DFnvdmnG%15y;RS* z`?YJ=&Qg9n6Ahg%gEzZ}8}@?!-LHcK{p$R;-p zb#`|4`K>I)#l>j|4${L0#tR)Py2iR!_aY|j*egPv5Z_$d(o1r zBFeepRvkVDndZHdho7I{wQCnJa03TXa|-`YNJz-o*x3I4`!PKa&Z=U=Rj}64PM;Uv z#<~V|AadWCO`A4(_fL?ya{eN@M+|7x#FUhk`h&jbM566`IG-FX(EHap5v46lJ6Yc6 z$A|h5#-6UOCm-R&y)RllG(6mo_PeTpeUGmCy1p&#lDisgOM7y4b9%;kcz94T))50& z$e3oo*ZA?vmoGz2vH6B&64KI_^kZz})rD)1TE!!KKN7ucVKLa2cJjYhSr)0IJ?hNT zwqt~{m8+{H8w*R&36bbK4TH#?k!R$C>&=reLqF-5OFtb(+C#yas9ssd4%!fSoX*Y3 zx-nh5(zyTKJ9|m4O5=RveC&V*Tl|eqttl9mH1s2IZD;~Y)n^)XS-g!uTkZ{L1) zmD#y-CtkxD`E)sZYHDh%D?dLsx5KX3UTQy8J;v%tSW#J-=gP7V`E;yLwMa^)^A{(b3VfLYDe5 zrt?DS8tpSaSikE2iT&xDHg8^BLM~noz$WL7&=A}Z`1ISiZ^f6pvOBV^^f6^@hgkPS ztD_X34CiqplhV)Ccnw~02?>|cSLp`1G3x1}P3)}FO@(*rA9nuu+FQ|WU(!EOYLM$& zeUR{1c!1^Dlil$-_hM;GObmPQZ>A%08Q6Wh8nU!ShFr=}oUdFdH8_BQ*I{6VOH>sh zj`g?dP}8xeEG#VEJL%D8SnF8U+zcDk7Ktt^D|0Np?&|v5u9!kh?n4Rqd8DZjMI$yQ zCeNs%@r~G53mQYwVC!Sj?*JEEI`2jbejJpT+M7eWyY+1TFMq1|AiN z?x(T>g#kN^x*}=Jeg94&Xr@@N>%mIqwzi``X2(#22L}hqFW8qJHS8HS2w1iKpz)XS z@nF?zRDJp!;-_l+rdyYb-(H`nl$Mdnj$%)=?zWfW3@Ppzzf!gEX?bZ8i8)F0Br)&~ z$)1lrSZRIW)1NU(&rI9fw+2!P8!5~I zMaPBHi@*C+8|Pn>Nixnat*)N%OeShPfkj(N&kAvb)T^94Ny1=3LCU2Np65d@#RlUe zBY$?-;bl_$Cl;#?5>8^aO#aik2Dy2E{q^p#^PZH}4K06~(&&c|rx*MFoS(`}@%>z$ zH|$APgG2=bV&I9Eda&xRWkR|rLJGaRe$7qnzHa;Pi2QYd#{pA&#($7wFH@O`;d@#l zRMrfwEN^)ek?6v1dp2ZCY%~qG8Gitygq+#;{ z55x_=4p7Ek3SY*1f*3HAV8xr7Jn?Y0w+Hr(&&a}p+6I|@oL;D}uXic?LJuQq%KpHw z_tw#n@_|qP`A3OHZbAR<-o~VuLdUKR7@*wU9y1N@1=ycSdQ~%Aznc)gCdW zsz`Em7~o~F^EETCdU(7cL#Sozd14?@oVCt5QBU+cs{7KHOA7lr);~D=oW@$KpBY8A zA3ls98+OSt&?hHBKcj(d@~mVWlaQ+mpz4Vn<00!v4I1#K;rG3cr5o!2WiDR4h;7LK z0dTKo0h>JWP=TI3&=MlZ(AYRnHz&qwUCWL-nt6dDEP~<$1Ryxk4G+ds-1ozq7>Ql0@9Mx!3*!PhMCT<>cg4O(^&+|3v=6 zb8iY0PL7sE-RbG+K$FIgoiK?)>6GF76N?68nwird8zT{rw^d7fwh{C(R?Nj7p77Sz z*8IFY&h@Kn%eJ$wnO^ugGGddiCCsrNm7f^kCcozxog38^n+wfUhuvYG@C_|;SAl?m zSMiYA=_k$2r~Buy%#v6X?&rhP?d|O(fG{#jN=`<{m0`P{@736>Z0jLbi_4eC`YNw8 z6PJ?kvKh9y4n4_ zz`8$1(!zChbzRB~v6Ba`s|z<3ayLxLwmK*JR#z7l4VLE-H9N=c(EEap|(j{bC zpdrfqRf91-QR|-X0H*dmXtCxd`X}5gDvev`e3<+Xoy08L;!{f3jh0&eybp?7HhWJuOQRa1 zV@zl!;egX88z%pv=aS8Djsa$5wnwP`BYB!>ImW!;abe+y@~_Lw%WZ8DL`|FphE>k6 z6=V-qPuCLScs1%Oz}{xCVX~BCePDESH1fWx06*cxxdU?%;n0Hx;q{$=|L?yVbZ#lu z6T*9I`tPXOj~aewUS%WiPdp zh4QFVU6{#OnLpxwa&mHfJeCyS6M``>GT%5>pqH4OjMr!WDS+bQ<>htx@_x>cbr=Kl z{nuIb9YI?*-8z;23capKH2c6lz??Rl89>ZYS9EPNYmgl%*oc3z1RfuC^ z_m*!9Rax!fP;2_Pw#e+_5^|Y(`nNCLMTl{-SlFTiO7sm@dZ;IAT)toT9vq%t zP)VCw7)~keabWUq*T6^m;_Ey;%XD+F_Dp?34bCryTx60Ec-%X7oKruX&`fsMPBPe3 znI=*Rl^#Ly;z>vpK6)f6BZD>{&pm+W+6kwBxnW*F2bh$R5vc)5lZN*$ zcg<=awRdnh%(W4ZRl{QqGHk=b!_S;PjZio%B!IU6bdf8qI2&fJT?K)C-_3+l5sZ}f@{mIirOsGDxB##6DL*oF;*Q`lwzoBdBbucGEetLw+? ztx?lbcW`v%;pX;89xivR=*2d2ctq3)|52NRpVVjzo0Mii+n#K>#KK7cm%kMfW zFW=WsuBro8^}G;Qs2HQ`n|+mj3V#qbh{Q=pPVX-PiBnWu?B?V&u;|OcF>>VK*vw#w z5Zm)wX0Kz{m|J?(1ek+G5}mNhVS}gx#4`ZIw*_yJXq00AYiK18 z96T6ud)O0cbSpvMNKRq|s5DMayghz&`n>gnzkOZD4L=waaYfbu66nTZqR zCT5S*_=N6i!-fr6+UowhO?$p0D{F})G_PT1x>7?X@7;(Z)=DQja3Weu)OWCR3916z z4O<3`4zbo1il)EDngLT!EQYG2b&ahfSjEp1rXxes3wFg`LocEM1Vh8Z*4$LYf>P0X zgO}9JnMK*u&Dl%P{=qA=onM7^-_sL~X5LvLrL=QQLBX9~*oy4I2>!mZtC{^1=;kZ( z_7k)yY%Gu-3?u@o$^=vqdV?~7u+PC*1#^nOJ*yu8eqmpb&ZRNV(>ZGgZ@aB}ftLR_3P z_euZVpB8-u*t5FEkhuE$`Y@`x0qEA%H){dR{C;HmaqZd#?8?kqdi()RLgE`JLM48) z_>9{#XdV1_TVNh}drAOB(zWz?RRtp%u!|R z=<3X3lpz0hK0v}suVThLTyb>7P&+Wm^8)OkWZeNGfj37-!NWvpF($0P7>w_Rl!W7 z*{`jw?X9?m@q#d2o9y^FL-5z9&zy->PbbG5#ga5MG+fN>EbaFlqigyxw%oO5HvBll z_HBugC}%z^KrNHzfd*&vP-TW<(dwXF+H79aP$+k&V&t2hGE`PT?xs~B8~x8Wm5ghP zbd0-)tm1!VBl!azZB0c6es)4I68o;$AeXVjdP-K)hwOWJcI`q>BhEU=Xz|vAHxD9e zoSs`3As~>jP}?{|x=Rk!2oKX+P`F>bP|wmv;U-v_@bTOr;y!+~EHeZTg4E;mLq8_E zb@+N^-vtw&Z6WnYRAc4I!|RAAN3l)lSgL}80y^_-~+7S1kKGqd>`-0utigibi(9+4KVD<`3&1n(+l8Gb9J`{E0HkI z{+#-@5al|Wl(cm9&u>kYm2zEfBoRM&ZOqaZq-xW!ln9)F;oURYJ-i|hbIfL@y#L|lp|(Ykd}89FT+ zNpMfDGR(x4Sj_gAXw#i~^Lq0saHJrvH?O%#E@fw6omqo3UK>CMM9q88K9V%3>9Hc8 z!N5PCz6~{@9Y{(@C?Yx48vRSupw9CCiICe`*=;ste~zq-XM_k7lby(aeboE$@#?@| ziTh%#_C_|U3KUjWE&;5d{5h6lqB#!`lbrf75LCos6OxiJ@G0`whaNDsPTLi~t-G~( zqQBZCzl*Un7ZIX;56=okX=d(Ug?Iq|^}j02y$NVkC9Q2@j#tj>^#q!SzV+m&;91d( zV)vOB+*d~h&JU;^`1tXU+p^yCG%D+lDo}AgWYIRmTps|6A=?Z?LkJ$si54+d=~`Ki zr5AbL^!3Tp=}zRX`we|{sj@IO`$5p`@1F)*N(k2+vDVim`LrB~fpW*Rw*UQMWM(aofQ*Rmx~QBstT&7Ku-xU1z;T`EBNz*7w_uA3M~VtAoY@( z3_k9jL(ebr7r6`XI(fpS?A@zZS^&D15{Pv4EYM(p<^>pv(f{Nae_kM#=>>04W3f%h zdmQTl?O1^WBwj=0K>Pt3v)-y>40wDM+Iv@5R||_Y(4V_@;g!@s4c zBwCAkpaJs(s{i%atV&uzSsD68W_)$y-wsEUG#N=5F|4E{yqEAb6p1#_)3cG}>i3yT z!cS;`Tq*#1fGQyHI5{~%@@c7q)U2AgoD01W(bCx&bNex>^rQ;8N)hVmC_$u7u`g1M zX_6hBzmp$&3`{u}xh# zx7ap5Nwa}=7hP~gMGx63^idF4pFsEnv5Sj~D<~)&IU-CLCy}BaHK^e4zf+?=yH|ws z$hjDm{$ishy?|V#FZuc|KMUOnjsaR31XSY5nmQVP#PgPx7EIn)Iq2ek0KO+Y@5>FeBk0M01tx>0 zNm0j?1f3O<6c=|!x-l>K6HB>|A9*z@`!NYKRMTtf1pK zLx5OJ%3PF*#6JemNuxzt@J7~@CY!Pgm(~-Eo*JDl5F^qWI1A>1k6>THIWM9Efe^q! zc(`GW_60W#qQ3*U7~gdg#1&;e zHqe{MARv%ZS`o-7elEp-u8!ag{7vIx0MWWFXq~8D)gQ8cHgSwnA z#2K=eJs1JtQkK|keV~b*xDsea4I6|qj(|fdxc&V5cXzS}^RUG$ZUYaGe+vr>43^rA z$~lVpTpeG+UyS z!R4kxfD)+mXygFVNxk(T2)ieems8IQ{oC4lMni)oC}Coe5i}KfP~NNtCMTUPT|&Qb z5+#TZuGFC?$V@2xUnV)z_zY7$+gyu+`wl$-6UgSky&#=>9Wf#q%`O`4f9d8xWxxud zp3-I@7^1U*dd4g3;k2@hPaF0$uIQDA<_=1XyQieMGs8A$4fcyzELhTLb+94pf)YBt z+}%5=9zW-u$>!68mq@#8rnKUa&PqVP!Vok9=)x(KW1#PIPMk8{y%@$ z%>nX;H3R=hz|vcBA*UnHq&lw{%oEDD@LsBJoQ5@W;QajKUPng!RKGy1kqi~OB0asw zZeyLFKiko{q3Q)`K#V9hIFl?}c6GP~Z2ga^23|jqfJpdgjPu4o#4EJS`Ls^Mc;PAo zq8U7DLNh)9l8|F*v{u!`#TY9r`#co%+~Tovv^cX~Mo_-D67yR!h?;%`TE21RkG7M% zXrnKPY|AY+eLi1>B#GTBzw&e1Z>h^~DSzch{?Th+LBc?AZk_(9gI|ogexCi&o_Pk4 zY@$4`^KIc@V7z-uOy}ugk&TvyT|Pf%Mq64mp{u!9FcS(-@rlkcR={B(sKBfHOxCW& zc8O4>2JI$W;yJHMFv+ly(;H6X@!;PgDc+-qEdM+|MaXhzGig6_+oxnh2M&AS{v>3p@e3xATSD$p5Nok zpclPE>dNiJw3v|3SG@Tm z8?{A1+uW=*C1uhS*XGIKft?#5ZDwP?d`vybK&)#R7}?b5gG8l-A1x% z{zFLr^7m3QX3|cE1KuSP{d3UFG}=W;fTT(Dj?yxK?zXhD+`n=i$YElUA$lDIOUz3$ zkCqZ;j~_oSDG{WqS4~JKG-FYJe!fuc_3dNuSh$T##YNkl^*1{HxJh`Kn5CL9oU~U=L;q(#mW{a_FW)p$wMh+h^KXj!CRJBcp4!_E!x`07>gcDKlnVh>fx>G+IdFUhryHQlSyceo{~e`R?N z5#wxhK@&a8d&GaNW@cta2IYh>c%Y^su$V)!CL-yiWeBW2MuPVP|6oH3R-&93ot&J^ z$*=lBE*rk+`NebjUD)I)nwdZa7=y|h(`3~@cPQ>IQL`EH`f}nRJwo*(nkN){#7AT! z`qWz5C@FtV11m3%U-2VdVAa@cy^a?&lP3_juzNLI?}@m58^*L+8uX?Thb?75HCWat ztzW{#q#Qo76@8MoydUt}PH9JtGiP9TL4@9icmwSF4oRW?j6&trVa?K>S=lpv>szP2 z`>&BEI23ogViBr*-q_bKUl99UZrCPbfKSE#Fukyxxxe<|7SQMtg@$n0t=L>d6mh{{GX zYO9b_QZ5=A8k(3?gYE_0D!{&lH4@DnNDp#hs(*Ck%P-BeBX1)2d5)p3!Cu1MFy~W8 zyVv-Km43j32T`vK_C+@S`WuMDq{6e6U_&3XN?rJBsv#oK+tbcYgnQG5pdjhLD=U3K z{k6(3e(7Hx@8^;A;F|~-D~Hh5>o|vX{N~e7y9vx@*L~qGK8tjsFA3s*RL;za=JOWd3nZg7unhh$7wJmP1YL# z)98S~1H$~YGC%CMJd6c6oN(GR6WK|U3wmLUl`TY*vx1TEYXz8nSoD?UQ9v1;9H{;J zHIMMWElkkh_Ck-b{B>g4(Zxj~wyC=WZZpSX17e3D_6-1Nvvm>h48vK84N6x^YXiH%0K0HjdQ}=1b#oBI3Bp86fPBV>s}gM zt1#-HL!sQEmcwG%9r?1E(fq{W=3!RD;gMJ3G{ltHeZRtxOBI~;^1){7b-Y&5yR_uPEVr1%fb%9b08a>Cn+mN7-{`gc?;-5llN``Ov! z?iahs)m;^z7ycy#L2qhtiMv7V-apRVwF{;oYX#4PM7ESr z`Xf8rj7lLc+&zXt!GNvZ%;WvN`;u>U%D}tdt;#@1O-v}DOIw&8&eaVOyKQhGt&O^K zXNB|Nng(ahOt{JduGVy06&rxQxD^z18$OCHgrVGBVnF?u17d>_8W5m6ONlLsn~`47 zz`_A&tgnAWUS3y6r^~IJ4!)?{URS5%y%QLGMK+d}jXF9B%^7?wNt*j&o3gUi38#7% zyrUMw>_tj_vOJg|q_@PKpaFjk2g@kgX|tA}jciI0~T-|oJBrz@>osfF| z24^5ZR5M;4p6?6Z_XM^it_t9r_m;1t!IaxD*e03VK57$>b+P*IJ)gj^^ZIpEuj4+t zG$_+z1DGD(_Vu+`O5pKge5c!C%{HU<^z;Blmb;C`G+Rds1P^etQEEqBC7uP;n)0!Y zxXNT_+x_pL>l&Ofi{=F|9ics$^;*}m@pSvQg`bGyzCL65l>81mSMeP|4}{dy+5qq{ zE^H;iO3;ZW$)2P3m^1gjJN#SV#s=&FsJ4-`)E6WkOL#E*=lnLj@ZiA%2zhI7DxTw5 z*Ru6=J9sEE>|qS8YO{H{I& z=jm3$aIb9Z=8YR+vcp)BjY>}PHu0S{k_bxhxD1;lxQT}wG`nWOlA~EbxmsFU@|Fjc zxvphx^6;t~HDJXgxd6wp;=$OJ(UYH+a2sS&cDAej zAMK+nb1TaxMcsyQt9IK{hKUB}8^`>I7XX`zx-~Emxi;9fcg0g$rRC*$SSU*81gMf} zIoDd;fRcd(*pHSffOn&{rgBDge|2(SVg5a?!_zSE7fV*XeL9 zU0m{o+D|Rg6J@+CwnTVdaWX9bQ9^yhhFt~+)F)l%CfPsdmR z$`|O}R=pLEkez?PFBf($sG3Dh(RE^CJ7_s z_za*+9Uaf)E;pll0^toOlT;6*Gg)JiYKlw5dF=-73@808ijp?_MDXsx`p0K#yuc zxoKV6qkK;G$Pu-4Eea9Kg{k=xbnBMFX#D;AK(D`Efg#t{ddc0*?Qm>U$C#T@ z*Xa7QI*^-mMYdgug}&e+3mjvbN>K#ODBH^G?#!K;Ey7f@w;q#Gf%@vRFciC)M;5k! z&X7RUmsm`&CkST9evrvu+7(NlPD6-$hLC~MGz8It{9P(N%;K)_9T!jzcE#E{2V$Fo z%-9G+6=tTIp}i~tjh2{xjFFOyVZtE0|GJbv=iccnQ&8ip3x z{|2*{Y6S<{)m6RlDZyL*3U!T9 zYyd(`7zumFlo1kYuPP;a_24A-bjN zYES?pF9zu-M&0Az3kr~oPSVa|Y$JHn9eSV$F8mr3$n_=W-=Z58MLXPeFQLXdrj;&3Jds1cQpHLB4P z_yKlG;52(FWOx}Y>E5T)Fy2GDg;#SC-Y-x!jz!%pYjV5oArj{1<|4JHrR}K<@mC#% zGY+Os6fYX9+C9{IJ{HO<{CUp)2vrRYjS&(HUCiZVskK)Mz#;U8vg_E=Ara9xGDMUz?IPRxS$u!qM5ez#tbMH6*Ml zBg)Oe&*}o~;8y(nOmsQ`ED5J6aRg7Tc>#PNpmXuF-g37P)0dygQ&NT~B6!ixPTfl5 z3mkz1`S>z%tRWapGpbZVsJII4ED|fj8UYiWNHi8n9=l1Hp40*sMovx+5{9_AB8{{u zLWDhdP{D86`^uFw=Qw~?mnG_G9Li|a?TWw2UMcNCgf&|7lM%MD4gjZHjM+cr45FGt zl=Bqy7)G(+r>|w`T(3TTvd+@h(usy0zZ}56uaC##U&m7L*wy`4in=L^d{nkubtFCN z;_|^&W*s568WmIPZ#_|*d@(nFIyTFmxC*OI;zJXYd~Femf?j4j@-aderw_cn6U7DZ zJS2DH2PeAHIkXU)ZedZi*zkp`i@Lej2f}FHGcHKwHtK3J0#<`0k87DVRPj~jpSifWkYFLKV$QU(u#A?ns9St@9<~ne5fpU`%ul%*wGK~%SHXLrW#kDzc~1bidd)xY zoe*)b>1ky>Dl1bXoFdjyi0x1=06{xJlX7jkwQkMH_D3pd5v{|KjUR#cV9j_tE>(L* zCs+6IY51LBUn$WOMei`=vW4|_uOsTiYqzDP#W@U=2()G<{>&peT!|l9Fva_u8jUJI z$Y_>$c#nud_GE^fDgZTaoEyYe|peJL`& zn9VV2OvB&~9-b-yYCHve6oOJfjj~EBrvo`3h!RZ{#MTU3il4L^PHT%NG{fh~#=^!L z^-yTYC2o+;FA}XDpMhF*42dgn$h`vd3^hq`-@cP7Dga2-M9BL^Mbpsb;k!KD&LaOC zat+FwOBu9EmNjr&|NHO1U@4Tcw6ohsu@1-3-aL)Rxf6(K3b4N7z~imDT0juKM?K>% zWlu^==Wqak6#0{q;J-s0$Wu8mp^{|#zqoQYn}~0jtY27i@IUb4ps^< zJ?4>Iswe7LS^;%>dcnQID8I{!g!pHX0F~>gP=pKg(9O$VpPw}2Vc*EK{MqQ~EZDRcFlp6p&%OB`r;%KPin+3N3l*31s--S@)K&U<3LfH++ z^1uJy4GROf3s8hF6vqw@8b5&#h9U=?e&)=X%@x=}&_huefDqW|VKD6$>xrWKc5|Ek z`0+Pl;J6u9d2NGp0N(hg{Lp^_e^GgOVg!Osj{&yAuVHLV8t);`rE5i4#SNTj zcQZQ2p~!~Kz<>)R(Dw{b9H48G5HGTI_`e3uNJod+e?tHkpF4n0Pc#N?4$19oubx*H z<-AJgzAGNU)D1-gOd;eNvaW=MS?YJXC8bKk@#a?9W;4IE3`^lNYOTK$cf&NM5DeCx zhhP%Hyc14}sHxj;@v-oP@8b+Hs`mBq@-nz55Wv)iHSMUYs{`Hm#B&-3eo3y40mk?g zh|K^c5OTr!jJm4d`)68Kv46@4`N-E7ofdpjk&#>f=atbSz=sKJM~WLGBSIwjhXbGC z2ZyNopH@(8pu-WE+HhA4C)A`8j)Yy6Ng@H`F?v{0w>_wTcnc#Nv1{3|{a4pICn9K3 zRnVwx4W?kkRd7HQ8vxDW?9!!6U=Z16lJmykF`(Ve%{_n}=xj*mM$AKMV-3jPpVA7z zoje?h%zZT+^GdlVaD=P`&d|BS6_($C!pPMHy!vsfYu36*KT8`2cA(mR11v|Qr;?`l z@+5Zyf+D0IIeWDt&20;-D!A;fG42N2c-<^*7NSKCdyAhL&Gi8n1ty`xE;=<_@-(~#%8_60x{yvQ{;eDp-0P0J!+4FFbhORw~ciHYUq<>7RIevF&ZzL7mfUE7q!<1`9I zw?;s3N@(V8SQl{YWM{Lp-X{A^ z9S&uPU+99xVjBi`j~pd?Q*DmD$mY`vnwb#N0h;I@#0RiHL45xW6COiHHeByadE)6aT+S^k{AleoZg?P zIsE*&sw~g7GDE=knWQ#YVQ`k>!-o%Fzd9D^3D9|Qt{Lu@hQYX$lq*3+y<@IB7@^=#EU%k8Ue+QG~LN7qMnMxuZI8|`sZoH=yHJvgH0YgSk4*V&Z&G+q_ z^-3~j``7v^Yu;gd@l4le!AK-qRV9U>g-QO$u#DZ^@+WApR$$VTp6 zyTqTpbCg!>4}J)bv+_Bd7zWHE^D-+rUw)uTra=HGvb}S>#vg&)1nf(>c~&eo(g;e^ zfT5iv7vcv!TuSR!R%Vi9DC$mB6_8)}7zvEe;2*@WPlmU(PTv(rhEf%H*v5!wnB4CD zH-uCIENVk8p!V4Q-$QKW$18(=BL;qgw`$p&JPc7}aBBXae||#^MIuK8fn9*K3G9n^ zDK_Y~@3byLfwh;qJb~srwVI-MyWX6v$Pdoy85;`{^jPeGtHFbhVVOV^4Ocj9g#ooV zq&1pnkgMV(1`Lc$6gCKxE4l&ri}ge?r!%p9EU<1*%z+r{GcRBx5>H{HCYs$nk_64N z`tQFdN7~ZRdxF|fyT|fFEVgO<>sN$U6-?gP{N481+3Gj}74#rmWDEY}K@&-L(ZdGu zim({tL7X8&PT+q%L!r;*zDUnvvFmVN36-|xjeQBM z93Z~Y=e0C9AK~8gjH<{xT5g=vfh`Y2<@bvLBQ@tX8)}fEQy7C_D>KB^1&F140=MdR ztFbHg;{0 z_>R$7?mUE&h$I3dG=#3>=Qsj3{MiXJ4G#Dsl<`Igxj&V&{J`O}XArmH zyMUsg1Z966vF0?QSsIxNXBdI*NCxmXz+DYN^4y$%BY_RpYoyrUWt3ctyS(_LiZ;nlQA+sHZVN5u@ z7AA&YM2GN6#i1m1uHpmx0fJ>RAysj}yta7nIFFfhW&T##HYEIDF4)^2|Qat=j ztb4dZe=IFq7j@eeyBh5ysBFoEX)4v?o&c=tketDslWhn~nc-K`lLC#Xww9@OFOFVl zo;h>K2${LV4qbnM{D*R!R^nsBaf`{tr2sxQf;H?IK|70fX2=D{j?f#}NUFcQ7OIkF zoImm9%M-+gl~v$?%eiuNf?&82j$WWO8y98eCX_U%q5h>_^=rttOt|EPZ?`^+eeXwF*VIhbV(kg=Vd2dUs0p%*%re zhVa=Ne2q-fS63NK8>kmnZ5kEO?bDk#_rc$@_G&*b8VG+J_jv-I?}usCr~;1^J;+R8$#K??7!^0|)2)UkByOhn#Tci!ym ztgDO<7G~6yMeN||c1wME85sj(;}|PX{0xf8%m#~pK|ZuWNyV{Cw@QQD7lj(c%WJ4> z$G{5#Ebn7q_bX}nLJPxtA+W#&!RIz6Q@hv7wUK0V&>`XK16dwy56aS0D)6Cu1rB4< zxk9lc+$#<=NiZGj$8NdF$8wwxZQ@O|jhk-Okj2yi|n6haIw*QZ-@K2#T6d#BPI6{uDo{?@S z7yL79u|>de0uBSXoa2D@@M8>Tm(jztkN%aPf2l~H*`G0awU!ddfoBw!BqLN+?(ET4c&N-11;DQijR;>02#y2ca;ty8p1GBv@vBxMp*8%M8f@Rfv}+n{EwaD&RJQOge=m45+@O zBNmH1H8tC10(6;_mLQ)n9e*9EMt;jH$!2@0oNO;xd%eA}V@@(N z9rFWW2l@vfoz5PNcJ}4T_>9IvTzgRh*B<&+Wb|y2+K%&M=&_Kzpcs=-QtjnQ7|rda zU=Rnbe!tneID|+&NM$-!jedv_;>iQ@=KG9JRHo%>je9J_GXP3FWqAVRaYl%Z z0>_b9?A0>v24|8yn4#_=Mq)92(5(G_eh4`TkGGN7c12#a_~R23cuA-{t~k|aoIh5s z=>*?z=kiUJwD(gUSZk0BBiqk$Kp*|$eqAa7<*G|Ex7TrJkRtIC_$hS`Zz8~628sjT zH?UK342r|2&9M|08A-%>!*2%=jldC$g(f-h0=0fZOGhWh>Q>!*T*_kLe9 z4~QS13_ejXa<~7=DV+{7lHTIzJ|By2&fy5v+Yg^pNxU~Fpa!N)2^TFu71)BNi1D^3 zunj8D3b`W3f*dLBIsMWEIRa|t+jsBKc~d603*EV=nTdJ94H=%(8Q9a^_9QS(CY$hE zw^93Ld2lixL&)r6_U{7*@d_HGXI(~%;o{-~$~D!5weD=iFO3U*Nm?79fAuk)4(~3| zQ{X_l?KvsPHd|&GWMhIx28t8jq>cuvC9{1uF|rZSehD0x3SdkcASupJQhfffHGpk_ zGX%IiP9sB4luXlpr$DX$oG%b39}DsnIDa%lz`qbA4}&pF zU5h>?+3c=zS{q<3v>|;_&H#D4(jMsED^t8H1y7#fWE;!CEu2MbFk;AV&kHqP9wg6d zqapK=PiYv85D3ON=nj(Kd&)D;VR}>=gFXE5Cvq}wSb{0*dPQ%HRUF}aA9>A25>!06 zrDB6^6h){kRRiV7jG!MqWbZs=tifhLt`R22_J>~`g{0Yvl7ezTE;J&p15v}Yix1S5~g$?YZPUt*$oeisnt z3I+0CeN&MvqA2H(Ef5Dld0T4EU`(95EG;d?onJ_a;4MeCm-hHn!m$f*j#@>?3ngQ! zl+NKO94ZD}7>L4?p(+-aE)C;K7xZ8>R=fzFDxac2r{kbG)>AhJ7@_VxI!Ih_0atge zDHJ+(!uu?0`dSEYxG`eE8w9q!{lDYx(|{-20gMfDp-{sv1fvS`eAdT2tUHQaN^8UP zlh$FRuGG1INeT%b-h}2Cg=qcpZrtb+ZWi{8V=QZHL7wpc(Mc6eaCcR;*DQG+3@i#| z^4d{3IUiU!i@M>F0P)7hPtrF}2dBV#LafKp9}=4W?|ASzLou83csK68btbS6ehJdU zf-~4;wVtntE@xa`W(Yw^pwV9HD(cL2+_mE3QmC6#>9=CcMyW&}djf3+FW?%fm*dD0 zM|cBB7DW>rF2UHqRB%**iBd^>Ahe#z81&h0>2PujW577U_Hth&% zLuDo8Ddg9JTfTbrD!SZYz>OuX{4V&8B#aik`^a@T#ufSujUM8T5r){Rec)H_2`E>f zo6H+yAXR4n>V#v&~=&qGxYv zYimLO(x_m5$!gBtHRMDGxx79wz>NB92mCiCC@I+EkxH;^RC3Ks&|3S=RrG^@g8Iok zp9&Q=S{IyAL=5GN`xmG*%3mOPCU(l}csK+LltHaGt>Jl%^^*9=e(huKRs zkT`?`sjs5qI#Qjxdj?cV@3XM5-+5&(wR!VqaGMmxNe@|QzhvsgMOowSM{)7NAdbGn z=X)69{rpi>&dndu%aC?M=%G;6pw^7Y>FB_hhYK5UMMqIV!8rzpWb-S(BU&$Y+vVXfp3SL`#lSBM`T$ zd*I8wxpp&Yk<7{xj%`wYdA(2+7uMl;|IBDR)jcJ90m6akXE?mgc~hW z@AhBLc83oLbgYN0*qw*u((Q#DVZ=~9Z~z#P2GkGfHS?8C#`S3I%Hk2K@2dU$;PxXk zJhFA8r3>zG0M}LH4^V~7WGR7Cv+l<)c?y)Gp6I8!MNqL!HQ`XMeri7y`Xx~AWnV|4 z{D%g)jt&m+&JRy}GE;xYg=G~LN8$1re|tE<*BM^(gabU`4dw+Thy`QEPs~SP*Ymbo zNLU36Mg6E1BiAu`;XFhyNRD6sy(Fb4h+tT(ahVYXeRF`N;A z)*wHI>0)dwBnZC_h8LHG#Wuk@%W!C*lOYuDBMS|p&p^Uj{;<6o)k1J27BvPW7l141 z7o2lMYGl>MfsF2MOc+wEwRKSH!M}7b^RdBTg5!aMQ;^1$&tVm5NRAC`D9`oq0^ycS z1%+08C^=qq9`2p9ms)4~5?0dM+8d3QI6Mbh0G+v|1dgu1JY^o+Ke0LRIL=_SKO!ZM z$(vJnzo1^naE8q3SehUXB3-c>$_4T^+Rs?^z0CBr9Lk`=umo$tZK~<$1XMwmMQ{Kw z0T_@2-~%n7_rRg5Dgfe@%<55j*n#MYAv-YBc`-sP{59xi$HyJ`VP?B4o)?lD3!xau z&wc#u)RcJZvmb4lM-Y4Au!mH*KYBe=;yzrh z8075k{){^9Rrye;P%Oasm1Jl+JTs6Ff>lW_oA(%^NZe4Y3+gK@d|2d>SUAHFkSn<9 z0sS4tsl`<$35^|M{>}&7at6F3bO(}4YU~&HMO-CiMuiH7YbwZY-JZIJsiq(15ML^& zpD7QR4bY`aA3lsS0rF`i9W;<;CXzGG06^P=D*eu03dz|2s!Ere@;R6^ei6olPIMNp zyW$zPxLN{KTXr_w^rU9E%!#$VRnRjIr_AG~;b~H&uz+teP6M~!&44T)buHJ;Aw87C z;bv4N+~?qGH0}cmNLNeUEHM{}6$gjhDJ6YGj z$}+M;M`vfKidZap0tn?0HoOgi0ivXHsK(QZs#4VLO6NwmNQ(UjL5yB;GJ3sN4QE@A z(&=YOuP2#PmL-q@>u|1D-fLzAfDLpq366oUv08LbNEK1AhkKdEvqZG@wj|Ld9sk zTyO|1B4W$4`{a)NB>`-#gA9+4KrtlEh$(fLU6FGfST$rA1!?J6RRP?%sCW<^Gg34X z2icI@+rJ)j!&ePFj35Epp=VEwuZVK)8Ie=F2g?^o5vc?_a zcgWyuGkUmn^359$L>d}AIFkKCz;y17REun6q;8%;dP~Q)@CJ(lJ^2*aJs6rrD>RE` zX0Vs8GB@Ece6A|Wi5x(JB|X?rOiWCSjBq@3kN<{KF+)Iq=&Bf8w>36~^&zkxl`sT3 z_Lf8Eg<*wsFwS2k*SQ8>Kkq6W1R);>Ucv%vK@JG}zmXC8A&Qe3gWbS5AQ4eXNgr5n zY6HqHE<0p65i!&T?DQ1aZtbPk-mFFTAWhZSlz0gQH01AOTv~EeRu)GyZ~>jCOj4(0 zGr;8I`zWbyMr;%qht=*0-GO6}{2m|y+!fJg1AvV)VNf6Ah=CP&82pak6Nqg(j9X~P zt6@oSg`1d~tw|6Fvg9@3Pl7b*g~O_^8~A|#`8KX=K#4(a5m4hF+57B%y*b1-EHjSb z;ffc#!_4Tck*2mvCEx}}OKa;-sBLHpytgZcqr^eZ1mO$+AmVM{L!{Nh$cU;=2M$UP z4q6y>VQxsQVfGo43Md_9FXW4q*0FLBUQ9K-Kx+zHLRD}C?;0dsTqSjkuLgjIJQ#=r zhyA!$1p@=h3}^vVw2$IiI84HwhrgP-Shy4`daa8VK7PcF!8=Kv2ScLE_4~~YtA*1z z@5ftTBN=OONZ1Zi3M@rwlOAY6Yq z`GGtyvy03l<=6xYX!jE}FVVTf&?=mLPUeyO4~1ZSK?26nFsRBDr_qt}s?yR-p*zrG z;^RR;0a8mPSQNYgWW**eGbEEtY6#H49`F-XR$PMsfWbofOLxEGK}LIlm0@(fR#xzd zvel&HSR*=n+y{wE2MhGH8D?l6xOkD7aVhry)84uN<(RJh|0*$|8M};|)!4+)klaId zGi~f@*@cYTrm2KPl+-Fj>6SGa3De9<3Y8i~QPe1fx_f#?(=-%KljhdOU=>M?qR{61 zKCkBazMg;J`E7ZvHLqnBSJ&n|kMlS_$LI4o6b4f%Q@hk8kpkF0o&(V&&}!7&DW8NA z2}KVjm4nwU<3ht{I*u!O)vTx6OOL`^w!Br55yF(1bW&?b3j=-!sm?Mi6{km zSz5)C(oLc9=(0IW$pT2>c3r`c`Hx7S7A$)hQH+*a*PML&%+C zQ#uXp_tNpQVF+fKy$+};5AcQ5m%f&1Y&)_(R~T@R()|;?u1y#Sa{_h0THlR<0-P0f zacZT<#L#b_el~kHsx&z+DmERM<3d}`{$IQmS|ie$m^o4@EFDZn#trt)cBH2j7fDM~ z=Bg|j{~anq`5>uH|Kd9Q_gl9@3Lil*d$?2IN$+O9f+jUJAX}x#^;--mM@yssn02SA ztp*!MJP~23jHG^_LLmp~4Y&s#E{9iyz3GS4H(&oGIC8>*O)k*hqiHG(TIkjRtVw(e zO+=x$QAta_%T5TCuC55+2DXNgMhK)ABOP`VTOa&STC)guD6RI9zZD#^d1q<<(O?IH z9KfmYYOjrbSmC3GJFk%4Nj$MI8#$kYZ}dONZTH+~9y;kDC<$`Rx!;Vv5!%V~$ny4U_oq2uTo_9>}{ zPj)VpY~gx@P=>a9497m9P0&bQJu62FM6*?3$*H343L#^43jSq>57S*JX#sEwSYcH7 z>8%C!td|A9dd#4({`tvN9mfq}QYz>cjk>liBv9aFJf0&&`VumR(4ek&9mqz(GDiwEo1 zCJ(mp-ntcAGS2A*#$)t#$yaCkI$;x?_vW@2Qg)GD&WVgS;iI!dK0FfG^`=hGk_v^x zIHf#UuouFm)Nu!-R4lS);D8BR&%(mv%3A%sSpBU#fei*d1w1ENDj-elQiFubh$2@I zsE-%1rNv&E84?oWwq*+f+|SNbS67c$PdYq7o$@m@tEEdH+}t-)WOQMv4an^YZO|I1 zpJQSK64@L?SxI&dxf95zuDIT)?;*Y)@>=0)QM4<9}p0N*(+ zGH#|^yH6p-&W-126gWrJ@6Y+i$q z+>ideCP3)}aWetyY54)SWu~Y5?AfzWYR+o5NHbwz02d;f8%+>NH_LWM7QC;dG9j@t z{EAERg}((6mk*Kbt>y#+lx6i%P!(_7=&0IV!*}MKkw_h!)Ba(!n{<}wbLq z)4MsQ*oW6a9_QddfAx-AhMbue;x0>ffpq0ox7Prx(^`w?1p|U=4%p~E81nsj_xB6K zH*T4=EZSLn{k2#9`l#2bjnnP^Sa__@$KlZrdW{=@*zT9M-qmle7dXAR*N~PvWd00k ze@tEVP1mWjadw$K`+|FPT$-WHT|XyPIkp%iKd{Xhgn`3G!m?qtgG
GSu@OQ61P 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 diff --git a/targets/widget/Assets.xcassets/Contents.json b/targets/widget/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/targets/widget/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/targets/widget/Info.plist b/targets/widget/Info.plist deleted file mode 100644 index 5510804..0000000 --- a/targets/widget/Info.plist +++ /dev/null @@ -1,11 +0,0 @@ - - - - - NSExtension - - NSExtensionPointIdentifier - com.apple.widgetkit-extension - - - \ No newline at end of file diff --git a/targets/widget/WidgetControl.swift b/targets/widget/WidgetControl.swift deleted file mode 100644 index 4c6cb8c..0000000 --- a/targets/widget/WidgetControl.swift +++ /dev/null @@ -1,93 +0,0 @@ -import AppIntents -import SwiftUI -import WidgetKit - -// MARK: - Control Widget for Quick Wake Action - -struct widgetControl: ControlWidget { - static let kind: String = "wakecontrol" - - var body: some ControlWidgetConfiguration { - AppIntentControlConfiguration( - kind: Self.kind, - provider: WakeControlProvider() - ) { value in - ControlWidgetButton(action: WakeDeviceIntent(deviceId: value.deviceId, deviceName: value.deviceName)) { - Label(value.deviceName, systemImage: "bolt.fill") - } - } - .displayName("Wake Device") - .description("Quickly wake a device from Control Center.") - } -} - -// MARK: - Control Widget Value - -extension widgetControl { - struct Value { - var deviceId: String - var deviceName: String - } -} - -// MARK: - Control Widget Provider - -struct WakeControlProvider: AppIntentControlValueProvider { - func previewValue(configuration: WakeControlConfiguration) -> widgetControl.Value { - widgetControl.Value( - deviceId: configuration.device?.id ?? "", - deviceName: configuration.device?.name ?? "Select Device" - ) - } - - func currentValue(configuration: WakeControlConfiguration) async throws -> widgetControl.Value { - widgetControl.Value( - deviceId: configuration.device?.id ?? "", - deviceName: configuration.device?.name ?? "Select Device" - ) - } -} - -// MARK: - Control Widget Configuration Intent - -struct WakeControlConfiguration: ControlConfigurationIntent { - static let title: LocalizedStringResource = "Device to Wake" - - @Parameter(title: "Device") - var device: DeviceEntity? -} - -// MARK: - Wake Device Intent (Opens Deep Link) - -struct WakeDeviceIntent: AppIntent { - static var title: LocalizedStringResource = "Wake Device" - static var description = IntentDescription("Wakes the selected device using Wake-on-LAN.") - static var openAppWhenRun: Bool = true - static var isDiscoverable: Bool = true - - @Parameter(title: "Device ID") - var deviceId: String - - @Parameter(title: "Device Name") - var deviceName: String - -// static var parameterSummary: some ParameterSummary { -// Summary("Wake \(\.$deviceName)") -// } - - init() { - self.deviceId = "" - self.deviceName = "" - } - - init(deviceId: String, deviceName: String) { - self.deviceId = deviceId - self.deviceName = deviceName - } - - func perform() async throws -> some IntentResult & OpensIntent { - print("WakeDeviceIntent performed for device: \(deviceName) (id: \(deviceId))") - let url = URL(string: "remotewol-upsnap://actions/wake/\(deviceId)")! - return .result(opensIntent: OpenURLIntent(url)) - } -} diff --git a/targets/widget/expo-target.config.js b/targets/widget/expo-target.config.js deleted file mode 100644 index cfaf4d2..0000000 --- a/targets/widget/expo-target.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */ -module.exports = config => ({ - type: "widget", - icon: 'https://github.com/expo.png', - entitlements: { - "com.apple.security.application-groups": [ - "group.abunchofknowitalls.remotewol-upsnap" - ] - }, -}); \ No newline at end of file diff --git a/targets/widget/generated.entitlements b/targets/widget/generated.entitlements deleted file mode 100644 index c9bb277..0000000 --- a/targets/widget/generated.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.application-groups - - group.abunchofknowitalls.remotewol-upsnap - - - \ No newline at end of file diff --git a/targets/widget/index.swift b/targets/widget/index.swift deleted file mode 100644 index 8b772cd..0000000 --- a/targets/widget/index.swift +++ /dev/null @@ -1,11 +0,0 @@ -import WidgetKit -import SwiftUI - -@main -struct exportWidgets: WidgetBundle { - var body: some Widget { - // Export widgets here - widget() - // widgetControl() - } -} diff --git a/targets/widget/remotewol-ios.icon/Assets/Image.png b/targets/widget/remotewol-ios.icon/Assets/Image.png deleted file mode 100644 index 391371e1d0bc1a4350d9f35b451a9a7e9af6a6ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14535 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 diff --git a/targets/widget/remotewol-ios.icon/Assets/display 2.png b/targets/widget/remotewol-ios.icon/Assets/display 2.png deleted file mode 100644 index 8783a716787f77305b20ac244551525964116713..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10298 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 z 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 aPdei^8&JRV>#t8@j~%f)TzCk3^?v}fW$qaO diff --git a/targets/widget/remotewol-ios.icon/Assets/display.png b/targets/widget/remotewol-ios.icon/Assets/display.png deleted file mode 100644 index 5c2a7313d42e25c098ddb4ff5647b1e77073a050..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10632 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 XeCJ`+6fc$EUqK!-GBYgIcfR&t&4gT3 diff --git a/targets/widget/remotewol-ios.icon/icon.json b/targets/widget/remotewol-ios.icon/icon.json deleted file mode 100644 index ebdf087..0000000 --- a/targets/widget/remotewol-ios.icon/icon.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "fill" : { - "automatic-gradient" : "extended-gray:1.00000,1.00000" - }, - "groups" : [ - { - "layers" : [ - { - "hidden" : false, - "image-name" : "Image.png", - "name" : "Image", - "position" : { - "scale" : 0.65, - "translation-in-points" : [ - 0, - -52.942187500000045 - ] - } - }, - { - "fill-specializations" : [ - { - "appearance" : "dark", - "value" : "none" - } - ], - "image-name-specializations" : [ - { - "value" : "display 2.png" - }, - { - "appearance" : "dark", - "value" : "display.png" - }, - { - "appearance" : "tinted", - "value" : "display 3.png" - } - ], - "name" : "display 3", - "position" : { - "scale" : 1.25, - "translation-in-points" : [ - 0, - -10.625 - ] - } - } - ], - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 - }, - "translucency" : { - "enabled" : true, - "value" : 0.5 - } - } - ], - "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" - } -} \ No newline at end of file diff --git a/targets/widget/widgets.swift b/targets/widget/widgets.swift deleted file mode 100644 index ef5afd2..0000000 --- a/targets/widget/widgets.swift +++ /dev/null @@ -1,208 +0,0 @@ -import WidgetKit -import SwiftUI - -// MARK: - Timeline Entry - -struct DeviceEntry: TimelineEntry { - let date: Date - let configuration: ConfigurationAppIntent - let device: DeviceInfo? -} - -// MARK: - Timeline Provider - -struct Provider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> DeviceEntry { - DeviceEntry( - date: Date(), - configuration: ConfigurationAppIntent(), - device: DeviceInfo(id: "placeholder", name: "My Computer", mac: "AA:BB:CC:DD:EE:FF", ip: "192.168.1.100", canSleep: true, canShutdown: true, status: "unknown") - ) - } - - func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> DeviceEntry { - let device = configuration.device.flatMap { SharedDeviceData.getDevice(id: $0.id) } - return DeviceEntry(date: Date(), configuration: configuration, device: device) - } - - func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { - let device = configuration.device.flatMap { SharedDeviceData.getDevice(id: $0.id) } - let entry = DeviceEntry(date: Date(), configuration: configuration, device: device) - - // Widgets don't update frequently enough for status, so we just provide a static entry - // Refresh every 30 minutes to pick up device list changes - let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())! - return Timeline(entries: [entry], policy: .after(nextUpdate)) - } -} - -// MARK: - Deep Link Helper - -enum DeviceAction: String { - case wake - case sleep - case restart - case shutdown - - func url(for deviceId: String) -> URL { - URL(string: "remotewol-upsnap://action/\(self.rawValue)/\(deviceId)")! - } -} - -// MARK: - Widget View - -struct DeviceWidgetEntryView: View { - var entry: Provider.Entry - @Environment(\.widgetFamily) var widgetFamily - - var body: some View { - if let device = entry.device { - VStack(alignment: .leading, spacing: 8) { - // Device Info Header - VStack(alignment: .leading, spacing: 2) { - Text(device.name) - .font(.system(size: 15, weight: .semibold)) - .lineLimit(1) - Text(device.mac) - .font(.system(size: 11)) - .foregroundColor(.secondary) - .lineLimit(1) - } - - Spacer(minLength: 4) - - // Action Buttons - if widgetFamily == .systemSmall { - // Small widget: 2x2 grid - VStack(spacing: 6) { - HStack(spacing: 6) { - ActionButton(action: .wake, deviceId: device.id, enabled: device.status == "offline") - ActionButton(action: .sleep, deviceId: device.id, enabled: device.status == "online" && device.canSleep) - } - HStack(spacing: 6) { - ActionButton(action: .restart, deviceId: device.id, enabled: device.status == "online" && device.canShutdown) - ActionButton(action: .shutdown, deviceId: device.id, enabled: device.status == "online" && device.canShutdown) - } - } - } else { - // Medium/Large widget: horizontal layout - HStack(spacing: 8) { - ActionButton(action: .wake, deviceId: device.id, enabled: device.status == "offline") - ActionButton(action: .sleep, deviceId: device.id, enabled: device.status == "online" && device.canSleep) - ActionButton(action: .restart, deviceId: device.id, enabled: device.status == "online" && device.canShutdown) - ActionButton(action: .shutdown, deviceId: device.id, enabled: device.status == "online" && device.canShutdown) - } - } - } - .padding(.vertical, 4) - } else { - // No device selected - VStack(spacing: 8) { - Image(systemName: "desktopcomputer") - .font(.system(size: 32)) - .foregroundColor(.secondary) - Text("Select a Device") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.secondary) - Text("Long press to configure") - .font(.system(size: 11)) - .foregroundColor(.secondary.opacity(0.7)) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } -} - -// MARK: - Action Button - -struct ActionButton: View { - let action: DeviceAction - let deviceId: String - let enabled: Bool - - var icon: String { - switch action { - case .wake: return "bolt.fill" - case .sleep: return "moon.fill" - case .restart: return "arrow.clockwise" - case .shutdown: return "power" - } - } - - var color: Color { - switch action { - case .wake: return .green - case .sleep: return .orange - case .restart: return .blue - case .shutdown: return .red - } - } - - var disabled: Color { - return .gray - } - - var label: String { - switch action { - case .wake: return "Wake" - case .sleep: return "Sleep" - case .restart: return "Restart" - case .shutdown: return "Shut Down" - } - } - - var body: some View { - Link(destination: enabled ? action.url(for: deviceId) : URL("")!) { - VStack(spacing: 2) { - Image(systemName: icon) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(enabled ? color : disabled) - } - .frame(maxWidth: .infinity, minHeight: 36) - .background((enabled ? color : disabled).opacity(0.15)) - .cornerRadius(8) - .disabled(!enabled) - } - } -} - -// MARK: - Widget Definition - -struct DeviceControlWidget: Widget { - let kind: String = "widget" - - var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in - DeviceWidgetEntryView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) - } - .configurationDisplayName("Device Control") - .description("Control your devices with wake, sleep, restart, and shutdown actions.") - .supportedFamilies([.systemSmall, .systemMedium]) - } -} - -// Keep legacy name for backwards compatibility with existing widget installations -typealias widget = DeviceControlWidget - -// MARK: - Preview - -#Preview(as: .systemSmall) { - DeviceControlWidget() -} timeline: { - DeviceEntry( - date: .now, - configuration: ConfigurationAppIntent(), - device: DeviceInfo(id: "preview", name: "Gaming PC", mac: "AA:BB:CC:DD:EE:FF", ip: "192.168.1.100", canSleep: true, canShutdown: true, status: "online") - ) -} - -#Preview(as: .systemMedium) { - DeviceControlWidget() -} timeline: { - DeviceEntry( - date: .now, - configuration: ConfigurationAppIntent(), - device: DeviceInfo(id: "preview", name: "Home Server", mac: "11:22:33:44:55:66", ip: "192.168.1.50", canSleep: true, canShutdown: true, status: "offline") - ) -} -- 2.54.0 From 4dfb830f7a068ea2534beae6281b4e908d328528 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Thu, 22 Oct 2026 18:55:53 -0400 Subject: [PATCH 2/2] feat: refresh PocketBase auth tokens automatically (#4) Co-authored-by: Joshua Higgins --- src/context/AuthContext.tsx | 284 +++++++++++++++++++++++++---- src/services/api.ts | 348 ++++++++++++++++++++++++++---------- src/services/jwtExpiry.ts | 41 +++++ 3 files changed, 538 insertions(+), 135 deletions(-) create mode 100644 src/services/jwtExpiry.ts diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index be15a6b..8e9d125 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,6 +1,8 @@ -import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; +import { AppState } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; -import api from '../services/api'; +import api, { isAuthCollection, isRevokedAuthError } from '../services/api'; +import { getJwtExpiryMs } from '../services/jwtExpiry'; import { syncDevicesToWidget } from '../services/widgetSync'; import { AuthResponse, User } from '../types'; @@ -17,6 +19,22 @@ interface AuthContextType { const AuthContext = createContext(undefined); +const AUTH_COLLECTION_KEY = 'auth_collection'; +const REFRESH_BEFORE_EXPIRY_MS = 60_000; +const MAX_REFRESH_DELAY_MS = 2_147_483_647; +const REFRESH_RETRY_MS = 30_000; +const REFRESH_BACKOFF_MS = 15_000; + +function msUntilRefresh(token: string): number | null { + const expiresAt = getJwtExpiryMs(token); + if (expiresAt == null) return null; + return expiresAt - Date.now() - REFRESH_BEFORE_EXPIRY_MS; +} + +function collectionFromUser(user: User | null): 'users' | '_superusers' | null { + return isAuthCollection(user?.collectionName) ? user.collectionName : null; +} + export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [user, setUser] = useState(null); const [serverAddress, setServerAddress] = useState(null); @@ -24,44 +42,192 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [isLoading, setIsLoading] = useState(true); const [canCreate, setCanCreate] = useState(null); - const clearSession = useCallback(async () => { - await AsyncStorage.multiRemove(['auth_token', 'auth_user', 'auth_can_create']); + const refreshTimerRef = useRef | null>(null); + const refreshingRef = useRef(false); + const sessionWriteRef = useRef>(Promise.resolve()); + const refreshSessionRef = useRef<() => Promise>(async () => {}); + const armRefreshTimerRef = useRef<(token: string, overdueDelay?: number) => void>(() => {}); + const enqueueSessionWrite = useCallback((task: () => Promise) => { + const run = sessionWriteRef.current.then(task, task); + sessionWriteRef.current = run.then( + () => undefined, + () => undefined, + ); + return run; + }, []); + + const clearRefreshTimer = useCallback(() => { + if (refreshTimerRef.current != null) { + clearTimeout(refreshTimerRef.current); + refreshTimerRef.current = null; + } + }, []); + + const clearSession = useCallback(async () => { + clearRefreshTimer(); api.clearToken(); api.clearCanCreate(); + api.clearAuthCollection(); setToken(null); setUser(null); setCanCreate(null); + + await enqueueSessionWrite(async () => { + await AsyncStorage.multiRemove([ + 'auth_token', + 'auth_user', + 'auth_can_create', + AUTH_COLLECTION_KEY, + ]); + }); + }, [clearRefreshTimer, enqueueSessionWrite]); + + const armRefreshTimer = useCallback((nextToken: string, overdueDelay = 0) => { + clearRefreshTimer(); + const untilRefresh = msUntilRefresh(nextToken); + if (untilRefresh == null) return; + + const delay = untilRefresh <= 0 ? overdueDelay : Math.min(untilRefresh, MAX_REFRESH_DELAY_MS); + const rescheduleOnly = untilRefresh > MAX_REFRESH_DELAY_MS; + + refreshTimerRef.current = setTimeout(() => { + refreshTimerRef.current = null; + const latest = api.getToken(); + if (!latest) return; + if (rescheduleOnly) { + armRefreshTimerRef.current(latest); + return; + } + void refreshSessionRef.current(); + }, delay); + }, [clearRefreshTimer]); + + const refreshSession = useCallback(async () => { + if (refreshingRef.current || !api.getToken()) return; + + refreshingRef.current = true; + try { + await api.refreshAuth(); + } catch { + // The failure listener either keeps the session and retries, or signs out. + } finally { + refreshingRef.current = false; + } + }, []); + + const handleRefreshFailure = useCallback(async (error: unknown) => { + if (isRevokedAuthError(error)) { + await clearSession(); + return; + } + + if (!api.getToken()) return; + + clearRefreshTimer(); + refreshTimerRef.current = setTimeout(() => { + refreshTimerRef.current = null; + void refreshSessionRef.current(); + }, REFRESH_RETRY_MS); + }, [clearRefreshTimer, clearSession]); + + const persistRefreshedAuth = useCallback(async (auth: AuthResponse) => { + await enqueueSessionWrite(async () => { + if (api.getToken() !== auth.token) return; + + const collection = api.getAuthCollection(); + const pairs: [string, string][] = [ + ['auth_token', auth.token], + ['auth_user', JSON.stringify(auth.record)], + ]; + if (collection) { + pairs.push([AUTH_COLLECTION_KEY, collection]); + } + if (collection === '_superusers') { + pairs.push(['auth_can_create', 'true']); + } + + await AsyncStorage.multiSet(pairs); + if (api.getToken() !== auth.token) return; + + setToken(auth.token); + setUser(auth.record); + if (collection === '_superusers') { + setCanCreate(true); + } + armRefreshTimerRef.current(auth.token, REFRESH_BACKOFF_MS); + }); + }, [enqueueSessionWrite]); + + const watchToken = useCallback((nextToken: string) => { + const untilRefresh = msUntilRefresh(nextToken); + if (untilRefresh != null && untilRefresh <= 0) { + void refreshSessionRef.current(); + return; + } + armRefreshTimerRef.current(nextToken); }, []); + useEffect(() => { + refreshSessionRef.current = refreshSession; + armRefreshTimerRef.current = armRefreshTimer; + }, [armRefreshTimer, refreshSession]); + useEffect(() => { api.setUnauthorizedHandler(clearSession); + api.setAuthUpdateListener(persistRefreshedAuth); + api.setAuthFailureListener(handleRefreshFailure); return () => { api.setUnauthorizedHandler(null); + api.setAuthUpdateListener(null); + api.setAuthFailureListener(null); + clearRefreshTimer(); }; - }, [clearSession]); + }, [clearRefreshTimer, clearSession, handleRefreshFailure, persistRefreshedAuth]); useEffect(() => { let cancelled = false; - void AsyncStorage.multiGet([ - 'auth_token', - 'auth_user', - 'auth_server_address', - 'auth_can_create', - ]).then( - ([[, storedToken], [, storedUser], [, storedServerAddress], [, storedCanCreate]]) => { + void (async () => { + try { + const [ + [, storedToken], + [, storedUser], + [, storedServerAddress], + [, storedCanCreate], + [, storedCollection], + ] = await AsyncStorage.multiGet([ + 'auth_token', + 'auth_user', + 'auth_server_address', + 'auth_can_create', + AUTH_COLLECTION_KEY, + ]); if (cancelled) return; - if (storedCanCreate) { - setCanCreate(storedCanCreate === 'true'); + let restoredUser: User | null = null; + if (storedToken && storedUser) { + restoredUser = JSON.parse(storedUser) as User; + api.setToken(storedToken); + setToken(storedToken); + setUser(restoredUser); } - if (storedToken && storedUser) { - setToken(storedToken); - api.setToken(storedToken); - setUser(JSON.parse(storedUser)); + const collection = isAuthCollection(storedCollection) + ? storedCollection + : collectionFromUser(restoredUser); + if (collection) { + api.setAuthCollection(collection); + } + + if (collection === '_superusers') { + api.setCanCreate(true); + setCanCreate(true); + } else if (storedCanCreate) { + const restoredCanCreate = storedCanCreate === 'true'; + api.setCanCreate(restoredCanCreate); + setCanCreate(restoredCanCreate); } if (storedServerAddress) { @@ -69,36 +235,78 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children api.setAddress(storedServerAddress + '/api'); } - setIsLoading(false); - }, - error => { + const untilRefresh = storedToken ? msUntilRefresh(storedToken) : null; + const refreshIsDue = !!storedToken && !!restoredUser && untilRefresh != null && untilRefresh <= 0; + + if (refreshIsDue) { + try { + // Hold the loading gate so an expired token does not flash the device list. + await api.refreshAuth(); + } catch { + // Revoked tokens sign out; transient failures keep the session and retry. + } + } else if (storedToken && restoredUser) { + watchToken(storedToken); + } + + if (!cancelled) { + setIsLoading(false); + } + } catch (error) { if (cancelled) return; console.error('Failed to load auth', error); setIsLoading(false); } - ); + })(); return () => { cancelled = true; }; + }, [watchToken]); + + useEffect(() => { + const subscription = AppState.addEventListener('change', nextState => { + if (nextState !== 'active') return; + + const currentToken = api.getToken(); + if (!currentToken) return; + + const untilRefresh = msUntilRefresh(currentToken); + if (untilRefresh == null) return; + if (untilRefresh <= 0) { + void refreshSessionRef.current(); + return; + } + armRefreshTimerRef.current(currentToken); + }); + + return () => { + subscription.remove(); + }; }, []); - const login = async (serverAddress: string, identity: string, password: string) => { - try { - const response: AuthResponse = await api.authenticate(serverAddress, identity, password); - - await AsyncStorage.setItem('auth_token', response.token); - await AsyncStorage.setItem('auth_user', JSON.stringify(response.record)); - await AsyncStorage.setItem('auth_server_address', serverAddress); - await AsyncStorage.setItem('auth_can_create', api.getCanCreate() ? 'true' : 'false'); - - setToken(response.token); - setUser(response.record); - setServerAddress(serverAddress); - setCanCreate(api.getCanCreate() || false); - } catch (error) { - throw error; - } + const login = async (nextServerAddress: string, identity: string, password: string) => { + const response: AuthResponse = await api.authenticate(nextServerAddress, identity, password); + const collection = api.getAuthCollection(); + + await enqueueSessionWrite(async () => { + const pairs: [string, string][] = [ + ['auth_token', response.token], + ['auth_user', JSON.stringify(response.record)], + ['auth_server_address', nextServerAddress], + ['auth_can_create', api.getCanCreate() ? 'true' : 'false'], + ]; + if (collection) { + pairs.push([AUTH_COLLECTION_KEY, collection]); + } + await AsyncStorage.multiSet(pairs); + }); + + setToken(response.token); + setUser(response.record); + setServerAddress(nextServerAddress); + setCanCreate(api.getCanCreate() || false); + watchToken(response.token); }; const logout = async () => { diff --git a/src/services/api.ts b/src/services/api.ts index 5420f04..2fa797a 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -5,12 +5,47 @@ import { PermissionResponse, } from '../types'; +export type AuthCollection = 'users' | '_superusers'; + +export type ApiError = Error & { + status?: number; + isAuthError?: boolean; +}; + +export type AuthUpdateListener = (auth: AuthResponse) => void | Promise; +export type AuthFailureListener = (error: unknown) => void | Promise; + +const AUTH_ERROR_MESSAGE = /authorization token|invalid token|unauthorized|not authenticated/i; + +export function isAuthCollection(value: unknown): value is AuthCollection { + return value === 'users' || value === '_superusers'; +} + +export function isRevokedAuthError(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false; + const status = (error as { status?: unknown }).status; + return status === 401 || status === 403 || status === 404; +} + +function isSessionChanged(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + (error as { sessionChanged?: boolean }).sessionChanged === true + ); +} + class UpSnapAPI { private token: string | null = null; private address: string | null = null; private canCreate: boolean | null = null; + private authCollection: AuthCollection | null = null; + private authEpoch = 0; private unauthorizedHandler: (() => Promise) | null = null; + private authUpdateListener: AuthUpdateListener | null = null; + private authFailureListener: AuthFailureListener | null = null; private isHandlingUnauthorized = false; + private refreshInFlight: Promise | null = null; setToken(token: string) { this.token = token; @@ -22,6 +57,7 @@ class UpSnapAPI { clearToken() { this.token = null; + this.authEpoch += 1; } setAddress(address: string) { @@ -48,10 +84,30 @@ class UpSnapAPI { this.canCreate = null; } + setAuthCollection(collection: AuthCollection | null) { + this.authCollection = collection; + } + + getAuthCollection(): AuthCollection | null { + return this.authCollection; + } + + clearAuthCollection() { + this.authCollection = null; + } + setUnauthorizedHandler(handler: (() => Promise) | null) { this.unauthorizedHandler = handler; } + setAuthUpdateListener(listener: AuthUpdateListener | null) { + this.authUpdateListener = listener; + } + + setAuthFailureListener(listener: AuthFailureListener | null) { + this.authFailureListener = listener; + } + private async handleUnauthorized() { if (!this.unauthorizedHandler || this.isHandlingUnauthorized) { return; @@ -66,7 +122,7 @@ class UpSnapAPI { } } - private async throwApiError(response: Response, fallbackMessage: string): Promise { + private async errorFromResponse(response: Response, fallbackMessage: string): Promise { let message = fallbackMessage; try { @@ -76,23 +132,26 @@ class UpSnapAPI { // Ignore JSON parsing errors and use the fallback message. } - const isAuthError = + const apiError = new Error(message) as ApiError; + apiError.status = response.status; + apiError.isAuthError = response.status === 401 || response.status === 403 || - /authorization token|invalid token|unauthorized|not authenticated/i.test(message); + AUTH_ERROR_MESSAGE.test(message); + return apiError; + } - if (isAuthError) { + private async throwApiError( + response: Response, + fallbackMessage: string, + skipUnauthorizedHandler = false, + ): Promise { + const error = await this.errorFromResponse(response, fallbackMessage); + + if (error.isAuthError && !skipUnauthorizedHandler) { await this.handleUnauthorized(); } - const error = new Error(message) as Error & { - status?: number; - isAuthError?: boolean; - }; - - error.status = response.status; - error.isAuthError = isAuthError; - throw error; } @@ -108,11 +167,166 @@ class UpSnapAPI { return headers; } + private sessionChanged(): ApiError { + const error = new Error('Auth session changed') as ApiError & { sessionChanged: true }; + error.sessionChanged = true; + error.isAuthError = true; + return error; + } + + /** + * One refresh is shared by the expiry timer and every request that receives a 401. + * PocketBase returns a new auth token and record for the same collection. + */ + async refreshAuth(): Promise { + if (!this.refreshInFlight) { + this.refreshInFlight = this.performRefresh().finally(() => { + this.refreshInFlight = null; + }); + } + + return this.refreshInFlight; + } + + private async performRefresh(): Promise { + const epoch = this.authEpoch; + const tokenAtStart = this.token; + + try { + if (!tokenAtStart || !this.address) { + throw Object.assign(new Error('Not authenticated'), { + status: 401, + isAuthError: true, + }) as ApiError; + } + + const collections: AuthCollection[] = this.authCollection + ? [this.authCollection] + : ['users', '_superusers']; + let lastError: ApiError | null = null; + + for (let index = 0; index < collections.length; index += 1) { + const collection = collections[index]; + const response = await fetch( + `${this.address}/collections/${collection}/auth-refresh`, + { + method: 'POST', + headers: this.getHeaders(), + }, + ); + + if (this.authEpoch !== epoch || this.token !== tokenAtStart) { + throw this.sessionChanged(); + } + + if (response.ok) { + const data = (await response.json()) as AuthResponse; + if (this.authEpoch !== epoch || this.token !== tokenAtStart) { + throw this.sessionChanged(); + } + if (!data?.token) { + throw Object.assign(new Error('Auth refresh failed'), { + status: 401, + isAuthError: true, + }) as ApiError; + } + + this.authCollection = isAuthCollection(data.record?.collectionName) + ? data.record.collectionName + : collection; + this.token = data.token; + if (this.authCollection === '_superusers') { + this.canCreate = true; + } + + try { + await this.authUpdateListener?.(data); + } catch (error) { + console.error('Failed to persist refreshed auth', error); + } + + return data; + } + + lastError = await this.errorFromResponse(response, 'Auth refresh failed'); + const tryNextCollection = + !this.authCollection && index < collections.length - 1 && response.status === 403; + if (!tryNextCollection) { + throw lastError; + } + } + + throw ( + lastError ?? + (Object.assign(new Error('Auth refresh failed'), { + status: 401, + isAuthError: true, + }) as ApiError) + ); + } catch (error) { + if (!isSessionChanged(error)) { + try { + await this.authFailureListener?.(error); + } catch (listenerError) { + console.error('Failed to handle auth refresh failure', listenerError); + } + } + throw error; + } + } + + /** + * On 401, refresh once and retry the original request. + * A second 401, or a refresh the server rejects, ends the session. + */ + private async request(url: string, fallbackMessage: string, init?: RequestInit): Promise { + const send = () => + fetch(url, { + ...init, + headers: this.getHeaders(), + }); + + const response = await send(); + if (response.ok) { + return response; + } + + if (response.status === 401 && this.token) { + try { + await this.refreshAuth(); + } catch (error) { + if (isSessionChanged(error)) { + throw error; + } + if (!isRevokedAuthError(error)) { + throw error; + } + await this.handleUnauthorized(); + return this.throwApiError(response, fallbackMessage, true); + } + + const retried = await send(); + if (!retried.ok) { + if (retried.status === 401) { + await this.handleUnauthorized(); + return this.throwApiError(retried, fallbackMessage, true); + } + return this.throwApiError(retried, fallbackMessage); + } + return retried; + } + + return this.throwApiError(response, fallbackMessage); + } + async authenticate( serverAddress: string, identity: string, password: string ): Promise { + this.authEpoch += 1; + this.token = null; + this.authCollection = null; this.address = serverAddress + '/api'; const response = await fetch( @@ -125,7 +339,7 @@ class UpSnapAPI { ); if (!response.ok) { - const response = await fetch( + const superuserResponse = await fetch( `${this.address}/collections/_superusers/auth-with-password`, { method: 'POST', @@ -134,18 +348,20 @@ class UpSnapAPI { } ); - if (!response.ok) { - await this.throwApiError(response, 'Authentication failed'); + if (!superuserResponse.ok) { + await this.throwApiError(superuserResponse, 'Authentication failed'); } - const data: AuthResponse = await response.json(); + const data: AuthResponse = await superuserResponse.json(); this.token = data.token; + this.authCollection = '_superusers'; this.canCreate = true; return data; } const data: AuthResponse = await response.json(); this.token = data.token; + this.authCollection = 'users'; const userID = data.record.id; const userPermissionResponse = await fetch( @@ -165,142 +381,80 @@ class UpSnapAPI { } async getDevices(page = 1, perPage = 100): Promise { - const response = await fetch( + const response = await this.request( `${this.address}/collections/devices/records?page=${page}&perPage=${perPage}`, - { - headers: this.getHeaders(), - } + 'Failed to fetch devices', ); - if (!response.ok) { - await this.throwApiError(response, 'Failed to fetch devices'); - } - const data = await response.json(); return data.items; } async getDevice(id: string): Promise { - const response = await fetch( + const response = await this.request( `${this.address}/collections/devices/records/${id}`, - { - headers: this.getHeaders(), - } + 'Failed to fetch device', ); - if (!response.ok) { - await this.throwApiError(response, 'Failed to fetch device'); - } - return response.json(); } async createDevice(device: Partial): Promise { - const response = await fetch( + const response = await this.request( `${this.address}/collections/devices/records`, + 'Failed to create device', { method: 'POST', - headers: this.getHeaders(), body: JSON.stringify(device), - } + }, ); - if (!response.ok) { - await this.throwApiError(response, 'Failed to create device'); - } - return response.json(); } async updateDevice(id: string, device: Partial): Promise { - const response = await fetch( + const response = await this.request( `${this.address}/collections/devices/records/${id}`, + 'Failed to update device', { method: 'PATCH', - headers: this.getHeaders(), body: JSON.stringify(device), - } + }, ); - if (!response.ok) { - await this.throwApiError(response, 'Failed to update device'); - } - return response.json(); } async deleteDevice(id: string): Promise { - const response = await fetch( + await this.request( `${this.address}/collections/devices/records/${id}`, - { - method: 'DELETE', - headers: this.getHeaders(), - } + 'Failed to delete device', + { method: 'DELETE' }, ); - - if (!response.ok) { - await this.throwApiError(response, 'Failed to delete device'); - } } async wakeDevice(id: string): Promise { - const response = await fetch(`${this.address}/upsnap/wake/${id}`, { - headers: this.getHeaders(), - }); - - if (!response.ok) { - await this.throwApiError(response, 'Failed to wake device'); - } + await this.request(`${this.address}/upsnap/wake/${id}`, 'Failed to wake device'); } async wakeGroup(id: string): Promise { - const response = await fetch(`${this.address}/upsnap/wakegroup/${id}`, { - headers: this.getHeaders(), - }); - - if (!response.ok) { - await this.throwApiError(response, 'Failed to wake group'); - } + await this.request(`${this.address}/upsnap/wakegroup/${id}`, 'Failed to wake group'); } async sleepDevice(id: string): Promise { - const response = await fetch(`${this.address}/upsnap/sleep/${id}`, { - headers: this.getHeaders(), - }); - - if (!response.ok) { - await this.throwApiError(response, 'Failed to sleep device'); - } + await this.request(`${this.address}/upsnap/sleep/${id}`, 'Failed to sleep device'); } async rebootDevice(id: string): Promise { - const response = await fetch(`${this.address}/upsnap/reboot/${id}`, { - headers: this.getHeaders(), - }); - - if (!response.ok) { - await this.throwApiError(response, 'Failed to reboot device'); - } + await this.request(`${this.address}/upsnap/reboot/${id}`, 'Failed to reboot device'); } async shutdownDevice(id: string): Promise { - const response = await fetch(`${this.address}/upsnap/shutdown/${id}`, { - headers: this.getHeaders(), - }); - - if (!response.ok) { - await this.throwApiError(response, 'Failed to shutdown device'); - } + await this.request(`${this.address}/upsnap/shutdown/${id}`, 'Failed to shutdown device'); } async scanNetwork(): Promise { - const response = await fetch(`${this.address}/upsnap/scan`, { - headers: this.getHeaders(), - }); - - if (!response.ok) { - await this.throwApiError(response, 'Failed to scan network'); - } + const response = await this.request(`${this.address}/upsnap/scan`, 'Failed to scan network'); const data = await response.json(); if (data.devices && Array.isArray(data.devices)) { diff --git a/src/services/jwtExpiry.ts b/src/services/jwtExpiry.ts new file mode 100644 index 0000000..426e358 --- /dev/null +++ b/src/services/jwtExpiry.ts @@ -0,0 +1,41 @@ +const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + +function decodeBase64Url(segment: string): string { + const normalized = segment.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4); + const bytes: number[] = []; + + for (let index = 0; index < padded.length; index += 4) { + const a = BASE64_ALPHABET.indexOf(padded[index]); + const b = BASE64_ALPHABET.indexOf(padded[index + 1]); + const c = padded[index + 2] === '=' ? 0 : BASE64_ALPHABET.indexOf(padded[index + 2]); + const d = padded[index + 3] === '=' ? 0 : BASE64_ALPHABET.indexOf(padded[index + 3]); + if (a < 0 || b < 0 || c < 0 || d < 0) { + throw new Error('Invalid base64url'); + } + + const chunk = (a << 18) | (b << 12) | (c << 6) | d; + bytes.push((chunk >> 16) & 255); + if (padded[index + 2] !== '=') bytes.push((chunk >> 8) & 255); + if (padded[index + 3] !== '=') bytes.push(chunk & 255); + } + + return String.fromCharCode(...bytes); +} + +/** + * Reads the JWT `exp` claim in milliseconds. + * Scheduling only; the signature is not verified. + */ +export function getJwtExpiryMs(token: string): number | null { + const segment = token.split('.')[1]; + if (!segment) return null; + + try { + const payload = JSON.parse(decodeBase64Url(segment)) as { exp?: unknown }; + if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp)) return null; + return payload.exp * 1000; + } catch { + return null; + } +} -- 2.54.0