Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/detect-platform-change/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ inputs:
description: 'Paths to ignore for all platforms'
required: false
default: >-
"!{**/*.md,**/LICENSE*,**/.gitignore,**/CHANGELOG*,**/.npmignore,**/.eslintrc*,**/.prettierrc*,**/.gitattributes,**/.watchmanconfig,**/.fingerprintignore}"
"!{**/*.md,**/LICENSE*,**/.gitignore,**/CHANGELOG*,**/.npmignore,**/.eslintrc*,**/.prettierrc*,**/.gitattributes,**/.watchmanconfig,**/.fingerprintignore,**/*.web.*}"

outputs:
should_run_android:
Expand Down
11 changes: 5 additions & 6 deletions .github/workflows/brownfield.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,6 @@ jobs:
runs-on: macos-15
needs: analyze-changes
if: needs.analyze-changes.outputs.compilation_test_ios == 'true' || needs.analyze-changes.outputs.all_tests == 'true'
strategy:
fail-fast: true
matrix:
build-type: [debug, release]
steps:
- name: 👀 Checkout
uses: actions/checkout@v5
Expand All @@ -302,10 +298,13 @@ jobs:
run: yarn install --frozen-lockfile
- name: 👷 Build Expo CLI
run: yarn workspace @expo/cli prepare
- name: 🍏 Build iOS artifacts
- name: 🍏 Build iOS artifacts (Release)
run: |
npx expo prebuild --clean --platform ios
npx expo-brownfield build:ios --${{ matrix.build-type }} --verbose
npx expo-brownfield build:ios --release --verbose
working-directory: apps/brownfield-tester/expo-app
- name: 🍏 Build iOS artifacts (Debug)
run: npx expo-brownfield build:ios --debug --verbose
working-directory: apps/brownfield-tester/expo-app
- name: 🔔 Notify on Slack
uses: ./.github/actions/slack-notify
Expand Down
42 changes: 37 additions & 5 deletions .github/workflows/test-suite-brownfield-isolated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,10 @@ jobs:
"packages/expo-brownfield/src/**",
"packages/expo-brownfield/e2e/maestro/**"

android-e2e:
android-build:
runs-on: ubuntu-24.04
needs: detect-platform-for-e2e
if: needs.detect-platform-for-e2e.outputs.should_run_android == 'true'
strategy:
matrix:
api-level: [36]
env:
ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:MaxMetaspaceSize=4096m"
Expand Down Expand Up @@ -84,11 +81,46 @@ jobs:
react-native-gradle-downloads: 'true'
- name: 🧶 Install workspace node modules
run: yarn install --frozen-lockfile
- name: 👷 Build Expo CLI
run: yarn workspace @expo/cli prepare
- name: 🤖 Build and publish Android artifacts (apps/brownfield-tester/expo-app)
run: |
npx expo prebuild --clean -p android
npx expo-brownfield build:android --repo MavenLocal --verbose
working-directory: apps/brownfield-tester/expo-app
- name: 🏗️ Build APK
run: |
./gradlew clean
./gradlew assembleRelease --refresh-dependencies
working-directory: apps/brownfield-tester/android-integrated
- name: 💾 Store APK artifact
uses: actions/upload-artifact@v4
with:
name: expo-brownfield-android-release
path: apps/brownfield-tester/android-integrated/app/build/outputs/apk/release/app-release.apk

