@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 +17 -4
- package/src/VoltraModule.ts +2 -0
- package/src/components/VoltraLiveActivityPreview.tsx +29 -0
- package/src/components/VoltraView.tsx +49 -0
- package/src/components/VoltraWidgetPreview.tsx +38 -0
- package/src/events.ts +70 -0
- package/src/helpers.ts +21 -0
- package/src/index.ts +52 -0
- package/src/live-activity/api.ts +224 -0
- package/src/logger.ts +35 -0
- package/src/native/NativeVoltra.ts +118 -0
- package/src/native/VoltraRNNativeComponent.ts +9 -0
- package/src/preload.ts +50 -0
- package/src/types.ts +12 -0
- package/src/utils/assertRunningOnApple.ts +10 -0
- package/src/utils/helpers.ts +1 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/useUpdateOnHMR.ts +25 -0
- package/src/widgets/server-credentials.ts +16 -0
- package/src/widgets/widget-api.ts +65 -0
- package/ios/ui/Generated/Parameters/.generated +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@use-voltra/ios-client",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
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.
|
|
51
|
-
"@use-voltra/ios": "2.0.0-rc.
|
|
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,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 @@
|
|
|
1
|
+
export { isGlassSupported, isHeadless } from '../helpers.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
|
+
}
|