@use-voltra/ios-client 2.0.0-rc.1 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-voltra/ios-client",
3
- "version": "2.0.0-rc.1",
3
+ "version": "2.0.0-rc.2",
4
4
  "description": "Client-only Voltra APIs for iOS",
5
5
  "main": "./build/commonjs/index.js",
6
6
  "module": "./build/module/index.js",
@@ -25,9 +25,22 @@
25
25
  "build",
26
26
  "expo-plugin",
27
27
  "ios",
28
+ "src",
28
29
  "!ios/.build",
29
30
  "Voltra.podspec",
30
- "README.md"
31
+ "README.md",
32
+ "*.podspec",
33
+ "react-native.config.js",
34
+ "!ios/build",
35
+ "!android/build",
36
+ "!android/gradle",
37
+ "!android/gradlew",
38
+ "!android/gradlew.bat",
39
+ "!android/local.properties",
40
+ "!**/__tests__",
41
+ "!**/__fixtures__",
42
+ "!**/__mocks__",
43
+ "!**/.*"
31
44
  ],
32
45
  "scripts": {
33
46
  "build": "npm run clean && npm run build:expo-plugin && bob build",
@@ -47,8 +60,8 @@
47
60
  "@babel/core": "^7.27.4",
48
61
  "@expo/config-plugins": "~10.1.2",
49
62
  "@expo/plist": "^0.3.5",
50
- "@use-voltra/expo-plugin": "2.0.0-rc.1",
51
- "@use-voltra/ios": "2.0.0-rc.1",
63
+ "@use-voltra/expo-plugin": "2.0.0-rc.2",
64
+ "@use-voltra/ios": "2.0.0-rc.2",
52
65
  "dedent": "^1.7.1",
53
66
  "xcode": "^3.0.1"
54
67
  },