android-e2e:
runs-on: ubuntu-24.04
needs: android-build
strategy:
matrix:
api-level: [36]
steps:
- name: 👀 Checkout
uses: actions/checkout@v5
- name: 🧹 Cleanup GitHub Linux runner disk space
uses: ./.github/actions/cleanup-linux-disk-space
- name: 🏗️ Setup Bun
uses: oven-sh/setup-bun@v2
with:
# Version `1.x` fails due to https://github.com/oven-sh/setup-bun/issues/37
# TODO(cedric): swap `latest` back once the issue is resolved
bun-version: latest
- name: 🌠 Download builds
uses: actions/download-artifact@v4
with:
name: expo-brownfield-android-release
path: apps/brownfield-tester/android-integrated/apk
- name: 🍺 Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
Expand All @@ -99,7 +131,7 @@ jobs:
avd-api: ${{ matrix.api-level }}
avd-name: avd-${{ matrix.api-level }}
script: |
cd apps/brownfield-tester/android-integrated && ./gradlew clean && ./gradlew installRelease --refresh-dependencies
adb install -r apps/brownfield-tester/android-integrated/apk/*.apk
cd packages/expo-brownfield/e2e && maestro test maestro/__tests__/android
- name: 🔔 Notify on Slack
uses: ./.github/actions/slack-notify
Expand Down
13 changes: 0 additions & 13 deletions apps/expo-go/android/expoview/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,6 @@
<string name="persistent_notification_channel_group">Expo</string>
<string name="persistent_notification_channel_name">Experience notifications</string>
<string name="persistent_notification_channel_desc">Persistent notifications that provide debug info about open experiences</string>
<string name="devmenu_hide_element_inspector">Hide Element Inspector</string>
<string name="devmenu_show_element_inspector">Show Element Inspector</string>
<string name="devmenu_element_inspector_unavailable">Element Inspector Unavailable</string>
<string name="devmenu_open_js_debugger">Open JS Debugger</string>
<string name="devmenu_disable_fast_refresh">Disable Fast Refresh</string>
<string name="devmenu_enable_fast_refresh">Enable Fast Refresh</string>
<string name="devmenu_show_fab">Show developer action button</string>
<string name="devmenu_hide_fab">Hide developer action button</string>
<string name="devmenu_fast_refresh_unavailable">Fast Refresh Unavailable</string>
<string name="devmenu_fast_refresh_unavailable_details">Use the Reload button above to reload when in production mode. Switch back to development mode to use Fast Refresh.</string>
<string name="devmenu_hide_performance_monitor">Hide Performance Monitor</string>
<string name="devmenu_show_performance_monitor">Show Performance Monitor</string>
<string name="devmenu_performance_monitor_unavailable">Performance Monitor Unavailable</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="help_dialog">Make sure you are signed in to the same Expo account on your computer and this app. Also verify that your computer is connected to the internet, and ideally to the same Wi-Fi network as your mobile device. Lastly, ensure that you are using the latest version of Expo CLI. Pull to refresh to update.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ - (RCTDevSettings *) devSettings:(id)host {
}

items[@"dev-remote-debug"] = @{
@"label": @"Open JS Debugger",
@"label": @"Open DevTools",
@"isEnabled": @YES
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import ListButton from '../../components/ListButton';

type Props = {
recorder?: AudioRecorder;
canRecord?: boolean;
};

function AudioInputSelector({ recorder }: Props) {
function AudioInputSelector({ recorder, canRecord }: Props) {
const [availableInputs, setAvailableInputs] = useState<RecordingInput[]>([]);
const [currentInput, setCurrentInput] = useState<RecordingInput | null>(null);

Expand All @@ -23,7 +24,7 @@ function AudioInputSelector({ recorder }: Props) {

useEffect(() => {
checkInputs();
}, [checkInputs]);
}, [checkInputs, canRecord]);

return (
<View>
Expand Down
10 changes: 8 additions & 2 deletions apps/native-component-list/src/screens/Audio/AudioPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@ type AudioPlayerProps = {
source: AudioSource | string | number;
style?: StyleProp<ViewStyle>;
downloadFirst?: boolean;
crossOrigin?: 'anonymous' | 'use-credentials';
};

const localSource = require('../../../assets/sounds/polonez.mp3');
const remoteSource =
'https://p.scdn.co/mp3-preview/f7a8ab9c5768009b65a30e9162555e8f21046f46?cid=162b7dc01f3a4a2ca32ed3cec83d1e02';

export default function AudioPlayer({ source, style, downloadFirst = false }: AudioPlayerProps) {
export default function AudioPlayer({
source,
style,
downloadFirst = false,
crossOrigin,
}: AudioPlayerProps) {
const [currentSource, setCurrentSource] = React.useState(source);
const player = useAudioPlayer(source, { downloadFirst });
const player = useAudioPlayer(source, { downloadFirst, crossOrigin });
const status = useAudioPlayerStatus(player);

const setVolume = (volume: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function AudioScreen(props: any) {
Auth: 'Bearer some-token',
},
}}
crossOrigin="anonymous"
style={styles.player}
/>
<HeadingText>Local asset player</HeadingText>
Expand Down
6 changes: 5 additions & 1 deletion apps/native-component-list/src/screens/Audio/JsiAudioBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export function JsiAudioBar({ player, isPlaying }: { player: AudioPlayer; isPlay
});

if (!player.isAudioSamplingSupported) {
return <Text style={styles.errorText}>Audio sampling is not supported on this platform</Text>;
return (
<Text style={styles.errorText}>
Audio sampling requires the crossOrigin option for cross-origin sources
</Text>
);
}

if (!isPlaying) {
Expand Down
2 changes: 1 addition & 1 deletion apps/native-component-list/src/screens/Audio/Recorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export default function Recorder({ onDone, style }: RecorderProps) {
{_formatTime(recorderState.durationMillis / 1000)}
</Text>
</View>
<AudioInputSelector recorder={audioRecorder} />
<AudioInputSelector recorder={audioRecorder} canRecord={recorderState.canRecord} />
{maybeRenderErrorOverlay()}
</View>
);
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/debugging/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The Developer menu provides the following options:
- **Go Home**: To leave your app and navigate back to the dev client's or Expo Go app's Home screen.
- **Toggle performance monitor**: To view the performance information about your app.
- **Toggle element inspector**: To enable or disable the element inspector overlay.
- **Open JS debugger**: To open React Native DevTools which provides access to Console, Sources, Network (**Expo only**), Memory, Components, and Profiler, tabs for apps using Hermes. For more information, see the [Debugging with React Native DevTools](#debugging-with-react-native-devtools) section.
- **Open DevTools** (formerly **Open JS debugger**): To open React Native DevTools which provides access to Console, Sources, Network (**Expo only**), Memory, Components, and Profiler, tabs for apps using Hermes. For more information, see the [Debugging with React Native DevTools](#debugging-with-react-native-devtools) section.
- **Fast Refresh**: To toggle automatic refreshing of the JS bundle whenever you make changes to files in your project using a text editor.

Now, let's explore some of these options in details.
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/guides/using-hermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Note that the Hermes bytecode format may change between different Hermes version

## JavaScript debugger

To debug JavaScript code running with Hermes, you can start your project with `npx expo start` then press <kbd>j</kbd> to open the debugger in Google Chrome or Microsoft Edge. The developer menu of development builds and Expo Go also have the **Open JS Debugger** option to do the same.
To debug JavaScript code running with Hermes, you can start your project with `npx expo start` then press <kbd>j</kbd> to open the debugger in Google Chrome or Microsoft Edge. The developer menu of development builds and Expo Go also have the **Open DevTools** (formerly **Open JS Debugger**) option to do the same.

Alternatively, you can use the JavaScript inspector by opening [Google Chrome DevTools manually](https://reactnative.dev/docs/other-debugging-methods#remote-javascript-debugging-deprecated)

Expand Down
23 changes: 13 additions & 10 deletions docs/scripts/generate-markdown-pages-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,7 +1352,6 @@ describe('extractFrontmatter', () => {
path.join(tmpDir, 'full.mdx'),
[
'---',
'modificationDate: July 08, 2025',
'title: Camera',
'description: A camera component.',
"platforms: ['android', 'ios']",
Expand Down Expand Up @@ -1385,28 +1384,32 @@ describe('extractFrontmatter', () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it('extracts only modificationDate including delimiters', () => {
it('extracts full frontmatter including delimiters', () => {
const result = extractFrontmatter(path.join(tmpDir, 'full.mdx'));
expect(result).not.toBeNull();
expect(result).toContain('modificationDate: July 08, 2025');
expect(result).not.toContain('title: Camera');
expect(result).not.toContain('description: A camera component.');
expect(result).not.toContain('platforms:');
expect(result).toContain('title: Camera');
expect(result).toContain('description: A camera component.');
expect(result).toContain('platforms:');
// Should include --- delimiters
expect(result).toMatch(/^---\n/);
expect(result).toMatch(/\n---\n$/);
// Should NOT include content after frontmatter
expect(result).not.toContain('import');
});

it('returns null when frontmatter has no modificationDate', () => {
it('extracts minimal frontmatter', () => {
const result = extractFrontmatter(path.join(tmpDir, 'minimal.mdx'));
expect(result).toBeNull();
expect(result).not.toBeNull();
expect(result).toContain('title: Minimal');
expect(result).not.toContain('# Minimal');
});

it('returns null when modificationDate is empty', () => {
it('strips lines with empty values', () => {
const result = extractFrontmatter(path.join(tmpDir, 'empty-values.mdx'));
expect(result).toBeNull();
expect(result).not.toBeNull();
expect(result).toContain('title: Camera');
expect(result).toContain('description: A camera.');
expect(result).not.toContain('modificationDate');
});

it('returns null when all frontmatter fields are empty', () => {
Expand Down
7 changes: 4 additions & 3 deletions docs/scripts/generate-markdown-pages-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export function findMdxSource(htmlPath: string, outDir: string, pagesDir: string

/**
* Extract the raw YAML frontmatter block (including --- delimiters) from an MDX file.
* Keeps only non-empty `modificationDate` and drops other frontmatter fields.
* Strips lines with empty values (e.g. `modificationDate:` injected by append-dates.js
* with no value in shallow CI clones).
* Returns the frontmatter string with trailing newline, or null if no frontmatter found.
*/
export function extractFrontmatter(mdxPath: string): string | null {
Expand All @@ -36,12 +37,12 @@ export function extractFrontmatter(mdxPath: string): string | null {
}
const filtered = match[1]
.split('\n')
.filter(line => /^modificationDate:\s+\S/.test(line))
.filter(line => !/^\w+:\s*$/.test(line))
.join('\n');
if (!filtered.trim()) {
return null;
}
return '---\n' + filtered + '\n---\n';
return '---\n' + filtered + '---\n';
}

function createTurndownService(): TurndownService {
Expand Down
2 changes: 1 addition & 1 deletion guides/releasing/Quality Assurance.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Unversioned QA: Test in native-component-list.
- Reload manually, your change should appear
- Make and save another change, reenable Fast Refresh, your change should show up automatically
- Debug JS in-place
- Open JS debugger either pressing `j` or from the dev menu in Expo Go
- Open DevTools either pressing `j` or from the dev menu in Expo Go
- Add a breakpoint (maybe add a button to your app), ensure the breakpoint works
- Click Reload on the webpage, make sure it reloads the app
- Other dev tools
Expand Down
2 changes: 1 addition & 1 deletion packages/@expo/cli/e2e/__tests__/export/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ describe('server-output', () => {
(server.isWorkerd ? it.skip : it)('supports runtime API', async () => {
await expect(server.fetchAsync('/api/runtime').then((r) => r.json())).resolves.toEqual({
environment: expect.stringMatching(/production|development/),
origin: 'null',
origin: expect.stringMatching(/^http/),
});
});

Expand Down
5 changes: 5 additions & 0 deletions packages/expo-audio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

- [iOS] Add support for `shouldRouteThroughEarpiece`. ([#43089](https://github.com/expo/expo/pull/43089) by [@alanjhughes](https://github.com/alanjhughes))
- [Android] Make it possible to add/remove the foreground service and foreground service permissions with a config plugin. ([#43014](https://github.com/expo/expo/pull/43014) by [@behenate](https://github.com/behenate))
- [Web] Add support for audio sampling. ([#43149](https://github.com/expo/expo/pull/43149) by [@alanjhughes](https://github.com/alanjhughes))
- [Web] Add support for media controls. ([#43150](https://github.com/expo/expo/pull/43150) by [@alanjhughes](https://github.com/alanjhughes))
- [Web] Add support for selecting recording inputs. ([#43151](https://github.com/expo/expo/pull/43151) by [@alanjhughes](https://github.com/alanjhughes))
- [Web] Add support for recording metering. ([#43152](https://github.com/expo/expo/pull/43152) by [@alanjhughes](https://github.com/alanjhughes))

### 🐛 Bug fixes

Expand All @@ -17,6 +21,7 @@
### 💡 Others

- [Android] Improve event handling. ([#43121](https://github.com/expo/expo/pull/43121) by [@alanjhughes](https://github.com/alanjhughes))
- [Android] Rework native audio service handling. ([#43015](https://github.com/expo/expo/pull/43015) by [@behenate](https://github.com/behenate))

## 55.0.5 — 2026-02-08

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@ package expo.modules.audio

import expo.modules.kotlin.exception.CodedException

private const val defaultPlaybackServiceTip =
"Make sure that the expo-audio config plugin is properly configured with " +
"'enableBackgroundPlayback: true' to avoid issues with lock screen controls " +
"and background audio playback."

private const val defaultRecordingServiceTip =
"Make sure that the expo-audio config plugin is properly configured with " +
"'enableBackgroundRecording: true' to avoid issues with background recording."

internal class AudioPermissionsException :
CodedException("RECORD_AUDIO permission has not been granted")

internal class NotificationPermissionsException :
CodedException(
"POST_NOTIFICATIONS permission has not been granted. This permission is required when using background recording (allowsBackgroundRecording: true). " +
"Request notification permissions using AudioModule.requestNotificationPermissionsAsync() before calling prepareRecording()."
)

internal class GetAudioInputNotSupportedException :
CodedException("Getting current audio input is not supported on devices running Android version lower than Android 9.0")

Expand All @@ -22,3 +37,23 @@ internal class AudioRecorderPrepareException(cause: Throwable) :

internal class AudioRecorderAlreadyPreparedException :
CodedException("AudioRecorder has already been prepared. Stop or release the current session before preparing again.")

internal fun getPlaybackServiceErrorMessage(
message: String?,
tip: String = defaultPlaybackServiceTip
): String {
return (message ?: "expo-audio playback service error") + ". $tip"
}

internal fun getRecordingServiceErrorMessage(
message: String?,
tip: String = defaultRecordingServiceTip
): String {
return (message ?: "expo-audio recording service error") + ". $tip"
}

internal class AudioRecordingServiceException(message: String?, cause: Throwable? = null) :
CodedException(getRecordingServiceErrorMessage(message), cause)

internal class AudioPlaybackServiceException(message: String?, cause: Throwable? = null) :
CodedException(getPlaybackServiceErrorMessage(message), cause)
Loading
Loading