Skip to content

Conversation

@mrevanzak
Copy link

@mrevanzak mrevanzak commented Jan 14, 2026

Summary

  • Add support for Apple's supplementalActivityFamilies modifier (iOS 18+) enabling Live Activities on watchOS Smart Stack and CarPlay
  • Introduce supplementalActivityFamilies.small variant in TypeScript API with automatic fallback to lockScreen content
  • Require explicit opt-in via plugin config liveActivity.supplementalActivityFamilies: ["small"]

Changes

TypeScript

  • Add supplementalActivityFamilies.small to LiveActivityVariants type
  • Add sup_sm JSON key to renderer output
  • Update renderer to handle supplemental regions

Swift

  • Add supplementalSmall case to VoltraRegion enum
  • Add VoltraAdaptiveLockScreenView with @Environment(\.activityFamily) detection
  • iOS 18+ availability checks with graceful fallback for older versions

Plugin

  • Add LiveActivityConfig type with supplementalActivityFamilies option
  • Generate VoltraWidgetWithSupplementalActivityFamilies wrapper when configured
  • Add validation for activity family configuration

Usage

{
  "expo": {
    "plugins": [
      ["voltra", {
        "groupIdentifier": "group.com.example",
        "liveActivity": {
          "supplementalActivityFamilies": ["small"]
        }
      }]
    ]
  }
}
 useLiveActivity({
  ...,
  supplementalActivityFamilies: {
    small: <CompactWatchView />  
  }
})

Requirements

Feature Minimum iOS
watchOS Smart Stack iOS 18.0
CarPlay Dashboard iOS 26.0

Example

WatchOS
incoming-9F3850CE-078B-480B-9344-9FD8B63F9E3E

- Add renderer tests for supplemental.small variant serialization
- Add plugin tests for validation and widget bundle generation
- Add WatchLiveActivity example demonstrating supplemental.small
- Enable supplementalFamilies in example app config
Swift syntax requires @unknown default to be its own case, cannot combine with other patterns.
- Add comprehensive guide at development/supplemental-activity-families.md
- Update plugin-configuration.md with liveActivity config section
- Add navigation entry in development/_meta.json
- Reference supplemental families in developing-live-activities.md
@vercel
Copy link

vercel bot commented Jan 14, 2026

@mrevanzak is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

@mrevanzak
Copy link
Author

closed #11

@mrevanzak
Copy link
Author

@V3RON

@mrevanzak
Copy link
Author

crazy that Opus did this in one shot 🤯

@V3RON V3RON self-requested a review January 14, 2026 19:40
@V3RON
Copy link
Contributor

V3RON commented Jan 14, 2026

I'm going to go through it tomorrow! 👀

Align API naming with Apple's native .supplementalActivityFamilies()
modifier.

Changes:
- TypeScript: supplemental -> supplementalActivityFamilies in variants
- Plugin config: supplementalFamilies -> supplementalActivityFamilies
- Swift: supplementalSmall -> supplementalActivityFamiliesSmall
- JSON key: sup_sm -> saf_sm
- Generated Swift wrapper: VoltraWidgetWithSupplementalActivityFamilies
- Update all tests and documentation
@mrevanzak mrevanzak force-pushed the feat/activity-family branch from dca490f to 1ac4a0a Compare January 14, 2026 20:05
Comment on lines +104 to +106
/// A view that adapts its content based on the activity family environment
/// - For .small (watchOS/CarPlay): Uses supplementalActivityFamiliesSmall content if available, falls back to lockScreen
/// - For .medium (iPhone lock screen) and unknown: Always uses lockScreen
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I understand this better. Apple’s docs aren't very clear about what behavior to expect 👌

* Supported supplemental activity families (iOS 18+)
* These enable Live Activities to appear on watchOS Smart Stack and CarPlay
*/
export type ActivityFamily = 'small'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about 'medium'?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium is the default one that we have without any config @V3RON. the lockscreen one. so no need to specify it again. Even if we pass [.small, .medium], it will have the same effect as [.small].

Starting with iOS 18, Live Activities can appear on additional surfaces beyond the iPhone lock screen and Dynamic Island:

