Skip to content

feat(ios): add widget kind option to keep placed widgets across migrations - #272

Open
rlods wants to merge 4 commits into
callstackincubator:mainfrom
rlods:feat/ios-widget-kind-option
Open

feat(ios): add widget kind option to keep placed widgets across migrations#272
rlods wants to merge 4 commits into
callstackincubator:mainfrom
rlods:feat/ios-widget-kind-option

Conversation

@rlods

@rlods rlods commented Sep 7, 2026

Copy link
Copy Markdown

Summary

Adds an optional kind to iOS widget configs. It overrides the WidgetKit kind of the generated WidgetConfiguration, which today is hardcoded to Voltra_Widget_<id>.

WidgetKit identifies a widget placed on the Home Screen by extension bundle id + kind. targetName already lets an app keep its extension bundle id, but without control over the kind, every widget migrated from a hand-written WidgetKit extension turns into a dead placeholder after the update and users have to remove and re-add it.

With kind pinned to the legacy value, placed widgets pick up the Voltra timeline in place.

[
    '@use-voltra/ios-client', {
        targetName: 'MyWidgets', // existing extension target
        widgets: [
            {
                id: 'streak',
                kind: 'StreakWidget', // to override `Voltra_Widget_<id>`
                displayName: 'Streak',
                description: {}
            }
        ],
    }
]

Changes

  • IOSWidgetConfig.kind?: string, default unchanged (Voltra_Widget_<id>). widgetKind() / widgetKindOverrides() helpers in constants.ts.
  • Swift template uses widgetKind(widget) for both StaticConfiguration and AppIntentConfiguration.
  • Overrides are written to both Info.plists as Voltra_WidgetKinds: { id: kind } (main app via configureInfoPlist, extension via configureWidgetExtensionPlist).
  • New ios/shared/VoltraWidgetKind.swift maps id → kind (plist override, else prefix) and kind → id (reverse lookup, else strip prefix, else nil for non-Voltra kinds). Used by reloadTimeline, getActiveWidgets (so name still reports the widget id), getInstalledWidgetIds (orphan cleanup) and VoltraRefreshIntent. It lives in ios/shared so both podspecs compile it into the app and the extension.
  • Validation: kind must be a non-empty string; kinds must be unique across widgets, including a custom kind colliding with another widget's default.
  • Docs: kind in the widgets option list plus a "Keeping the kind of an existing widget" section. Changeset: @use-voltra/ios-client minor.

Testing

  • pnpm run test in packages/ios-client: 88 tests pass, including new cases for the template (kind used, no Voltra_Widget_ left) and validation (empty kind, duplicate kinds, custom vs default collision).
  • pnpm run typecheck, pnpm run lint, swiftformat --lint ios, swift test (69 tests) pass.
  • Extension sources plus VoltraWidgetService.swift type-check against the iOS 17 simulator SDK.
  • Dogfooding: we run this as a patch on @use-voltra/ios-client@2.2.0 to migrate a production widget from a hand-written WidgetKit extension. On-device upgrade verification is still in progress, hence the draft.

Checklist

  • Tests pass (npm test)
  • Linting passes (npm run lint:libOnly)
  • Formatting is correct (npm run format:check)

🤖 Generated with Claude Code

@rlods
rlods marked this pull request as ready for review September 7, 2026 21:10
@rlods

rlods commented Sep 10, 2026

Copy link
Copy Markdown
Author

cc @V3RON 👋 random ping amongst recent contributors, could you please have a look and release if good for you?

@V3RON

V3RON commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Hey @rlods!

I pushed some commits to make sure your changes are reflected in both the Expo Config Plugin and RN CLI (voltra apply) paths. Let me know if they work for you. If so, I'll merge the PR and release it 👌

rlods and others added 4 commits September 11, 2026 01:16
…rations

WidgetKit identifies a placed widget by extension bundle id + kind. Voltra
hardcoded `Voltra_Widget_<id>`, so a widget migrated from a hand-written
extension turned into a placeholder on users' Home Screens. `kind` lets the
config pin the legacy value. The override travels through Info.plist
(`Voltra_WidgetKinds`, app + extension) and a `VoltraWidgetKind` helper maps
id <-> kind for timeline reloads, `getActiveWidgets` and orphan cleanup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
VoltraWidgetKind read Bundle.main from a computed property, so the id <-> kind
mapping - the only new native logic behind the `kind` option - could not be
covered: Bundle.main is the runner in VoltraSharedTests. Split a pure core out
of each direction and move the plist read to VoltraConfig, which already owns
Info.plist access and established this shape with voltraVersion(bundle:). The
map is now read once per process rather than on every reloadTimeline.
Both plugin steps only ever set the key. A prebuild over an existing ios/
directory keeps what the plist already holds, so removing `kind` from the config
left the widget answering to a kind nobody configured any more - the one Voltra
key where a stale value silently changes which Home Screen instances a widget
owns. Widen widgetKindOverrides to the fields it actually reads so the mapping
can be covered without casting fixtures.
The bare React Native path is a separate implementation from the Expo plugin: it
has its own config types, plist writer and Swift codegen, and it hardcoded
Voltra_Widget_<id>. The native half is shared through the installed
@use-voltra/ios-client, so this is plumbing - resolve the kind from one helper,
write Voltra_WidgetKinds into the app and extension Info.plists, and reject
duplicate kinds the way normalization already rejects duplicate ids.

Unlike prebuild, `voltra apply` mutates a native project it does not own, so the
main app plist deletes the key when no widget pins a kind.
@rlods
rlods force-pushed the feat/ios-widget-kind-option branch from 7a11bc2 to 337b09f Compare September 10, 2026 23:26
@rlods

rlods commented Sep 10, 2026

Copy link
Copy Markdown
Author

I pushed some commits to make sure your changes are reflected in both the Expo Config Plugin and RN CLI (voltra apply) paths. Let me know if they work for you. If so, I'll merge the PR and release it 👌

thanks @V3RON - they look good to me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants