From 68beee51e48fa3f45d703fde5ce473531019b58f Mon Sep 17 00:00:00 2001 From: Alan Hughes <30924086+alanjhughes@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:55:14 +0000 Subject: [PATCH 01/15] [ios][expo-go] Fix error view not displaying correctly (#42303) --- apps/bare-expo/ios/Podfile.lock | 6 +++--- .../expo-go/ios/Client/SwiftUI/Services/DataService.swift | 1 + apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m | 8 ++++---- .../Kernel/ReactAppManager/EXReactAppExceptionHandler.m | 4 +--- .../ios/Exponent/Kernel/Views/EXAppViewController.mm | 4 ---- apps/expo-go/ios/Podfile.lock | 2 +- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/apps/bare-expo/ios/Podfile.lock b/apps/bare-expo/ios/Podfile.lock index 6f5987ec4664a9..7992007135252a 100644 --- a/apps/bare-expo/ios/Podfile.lock +++ b/apps/bare-expo/ios/Podfile.lock @@ -3854,7 +3854,7 @@ SPEC CHECKSUMS: EXUpdates: 83e4d666a085b44149f3b21d5bd057ad37b2c3e5 EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b FBLazyVector: 3a7ea85f6009224ad89f7daeda516f189e6b5759 - hermes-engine: f631dcabc3dc2d46dc5f32c6c79d48d1d9e9aac6 + hermes-engine: 6bb3000824be2770010ae038914fa26721255c8e libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -3872,7 +3872,7 @@ SPEC CHECKSUMS: React: 4bc1f928568ad4bcfd147260f907b4ea5873a03b React-callinvoker: 8dd44c31888a314694efd0ef5f19ab7e0e855ef8 React-Core: 0c1f3042e1204c0512b2e4263062c66550d7e6a3 - React-Core-prebuilt: baa77ffa5636202bd80ae54a374df8b194f96ed6 + React-Core-prebuilt: 7894b037a2f0fa699a44de6c88d20acd4235b255 React-CoreModules: f6a626221d52f21b5eb8df2d79780b96f20240e5 React-cxxreact: 2e3990595049d43dd1d59ccd6cb35545f0dc6f03 React-debug: 60be0767f5672afc81bfd6a50d996507430f7906 @@ -3942,7 +3942,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: bfb12ead469222b022a2024f32aba47ce50de512 ReactCodegen: b9b0ea5c72e425fa5a89a031ada3e64dfd839771 ReactCommon: 0084791d25c4deae3d8b77efd4440fb2d5c38746 - ReactNativeDependencies: c5e58d6ef1758f36ac4ee8b97949e0fb78f31b21 + ReactNativeDependencies: 17a617edb4d5883a4c48339514ccb8b765f8af4f RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4 RNCMaskedView: 3c9d7586e2b9bbab573591dcb823918bc4668005 RNCPicker: e0149590451d5eae242cf686014a6f6d808f93c7 diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift index d77f01733f6475..c30be9afb72f15 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/DataService.swift @@ -1,6 +1,7 @@ // Copyright 2015-present 650 Industries. All rights reserved. import Foundation + @MainActor class DataService: ObservableObject { @Published var projects: [ExpoProject] = [] diff --git a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m index f7d789f12df5fb..863dcc55946deb 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m +++ b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m @@ -207,12 +207,12 @@ - (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:( [appStateModule setState:@"active"]; } _visibleApp = appRecord; + [self _unregisterUnusedAppRecords]; } else { _visibleApp = nil; - } - - if (_visibleApp != nil) { - [self _unregisterUnusedAppRecords]; + if (appRecordPreviouslyVisible) { + [_appRegistry unregisterAppWithRecord:appRecordPreviouslyVisible]; + } } } } diff --git a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m index 63d8759df0e1ad..6242e80bf9004e 100644 --- a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m +++ b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppExceptionHandler.m @@ -65,9 +65,7 @@ - (void)handleFatalJSExceptionWithMessage:(nullable NSString *)message [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey]; - if ([self _isProdHome]) { - RCTFatal(error); - } + [_appRecord.viewController maybeShowError:error]; } - (void)updateJSExceptionWithMessage:(nullable NSString *)message diff --git a/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm b/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm index 05b0f3312e23cd..83170f5615c404 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm +++ b/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm @@ -192,10 +192,6 @@ - (void)maybeShowError:(NSError *)error dispatch_async(dispatch_get_main_queue(), ^{ [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; }); - } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { - // RCTRedBox already handled this - } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { - // RCTRedBox already handled this } else { dispatch_async(dispatch_get_main_queue(), ^{ [self _showErrorWithType:kEXFatalErrorTypeException error:error]; diff --git a/apps/expo-go/ios/Podfile.lock b/apps/expo-go/ios/Podfile.lock index b94849c7ad168a..fab722516f4d35 100644 --- a/apps/expo-go/ios/Podfile.lock +++ b/apps/expo-go/ios/Podfile.lock @@ -4730,7 +4730,7 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 8a82b93a6400c8e6551c0bcd66a9177f2e067aed GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - hermes-engine: 21f7021a3364f5f9dab02bdfd1fa0e21053e5dd5 + hermes-engine: 452f2dd7422b2fd7973ae9ca103898d28d7744f0 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 From 9dfc70b3e3bf52ac910dff604b441a63b9ab79ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosmaty?= Date: Tue, 20 Jan 2026 11:57:59 +0100 Subject: [PATCH 02/15] [NCL] Fix worklets tester can't be imported (#42330) --- apps/native-component-list/metro.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/native-component-list/metro.config.js b/apps/native-component-list/metro.config.js index 514ddb81cbc693..845b06ad6e3ad2 100644 --- a/apps/native-component-list/metro.config.js +++ b/apps/native-component-list/metro.config.js @@ -16,6 +16,7 @@ config.watchFolders = [ path.join(monorepoRoot, 'node_modules'), // Allow Metro to resolve "shared" `node_modules` of the monorepo path.join(monorepoRoot, 'apps/common'), // Allow Metro to resolve common ThemeProvider path.join(monorepoRoot, 'apps/bare-expo/modules/benchmarking'), // Allow Metro to resolve benchmarking folder + path.join(monorepoRoot, 'apps/bare-expo/modules/worklets-tester'), // Allow Metro to resolve worklets-tester folder path.join(monorepoRoot, 'apps/test-suite'), // Allow Metro to resolve test-suite app ]; From 467afe6b3e5f54acda60105a5b61038d75c6d280 Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Tue, 20 Jan 2026 17:28:10 +0530 Subject: [PATCH 03/15] [docs] Improve formatting for compound component API documentation (#42297) --- .../plugins/api/APISectionComponents.tsx | 25 ++- .../api/APISectionCompoundNames.test.ts | 66 ++++++++ .../plugins/api/APISectionCompoundNames.ts | 145 ++++++++++++++++++ .../TableOfContents/TableOfContentsLink.tsx | 3 +- 4 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 docs/components/plugins/api/APISectionCompoundNames.test.ts create mode 100644 docs/components/plugins/api/APISectionCompoundNames.ts diff --git a/docs/components/plugins/api/APISectionComponents.tsx b/docs/components/plugins/api/APISectionComponents.tsx index 2a0922eb8188c6..bae42bbc780e82 100644 --- a/docs/components/plugins/api/APISectionComponents.tsx +++ b/docs/components/plugins/api/APISectionComponents.tsx @@ -8,9 +8,10 @@ import { CommentData, GeneratedData, PropsDefinitionData, - TypeSignaturesData, TypeDefinitionData, + TypeSignaturesData, } from './APIDataTypes'; +import { buildCompoundNameByComponent } from './APISectionCompoundNames'; import { APISectionDeprecationNote } from './APISectionDeprecationNote'; import APISectionProps from './APISectionProps'; import { @@ -88,7 +89,8 @@ const getComponentTypeParameters = ({ const renderComponent = ( { name, comment, type, extendedTypes, children, signatures }: GeneratedData, sdkVersion: string, - componentsProps?: PropsDefinitionData[] + componentsProps?: PropsDefinitionData[], + compoundNameByComponent?: Map ) => { const resolvedSignatures = getComponentSignatures({ signatures, type }); const resolvedType = getComponentType({ signatures: resolvedSignatures }); @@ -97,7 +99,8 @@ const renderComponent = ( extendedTypes, signatures: resolvedSignatures, }); - const resolvedName = getComponentName(name, children); + const baseName = getComponentName(name, children); + const resolvedName = compoundNameByComponent?.get(baseName) ?? baseName; const extractedComment = getComponentComment(comment, resolvedSignatures); return ( @@ -128,7 +131,7 @@ const renderComponent = ( ) : null} @@ -136,8 +139,12 @@ const renderComponent = ( ); }; -const APISectionComponents = ({ data, sdkVersion, componentsProps }: APISectionComponentsProps) => - data?.length ? ( +const APISectionComponents = ({ data, sdkVersion, componentsProps }: APISectionComponentsProps) => { + if (!data?.length) { + return null; + } + const compoundNameByComponent = buildCompoundNameByComponent(data); + return ( <>

{data.length === 1 ? 'Component' : 'Components'}

{data.map(component => @@ -146,10 +153,12 @@ const APISectionComponents = ({ data, sdkVersion, componentsProps }: APISectionC sdkVersion, componentsProps.filter(cp => getPossibleComponentPropsNames(component.name, component.children).includes(cp.name) - ) + ), + compoundNameByComponent ) )} - ) : null; + ); +}; export default APISectionComponents; diff --git a/docs/components/plugins/api/APISectionCompoundNames.test.ts b/docs/components/plugins/api/APISectionCompoundNames.test.ts new file mode 100644 index 00000000000000..b4d8581abc53d6 --- /dev/null +++ b/docs/components/plugins/api/APISectionCompoundNames.test.ts @@ -0,0 +1,66 @@ +import { GeneratedData, PropData, TypeDefinitionData, TypeDocKind } from './APIDataTypes'; +import { buildCompoundNameByComponent } from './APISectionCompoundNames'; + +const makeComponentType = (propsName: string): TypeDefinitionData => ({ + type: 'reference', + name: 'React.FC', + typeArguments: [{ type: 'reference', name: propsName }], +}); + +const makeProp = (name: string, type?: TypeDefinitionData, defaultValue?: string): PropData => + ({ + name, + kind: TypeDocKind.Property, + type, + defaultValue, + }) as PropData; + +const makeComponent = (name: string, propsName: string, children: PropData[] = []): GeneratedData => + ({ + name, + kind: TypeDocKind.Class, + type: makeComponentType(propsName), + children, + }) as GeneratedData; + +describe('buildCompoundNameByComponent', () => { + test('maps direct component properties to compound names', () => { + const menuItem = makeComponent('MenuItem', 'MenuItemProps'); + const menu = makeComponent('Menu', 'MenuProps', [ + makeProp('Item', makeComponentType('MenuItemProps')), + ]); + + const result = Object.fromEntries(buildCompoundNameByComponent([menu, menuItem])); + + expect(result).toEqual({ + MenuItem: 'Menu.Item', + }); + }); + + test('chains compound names when parent is itself a compound component', () => { + const menuItemIcon = makeComponent('MenuItemIcon', 'MenuItemIconProps'); + const menuItem = makeComponent('MenuItem', 'MenuItemProps', [ + makeProp('Icon', makeComponentType('MenuItemIconProps')), + ]); + const menu = makeComponent('Menu', 'MenuProps', [ + makeProp('Item', makeComponentType('MenuItemProps')), + ]); + + const result = Object.fromEntries(buildCompoundNameByComponent([menu, menuItem, menuItemIcon])); + + expect(result).toEqual({ + MenuItem: 'Menu.Item', + MenuItemIcon: 'Menu.Item.Icon', + }); + }); + + test('uses string default values as component targets', () => { + const tabs = makeComponent('Tabs', 'TabsProps', [makeProp('Tab', undefined, 'Tab')]); + + const result = Object.fromEntries(buildCompoundNameByComponent([tabs])); + + expect(result).toEqual({ + Tab: 'Tabs.Tab', + }); + }); +}); diff --git a/docs/components/plugins/api/APISectionCompoundNames.ts b/docs/components/plugins/api/APISectionCompoundNames.ts new file mode 100644 index 00000000000000..afae562c8e5059 --- /dev/null +++ b/docs/components/plugins/api/APISectionCompoundNames.ts @@ -0,0 +1,145 @@ +import { GeneratedData, PropData, TypeDefinitionData, TypeDocKind } from './APIDataTypes'; +import { getComponentName } from './APISectionUtils'; + +const componentTypeNames = new Set([ + 'React.FC', + 'FC', + 'ForwardRefExoticComponent', + 'React.ForwardRefExoticComponent', + 'ComponentType', + 'React.ComponentType', + 'NamedExoticComponent', + 'React.NamedExoticComponent', +]); + +const getComponentPropertyChildren = (entry: GeneratedData): PropData[] => { + const candidates: PropData[] = []; + const pushChildren = (children?: PropData[]) => { + if (children?.length) { + candidates.push(...children); + } + }; + + if ('children' in entry) { + pushChildren(entry.children as PropData[] | undefined); + } + pushChildren(entry.type?.declaration?.children); + entry.type?.types?.forEach(type => { + pushChildren(type.declaration?.children); + }); + + if (candidates.length === 0) { + return []; + } + + const seen = new Set(); + return candidates.filter(child => { + if (!child || child.kind !== TypeDocKind.Property) { + return false; + } + const id = `${child.name ?? ''}-${child.kind ?? ''}`; + if (seen.has(id)) { + return false; + } + seen.add(id); + return true; + }); +}; + +const getPropsTypeNameFromComponentType = (type?: TypeDefinitionData): string | undefined => { + if (!type) { + return undefined; + } + if (type.type === 'reference') { + const typeName = type.name ?? type.target?.qualifiedName; + if (!typeName || !componentTypeNames.has(typeName)) { + return undefined; + } + const propsType = type.typeArguments?.[0]; + return propsType?.type === 'reference' ? propsType.name : undefined; + } + if (type.type === 'intersection' || type.type === 'union') { + for (const nested of type.types ?? []) { + const found = getPropsTypeNameFromComponentType(nested); + if (found) { + return found; + } + } + } + return undefined; +}; + +export const buildCompoundNameByComponent = (components: GeneratedData[]) => { + const componentNameByPropsType = new Map(); + const propertiesByEntry = new Map(); + const baseNameByEntry = new Map(); + + components.forEach(entry => { + const baseName = getComponentName(entry.name, entry.children); + const propsTypeName = getPropsTypeNameFromComponentType(entry.type); + if (propsTypeName && baseName) { + componentNameByPropsType.set(propsTypeName, baseName); + } + if (baseName) { + baseNameByEntry.set(entry, baseName); + } + propertiesByEntry.set(entry, getComponentPropertyChildren(entry)); + }); + + const resolveComponentFromProperty = (prop: PropData) => { + if (typeof prop.defaultValue === 'string') { + return prop.defaultValue; + } + const propsTypeName = getPropsTypeNameFromComponentType(prop.type); + if (!propsTypeName) { + return undefined; + } + return componentNameByPropsType.get(propsTypeName); + }; + + const directMap = new Map(); + + components.forEach(entry => { + const parentName = baseNameByEntry.get(entry); + if (!parentName) { + return; + } + const properties = propertiesByEntry.get(entry) ?? []; + properties.forEach(property => { + if (!property.name) { + return; + } + const target = resolveComponentFromProperty(property); + if (!target) { + return; + } + directMap.set(target, `${parentName}.${property.name}`); + }); + }); + + const compoundMap = new Map(directMap); + + components.forEach(entry => { + const parentName = baseNameByEntry.get(entry); + if (!parentName) { + return; + } + const parentAlias = directMap.get(parentName); + if (!parentAlias) { + return; + } + const properties = propertiesByEntry.get(entry) ?? []; + properties.forEach(property => { + if (!property.name) { + return; + } + const target = resolveComponentFromProperty(property); + if (!target) { + return; + } + compoundMap.set(target, `${parentAlias}.${property.name}`); + }); + }); + + return compoundMap; +}; diff --git a/docs/ui/components/TableOfContents/TableOfContentsLink.tsx b/docs/ui/components/TableOfContents/TableOfContentsLink.tsx index efe3881ff058cf..408da4b90d8aca 100644 --- a/docs/ui/components/TableOfContents/TableOfContentsLink.tsx +++ b/docs/ui/components/TableOfContents/TableOfContentsLink.tsx @@ -85,7 +85,8 @@ export const TableOfContentsLink = forwardRef(arguments: argType)` with `someFunction()` */ function trimCodedTitle(str: string) { - if (!str.includes('...')) { + const hasParens = str.includes('('); + if (!str.includes('...') && hasParens) { const dotIdx = str.indexOf('.'); if (dotIdx > 0) { str = str.slice(Math.max(0, dotIdx + 1)); From 523cc4138080cdbc739f6c0377cb89f9d3848419 Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Tue, 20 Jan 2026 17:28:23 +0530 Subject: [PATCH 04/15] [docs] Improve build locally overview and its discoverability (#42280) --- docs/constants/navigation.js | 2 +- docs/pages/guides/local-app-development.mdx | 4 +-- docs/pages/guides/local-app-overview.mdx | 28 +++++++++++---------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/constants/navigation.js b/docs/constants/navigation.js index bc327a0cafd0bb..377ac97eaced3e 100644 --- a/docs/constants/navigation.js +++ b/docs/constants/navigation.js @@ -177,7 +177,7 @@ export const general = [ { expanded: false } ), makeGroup( - 'Compile locally', + 'Build locally', [ makePage('guides/local-app-overview.mdx'), makePage('guides/local-app-development.mdx'), diff --git a/docs/pages/guides/local-app-development.mdx b/docs/pages/guides/local-app-development.mdx index 1c66e0d0e0dd7d..4f2e735d27b28d 100644 --- a/docs/pages/guides/local-app-development.mdx +++ b/docs/pages/guides/local-app-development.mdx @@ -1,7 +1,7 @@ --- -title: Local app development +title: Create a debug build locally sidebar_title: Development -description: Learn how to compile and build your Expo app locally. +description: Learn how to create a debug build for your Expo app locally. --- import { BuildIcon } from '@expo/styleguide-icons/custom/BuildIcon'; diff --git a/docs/pages/guides/local-app-overview.mdx b/docs/pages/guides/local-app-overview.mdx index e7a51f4c30d319..4f0212d2407a65 100644 --- a/docs/pages/guides/local-app-overview.mdx +++ b/docs/pages/guides/local-app-overview.mdx @@ -1,27 +1,29 @@ --- -title: 'Compile locally: Overview' +title: 'Build locally: Overview' sidebar_title: Overview -description: An overview on local app compilation process for your Expo apps. +description: An overview of how to build your app locally using your own machine for Expo projects. +searchRank: 90 +searchPosition: 1 --- import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; import { BoxLink } from '~/ui/components/BoxLink'; -You can leverage your local development environment to compile your app locally by utilizing Android Studio and Xcode. This compilation process can be done for both debug and release builds. This page provides an overview of the local app compilation process and references to other guides that might be necessary in this workflow. +You can leverage your local development environment to build your app locally by utilizing Android Studio and Xcode. This build process can be done for both debug and release builds. This page provides an overview on different ways to build your app locally using your own machine and references to other guides that might be necessary in this workflow. -## When to compile your app locally +## When to build your app locally -There are different scenarios when you want to compile your app on your developer machine. It includes: +There are different scenarios when you want to build your app on your developer machine: - You want to iterate quickly on native code changes or test platform-specific changes in your debug build - You want to manually generate native code to test your debug build -- Any scenario where you are required to create builds inside an environment where access to a network is restricted +- Any scenario where you are required to create builds inside an environment where access to a network is restricted. - You want to locally manage your own credentials (such as upload key, and so on) - You want to test or integrate your own custom build cache provider - You want to opt out of prebuilt Expo Modules for Android and compile them from source locally once -> **Note**: Compiling your app locally complements EAS Build. You can keep using the build service for cloud automation and fall back to local builds for development. +> **Note**: Building your app locally complements EAS Build. You can keep using the build service for cloud automation and fall back to local builds for development. ## Prerequisites @@ -30,24 +32,24 @@ You need to install and set up Android Studio and Xcode to compile and run Andro - [Android Studio](/get-started/set-up-your-environment/?platform=android&device=physical&mode=development-build&buildEnv=local#set-up-an-android-device-with-a-development-build) - [Xcode](/get-started/set-up-your-environment/?platform=ios&device=physical&mode=development-build&buildEnv=local#set-up-an-ios-device-with-a-development-build) -## Compiling your debug build +## Creating your debug build locally To quickly build and iterate on a debug build, you can use Expo CLI's `npx expo run:[android|ios]` commands. These commands compile your project, using your locally installed Android SDK or Xcode, into a debug build of your app. -## Compiling your release build +## Creating your release build locally To create a release build (also known as production build) of your app, you generate signing credentials by utilizing tools provided by Android Studio and Xcode. Then, you can generate a release build and follow the process of manually submitting your app to Google Play Store or Apple App Store. From 9841f7fc0bc8399dbce8fc73135567e30149236a Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Tue, 20 Jan 2026 13:20:02 +0100 Subject: [PATCH 05/15] [widgets] Initial implementation of Expo Widgets (#42286) # Why Initial implementation of Expo Widgets, will need a few follow ups and improvements before alpha. # How Updating widget snapshot serializes `@expo/ui` views tree to JSON and then recreates it in widget/live-activity using SwiftUI. # Test Plan Manual testing in #42094 --- packages/expo-widgets/build/ExpoWidgets.d.ts | 11 ++ .../expo-widgets/build/ExpoWidgets.d.ts.map | 1 + packages/expo-widgets/build/ExpoWidgets.js | 3 + .../expo-widgets/build/ExpoWidgets.js.map | 1 + packages/expo-widgets/build/Widgets.d.ts | 9 ++ packages/expo-widgets/build/Widgets.d.ts.map | 1 + packages/expo-widgets/build/Widgets.js | 36 +++++ packages/expo-widgets/build/Widgets.js.map | 1 + .../expo-widgets/build/Widgets.types.d.ts | 35 +++++ .../expo-widgets/build/Widgets.types.d.ts.map | 1 + packages/expo-widgets/build/Widgets.types.js | 2 + .../expo-widgets/build/Widgets.types.js.map | 1 + packages/expo-widgets/build/constants.d.ts | 3 + .../expo-widgets/build/constants.d.ts.map | 1 + packages/expo-widgets/build/constants.js | 10 ++ packages/expo-widgets/build/constants.js.map | 1 + packages/expo-widgets/build/index.d.ts | 3 +- packages/expo-widgets/build/index.d.ts.map | 2 +- packages/expo-widgets/build/index.js | 3 +- packages/expo-widgets/build/index.js.map | 2 +- packages/expo-widgets/build/serializer.d.ts | 3 + .../expo-widgets/build/serializer.d.ts.map | 1 + packages/expo-widgets/build/serializer.js | 26 ++++ packages/expo-widgets/build/serializer.js.map | 1 + packages/expo-widgets/ios/Brotli+Data.swift | 32 +++++ packages/expo-widgets/ios/ExpoWidgets.podspec | 1 + .../expo-widgets/ios/Widgets/AppIntent.swift | 66 +++++++++ .../expo-widgets/ios/Widgets/Buttons.swift | 58 ++++++++ .../ios/Widgets/DynamicView.swift | 130 ++++++++++++++++++ .../expo-widgets/ios/Widgets/EntryView.swift | 25 ++++ .../ios/Widgets/LiveActivity.swift | 59 ++++++++ .../ios/Widgets/TimelineEntry.swift | 7 + .../ios/Widgets/TimelineProvider.swift | 31 +++++ packages/expo-widgets/ios/Widgets/Type.swift | 19 +++ packages/expo-widgets/ios/Widgets/Utils.swift | 44 ++++++ .../expo-widgets/ios/WidgetsContext.swift | 7 + packages/expo-widgets/ios/WidgetsEvents.swift | 16 +++ .../expo-widgets/ios/WidgetsExceptions.swift | 13 ++ packages/expo-widgets/ios/WidgetsModule.swift | 99 ++++++++++++- .../expo-widgets/ios/WidgetsStorage.swift | 46 +++++++ packages/expo-widgets/package.json | 3 +- packages/expo-widgets/plugin/build/index.js | 5 +- .../plugin/build/xcode/addBuildPhases.js | 36 +---- .../build/xcode/addToPbxProjectSection.js | 9 +- packages/expo-widgets/plugin/src/index.ts | 6 +- .../plugin/src/xcode/addBuildPhases.ts | 3 +- .../src/xcode/addToPbxProjectSection.ts | 15 +- packages/expo-widgets/scripts/autolinking.rb | 4 +- packages/expo-widgets/src/ExpoWidgets.ts | 12 ++ packages/expo-widgets/src/Widgets.ts | 74 ++++++++++ packages/expo-widgets/src/Widgets.types.ts | 46 +++++++ packages/expo-widgets/src/constants.ts | 11 ++ packages/expo-widgets/src/index.ts | 2 + packages/expo-widgets/src/serializer.ts | 30 ++++ 54 files changed, 999 insertions(+), 68 deletions(-) create mode 100644 packages/expo-widgets/build/ExpoWidgets.d.ts create mode 100644 packages/expo-widgets/build/ExpoWidgets.d.ts.map create mode 100644 packages/expo-widgets/build/ExpoWidgets.js create mode 100644 packages/expo-widgets/build/ExpoWidgets.js.map create mode 100644 packages/expo-widgets/build/Widgets.d.ts create mode 100644 packages/expo-widgets/build/Widgets.d.ts.map create mode 100644 packages/expo-widgets/build/Widgets.js create mode 100644 packages/expo-widgets/build/Widgets.js.map create mode 100644 packages/expo-widgets/build/Widgets.types.d.ts create mode 100644 packages/expo-widgets/build/Widgets.types.d.ts.map create mode 100644 packages/expo-widgets/build/Widgets.types.js create mode 100644 packages/expo-widgets/build/Widgets.types.js.map create mode 100644 packages/expo-widgets/build/constants.d.ts create mode 100644 packages/expo-widgets/build/constants.d.ts.map create mode 100644 packages/expo-widgets/build/constants.js create mode 100644 packages/expo-widgets/build/constants.js.map create mode 100644 packages/expo-widgets/build/serializer.d.ts create mode 100644 packages/expo-widgets/build/serializer.d.ts.map create mode 100644 packages/expo-widgets/build/serializer.js create mode 100644 packages/expo-widgets/build/serializer.js.map create mode 100644 packages/expo-widgets/ios/Brotli+Data.swift create mode 100644 packages/expo-widgets/ios/Widgets/AppIntent.swift create mode 100644 packages/expo-widgets/ios/Widgets/Buttons.swift create mode 100644 packages/expo-widgets/ios/Widgets/DynamicView.swift create mode 100644 packages/expo-widgets/ios/Widgets/EntryView.swift create mode 100644 packages/expo-widgets/ios/Widgets/LiveActivity.swift create mode 100644 packages/expo-widgets/ios/Widgets/TimelineEntry.swift create mode 100644 packages/expo-widgets/ios/Widgets/TimelineProvider.swift create mode 100644 packages/expo-widgets/ios/Widgets/Type.swift create mode 100644 packages/expo-widgets/ios/Widgets/Utils.swift create mode 100644 packages/expo-widgets/ios/WidgetsContext.swift create mode 100644 packages/expo-widgets/ios/WidgetsEvents.swift create mode 100644 packages/expo-widgets/ios/WidgetsExceptions.swift create mode 100644 packages/expo-widgets/ios/WidgetsStorage.swift create mode 100644 packages/expo-widgets/src/ExpoWidgets.ts create mode 100644 packages/expo-widgets/src/Widgets.ts create mode 100644 packages/expo-widgets/src/Widgets.types.ts create mode 100644 packages/expo-widgets/src/constants.ts create mode 100644 packages/expo-widgets/src/serializer.ts diff --git a/packages/expo-widgets/build/ExpoWidgets.d.ts b/packages/expo-widgets/build/ExpoWidgets.d.ts new file mode 100644 index 00000000000000..313c382353cd1b --- /dev/null +++ b/packages/expo-widgets/build/ExpoWidgets.d.ts @@ -0,0 +1,11 @@ +import { NativeModule } from 'expo'; +import { ExpoWidgetsEvents } from './Widgets.types'; +declare class ExpoWidgetModule extends NativeModule { + reloadWidget(timeline?: string): void; + updateWidget(name: string, data: string, props?: Record): void; + startLiveActivity(name: string, nodes: string, url?: string): string; + updateLiveActivity(id: string, name: string, nodes: string): string; +} +declare const _default: ExpoWidgetModule; +export default _default; +//# sourceMappingURL=ExpoWidgets.d.ts.map \ No newline at end of file diff --git a/packages/expo-widgets/build/ExpoWidgets.d.ts.map b/packages/expo-widgets/build/ExpoWidgets.d.ts.map new file mode 100644 index 00000000000000..2c046657c05fcc --- /dev/null +++ b/packages/expo-widgets/build/ExpoWidgets.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoWidgets.d.ts","sourceRoot":"","sources":["../src/ExpoWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACpE,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IACrC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAC3E,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM;IACpE,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;CACpE;;AAED,wBAAoE"} \ No newline at end of file diff --git a/packages/expo-widgets/build/ExpoWidgets.js b/packages/expo-widgets/build/ExpoWidgets.js new file mode 100644 index 00000000000000..19c792650aba48 --- /dev/null +++ b/packages/expo-widgets/build/ExpoWidgets.js @@ -0,0 +1,3 @@ +import { requireNativeModule } from 'expo'; +export default requireNativeModule('ExpoWidgets'); +//# sourceMappingURL=ExpoWidgets.js.map \ No newline at end of file diff --git a/packages/expo-widgets/build/ExpoWidgets.js.map b/packages/expo-widgets/build/ExpoWidgets.js.map new file mode 100644 index 00000000000000..1492cffb4488d2 --- /dev/null +++ b/packages/expo-widgets/build/ExpoWidgets.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoWidgets.js","sourceRoot":"","sources":["../src/ExpoWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAWzD,eAAe,mBAAmB,CAAmB,aAAa,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { ExpoWidgetsEvents } from './Widgets.types';\n\ndeclare class ExpoWidgetModule extends NativeModule {\n reloadWidget(timeline?: string): void;\n updateWidget(name: string, data: string, props?: Record): void;\n startLiveActivity(name: string, nodes: string, url?: string): string;\n updateLiveActivity(id: string, name: string, nodes: string): string;\n}\n\nexport default requireNativeModule('ExpoWidgets');\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.d.ts b/packages/expo-widgets/build/Widgets.d.ts new file mode 100644 index 00000000000000..800c448b7445b4 --- /dev/null +++ b/packages/expo-widgets/build/Widgets.d.ts @@ -0,0 +1,9 @@ +import React from 'react'; +import ExpoWidgetModule from './ExpoWidgets'; +import { LiveActivityComponent, WidgetProps } from './Widgets.types'; +export declare const startLiveActivity: (name: string, liveActivity: LiveActivityComponent, url?: string) => string; +export declare const updateLiveActivity: (id: string, name: string, liveActivity: LiveActivityComponent) => void; +export declare const updateWidgetTimeline: (name: string, dates: Date[], widget: (p: WidgetProps) => React.JSX.Element, props?: T) => void; +export declare const updateWidgetSnapshot: (name: string, widget: (p: WidgetProps) => React.JSX.Element, props?: T) => void; +export declare const addEventListener: typeof ExpoWidgetModule.addListener; +//# sourceMappingURL=Widgets.d.ts.map \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.d.ts.map b/packages/expo-widgets/build/Widgets.d.ts.map new file mode 100644 index 00000000000000..e958c65d9b3013 --- /dev/null +++ b/packages/expo-widgets/build/Widgets.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Widgets.d.ts","sourceRoot":"","sources":["../src/Widgets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,gBAAgB,MAAM,eAAe,CAAC;AAC7C,OAAO,EAEL,qBAAqB,EAErB,WAAW,EACZ,MAAM,iBAAiB,CAAC;AAIzB,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,EACZ,cAAc,qBAAqB,EACnC,MAAM,MAAM,WAIb,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,IAAI,MAAM,EACV,MAAM,MAAM,EACZ,cAAc,qBAAqB,SAIpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACnD,MAAM,MAAM,EACZ,OAAO,IAAI,EAAE,EACb,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO,EAChD,QAAQ,CAAC,SA6BV,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACnD,MAAM,MAAM,EACZ,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO,EAChD,QAAQ,CAAC,SAGV,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,OAAO,gBAAgB,CAAC,WAA0C,CAAC"} \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.js b/packages/expo-widgets/build/Widgets.js new file mode 100644 index 00000000000000..57b75a5425fcc4 --- /dev/null +++ b/packages/expo-widgets/build/Widgets.js @@ -0,0 +1,36 @@ +import ExpoWidgetModule from './ExpoWidgets'; +import { supportedFamilies } from './constants'; +import { serialize } from './serializer'; +export const startLiveActivity = (name, liveActivity, url) => { + const text = serialize(liveActivity()); + return ExpoWidgetModule.startLiveActivity(name, text, url); +}; +export const updateLiveActivity = (id, name, liveActivity) => { + const text = serialize(liveActivity()); + ExpoWidgetModule.updateLiveActivity(id, name, text); +}; +export const updateWidgetTimeline = (name, dates, widget, props) => { + const fakeProps = Object.keys(props || {}).reduce((acc, key) => { + acc[key] = `{{${key}}}`; + return acc; + }, {}); + const data = supportedFamilies + .map((family) => ({ + family, + entries: dates.map((date) => ({ + timestamp: date.getTime(), + content: widget({ date, family, ...fakeProps }), + })), + })) + .reduce((acc, { family, entries }) => { + acc[family] = entries; + return acc; + }, {}); + ExpoWidgetModule.updateWidget(name, serialize(data), props); + ExpoWidgetModule.reloadWidget(); +}; +export const updateWidgetSnapshot = (name, widget, props) => { + updateWidgetTimeline(name, [new Date()], widget, props || {}); +}; +export const addEventListener = ExpoWidgetModule.addListener; +//# sourceMappingURL=Widgets.js.map \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.js.map b/packages/expo-widgets/build/Widgets.js.map new file mode 100644 index 00000000000000..0f7336abb7fcf5 --- /dev/null +++ b/packages/expo-widgets/build/Widgets.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Widgets.js","sourceRoot":"","sources":["../src/Widgets.ts"],"names":[],"mappings":"AAEA,OAAO,gBAAgB,MAAM,eAAe,CAAC;AAO7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,IAAY,EACZ,YAAmC,EACnC,GAAY,EACZ,EAAE;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACvC,OAAO,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,EAAU,EACV,IAAY,EACZ,YAAmC,EACnC,EAAE;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACvC,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAY,EACZ,KAAa,EACb,MAAgD,EAChD,KAAS,EACT,EAAE;IACF,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAC/C,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACX,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC;QACxB,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAA4B,CAC7B,CAAC;IAEF,MAAM,IAAI,GAA8C,iBAAiB;SACtE,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChB,MAAM;QACN,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC5B,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE;YACzB,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAI,SAAe,EAAE,CAAC;SACvD,CAAC,CAAC;KACJ,CAAC,CAAC;SACF,MAAM,CACL,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;QACtB,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAA+C,CAChD,CAAC;IAEJ,gBAAgB,CAAC,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAE5D,gBAAgB,CAAC,YAAY,EAAE,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAY,EACZ,MAAgD,EAChD,KAAS,EACT,EAAE;IACF,oBAAoB,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,IAAK,EAAQ,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAwC,gBAAgB,CAAC,WAAW,CAAC","sourcesContent":["import React from 'react';\n\nimport ExpoWidgetModule from './ExpoWidgets';\nimport {\n ExpoTimelineEntry,\n LiveActivityComponent,\n WidgetFamily,\n WidgetProps,\n} from './Widgets.types';\nimport { supportedFamilies } from './constants';\nimport { serialize } from './serializer';\n\nexport const startLiveActivity = (\n name: string,\n liveActivity: LiveActivityComponent,\n url?: string\n) => {\n const text = serialize(liveActivity());\n return ExpoWidgetModule.startLiveActivity(name, text, url);\n};\n\nexport const updateLiveActivity = (\n id: string,\n name: string,\n liveActivity: LiveActivityComponent\n) => {\n const text = serialize(liveActivity());\n ExpoWidgetModule.updateLiveActivity(id, name, text);\n};\n\nexport const updateWidgetTimeline = (\n name: string,\n dates: Date[],\n widget: (p: WidgetProps) => React.JSX.Element,\n props?: T\n) => {\n const fakeProps = Object.keys(props || {}).reduce(\n (acc, key) => {\n acc[key] = `{{${key}}}`;\n return acc;\n },\n {} as Record\n );\n\n const data: Record = supportedFamilies\n .map((family) => ({\n family,\n entries: dates.map((date) => ({\n timestamp: date.getTime(),\n content: widget({ date, family, ...(fakeProps as T) }),\n })),\n }))\n .reduce(\n (acc, { family, entries }) => {\n acc[family] = entries;\n return acc;\n },\n {} as Record\n );\n\n ExpoWidgetModule.updateWidget(name, serialize(data), props);\n\n ExpoWidgetModule.reloadWidget();\n};\n\nexport const updateWidgetSnapshot = (\n name: string,\n widget: (p: WidgetProps) => React.JSX.Element,\n props?: T\n) => {\n updateWidgetTimeline(name, [new Date()], widget, props || ({} as T));\n};\n\nexport const addEventListener: typeof ExpoWidgetModule.addListener = ExpoWidgetModule.addListener;\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.types.d.ts b/packages/expo-widgets/build/Widgets.types.d.ts new file mode 100644 index 00000000000000..519584cb3391f0 --- /dev/null +++ b/packages/expo-widgets/build/Widgets.types.d.ts @@ -0,0 +1,35 @@ +import { ReactNode } from 'react'; +export type WidgetFamily = 'systemSmall' | 'systemMedium' | 'systemLarge' | 'systemExtraLarge' | 'accessoryCircular' | 'accessoryRectangular' | 'accessoryInline'; +export type WidgetDimensions = { + width: number; + height: number; +}; +export type WidgetProps = { + date: Date; + family: WidgetFamily; +} & T; +export type ExpoTimelineEntry = { + timestamp: number; + content: ReactNode; +}; +export type ExpoLiveActivityEntry = { + banner: ReactNode; + compactLeading?: ReactNode; + compactTrailing?: ReactNode; + minimal?: ReactNode; + expandedCenter?: ReactNode; + expandedLeading?: ReactNode; + expandedTrailing?: ReactNode; + expandedBottom?: ReactNode; +}; +export type LiveActivityComponent = () => ExpoLiveActivityEntry; +export type UserInteractionEvent = { + source: string; + target: string; + timestamp: number; + type: 'ExpoWidgetsUserInteraction'; +}; +export type ExpoWidgetsEvents = { + onUserInteraction: (event: UserInteractionEvent) => void; +}; +//# sourceMappingURL=Widgets.types.d.ts.map \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.types.d.ts.map b/packages/expo-widgets/build/Widgets.types.d.ts.map new file mode 100644 index 00000000000000..c4a558d5cab335 --- /dev/null +++ b/packages/expo-widgets/build/Widgets.types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Widgets.types.d.ts","sourceRoot":"","sources":["../src/Widgets.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,MAAM,MAAM,YAAY,GACpB,aAAa,GACb,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,mBAAmB,GACnB,sBAAsB,GACtB,iBAAiB,CAAC;AAEtB,MAAM,MAAM,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,IAAI;IAC1C,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,CAAC,CAAC;AAEN,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,SAAS,CAAC;IAClB,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,cAAc,CAAC,EAAE,SAAS,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,MAAM,qBAAqB,CAAC;AAEhE,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,4BAA4B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,iBAAiB,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;CAC1D,CAAC"} \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.types.js b/packages/expo-widgets/build/Widgets.types.js new file mode 100644 index 00000000000000..35f67658bad9b5 --- /dev/null +++ b/packages/expo-widgets/build/Widgets.types.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=Widgets.types.js.map \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.types.js.map b/packages/expo-widgets/build/Widgets.types.js.map new file mode 100644 index 00000000000000..d95a378f80e05e --- /dev/null +++ b/packages/expo-widgets/build/Widgets.types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Widgets.types.js","sourceRoot":"","sources":["../src/Widgets.types.ts"],"names":[],"mappings":"","sourcesContent":["import { ReactNode } from 'react';\n\nexport type WidgetFamily =\n | 'systemSmall'\n | 'systemMedium'\n | 'systemLarge'\n | 'systemExtraLarge'\n | 'accessoryCircular'\n | 'accessoryRectangular'\n | 'accessoryInline';\n\nexport type WidgetDimensions = { width: number; height: number };\n\nexport type WidgetProps = {\n date: Date;\n family: WidgetFamily;\n} & T;\n\nexport type ExpoTimelineEntry = {\n timestamp: number;\n content: ReactNode;\n};\n\nexport type ExpoLiveActivityEntry = {\n banner: ReactNode;\n compactLeading?: ReactNode;\n compactTrailing?: ReactNode;\n minimal?: ReactNode;\n expandedCenter?: ReactNode;\n expandedLeading?: ReactNode;\n expandedTrailing?: ReactNode;\n expandedBottom?: ReactNode;\n};\n\nexport type LiveActivityComponent = () => ExpoLiveActivityEntry;\n\nexport type UserInteractionEvent = {\n source: string;\n target: string;\n timestamp: number;\n type: 'ExpoWidgetsUserInteraction';\n};\n\nexport type ExpoWidgetsEvents = {\n onUserInteraction: (event: UserInteractionEvent) => void;\n};\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/build/constants.d.ts b/packages/expo-widgets/build/constants.d.ts new file mode 100644 index 00000000000000..730e67dc44ae44 --- /dev/null +++ b/packages/expo-widgets/build/constants.d.ts @@ -0,0 +1,3 @@ +import { WidgetFamily } from './Widgets.types'; +export declare const supportedFamilies: WidgetFamily[]; +//# sourceMappingURL=constants.d.ts.map \ No newline at end of file diff --git a/packages/expo-widgets/build/constants.d.ts.map b/packages/expo-widgets/build/constants.d.ts.map new file mode 100644 index 00000000000000..18d7d3583b813e --- /dev/null +++ b/packages/expo-widgets/build/constants.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,eAAO,MAAM,iBAAiB,EAAE,YAAY,EAQ3C,CAAC"} \ No newline at end of file diff --git a/packages/expo-widgets/build/constants.js b/packages/expo-widgets/build/constants.js new file mode 100644 index 00000000000000..3330735e1dac0e --- /dev/null +++ b/packages/expo-widgets/build/constants.js @@ -0,0 +1,10 @@ +export const supportedFamilies = [ + 'systemSmall', + 'systemMedium', + 'systemLarge', + 'systemExtraLarge', + 'accessoryCircular', + 'accessoryRectangular', + 'accessoryInline', +]; +//# sourceMappingURL=constants.js.map \ No newline at end of file diff --git a/packages/expo-widgets/build/constants.js.map b/packages/expo-widgets/build/constants.js.map new file mode 100644 index 00000000000000..11fc34e85be69f --- /dev/null +++ b/packages/expo-widgets/build/constants.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,iBAAiB,GAAmB;IAC/C,aAAa;IACb,cAAc;IACd,aAAa;IACb,kBAAkB;IAClB,mBAAmB;IACnB,sBAAsB;IACtB,iBAAiB;CAClB,CAAC","sourcesContent":["import { WidgetFamily } from './Widgets.types';\n\nexport const supportedFamilies: WidgetFamily[] = [\n 'systemSmall',\n 'systemMedium',\n 'systemLarge',\n 'systemExtraLarge',\n 'accessoryCircular',\n 'accessoryRectangular',\n 'accessoryInline',\n];\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/build/index.d.ts b/packages/expo-widgets/build/index.d.ts index e26a57a8ca84c6..cd0d70c9ef2b65 100644 --- a/packages/expo-widgets/build/index.d.ts +++ b/packages/expo-widgets/build/index.d.ts @@ -1,2 +1,3 @@ -export {}; +export * from './Widgets'; +export * from './Widgets.types'; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/expo-widgets/build/index.d.ts.map b/packages/expo-widgets/build/index.d.ts.map index 535b86d29365e1..f9c355e89e5a88 100644 --- a/packages/expo-widgets/build/index.d.ts.map +++ b/packages/expo-widgets/build/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC"} \ No newline at end of file diff --git a/packages/expo-widgets/build/index.js b/packages/expo-widgets/build/index.js index f8a711af8c0098..210c0e3ddd1ae2 100644 --- a/packages/expo-widgets/build/index.js +++ b/packages/expo-widgets/build/index.js @@ -1,2 +1,3 @@ -export {}; +export * from './Widgets'; +export * from './Widgets.types'; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/expo-widgets/build/index.js.map b/packages/expo-widgets/build/index.js.map index 57f378a5305d26..8f43b6faedab5f 100644 --- a/packages/expo-widgets/build/index.js.map +++ b/packages/expo-widgets/build/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC","sourcesContent":["export * from './Widgets';\nexport * from './Widgets.types';\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/build/serializer.d.ts b/packages/expo-widgets/build/serializer.d.ts new file mode 100644 index 00000000000000..74ff8079840900 --- /dev/null +++ b/packages/expo-widgets/build/serializer.d.ts @@ -0,0 +1,3 @@ +import { ExpoLiveActivityEntry, ExpoTimelineEntry, WidgetFamily } from './Widgets.types'; +export declare const serialize: (entry: Record | ExpoLiveActivityEntry) => string; +//# sourceMappingURL=serializer.d.ts.map \ No newline at end of file diff --git a/packages/expo-widgets/build/serializer.d.ts.map b/packages/expo-widgets/build/serializer.d.ts.map new file mode 100644 index 00000000000000..a481519fe46bf1 --- /dev/null +++ b/packages/expo-widgets/build/serializer.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAyBzF,eAAO,MAAM,SAAS,GACpB,OAAO,MAAM,CAAC,YAAY,EAAE,iBAAiB,EAAE,CAAC,GAAG,qBAAqB,WAGzE,CAAC"} \ No newline at end of file diff --git a/packages/expo-widgets/build/serializer.js b/packages/expo-widgets/build/serializer.js new file mode 100644 index 00000000000000..5a011a662bb9fc --- /dev/null +++ b/packages/expo-widgets/build/serializer.js @@ -0,0 +1,26 @@ +const getName = (value) => { + if (typeof value === 'string') { + return value; + } + else if (typeof value === 'function') { + return value.name; + } + return value; +}; +const replacer = (key, value) => { + switch (key) { + case 'type': + return getName(value); + case '_owner': + case '_store': + case 'ref': + case 'key': + return; + default: + return value; + } +}; +export const serialize = (entry) => { + return JSON.stringify(entry, replacer); +}; +//# sourceMappingURL=serializer.js.map \ No newline at end of file diff --git a/packages/expo-widgets/build/serializer.js.map b/packages/expo-widgets/build/serializer.js.map new file mode 100644 index 00000000000000..428f1ea937ee4d --- /dev/null +++ b/packages/expo-widgets/build/serializer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"serializer.js","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,GAAG,CAAC,KAAwB,EAAE,EAAE;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;SAAM,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC,IAAI,CAAC;IACpB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAE,KAAU,EAAE,EAAE;IAC3C,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;QACxB,KAAK,QAAQ,CAAC;QACd,KAAK,QAAQ,CAAC;QACd,KAAK,KAAK,CAAC;QACX,KAAK,KAAK;YACR,OAAO;QACT;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,KAAwE,EACxE,EAAE;IACF,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;AACzC,CAAC,CAAC","sourcesContent":["import { ExpoLiveActivityEntry, ExpoTimelineEntry, WidgetFamily } from './Widgets.types';\n\nconst getName = (value: string | Function) => {\n if (typeof value === 'string') {\n return value;\n } else if (typeof value === 'function') {\n return value.name;\n }\n return value;\n};\n\nconst replacer = (key: string, value: any) => {\n switch (key) {\n case 'type':\n return getName(value);\n case '_owner':\n case '_store':\n case 'ref':\n case 'key':\n return;\n default:\n return value;\n }\n};\n\nexport const serialize = (\n entry: Record | ExpoLiveActivityEntry\n) => {\n return JSON.stringify(entry, replacer);\n};\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/ios/Brotli+Data.swift b/packages/expo-widgets/ios/Brotli+Data.swift new file mode 100644 index 00000000000000..06167437d9aa23 --- /dev/null +++ b/packages/expo-widgets/ios/Brotli+Data.swift @@ -0,0 +1,32 @@ +import Foundation +import Compression + +extension Data { + func brotliCompressed() throws -> Data { + var output = Data() + + let filter = try OutputFilter(.compress, using: .brotli, bufferCapacity: 65_536) { chunk in + if let chunk = chunk { + output.append(chunk) + } + } + + try filter.write(self) + try filter.finalize() + return output + } + + func brotliDecompressed() throws -> Data { + var output = Data() + + let filter = try OutputFilter(.decompress, using: .brotli, bufferCapacity: 65_536) { chunk in + if let chunk = chunk { + output.append(chunk) + } + } + + try filter.write(self) + try filter.finalize() + return output + } +} diff --git a/packages/expo-widgets/ios/ExpoWidgets.podspec b/packages/expo-widgets/ios/ExpoWidgets.podspec index 7adcb15e93611b..3363997cd2b28e 100644 --- a/packages/expo-widgets/ios/ExpoWidgets.podspec +++ b/packages/expo-widgets/ios/ExpoWidgets.podspec @@ -18,6 +18,7 @@ Pod::Spec.new do |s| s.static_framework = true s.dependency 'ExpoModulesCore' + s.dependency 'ExpoUI' # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/packages/expo-widgets/ios/Widgets/AppIntent.swift b/packages/expo-widgets/ios/Widgets/AppIntent.swift new file mode 100644 index 00000000000000..6cdb370749e956 --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/AppIntent.swift @@ -0,0 +1,66 @@ +import AppIntents +import JavaScriptCore +import WidgetKit + +@available(iOS 16.0, *) +struct WidgetUserInteraction: AppIntent { + // title is not used for non-discoverable intents, but it is required + static var title: LocalizedStringResource = "User Interaction" + static var isDiscoverable: Bool = false + @Parameter(title: "source") + var source: String? + + @Parameter(title: "target") + var target: String? + + init() {} + init(source: String?, target: String?) { + self.source = source + self.target = target + } + + func perform() async throws -> some IntentResult { + guard let source else { + return .result() + } + + WidgetsEvents.shared.sendNotification(type: .userEvent, data: [ + "source": source as Any, + "target": target as Any, + "timestamp": Date().timeIntervalSince1970 + ]) + + WidgetCenter.shared.reloadTimelines(ofKind: source) + + return .result() + } +} + +@available(iOS 16.0, *) +struct LiveActivityUserInteraction: LiveActivityIntent { + // title is not used for non-discoverable intents, but it is required + static var title: LocalizedStringResource = "User Interaction" + static var isDiscoverable: Bool = false + + @Parameter(title: "source") + var source: String? + + @Parameter(title: "target") + var target: String? + + init() {} + init(source: String?, target: String?) { + self.source = source + self.target = target + } + + func perform() async throws -> some IntentResult { + WidgetsEvents.shared.sendNotification(type: .userEvent, data: [ + "source": source as Any, + "target": target as Any, + "timestamp": Date().timeIntervalSince1970 + ]) + + return .result() + } +} diff --git a/packages/expo-widgets/ios/Widgets/Buttons.swift b/packages/expo-widgets/ios/Widgets/Buttons.swift new file mode 100644 index 00000000000000..e118cccc282cc4 --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/Buttons.swift @@ -0,0 +1,58 @@ +import SwiftUI +import ExpoModulesCore +import ExpoUI + +final class ButtonProps: ExpoUI.ButtonProps { + @Field var source: String? + @Field var target: String? +} + +@available(iOS 17.0, *) +struct WidgetButtonView: ExpoSwiftUI.View { + @ObservedObject var props: ButtonProps + + var body: some View { + SwiftUI.Button( + role: props.role?.toNativeRole(), + intent: WidgetUserInteraction( + source: props.source, + target: props.target + ) + ) { + if let label = props.label { + if let systemImage = props.systemImage { + Label(title: { Text(label) }, icon: { Image(systemName: systemImage) }) + } else { + Text(label) + } + } else { + Children() + } + } + } +} + +@available(iOS 17.0, *) +struct LiveActivityButtonView: ExpoSwiftUI.View { + @ObservedObject var props: ButtonProps + + var body: some View { + SwiftUI.Button( + role: props.role?.toNativeRole(), + intent: LiveActivityUserInteraction( + source: props.source, + target: props.target + ) + ) { + if let label = props.label { + if let systemImage = props.systemImage { + Label(title: { Text(label) }, icon: { Image(systemName: systemImage) }) + } else { + Text(label) + } + } else { + Children() + } + } + } +} diff --git a/packages/expo-widgets/ios/Widgets/DynamicView.swift b/packages/expo-widgets/ios/Widgets/DynamicView.swift new file mode 100644 index 00000000000000..a30dfdfb83a2af --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/DynamicView.swift @@ -0,0 +1,130 @@ +import SwiftUI +import ExpoModulesCore +import ExpoUI + +// TODO(@jakex7): Hack to satisfy ExpoSwiftUI.AnyChild with random UUID value +class NodeIdentityWrapper { + let id: UUID + init(id: UUID) { + self.id = id + } +} +extension ObjectIdentifier: @retroactive Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(String(describing: self)) + } +} + +public struct WidgetsDynamicView: View, ExpoSwiftUI.AnyChild { + let node: [String: Any] + let source: String + let kind: WidgetsKind + + let uuid = NodeIdentityWrapper(id: UUID()) + public var id: ObjectIdentifier { + ObjectIdentifier(uuid) + } + + public init(source: String, kind: WidgetsKind, node: [String: Any]) { + self.source = source + self.kind = kind + self.node = node + } + + @ViewBuilder + public var body: some View { + switch node["type"] as? String { + case "Text": + if let rawProps = node["props"] as? [String: Any], + let children = rawProps["children"] as? String { + render(TextView.self, TextViewProps.self) { props in + props.text = children + } + } else { + EmptyView() + } + case "HStack": + render(HStackView.self, HStackViewProps.self, updateProps: updateChildren) + case "VStack": + render(VStackView.self, VStackViewProps.self, updateProps: updateChildren) + case "ZStack": + render(ZStackView.self, ZStackViewProps.self, updateProps: updateChildren) + case "Rectangle": + render(RectangleView.self, RectangleViewProps.self) + case "RoundedRectangle": + render(RoundedRectangleView.self, RoundedRectangleViewProps.self) + case "Capsule": + render(CapsuleView.self, CapsuleViewProps.self) + case "Circle": + render(CircleView.self, CircleViewProps.self) + case "Image": + render(ImageView.self, ImageViewProps.self) + case "Divider": + render(DividerView.self, DividerProps.self) + case "Ellipse": + render(EllipseView.self, EllipseViewProps.self) + case "Label": + render(LabelView.self, LabelViewProps.self) + case "Progress": + render(ProgressView.self, ProgressViewProps.self) + case "Spacer": + render(SpacerView.self, SpacerViewProps.self) + case "UnevenRoundedRectangle": + render(UnevenRoundedRectangleView.self, UnevenRoundedRectangleViewProps.self) + case "Gauge": + render(GaugeView.self, GaugeProps.self) + case "Button": + if #available(iOS 17.0, *) { + switch kind { + case .widget: + render(WidgetButtonView.self, ButtonProps.self) { buttonProps in + buttonProps.source = source + } + case .liveActivity: + render(LiveActivityButtonView.self, ButtonProps.self) { buttonProps in + buttonProps.source = source + } + } + } else { + render(ExpoUI.Button.self, ExpoUI.ButtonProps.self, updateProps: updateChildren) + } + + default: + EmptyView() + } + } + + // MARK: - Render Method + + @ViewBuilder + private func render(_ viewType: V.Type, _ propsType: P.Type, updateProps: ((_ initialProps: P) throws -> Void)? = nil) -> some View + where P: UIBaseViewProps, V: ExpoSwiftUI.View, V.Props == P { + // immediately invoked closure {}() here because we can't use 'do-catch' inside @ViewBuilder + { + do { + if let rawProps = node["props"] as? [String: Any] { + let props = try propsType.init(rawProps: rawProps, context: WidgetsContext.shared.context) + try updateProps?(props) + return AnyView(UIBaseView(props: props)) + } + return AnyView(EmptyView()) + } catch { + return AnyView(EmptyView()) + } + }() + } + + // MARK: - Function that sets children as DynamicView + + private func updateChildren

(_ initialProps: P) throws + where P: UIBaseViewProps { + if let props = node["props"] as? [String: Any] { + if let children = props["children"] as? [[String: Any]] { + initialProps.children = children.map { WidgetsDynamicView(source: source, kind: kind, node: $0) } + } else if let child = props["children"] as? [String: Any] { + initialProps.children = [WidgetsDynamicView(source: source, kind: kind, node: child)] + } + } + } +} diff --git a/packages/expo-widgets/ios/Widgets/EntryView.swift b/packages/expo-widgets/ios/Widgets/EntryView.swift new file mode 100644 index 00000000000000..ed600c2742773d --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/EntryView.swift @@ -0,0 +1,25 @@ +import SwiftUI +import ExpoModulesCore +import WidgetKit + +public struct WidgetsEntryView: View { + @Environment(\.widgetFamily) var widgetFamily + var entry: WidgetsTimelineProvider.Entry + + public init(entry: WidgetsTimelineProvider.Entry) { + self.entry = entry + } + + public var body: some View { + if let node = entry.node { + if #available(iOS 17.0, *) { + WidgetsDynamicView(source: entry.source, kind: .widget, node: node) + .containerBackground(.clear, for: .widget) + } else { + WidgetsDynamicView(source: entry.source, kind: .widget, node: node) + } + } else { + EmptyView() + } + } +} diff --git a/packages/expo-widgets/ios/Widgets/LiveActivity.swift b/packages/expo-widgets/ios/Widgets/LiveActivity.swift new file mode 100644 index 00000000000000..44de69aa13d650 --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/LiveActivity.swift @@ -0,0 +1,59 @@ +import SwiftUI +import WidgetKit +import ExpoModulesCore +import ActivityKit + +struct LiveActivityAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + var name: String + } +} + +@available(iOS 16.1, *) +public struct WidgetLiveActivity: Widget { + let widgetContext: AppContext = AppContext() + + public init() {} + + public var body: some WidgetConfiguration { + ActivityConfiguration(for: LiveActivityAttributes.self) { context in + let nodes = getLiveActivityNodes(forName: context.state.name) + return liveActivitySection("banner", source: context.activityID, nodes: nodes) + } dynamicIsland: { context in + let nodes = getLiveActivityNodes(forName: context.state.name) + return DynamicIsland { + expandedContent(source: context.activityID, nodes: nodes) + } compactLeading: { + liveActivitySection("compactLeading", source: context.activityID, nodes: nodes) + } compactTrailing: { + liveActivitySection("compactTrailing", source: context.activityID, nodes: nodes) + } minimal: { + liveActivitySection("minimal", source: context.activityID, nodes: nodes) + } + .widgetURL(getLiveActivityUrl(forName: context.state.name)) + } + } + + @DynamicIslandExpandedContentBuilder + private func expandedContent(source: String, nodes: [String: Any]?) -> DynamicIslandExpandedContent { + DynamicIslandExpandedRegion(.center) { + liveActivitySection("expandedCenter", source: source, nodes: nodes) + } + DynamicIslandExpandedRegion(.leading) { + liveActivitySection("expandedLeading", source: source, nodes: nodes) + } + DynamicIslandExpandedRegion(.trailing) { + liveActivitySection("expandedTrailing", source: source, nodes: nodes) + } + DynamicIslandExpandedRegion(.bottom) { + liveActivitySection("expandedBottom", source: source, nodes: nodes) + } + } + + private func liveActivitySection(_ sectionName: String, source: String, nodes: [String: Any]?) -> some View { + guard let node = nodes?[sectionName] as? [String: Any] else { + return AnyView(EmptyView()) + } + return AnyView(WidgetsDynamicView(source: source, kind: .liveActivity, node: node)) + } +} diff --git a/packages/expo-widgets/ios/Widgets/TimelineEntry.swift b/packages/expo-widgets/ios/Widgets/TimelineEntry.swift new file mode 100644 index 00000000000000..34fa8a05f92b29 --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/TimelineEntry.swift @@ -0,0 +1,7 @@ +import WidgetKit + +public struct WidgetsTimelineEntry: WidgetKit.TimelineEntry { + public let date: Date + public let source: String + public let node: [String: Any]? +} diff --git a/packages/expo-widgets/ios/Widgets/TimelineProvider.swift b/packages/expo-widgets/ios/Widgets/TimelineProvider.swift new file mode 100644 index 00000000000000..2b30f702c244b0 --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/TimelineProvider.swift @@ -0,0 +1,31 @@ +import WidgetKit + +public struct WidgetsTimelineProvider: TimelineProvider { + public func placeholder(in context: Context) -> WidgetsTimelineEntry { + WidgetsTimelineEntry(date: Date(), source: name, node: nil) + } + + public func getSnapshot(in context: Context, completion: @escaping @Sendable (WidgetsTimelineEntry) -> Void) { + completion(WidgetsTimelineEntry(date: Date(), source: name, node: nil)) + } + + public func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline) -> Void) { + let groupIdentifier = Bundle.main.object(forInfoDictionaryKey: "ExpoWidgetsAppGroupIdentifier") as? String + guard let groupIdentifier else { + fatalError("Could not get the app group identifier from Info.plist") + } + + let entries = parseTimeline(identifier: groupIdentifier, name: name, family: context.family) + + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) + } + + public typealias Entry = WidgetsTimelineEntry + + let name: String + + public init(name: String) { + self.name = name + } +} diff --git a/packages/expo-widgets/ios/Widgets/Type.swift b/packages/expo-widgets/ios/Widgets/Type.swift new file mode 100644 index 00000000000000..6a2ee9437f4dcb --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/Type.swift @@ -0,0 +1,19 @@ +import WidgetKit + +public enum WidgetsKind { + case widget + case liveActivity +} + +func getKeyFor(widgetFamily: WidgetFamily) -> String { + switch widgetFamily { + case .systemSmall: return "systemSmall" + case .systemMedium: return "systemMedium" + case .systemLarge: return "systemLarge" + case .systemExtraLarge: return "systemExtraLarge" + case .accessoryCircular: return "accessoryCircular" + case .accessoryRectangular: return "accessoryRectangular" + case .accessoryInline: return "accessoryInline" + default: return "systemSmall" + } +} diff --git a/packages/expo-widgets/ios/Widgets/Utils.swift b/packages/expo-widgets/ios/Widgets/Utils.swift new file mode 100644 index 00000000000000..0e89a43097e851 --- /dev/null +++ b/packages/expo-widgets/ios/Widgets/Utils.swift @@ -0,0 +1,44 @@ +import WidgetKit + +func parseTimeline(identifier: String, name: String, family: WidgetFamily) -> [WidgetsTimelineEntry] { + var json = WidgetsStorage.getString(forKey: "__expo_widgets_\(name)") ?? "" + let props = WidgetsStorage.getDictionary(forKey: "__expo_widgets_\(name)_props") ?? [:] + + // Inject props + for (key, value) in props { + let placeholder = "{{\(key)}}" + if let stringValue = value as? String { + json = json.replacingOccurrences(of: placeholder, with: stringValue) + } else if let numberValue = value as? NSNumber { + json = json.replacingOccurrences(of: placeholder, with: numberValue.stringValue) + } + } + + guard let data = try? JSONSerialization.jsonObject(with: json.data(using: .utf8)!) as? [String: Any] else { + return [] + } + guard let dataForFamily = data[getKeyFor(widgetFamily: family)] as? [[String: Any]] else { + return [] + } + + return dataForFamily.map { + WidgetsTimelineEntry( + date: Date(timeIntervalSince1970: Double($0["timestamp"] as? Int ?? 0) / 1000.0), + source: name, node: $0["content"] as? [String: Any] + ) + } +} + +func getLiveActivityNodes(forName name: String) -> [String: Any] { + let data = WidgetsStorage.getData(forKey: "__expo_widgets_live_activity_\(name)") + let decompressedData = try? data?.brotliDecompressed() ?? Data() + return (try? JSONSerialization.jsonObject(with: decompressedData!) as? [String: Any]) ?? [:] +} + +func getLiveActivityUrl(forName name: String) -> URL? { + let data = WidgetsStorage.getData(forKey: "__expo_widgets_live_activity_\(name)_url") + if let data, let url = String(data: data, encoding: .utf8) { + return URL(string: url) + } + return nil +} diff --git a/packages/expo-widgets/ios/WidgetsContext.swift b/packages/expo-widgets/ios/WidgetsContext.swift new file mode 100644 index 00000000000000..773736591450af --- /dev/null +++ b/packages/expo-widgets/ios/WidgetsContext.swift @@ -0,0 +1,7 @@ +import ExpoModulesCore + +// Shared AppContext for widgets +struct WidgetsContext { + static let shared = WidgetsContext() + let context: AppContext = AppContext() +} diff --git a/packages/expo-widgets/ios/WidgetsEvents.swift b/packages/expo-widgets/ios/WidgetsEvents.swift new file mode 100644 index 00000000000000..0dec3e31bbcc04 --- /dev/null +++ b/packages/expo-widgets/ios/WidgetsEvents.swift @@ -0,0 +1,16 @@ +public enum WidgetsEventType: String { + case userEvent = "ExpoWidgetsUserInteraction" +} + +public class WidgetsEvents { + public static let shared = WidgetsEvents() + + private init() {} + + public func sendNotification(type: WidgetsEventType, data: [String: Any]) { + var eventData = data + eventData["type"] = type.rawValue + let userInfo = ["eventData": eventData] as [String: Any] + NotificationCenter.default.post(name: onUserInteractionNotification, object: nil, userInfo: userInfo) + } +} diff --git a/packages/expo-widgets/ios/WidgetsExceptions.swift b/packages/expo-widgets/ios/WidgetsExceptions.swift new file mode 100644 index 00000000000000..200a136bde569c --- /dev/null +++ b/packages/expo-widgets/ios/WidgetsExceptions.swift @@ -0,0 +1,13 @@ +import ExpoModulesCore + +internal final class LiveActivitiesNotSupportedException: Exception, @unchecked Sendable { + override var reason: String { + "Live Activities are not supported on this device" + } +} + +internal final class StartLiveActivityException: GenericException, @unchecked Sendable { + override var reason: String { + "Failed to start live activity: \(param)" + } +} diff --git a/packages/expo-widgets/ios/WidgetsModule.swift b/packages/expo-widgets/ios/WidgetsModule.swift index a6aa1e9af4adef..20eb5290945fc3 100644 --- a/packages/expo-widgets/ios/WidgetsModule.swift +++ b/packages/expo-widgets/ios/WidgetsModule.swift @@ -1,7 +1,104 @@ import ExpoModulesCore +import ActivityKit +import WidgetKit +import JavaScriptCore -public class WidgetsModule: Module { +private let onUserInteraction = "onExpoWidgetsUserInteraction" +let onUserInteractionNotification = Notification.Name(onUserInteraction) + +public final class WidgetsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWidgets") + + Events(onUserInteraction) + + OnStartObserving { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleUserInteractionNotification), + name: onUserInteractionNotification, + object: nil + ) + } + + OnStopObserving { + NotificationCenter.default.removeObserver( + self, + name: onUserInteractionNotification, + object: nil + ) + } + + Function("reloadWidget") { (timeline: String?) in + if let timeline = timeline { + WidgetCenter.shared.reloadTimelines(ofKind: timeline) + } else { + WidgetCenter.shared.reloadAllTimelines() + } + } + + Function("updateWidget") { (name: String, data: String, props: [String: Any]?, updateFunction: String?) in + WidgetsStorage.set(data, forKey: "__expo_widgets_\(name)") + if let props { + WidgetsStorage.set(props, forKey: "__expo_widgets_\(name)_props") + } + } + + Function("startLiveActivity") { (name: String, nodes: String, url: URL?) throws -> String in + guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() } + guard ActivityAuthorizationInfo().areActivitiesEnabled else { + throw LiveActivitiesNotSupportedException() + } + + let nodesData = nodes.data(using: .utf8) + guard let compressedData = try nodesData?.brotliCompressed() else { + throw LiveActivitiesNotSupportedException() + } + + WidgetsStorage.set(compressedData, forKey: "__expo_widgets_live_activity_\(name)") + if let url { + WidgetsStorage.set(url.absoluteString, forKey: "__expo_widgets_live_activity_\(name)_url") + } + + do { + let initialState = LiveActivityAttributes.ContentState(name: name) + + let activity = try Activity.request( + attributes: LiveActivityAttributes(), + content: .init(state: initialState, staleDate: nil), + pushType: nil + ) + + return activity.id + } catch { + throw StartLiveActivityException(error.localizedDescription) + } + } + + Function("updateLiveActivity") { (id: String, name: String, nodes: String) throws in + guard #available(iOS 16.2, *) else { throw LiveActivitiesNotSupportedException() } + + guard let activity = Activity.activities.first(where: { $0.id == id }) + else { throw LiveActivitiesNotSupportedException() } + + let nodesData = nodes.data(using: .utf8) + guard let compressedData = try nodesData?.brotliCompressed() else { + throw LiveActivitiesNotSupportedException() + } + + WidgetsStorage.set(compressedData, forKey: "__expo_widgets_live_activity_\(name)") + + Task { + let newState = LiveActivityAttributes.ContentState(name: name) + await activity.update(ActivityContent(state: newState, staleDate: nil)) + } + } + } + + @objc func handleUserInteractionNotification(_ notification: Notification) { + guard let userInfo = notification.userInfo as? [String: Any], + let eventData = userInfo["eventData"] as? [String: Any] + else { return } + self.sendEvent(onUserInteraction, eventData) } } diff --git a/packages/expo-widgets/ios/WidgetsStorage.swift b/packages/expo-widgets/ios/WidgetsStorage.swift new file mode 100644 index 00000000000000..40c86a9b473d65 --- /dev/null +++ b/packages/expo-widgets/ios/WidgetsStorage.swift @@ -0,0 +1,46 @@ +enum WidgetsStorage { + static var appGroupIdentifier: String? = Bundle.main.object(forInfoDictionaryKey: "ExpoWidgetsAppGroupIdentifier") as? String + static let defaults = UserDefaults(suiteName: appGroupIdentifier) + + static func set(_ value: [String: Any], forKey key: String) { + guard let defaults else { return } + + defaults.set(value, forKey: key) + } + + static func set(_ value: String, forKey key: String) { + guard let defaults else { return } + + defaults.set(value, forKey: key) + } + + static func set(_ value: Data, forKey key: String) { + guard let defaults else { return } + + defaults.set(value, forKey: key) + } + + static func getDictionary(forKey key: String) -> [String: Any]? { + guard let defaults else { return nil } + + return defaults.dictionary(forKey: key) + } + + static func getData(forKey key: String) -> Data? { + guard let defaults else { return nil } + + return defaults.data(forKey: key) + } + + static func getString(forKey key: String) -> String? { + guard let defaults else { return nil } + + return defaults.string(forKey: key) + } + + static func removeObject(forKey key: String) { + guard let defaults else { return } + + defaults.removeObject(forKey: key) + } +} diff --git a/packages/expo-widgets/package.json b/packages/expo-widgets/package.json index babcdc3b19ccdf..c489808e8370b8 100644 --- a/packages/expo-widgets/package.json +++ b/packages/expo-widgets/package.json @@ -31,7 +31,8 @@ "license": "MIT", "homepage": "https://docs.expo.dev/versions/latest/sdk/widgets/", "dependencies": { - "@expo/plist": "^0.4.7" + "@expo/plist": "^0.4.7", + "@expo/ui": "0.2.0-beta.10" }, "devDependencies": { "expo-module-scripts": "^5.0.7" diff --git a/packages/expo-widgets/plugin/build/index.js b/packages/expo-widgets/plugin/build/index.js index 82fc65077acda3..26e03902a378ba 100644 --- a/packages/expo-widgets/plugin/build/index.js +++ b/packages/expo-widgets/plugin/build/index.js @@ -27,10 +27,7 @@ const withWidgets = (config, props) => { groupIdentifier = `group.${config.ios.bundleIdentifier}`; console.log(`No groupIdentifier provided, fallback to: ${groupIdentifier}`); } - let widgets = props.widgets; - if (!widgets) { - widgets = []; - } + const widgets = props.widgets ?? []; const enablePushNotifications = props.enablePushNotifications ?? false; const frequentUpdates = props.frequentUpdates ?? false; let sharedFiles = []; diff --git a/packages/expo-widgets/plugin/build/xcode/addBuildPhases.js b/packages/expo-widgets/plugin/build/xcode/addBuildPhases.js index 5f15a338efcbec..1bf2eca4b2c9de 100644 --- a/packages/expo-widgets/plugin/build/xcode/addBuildPhases.js +++ b/packages/expo-widgets/plugin/build/xcode/addBuildPhases.js @@ -1,40 +1,6 @@ "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); Object.defineProperty(exports, "__esModule", { value: true }); exports.addBuildPhases = addBuildPhases; -const util = __importStar(require("util")); function addBuildPhases(xcodeProject, { targetUuid, groupName, productFile, widgetFiles, }) { const buildPath = `""`; const folderType = 'app_extension'; @@ -46,7 +12,7 @@ function addBuildPhases(xcodeProject, { targetUuid, groupName, productFile, widg .buildPhaseObject('PBXCopyFilesBuildPhase', groupName, productFile.target) .files.push({ value: productFile.uuid, - comment: util.format('%s in %s', productFile.basename, productFile.group), // longComment(file); + comment: `${productFile.basename} in ${productFile.group}`, }); xcodeProject.addToPbxBuildFileSection(productFile); // Frameworks build phase diff --git a/packages/expo-widgets/plugin/build/xcode/addToPbxProjectSection.js b/packages/expo-widgets/plugin/build/xcode/addToPbxProjectSection.js index 1bd65ff9fcbe39..3416f37c62573d 100644 --- a/packages/expo-widgets/plugin/build/xcode/addToPbxProjectSection.js +++ b/packages/expo-widgets/plugin/build/xcode/addToPbxProjectSection.js @@ -3,12 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.addToPbxProjectSection = addToPbxProjectSection; function addToPbxProjectSection(xcodeProject, target) { xcodeProject.addToPbxProjectSection(target); + const pbxProjectSection = xcodeProject.pbxProjectSection(); + const project = pbxProjectSection[xcodeProject.getFirstProject().uuid]; // Add target attributes to project section - if (!xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid].attributes - .TargetAttributes) { - xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid].attributes.TargetAttributes = {}; + if (!project.attributes.TargetAttributes) { + project.attributes.TargetAttributes = {}; } - xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid].attributes.TargetAttributes[target.uuid] = { + project.attributes.TargetAttributes[target.uuid] = { LastSwiftMigration: 1250, }; } diff --git a/packages/expo-widgets/plugin/src/index.ts b/packages/expo-widgets/plugin/src/index.ts index ad96ce79f2acd4..36349ec481a4f4 100644 --- a/packages/expo-widgets/plugin/src/index.ts +++ b/packages/expo-widgets/plugin/src/index.ts @@ -43,11 +43,7 @@ const withWidgets: ConfigPlugin = (config, props) console.log(`No groupIdentifier provided, fallback to: ${groupIdentifier}`); } - let widgets = props.widgets; - if (!widgets) { - widgets = []; - } - + const widgets = props.widgets ?? []; const enablePushNotifications = props.enablePushNotifications ?? false; const frequentUpdates = props.frequentUpdates ?? false; diff --git a/packages/expo-widgets/plugin/src/xcode/addBuildPhases.ts b/packages/expo-widgets/plugin/src/xcode/addBuildPhases.ts index bed029f692282b..19df40dbf8e7d8 100644 --- a/packages/expo-widgets/plugin/src/xcode/addBuildPhases.ts +++ b/packages/expo-widgets/plugin/src/xcode/addBuildPhases.ts @@ -1,5 +1,4 @@ import { XcodeProject } from 'expo/config-plugins'; -import * as util from 'util'; export function addBuildPhases( xcodeProject: XcodeProject, @@ -47,7 +46,7 @@ export function addBuildPhases( .buildPhaseObject('PBXCopyFilesBuildPhase', groupName, productFile.target) .files.push({ value: productFile.uuid, - comment: util.format('%s in %s', productFile.basename, productFile.group), // longComment(file); + comment: `${productFile.basename} in ${productFile.group}`, }); xcodeProject.addToPbxBuildFileSection(productFile); diff --git a/packages/expo-widgets/plugin/src/xcode/addToPbxProjectSection.ts b/packages/expo-widgets/plugin/src/xcode/addToPbxProjectSection.ts index c0ae9b3f8c7528..daf44a31583db4 100644 --- a/packages/expo-widgets/plugin/src/xcode/addToPbxProjectSection.ts +++ b/packages/expo-widgets/plugin/src/xcode/addToPbxProjectSection.ts @@ -3,18 +3,13 @@ import { XcodeProject } from 'expo/config-plugins'; export function addToPbxProjectSection(xcodeProject: XcodeProject, target: { uuid: string }) { xcodeProject.addToPbxProjectSection(target); + const pbxProjectSection = xcodeProject.pbxProjectSection(); + const project = pbxProjectSection[xcodeProject.getFirstProject().uuid]; // Add target attributes to project section - if ( - !xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid].attributes - .TargetAttributes - ) { - xcodeProject.pbxProjectSection()[ - xcodeProject.getFirstProject().uuid - ].attributes.TargetAttributes = {}; + if (!project.attributes.TargetAttributes) { + project.attributes.TargetAttributes = {}; } - xcodeProject.pbxProjectSection()[xcodeProject.getFirstProject().uuid].attributes.TargetAttributes[ - target.uuid - ] = { + project.attributes.TargetAttributes[target.uuid] = { LastSwiftMigration: 1250, }; } diff --git a/packages/expo-widgets/scripts/autolinking.rb b/packages/expo-widgets/scripts/autolinking.rb index 651db0ef6ac93a..ceeadc6d62a4de 100644 --- a/packages/expo-widgets/scripts/autolinking.rb +++ b/packages/expo-widgets/scripts/autolinking.rb @@ -25,10 +25,10 @@ def use_expo_native_module!(config_command = $default_command) def expo_widgets_post_install(installer) installer.pods_project.targets.each do |target| - if target.name == 'ExpoModulesCore' + if target.name == 'ExpoModulesCore' || target.name == 'ExpoUI' target.build_configurations.each do |config| config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'No' end end end -end \ No newline at end of file +end diff --git a/packages/expo-widgets/src/ExpoWidgets.ts b/packages/expo-widgets/src/ExpoWidgets.ts new file mode 100644 index 00000000000000..3d3526bd6cf462 --- /dev/null +++ b/packages/expo-widgets/src/ExpoWidgets.ts @@ -0,0 +1,12 @@ +import { NativeModule, requireNativeModule } from 'expo'; + +import { ExpoWidgetsEvents } from './Widgets.types'; + +declare class ExpoWidgetModule extends NativeModule { + reloadWidget(timeline?: string): void; + updateWidget(name: string, data: string, props?: Record): void; + startLiveActivity(name: string, nodes: string, url?: string): string; + updateLiveActivity(id: string, name: string, nodes: string): string; +} + +export default requireNativeModule('ExpoWidgets'); diff --git a/packages/expo-widgets/src/Widgets.ts b/packages/expo-widgets/src/Widgets.ts new file mode 100644 index 00000000000000..9ee7bed9cc7164 --- /dev/null +++ b/packages/expo-widgets/src/Widgets.ts @@ -0,0 +1,74 @@ +import React from 'react'; + +import ExpoWidgetModule from './ExpoWidgets'; +import { + ExpoTimelineEntry, + LiveActivityComponent, + WidgetFamily, + WidgetProps, +} from './Widgets.types'; +import { supportedFamilies } from './constants'; +import { serialize } from './serializer'; + +export const startLiveActivity = ( + name: string, + liveActivity: LiveActivityComponent, + url?: string +) => { + const text = serialize(liveActivity()); + return ExpoWidgetModule.startLiveActivity(name, text, url); +}; + +export const updateLiveActivity = ( + id: string, + name: string, + liveActivity: LiveActivityComponent +) => { + const text = serialize(liveActivity()); + ExpoWidgetModule.updateLiveActivity(id, name, text); +}; + +export const updateWidgetTimeline = ( + name: string, + dates: Date[], + widget: (p: WidgetProps) => React.JSX.Element, + props?: T +) => { + const fakeProps = Object.keys(props || {}).reduce( + (acc, key) => { + acc[key] = `{{${key}}}`; + return acc; + }, + {} as Record + ); + + const data: Record = supportedFamilies + .map((family) => ({ + family, + entries: dates.map((date) => ({ + timestamp: date.getTime(), + content: widget({ date, family, ...(fakeProps as T) }), + })), + })) + .reduce( + (acc, { family, entries }) => { + acc[family] = entries; + return acc; + }, + {} as Record + ); + + ExpoWidgetModule.updateWidget(name, serialize(data), props); + + ExpoWidgetModule.reloadWidget(); +}; + +export const updateWidgetSnapshot = ( + name: string, + widget: (p: WidgetProps) => React.JSX.Element, + props?: T +) => { + updateWidgetTimeline(name, [new Date()], widget, props || ({} as T)); +}; + +export const addEventListener: typeof ExpoWidgetModule.addListener = ExpoWidgetModule.addListener; diff --git a/packages/expo-widgets/src/Widgets.types.ts b/packages/expo-widgets/src/Widgets.types.ts new file mode 100644 index 00000000000000..046c0548955608 --- /dev/null +++ b/packages/expo-widgets/src/Widgets.types.ts @@ -0,0 +1,46 @@ +import { ReactNode } from 'react'; + +export type WidgetFamily = + | 'systemSmall' + | 'systemMedium' + | 'systemLarge' + | 'systemExtraLarge' + | 'accessoryCircular' + | 'accessoryRectangular' + | 'accessoryInline'; + +export type WidgetDimensions = { width: number; height: number }; + +export type WidgetProps = { + date: Date; + family: WidgetFamily; +} & T; + +export type ExpoTimelineEntry = { + timestamp: number; + content: ReactNode; +}; + +export type ExpoLiveActivityEntry = { + banner: ReactNode; + compactLeading?: ReactNode; + compactTrailing?: ReactNode; + minimal?: ReactNode; + expandedCenter?: ReactNode; + expandedLeading?: ReactNode; + expandedTrailing?: ReactNode; + expandedBottom?: ReactNode; +}; + +export type LiveActivityComponent = () => ExpoLiveActivityEntry; + +export type UserInteractionEvent = { + source: string; + target: string; + timestamp: number; + type: 'ExpoWidgetsUserInteraction'; +}; + +export type ExpoWidgetsEvents = { + onUserInteraction: (event: UserInteractionEvent) => void; +}; diff --git a/packages/expo-widgets/src/constants.ts b/packages/expo-widgets/src/constants.ts new file mode 100644 index 00000000000000..f1ddcc46d900d7 --- /dev/null +++ b/packages/expo-widgets/src/constants.ts @@ -0,0 +1,11 @@ +import { WidgetFamily } from './Widgets.types'; + +export const supportedFamilies: WidgetFamily[] = [ + 'systemSmall', + 'systemMedium', + 'systemLarge', + 'systemExtraLarge', + 'accessoryCircular', + 'accessoryRectangular', + 'accessoryInline', +]; diff --git a/packages/expo-widgets/src/index.ts b/packages/expo-widgets/src/index.ts index e69de29bb2d1d6..48a4691ea3c713 100644 --- a/packages/expo-widgets/src/index.ts +++ b/packages/expo-widgets/src/index.ts @@ -0,0 +1,2 @@ +export * from './Widgets'; +export * from './Widgets.types'; diff --git a/packages/expo-widgets/src/serializer.ts b/packages/expo-widgets/src/serializer.ts new file mode 100644 index 00000000000000..ca74388741d1b3 --- /dev/null +++ b/packages/expo-widgets/src/serializer.ts @@ -0,0 +1,30 @@ +import { ExpoLiveActivityEntry, ExpoTimelineEntry, WidgetFamily } from './Widgets.types'; + +const getName = (value: string | Function) => { + if (typeof value === 'string') { + return value; + } else if (typeof value === 'function') { + return value.name; + } + return value; +}; + +const replacer = (key: string, value: any) => { + switch (key) { + case 'type': + return getName(value); + case '_owner': + case '_store': + case 'ref': + case 'key': + return; + default: + return value; + } +}; + +export const serialize = ( + entry: Record | ExpoLiveActivityEntry +) => { + return JSON.stringify(entry, replacer); +}; From dc9d8dc6ec6cf500261286ebf2954b95da4fb71a Mon Sep 17 00:00:00 2001 From: Jakub Tkacz <32908614+Ubax@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:43:59 +0100 Subject: [PATCH 06/15] [expo-router] Fix withAnchor for deeply nested routes (#40528) # Why When using `withAnchor` to navigate to deeply nested routes (e.g., `/second/third/target`), the anchor route was only being loaded for the root level of the navigation stack. This meant that when navigating back from a deeply nested screen, users would not see the expected anchor screen at intermediate navigation levels. Fixes https://github.com/expo/expo/issues/40230 **Before** https://github.com/user-attachments/assets/ef97423e-3174-44bd-873a-776ea1387bf9 **After** https://github.com/user-attachments/assets/546563b4-e573-4bb7-8503-49a7f1602db7 # How The fix propagates the `initial` parameter through all nested `params` objects in the navigation payload, not just the root level. Previously, only `rootPayload.params.initial` was set, but nested navigators also need this flag to properly load their anchor routes. The change replaces: ```ts rootPayload.params.initial = !withAnchor; ``` With a loop that traverses all nested params: ```ts let currentParams = rootPayload.params; while (currentParams) { currentParams.initial = !withAnchor; currentParams = currentParams.params; } ``` Additionally, the condition `withAnchor !== undefined` was simplified to just `withAnchor` since the only meaningful case is when `withAnchor` is truthy. # Test Plan 1. Unit tests 2. Manual testing # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/expo-router/CHANGELOG.md | 1 + .../build/global-state/routing.d.ts.map | 2 +- .../expo-router/build/global-state/routing.js | 9 +++-- .../build/global-state/routing.js.map | 2 +- .../__tests__/initialRouteName.test.ios.tsx | 2 +- .../src/__tests__/push.test.ios.tsx | 2 +- .../expo-router/src/global-state/routing.ts | 9 +++-- .../src/link/__tests__/Link.test.ios.tsx | 36 +++++++++++++++++++ 8 files changed, 55 insertions(+), 8 deletions(-) diff --git a/packages/expo-router/CHANGELOG.md b/packages/expo-router/CHANGELOG.md index 39b715c391e29f..1f0eb3dcd28e5a 100644 --- a/packages/expo-router/CHANGELOG.md +++ b/packages/expo-router/CHANGELOG.md @@ -70,6 +70,7 @@ - [ios] fix build error 'Logger' is ambiguous ([#42229](https://github.com/expo/expo/pull/42229) by [@Ubax](https://github.com/Ubax)) - [ios] fix shadow color in native tabs ([#42125](https://github.com/expo/expo/pull/42125) by [@Ubax](https://github.com/Ubax)) - Preserve search params for loader data fetches ([#42227](https://github.com/expo/expo/pull/42227) by [@hassankhan](https://github.com/hassankhan)) +- fix withAnchor for deeply nested routes ([#40528](https://github.com/expo/expo/pull/40528) by [@Ubax](https://github.com/Ubax)) ### 💡 Others diff --git a/packages/expo-router/build/global-state/routing.d.ts.map b/packages/expo-router/build/global-state/routing.d.ts.map index 0296b23d11cd4c..0bba306b04cc32 100644 --- a/packages/expo-router/build/global-state/routing.d.ts.map +++ b/packages/expo-router/build/global-state/routing.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/global-state/routing.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,eAAe,EACpB,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAC3B,aAAa,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvC,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAUvD,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAWhD,UAAU,UAAU;IAClB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE;QACP,OAAO,EAAE,aAAa,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,MAAM,YAAY;WACV,CAAC,gBAAgB,GAAG,UAAU,CAAC,EAAE;2BACnB,IAAI;wBACX,MAAM,IAAI;;;;;;;gBASlB,gBAAgB,GAAG,UAAU;aAMhC,SAAS,CAAC,sBAAsB,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;CA6BjE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAE7D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE9D;AAED,wBAAgB,MAAM,SAGrB;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE/D;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE1D;AAED,wBAAgB,OAAO,CAAC,KAAK,GAAE,MAAU,QAMxC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAEhE;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE7D;AAED,wBAAgB,UAAU,SAKzB;AAED,wBAAgB,MAAM,SAMrB;AAED,wBAAgB,SAAS,IAAI,OAAO,CAenC;AAED,wBAAgB,UAAU,IAAI,OAAO,CAmBpC;AAED,wBAAgB,SAAS,CACvB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAM,OAO/E;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,eAAe,CAAC;IAEtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,MAAM,CAAC,YAAY,EAAE,IAAI,EAAE,OAAO,GAAE,aAAkB,QA4CrE;AA+GD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,iBAAiB,EAAE,YAAY,CAAC,GAAG,CAAC,uBAwB5E;AAKD,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,EACzB,gBAAgB,EAAE,eAAe,EAEjC,kBAAkB,GAAE,OAAe;;;;;;;;;;;;;;;;;;;;;EAoDpC"} \ No newline at end of file +{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/global-state/routing.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,eAAe,EACpB,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAC3B,aAAa,EACd,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvC,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAUvD,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAWhD,UAAU,UAAU;IAClB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE;QACP,OAAO,EAAE,aAAa,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,eAAO,MAAM,YAAY;WACV,CAAC,gBAAgB,GAAG,UAAU,CAAC,EAAE;2BACnB,IAAI;wBACX,MAAM,IAAI;;;;;;;gBASlB,gBAAgB,GAAG,UAAU;aAMhC,SAAS,CAAC,sBAAsB,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;CA6BjE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAE7D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE9D;AAED,wBAAgB,MAAM,SAGrB;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE/D;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE1D;AAED,wBAAgB,OAAO,CAAC,KAAK,GAAE,MAAU,QAMxC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAEhE;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,iBAAiB,QAE7D;AAED,wBAAgB,UAAU,SAKzB;AAED,wBAAgB,MAAM,SAMrB;AAED,wBAAgB,SAAS,IAAI,OAAO,CAenC;AAED,wBAAgB,UAAU,IAAI,OAAO,CAmBpC;AAED,wBAAgB,SAAS,CACvB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAM,OAO/E;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,eAAe,CAAC;IAEtC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,wBAAgB,MAAM,CAAC,YAAY,EAAE,IAAI,EAAE,OAAO,GAAE,aAAkB,QA4CrE;AAoHD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,iBAAiB,EAAE,YAAY,CAAC,GAAG,CAAC,uBAwB5E;AAKD,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,EACzB,gBAAgB,EAAE,eAAe,EAEjC,kBAAkB,GAAE,OAAe;;;;;;;;;;;;;;;;;;;;;EAoDpC"} \ No newline at end of file diff --git a/packages/expo-router/build/global-state/routing.js b/packages/expo-router/build/global-state/routing.js index 6cdc569f359c8c..987e254d279b05 100644 --- a/packages/expo-router/build/global-state/routing.js +++ b/packages/expo-router/build/global-state/routing.js @@ -263,7 +263,7 @@ function getNavigateAction(baseHref, options, type = 'NAVIGATE', withAnchor, sin else if (type === 'REPLACE' && navigationState.type === 'drawer') { type = 'JUMP_TO'; } - if (withAnchor !== undefined) { + if (withAnchor) { if (rootPayload.params.initial) { if (process.env.NODE_ENV !== 'production') { console.warn(`The parameter 'initial' is a reserved parameter name in React Navigation`); @@ -278,7 +278,12 @@ function getNavigateAction(baseHref, options, type = 'NAVIGATE', withAnchor, sin * True: You want the initialRouteName to load. * False: You do not want the initialRouteName to load. */ - rootPayload.params.initial = !withAnchor; + // Set initial on root and all nested params so anchors are loaded at every level + let currentParams = rootPayload.params; + while (currentParams) { + currentParams.initial = !withAnchor; + currentParams = currentParams.params; + } } const expoParams = isPreviewNavigation ? { diff --git a/packages/expo-router/build/global-state/routing.js.map b/packages/expo-router/build/global-state/routing.js.map index c67c521d272169..c3fb4ba6110960 100644 --- a/packages/expo-router/build/global-state/routing.js.map +++ b/packages/expo-router/build/global-state/routing.js.map @@ -1 +1 @@ -{"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/global-state/routing.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n NavigationAction,\n type NavigationState,\n PartialRoute,\n type PartialState,\n type NavigationContainerRef,\n ParamListBase,\n} from '@react-navigation/native';\nimport { IS_DOM } from 'expo/dom';\nimport * as Linking from 'expo-linking';\nimport { type RefObject } from 'react';\nimport { Platform } from 'react-native';\n\nimport { store } from './router-store';\nimport {\n emitDomDismiss,\n emitDomDismissAll,\n emitDomGoBack,\n emitDomLinkEvent,\n emitDomSetParams,\n} from '../domComponents/emitDomEvent';\nimport { ResultState } from '../fork/getStateFromPath';\nimport { applyRedirects } from '../getRoutesRedirects';\nimport { resolveHref, resolveHrefStringWithSegments } from '../link/href';\nimport { matchDynamicName } from '../matchers';\nimport {\n appendInternalExpoRouterParams,\n INTERNAL_EXPO_ROUTER_IS_PREVIEW_NAVIGATION_PARAM_NAME,\n INTERNAL_EXPO_ROUTER_NO_ANIMATION_PARAM_NAME,\n type InternalExpoRouterParams,\n} from '../navigationParams';\nimport { Href } from '../types';\nimport { SingularOptions } from '../useScreens';\nimport { shouldLinkExternally } from '../utils/url';\n\nfunction assertIsReady() {\n if (!store.navigationRef.isReady()) {\n throw new Error(\n 'Attempted to navigate before mounting the Root Layout component. Ensure the Root Layout component is rendering a Slot, or other navigator on the first render.'\n );\n }\n}\n\ninterface LinkAction {\n type: 'ROUTER_LINK';\n payload: {\n options: LinkToOptions;\n href: string;\n };\n}\n\nexport const routingQueue = {\n queue: [] as (NavigationAction | LinkAction)[],\n subscribers: new Set<() => void>(),\n subscribe(callback: () => void) {\n routingQueue.subscribers.add(callback);\n return () => {\n routingQueue.subscribers.delete(callback);\n };\n },\n snapshot() {\n return routingQueue.queue;\n },\n add(action: NavigationAction | LinkAction) {\n routingQueue.queue.push(action);\n for (const callback of routingQueue.subscribers) {\n callback();\n }\n },\n run(ref: RefObject | null>) {\n // Reset the identity of the queue.\n const events = routingQueue.queue;\n routingQueue.queue = [];\n let action: NavigationAction | LinkAction | undefined;\n while ((action = events.shift())) {\n if (ref.current) {\n if (action.type === 'ROUTER_LINK') {\n const {\n payload: { href, options },\n } = action as LinkAction;\n\n action = getNavigateAction(\n href,\n options,\n options.event,\n options.withAnchor,\n options.dangerouslySingular,\n !!options.__internal__PreviewKey\n );\n if (action) {\n ref.current.dispatch(action);\n }\n } else {\n ref.current.dispatch(action);\n }\n }\n }\n },\n};\n\nexport type NavigationOptions = Omit;\n\nexport function navigate(url: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(url), { ...options, event: 'NAVIGATE' });\n}\n\nexport function reload() {\n // TODO(EvanBacon): add `reload` support.\n throw new Error('The reload method is not implemented in the client-side router yet.');\n}\n\nexport function prefetch(href: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(href), { ...options, event: 'PRELOAD' });\n}\n\nexport function push(url: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(url), { ...options, event: 'PUSH' });\n}\n\nexport function dismiss(count: number = 1) {\n if (emitDomDismiss(count)) {\n return;\n }\n\n routingQueue.add({ type: 'POP', payload: { count } });\n}\n\nexport function dismissTo(href: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(href), { ...options, event: 'POP_TO' });\n}\n\nexport function replace(url: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(url), { ...options, event: 'REPLACE' });\n}\n\nexport function dismissAll() {\n if (emitDomDismissAll()) {\n return;\n }\n routingQueue.add({ type: 'POP_TO_TOP' });\n}\n\nexport function goBack() {\n if (emitDomGoBack()) {\n return;\n }\n assertIsReady();\n routingQueue.add({ type: 'GO_BACK' });\n}\n\nexport function canGoBack(): boolean {\n if (IS_DOM) {\n throw new Error(\n 'canGoBack imperative method is not supported. Pass the property to the DOM component instead.'\n );\n }\n // Return a default value here if the navigation hasn't mounted yet.\n // This can happen if the user calls `canGoBack` from the Root Layout route\n // before mounting a navigator. This behavior exists due to React Navigation being dynamically\n // constructed at runtime. We can get rid of this in the future if we use\n // the static configuration internally.\n if (!store.navigationRef.isReady()) {\n return false;\n }\n return store.navigationRef?.current?.canGoBack() ?? false;\n}\n\nexport function canDismiss(): boolean {\n if (IS_DOM) {\n throw new Error(\n 'canDismiss imperative method is not supported. Pass the property to the DOM component instead.'\n );\n }\n let state = store.state;\n\n // Keep traversing down the state tree until we find a stack navigator that we can pop\n while (state) {\n if (state.type === 'stack' && state.routes.length > 1) {\n return true;\n }\n if (state.index === undefined) return false;\n\n state = state.routes?.[state.index]?.state as any;\n }\n\n return false;\n}\n\nexport function setParams(\n params: Record = {}\n) {\n if (emitDomSetParams(params)) {\n return;\n }\n assertIsReady();\n return (store.navigationRef?.current?.setParams as any)(params);\n}\n\nexport type LinkToOptions = {\n event?: string;\n\n /**\n * Relative URL references are either relative to the directory or the document. By default, relative paths are relative to the document.\n * @see: [MDN's documentation on Resolving relative references to a URL](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references).\n */\n relativeToDirectory?: boolean;\n\n /**\n * Include the anchor when navigating to a new navigator\n */\n withAnchor?: boolean;\n\n /**\n * When navigating in a Stack, remove all screen from the history that match the singular condition\n *\n * If used with `push`, the history will be filtered even if no navigation occurs.\n */\n dangerouslySingular?: SingularOptions;\n\n __internal__PreviewKey?: string;\n};\n\nexport function linkTo(originalHref: Href, options: LinkToOptions = {}) {\n originalHref = typeof originalHref == 'string' ? originalHref : resolveHref(originalHref);\n let href: string | undefined | null = originalHref;\n\n if (emitDomLinkEvent(href, options)) {\n return;\n }\n\n if (shouldLinkExternally(href)) {\n if (href.startsWith('//') && Platform.OS !== 'web') {\n href = `https:${href}`;\n }\n\n Linking.openURL(href);\n return;\n }\n\n if (href === '..' || href === '../') {\n assertIsReady();\n const navigationRef = store.navigationRef.current;\n\n if (navigationRef == null) {\n throw new Error(\n \"Couldn't find a navigation object. Is your component inside NavigationContainer?\"\n );\n }\n\n if (!store.linking) {\n throw new Error('Attempted to link to route when no routes are present');\n }\n\n navigationRef.goBack();\n return;\n }\n\n const linkAction: LinkAction = {\n type: 'ROUTER_LINK',\n payload: {\n href,\n options,\n },\n };\n\n routingQueue.add(linkAction);\n}\n\nfunction getNavigateAction(\n baseHref: string,\n options: LinkToOptions,\n type = 'NAVIGATE',\n withAnchor?: boolean,\n singular?: SingularOptions,\n isPreviewNavigation?: boolean\n) {\n let href: string | undefined = baseHref;\n assertIsReady();\n const navigationRef = store.navigationRef.current;\n\n if (navigationRef == null) {\n throw new Error(\n \"Couldn't find a navigation object. Is your component inside NavigationContainer?\"\n );\n }\n if (!store.linking) {\n throw new Error('Attempted to link to route when no routes are present');\n }\n const rootState = navigationRef.getRootState();\n\n href = resolveHrefStringWithSegments(href, store.getRouteInfo(), options);\n href = applyRedirects(href, store.redirects) ?? undefined;\n\n // If the href is undefined, it means that the redirect has already been handled the navigation\n if (!href) {\n return;\n }\n\n const state = store.linking.getStateFromPath!(href, store.linking.config);\n\n if (!state || state.routes.length === 0) {\n console.error('Could not generate a valid navigation state for the given path: ' + href);\n return;\n }\n /**\n * We need to find the deepest navigator where the action and current state diverge, If they do not diverge, the\n * lowest navigator is the target.\n *\n * By default React Navigation will target the current navigator, but this doesn't work for all actions\n * For example:\n * - /deeply/nested/route -> /top-level-route the target needs to be the top-level navigator\n * - /stack/nestedStack/page -> /stack1/nestedStack/other-page needs to target the nestedStack navigator\n *\n * This matching needs to done by comparing the route names and the dynamic path, for example\n * - /1/page -> /2/anotherPage needs to target the /[id] navigator\n *\n * Other parameters such as search params and hash are not evaluated.\n */\n\n const { actionStateRoute, navigationState } = findDivergentState(\n state,\n rootState,\n type === 'PRELOAD'\n );\n\n /*\n * We found the target navigator, but the payload is in the incorrect format\n * We need to convert the action state to a payload that can be dispatched\n */\n const rootPayload = getPayloadFromStateRoute(actionStateRoute || {});\n\n if (type === 'PUSH' && navigationState.type !== 'stack') {\n type = 'NAVIGATE';\n } else if (navigationState.type === 'expo-tab') {\n type = 'JUMP_TO';\n } else if (type === 'REPLACE' && navigationState.type === 'drawer') {\n type = 'JUMP_TO';\n }\n\n if (withAnchor !== undefined) {\n if (rootPayload.params.initial) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(`The parameter 'initial' is a reserved parameter name in React Navigation`);\n }\n }\n /*\n * The logic for initial can seen backwards depending on your perspective\n * True: The initialRouteName is not loaded. The incoming screen is the initial screen (default)\n * False: The initialRouteName is loaded. THe incoming screen is placed after the initialRouteName\n *\n * withAnchor flips the perspective.\n * True: You want the initialRouteName to load.\n * False: You do not want the initialRouteName to load.\n */\n rootPayload.params.initial = !withAnchor;\n }\n\n const expoParams: InternalExpoRouterParams = isPreviewNavigation\n ? {\n [INTERNAL_EXPO_ROUTER_IS_PREVIEW_NAVIGATION_PARAM_NAME]: true,\n [INTERNAL_EXPO_ROUTER_NO_ANIMATION_PARAM_NAME]: true,\n }\n : {};\n const params = appendInternalExpoRouterParams(rootPayload.params, expoParams);\n\n return {\n type,\n target: navigationState.key,\n payload: {\n // key: rootPayload.key,\n name: rootPayload.screen,\n params,\n singular,\n },\n };\n}\n\n/**\n * React Navigation uses params to store information about the screens, rather then create new state for each level.\n * This function traverses the action state that will not be part of state and returns a payload that can be used in action.\n */\nexport function getPayloadFromStateRoute(_actionStateRoute: PartialRoute) {\n const rootPayload: Record = { params: {} };\n let payload = rootPayload;\n let params = payload.params;\n let actionStateRoute: PartialRoute | undefined = _actionStateRoute;\n\n while (actionStateRoute) {\n Object.assign(params, { ...payload.params, ...actionStateRoute.params });\n // Assign the screen name to the payload\n payload.screen = actionStateRoute.name;\n // Merge the params, ensuring that we create a new object\n payload.params = { ...params };\n\n // Params don't include the screen, thats a separate attribute\n delete payload.params['screen'];\n\n // Continue down the payload tree\n // Initially these values are separate, but React Nav merges them after the first layer\n payload = payload.params;\n params = payload;\n\n actionStateRoute = actionStateRoute.state?.routes[actionStateRoute.state?.routes.length - 1];\n }\n return rootPayload;\n}\n\n/*\n * Traverse the state tree comparing the current state and the action state until we find where they diverge\n */\nexport function findDivergentState(\n _actionState: ResultState,\n _navigationState: NavigationState,\n // If true, look through all tabs to find the target state, rather then just the current tab\n lookThroughAllTabs: boolean = false\n) {\n let actionState: PartialState | undefined = _actionState;\n let navigationState: NavigationState | undefined = _navigationState;\n let actionStateRoute: PartialRoute | undefined;\n const navigationRoutes = [];\n while (actionState && navigationState) {\n actionStateRoute = actionState.routes[actionState.routes.length - 1];\n const stateRoute = (() => {\n if (navigationState.type === 'tab' && lookThroughAllTabs) {\n return (\n navigationState.routes.find((route) => route.name === actionStateRoute?.name) ||\n navigationState.routes[navigationState.index ?? 0]\n );\n }\n return navigationState.routes[navigationState.index ?? 0];\n })();\n\n const childState: PartialState | undefined = actionStateRoute.state;\n const nextNavigationState = stateRoute.state;\n\n const dynamicName = matchDynamicName(actionStateRoute.name);\n\n const didActionAndCurrentStateDiverge =\n actionStateRoute.name !== stateRoute.name ||\n !childState ||\n !nextNavigationState ||\n (dynamicName &&\n // @ts-expect-error: TODO(@kitten): This isn't properly typed, so the index access fails\n actionStateRoute.params?.[dynamicName.name] !== stateRoute.params?.[dynamicName.name]);\n\n if (didActionAndCurrentStateDiverge) {\n // If we are looking through all tabs, we need to add new tab id if this is the last route\n // Otherwise we wouldn't be able to change the tab\n if (navigationState.type === 'tab' && lookThroughAllTabs) {\n navigationRoutes.push(stateRoute);\n }\n break;\n }\n\n navigationRoutes.push(stateRoute);\n\n actionState = childState;\n navigationState = nextNavigationState as NavigationState;\n }\n\n return {\n actionState,\n navigationState,\n actionStateRoute,\n navigationRoutes,\n };\n}\n"]} \ No newline at end of file +{"version":3,"file":"routing.js","sourceRoot":"","sources":["../../src/global-state/routing.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n NavigationAction,\n type NavigationState,\n PartialRoute,\n type PartialState,\n type NavigationContainerRef,\n ParamListBase,\n} from '@react-navigation/native';\nimport { IS_DOM } from 'expo/dom';\nimport * as Linking from 'expo-linking';\nimport { type RefObject } from 'react';\nimport { Platform } from 'react-native';\n\nimport { store } from './router-store';\nimport {\n emitDomDismiss,\n emitDomDismissAll,\n emitDomGoBack,\n emitDomLinkEvent,\n emitDomSetParams,\n} from '../domComponents/emitDomEvent';\nimport { ResultState } from '../fork/getStateFromPath';\nimport { applyRedirects } from '../getRoutesRedirects';\nimport { resolveHref, resolveHrefStringWithSegments } from '../link/href';\nimport { matchDynamicName } from '../matchers';\nimport {\n appendInternalExpoRouterParams,\n INTERNAL_EXPO_ROUTER_IS_PREVIEW_NAVIGATION_PARAM_NAME,\n INTERNAL_EXPO_ROUTER_NO_ANIMATION_PARAM_NAME,\n type InternalExpoRouterParams,\n} from '../navigationParams';\nimport { Href } from '../types';\nimport { SingularOptions } from '../useScreens';\nimport { shouldLinkExternally } from '../utils/url';\n\nfunction assertIsReady() {\n if (!store.navigationRef.isReady()) {\n throw new Error(\n 'Attempted to navigate before mounting the Root Layout component. Ensure the Root Layout component is rendering a Slot, or other navigator on the first render.'\n );\n }\n}\n\ninterface LinkAction {\n type: 'ROUTER_LINK';\n payload: {\n options: LinkToOptions;\n href: string;\n };\n}\n\nexport const routingQueue = {\n queue: [] as (NavigationAction | LinkAction)[],\n subscribers: new Set<() => void>(),\n subscribe(callback: () => void) {\n routingQueue.subscribers.add(callback);\n return () => {\n routingQueue.subscribers.delete(callback);\n };\n },\n snapshot() {\n return routingQueue.queue;\n },\n add(action: NavigationAction | LinkAction) {\n routingQueue.queue.push(action);\n for (const callback of routingQueue.subscribers) {\n callback();\n }\n },\n run(ref: RefObject | null>) {\n // Reset the identity of the queue.\n const events = routingQueue.queue;\n routingQueue.queue = [];\n let action: NavigationAction | LinkAction | undefined;\n while ((action = events.shift())) {\n if (ref.current) {\n if (action.type === 'ROUTER_LINK') {\n const {\n payload: { href, options },\n } = action as LinkAction;\n\n action = getNavigateAction(\n href,\n options,\n options.event,\n options.withAnchor,\n options.dangerouslySingular,\n !!options.__internal__PreviewKey\n );\n if (action) {\n ref.current.dispatch(action);\n }\n } else {\n ref.current.dispatch(action);\n }\n }\n }\n },\n};\n\nexport type NavigationOptions = Omit;\n\nexport function navigate(url: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(url), { ...options, event: 'NAVIGATE' });\n}\n\nexport function reload() {\n // TODO(EvanBacon): add `reload` support.\n throw new Error('The reload method is not implemented in the client-side router yet.');\n}\n\nexport function prefetch(href: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(href), { ...options, event: 'PRELOAD' });\n}\n\nexport function push(url: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(url), { ...options, event: 'PUSH' });\n}\n\nexport function dismiss(count: number = 1) {\n if (emitDomDismiss(count)) {\n return;\n }\n\n routingQueue.add({ type: 'POP', payload: { count } });\n}\n\nexport function dismissTo(href: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(href), { ...options, event: 'POP_TO' });\n}\n\nexport function replace(url: Href, options?: NavigationOptions) {\n return linkTo(resolveHref(url), { ...options, event: 'REPLACE' });\n}\n\nexport function dismissAll() {\n if (emitDomDismissAll()) {\n return;\n }\n routingQueue.add({ type: 'POP_TO_TOP' });\n}\n\nexport function goBack() {\n if (emitDomGoBack()) {\n return;\n }\n assertIsReady();\n routingQueue.add({ type: 'GO_BACK' });\n}\n\nexport function canGoBack(): boolean {\n if (IS_DOM) {\n throw new Error(\n 'canGoBack imperative method is not supported. Pass the property to the DOM component instead.'\n );\n }\n // Return a default value here if the navigation hasn't mounted yet.\n // This can happen if the user calls `canGoBack` from the Root Layout route\n // before mounting a navigator. This behavior exists due to React Navigation being dynamically\n // constructed at runtime. We can get rid of this in the future if we use\n // the static configuration internally.\n if (!store.navigationRef.isReady()) {\n return false;\n }\n return store.navigationRef?.current?.canGoBack() ?? false;\n}\n\nexport function canDismiss(): boolean {\n if (IS_DOM) {\n throw new Error(\n 'canDismiss imperative method is not supported. Pass the property to the DOM component instead.'\n );\n }\n let state = store.state;\n\n // Keep traversing down the state tree until we find a stack navigator that we can pop\n while (state) {\n if (state.type === 'stack' && state.routes.length > 1) {\n return true;\n }\n if (state.index === undefined) return false;\n\n state = state.routes?.[state.index]?.state as any;\n }\n\n return false;\n}\n\nexport function setParams(\n params: Record = {}\n) {\n if (emitDomSetParams(params)) {\n return;\n }\n assertIsReady();\n return (store.navigationRef?.current?.setParams as any)(params);\n}\n\nexport type LinkToOptions = {\n event?: string;\n\n /**\n * Relative URL references are either relative to the directory or the document. By default, relative paths are relative to the document.\n * @see: [MDN's documentation on Resolving relative references to a URL](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references).\n */\n relativeToDirectory?: boolean;\n\n /**\n * Include the anchor when navigating to a new navigator\n */\n withAnchor?: boolean;\n\n /**\n * When navigating in a Stack, remove all screen from the history that match the singular condition\n *\n * If used with `push`, the history will be filtered even if no navigation occurs.\n */\n dangerouslySingular?: SingularOptions;\n\n __internal__PreviewKey?: string;\n};\n\nexport function linkTo(originalHref: Href, options: LinkToOptions = {}) {\n originalHref = typeof originalHref == 'string' ? originalHref : resolveHref(originalHref);\n let href: string | undefined | null = originalHref;\n\n if (emitDomLinkEvent(href, options)) {\n return;\n }\n\n if (shouldLinkExternally(href)) {\n if (href.startsWith('//') && Platform.OS !== 'web') {\n href = `https:${href}`;\n }\n\n Linking.openURL(href);\n return;\n }\n\n if (href === '..' || href === '../') {\n assertIsReady();\n const navigationRef = store.navigationRef.current;\n\n if (navigationRef == null) {\n throw new Error(\n \"Couldn't find a navigation object. Is your component inside NavigationContainer?\"\n );\n }\n\n if (!store.linking) {\n throw new Error('Attempted to link to route when no routes are present');\n }\n\n navigationRef.goBack();\n return;\n }\n\n const linkAction: LinkAction = {\n type: 'ROUTER_LINK',\n payload: {\n href,\n options,\n },\n };\n\n routingQueue.add(linkAction);\n}\n\nfunction getNavigateAction(\n baseHref: string,\n options: LinkToOptions,\n type = 'NAVIGATE',\n withAnchor?: boolean,\n singular?: SingularOptions,\n isPreviewNavigation?: boolean\n) {\n let href: string | undefined = baseHref;\n assertIsReady();\n const navigationRef = store.navigationRef.current;\n\n if (navigationRef == null) {\n throw new Error(\n \"Couldn't find a navigation object. Is your component inside NavigationContainer?\"\n );\n }\n if (!store.linking) {\n throw new Error('Attempted to link to route when no routes are present');\n }\n const rootState = navigationRef.getRootState();\n\n href = resolveHrefStringWithSegments(href, store.getRouteInfo(), options);\n href = applyRedirects(href, store.redirects) ?? undefined;\n\n // If the href is undefined, it means that the redirect has already been handled the navigation\n if (!href) {\n return;\n }\n\n const state = store.linking.getStateFromPath!(href, store.linking.config);\n\n if (!state || state.routes.length === 0) {\n console.error('Could not generate a valid navigation state for the given path: ' + href);\n return;\n }\n /**\n * We need to find the deepest navigator where the action and current state diverge, If they do not diverge, the\n * lowest navigator is the target.\n *\n * By default React Navigation will target the current navigator, but this doesn't work for all actions\n * For example:\n * - /deeply/nested/route -> /top-level-route the target needs to be the top-level navigator\n * - /stack/nestedStack/page -> /stack1/nestedStack/other-page needs to target the nestedStack navigator\n *\n * This matching needs to done by comparing the route names and the dynamic path, for example\n * - /1/page -> /2/anotherPage needs to target the /[id] navigator\n *\n * Other parameters such as search params and hash are not evaluated.\n */\n\n const { actionStateRoute, navigationState } = findDivergentState(\n state,\n rootState,\n type === 'PRELOAD'\n );\n\n /*\n * We found the target navigator, but the payload is in the incorrect format\n * We need to convert the action state to a payload that can be dispatched\n */\n const rootPayload = getPayloadFromStateRoute(actionStateRoute || {});\n\n if (type === 'PUSH' && navigationState.type !== 'stack') {\n type = 'NAVIGATE';\n } else if (navigationState.type === 'expo-tab') {\n type = 'JUMP_TO';\n } else if (type === 'REPLACE' && navigationState.type === 'drawer') {\n type = 'JUMP_TO';\n }\n\n if (withAnchor) {\n if (rootPayload.params.initial) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(`The parameter 'initial' is a reserved parameter name in React Navigation`);\n }\n }\n /*\n * The logic for initial can seen backwards depending on your perspective\n * True: The initialRouteName is not loaded. The incoming screen is the initial screen (default)\n * False: The initialRouteName is loaded. THe incoming screen is placed after the initialRouteName\n *\n * withAnchor flips the perspective.\n * True: You want the initialRouteName to load.\n * False: You do not want the initialRouteName to load.\n */\n // Set initial on root and all nested params so anchors are loaded at every level\n let currentParams = rootPayload.params;\n while (currentParams) {\n currentParams.initial = !withAnchor;\n currentParams = currentParams.params;\n }\n }\n\n const expoParams: InternalExpoRouterParams = isPreviewNavigation\n ? {\n [INTERNAL_EXPO_ROUTER_IS_PREVIEW_NAVIGATION_PARAM_NAME]: true,\n [INTERNAL_EXPO_ROUTER_NO_ANIMATION_PARAM_NAME]: true,\n }\n : {};\n const params = appendInternalExpoRouterParams(rootPayload.params, expoParams);\n\n return {\n type,\n target: navigationState.key,\n payload: {\n // key: rootPayload.key,\n name: rootPayload.screen,\n params,\n singular,\n },\n };\n}\n\n/**\n * React Navigation uses params to store information about the screens, rather then create new state for each level.\n * This function traverses the action state that will not be part of state and returns a payload that can be used in action.\n */\nexport function getPayloadFromStateRoute(_actionStateRoute: PartialRoute) {\n const rootPayload: Record = { params: {} };\n let payload = rootPayload;\n let params = payload.params;\n let actionStateRoute: PartialRoute | undefined = _actionStateRoute;\n\n while (actionStateRoute) {\n Object.assign(params, { ...payload.params, ...actionStateRoute.params });\n // Assign the screen name to the payload\n payload.screen = actionStateRoute.name;\n // Merge the params, ensuring that we create a new object\n payload.params = { ...params };\n\n // Params don't include the screen, thats a separate attribute\n delete payload.params['screen'];\n\n // Continue down the payload tree\n // Initially these values are separate, but React Nav merges them after the first layer\n payload = payload.params;\n params = payload;\n\n actionStateRoute = actionStateRoute.state?.routes[actionStateRoute.state?.routes.length - 1];\n }\n return rootPayload;\n}\n\n/*\n * Traverse the state tree comparing the current state and the action state until we find where they diverge\n */\nexport function findDivergentState(\n _actionState: ResultState,\n _navigationState: NavigationState,\n // If true, look through all tabs to find the target state, rather then just the current tab\n lookThroughAllTabs: boolean = false\n) {\n let actionState: PartialState | undefined = _actionState;\n let navigationState: NavigationState | undefined = _navigationState;\n let actionStateRoute: PartialRoute | undefined;\n const navigationRoutes = [];\n while (actionState && navigationState) {\n actionStateRoute = actionState.routes[actionState.routes.length - 1];\n const stateRoute = (() => {\n if (navigationState.type === 'tab' && lookThroughAllTabs) {\n return (\n navigationState.routes.find((route) => route.name === actionStateRoute?.name) ||\n navigationState.routes[navigationState.index ?? 0]\n );\n }\n return navigationState.routes[navigationState.index ?? 0];\n })();\n\n const childState: PartialState | undefined = actionStateRoute.state;\n const nextNavigationState = stateRoute.state;\n\n const dynamicName = matchDynamicName(actionStateRoute.name);\n\n const didActionAndCurrentStateDiverge =\n actionStateRoute.name !== stateRoute.name ||\n !childState ||\n !nextNavigationState ||\n (dynamicName &&\n // @ts-expect-error: TODO(@kitten): This isn't properly typed, so the index access fails\n actionStateRoute.params?.[dynamicName.name] !== stateRoute.params?.[dynamicName.name]);\n\n if (didActionAndCurrentStateDiverge) {\n // If we are looking through all tabs, we need to add new tab id if this is the last route\n // Otherwise we wouldn't be able to change the tab\n if (navigationState.type === 'tab' && lookThroughAllTabs) {\n navigationRoutes.push(stateRoute);\n }\n break;\n }\n\n navigationRoutes.push(stateRoute);\n\n actionState = childState;\n navigationState = nextNavigationState as NavigationState;\n }\n\n return {\n actionState,\n navigationState,\n actionStateRoute,\n navigationRoutes,\n };\n}\n"]} \ No newline at end of file diff --git a/packages/expo-router/src/__tests__/initialRouteName.test.ios.tsx b/packages/expo-router/src/__tests__/initialRouteName.test.ios.tsx index da36def084b43a..cbf109e719ee5f 100644 --- a/packages/expo-router/src/__tests__/initialRouteName.test.ios.tsx +++ b/packages/expo-router/src/__tests__/initialRouteName.test.ios.tsx @@ -160,7 +160,7 @@ it('push should include (group)/index as an anchor route when using withAnchor', { key: expect.any(String), name: 'orange', - params: {}, + params: { initial: false }, path: undefined, }, ], diff --git a/packages/expo-router/src/__tests__/push.test.ios.tsx b/packages/expo-router/src/__tests__/push.test.ios.tsx index 2c9d22b129b235..d1dda92a6a62f5 100644 --- a/packages/expo-router/src/__tests__/push.test.ios.tsx +++ b/packages/expo-router/src/__tests__/push.test.ios.tsx @@ -607,7 +607,7 @@ it('push should also add anchor routes', () => { { key: expect.any(String), name: 'orange', - params: {}, + params: { initial: false }, path: undefined, }, ], diff --git a/packages/expo-router/src/global-state/routing.ts b/packages/expo-router/src/global-state/routing.ts index 2bf19d65a43245..c4c8b6797ae361 100644 --- a/packages/expo-router/src/global-state/routing.ts +++ b/packages/expo-router/src/global-state/routing.ts @@ -337,7 +337,7 @@ function getNavigateAction( type = 'JUMP_TO'; } - if (withAnchor !== undefined) { + if (withAnchor) { if (rootPayload.params.initial) { if (process.env.NODE_ENV !== 'production') { console.warn(`The parameter 'initial' is a reserved parameter name in React Navigation`); @@ -352,7 +352,12 @@ function getNavigateAction( * True: You want the initialRouteName to load. * False: You do not want the initialRouteName to load. */ - rootPayload.params.initial = !withAnchor; + // Set initial on root and all nested params so anchors are loaded at every level + let currentParams = rootPayload.params; + while (currentParams) { + currentParams.initial = !withAnchor; + currentParams = currentParams.params; + } } const expoParams: InternalExpoRouterParams = isPreviewNavigation diff --git a/packages/expo-router/src/link/__tests__/Link.test.ios.tsx b/packages/expo-router/src/link/__tests__/Link.test.ios.tsx index cd39bee1ae5eb8..140ae771aff3ce 100644 --- a/packages/expo-router/src/link/__tests__/Link.test.ios.tsx +++ b/packages/expo-router/src/link/__tests__/Link.test.ios.tsx @@ -284,6 +284,42 @@ it('can preserve the initialRoute with shared groups', () => { expect(screen.getByTestId('link')).toBeDefined(); }); +it('can preserve the anchor for every level in nested stack', () => { + renderRouter({ + _layout: () => , + '(inner)/_layout': () => , + '(inner)/index': () => ( + + Link to Target + + ), + 'second/_layout': () => , + 'second/index': () => Second Index, + 'second/third/_layout': { + unstable_settings: { + anchor: 'anchor', + }, + default: () => , + }, + 'second/third/anchor': () => Anchor, + 'second/third/target': () => Target, + }); + + expect(screen.getByTestId('link-to-target')).toBeVisible(); + + act(() => { + fireEvent.press(screen.getByTestId('link-to-target')); + }); + + expect(screen.getByTestId('target')).toBeVisible(); + + act(() => { + router.back(); + }); + + expect(screen.getByTestId('anchor')).toBeVisible(); +}); + describe('singular', () => { test('can dynamically route using singular', () => { renderRouter( From 7bfb703a6d0ced49a180ce1d7e03bd7e3d25ada7 Mon Sep 17 00:00:00 2001 From: Patryk Mleczek <67064618+pmleczek@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:18:07 +0100 Subject: [PATCH 07/15] [ios][brownfield] fix ExpoAppDelegate.swift symlink issues (#42328) # Why Actions that revolve around the symlink (e.g. `npm pack` or just directly running `yarn prepack`) end in an error wven though the file exists: ```bash > expo-brownfield@0.0.1 prepack > cp -L plugin/templates/ios/ExpoAppDelegate.swift plugin/templates/ios/ExpoAppDelegate.swift.tmp \ && mv plugin/templates/ios/ExpoAppDelegate.swift.tmp plugin/templates/ios/ExpoAppDelegate.swift cp: plugin/templates/ios/ExpoAppDelegate.swift: No such file or directory ``` That's because symlink seems to be an absolute path instead of a relative one: ```bash > ls -al plugin/templates/ios total 64 drwxr-xr-x 11 patrykmleczek staff 352 Jan 20 10:56 . drwxr-xr-x 5 patrykmleczek staff 160 Jan 20 10:30 .. lrwxr-xr-x 1 patrykmleczek staff 87 Jan 20 10:56 ExpoAppDelegate.swift -> /Users/gabriel/Workspace/expo/expo/packages/expo/ios/AppDelegates/ExpoAppDelegate.swift -rw-r--r-- 1 patrykmleczek staff 516 Jan 9 08:13 Info.plist ``` # How Changed symlink to be a relative path to the original `ExpoAppDelegate.swift` # Test Plan Tested using `npx expo prebuild` and `npx expo-brownfield build-ios` in `apps/minimal-tester` # Checklist - [X] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). --- .../expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift b/packages/expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift index ec8bccd5cb2af5..658dc16a135197 120000 --- a/packages/expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift +++ b/packages/expo-brownfield/plugin/templates/ios/ExpoAppDelegate.swift @@ -1 +1 @@ -/Users/gabriel/Workspace/expo/expo/packages/expo/ios/AppDelegates/ExpoAppDelegate.swift \ No newline at end of file +../../../../expo/ios/AppDelegates/ExpoAppDelegate.swift \ No newline at end of file From 40aa768ef6b47656018ad5aee268541ce5aff856 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Tue, 20 Jan 2026 14:30:21 +0100 Subject: [PATCH 08/15] [widgets] Initial interactivity (#42288) # Why Interactive widgets are cool. # How Set the update function and invoke it in JSC when interaction is made. --- packages/expo-widgets/build/ExpoWidgets.d.ts | 2 +- .../expo-widgets/build/ExpoWidgets.d.ts.map | 2 +- packages/expo-widgets/build/ExpoWidgets.js.map | 2 +- packages/expo-widgets/build/Widgets.d.ts | 4 ++-- packages/expo-widgets/build/Widgets.d.ts.map | 2 +- packages/expo-widgets/build/Widgets.js | 9 +++++---- packages/expo-widgets/build/Widgets.js.map | 2 +- .../expo-widgets/ios/Widgets/AppIntent.swift | 18 ++++++++++++++++++ packages/expo-widgets/ios/WidgetsModule.swift | 3 +++ packages/expo-widgets/src/ExpoWidgets.ts | 7 ++++++- packages/expo-widgets/src/Widgets.ts | 10 ++++++---- 11 files changed, 45 insertions(+), 16 deletions(-) diff --git a/packages/expo-widgets/build/ExpoWidgets.d.ts b/packages/expo-widgets/build/ExpoWidgets.d.ts index 313c382353cd1b..fcafbb77d9adc0 100644 --- a/packages/expo-widgets/build/ExpoWidgets.d.ts +++ b/packages/expo-widgets/build/ExpoWidgets.d.ts @@ -2,7 +2,7 @@ import { NativeModule } from 'expo'; import { ExpoWidgetsEvents } from './Widgets.types'; declare class ExpoWidgetModule extends NativeModule { reloadWidget(timeline?: string): void; - updateWidget(name: string, data: string, props?: Record): void; + updateWidget(name: string, data: string, props?: Record, updateFunction?: string): void; startLiveActivity(name: string, nodes: string, url?: string): string; updateLiveActivity(id: string, name: string, nodes: string): string; } diff --git a/packages/expo-widgets/build/ExpoWidgets.d.ts.map b/packages/expo-widgets/build/ExpoWidgets.d.ts.map index 2c046657c05fcc..6639a5a55ba455 100644 --- a/packages/expo-widgets/build/ExpoWidgets.d.ts.map +++ b/packages/expo-widgets/build/ExpoWidgets.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"ExpoWidgets.d.ts","sourceRoot":"","sources":["../src/ExpoWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACpE,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IACrC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAC3E,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM;IACpE,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;CACpE;;AAED,wBAAoE"} \ No newline at end of file +{"version":3,"file":"ExpoWidgets.d.ts","sourceRoot":"","sources":["../src/ExpoWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACpE,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IACrC,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IACP,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM;IACpE,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;CACpE;;AAED,wBAAoE"} \ No newline at end of file diff --git a/packages/expo-widgets/build/ExpoWidgets.js.map b/packages/expo-widgets/build/ExpoWidgets.js.map index 1492cffb4488d2..3e9daff21adf02 100644 --- a/packages/expo-widgets/build/ExpoWidgets.js.map +++ b/packages/expo-widgets/build/ExpoWidgets.js.map @@ -1 +1 @@ -{"version":3,"file":"ExpoWidgets.js","sourceRoot":"","sources":["../src/ExpoWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAWzD,eAAe,mBAAmB,CAAmB,aAAa,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { ExpoWidgetsEvents } from './Widgets.types';\n\ndeclare class ExpoWidgetModule extends NativeModule {\n reloadWidget(timeline?: string): void;\n updateWidget(name: string, data: string, props?: Record): void;\n startLiveActivity(name: string, nodes: string, url?: string): string;\n updateLiveActivity(id: string, name: string, nodes: string): string;\n}\n\nexport default requireNativeModule('ExpoWidgets');\n"]} \ No newline at end of file +{"version":3,"file":"ExpoWidgets.js","sourceRoot":"","sources":["../src/ExpoWidgets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAgBzD,eAAe,mBAAmB,CAAmB,aAAa,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { ExpoWidgetsEvents } from './Widgets.types';\n\ndeclare class ExpoWidgetModule extends NativeModule {\n reloadWidget(timeline?: string): void;\n updateWidget(\n name: string,\n data: string,\n props?: Record,\n updateFunction?: string\n ): void;\n startLiveActivity(name: string, nodes: string, url?: string): string;\n updateLiveActivity(id: string, name: string, nodes: string): string;\n}\n\nexport default requireNativeModule('ExpoWidgets');\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.d.ts b/packages/expo-widgets/build/Widgets.d.ts index 800c448b7445b4..20c2780380b298 100644 --- a/packages/expo-widgets/build/Widgets.d.ts +++ b/packages/expo-widgets/build/Widgets.d.ts @@ -3,7 +3,7 @@ import ExpoWidgetModule from './ExpoWidgets'; import { LiveActivityComponent, WidgetProps } from './Widgets.types'; export declare const startLiveActivity: (name: string, liveActivity: LiveActivityComponent, url?: string) => string; export declare const updateLiveActivity: (id: string, name: string, liveActivity: LiveActivityComponent) => void; -export declare const updateWidgetTimeline: (name: string, dates: Date[], widget: (p: WidgetProps) => React.JSX.Element, props?: T) => void; -export declare const updateWidgetSnapshot: (name: string, widget: (p: WidgetProps) => React.JSX.Element, props?: T) => void; +export declare const updateWidgetTimeline: (name: string, dates: Date[], widget: (p: WidgetProps) => React.JSX.Element, props?: T, updateFunction?: string) => void; +export declare const updateWidgetSnapshot: (name: string, widget: (p: WidgetProps) => React.JSX.Element, props?: T, updateFunction?: string) => void; export declare const addEventListener: typeof ExpoWidgetModule.addListener; //# sourceMappingURL=Widgets.d.ts.map \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.d.ts.map b/packages/expo-widgets/build/Widgets.d.ts.map index e958c65d9b3013..f7d64a5b729dea 100644 --- a/packages/expo-widgets/build/Widgets.d.ts.map +++ b/packages/expo-widgets/build/Widgets.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"Widgets.d.ts","sourceRoot":"","sources":["../src/Widgets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,gBAAgB,MAAM,eAAe,CAAC;AAC7C,OAAO,EAEL,qBAAqB,EAErB,WAAW,EACZ,MAAM,iBAAiB,CAAC;AAIzB,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,EACZ,cAAc,qBAAqB,EACnC,MAAM,MAAM,WAIb,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,IAAI,MAAM,EACV,MAAM,MAAM,EACZ,cAAc,qBAAqB,SAIpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACnD,MAAM,MAAM,EACZ,OAAO,IAAI,EAAE,EACb,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO,EAChD,QAAQ,CAAC,SA6BV,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACnD,MAAM,MAAM,EACZ,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO,EAChD,QAAQ,CAAC,SAGV,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,OAAO,gBAAgB,CAAC,WAA0C,CAAC"} \ No newline at end of file +{"version":3,"file":"Widgets.d.ts","sourceRoot":"","sources":["../src/Widgets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,gBAAgB,MAAM,eAAe,CAAC;AAC7C,OAAO,EAEL,qBAAqB,EAErB,WAAW,EACZ,MAAM,iBAAiB,CAAC;AAIzB,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,EACZ,cAAc,qBAAqB,EACnC,MAAM,MAAM,WAIb,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,IAAI,MAAM,EACV,MAAM,MAAM,EACZ,cAAc,qBAAqB,SAIpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACnD,MAAM,MAAM,EACZ,OAAO,IAAI,EAAE,EACb,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO,EAChD,QAAQ,CAAC,EACT,iBAAiB,MAAM,SA6BxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACnD,MAAM,MAAM,EACZ,QAAQ,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO,EAChD,QAAQ,CAAC,EACT,iBAAiB,MAAM,SAGxB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,OAAO,gBAAgB,CAAC,WAA0C,CAAC"} \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.js b/packages/expo-widgets/build/Widgets.js index 57b75a5425fcc4..521c8b77f53aff 100644 --- a/packages/expo-widgets/build/Widgets.js +++ b/packages/expo-widgets/build/Widgets.js @@ -9,7 +9,7 @@ export const updateLiveActivity = (id, name, liveActivity) => { const text = serialize(liveActivity()); ExpoWidgetModule.updateLiveActivity(id, name, text); }; -export const updateWidgetTimeline = (name, dates, widget, props) => { +export const updateWidgetTimeline = (name, dates, widget, props, updateFunction) => { const fakeProps = Object.keys(props || {}).reduce((acc, key) => { acc[key] = `{{${key}}}`; return acc; @@ -26,11 +26,12 @@ export const updateWidgetTimeline = (name, dates, widget, props) => { acc[family] = entries; return acc; }, {}); - ExpoWidgetModule.updateWidget(name, serialize(data), props); + ExpoWidgetModule.updateWidget(name, serialize(data), props, updateFunction); ExpoWidgetModule.reloadWidget(); }; -export const updateWidgetSnapshot = (name, widget, props) => { - updateWidgetTimeline(name, [new Date()], widget, props || {}); +export const updateWidgetSnapshot = (name, widget, props, updateFunction // (target: string, props: T) => T +) => { + updateWidgetTimeline(name, [new Date()], widget, props || {}, updateFunction); }; export const addEventListener = ExpoWidgetModule.addListener; //# sourceMappingURL=Widgets.js.map \ No newline at end of file diff --git a/packages/expo-widgets/build/Widgets.js.map b/packages/expo-widgets/build/Widgets.js.map index 0f7336abb7fcf5..92f53cf0b0333e 100644 --- a/packages/expo-widgets/build/Widgets.js.map +++ b/packages/expo-widgets/build/Widgets.js.map @@ -1 +1 @@ -{"version":3,"file":"Widgets.js","sourceRoot":"","sources":["../src/Widgets.ts"],"names":[],"mappings":"AAEA,OAAO,gBAAgB,MAAM,eAAe,CAAC;AAO7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,IAAY,EACZ,YAAmC,EACnC,GAAY,EACZ,EAAE;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACvC,OAAO,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,EAAU,EACV,IAAY,EACZ,YAAmC,EACnC,EAAE;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACvC,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAY,EACZ,KAAa,EACb,MAAgD,EAChD,KAAS,EACT,EAAE;IACF,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAC/C,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACX,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC;QACxB,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAA4B,CAC7B,CAAC;IAEF,MAAM,IAAI,GAA8C,iBAAiB;SACtE,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChB,MAAM;QACN,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC5B,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE;YACzB,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAI,SAAe,EAAE,CAAC;SACvD,CAAC,CAAC;KACJ,CAAC,CAAC;SACF,MAAM,CACL,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;QACtB,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAA+C,CAChD,CAAC;IAEJ,gBAAgB,CAAC,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAE5D,gBAAgB,CAAC,YAAY,EAAE,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAY,EACZ,MAAgD,EAChD,KAAS,EACT,EAAE;IACF,oBAAoB,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,IAAK,EAAQ,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAwC,gBAAgB,CAAC,WAAW,CAAC","sourcesContent":["import React from 'react';\n\nimport ExpoWidgetModule from './ExpoWidgets';\nimport {\n ExpoTimelineEntry,\n LiveActivityComponent,\n WidgetFamily,\n WidgetProps,\n} from './Widgets.types';\nimport { supportedFamilies } from './constants';\nimport { serialize } from './serializer';\n\nexport const startLiveActivity = (\n name: string,\n liveActivity: LiveActivityComponent,\n url?: string\n) => {\n const text = serialize(liveActivity());\n return ExpoWidgetModule.startLiveActivity(name, text, url);\n};\n\nexport const updateLiveActivity = (\n id: string,\n name: string,\n liveActivity: LiveActivityComponent\n) => {\n const text = serialize(liveActivity());\n ExpoWidgetModule.updateLiveActivity(id, name, text);\n};\n\nexport const updateWidgetTimeline = (\n name: string,\n dates: Date[],\n widget: (p: WidgetProps) => React.JSX.Element,\n props?: T\n) => {\n const fakeProps = Object.keys(props || {}).reduce(\n (acc, key) => {\n acc[key] = `{{${key}}}`;\n return acc;\n },\n {} as Record\n );\n\n const data: Record = supportedFamilies\n .map((family) => ({\n family,\n entries: dates.map((date) => ({\n timestamp: date.getTime(),\n content: widget({ date, family, ...(fakeProps as T) }),\n })),\n }))\n .reduce(\n (acc, { family, entries }) => {\n acc[family] = entries;\n return acc;\n },\n {} as Record\n );\n\n ExpoWidgetModule.updateWidget(name, serialize(data), props);\n\n ExpoWidgetModule.reloadWidget();\n};\n\nexport const updateWidgetSnapshot = (\n name: string,\n widget: (p: WidgetProps) => React.JSX.Element,\n props?: T\n) => {\n updateWidgetTimeline(name, [new Date()], widget, props || ({} as T));\n};\n\nexport const addEventListener: typeof ExpoWidgetModule.addListener = ExpoWidgetModule.addListener;\n"]} \ No newline at end of file +{"version":3,"file":"Widgets.js","sourceRoot":"","sources":["../src/Widgets.ts"],"names":[],"mappings":"AAEA,OAAO,gBAAgB,MAAM,eAAe,CAAC;AAO7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,IAAY,EACZ,YAAmC,EACnC,GAAY,EACZ,EAAE;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACvC,OAAO,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,EAAU,EACV,IAAY,EACZ,YAAmC,EACnC,EAAE;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACvC,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAY,EACZ,KAAa,EACb,MAAgD,EAChD,KAAS,EACT,cAAuB,EACvB,EAAE;IACF,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAC/C,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACX,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC;QACxB,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAA4B,CAC7B,CAAC;IAEF,MAAM,IAAI,GAA8C,iBAAiB;SACtE,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAChB,MAAM;QACN,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC5B,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE;YACzB,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAI,SAAe,EAAE,CAAC;SACvD,CAAC,CAAC;KACJ,CAAC,CAAC;SACF,MAAM,CACL,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;QACtB,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAA+C,CAChD,CAAC;IAEJ,gBAAgB,CAAC,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;IAE5E,gBAAgB,CAAC,YAAY,EAAE,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAY,EACZ,MAAgD,EAChD,KAAS,EACT,cAAuB,CAAC,kCAAkC;EAC1D,EAAE;IACF,oBAAoB,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,IAAK,EAAQ,EAAE,cAAc,CAAC,CAAC;AACvF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAwC,gBAAgB,CAAC,WAAW,CAAC","sourcesContent":["import React from 'react';\n\nimport ExpoWidgetModule from './ExpoWidgets';\nimport {\n ExpoTimelineEntry,\n LiveActivityComponent,\n WidgetFamily,\n WidgetProps,\n} from './Widgets.types';\nimport { supportedFamilies } from './constants';\nimport { serialize } from './serializer';\n\nexport const startLiveActivity = (\n name: string,\n liveActivity: LiveActivityComponent,\n url?: string\n) => {\n const text = serialize(liveActivity());\n return ExpoWidgetModule.startLiveActivity(name, text, url);\n};\n\nexport const updateLiveActivity = (\n id: string,\n name: string,\n liveActivity: LiveActivityComponent\n) => {\n const text = serialize(liveActivity());\n ExpoWidgetModule.updateLiveActivity(id, name, text);\n};\n\nexport const updateWidgetTimeline = (\n name: string,\n dates: Date[],\n widget: (p: WidgetProps) => React.JSX.Element,\n props?: T,\n updateFunction?: string\n) => {\n const fakeProps = Object.keys(props || {}).reduce(\n (acc, key) => {\n acc[key] = `{{${key}}}`;\n return acc;\n },\n {} as Record\n );\n\n const data: Record = supportedFamilies\n .map((family) => ({\n family,\n entries: dates.map((date) => ({\n timestamp: date.getTime(),\n content: widget({ date, family, ...(fakeProps as T) }),\n })),\n }))\n .reduce(\n (acc, { family, entries }) => {\n acc[family] = entries;\n return acc;\n },\n {} as Record\n );\n\n ExpoWidgetModule.updateWidget(name, serialize(data), props, updateFunction);\n\n ExpoWidgetModule.reloadWidget();\n};\n\nexport const updateWidgetSnapshot = (\n name: string,\n widget: (p: WidgetProps) => React.JSX.Element,\n props?: T,\n updateFunction?: string // (target: string, props: T) => T\n) => {\n updateWidgetTimeline(name, [new Date()], widget, props || ({} as T), updateFunction);\n};\n\nexport const addEventListener: typeof ExpoWidgetModule.addListener = ExpoWidgetModule.addListener;\n"]} \ No newline at end of file diff --git a/packages/expo-widgets/ios/Widgets/AppIntent.swift b/packages/expo-widgets/ios/Widgets/AppIntent.swift index 6cdb370749e956..d886819147a548 100644 --- a/packages/expo-widgets/ios/Widgets/AppIntent.swift +++ b/packages/expo-widgets/ios/Widgets/AppIntent.swift @@ -23,6 +23,24 @@ struct WidgetUserInteraction: AppIntent { guard let source else { return .result() } + let props = WidgetsStorage.getDictionary(forKey: "__expo_widgets_\(source)_props") + let updateFunction = WidgetsStorage.getString(forKey: "__expo_widgets_\(source)_updateFunction") + + guard let props, let updateFunction else { + return .result() + } + if let context = JSContext() { + context.setObject(target, forKeyedSubscript: "target" as NSString) + context.setObject(props, forKeyedSubscript: "prevProps" as NSString) + let jsCode = "var updateFunction = \(String(describing: updateFunction)); updateFunction(target, prevProps);" + let resultValue = context.evaluateScript(jsCode) + + if let resultDict = resultValue?.toObject() as? [String: Any] { + WidgetsStorage.set(resultDict, forKey: "__expo_widgets_\(source)_props") + } else { + return .result() + } + } WidgetsEvents.shared.sendNotification(type: .userEvent, data: [ "source": source as Any, diff --git a/packages/expo-widgets/ios/WidgetsModule.swift b/packages/expo-widgets/ios/WidgetsModule.swift index 20eb5290945fc3..30b3ac3a2070e5 100644 --- a/packages/expo-widgets/ios/WidgetsModule.swift +++ b/packages/expo-widgets/ios/WidgetsModule.swift @@ -42,6 +42,9 @@ public final class WidgetsModule: Module { if let props { WidgetsStorage.set(props, forKey: "__expo_widgets_\(name)_props") } + if let updateFunction { + WidgetsStorage.set(updateFunction, forKey: "__expo_widgets_\(name)_updateFunction") + } } Function("startLiveActivity") { (name: String, nodes: String, url: URL?) throws -> String in diff --git a/packages/expo-widgets/src/ExpoWidgets.ts b/packages/expo-widgets/src/ExpoWidgets.ts index 3d3526bd6cf462..403f78820e8d1c 100644 --- a/packages/expo-widgets/src/ExpoWidgets.ts +++ b/packages/expo-widgets/src/ExpoWidgets.ts @@ -4,7 +4,12 @@ import { ExpoWidgetsEvents } from './Widgets.types'; declare class ExpoWidgetModule extends NativeModule { reloadWidget(timeline?: string): void; - updateWidget(name: string, data: string, props?: Record): void; + updateWidget( + name: string, + data: string, + props?: Record, + updateFunction?: string + ): void; startLiveActivity(name: string, nodes: string, url?: string): string; updateLiveActivity(id: string, name: string, nodes: string): string; } diff --git a/packages/expo-widgets/src/Widgets.ts b/packages/expo-widgets/src/Widgets.ts index 9ee7bed9cc7164..5060f437fe90a5 100644 --- a/packages/expo-widgets/src/Widgets.ts +++ b/packages/expo-widgets/src/Widgets.ts @@ -32,7 +32,8 @@ export const updateWidgetTimeline = ( name: string, dates: Date[], widget: (p: WidgetProps) => React.JSX.Element, - props?: T + props?: T, + updateFunction?: string ) => { const fakeProps = Object.keys(props || {}).reduce( (acc, key) => { @@ -58,7 +59,7 @@ export const updateWidgetTimeline = ( {} as Record ); - ExpoWidgetModule.updateWidget(name, serialize(data), props); + ExpoWidgetModule.updateWidget(name, serialize(data), props, updateFunction); ExpoWidgetModule.reloadWidget(); }; @@ -66,9 +67,10 @@ export const updateWidgetTimeline = ( export const updateWidgetSnapshot = ( name: string, widget: (p: WidgetProps) => React.JSX.Element, - props?: T + props?: T, + updateFunction?: string // (target: string, props: T) => T ) => { - updateWidgetTimeline(name, [new Date()], widget, props || ({} as T)); + updateWidgetTimeline(name, [new Date()], widget, props || ({} as T), updateFunction); }; export const addEventListener: typeof ExpoWidgetModule.addListener = ExpoWidgetModule.addListener; From 48f379dcc6d23a0053fab9f3f27a4b16f695a2c6 Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Tue, 20 Jan 2026 14:53:20 +0100 Subject: [PATCH 09/15] [go] Exclude expo-widgets from Expo Go (#42337) # Why `expo-widgets` should not be available in Expo Go # How Exclude package from autolinking # Test Plan `pod install` in `expo-go` --- apps/expo-go/android/settings.gradle | 3 ++- apps/expo-go/ios/Podfile | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/expo-go/android/settings.gradle b/apps/expo-go/android/settings.gradle index 9d2a00012493c7..049d38a5b09726 100644 --- a/apps/expo-go/android/settings.gradle +++ b/apps/expo-go/android/settings.gradle @@ -43,7 +43,8 @@ expoAutolinking.exclude = [ '@expo/ui', 'expo-mesh-gradient', '@expo/app-integrity', - '@expo/home' + '@expo/home', + 'expo-widgets' ] expoAutolinking.useExpoModules() diff --git a/apps/expo-go/ios/Podfile b/apps/expo-go/ios/Podfile index d12e5bcb865446..2e688cd646e867 100644 --- a/apps/expo-go/ios/Podfile +++ b/apps/expo-go/ios/Podfile @@ -52,7 +52,8 @@ target 'Expo Go' do 'expo-splash-screen', '@expo/ui', '@expo/app-integrity', - 'expo-brownfield' + 'expo-brownfield', + 'expo-widgets' ], includeTests: true, flags: { From 5da3f37395b49decb909e1d77ac3eb32fc9e8d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosmaty?= Date: Tue, 20 Jan 2026 15:30:44 +0100 Subject: [PATCH 10/15] [core][Android] Enable `worklets` integration by default (#42109) --- .../expo-modules-core/android/build.gradle | 33 +++++++++++-------- .../installers/WorkletRuntimeInstaller.cpp | 11 ++++--- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/expo-modules-core/android/build.gradle b/packages/expo-modules-core/android/build.gradle index dcdbc0ebc0c156..156089a1187757 100644 --- a/packages/expo-modules-core/android/build.gradle +++ b/packages/expo-modules-core/android/build.gradle @@ -41,6 +41,12 @@ def isExpoModulesCoreTests = { return false }.call() +def isTests = { + Gradle gradle = getGradle() + String tskReqStr = gradle.getStartParameter().getTaskRequests().toString() + return tskReqStr =~ /connected\w*AndroidTest/ +}.call() + def expoModuleExtension = project.extensions.getByType(ExpoModuleExtension) def reactNativeArchitectures() { @@ -57,15 +63,19 @@ USE_HERMES = USE_HERMES && isExpoModulesCoreTests def shouldTurnWarningsIntoErrors = findProperty("EXPO_TURN_WARNINGS_INTO_ERRORS") == "true" -def enableWorkletsIntegration = !isExpoModulesCoreTests && findProperty("expo.core.worklets") == "true" +def enableWorkletsIntegration = !isTests def workletsProject = enableWorkletsIntegration ? findProject(":react-native-worklets") : null -if (enableWorkletsIntegration && workletsProject == null) { - throw new GradleException("The 'expo.core.worklets' property is set to true, but the ':react-native-worklets' project is not found.") -} - if (workletsProject != null) { - evaluationDependsOn(":react-native-worklets") + evaluationDependsOn(workletsProject.path) + + afterEvaluate { + println("Linking react-native-worklets native libs into expo-modules-core build tasks") + println(workletsProject.tasks.getByName("mergeDebugNativeLibs")) + println(workletsProject.tasks.getByName("mergeReleaseNativeLibs")) + tasks.getByName("buildCMakeDebug").dependsOn(workletsProject.tasks.getByName("mergeDebugNativeLibs")) + tasks.getByName("buildCMakeRelWithDebInfo").dependsOn(workletsProject.tasks.getByName("mergeReleaseNativeLibs")) + } } def reactNativeWorkletsRootDir = workletsProject?.projectDir?.parentFile @@ -216,6 +226,10 @@ dependencies { compileOnly 'com.facebook.fbjni:fbjni:0.5.1' + if (workletsProject != null) { + implementation workletsProject + } + testImplementation 'androidx.test:core:1.7.0' testImplementation 'junit:junit:4.13.2' testImplementation 'io.mockk:mockk:1.13.10' @@ -301,10 +315,3 @@ def generatePCHTask = tasks.register("generatePCH") { tasks.register("prepareKotlinBuildScriptModel") { dependsOn(generatePCHTask) } - -if (workletsProject != null) { - afterEvaluate { - tasks.getByName("externalNativeBuildDebug").dependsOn(findProject(":react-native-worklets").tasks.getByName("externalNativeBuildDebug")) - tasks.getByName("externalNativeBuildRelease").dependsOn(findProject(":react-native-worklets").tasks.getByName("externalNativeBuildRelease")) - } -} diff --git a/packages/expo-modules-core/android/src/main/cpp/installers/WorkletRuntimeInstaller.cpp b/packages/expo-modules-core/android/src/main/cpp/installers/WorkletRuntimeInstaller.cpp index 7a3729c8440d95..37cf57f8538463 100644 --- a/packages/expo-modules-core/android/src/main/cpp/installers/WorkletRuntimeInstaller.cpp +++ b/packages/expo-modules-core/android/src/main/cpp/installers/WorkletRuntimeInstaller.cpp @@ -71,11 +71,12 @@ void WorkletRuntimeInstaller::prepareRuntime( cxxPart ); - MainRuntimeInstaller::installModules( - runtime, - cxxPart, - mainObject - ); +// TODO(@lukmccall): Re-enable module installation when iOS start supporting exporting native modules +// MainRuntimeInstaller::installModules( +// runtime, +// cxxPart, +// mainObject +// ); #endif } From 0f0c5be49caa205a17c2798c157ce06ac5dccf70 Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Tue, 20 Jan 2026 20:02:06 +0530 Subject: [PATCH 11/15] [docs] Improve and organize EAS environment variables (#41604) Co-authored-by: Kadi Kraman --- docs/constants/navigation.js | 13 +- docs/deploy.sh | 3 + docs/pages/eas-update/debug.mdx | 2 +- docs/pages/eas/environment-variables.mdx | 453 ------------------ docs/pages/eas/environment-variables/faq.mdx | 77 +++ .../pages/eas/environment-variables/index.mdx | 157 ++++++ .../eas/environment-variables/manage.mdx | 244 ++++++++++ .../pages/eas/environment-variables/usage.mdx | 197 ++++++++ .../eas/environment-variables/without-eas.mdx | 36 ++ .../eas/hosting/environment-variables.mdx | 47 -- docs/pages/guides/environment-variables.mdx | 2 +- docs/pages/guides/google-authentication.mdx | 2 +- docs/public/static/images/env-vars/add.png | Bin 50395 -> 31511 bytes .../static/images/env-vars/creation-form.png | Bin 0 -> 28696 bytes .../images/env-vars/custom-environment.png | Bin 0 -> 28464 bytes docs/public/static/images/env-vars/export.png | Bin 0 -> 85495 bytes docs/public/static/images/env-vars/list.png | Bin 153078 -> 91626 bytes 17 files changed, 728 insertions(+), 505 deletions(-) delete mode 100644 docs/pages/eas/environment-variables.mdx create mode 100644 docs/pages/eas/environment-variables/faq.mdx create mode 100644 docs/pages/eas/environment-variables/index.mdx create mode 100644 docs/pages/eas/environment-variables/manage.mdx create mode 100644 docs/pages/eas/environment-variables/usage.mdx create mode 100644 docs/pages/eas/environment-variables/without-eas.mdx delete mode 100644 docs/pages/eas/hosting/environment-variables.mdx create mode 100644 docs/public/static/images/env-vars/creation-form.png create mode 100644 docs/public/static/images/env-vars/custom-environment.png create mode 100644 docs/public/static/images/env-vars/export.png diff --git a/docs/constants/navigation.js b/docs/constants/navigation.js index 377ac97eaced3e..0e51215a52c5c6 100644 --- a/docs/constants/navigation.js +++ b/docs/constants/navigation.js @@ -408,7 +408,17 @@ export const eas = [ makePage('eas/index.mdx'), makePage('eas/json.mdx'), makePage('eas/cli.mdx'), - makePage('eas/environment-variables.mdx'), + makeGroup( + 'Environment variables', + [ + makePage('eas/environment-variables/index.mdx'), + makePage('eas/environment-variables/manage.mdx'), + makePage('eas/environment-variables/usage.mdx'), + makePage('eas/environment-variables/without-eas.mdx'), + makePage('eas/environment-variables/faq.mdx'), + ], + { expanded: false } + ), ], { expanded: true, @@ -499,7 +509,6 @@ export const eas = [ makePage('eas/hosting/introduction.mdx'), makePage('eas/hosting/get-started.mdx'), makePage('eas/hosting/deployments-and-aliases.mdx'), - makePage('eas/hosting/environment-variables.mdx'), makePage('eas/hosting/custom-domain.mdx'), makePage('eas/hosting/api-routes.mdx'), makePage('eas/hosting/workflows.mdx'), diff --git a/docs/deploy.sh b/docs/deploy.sh index 3aa7ee38242ee0..59c8241ddb6398 100755 --- a/docs/deploy.sh +++ b/docs/deploy.sh @@ -377,6 +377,9 @@ redirects[router/reference/middleware]=router/web/middleware redirects[router/reference/static-rendering]=router/web/static-rendering redirects[router/reference/async-routes]=router/web/async-routes +# After creating EAS environment variables section +redirects[eas/hosting/environment-variables]=eas/environment-variables/usage/#using-environment-variables-with-eas-hosting + echo "::group::[5/6] Add custom redirects" for i in "${!redirects[@]}" # iterate over keys do diff --git a/docs/pages/eas-update/debug.mdx b/docs/pages/eas-update/debug.mdx index cb8d9fd5c5ef75..1ba0093cb57467 100644 --- a/docs/pages/eas-update/debug.mdx +++ b/docs/pages/eas-update/debug.mdx @@ -102,7 +102,7 @@ To diagnose the error causing the update crash: - See the [Troubleshooting guide on runtime issues](/debugging/runtime-issues/) to apply a strategy to identify the error. - After identifying the error, publish a new update that fixes the crash to resolve the issue. -A common reason a new update does not work but embedded code does is due to a missing environment variable. See [how environment variables work with EAS Update](/eas-update/environment-variables/) for more information. +A common reason a new update does not work but embedded code does is due to a missing environment variable. See [how environment variables work with EAS Update](/eas/environment-variables/usage/#using-environment-variables-with-eas-update) for more information. ### Failed to load all assets diff --git a/docs/pages/eas/environment-variables.mdx b/docs/pages/eas/environment-variables.mdx deleted file mode 100644 index 30941c23d7c22b..00000000000000 --- a/docs/pages/eas/environment-variables.mdx +++ /dev/null @@ -1,453 +0,0 @@ ---- -title: Environment variables in EAS -sidebar_title: Environment variables -description: Learn how to use and manage environment variables in EAS with examples. -searchRank: 8 ---- - -import { Collapsible } from '~/ui/components/Collapsible'; -import { ContentSpotlight } from '~/ui/components/ContentSpotlight'; -import { Terminal } from '~/ui/components/Snippet'; -import { CODE } from '~/ui/components/Text'; - -[Environment variables in Expo](/guides/environment-variables) describe how to use environment variables with the Expo framework and **.env** files to set environment variables that can be inlined in your JavaScript code. Expo CLI will substitute prefixed variables in your code (for example, `process.env.EXPO_PUBLIC_VARNAME`) with the corresponding environment variable values in **.env** files on your development machine. - -Since EAS Build and Workflows jobs run on a remote server, **.env** files may not be available. For instance, if **.env** files are excluded from your project, it is because they are listed in **.gitignore** or not committed to your local version control system. - -Additionally, you may want to use environment variables outside of your project code to customize your app binary at build time, like setting a bundle identifier or a private key for an error reporting service. To accommodate for those needs we have a separate (but compatible) mechanism for managing environment variables in EAS, which is focused on storing and managing environment variables on EAS servers and synchronizing them for local development. - -This guide describes how to use and manage environment variables in EAS with key practical examples. - -## Key concepts - - - -By default, all projects have three environments for environment variables: `development`, `preview` and `production`. On **Production** and **Enterprise** plans, you can also create custom environments to better organize your environment variables for complex workflows. Environments are independent sets of environment variables that can be used to customize your app in different contexts. For example, you might want to use different API keys for development and production, or different bundle identifiers for different App Store releases. Environments allows you to do so. Every EAS Build and Workflows job runs using environment variables from one of the available environments. You can also use environments for updates, allowing you to use the same set of environment variables for your build jobs. You can do this when publishing an update by providing the `--environment` flag. - -Environment variables can be assigned to multiple environments and have the same value across all of them, or be created for a single environment, so that you can set a specific value for a single environment. - - - - - -Project-wide environment variables are specific to a single EAS project. You can view and manage them by navigating to the [Environment Variables](https://expo.dev/accounts/[account]/projects/[project]/environment-variables) page on your project. - -These environment variables are available in any jobs that run on EAS servers and updates for this project. They can also be pulled locally for development if their visibility setting allows it. - - - - - -Account-wide environment variables are available across all of your projects in your EAS account. You can view and manage them by navigating to [Environment Variables](https://expo.dev/accounts/[account]/settings/environment-variables) page on your account. - -They are available in jobs that run on EAS servers and updates, together with project-wide variables for a project. You can pull them locally or read outside of EAS servers if their visibility setting allows it. - - - - - -There are three different visibility settings: - -| Visibility | Description | -| ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| Plain text | Visible on the website, in EAS CLI, and in logs. | -| Sensitive | Obfuscated in build and workflow jobs logs. You can use a toggle to make them visible on the website. They're also readable in EAS CLI. | -| Secret | Not readable outside of the EAS servers, including on the website and in EAS CLI. They are obfuscated in build and workflow jobs logs. | - -> **warning** Always remember that **anything that is included in your client side code should be considered public and readable to any individual that can run the application**. Secret type environment variables are intended to be used to provide values to an EAS Build or Workflows job so that they may be used to alter how a job runs. For example, a good use case is setting an `NPM_TOKEN` to install private packages from npm, or a setting a Sentry API key to create a release and upload your source maps. Secrets do not provide any additional security for values that you end up embedding in your application itself. - - - -## Creating and using environment variables - -The sections below use the following common environment variables as examples: - -- `EXPO_PUBLIC_API_URL`: a plain text [`EXPO_PUBLIC_`](/guides/environment-variables/) variable that holds the URL of the API server -- `APP_VARIANT`: a plain text variable to select an [app variant](/tutorial/eas/multiple-app-variants/) -- `GOOGLE_SERVICES_JSON`: a secret file variable to supply your git ignored **google-services.json** file to the build job -- `SENTRY_AUTH_TOKEN`: a sensitive variable that holds the authentication token for Sentry used to upload source maps after builds and updates - -### Use environment variables in your code - -The environment variables with the [`EXPO_PUBLIC_`](/guides/environment-variables) prefix are available as `process.env` variables in your app's code. This means you can use them to dynamically configure your app behavior based on the values from environment variables. - -```tsx -import { Button } from 'react-native'; - -function Post() { - const apiUrl = process.env.EXPO_PUBLIC_API_URL; - - async function onPress() { - await fetch(apiUrl, { ... }) - } - - return