From bc260b3b55d48bb369e20986acde4ae1089b1ea0 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 29 Jan 2026 09:28:09 +0100 Subject: [PATCH 1/5] feat: implement getActiveWidgets --- android/src/main/java/voltra/VoltraModule.kt | 42 ++++++++++++++++++++ ios/app/VoltraModule.swift | 4 ++ ios/app/VoltraModuleImpl.swift | 32 +++++++++++++++ src/VoltraModule.ts | 5 +++ src/android/client.ts | 2 + src/android/widgets/api.ts | 18 ++++++++- src/android/widgets/index.ts | 2 + src/android/widgets/types.ts | 15 +++++++ src/client.ts | 3 +- src/widgets/types.ts | 10 +++++ src/widgets/widget-api.ts | 21 +++++++++- 11 files changed, 150 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/voltra/VoltraModule.kt b/android/src/main/java/voltra/VoltraModule.kt index 68f0d88..d62569e 100644 --- a/android/src/main/java/voltra/VoltraModule.kt +++ b/android/src/main/java/voltra/VoltraModule.kt @@ -1,5 +1,6 @@ package voltra +import android.appwidget.AppWidgetManager import android.util.Log import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp @@ -171,6 +172,47 @@ class VoltraModule : Module() { Log.d(TAG, "clearAllAndroidWidgets completed") } + AsyncFunction("getActiveWidgets") { + val context = appContext.reactContext ?: return@AsyncFunction emptyList>() + val manager = AppWidgetManager.getInstance(context) + val packageName = context.packageName + + // 1. Get all providers defined in this app + val installedProviders = + manager.installedProviders.filter { + it.provider.packageName == packageName + } + + val activeWidgets = mutableListOf>() + + // 2. Iterate over every provider + for (providerInfo in installedProviders) { + val ids = manager.getAppWidgetIds(providerInfo.provider) + + // 3. Iterate over every instance of that widget + for (id in ids) { + val options = manager.getAppWidgetOptions(id) + val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) + val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) + + // Get short class name (e.g. ".MyWidget") + val shortClassName = providerInfo.provider.shortClassName + + activeWidgets.add( + mapOf( + "widgetId" to id, + "providerClassName" to shortClassName, + "label" to providerInfo.loadLabel(context.packageManager).toString(), + "width" to minWidth, + "height" to minHeight, + ), + ) + } + } + + activeWidgets + } + AsyncFunction("requestPinGlanceAppWidget") { widgetId: String, options: Map?, diff --git a/ios/app/VoltraModule.swift b/ios/app/VoltraModule.swift index 98e8057..afb0102 100644 --- a/ios/app/VoltraModule.swift +++ b/ios/app/VoltraModule.swift @@ -117,6 +117,10 @@ public class VoltraModule: Module { await self.impl.clearAllWidgets() } + AsyncFunction("getActiveWidgets") { () async throws -> [[String: String]] in + return try await self.impl.getActiveWidgets() + } + View(VoltraRN.self) { Prop("payload") { (view, payload: String) in view.setPayload(payload) diff --git a/ios/app/VoltraModuleImpl.swift b/ios/app/VoltraModuleImpl.swift index 3a55331..6a59b99 100644 --- a/ios/app/VoltraModuleImpl.swift +++ b/ios/app/VoltraModuleImpl.swift @@ -249,8 +249,40 @@ public class VoltraModuleImpl { print("[Voltra] Cleared all widgets") } + func getActiveWidgets() async throws -> [[String: String]] { + return try await withCheckedThrowingContinuation { continuation in + WidgetCenter.shared.getCurrentConfigurations { result in + switch result { + case let .success(widgetInfos): + let mapped = widgetInfos.map { widget -> [String: String] in + return [ + "kind": widget.kind, + "family": self.mapWidgetFamily(widget.family), + ] + } + continuation.resume(returning: mapped) + case let .failure(error): + continuation.resume(throwing: error) + } + } + } + } + // MARK: - Private Helpers + private func mapWidgetFamily(_ family: WidgetFamily) -> String { + switch family { + case .systemSmall: return "systemSmall" + case .systemMedium: return "systemMedium" + case .systemLarge: return "systemLarge" + case .systemExtraLarge: return "systemExtraLarge" + case .accessoryCircular: return "accessoryCircular" + case .accessoryRectangular: return "accessoryRectangular" + case .accessoryInline: return "accessoryInline" + @unknown default: return "unknown" + } + } + private func mapError(_ error: Error) -> Error { if let serviceError = error as? VoltraLiveActivityError { switch serviceError { diff --git a/src/VoltraModule.ts b/src/VoltraModule.ts index c31095f..55a3712 100644 --- a/src/VoltraModule.ts +++ b/src/VoltraModule.ts @@ -198,6 +198,11 @@ export interface VoltraModuleSpec { */ clearAllWidgets(): Promise + /** + * Fetches all active widget configurations/instances + */ + getActiveWidgets(): Promise + /** * Add an event listener */ diff --git a/src/android/client.ts b/src/android/client.ts index 331b710..7f51b61 100644 --- a/src/android/client.ts +++ b/src/android/client.ts @@ -21,6 +21,7 @@ export type { export { clearAllAndroidWidgets, clearAndroidWidget, + getActiveWidgets, reloadAndroidWidgets, requestPinAndroidWidget, updateAndroidWidget, @@ -30,6 +31,7 @@ export type { AndroidWidgetSizeVariant, AndroidWidgetVariants, UpdateAndroidWidgetOptions, + WidgetInfo, } from './widgets/types.js' // Preload API diff --git a/src/android/widgets/api.ts b/src/android/widgets/api.ts index 25208d9..a4af5b4 100644 --- a/src/android/widgets/api.ts +++ b/src/android/widgets/api.ts @@ -1,6 +1,6 @@ import VoltraModule from '../../VoltraModule.js' import { renderAndroidWidgetToString } from './renderer.js' -import type { AndroidWidgetVariants, UpdateAndroidWidgetOptions } from './types.js' +import type { AndroidWidgetVariants, UpdateAndroidWidgetOptions, WidgetInfo } from './types.js' // Re-export types for public API export type { @@ -8,6 +8,7 @@ export type { AndroidWidgetSizeVariant, AndroidWidgetVariants, UpdateAndroidWidgetOptions, + WidgetInfo, } from './types.js' /** @@ -148,3 +149,18 @@ export const requestPinAndroidWidget = async ( ): Promise => { return VoltraModule.requestPinGlanceAppWidget(widgetId, options) } + +/** + * Fetches all active widget instances for the containing app on Android. + * + * @returns A promise that resolves to an array of active widget instances. + * + * @example + * ```typescript + * const widgets = await getActiveWidgets() + * console.log('Active widgets:', widgets) + * ``` + */ +export const getActiveWidgets = async (): Promise => { + return VoltraModule.getActiveWidgets() +} diff --git a/src/android/widgets/index.ts b/src/android/widgets/index.ts index fb6b9dc..4945798 100644 --- a/src/android/widgets/index.ts +++ b/src/android/widgets/index.ts @@ -2,6 +2,7 @@ export { clearAllAndroidWidgets, clearAndroidWidget, + getActiveWidgets, reloadAndroidWidgets, requestPinAndroidWidget, updateAndroidWidget, @@ -13,4 +14,5 @@ export type { AndroidWidgetSizeVariant, AndroidWidgetVariants, UpdateAndroidWidgetOptions, + WidgetInfo, } from './types.js' diff --git a/src/android/widgets/types.ts b/src/android/widgets/types.ts index 88d76b9..c8d4f8e 100644 --- a/src/android/widgets/types.ts +++ b/src/android/widgets/types.ts @@ -17,6 +17,21 @@ export type AndroidWidgetSizeVariant = { content: ReactNode } +/** + * Information about an active widget instance on Android + */ +export type WidgetInfo = { + /** The unique ID for this widget instance (required for updates) */ + widgetId: number + /** The class name of the provider (e.g., ".WeatherWidget") */ + providerClassName: string + /** Current labeling associated with the widget */ + label: string + /** Dimensions in dp as reported by the system */ + width: number + height: number +} + /** * Widget variants using size-based breakpoints. * Android picks the best matching variant based on widget dimensions. diff --git a/src/client.ts b/src/client.ts index 11c979d..d12abac 100644 --- a/src/client.ts +++ b/src/client.ts @@ -42,10 +42,11 @@ export { export type { DismissalPolicy, LiveActivityVariants } from './live-activity/types.js' // Widget API -export type { WidgetFamily, WidgetVariants } from './widgets/types.js' +export type { WidgetFamily, WidgetInfo, WidgetVariants } from './widgets/types.js' export { clearAllWidgets, clearWidget, + getActiveWidgets, reloadWidgets, type ScheduledWidgetEntry, scheduleWidget, diff --git a/src/widgets/types.ts b/src/widgets/types.ts index 46c482e..c15d329 100644 --- a/src/widgets/types.ts +++ b/src/widgets/types.ts @@ -18,6 +18,16 @@ export type WidgetFamily = */ export type WidgetVariants = Partial> +/** + * Information about an active widget configuration on iOS + */ +export interface WidgetInfo { + /** The 'kind' string defined in your WidgetExtension */ + kind: string + /** The visual size of the widget */ + family: WidgetFamily +} + /** * A single entry in a widget timeline with scheduled display time and content */ diff --git a/src/widgets/widget-api.ts b/src/widgets/widget-api.ts index 769e892..baad42a 100644 --- a/src/widgets/widget-api.ts +++ b/src/widgets/widget-api.ts @@ -2,11 +2,11 @@ import type { UpdateWidgetOptions } from '../types.js' import { assertRunningOnApple } from '../utils/assertRunningOnApple.js' import VoltraModule from '../VoltraModule.js' import { renderWidgetToString } from './renderer.js' -import { ScheduledWidgetEntry, WidgetVariants } from './types.js' +import { ScheduledWidgetEntry, WidgetInfo, WidgetVariants } from './types.js' // Re-export types for public API export type { UpdateWidgetOptions } from '../types.js' -export type { ScheduledWidgetEntry } from './types.js' +export type { ScheduledWidgetEntry, WidgetInfo } from './types.js' /** * Update a home screen widget with new content. @@ -184,3 +184,20 @@ export const scheduleWidget = async (widgetId: string, entries: ScheduledWidgetE return VoltraModule.scheduleWidget(widgetId, timelineJson) } + +/** + * Fetches all active widget configurations for the containing app on iOS. + * + * @returns A promise that resolves to an array of active widget configurations. + * + * @example + * ```typescript + * const widgets = await getActiveWidgets() + * console.log('Active widgets:', widgets) + * ``` + */ +export const getActiveWidgets = async (): Promise => { + if (!assertRunningOnApple()) return [] + + return VoltraModule.getActiveWidgets() +} From 354467351ff8836d8c8c126e6e512a3127bdb0b1 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 29 Jan 2026 09:32:13 +0100 Subject: [PATCH 2/5] feat: expose user-defined widget name --- android/src/main/java/voltra/VoltraModule.kt | 12 +++++++++++- ios/app/VoltraModuleImpl.swift | 6 ++++++ src/android/widgets/types.ts | 2 ++ src/widgets/types.ts | 2 ++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/voltra/VoltraModule.kt b/android/src/main/java/voltra/VoltraModule.kt index d62569e..ed61702 100644 --- a/android/src/main/java/voltra/VoltraModule.kt +++ b/android/src/main/java/voltra/VoltraModule.kt @@ -195,11 +195,21 @@ class VoltraModule : Module() { val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) - // Get short class name (e.g. ".MyWidget") + // Get short class name (e.g. ".widget.VoltraWidget_weatherReceiver") val shortClassName = providerInfo.provider.shortClassName + val prefix = ".widget.VoltraWidget_" + val suffix = "Receiver" + val name = + if (shortClassName.startsWith(prefix) && shortClassName.endsWith(suffix)) { + shortClassName.substring(prefix.length, shortClassName.length - suffix.length) + } else { + shortClassName + } + activeWidgets.add( mapOf( + "name" to name, "widgetId" to id, "providerClassName" to shortClassName, "label" to providerInfo.loadLabel(context.packageManager).toString(), diff --git a/ios/app/VoltraModuleImpl.swift b/ios/app/VoltraModuleImpl.swift index 6a59b99..20555dd 100644 --- a/ios/app/VoltraModuleImpl.swift +++ b/ios/app/VoltraModuleImpl.swift @@ -255,7 +255,13 @@ public class VoltraModuleImpl { switch result { case let .success(widgetInfos): let mapped = widgetInfos.map { widget -> [String: String] in + let prefix = "Voltra_Widget_" + let name = widget.kind.hasPrefix(prefix) + ? String(widget.kind.dropFirst(prefix.count)) + : widget.kind + return [ + "name": name, "kind": widget.kind, "family": self.mapWidgetFamily(widget.family), ] diff --git a/src/android/widgets/types.ts b/src/android/widgets/types.ts index c8d4f8e..000ef5e 100644 --- a/src/android/widgets/types.ts +++ b/src/android/widgets/types.ts @@ -21,6 +21,8 @@ export type AndroidWidgetSizeVariant = { * Information about an active widget instance on Android */ export type WidgetInfo = { + /** The name (ID) of the widget as defined in the config plugin */ + name: string /** The unique ID for this widget instance (required for updates) */ widgetId: number /** The class name of the provider (e.g., ".WeatherWidget") */ diff --git a/src/widgets/types.ts b/src/widgets/types.ts index c15d329..5c40904 100644 --- a/src/widgets/types.ts +++ b/src/widgets/types.ts @@ -22,6 +22,8 @@ export type WidgetVariants = Partial> * Information about an active widget configuration on iOS */ export interface WidgetInfo { + /** The name (ID) of the widget as defined in the config plugin */ + name: string /** The 'kind' string defined in your WidgetExtension */ kind: string /** The visual size of the widget */ From bd3382d0d9caab467502fdc193d4a1e62d2ba377 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 29 Jan 2026 10:14:19 +0100 Subject: [PATCH 3/5] feat: add playground --- .../components/ActiveWidgetsAndroidCard.tsx | 121 ++++++++++++++++++ example/components/ActiveWidgetsIOSCard.tsx | 114 +++++++++++++++++ example/screens/android/AndroidScreen.tsx | 3 + .../live-activities/LiveActivitiesScreen.tsx | 3 + 4 files changed, 241 insertions(+) create mode 100644 example/components/ActiveWidgetsAndroidCard.tsx create mode 100644 example/components/ActiveWidgetsIOSCard.tsx diff --git a/example/components/ActiveWidgetsAndroidCard.tsx b/example/components/ActiveWidgetsAndroidCard.tsx new file mode 100644 index 0000000..ac84445 --- /dev/null +++ b/example/components/ActiveWidgetsAndroidCard.tsx @@ -0,0 +1,121 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { getActiveWidgets, WidgetInfo } from 'voltra/android/client' + +import { Card } from './Card' + +export function ActiveWidgetsAndroidCard() { + const [widgets, setWidgets] = useState([]) + const [loading, setLoading] = useState(true) + + const refreshWidgets = useCallback(async () => { + setLoading(true) + try { + const activeWidgets = await getActiveWidgets() + setWidgets(activeWidgets) + } catch (error) { + console.error('Failed to fetch active widgets:', error) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + refreshWidgets() + }, [refreshWidgets]) + + return ( + + + Active Widgets + + {loading ? ( + + ) : ( + Refresh + )} + + + + Currently active widget instances on your home screen. + + {widgets.length > 0 ? ( + + {widgets.map((widget) => ( + + {widget.name} + ID: {widget.widgetId} + + {widget.width}x{widget.height}dp + + + ))} + + ) : ( + + {loading ? 'Fetching widgets...' : 'No active widgets found'} + + )} + + ) +} + +const styles = StyleSheet.create({ + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + refreshButton: { + paddingVertical: 4, + paddingHorizontal: 12, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + borderRadius: 12, + }, + refreshText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '600', + }, + scroll: { + marginTop: 12, + }, + widgetItem: { + backgroundColor: 'rgba(255, 255, 255, 0.05)', + borderRadius: 12, + padding: 12, + marginRight: 12, + width: 140, + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.1)', + }, + widgetName: { + color: '#FFFFFF', + fontWeight: '700', + fontSize: 16, + marginBottom: 4, + }, + widgetId: { + color: '#CBD5F5', + fontSize: 11, + }, + widgetDetails: { + color: '#CBD5F5', + fontSize: 11, + marginTop: 2, + }, + provider: { + color: 'rgba(203, 213, 245, 0.5)', + fontSize: 10, + marginTop: 8, + }, + emptyState: { + padding: 20, + alignItems: 'center', + }, + emptyText: { + color: '#CBD5F5', + fontStyle: 'italic', + }, +}) diff --git a/example/components/ActiveWidgetsIOSCard.tsx b/example/components/ActiveWidgetsIOSCard.tsx new file mode 100644 index 0000000..268961e --- /dev/null +++ b/example/components/ActiveWidgetsIOSCard.tsx @@ -0,0 +1,114 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { getActiveWidgets, WidgetInfo } from 'voltra/client' + +import { Card } from './Card' + +export function ActiveWidgetsIOSCard() { + const [widgets, setWidgets] = useState([]) + const [loading, setLoading] = useState(true) + + const refreshWidgets = useCallback(async () => { + setLoading(true) + try { + const activeWidgets = await getActiveWidgets() + setWidgets(activeWidgets) + } catch (error) { + console.error('Failed to fetch active widgets:', error) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + refreshWidgets() + }, [refreshWidgets]) + + return ( + + + Active Home Widgets + + {loading ? ( + + ) : ( + Refresh + )} + + + + Active Home Screen widget configurations for this app. + + {widgets.length > 0 ? ( + + {widgets.map((widget, index) => ( + + {widget.name} + {widget.family} + + ))} + + ) : ( + + {loading ? 'Fetching widgets...' : 'No active widgets found'} + + )} + + ) +} + +const styles = StyleSheet.create({ + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + refreshButton: { + paddingVertical: 4, + paddingHorizontal: 12, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + borderRadius: 12, + }, + refreshText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '600', + }, + scroll: { + marginTop: 12, + }, + widgetItem: { + backgroundColor: 'rgba(130, 50, 255, 0.1)', + borderRadius: 12, + padding: 12, + marginRight: 12, + width: 140, + borderWidth: 1, + borderColor: 'rgba(130, 50, 255, 0.2)', + }, + widgetName: { + color: '#FFFFFF', + fontWeight: '700', + fontSize: 16, + marginBottom: 4, + }, + widgetFamily: { + color: '#CBD5F5', + fontSize: 11, + fontWeight: '600', + }, + kind: { + color: 'rgba(203, 213, 245, 0.5)', + fontSize: 10, + marginTop: 8, + }, + emptyState: { + padding: 20, + alignItems: 'center', + }, + emptyText: { + color: '#CBD5F5', + fontStyle: 'italic', + }, +}) diff --git a/example/screens/android/AndroidScreen.tsx b/example/screens/android/AndroidScreen.tsx index d5df960..3bfb229 100644 --- a/example/screens/android/AndroidScreen.tsx +++ b/example/screens/android/AndroidScreen.tsx @@ -2,6 +2,7 @@ import { useRouter } from 'expo-router' import React from 'react' import { ScrollView, StyleSheet, Text, View } from 'react-native' +import { ActiveWidgetsAndroidCard } from '~/components/ActiveWidgetsAndroidCard' import { Button } from '~/components/Button' import { Card } from '~/components/Card' @@ -41,6 +42,8 @@ export default function AndroidScreen() { write Kotlin or XML anymore. + + {ANDROID_SECTIONS.map((section) => ( {section.title} diff --git a/example/screens/live-activities/LiveActivitiesScreen.tsx b/example/screens/live-activities/LiveActivitiesScreen.tsx index 4572b54..a97bf44 100644 --- a/example/screens/live-activities/LiveActivitiesScreen.tsx +++ b/example/screens/live-activities/LiveActivitiesScreen.tsx @@ -5,6 +5,7 @@ import { endAllLiveActivities } from 'voltra/client' import { Button } from '~/components/Button' import { Card } from '~/components/Card' +import { ActiveWidgetsIOSCard } from '~/components/ActiveWidgetsIOSCard' import { NotificationsCard } from '~/components/NotificationsCard' import BasicLiveActivity from '~/screens/live-activities/BasicLiveActivity' import CompassLiveActivity from '~/screens/live-activities/CompassLiveActivity' @@ -208,6 +209,8 @@ export default function LiveActivitiesScreen() { + + {CARD_ORDER.map(renderCard)} From 3cf20b7d1c6a98de523a1b7bc475f98d7a80412b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 29 Jan 2026 10:15:12 +0100 Subject: [PATCH 4/5] style: reformat --- ios/app/VoltraModuleImpl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/app/VoltraModuleImpl.swift b/ios/app/VoltraModuleImpl.swift index 20555dd..6b91a1e 100644 --- a/ios/app/VoltraModuleImpl.swift +++ b/ios/app/VoltraModuleImpl.swift @@ -250,7 +250,7 @@ public class VoltraModuleImpl { } func getActiveWidgets() async throws -> [[String: String]] { - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { continuation in WidgetCenter.shared.getCurrentConfigurations { result in switch result { case let .success(widgetInfos): From 072ca4c1575f8de810301918afc9b402d4bfb46d Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 29 Jan 2026 10:24:34 +0100 Subject: [PATCH 5/5] docs: update website --- website/docs/android/development/_meta.json | 5 +++ .../android/development/developing-widgets.md | 1 + .../development/querying-active-widgets.md | 36 ++++++++++++++++ website/docs/ios/development/_meta.json | 5 +++ .../ios/development/developing-widgets.md | 13 ++++++ .../development/querying-active-widgets.md | 41 +++++++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 website/docs/android/development/querying-active-widgets.md create mode 100644 website/docs/ios/development/querying-active-widgets.md diff --git a/website/docs/android/development/_meta.json b/website/docs/android/development/_meta.json index 3a89944..b0185ee 100644 --- a/website/docs/android/development/_meta.json +++ b/website/docs/android/development/_meta.json @@ -4,6 +4,11 @@ "name": "developing-widgets", "label": "Developing Widgets" }, + { + "type": "file", + "name": "querying-active-widgets", + "label": "Querying Active Widgets" + }, { "type": "file", "name": "styling", diff --git a/website/docs/android/development/developing-widgets.md b/website/docs/android/development/developing-widgets.md index 153867f..7384b37 100644 --- a/website/docs/android/development/developing-widgets.md +++ b/website/docs/android/development/developing-widgets.md @@ -60,6 +60,7 @@ Unlike standard React Native or iOS Stacks, Android Glance layouts are more rest ## Advanced Features +- **[Querying Active Widgets](./querying-active-widgets):** Detect active widget instances and their sizes. - **[Testing and Previews](./testing-and-previews):** Preview layouts within your app. - **[Widget Picker Previews](../api/plugin-configuration#widget-picker-previews):** Configure how your widget appears in the Android widget picker. - **[Image Preloading](./image-preloading):** Cache remote images for use in widgets. diff --git a/website/docs/android/development/querying-active-widgets.md b/website/docs/android/development/querying-active-widgets.md new file mode 100644 index 0000000..624c19f --- /dev/null +++ b/website/docs/android/development/querying-active-widgets.md @@ -0,0 +1,36 @@ +# Querying Active Widgets + +On Android, you can detect every active instance of your widgets currently placed on the Home Screen. This is particularly useful for Android since each widget instance can have different dimensions and a unique `widgetId`. + +## getActiveWidgets API + +The `getActiveWidgets` function returns a promise that resolves to an array of all active widget instances for your app. + +```typescript +import { getActiveWidgets } from 'voltra/android' + +async function checkAndroidWidgets() { + const activeWidgets = await getActiveWidgets() + + console.log(`Found ${activeWidgets.length} active widget instances`) + + activeWidgets.forEach(widget => { + console.log(`- Widget Name: ${widget.name}`) + console.log(` ID: ${widget.widgetId}`) + console.log(` Size: ${widget.width}x${widget.height}dp`) + }) +} +``` + +### WidgetInfo Object + +Each object in the returned array contains: + +| Property | Type | Description | +| :--- | :--- | :--- | +| `name` | `string` | The unique ID of the widget as defined in your Expo config plugin (e.g., `"weather"`). | +| `widgetId` | `number` | The unique system identifier for this specific widget instance. | +| `providerClassName` | `string` | The full class name of the widget provider (e.g., `".widget.VoltraWidget_weatherReceiver"`). | +| `label` | `string` | The human-readable label shown in the Android widget picker. | +| `width` | `number` | The current width of the widget instance in dp. | +| `height` | `number` | The current height of the widget instance in dp. | diff --git a/website/docs/ios/development/_meta.json b/website/docs/ios/development/_meta.json index 79513cc..2816115 100644 --- a/website/docs/ios/development/_meta.json +++ b/website/docs/ios/development/_meta.json @@ -14,6 +14,11 @@ "name": "developing-widgets", "label": "Developing Widgets" }, + { + "type": "file", + "name": "querying-active-widgets", + "label": "Querying Active Widgets" + }, { "type": "file", "name": "widget-pre-rendering", diff --git a/website/docs/ios/development/developing-widgets.md b/website/docs/ios/development/developing-widgets.md index a7d8c61..46f9652 100644 --- a/website/docs/ios/development/developing-widgets.md +++ b/website/docs/ios/development/developing-widgets.md @@ -233,6 +233,19 @@ await updateWidget('minimal', { ## Additional widget APIs +### getActiveWidgets + +Detect which widgets are currently installed on the user's home screen: + +```typescript +import { getActiveWidgets } from 'voltra/client' + +const widgets = await getActiveWidgets() +// [{ name: 'weather', family: 'systemSmall', kind: '...' }] +``` + +See [Querying Active Widgets](./querying-active-widgets) for more details. + ### reloadWidgets Force widget timelines to refresh their content after updating shared resources like preloaded images: diff --git a/website/docs/ios/development/querying-active-widgets.md b/website/docs/ios/development/querying-active-widgets.md new file mode 100644 index 0000000..b6edd4a --- /dev/null +++ b/website/docs/ios/development/querying-active-widgets.md @@ -0,0 +1,41 @@ +# Querying Active Widgets + +Voltra allows you to detect which widgets the user has currently placed on their Home Screen. This is useful for: + +- Determining if you need to update a specific widget +- Showing a list of active widgets in your app settings +- Optimizing background updates by only targeting installed widgets + +## getActiveWidgets API + +The `getActiveWidgets` function returns a promise that resolves to an array of all active widget configurations for your app. + +:::warning +There may be a slight delay in the data returned by this API. iOS caches widget configurations, and it might take a few moments for the system to reflect recent additions or removals of widgets on the Home Screen. +::: + +```typescript +import { getActiveWidgets } from 'voltra/client' + +async function checkWidgets() { + const activeWidgets = await getActiveWidgets() + + console.log(`User has ${activeWidgets.length} widgets installed`) + + activeWidgets.forEach(widget => { + console.log(`- Widget Name: ${widget.name}`) + console.log(` Family: ${widget.family}`) + console.log(` Kind: ${widget.kind}`) + }) +} +``` + +### WidgetInfo Object + +Each object in the returned array contains: + +| Property | Type | Description | +| :--- | :--- | :--- | +| `name` | `string` | The unique ID of the widget as defined in your Expo config plugin (e.g., `"weather"`). | +| `kind` | `string` | The internal identifier string used by iOS (e.g., `"Voltra_Widget_weather"`). | +| `family` | `WidgetFamily` | The size of the widget instance (e.g., `"systemSmall"`, `"systemMedium"`, etc.). |