Skip to content

Conversation

@mrevanzak
Copy link
Contributor

@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
Contributor Author

closed #11

@mrevanzak
Copy link
Contributor Author

@V3RON

@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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor 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

@mrevanzak
Copy link
Contributor Author

any update? @V3RON

@mrevanzak mrevanzak requested a review from V3RON January 22, 2026 04:20
@V3RON
Copy link
Contributor

V3RON commented Jan 22, 2026

Sorry! Focused on delivering Android as soon as possible. I'll get back to this PR in the morning 🙏

@V3RON
Copy link
Contributor

V3RON commented Jan 26, 2026

I allowed myself to go with a refactor to enable runtime switching between Watch/CarPlay-enabled and iPhone-only Live Activities. I'm waiting for my test Watch to charge and boot so I can test it in practice.

@V3RON
Copy link
Contributor

V3RON commented Jan 26, 2026

Okaaay. This .supplementalActivityFamilies works differently than I expected.

If you don't provide it, the Live Activity will still be displayed, but the OS will provide a fallback layout.
If you provide .small, the system will stop providing a fallback for WatchOS, CarPlay, or StandBy (small widget) and rely solely on your code. If you don’t handle it, you’ll get an empty view.
If you provide .medium, the system will stop providing a fallback for StandBy (full width) and rely solely on your code. If you don’t handle it, you’ll get an empty view.

Technically, we need four variants:

  • Default
  • With small
  • With medium
  • With both small and medium

Alternatively, we can have one variant with all families enabled and custom code to provide reasonable fallbacks via Voltra.

@V3RON
Copy link
Contributor

V3RON commented Jan 26, 2026

I think it's reasonable to go with the "Voltra provides fallback" approach, so we can return to a single WidgetAttributes variant.

Sorry for going back and forth on this, but testing it in practice really opened my eyes 🙈
I'm going to push an update later today.

@V3RON
Copy link
Contributor

V3RON commented Jan 26, 2026

Alright, I think it's ready for a review from your side @mrevanzak. Let me know what do you think about it.

@mrevanzak
Copy link
Contributor Author

Okaaay. This .supplementalActivityFamilies works differently than I expected.

If you don't provide it, the Live Activity will still be displayed, but the OS will provide a fallback layout.

If you provide .small, the system will stop providing a fallback for WatchOS, CarPlay, or StandBy (small widget) and rely solely on your code. If you don’t handle it, you’ll get an empty view.

If you provide .medium, the system will stop providing a fallback for StandBy (full width) and rely solely on your code. If you don’t handle it, you’ll get an empty view.

Technically, we need four variants:

  • Default

  • With small

  • With medium

  • With both small and medium

Alternatively, we can have one variant with all families enabled and custom code to provide reasonable fallbacks via Voltra.

hmmm interesting. I never delved deeply into it.

@mrevanzak
Copy link
Contributor Author

all good i think @V3RON

@V3RON V3RON merged commit 766f21b into callstackincubator:main Jan 27, 2026
4 of 5 checks passed
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