- **watchOS Smart Stack** (iOS 18+) - Appears on paired Apple Watch
- **CarPlay Dashboard** (iOS 26+) - Appears on CarPlay displays
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is CarPlay somehow connected to supplemental activity families? I think it just works with the default configuration.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@V3RON V3RON Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to verify this in my car later today 👀

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright haha

Comment on lines +13 to +29
function generateVoltraWidgetWrapper(familiesSwift: string): string {
return dedent`
// MARK: - Live Activity with Supplemental Activity Families
struct VoltraWidgetWithSupplementalActivityFamilies: Widget {
private let wrapped = VoltraWidget()
var body: some WidgetConfiguration {
if #available(iOS 18.0, *) {
return wrapped.body.supplementalActivityFamilies([${familiesSwift}])
} else {
return wrapped.body
}
}
}
`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for myself: make sure to format generated code correctly, so it's more pleasant to read.

@V3RON
Copy link
Contributor

V3RON commented Jan 16, 2026

There may be cases where we want to enable Watch only for a subset of Live Activities. In the current implementation, it's either none or all-in. I'm wondering if we should refactor this so we have two configs for Live Activities: one for "non-Watch" and another for "Watch-enabled". We could then target one of them based on the config parameter the user passes when displaying the Live Activity. This would also work remotely. WDYT?

var body: some Widget {
        // Config 1: Standard (No supplemental families)
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            DeliveryLockScreenView(context: context)
        } dynamicIsland: { context in
            DynamicIsland { ... } // Your Dynamic Island code
        }

        // Config 2: Watch Enabled (Adds .small / .medium)
        ActivityConfiguration(for: DeliveryAttributesWithWatch.self) { context in
            DeliveryLockScreenView(context: context) // You can reuse the same view code
        } dynamicIsland: { context in
             DynamicIsland { ... } // Reuse same Island code
        }
        .supplementalActivityFamilies([.small, .medium]) // <--- The specific difference
    }

@mrevanzak
Copy link
Author

There may be cases where we want to enable Watch only for a subset of Live Activities. In the current implementation, it's either none or all-in. I'm wondering if we should refactor this so we have two configs for Live Activities: one for "non-Watch" and another for "Watch-enabled". We could then target one of them based on the config parameter the user passes when displaying the Live Activity. This would also work remotely. WDYT?

var body: some Widget {
        // Config 1: Standard (No supplemental families)
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            DeliveryLockScreenView(context: context)
        } dynamicIsland: { context in
            DynamicIsland { ... } // Your Dynamic Island code
        }

        // Config 2: Watch Enabled (Adds .small / .medium)
        ActivityConfiguration(for: DeliveryAttributesWithWatch.self) { context in
            DeliveryLockScreenView(context: context) // You can reuse the same view code
        } dynamicIsland: { context in
             DynamicIsland { ... } // Reuse same Island code
        }
        .supplementalActivityFamilies([.small, .medium]) // <--- The specific difference
    }

so how is the code on react native gonna be?

@V3RON
Copy link
Contributor

V3RON commented Jan 16, 2026

We would accept supplementalActivityFamilies in startLiveActivity, and based on its value we would use a different Activity on the Swift side, switching from DefaultVoltraLiveActivityAttributes (currently VoltraAttributes) to WatchVoltraLiveActivityAttributes. I’m not sure whether it’s legitimate to change the attributes type during the lifetime of an activity, but I guess we’ll find out in practice.

@mrevanzak
Copy link
Author

We would accept supplementalActivityFamilies in startLiveActivity, and based on its value we would use a different Activity on the Swift side, switching from DefaultVoltraLiveActivityAttributes (currently VoltraAttributes) to WatchVoltraLiveActivityAttributes. I’m not sure whether it’s legitimate to change the attributes type during the lifetime of an activity, but I guess we’ll find out in practice.

just dont supply it into supplementalActivityFamilies object when start those?

@mrevanzak
Copy link
Author

or if you mean that you dont want certain variant to be not shown on watch at all then yeah we need to adjust it

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