diff --git a/.oxlintrc.json b/.oxlintrc.json index 9bd4aca7..5dec186d 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,5 +1,389 @@ { "plugins": ["typescript", "react", "oxc"], + "overrides": [ + { + "files": ["packages/core/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "expo", "message": "@use-voltra/core must stay platform-agnostic." }, + { "name": "react-native", "message": "@use-voltra/core must stay platform-agnostic." }, + { "name": "voltra", "message": "@use-voltra/core cannot depend on facade packages." }, + { "name": "@use-voltra/server", "message": "@use-voltra/core cannot depend on server packages." }, + { "name": "@use-voltra/ios", "message": "@use-voltra/core cannot depend on platform packages." }, + { "name": "@use-voltra/android", "message": "@use-voltra/core cannot depend on platform packages." }, + { "name": "@use-voltra/ios-client", "message": "@use-voltra/core cannot depend on client packages." }, + { "name": "@use-voltra/android-client", "message": "@use-voltra/core cannot depend on client packages." }, + { "name": "@use-voltra/ios-server", "message": "@use-voltra/core cannot depend on server packages." }, + { "name": "@use-voltra/android-server", "message": "@use-voltra/core cannot depend on server packages." }, + { "name": "@use-voltra/expo-plugin", "message": "@use-voltra/core cannot depend on tooling packages." }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + }, + { "name": "@use-voltra/android/internal", "message": "Private internal APIs are not allowed here." } + ] + } + ] + } + }, + { + "files": ["packages/server/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "expo", "message": "@use-voltra/server must stay server-safe." }, + { "name": "react-native", "message": "@use-voltra/server must stay server-safe." }, + { "name": "voltra", "message": "@use-voltra/server cannot depend on the facade package." }, + { "name": "@use-voltra/ios-client", "message": "@use-voltra/server cannot depend on client packages." }, + { + "name": "@use-voltra/android-client", + "message": "@use-voltra/server cannot depend on client packages." + }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + }, + { "name": "@use-voltra/android/internal", "message": "Private internal APIs are not allowed here." } + ] + } + ] + } + }, + { + "files": ["packages/ios/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "expo", "message": "@use-voltra/ios must stay server-safe." }, + { "name": "react-native", "message": "@use-voltra/ios must stay server-safe." }, + { "name": "voltra", "message": "@use-voltra/ios cannot depend on the facade package." }, + { "name": "@use-voltra/server", "message": "@use-voltra/ios cannot depend on server utilities." }, + { "name": "@use-voltra/android", "message": "@use-voltra/ios cannot depend on Android packages." }, + { "name": "@use-voltra/ios-client", "message": "@use-voltra/ios cannot depend on client packages." }, + { "name": "@use-voltra/android-client", "message": "@use-voltra/ios cannot depend on client packages." }, + { + "name": "@use-voltra/ios-server", + "message": "@use-voltra/ios cannot depend on server renderer packages." + }, + { + "name": "@use-voltra/android-server", + "message": "@use-voltra/ios cannot depend on Android server packages." + }, + { "name": "@use-voltra/expo-plugin", "message": "@use-voltra/ios cannot depend on tooling packages." }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/internal", + "message": "Private Android internals are only allowed through the facade." + } + ] + } + ] + } + }, + { + "files": ["packages/android/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "expo", "message": "@use-voltra/android must stay server-safe." }, + { "name": "react-native", "message": "@use-voltra/android must stay server-safe." }, + { "name": "voltra", "message": "@use-voltra/android cannot depend on the facade package." }, + { "name": "@use-voltra/server", "message": "@use-voltra/android cannot depend on server utilities." }, + { "name": "@use-voltra/ios", "message": "@use-voltra/android cannot depend on iOS packages." }, + { "name": "@use-voltra/ios-client", "message": "@use-voltra/android cannot depend on client packages." }, + { + "name": "@use-voltra/android-client", + "message": "@use-voltra/android cannot depend on client packages." + }, + { + "name": "@use-voltra/ios-server", + "message": "@use-voltra/android cannot depend on iOS server packages." + }, + { + "name": "@use-voltra/android-server", + "message": "@use-voltra/android cannot depend on server renderer packages." + }, + { + "name": "@use-voltra/expo-plugin", + "message": "@use-voltra/android cannot depend on tooling packages." + }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/internal", + "message": "Private Android internals are only allowed through the facade." + } + ] + } + ] + } + }, + { + "files": ["packages/ios-client/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "voltra", "message": "@use-voltra/ios-client cannot depend on the facade package." }, + { + "name": "@use-voltra/core", + "message": "@use-voltra/ios-client should consume the iOS package surface." + }, + { "name": "@use-voltra/server", "message": "@use-voltra/ios-client cannot depend on server utilities." }, + { "name": "@use-voltra/android", "message": "@use-voltra/ios-client cannot depend on Android packages." }, + { + "name": "@use-voltra/android-client", + "message": "@use-voltra/ios-client cannot depend on Android client packages." + }, + { + "name": "@use-voltra/ios-server", + "message": "@use-voltra/ios-client cannot depend on server renderer packages." + }, + { + "name": "@use-voltra/android-server", + "message": "@use-voltra/ios-client cannot depend on Android server packages." + }, + { + "name": "@use-voltra/expo-plugin", + "message": "@use-voltra/ios-client cannot depend on tooling packages." + }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client directly instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client directly instead of legacy client subpaths." + }, + { "name": "@use-voltra/android/internal", "message": "Private Android internals are not allowed here." } + ] + } + ] + } + }, + { + "files": ["packages/android-client/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "voltra", "message": "@use-voltra/android-client cannot depend on the facade package." }, + { + "name": "@use-voltra/core", + "message": "@use-voltra/android-client should consume the Android package surface." + }, + { + "name": "@use-voltra/server", + "message": "@use-voltra/android-client cannot depend on server utilities." + }, + { "name": "@use-voltra/ios", "message": "@use-voltra/android-client cannot depend on iOS packages." }, + { + "name": "@use-voltra/ios-client", + "message": "@use-voltra/android-client cannot depend on iOS client packages." + }, + { + "name": "@use-voltra/ios-server", + "message": "@use-voltra/android-client cannot depend on iOS server packages." + }, + { + "name": "@use-voltra/android-server", + "message": "@use-voltra/android-client cannot depend on server renderer packages." + }, + { + "name": "@use-voltra/expo-plugin", + "message": "@use-voltra/android-client cannot depend on tooling packages." + }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client directly instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client directly instead of legacy client subpaths." + }, + { "name": "@use-voltra/android/internal", "message": "Private Android internals are not allowed here." } + ] + } + ] + } + }, + { + "files": ["packages/ios-server/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "expo", "message": "@use-voltra/ios-server must stay server-safe." }, + { "name": "react-native", "message": "@use-voltra/ios-server must stay server-safe." }, + { "name": "voltra", "message": "@use-voltra/ios-server cannot depend on the facade package." }, + { "name": "@use-voltra/android", "message": "@use-voltra/ios-server cannot depend on Android packages." }, + { + "name": "@use-voltra/ios-client", + "message": "@use-voltra/ios-server cannot depend on client packages." + }, + { + "name": "@use-voltra/android-client", + "message": "@use-voltra/ios-server cannot depend on client packages." + }, + { + "name": "@use-voltra/android-server", + "message": "@use-voltra/ios-server cannot depend on Android server packages." + }, + { + "name": "@use-voltra/expo-plugin", + "message": "@use-voltra/ios-server cannot depend on tooling packages." + }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + }, + { "name": "@use-voltra/android/internal", "message": "Private Android internals are not allowed here." } + ] + } + ] + } + }, + { + "files": ["packages/android-server/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "expo", "message": "@use-voltra/android-server must stay server-safe." }, + { "name": "react-native", "message": "@use-voltra/android-server must stay server-safe." }, + { "name": "voltra", "message": "@use-voltra/android-server cannot depend on the facade package." }, + { "name": "@use-voltra/ios", "message": "@use-voltra/android-server cannot depend on iOS packages." }, + { + "name": "@use-voltra/ios-client", + "message": "@use-voltra/android-server cannot depend on client packages." + }, + { + "name": "@use-voltra/android-client", + "message": "@use-voltra/android-server cannot depend on client packages." + }, + { + "name": "@use-voltra/ios-server", + "message": "@use-voltra/android-server cannot depend on iOS server packages." + }, + { + "name": "@use-voltra/expo-plugin", + "message": "@use-voltra/android-server cannot depend on tooling packages." + }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + }, + { "name": "@use-voltra/android/internal", "message": "Private Android internals are not allowed here." } + ] + } + ] + } + }, + { + "files": ["packages/expo-plugin/src/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { "name": "voltra", "message": "@use-voltra/expo-plugin must stay installation-time only." }, + { "name": "@use-voltra/core", "message": "@use-voltra/expo-plugin must stay installation-time only." }, + { "name": "@use-voltra/server", "message": "@use-voltra/expo-plugin must stay installation-time only." }, + { "name": "@use-voltra/ios", "message": "@use-voltra/expo-plugin must stay installation-time only." }, + { "name": "@use-voltra/android", "message": "@use-voltra/expo-plugin must stay installation-time only." }, + { + "name": "@use-voltra/ios-client", + "message": "@use-voltra/expo-plugin must stay installation-time only." + }, + { + "name": "@use-voltra/android-client", + "message": "@use-voltra/expo-plugin must stay installation-time only." + }, + { + "name": "@use-voltra/ios-server", + "message": "@use-voltra/expo-plugin must stay installation-time only." + }, + { + "name": "@use-voltra/android-server", + "message": "@use-voltra/expo-plugin must stay installation-time only." + }, + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + }, + { "name": "@use-voltra/android/internal", "message": "Private Android internals are not allowed here." } + ] + } + ] + } + }, + { + "files": ["packages/voltra/src/**/*.{ts,tsx,js,jsx}", "packages/voltra/generator/**/*.{ts,tsx,js,jsx}"], + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@use-voltra/ios/client", + "message": "Use @use-voltra/ios-client instead of legacy client subpaths." + }, + { + "name": "@use-voltra/android/client", + "message": "Use @use-voltra/android-client instead of legacy client subpaths." + } + ] + } + ] + } + } + ], "ignorePatterns": [ "build/**", "plugin/build/**", diff --git a/package-lock.json b/package-lock.json index 01b1172b..b92152dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8456,6 +8456,10 @@ "resolved": "packages/android", "link": true }, + "node_modules/@use-voltra/android-client": { + "resolved": "packages/android-client", + "link": true + }, "node_modules/@use-voltra/android-server": { "resolved": "packages/android-server", "link": true @@ -8472,6 +8476,10 @@ "resolved": "packages/ios", "link": true }, + "node_modules/@use-voltra/ios-client": { + "resolved": "packages/ios-client", + "link": true + }, "node_modules/@use-voltra/ios-server": { "resolved": "packages/ios-server", "link": true @@ -19968,6 +19976,17 @@ "dependencies": { "@use-voltra/core": "1.3.1" }, + "peerDependencies": { + "react": "*" + } + }, + "packages/android-client": { + "name": "@use-voltra/android-client", + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "@use-voltra/android": "1.3.1" + }, "peerDependencies": { "expo": "*", "react": "*", @@ -19995,8 +20014,7 @@ "react-is": "^19.2.0" }, "peerDependencies": { - "react": "*", - "react-native": "*" + "react": "*" } }, "packages/expo-plugin": { @@ -20024,6 +20042,17 @@ "dependencies": { "@use-voltra/core": "1.3.1" }, + "peerDependencies": { + "react": "*" + } + }, + "packages/ios-client": { + "name": "@use-voltra/ios-client", + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "@use-voltra/ios": "1.3.1" + }, "peerDependencies": { "expo": "*", "react": "*", @@ -20053,10 +20082,12 @@ "license": "MIT", "dependencies": { "@use-voltra/android": "1.3.1", + "@use-voltra/android-client": "1.3.1", "@use-voltra/android-server": "1.3.1", "@use-voltra/core": "1.3.1", "@use-voltra/expo-plugin": "1.3.1", "@use-voltra/ios": "1.3.1", + "@use-voltra/ios-client": "1.3.1", "@use-voltra/ios-server": "1.3.1", "@use-voltra/server": "1.3.1", "react-is": "^19.2.0" diff --git a/packages/android-client/package.json b/packages/android-client/package.json new file mode 100644 index 00000000..c9382d8d --- /dev/null +++ b/packages/android-client/package.json @@ -0,0 +1,53 @@ +{ + "name": "@use-voltra/android-client", + "version": "1.3.1", + "description": "Client-only Voltra APIs for Android", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "exports": { + ".": { + "types": "./build/types/index.d.ts", + "require": "./build/cjs/index.js", + "import": "./build/esm/index.js", + "default": "./build/esm/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "build", + "README.md" + ], + "scripts": { + "build": "node ../../scripts/build-package.mjs packages/android-client", + "clean": "rm -rf build", + "lint": "oxlint src", + "typecheck": "tsc -p tsconfig.typecheck.json --noEmit" + }, + "dependencies": { + "@use-voltra/android": "1.3.1" + }, + "keywords": [ + "react-native", + "expo", + "voltra", + "android", + "widget" + ], + "author": "Saúl Sharma (https://x.com/saul_sharma), Szymon Chmal (https://x.com/chmalszymon)", + "repository": { + "type": "git", + "url": "git+https://github.com/callstackincubator/voltra.git", + "directory": "packages/android-client" + }, + "bugs": { + "url": "https://github.com/callstackincubator/voltra/issues" + }, + "license": "MIT", + "homepage": "https://use-voltra.dev", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } +} diff --git a/packages/android/src/VoltraModule.ts b/packages/android-client/src/VoltraModule.ts similarity index 87% rename from packages/android/src/VoltraModule.ts rename to packages/android-client/src/VoltraModule.ts index 4d749bc0..2e859e20 100644 --- a/packages/android/src/VoltraModule.ts +++ b/packages/android-client/src/VoltraModule.ts @@ -3,10 +3,15 @@ import { requireNativeModule } from 'expo' import type { StartAndroidOngoingNotificationOptions, UpdateAndroidOngoingNotificationOptions, -} from './live-update/types.js' +} from '@use-voltra/android' import type { EventSubscription, PreloadImageOptions, PreloadImagesResult, WidgetServerCredentials } from './types.js' export interface VoltraAndroidModuleSpec { + startAndroidLiveUpdate(payload: string, options: { updateName?: string; channelId?: string }): Promise + updateAndroidLiveUpdate(notificationId: string, payload: string): Promise + stopAndroidLiveUpdate(notificationId: string): Promise + isAndroidLiveUpdateActive(updateName: string): boolean + endAllAndroidLiveUpdates(): Promise startAndroidOngoingNotification( payload: string, options: StartAndroidOngoingNotificationOptions diff --git a/packages/android/src/components/VoltraView.tsx b/packages/android-client/src/components/VoltraView.tsx similarity index 53% rename from packages/android/src/components/VoltraView.tsx rename to packages/android-client/src/components/VoltraView.tsx index f951b073..0e49b361 100644 --- a/packages/android/src/components/VoltraView.tsx +++ b/packages/android-client/src/components/VoltraView.tsx @@ -1,52 +1,31 @@ import { requireNativeView } from 'expo' -import React, { ReactNode, useEffect, useMemo } from 'react' -import { StyleProp, ViewStyle } from 'react-native' +import React, { type ReactNode, useEffect, useMemo } from 'react' +import { type StyleProp, type ViewStyle } from 'react-native' -import { addVoltraListener, VoltraInteractionEvent } from '../events.js' -import { renderAndroidViewToJson } from '../widgets/renderer.js' +import { renderAndroidViewToJson } from '@use-voltra/android' + +import { addVoltraListener, type VoltraInteractionEvent } from '../events.js' const NativeVoltraView = requireNativeView('VoltraModule') -// Generate a unique ID for views that don't have one const generateViewId = () => `voltra-view-android-${Date.now()}-${Math.random().toString(36).substring(2, 9)}` export type VoltraViewProps = { - /** - * Unique identifier for this view instance. - * Used as 'source' in interaction events to identify which view triggered the event. - * If not provided, a unique ID will be generated automatically. - */ id?: string - /** - * Voltra JSX components to render - */ children: ReactNode - /** - * Style for the view container - */ style?: StyleProp - /** - * Callback when user interacts with components in the view. - * Events are filtered by this view's id (source). - */ onInteraction?: (event: VoltraInteractionEvent) => void } -/** - * A React Native component that renders Voltra UI for Android using Jetpack Compose. - */ export function VoltraView({ id, children, style, onInteraction }: VoltraViewProps) { - // Generate a stable ID if not provided const viewId = useMemo(() => id || generateViewId(), [id]) const payload = useMemo(() => JSON.stringify(renderAndroidViewToJson(children)), [children]) - // Subscribe to interaction events and filter by this view's ID useEffect(() => { if (!onInteraction) return const subscription = addVoltraListener('interaction', (event) => { - // Only forward events from this view if (event.source === viewId) { onInteraction({ type: 'interaction', diff --git a/packages/android/src/components/VoltraWidgetPreview.tsx b/packages/android-client/src/components/VoltraWidgetPreview.tsx similarity index 73% rename from packages/android/src/components/VoltraWidgetPreview.tsx rename to packages/android-client/src/components/VoltraWidgetPreview.tsx index 54baf59d..7f5760d9 100644 --- a/packages/android/src/components/VoltraWidgetPreview.tsx +++ b/packages/android-client/src/components/VoltraWidgetPreview.tsx @@ -1,11 +1,8 @@ import React from 'react' -import { StyleProp, ViewStyle } from 'react-native' +import { type StyleProp, type ViewStyle } from 'react-native' -import { VoltraView, VoltraViewProps } from './VoltraView.js' +import { VoltraView, type VoltraViewProps } from './VoltraView.js' -/** - * Android-specific widget sizes in dp. - */ export type AndroidWidgetFamily = 'small' | 'mediumSquare' | 'mediumWide' | 'mediumTall' | 'large' | 'extraLarge' const WIDGET_DIMENSIONS: Record = { @@ -18,19 +15,10 @@ const WIDGET_DIMENSIONS: Record & { - /** - * Android widget size to preview - */ family: AndroidWidgetFamily - /** - * Additional styles to apply on top of the widget dimensions - */ style?: StyleProp } -/** - * A preview component that renders Voltra Android JSX content at specific dimensions. - */ export function VoltraWidgetPreview({ family, style, children, ...voltraViewProps }: VoltraWidgetPreviewProps) { const dimensions = WIDGET_DIMENSIONS[family] const previewStyle: StyleProp = [ diff --git a/packages/ios/src/events.ts b/packages/android-client/src/events.ts similarity index 70% rename from packages/ios/src/events.ts rename to packages/android-client/src/events.ts index eb84787f..db0f7c93 100644 --- a/packages/ios/src/events.ts +++ b/packages/android-client/src/events.ts @@ -41,22 +41,6 @@ export type VoltraEventMap = { interaction: VoltraInteractionEvent } -/** - * Add a listener for Voltra events. - * - * Supported events: - * - `interaction`: User interactions with widgets (buttons, switches, checkboxes) (iOS only) - * - `stateChange`: Live Activity state changes (iOS only) - * - `activityTokenReceived`: Push token for Live Activity (iOS only) - * - `activityPushToStartTokenReceived`: Push-to-start token (iOS only) - * - * Note: On Android, interactions open the app directly (optionally via deep links) - * instead of emitting background events. - * - * @param event The event type to listen for - * @param listener Callback function to handle the event - * @returns EventSubscription with a remove() method to unsubscribe - */ export function addVoltraListener( event: K, listener: (event: VoltraEventMap[K]) => void diff --git a/packages/android/src/client.ts b/packages/android-client/src/index.ts similarity index 78% rename from packages/android/src/client.ts rename to packages/android-client/src/index.ts index 8e7e1f40..8ae5a392 100644 --- a/packages/android/src/client.ts +++ b/packages/android-client/src/index.ts @@ -1,6 +1,21 @@ -export { AndroidOngoingNotification } from './live-update/components.js' - -// Android ongoing notification API and types +export { + endAllAndroidLiveUpdates, + isAndroidLiveUpdateActive, + startAndroidLiveUpdate, + stopAndroidLiveUpdate, + updateAndroidLiveUpdate, + useAndroidLiveUpdate, +} from './live-update/api.js' +export type { + AndroidLiveUpdateJson, + AndroidLiveUpdateVariants, + AndroidLiveUpdateVariantsJson, + StartAndroidLiveUpdateOptions, + UpdateAndroidLiveUpdateOptions, + UseAndroidLiveUpdateOptions, + UseAndroidLiveUpdateResult, +} from './live-update/types.js' +export { AndroidOngoingNotification, renderAndroidOngoingNotificationPayload } from '@use-voltra/android' export { canPostPromotedAndroidNotifications, endAllAndroidOngoingNotifications, @@ -9,14 +24,13 @@ export { hasAndroidNotificationPermission, isAndroidOngoingNotificationActive, openAndroidNotificationSettings, - renderAndroidOngoingNotificationPayload, requestAndroidNotificationPermission, startAndroidOngoingNotification, stopAndroidOngoingNotification, upsertAndroidOngoingNotification, updateAndroidOngoingNotification, useAndroidOngoingNotification, -} from './live-update/api.js' +} from './ongoing-notification/api.js' export type { AndroidOngoingNotificationActionPayload, AndroidOngoingNotificationActionProps, @@ -41,9 +55,7 @@ export type { UpdateAndroidOngoingNotificationOptions, UseAndroidOngoingNotificationOptions, UseAndroidOngoingNotificationResult, -} from './live-update/types.js' - -// Android Widget API and types +} from '@use-voltra/android' export { clearAllAndroidWidgets, clearAndroidWidget, @@ -58,19 +70,14 @@ export type { AndroidWidgetVariants, UpdateAndroidWidgetOptions, WidgetInfo, -} from './widgets/types.js' - -// Android Widget Server Credentials API +} from '@use-voltra/android' export { clearWidgetServerCredentials, setWidgetServerCredentials, type WidgetServerCredentials, } from './widgets/server-credentials.js' - -// Preload API export { clearPreloadedImages, preloadImages, reloadWidgets } from './preload.js' - -// Android Preview Components +export * from './events.js' export { VoltraView, type VoltraViewProps } from './components/VoltraView.js' export { type AndroidWidgetFamily, diff --git a/packages/android-client/src/live-update/api.ts b/packages/android-client/src/live-update/api.ts new file mode 100644 index 00000000..ad9c77d6 --- /dev/null +++ b/packages/android-client/src/live-update/api.ts @@ -0,0 +1,125 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +import { renderAndroidLiveUpdateToString, type AndroidLiveUpdateVariants } from '@use-voltra/android' + +import VoltraModule from '../VoltraModule.js' +import { useUpdateOnHMR } from '../utils/index.js' +import type { + StartAndroidLiveUpdateOptions, + UpdateAndroidLiveUpdateOptions, + UseAndroidLiveUpdateOptions, + UseAndroidLiveUpdateResult, +} from './types.js' + +export const useAndroidLiveUpdate = ( + variants: AndroidLiveUpdateVariants, + options?: UseAndroidLiveUpdateOptions +): UseAndroidLiveUpdateResult => { + const [targetId, setTargetId] = useState(() => { + if (options?.updateName) { + return isAndroidLiveUpdateActive(options.updateName) ? options.updateName : null + } + return null + }) + + const isActive = targetId !== null + const optionsRef = useRef(options) + const variantsRef = useRef(variants) + const lastUpdateOptionsRef = useRef(undefined) + + useEffect(() => { + optionsRef.current = options + }, [options]) + + useEffect(() => { + variantsRef.current = variants + }, [variants]) + + useUpdateOnHMR() + + const start = useCallback(async (options?: StartAndroidLiveUpdateOptions) => { + const id = await startAndroidLiveUpdate(variantsRef.current, { + ...optionsRef.current, + ...options, + }) + setTargetId(id) + }, []) + + const update = useCallback( + async (options?: UpdateAndroidLiveUpdateOptions) => { + if (!targetId) { + return + } + + const updateOptions = { ...optionsRef.current, ...options } + lastUpdateOptionsRef.current = updateOptions + await updateAndroidLiveUpdate(targetId, variantsRef.current, updateOptions) + }, + [targetId] + ) + + const end = useCallback(async () => { + if (!targetId) { + return + } + + await stopAndroidLiveUpdate(targetId) + setTargetId(null) + }, [targetId]) + + useEffect(() => { + if (!options?.autoStart) { + return + } + + start() + }, [options?.autoStart, start]) + + useEffect(() => { + if (!options?.autoUpdate) return + update(lastUpdateOptionsRef.current) + }, [options?.autoUpdate, update, variants]) + + return { + start, + update, + end, + isActive, + } +} + +export const startAndroidLiveUpdate = async ( + variants: AndroidLiveUpdateVariants, + options?: StartAndroidLiveUpdateOptions +): Promise => { + const payload = renderAndroidLiveUpdateToString(variants) + + const notificationId = await VoltraModule.startAndroidLiveUpdate(payload, { + updateName: options?.updateName, + channelId: options?.channelId || variants.channelId || 'voltra_live_updates', + }) + + return notificationId +} + +export const updateAndroidLiveUpdate = async ( + notificationId: string, + variants: AndroidLiveUpdateVariants, + _options?: UpdateAndroidLiveUpdateOptions +): Promise => { + const payload = renderAndroidLiveUpdateToString(variants) + + return VoltraModule.updateAndroidLiveUpdate(notificationId, payload) +} + +export const stopAndroidLiveUpdate = async (notificationId: string): Promise => { + return VoltraModule.stopAndroidLiveUpdate(notificationId) +} + +export const isAndroidLiveUpdateActive = (updateName: string): boolean => { + return VoltraModule.isAndroidLiveUpdateActive(updateName) +} + +export async function endAllAndroidLiveUpdates(): Promise { + return VoltraModule.endAllAndroidLiveUpdates() +} diff --git a/packages/android-client/src/live-update/types.ts b/packages/android-client/src/live-update/types.ts new file mode 100644 index 00000000..106d3877 --- /dev/null +++ b/packages/android-client/src/live-update/types.ts @@ -0,0 +1,9 @@ +export type { + AndroidLiveUpdateJson, + AndroidLiveUpdateVariants, + AndroidLiveUpdateVariantsJson, + StartAndroidLiveUpdateOptions, + UpdateAndroidLiveUpdateOptions, + UseAndroidLiveUpdateOptions, + UseAndroidLiveUpdateResult, +} from '@use-voltra/android' diff --git a/packages/android/src/live-update/api.ts b/packages/android-client/src/ongoing-notification/api.ts similarity index 65% rename from packages/android/src/live-update/api.ts rename to packages/android-client/src/ongoing-notification/api.ts index 14ffa53a..06cddbe8 100644 --- a/packages/android/src/live-update/api.ts +++ b/packages/android-client/src/ongoing-notification/api.ts @@ -1,25 +1,26 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { PermissionsAndroid, Platform } from 'react-native' +import { + renderAndroidOngoingNotificationPayload, + type AndroidOngoingNotificationCapabilities, + type AndroidOngoingNotificationContent, + type AndroidOngoingNotificationFallbackBehavior, + type AndroidOngoingNotificationInput, + type AndroidOngoingNotificationPayload, + type AndroidOngoingNotificationStartResult, + type AndroidOngoingNotificationStatus, + type AndroidOngoingNotificationStopResult, + type AndroidOngoingNotificationUpdateResult, + type AndroidOngoingNotificationUpsertResult, + type StartAndroidOngoingNotificationOptions, + type UpdateAndroidOngoingNotificationOptions, + type UseAndroidOngoingNotificationOptions, + type UseAndroidOngoingNotificationResult, +} from '@use-voltra/android' + import { useUpdateOnHMR } from '../utils/index.js' import VoltraModule from '../VoltraModule.js' -import { renderAndroidOngoingNotificationContent } from './renderer.js' -import type { - AndroidOngoingNotificationCapabilities, - AndroidOngoingNotificationContent, - AndroidOngoingNotificationFallbackBehavior, - AndroidOngoingNotificationInput, - AndroidOngoingNotificationPayload, - AndroidOngoingNotificationStartResult, - AndroidOngoingNotificationStatus, - AndroidOngoingNotificationStopResult, - AndroidOngoingNotificationUpdateResult, - AndroidOngoingNotificationUpsertResult, - StartAndroidOngoingNotificationOptions, - UpdateAndroidOngoingNotificationOptions, - UseAndroidOngoingNotificationOptions, - UseAndroidOngoingNotificationResult, -} from './types.js' const NOTIFICATION_PERMISSION = PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS @@ -40,7 +41,7 @@ const serializeAndroidOngoingNotificationInput = (input: AndroidOngoingNotificat return JSON.stringify(input) } - return renderAndroidOngoingNotificationContent(input) + return renderAndroidOngoingNotificationPayload(input) } const getFilteredAndroidOngoingNotificationUpdateOptions = ( @@ -101,40 +102,6 @@ const createNotFoundStopResult = (notificationId: string): AndroidOngoingNotific reason: 'not_found', }) -export { renderAndroidOngoingNotificationPayload } from './renderer.js' - -/** - * @unstable This API is experimental and may change in future versions. - * - * React hook for managing Android ongoing notifications with automatic lifecycle handling. - * - * @param variants - The Android ongoing notification content variants to display - * @param options - Configuration options for the hook - * @returns Object with start, update, end methods and isActive state - * - * @example - * ```tsx - * import { AndroidOngoingNotification, useAndroidOngoingNotification } from 'voltra/android/client' - * - * const MyOngoingNotification = () => { - * const { start, update, end, isActive } = useAndroidOngoingNotification( - * , - * { - * notificationId: 'my-ongoing-notification', - * channelId: 'delivery_updates', - * autoStart: true, - * autoUpdate: true, - * } - * ) - * - * return ( - * - *