@@ -0,0 +1,2 @@
1
+ export type { Spec as VoltraIOSModuleSpec } from './native/NativeVoltra'
2
+ export { getNativeVoltra } from './native/NativeVoltra'
@@ -0,0 +1,29 @@
1
+ import React from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+
4
+ import { VoltraView, type VoltraViewProps } from './VoltraView.js'
5
+
6
+ const LOCK_SCREEN_DIMENSIONS = {
7
+ width: 364,
8
+ height: 160,
9
+ }
10
+
11
+ export type VoltraLiveActivityPreviewProps = Omit<VoltraViewProps, 'style'> & {
12
+ style?: StyleProp<ViewStyle>
13
+ }
14
+
15
+ export function VoltraLiveActivityPreview({ style, children, ...voltraViewProps }: VoltraLiveActivityPreviewProps) {
16
+ const previewStyle: StyleProp<ViewStyle> = [
17
+ {
18
+ width: LOCK_SCREEN_DIMENSIONS.width,
19
+ height: LOCK_SCREEN_DIMENSIONS.height,
20
+ },
21
+ style,
22
+ ]
23
+
24
+ return (
25
+ <VoltraView {...voltraViewProps} style={previewStyle}>
26
+ {children}
27
+ </VoltraView>
28
+ )
29
+ }
@@ -0,0 +1,49 @@
1
+ import React, { type ReactNode, useEffect, useMemo } from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+
4
+ import { renderVoltraVariantToJson } from '@use-voltra/ios'
5
+
6
+ import VoltraFabricView from '../native/VoltraRNNativeComponent'
7
+
8
+ import { addVoltraListener, type VoltraInteractionEvent } from '../events.js'
9
+
10
+ const generateViewId = () => `voltra-view-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
11
+
12
+ const voltraViewStyle: ViewStyle = {
13
+ flex: 1,
14
+ }
15
+
16
+ export type VoltraViewProps = {
17
+ id?: string
18
+ children: ReactNode
19
+ style?: StyleProp<ViewStyle>
20
+ onInteraction?: (event: VoltraInteractionEvent) => void
21
+ testID?: string
22
+ }
23
+
24
+ export function VoltraView({ id, children, style, onInteraction, testID }: VoltraViewProps) {
25
+ const viewId = useMemo(() => id || generateViewId(), [id])
26
+
27
+ const json = renderVoltraVariantToJson(children)
28
+ const payload = JSON.stringify(json)
29
+
30
+ useEffect(() => {
31
+ if (!onInteraction) return
32
+
33
+ const subscription = addVoltraListener('interaction', (event) => {
34
+ if (event.source === viewId) {
35
+ onInteraction({
36
+ type: 'interaction',
37
+ source: event.source,
38
+ timestamp: event.timestamp,
39
+ identifier: event.identifier,
40
+ payload: event.payload,
41
+ })
42
+ }
43
+ })
44
+
45
+ return () => subscription.remove()
46
+ }, [viewId, onInteraction])
47
+
48
+ return <VoltraFabricView payload={payload} style={[voltraViewStyle, style]} testID={testID} viewId={viewId} />
49
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+
4
+ import type { WidgetFamily } from '@use-voltra/ios'
5
+
6
+ import { VoltraView, type VoltraViewProps } from './VoltraView.js'
7
+
8
+ const WIDGET_DIMENSIONS: Record<WidgetFamily, { width: number; height: number }> = {
9
+ systemSmall: { width: 170, height: 170 },
10
+ systemMedium: { width: 364, height: 170 },
11
+ systemLarge: { width: 364, height: 382 },
12
+ systemExtraLarge: { width: 364, height: 768 },
13
+ accessoryCircular: { width: 76, height: 76 },
14
+ accessoryRectangular: { width: 172, height: 76 },
15
+ accessoryInline: { width: 172, height: 40 },
16
+ }
17
+
18
+ export type VoltraWidgetPreviewProps = Omit<VoltraViewProps, 'style'> & {
19
+ family: WidgetFamily
20
+ style?: StyleProp<ViewStyle>
21
+ }
22
+
23
+ export function VoltraWidgetPreview({ family, style, children, ...voltraViewProps }: VoltraWidgetPreviewProps) {
24
+ const dimensions = WIDGET_DIMENSIONS[family]
25
+ const previewStyle: StyleProp<ViewStyle> = [
26
+ {
27
+ width: dimensions.width,
28
+ height: dimensions.height,
29
+ },
30
+ style,
31
+ ]
32
+
33
+ return (
34
+ <VoltraView {...voltraViewProps} style={previewStyle}>
35
+ {children}
36
+ </VoltraView>
37
+ )
38
+ }
package/src/events.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { Platform } from 'react-native'
2
+
3
+ import type { EventSubscription } from './types.js'
4
+ import { getNativeVoltra } from './VoltraModule.js'
5
+
6
+ export type BasicVoltraEvent = {
7
+ source: string
8
+ timestamp: number
9
+ }
10
+
11
+ export type VoltraActivityState = 'active' | 'dismissed' | 'pending' | 'stale' | 'ended' | string
12
+ export type VoltraActivityTokenReceivedEvent = BasicVoltraEvent & {
13
+ type: 'activityTokenReceived'
14
+ activityName: string
15
+ pushToken: string
16
+ }
17
+ export type VoltraActivityPushToStartTokenReceivedEvent = BasicVoltraEvent & {
18
+ type: 'activityPushToStartTokenReceived'
19
+ pushToStartToken: string
20
+ }
21
+ export type VoltraActivityUpdateEvent = BasicVoltraEvent & {
22
+ type: 'stateChange'
23
+ activityName: string
24
+ activityState: VoltraActivityState
25
+ }
26
+
27
+ export type VoltraInteractionEvent = BasicVoltraEvent & {
28
+ type: 'interaction'
29
+ identifier: string
30
+ payload: string
31
+ }
32
+
33
+ const noopSubscription: EventSubscription = {
34
+ remove: () => {},
35
+ }
36
+
37
+ export type VoltraEventMap = {
38
+ activityTokenReceived: VoltraActivityTokenReceivedEvent
39
+ activityPushToStartTokenReceived: VoltraActivityPushToStartTokenReceivedEvent
40
+ stateChange: VoltraActivityUpdateEvent
41
+ interaction: VoltraInteractionEvent
42
+ }
43
+
44
+ export function addVoltraListener<K extends keyof VoltraEventMap>(
45
+ event: K,
46
+ listener: (event: VoltraEventMap[K]) => void
47
+ ): EventSubscription {
48
+ if (Platform.OS !== 'ios') {
49
+ console.warn(`[Voltra] Event '${event}' is only supported on iOS. Returning no-op subscription.`)
50
+ return noopSubscription
51
+ }
52
+
53
+ const voltraModule = getNativeVoltra()
54
+
55
+ switch (event) {
56
+ case 'activityTokenReceived':
57
+ return voltraModule.onActivityTokenReceived(listener as (arg: VoltraActivityTokenReceivedEvent) => void)
58
+ case 'activityPushToStartTokenReceived':
59
+ return voltraModule.onActivityPushToStartTokenReceived(
60
+ listener as (arg: VoltraActivityPushToStartTokenReceivedEvent) => void
61
+ )
62
+ case 'stateChange':
63
+ return voltraModule.onStateChanged(listener as (arg: VoltraActivityUpdateEvent) => void)
64
+ case 'interaction':
65
+ return voltraModule.onInteraction(listener as (arg: VoltraInteractionEvent) => void)
66
+ default:
67
+ console.warn(`[Voltra] Event '${event}' is not supported. Returning no-op subscription.`)
68
+ return noopSubscription
69
+ }
70
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { Platform } from 'react-native'
2
+
3
+ import { getNativeVoltra } from './VoltraModule.js'
4
+
5
+ export function isGlassSupported(): boolean {
6
+ if (Platform.OS !== 'ios') return false
7
+ const v: any = Platform.Version
8
+ let major = 0
9
+ if (typeof v === 'string') {
10
+ const m = parseInt(v.split('.')[0] || '0', 10)
11
+ if (!Number.isNaN(m)) major = m
12
+ } else if (typeof v === 'number') {
13
+ major = Math.floor(v)
14
+ }
15
+ return major >= 26
16
+ }
17
+
18
+ export function isHeadless(): boolean {
19
+ if (Platform.OS !== 'ios') return false
20
+ return getNativeVoltra().isHeadless?.() ?? false
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ export { Voltra } from '@use-voltra/ios'
2
+ export {
3
+ VoltraLiveActivityPreview,
4
+ type VoltraLiveActivityPreviewProps,
5
+ } from './components/VoltraLiveActivityPreview.js'
6
+ export { VoltraView, type VoltraViewProps } from './components/VoltraView.js'
7
+ export { VoltraWidgetPreview, type VoltraWidgetPreviewProps } from './components/VoltraWidgetPreview.js'
8
+ export * from './events.js'
9
+ export { isGlassSupported, isHeadless } from './helpers.js'
10
+ export { logger, type VoltraLogLevel } from './logger.js'
11
+ export {
12
+ endAllLiveActivities,
13
+ type EndLiveActivityOptions,
14
+ isLiveActivityActive,
15
+ type SharedLiveActivityOptions,
16
+ startLiveActivity,
17
+ type StartLiveActivityOptions,
18
+ stopLiveActivity,
19
+ updateLiveActivity,
20
+ type UpdateLiveActivityOptions,
21
+ useLiveActivity,
22
+ type UseLiveActivityOptions,
23
+ type UseLiveActivityResult,
24
+ } from './live-activity/api.js'
25
+ export type { DismissalPolicy, LiveActivityVariants } from '@use-voltra/ios'
26
+ export {
27
+ clearPreloadedImages,
28
+ type PreloadImageOptions,
29
+ preloadImages,
30
+ type PreloadImagesResult,
31
+ reloadLiveActivities,
32
+ } from './preload.js'
33
+ export { assertRunningOnApple } from './utils/assertRunningOnApple.js'
34
+ export { useUpdateOnHMR } from './utils/useUpdateOnHMR.js'
35
+ export * from './utils/helpers.js'
36
+ export type { VoltraElementJson, VoltraNodeJson } from './types.js'
37
+ export {
38
+ clearWidgetServerCredentials,
39
+ setWidgetServerCredentials,
40
+ type WidgetServerCredentials,
41
+ } from './widgets/server-credentials.js'
42
+ export type { WidgetFamily, WidgetInfo, WidgetVariants } from '@use-voltra/ios'
43
+ export {
44
+ clearAllWidgets,
45
+ clearWidget,
46
+ getActiveWidgets,
47
+ reloadWidgets,
48
+ type ScheduledWidgetEntry,
49
+ scheduleWidget,
50
+ updateWidget,
51
+ type UpdateWidgetOptions,
52
+ } from './widgets/widget-api.js'
@@ -0,0 +1,224 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+
3
+ import { renderLiveActivityToString, type DismissalPolicy, type LiveActivityVariants } from '@use-voltra/ios'
4
+
5
+ import { addVoltraListener } from '../events.js'
6
+ import { logger } from '../logger.js'
7
+ import { assertRunningOnApple, useUpdateOnHMR } from '../utils/index.js'
8
+ import { getNativeVoltra } from '../VoltraModule.js'
9
+
10
+ export type SharedLiveActivityOptions = {
11
+ staleDate?: number
12
+ relevanceScore?: number
13
+ dismissalPolicy?: DismissalPolicy
14
+ }
15
+
16
+ export type StartLiveActivityOptions = {
17
+ activityName?: string
18
+ deepLinkUrl?: string
19
+ channelId?: string
20
+ } & SharedLiveActivityOptions
21
+
22
+ export type UpdateLiveActivityOptions = SharedLiveActivityOptions
23
+
24
+ export type EndLiveActivityOptions = {
25
+ dismissalPolicy?: DismissalPolicy
26
+ }
27
+
28
+ export type UseLiveActivityOptions = {
29
+ activityName?: string
30
+ autoStart?: boolean
31
+ autoUpdate?: boolean
32
+ deepLinkUrl?: string
33
+ channelId?: string
34
+ }
35
+
36
+ export type UseLiveActivityResult = {
37
+ start: (options?: StartLiveActivityOptions) => Promise<void>
38
+ update: (options?: UpdateLiveActivityOptions) => Promise<void>
39
+ end: (options?: EndLiveActivityOptions) => Promise<void>
40
+ isActive: boolean
41
+ }
42
+
43
+ const normalizeSharedLiveActivityOptions = (
44
+ options?: SharedLiveActivityOptions
45
+ ): SharedLiveActivityOptions | undefined => {
46
+ if (!options) return undefined
47
+
48
+ const normalizedOptions: SharedLiveActivityOptions = {}
49
+
50
+ if (options.staleDate !== undefined) {
51
+ if (options.staleDate < Date.now()) {
52
+ logger.warn('Ignoring staleDate because it is in the past, the Live Activity would be dismissed immediately.')
53
+ } else {
54
+ normalizedOptions.staleDate = options.staleDate
55
+ }
56
+ }
57
+
58
+ const relevanceScore = options.relevanceScore ?? 0.0
59
+ if (relevanceScore < 0 || relevanceScore > 1) {
60
+ logger.warn('Ignoring relevanceScore because it is out of range [0.0, 1.0], using default 0.0')
61
+ normalizedOptions.relevanceScore = 0.0
62
+ } else {
63
+ normalizedOptions.relevanceScore = relevanceScore
64
+ }
65
+
66
+ return Object.keys(normalizedOptions).length > 0 ? normalizedOptions : undefined
67
+ }
68
+
69
+ const normalizeEndLiveActivityOptions = (
70
+ options?: EndLiveActivityOptions
71
+ ): { dismissalPolicy?: { type: 'immediate' | 'after'; date?: number } } | undefined => {
72
+ if (!options?.dismissalPolicy) return undefined
73
+
74
+ if (typeof options.dismissalPolicy === 'string') {
75
+ if (options.dismissalPolicy === 'immediate') {
76
+ return { dismissalPolicy: { type: 'immediate' } }
77
+ }
78
+ } else if (typeof options.dismissalPolicy === 'object' && 'after' in options.dismissalPolicy) {
79
+ return { dismissalPolicy: { type: 'after', date: options.dismissalPolicy.after } }
80
+ }
81
+
82
+ return { dismissalPolicy: { type: 'immediate' } }
83
+ }
84
+
85
+ export const useLiveActivity = (
86
+ variants: LiveActivityVariants,
87
+ options?: UseLiveActivityOptions
88
+ ): UseLiveActivityResult => {
89
+ const [targetId, setTargetId] = useState<string | null>(() => {
90
+ if (options?.activityName) {
91
+ return isLiveActivityActive(options.activityName) ? options.activityName : null
92
+ }
93
+
94
+ return null
95
+ })
96
+ const isActive = targetId !== null
97
+ const optionsRef = useRef(options)
98
+ const variantsRef = useRef(variants)
99
+ const lastUpdateOptionsRef = useRef<UpdateLiveActivityOptions | undefined>(undefined)
100
+
101
+ useEffect(() => {
102
+ optionsRef.current = options
103
+ }, [options])
104
+
105
+ useEffect(() => {
106
+ variantsRef.current = variants
107
+ }, [variants])
108
+
109
+ useUpdateOnHMR()
110
+
111
+ const start = useCallback(async (options?: StartLiveActivityOptions) => {
112
+ const id = await startLiveActivity(variantsRef.current, { ...optionsRef.current, ...options })
113
+ setTargetId(id)
114
+ }, [])
115
+
116
+ const update = useCallback(
117
+ async (options?: UpdateLiveActivityOptions) => {
118
+ if (!targetId) {
119
+ return
120
+ }
121
+
122
+ const updateOptions = { ...optionsRef.current, ...options }
123
+ lastUpdateOptionsRef.current = updateOptions
124
+ await updateLiveActivity(targetId, variantsRef.current, updateOptions)
125
+ },
126
+ [targetId]
127
+ )
128
+
129
+ const end = useCallback(
130
+ async (options?: EndLiveActivityOptions) => {
131
+ if (!targetId) {
132
+ return
133
+ }
134
+
135
+ await stopLiveActivity(targetId, options)
136
+ setTargetId(null)
137
+ },
138
+ [targetId]
139
+ )
140
+
141
+ useEffect(() => {
142
+ if (!options?.autoStart) {
143
+ return
144
+ }
145
+
146
+ start()
147
+ }, [options?.autoStart, start])
148
+
149
+ useEffect(() => {
150
+ if (!options?.autoUpdate) return
151
+ update(lastUpdateOptionsRef.current)
152
+ }, [options?.autoUpdate, update, variants])
153
+
154
+ useEffect(() => {
155
+ if (!targetId) return
156
+
157
+ const subscription = addVoltraListener('stateChange', (event) => {
158
+ if (event.activityName !== targetId) return
159
+
160
+ if (event.activityState === 'dismissed' || event.activityState === 'ended') {
161
+ setTargetId(null)
162
+ }
163
+ })
164
+
165
+ return () => subscription.remove()
166
+ }, [targetId])
167
+
168
+ return {
169
+ start,
170
+ update,
171
+ end,
172
+ isActive,
173
+ }
174
+ }
175
+
176
+ export const startLiveActivity = async (
177
+ variants: LiveActivityVariants,
178
+ options?: StartLiveActivityOptions
179
+ ): Promise<string> => {
180
+ if (!assertRunningOnApple()) return Promise.resolve('')
181
+
182
+ const payload = renderLiveActivityToString(variants)
183
+
184
+ const normalizedSharedOptions = normalizeSharedLiveActivityOptions(options)
185
+ const targetId = await getNativeVoltra().startLiveActivity(payload, {
186
+ target: 'liveActivity',
187
+ deepLinkUrl: options?.deepLinkUrl,
188
+ activityName: options?.activityName,
189
+ channelId: options?.channelId,
190
+ ...normalizedSharedOptions,
191
+ })
192
+ return targetId
193
+ }
194
+
195
+ export const updateLiveActivity = async (
196
+ targetId: string,
197
+ variants: LiveActivityVariants,
198
+ options?: UpdateLiveActivityOptions
199
+ ): Promise<void> => {
200
+ if (!assertRunningOnApple()) return Promise.resolve()
201
+
202
+ const payload = renderLiveActivityToString(variants)
203
+
204
+ const normalizedSharedOptions = normalizeSharedLiveActivityOptions(options)
205
+ return getNativeVoltra().updateLiveActivity(targetId, payload, normalizedSharedOptions ?? {})
206
+ }
207
+
208
+ export const stopLiveActivity = async (targetId: string, options?: EndLiveActivityOptions): Promise<void> => {
209
+ if (!assertRunningOnApple()) return Promise.resolve()
210
+
211
+ const normalizedOptions = normalizeEndLiveActivityOptions(options)
212
+ return getNativeVoltra().endLiveActivity(targetId, normalizedOptions ?? { dismissalPolicy: { type: 'immediate' } })
213
+ }
214
+
215
+ export const isLiveActivityActive = (activityName: string): boolean => {
216
+ if (!assertRunningOnApple()) return false
217
+
218
+ return getNativeVoltra().isLiveActivityActive(activityName)
219
+ }
220
+
221
+ export async function endAllLiveActivities(): Promise<void> {
222
+ if (!assertRunningOnApple()) return Promise.resolve()
223
+ return getNativeVoltra().endAllLiveActivities()
224
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,35 @@
1
+ const LEVELS = {
2
+ debug: 0,
3
+ info: 1,
4
+ warn: 2,
5
+ error: 3,
6
+ } as const
7
+ export type VoltraLogLevel = keyof typeof LEVELS
8
+
9
+ let currentLevel: VoltraLogLevel = __DEV__ ? 'debug' : 'info'
10
+
11
+ export const logger = {
12
+ debug: (message: string) => {
13
+ if (LEVELS.debug >= LEVELS[currentLevel]) {
14
+ console.log(`[Voltra] ${message}`)
15
+ }
16
+ },
17
+ info: (message: string) => {
18
+ if (LEVELS.info >= LEVELS[currentLevel]) {
19
+ console.info(`[Voltra] ${message}`)
20
+ }
21
+ },
22
+ warn: (message: string) => {
23
+ if (LEVELS.warn >= LEVELS[currentLevel]) {
24
+ console.warn(`[Voltra] ${message}`)
25
+ }
26
+ },
27
+ error: (message: string) => {
28
+ if (LEVELS.error >= LEVELS[currentLevel]) {
29
+ console.error(`[Voltra] ${message}`)
30
+ }
31
+ },
32
+ setLevel: (level: VoltraLogLevel) => {
33
+ currentLevel = level
34
+ },
35
+ }
@@ -0,0 +1,118 @@
1
+ import type { CodegenTypes, TurboModule } from 'react-native'
2
+ import { TurboModuleRegistry } from 'react-native'
3
+
4
+ type VoltraActivityTokenReceivedEvent = Readonly<{
5
+ source: string
6
+ timestamp: number
7
+ type: 'activityTokenReceived'
8
+ activityName: string
9
+ pushToken: string
10
+ }>
11
+
12
+ type VoltraActivityPushToStartTokenReceivedEvent = Readonly<{
13
+ source: string
14
+ timestamp: number
15
+ type: 'activityPushToStartTokenReceived'
16
+ pushToStartToken: string
17
+ }>
18
+
19
+ type VoltraActivityUpdateEvent = Readonly<{
20
+ source: string
21
+ timestamp: number
22
+ type: 'stateChange'
23
+ activityName: string
24
+ activityState: string
25
+ }>
26
+
27
+ type VoltraInteractionEvent = Readonly<{
28
+ source: string
29
+ timestamp: number
30
+ type: 'interaction'
31
+ identifier: string
32
+ payload: string
33
+ }>
34
+
35
+ type StartVoltraOptions = Readonly<{
36
+ target?: string
37
+ deepLinkUrl?: string
38
+ activityName?: string
39
+ staleDate?: number
40
+ relevanceScore?: number
41
+ channelId?: string
42
+ }>
43
+
44
+ type UpdateVoltraOptions = Readonly<{
45
+ staleDate?: number
46
+ relevanceScore?: number
47
+ }>
48
+
49
+ type DismissalPolicyOptions = Readonly<{
50
+ type: string
51
+ date?: number
52
+ }>
53
+
54
+ type EndVoltraOptions = Readonly<{
55
+ dismissalPolicy?: DismissalPolicyOptions
56
+ }>
57
+
58
+ type UpdateWidgetOptions = Readonly<{
59
+ deepLinkUrl?: string
60
+ }>
61
+
62
+ type PreloadImageOptions = Readonly<{
63
+ url: string
64
+ key: string
65
+ method?: string
66
+ headers?: Readonly<{ [key: string]: string }>
67
+ }>
68
+
69
+ type PreloadImageFailure = Readonly<{
70
+ key: string
71
+ error: string
72
+ }>
73
+
74
+ type PreloadImagesResult = Readonly<{
75
+ succeeded: string[]
76
+ failed: PreloadImageFailure[]
77
+ }>
78
+
79
+ type WidgetServerCredentials = Readonly<{
80
+ token: string
81
+ headers?: Readonly<{ [key: string]: string }>
82
+ }>
83
+
84
+ export interface Spec extends TurboModule {
85
+ readonly onInteraction: CodegenTypes.EventEmitter<VoltraInteractionEvent>
86
+ readonly onStateChanged: CodegenTypes.EventEmitter<VoltraActivityUpdateEvent>
87
+ readonly onActivityTokenReceived: CodegenTypes.EventEmitter<VoltraActivityTokenReceivedEvent>
88
+ readonly onActivityPushToStartTokenReceived: CodegenTypes.EventEmitter<VoltraActivityPushToStartTokenReceivedEvent>
89
+ startLiveActivity(jsonString: string, options: StartVoltraOptions): Promise<string>
90
+ updateLiveActivity(activityId: string, jsonString: string, options: UpdateVoltraOptions): Promise<void>
91
+ endLiveActivity(activityId: string, options: EndVoltraOptions): Promise<void>
92
+ endAllLiveActivities(): Promise<void>
93
+ getLatestVoltraActivityId(): Promise<string | null>
94
+ listVoltraActivityIds(): Promise<string[]>
95
+ isLiveActivityActive(activityName: string): boolean
96
+ isHeadless(): boolean
97
+ preloadImages(images: PreloadImageOptions[]): Promise<PreloadImagesResult>
98
+ reloadLiveActivities(activityNames?: string[] | null): Promise<void>
99
+ clearPreloadedImages(keys?: string[] | null): Promise<void>
100
+ updateWidget(widgetId: string, jsonString: string, options: UpdateWidgetOptions): Promise<void>
101
+ scheduleWidget(widgetId: string, timelineJson: string): Promise<void>
102
+ reloadWidgets(widgetIds?: string[] | null): Promise<void>
103
+ clearWidget(widgetId: string): Promise<void>
104
+ clearAllWidgets(): Promise<void>
105
+ getActiveWidgets<T = unknown>(): Promise<T[]>
106
+ setWidgetServerCredentials(credentials: WidgetServerCredentials): Promise<void>
107
+ clearWidgetServerCredentials(): Promise<void>
108
+ }
109
+
110
+ export function getNativeVoltra(): Spec {
111
+ const voltraModule = TurboModuleRegistry.get<Spec>('NativeVoltra')
112
+
113
+ if (voltraModule == null) {
114
+ throw new Error('NativeVoltra is not available')
115
+ }
116
+
117
+ return voltraModule
118
+ }
@@ -0,0 +1,9 @@
1
+ import type { HostComponent, ViewProps } from 'react-native'
2
+ import { codegenNativeComponent } from 'react-native'
3
+
4
+ export interface NativeProps extends ViewProps {
5
+ payload: string
6
+ viewId: string
7
+ }
8
+
9
+ export default codegenNativeComponent<NativeProps>('VoltraView') as HostComponent<NativeProps>
package/src/preload.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { Platform } from 'react-native'
2
+
3
+ import type { PreloadImageOptions, PreloadImagesResult } from './types.js'
4
+ import { getNativeVoltra } from './VoltraModule.js'
5
+
6
+ export type { PreloadImageOptions, PreloadImagesResult } from './types.js'
7
+
8
+ function assertIOS(name: string): boolean {
9
+ const isIOS = Platform.OS === 'ios'
10
+ if (!isIOS) console.error(`${name} is only available on iOS`)
11
+ return isIOS
12
+ }
13
+
14
+ export async function preloadImages(images: PreloadImageOptions[]): Promise<PreloadImagesResult> {
15
+ if (!assertIOS('preloadImages')) {
16
+ return { succeeded: [], failed: images.map((img) => ({ key: img.key, error: 'Not available on this platform' })) }
17
+ }
18
+
19
+ try {
20
+ return await getNativeVoltra().preloadImages(images)
21
+ } catch (error) {
22
+ return {
23
+ succeeded: [],
24
+ failed: images.map((img) => ({
25
+ key: img.key,
26
+ error: error instanceof Error ? error.message : 'Unknown error',
27
+ })),
28
+ }
29
+ }
30
+ }
31
+
32
+ export async function reloadLiveActivities(activityNames?: string[]): Promise<void> {
33
+ if (!assertIOS('reloadLiveActivities')) return
34
+
35
+ try {
36
+ await getNativeVoltra().reloadLiveActivities(activityNames ?? null)
37
+ } catch (error) {
38
+ console.error('Failed to reload Live Activities:', error)
39
+ }
40
+ }
41
+
42
+ export async function clearPreloadedImages(keys?: string[]): Promise<void> {
43
+ if (!assertIOS('clearPreloadedImages')) return
44
+
45
+ try {
46
+ await getNativeVoltra().clearPreloadedImages(keys ?? null)
47
+ } catch (error) {
48
+ console.error('Failed to clear preloaded images:', error)
49
+ }
50
+ }
package/src/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type {
2
+ EventSubscription,
3
+ PreloadImageFailure,
4
+ PreloadImageOptions,
5
+ PreloadImagesResult,
6
+ UpdateWidgetOptions,
7
+ VoltraElementJson,
8
+ VoltraElementRef,
9
+ VoltraNodeJson,
10
+ VoltraPropValue,
11
+ WidgetServerCredentials,
12
+ } from '@use-voltra/ios'
@@ -0,0 +1,10 @@
1
+ import { Platform } from 'react-native'
2
+
3
+ export const assertRunningOnApple = (): boolean => {
4
+ if (Platform.OS !== 'ios') {
5
+ console.error(`Voltra is available only on iOS!`)
6
+ return false
7
+ }
8
+
9
+ return true
10
+ }
@@ -0,0 +1 @@
1
+ export { isGlassSupported, isHeadless } from '../helpers.js'
@@ -0,0 +1,2 @@
1
+ export * from './assertRunningOnApple.js'
2
+ export * from './useUpdateOnHMR.js'
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ declare global {
4
+ var __accept: (...args: unknown[]) => void
5
+ }
6
+
7
+ export const useUpdateOnHMR = () => {
8
+ const [, forceUpdate] = useState(0)
9
+
10
+ useEffect(() => {
11
+ if (!__DEV__) {
12
+ return
13
+ }
14
+
15
+ const oldAccept = global['__accept']
16
+ global['__accept'] = (...args) => {
17
+ forceUpdate((prev) => prev + 1)
18
+ oldAccept?.(...args)
19
+ }
20
+
21
+ return () => {
22
+ global['__accept'] = oldAccept
23
+ }
24
+ }, [])
25
+ }
@@ -0,0 +1,16 @@
1
+ import type { WidgetServerCredentials } from '../types.js'
2
+ import { getNativeVoltra } from '../VoltraModule.js'
3
+
4
+ export type { WidgetServerCredentials } from '../types.js'
5
+
6
+ export async function setWidgetServerCredentials(credentials: WidgetServerCredentials): Promise<void> {
7
+ if (!credentials.token) {
8
+ throw new Error('[Voltra][iOS] setWidgetServerCredentials: token is required')
9
+ }
10
+
11
+ return getNativeVoltra().setWidgetServerCredentials(credentials)
12
+ }
13
+
14
+ export async function clearWidgetServerCredentials(): Promise<void> {
15
+ return getNativeVoltra().clearWidgetServerCredentials()
16
+ }
@@ -0,0 +1,65 @@
1
+ import { renderWidgetToString, type ScheduledWidgetEntry, type WidgetInfo, type WidgetVariants } from '@use-voltra/ios'
2
+
3
+ import type { UpdateWidgetOptions } from '../types.js'
4
+ import { assertRunningOnApple } from '../utils/assertRunningOnApple.js'
5
+ import { getNativeVoltra } from '../VoltraModule.js'
6
+
7
+ export type { UpdateWidgetOptions } from '../types.js'
8
+ export type { ScheduledWidgetEntry, WidgetInfo } from '@use-voltra/ios'
9
+
10
+ export const updateWidget = async (
11
+ widgetId: string,
12
+ variants: WidgetVariants,
13
+ options?: UpdateWidgetOptions
14
+ ): Promise<void> => {
15
+ if (!assertRunningOnApple()) return Promise.resolve()
16
+
17
+ const payload = renderWidgetToString(variants)
18
+
19
+ return getNativeVoltra().updateWidget(widgetId, payload, {
20
+ deepLinkUrl: options?.deepLinkUrl,
21
+ })
22
+ }
23
+
24
+ export const reloadWidgets = async (widgetIds?: string[]): Promise<void> => {
25
+ if (!assertRunningOnApple()) return Promise.resolve()
26
+
27
+ return getNativeVoltra().reloadWidgets(widgetIds ?? null)
28
+ }
29
+
30
+ export const clearWidget = async (widgetId: string): Promise<void> => {
31
+ if (!assertRunningOnApple()) return Promise.resolve()
32
+
33
+ return getNativeVoltra().clearWidget(widgetId)
34
+ }
35
+
36
+ export const clearAllWidgets = async (): Promise<void> => {
37
+ if (!assertRunningOnApple()) return Promise.resolve()
38
+
39
+ return getNativeVoltra().clearAllWidgets()
40
+ }
41
+
42
+ export const scheduleWidget = async (widgetId: string, entries: ScheduledWidgetEntry[]): Promise<void> => {
43
+ if (!assertRunningOnApple()) return Promise.resolve()
44
+
45
+ const renderedEntries = entries.map((entry) => ({
46
+ date: entry.date.getTime(),
47
+ json: renderWidgetToString(entry.variants),
48
+ deepLinkUrl: entry.deepLinkUrl,
49
+ }))
50
+
51
+ const timelineData = {
52
+ entries: renderedEntries,
53
+ policy: { type: 'never' },
54
+ }
55
+
56
+ const timelineJson = JSON.stringify(timelineData)
57
+
58
+ return getNativeVoltra().scheduleWidget(widgetId, timelineJson)
59
+ }
60
+
61
+ export const getActiveWidgets = async (): Promise<WidgetInfo[]> => {
62
+ if (!assertRunningOnApple()) return []
63
+
64
+ return getNativeVoltra().getActiveWidgets()
65
+ }
@@ -1,8 +0,0 @@
1
- This directory contains auto-generated Swift parameter files.
2
- DO NOT EDIT MANUALLY.
3
-
4
- Generated from: data/components.json
5
- Schema version: 1.0.0
6
-
7
- To regenerate these files, run:
8
- npm run generate