diff --git a/apps/native-component-list/src/screens/FileSystemScreen.tsx b/apps/native-component-list/src/screens/FileSystemScreen.tsx index 1ebbb2f2b83fac..128a460f53388d 100644 --- a/apps/native-component-list/src/screens/FileSystemScreen.tsx +++ b/apps/native-component-list/src/screens/FileSystemScreen.tsx @@ -109,10 +109,10 @@ function FileSourcesSection({ setCurrentFile }: { setCurrentFile: (f: File) => v { + onPress={async () => { const file = new File(Paths.cache, 'test_sandbox', 'test.txt'); file.create({ intermediates: true, overwrite: true }); - file.write('Hello from FileSystem sandbox! Timestamp: ' + Date.now()); + await file.write('Hello from FileSystem sandbox! Timestamp: ' + Date.now()); setCurrentFile(file); Alert.alert('Created', file.uri); }} @@ -279,7 +279,7 @@ function ReadWriteSection({ withCurrentFile }: { withCurrentFile: WithCurrentFil { - file.write('Written at ' + new Date().toISOString()); + await file.write('Written at ' + new Date().toISOString()); return 'OK - size is now: ' + file.size; })} /> @@ -287,7 +287,7 @@ function ReadWriteSection({ withCurrentFile }: { withCurrentFile: WithCurrentFil title="write() base64" action={withCurrentFile(async (file) => { // Base64 of "Hello Base64!" - file.write('SGVsbG8gQmFzZTY0IQ==', { encoding: 'base64' }); + await file.write('SGVsbG8gQmFzZTY0IQ==', { encoding: 'base64' }); return 'OK - text() = ' + truncate(await file.text()); })} /> @@ -295,7 +295,7 @@ function ReadWriteSection({ withCurrentFile }: { withCurrentFile: WithCurrentFil title="write() Uint8Array" action={withCurrentFile(async (file) => { const bytes = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" - file.write(bytes); + await file.write(bytes); return 'OK - text() = ' + file.textSync(); })} /> @@ -687,7 +687,7 @@ function DirectoryOperationsSection({ title="Create file 'test_created.txt' in picked dir" action={async () => { const file = safDirectory.createFile('test_created.txt', 'text/plain'); - file.write('Created at ' + new Date().toISOString()); + await file.write('Created at ' + new Date().toISOString()); setCurrentFile(file); return { uri: file.uri, name: file.name }; }} @@ -788,7 +788,7 @@ function FileLifecycleSection({ const name = `test_${Date.now()}.txt`; const file = new File(Paths.cache, 'test_sandbox', name); file.create({ intermediates: true }); - file.write('Created for lifecycle test'); + await file.write('Created for lifecycle test'); setCurrentFile(file); return { uri: file.uri, exists: file.exists, size: file.size }; }} diff --git a/apps/native-component-list/src/screens/UI/NavigationBarScreen.android.tsx b/apps/native-component-list/src/screens/UI/NavigationBarScreen.android.tsx new file mode 100644 index 00000000000000..47d63b4d3b8d9d --- /dev/null +++ b/apps/native-component-list/src/screens/UI/NavigationBarScreen.android.tsx @@ -0,0 +1,69 @@ +import { + Box, + Column, + Host, + Icon, + NavigationBar, + NavigationBarItem, + Surface, + Text as ComposeText, +} from '@expo/ui/jetpack-compose'; +import { align, fillMaxSize, fillMaxWidth, padding } from '@expo/ui/jetpack-compose/modifiers'; +import * as React from 'react'; + +const mailIcon = require('../../../assets/icons/ui/mail.xml'); +const personIcon = require('../../../assets/icons/ui/person.xml'); +const wifiIcon = require('../../../assets/icons/ui/wifi.xml'); + +const tabs = [ + { key: 'mail', label: 'Mail', icon: mailIcon }, + { key: 'profile', label: 'Profile', icon: personIcon }, + { key: 'network', label: 'Network', icon: wifiIcon }, +] as const; + +export default function NavigationBarScreen() { + const [selectedTab, setSelectedTab] = React.useState<(typeof tabs)[number]['key']>('mail'); + const selected = tabs.find((tab) => tab.key === selectedTab) ?? tabs[0]; + + return ( + + + + + + {selected.label} + + Tap a destination in the navigation bar. + + + + + + {tabs.map((tab) => ( + setSelectedTab(tab.key)}> + + + + + + + + {tab.label} + + + ))} + + + + ); +} + +NavigationBarScreen.navigationOptions = { + title: 'NavigationBar', +}; diff --git a/apps/native-component-list/src/screens/UI/UIScreen.android.tsx b/apps/native-component-list/src/screens/UI/UIScreen.android.tsx index 411d33a21a0455..42adb17f3f11ec 100644 --- a/apps/native-component-list/src/screens/UI/UIScreen.android.tsx +++ b/apps/native-component-list/src/screens/UI/UIScreen.android.tsx @@ -266,6 +266,14 @@ export const UIScreens = [ return optionalRequire(() => require('./ListScreen')); }, }, + { + name: 'NavigationBar component', + route: 'ui/navigation-bar', + options: {}, + getComponent() { + return optionalRequire(() => require('./NavigationBarScreen')); + }, + }, { name: 'BottomSheet component', route: 'ui/bottomsheet', diff --git a/apps/test-suite/tests/ContactsNext.ts b/apps/test-suite/tests/ContactsNext.ts index 4a90c4a24a5352..1ba7e7b02cad71 100644 --- a/apps/test-suite/tests/ContactsNext.ts +++ b/apps/test-suite/tests/ContactsNext.ts @@ -120,7 +120,7 @@ export async function test(t) { const url = 'https://picsum.photos/200'; const response = await fetch(url); const src = new File(Paths.cache, 'file.pdf'); - src.write(await response.bytes()); + await src.write(await response.bytes()); const contactDetails = { givenName: 'Image', familyName: 'User', @@ -960,7 +960,7 @@ export async function test(t) { const url = 'https://picsum.photos/200'; const response = await fetch(url); const src = new File(Paths.cache, 'file.pdf'); - src.write(await response.bytes()); + await src.write(await response.bytes()); await contact.setImage(src.uri); const retrievedImage = await contact.getImage(); const retrievedThumbnail = await contact.getThumbnail(); diff --git a/apps/test-suite/tests/FileSystem.ts b/apps/test-suite/tests/FileSystem.ts index 9c3eb73dc2c166..6b7ced025bafe8 100644 --- a/apps/test-suite/tests/FileSystem.ts +++ b/apps/test-suite/tests/FileSystem.ts @@ -79,13 +79,13 @@ export async function test({ describe, expect, it, ...t }) { expect(safDirectory.list().length).toBe(0); const file = safDirectory.createFile('newFile', 'text/plain'); - file.write('test'); + file.writeSync('test'); expect(file.textSync()).toBe('test'); expect(file.bytesSync()).toEqual(new Uint8Array([116, 101, 115, 116])); expect(file.base64Sync()).toBe('dGVzdA=='); const file2 = safDirectory.createFile('newFile2', 'text/plain'); - file2.write(new Uint8Array([116, 101, 115, 116])); + file2.writeSync(new Uint8Array([116, 101, 115, 116])); expect(file2.textSync()).toBe('test'); expect(file2.size).toBe(4); expect(safDirectory.size).toBe(8); @@ -111,7 +111,7 @@ export async function test({ describe, expect, it, ...t }) { if (Platform.OS === 'ios') { it('allows picking files', async () => { const file = new File(testDirectory, 'selectMe.txt'); - file.write('test'); + file.writeSync('test'); const result = await File.pickFileAsync(testDirectory); const safFile = Array.isArray(result) ? result[0] : result; @@ -124,7 +124,7 @@ export async function test({ describe, expect, it, ...t }) { expect(directory.exists).toBe(true); const file = new File(directory, 'test.txt'); - file.write('test'); + file.writeSync('test'); expect(file.exists).toBe(true); const selectedDirectory = await Directory.pickDirectoryAsync(testDirectory); @@ -138,7 +138,7 @@ export async function test({ describe, expect, it, ...t }) { // Create a file in the selected directory const file2 = new File(directory.uri, 'newFile.txt'); - file2.write('test'); + file2.writeSync('test'); expect(file2.exists).toBe(true); expect(file2.textSync()).toBe('test'); @@ -249,13 +249,13 @@ export async function test({ describe, expect, it, ...t }) { it('emits a modified event when a watched file is written', async () => { const file = new File(watcherDirectory, 'modified.txt'); file.create(); - file.write('before'); + file.writeSync('before'); const events: { type: string }[] = []; const subscription = file.watch((event) => events.push(event)); try { - file.write('after'); + file.writeSync('after'); await delay(300); expect(events.some((event) => event.type === 'modified')).toBe(true); @@ -284,7 +284,7 @@ export async function test({ describe, expect, it, ...t }) { it('filters events by requested types', async () => { const file = new File(watcherDirectory, 'filter.txt'); file.create(); - file.write('before'); + file.writeSync('before'); const events: { type: string }[] = []; const subscription = file.watch((event) => events.push(event), { @@ -292,7 +292,7 @@ export async function test({ describe, expect, it, ...t }) { }); try { - file.write('after'); + file.writeSync('after'); await delay(300); expect(events.length).toBe(0); @@ -421,7 +421,7 @@ export async function test({ describe, expect, it, ...t }) { it('Works with spaces as filename', () => { const outputFile = new File(testDirectory, 'my new file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); expect(outputFile.name).toBe('my new file.txt'); }); @@ -487,37 +487,44 @@ export async function test({ describe, expect, it, ...t }) { it('Writes a string to a file reference', () => { const outputFile = new File(testDirectory, 'file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); }); it('overwrites files by default when using write()', () => { const file = new File(testDirectory, 'overwrite-test.txt'); - file.write('First'); + file.writeSync('First'); expect(file.textSync()).toBe('First'); - file.write('Second'); + file.writeSync('Second'); expect(file.textSync()).toBe('Second'); }); it('overwrites a longer file with a shorter string', () => { const file = new File(testDirectory, 'overwrite-shorter.txt'); - file.write('This is a long string'); + file.writeSync('This is a long string'); expect(file.textSync()).toBe('This is a long string'); - file.write('Short'); + file.writeSync('Short'); expect(file.textSync()).toBe('Short'); }); - it('Writes a base64 encoded string to a file reference', () => { + it('Writes a base64 encoded string to a file reference using File.writeSync', () => { const outputFile = new File(testDirectory, 'file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('SGVsbG8gd29ybGQh', { encoding: 'base64' }); + outputFile.writeSync('SGVsbG8gd29ybGQh', { encoding: 'base64' }); expect(outputFile.textSync()).toEqual('Hello world!'); }); - it('Writes a string to a file reference', async () => { + it('Writes a base64 encoded string to a file reference using File.write', async () => { + const outputFile = new File(testDirectory, 'file_async.txt'); + expect(outputFile.exists).toBe(false); + await outputFile.write('SGVsbG8gd29ybGQh', { encoding: 'base64' }); + expect(outputFile.textSync()).toEqual('Hello world!'); + }); + + it('Writes a string to a file reference using File.writeSync', async () => { const outputFile = new File(testDirectory, 'file.txt'); outputFile.create(); - outputFile.write(new Uint8Array([97, 98, 99])); + outputFile.writeSync(new Uint8Array([97, 98, 99])); expect(outputFile.exists).toBe(true); expect(await outputFile.bytes()).toEqual(new Uint8Array([97, 98, 99])); expect(outputFile.bytesSync()).toEqual(new Uint8Array([97, 98, 99])); @@ -525,9 +532,17 @@ export async function test({ describe, expect, it, ...t }) { expect(outputFile.textSync()).toBe('abc'); }); + it('Writes a string to a file reference using File.write', async () => { + const outputFile = new File(testDirectory, 'file_async.txt'); + outputFile.create(); + await outputFile.write('0abcd'); + expect(outputFile.exists).toBe(true); + expect(await outputFile.text()).toBe('0abcd'); + }); + it('Reads a string from a file reference', async () => { const outputFile = new File(testDirectory, 'file2.txt'); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); const content = await outputFile.text(); expect(content).toBe('Hello world'); @@ -564,30 +579,44 @@ export async function test({ describe, expect, it, ...t }) { expect(content).toBe('Hello world'); }); - it('appends a string using File.write', async () => { + it('appends a string using File.writeSync', async () => { const file = new File(testDirectory, 'next-append.txt'); - file.write('Hello'); - file.write(' world', { append: true }); + file.writeSync('Hello'); + file.writeSync(' world', { append: true }); expect(file.textSync()).toBe('Hello world'); }); - it('appends bytes using File.write', async () => { + it('appends a string using File.write', async () => { + const file = new File(testDirectory, 'next-append_async.txt'); + await file.write('Hello'); + await file.write(' world', { append: true }); + expect(file.textSync()).toBe('Hello world'); + }); + + it('appends bytes using File.writeSync', async () => { const file = new File(testDirectory, 'next-append-bytes.txt'); - file.write('Hello'); - file.write(new Uint8Array([32, 119, 111, 114, 108, 100]), { append: true }); // ' world' + file.writeSync('Hello'); + file.writeSync(new Uint8Array([32, 119, 111, 114, 108, 100]), { append: true }); // ' world' + expect(file.textSync()).toBe('Hello world'); + }); + + it('appends bytes using File.write', async () => { + const file = new File(testDirectory, 'next-append-bytes_async.txt'); + await file.write('Hello'); + await file.write(new Uint8Array([32, 119, 111, 114, 108, 100]), { append: true }); // ' world' expect(file.textSync()).toBe('Hello world'); }); it('creates a new file if append is true but file does not exist', async () => { const file = new File(testDirectory, 'new-file-append.txt'); - file.write('Hello', { append: true }); + file.writeSync('Hello', { append: true }); expect(file.textSync()).toBe('Hello'); }); }); it('Deletes a file reference', () => { const outputFile = new File(testDirectory, 'file3.txt'); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); outputFile.delete(); @@ -601,7 +630,7 @@ export async function test({ describe, expect, it, ...t }) { const childDir = new Directory(parentDir, 'child'); childDir.create(); const file = new File(childDir, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); expect(parentDir.exists).toBe(true); parentDir.delete(); expect(parentDir.exists).toBe(false); @@ -694,7 +723,7 @@ export async function test({ describe, expect, it, ...t }) { const file = new File(testDirectory, 'newFolder'); file.create(); expect(file.exists).toBe(true); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.textSync()).toBe('Hello world'); file.create({ overwrite: true }); expect(file.textSync()).toBe(''); @@ -732,7 +761,7 @@ export async function test({ describe, expect, it, ...t }) { it('Copies it to a folder', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); src.copySync(dstFolder); @@ -745,14 +774,14 @@ export async function test({ describe, expect, it, ...t }) { it('Throws an error when copying to a nonexistant folder without options', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); const folder = new Directory(testDirectory, 'destination'); expect(() => file.copySync(folder)).toThrow(); }); it('Copies it to a file', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dst = new File(testDirectory, 'file2.txt'); src.copySync(dst); expect(dst.exists).toBe(true); @@ -770,7 +799,7 @@ export async function test({ describe, expect, it, ...t }) { try { dst.delete(); } catch {} - src.write('Hello world'); + src.writeSync('Hello world'); src.copySync(dst); expect(dst.uri).toBe(FS.documentDirectory + 'file.txt'); expect(dst.exists).toBe(true); @@ -779,17 +808,17 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination file exists and overwrite is not set', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); expect(() => src.copySync(dst)).toThrow(); }); it('overwrites destination file when overwrite is true', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); src.copySync(dst, { overwrite: true }); expect(dst.textSync()).toBe('source'); expect(src.exists).toBe(true); @@ -797,11 +826,11 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites file in destination directory when overwrite is true', () => { const src = new File(testDirectory, 'file.txt'); - src.write('new content'); + src.writeSync('new content'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); const existing = new File(dstFolder, 'file.txt'); - existing.write('old content'); + existing.writeSync('old content'); src.copySync(dstFolder, { overwrite: true }); expect(new File(dstFolder, 'file.txt').textSync()).toBe('new content'); }); @@ -846,11 +875,11 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites destination directory when overwrite is true', () => { const src = new Directory(testDirectory, 'srcDir'); src.create(); - new File(src, 'file.txt').write('from source'); + new File(src, 'file.txt').writeSync('from source'); const dst = new Directory(testDirectory, 'dstDir'); dst.create(); - new File(dst, 'old.txt').write('old content'); + new File(dst, 'old.txt').writeSync('old content'); src.copySync(dst, { overwrite: true }); expect(dst.exists).toBe(true); @@ -867,7 +896,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves it to a folder', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); src.moveSync(dstFolder); @@ -880,14 +909,14 @@ export async function test({ describe, expect, it, ...t }) { it('Throws an error when moving to a nonexistant folder without options', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); const folder = new Directory(testDirectory, 'destination'); expect(() => file.moveSync(folder)).toThrow(); }); it('moves it to a file', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const dst = new File(testDirectory, 'file2.txt'); src.moveSync(dst); expect(dst.exists).toBe(true); @@ -898,17 +927,17 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination file exists and overwrite is not set', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); expect(() => src.moveSync(dst)).toThrow(); }); it('overwrites destination file when overwrite is true', () => { const src = new File(testDirectory, 'src.txt'); - src.write('source'); + src.writeSync('source'); const dst = new File(testDirectory, 'dst.txt'); - dst.write('destination'); + dst.writeSync('destination'); src.moveSync(dst, { overwrite: true }); expect(dst.textSync()).toBe('source'); expect(src.uri).toBe(dst.uri); @@ -916,10 +945,10 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites file in destination directory when overwrite is true', () => { const src = new File(testDirectory, 'file.txt'); - src.write('new content'); + src.writeSync('new content'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); - new File(dstFolder, 'file.txt').write('old content'); + new File(dstFolder, 'file.txt').writeSync('old content'); src.moveSync(dstFolder, { overwrite: true }); expect(new File(dstFolder, 'file.txt').textSync()).toBe('new content'); expect(src.uri).toBe(new File(dstFolder, 'file.txt').uri); @@ -929,7 +958,7 @@ export async function test({ describe, expect, it, ...t }) { describe('When renaming a file', () => { it('renames a file and updates its uri and existence', () => { const originalFile = new File(testDirectory, 'original.txt'); - originalFile.write('Hello world'); + originalFile.writeSync('Hello world'); originalFile.rename('renamed.txt'); expect(originalFile.exists).toBe(true); expect(originalFile.uri).toBe(testDirectory + 'renamed.txt'); @@ -937,7 +966,7 @@ export async function test({ describe, expect, it, ...t }) { it('renames a file and verifies it appears in the parent directory listing', () => { const fileToRename = new File(testDirectory, 'toRename.txt'); - fileToRename.write('Hello world'); + fileToRename.writeSync('Hello world'); fileToRename.rename('renamedFile.txt'); const parentDir = new Directory(testDirectory); @@ -948,7 +977,7 @@ export async function test({ describe, expect, it, ...t }) { it('ensures the old file name no longer exists after renaming', () => { const file = new File(testDirectory, 'oldName.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); file.rename('newName.txt'); expect(new File(testDirectory, 'oldName.txt').exists).toBe(false); expect(new File(testDirectory, 'newName.txt').exists).toBe(true); @@ -956,7 +985,7 @@ export async function test({ describe, expect, it, ...t }) { it('retains file contents after renaming', () => { const file = new File(testDirectory, 'contentFile.txt'); - file.write('Sample content'); + file.writeSync('Sample content'); file.rename('contentFileRenamed.txt'); const renamedFile = new File(testDirectory, 'contentFileRenamed.txt'); expect(renamedFile.textSync()).toBe('Sample content'); @@ -965,8 +994,8 @@ export async function test({ describe, expect, it, ...t }) { it('throws an error when renaming to an existing file name', () => { const file1 = new File(testDirectory, 'fileA.txt'); const file2 = new File(testDirectory, 'fileB.txt'); - file1.write('A'); - file2.write('B'); + file1.writeSync('A'); + file2.writeSync('B'); expect(() => file1.rename('fileB.txt')).toThrow(); }); @@ -977,7 +1006,7 @@ export async function test({ describe, expect, it, ...t }) { it('renames a file and preserves file metadata', () => { const file = new File(testDirectory, 'metadata.txt'); - file.write('Content'); + file.writeSync('Content'); const originalSize = file.size; const originalMd5 = file.md5; file.rename('metadataRenamed.txt'); @@ -987,15 +1016,15 @@ export async function test({ describe, expect, it, ...t }) { it('throws an error when renaming to an empty string', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Content'); + file.writeSync('Content'); expect(() => file.rename('')).toThrow(); }); it('renames a file and updates parent directory listing correctly', () => { const file1 = new File(testDirectory, 'file1.txt'); const file2 = new File(testDirectory, 'file2.txt'); - file1.write('Content 1'); - file2.write('Content 2'); + file1.writeSync('Content 1'); + file2.writeSync('Content 2'); file1.rename('renamedFile1.txt'); @@ -1021,7 +1050,7 @@ export async function test({ describe, expect, it, ...t }) { it('Throws an error when moving to a nonexistant folder without options', () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); const folder = new Directory(testDirectory, 'some/nonexistent/directory/'); expect(() => file.moveSync(folder)).toThrow(); }); @@ -1046,13 +1075,13 @@ export async function test({ describe, expect, it, ...t }) { it('overwrites destination directory when overwrite is true', () => { const src = new Directory(testDirectory, 'srcDir'); src.create(); - new File(src, 'file.txt').write('from source'); + new File(src, 'file.txt').writeSync('from source'); const dstFolder = new Directory(testDirectory, 'destination'); dstFolder.create(); const dst = new Directory(dstFolder, 'srcDir'); dst.create(); - new File(dst, 'old.txt').write('old content'); + new File(dst, 'old.txt').writeSync('old content'); src.moveSync(dstFolder, { overwrite: true }); expect(src.exists).toBe(true); @@ -1109,7 +1138,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Copy operations - SAF file', () => { it('copies SAF file -> SAF directory (creates file inside)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const dstDir = safDirectory.createDirectory('targetDir'); srcFile.copySync(dstDir); @@ -1123,7 +1152,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF file -> local file', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const dstFile = new File(localDirectory, 'dest.txt'); srcFile.copySync(dstFile); @@ -1135,7 +1164,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF file -> local directory (creates file inside)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); srcFile.copySync(localDirectory); @@ -1149,7 +1178,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF directory -> SAF directory (existing)', () => { const srcDir = safDirectory.createDirectory('sourceDir'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const dstDir = safDirectory.createDirectory('destDir'); srcDir.copySync(dstDir); @@ -1166,7 +1195,7 @@ export async function test({ describe, expect, it, ...t }) { it('copies SAF directory -> local directory (existing)', () => { const srcDir = safDirectory.createDirectory('sourceDir2'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); // Destination directory must exist for directory copy srcDir.copySync(localDirectory); @@ -1183,7 +1212,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Copy operations - local to SAF', () => { it('copies local file -> SAF directory (creates file inside)', () => { const srcFile = new File(localDirectory, 'localfile.txt'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const dstDir = safDirectory.createDirectory('localToSafDir'); srcFile.copySync(dstDir); @@ -1198,7 +1227,7 @@ export async function test({ describe, expect, it, ...t }) { const srcDir = new Directory(localDirectory, 'localSourceDir'); srcDir.create(); const srcFile = new File(srcDir, 'nested.txt'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); srcDir.copySync(safDirectory); @@ -1251,7 +1280,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Move operations - SAF file', () => { it('moves SAF file -> SAF directory (creates file inside)', () => { const srcFile = safDirectory.createFile('moveSource.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; const dstDir = safDirectory.createDirectory('moveTargetDir'); @@ -1269,7 +1298,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF file -> local file', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; const dstFile = new File(localDirectory, 'dest.txt'); @@ -1283,7 +1312,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF file -> local directory (creates file inside)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; srcFile.moveSync(localDirectory); @@ -1300,7 +1329,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF directory -> SAF directory (existing)', () => { const srcDir = safDirectory.createDirectory('moveSrcDir'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const originalUri = srcDir.uri; const dstDir = safDirectory.createDirectory('moveDestDir'); @@ -1316,7 +1345,7 @@ export async function test({ describe, expect, it, ...t }) { it('moves SAF directory -> local directory (existing)', () => { const srcDir = safDirectory.createDirectory('moveSrcDir2'); const srcFile = srcDir.createFile('nested.txt', 'text/plain'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const originalUri = srcDir.uri; const localDest = new Directory(localDirectory, 'safMoveTarget'); @@ -1335,7 +1364,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Move operations - local to SAF', () => { it('moves local file -> SAF directory (creates file inside)', () => { const srcFile = new File(localDirectory, 'localMoveFile.txt'); - srcFile.write('test content'); + srcFile.writeSync('test content'); const originalUri = srcFile.uri; const dstDir = safDirectory.createDirectory('localMoveTarget'); @@ -1357,7 +1386,7 @@ export async function test({ describe, expect, it, ...t }) { const srcDir = new Directory(localDirectory, 'localMoveDir'); srcDir.create(); const srcFile = new File(srcDir, 'nested.txt'); - srcFile.write('nested content'); + srcFile.writeSync('nested content'); const originalUri = srcDir.uri; srcDir.moveSync(safDirectory); @@ -1397,7 +1426,7 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination directory does not exist (file copy)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test'); + srcFile.writeSync('test'); const nonExistentDir = new Directory(localDirectory, 'nonexistent'); expect(() => srcFile.copySync(nonExistentDir)).toThrow(); @@ -1405,7 +1434,7 @@ export async function test({ describe, expect, it, ...t }) { it('throws when destination directory does not exist (file move)', () => { const srcFile = safDirectory.createFile('source.txt', 'text/plain'); - srcFile.write('test'); + srcFile.writeSync('test'); const nonExistentDir = new Directory(localDirectory, 'nonexistent'); expect(() => srcFile.moveSync(nonExistentDir)).toThrow(); @@ -1432,7 +1461,7 @@ export async function test({ describe, expect, it, ...t }) { const dir = new Directory(testDirectory, 'contentDir/'); dir.create(); const file = new File(dir, 'file.txt'); - file.write('test'); + file.writeSync('test'); dir.rename('renamedContentDir'); const renamedDir = new Directory(testDirectory, 'renamedContentDir/'); expect(renamedDir.exists).toBe(true); @@ -1470,7 +1499,7 @@ export async function test({ describe, expect, it, ...t }) { const dir = new Directory(testDirectory, 'metadataDir/'); dir.create(); const file = new File(dir, 'test.txt'); - file.write('Test content'); + file.writeSync('Test content'); const originalSize = dir.size; @@ -1567,7 +1596,7 @@ export async function test({ describe, expect, it, ...t }) { const md5 = '2942bfabb3d05332b66eb128e0842cff'; const response = await fetch(url); const src = new File(testDirectory, 'file.pdf'); - src.write(await response.bytes()); + src.writeSync(await response.bytes()); expect(src.md5).toEqual(md5); }); @@ -1740,7 +1769,7 @@ export async function test({ describe, expect, it, ...t }) { const url = 'https://httpbingo.org/bytes/10240'; const file = new File(testDirectory, 'idempotent_progress.bin'); file.create(); - file.write('existing content'); + file.writeSync('existing content'); const progressUpdates: { bytesWritten: number; totalBytes: number }[] = []; @@ -1761,7 +1790,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Computes file properties', () => { it('computes size', async () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.size).toBe(11); }); @@ -1770,7 +1799,7 @@ export async function test({ describe, expect, it, ...t }) { testDirectory, 'creationTime_is_earlier_than_modificationTime_or_equal.txt' ); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.creationTime).not.toBeNull(); expect(file.modificationTime).not.toBeNull(); expect(file.creationTime).toBeLessThanOrEqual(file.modificationTime); @@ -1778,7 +1807,7 @@ export async function test({ describe, expect, it, ...t }) { it('computes md5', async () => { const file = new File(testDirectory, 'file.txt'); - file.write('Hello world'); + file.writeSync('Hello world'); expect(file.md5).toBe('3e25960a79dbc69b674cd4ec67a72c62'); }); @@ -1794,7 +1823,7 @@ export async function test({ describe, expect, it, ...t }) { const dir = new Directory(testDirectory, 'directory'); const file = new File(testDirectory, 'directory', 'file.txt'); file.create({ intermediates: true }); - file.write('Hello world'); + file.writeSync('Hello world'); expect(dir.size).toBe(11); }); }); @@ -1802,7 +1831,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Returns base64', () => { it('gets base64 of a file', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); expect(await src.base64()).toBe('SGVsbG8gd29ybGQ='); expect(src.base64Sync()).toBe('SGVsbG8gd29ybGQ='); }); @@ -1811,7 +1840,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Returns bytes', () => { it('gets file as a Uint8Array', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); expect(src.bytesSync()).toEqual( new Uint8Array([72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]) ); @@ -1911,7 +1940,7 @@ export async function test({ describe, expect, it, ...t }) { const url = `${testDirectory}execute_correctly.txt`; const src = new File(url); src.create(); - src.write('Hello World'); + src.writeSync('Hello World'); const result = src.info({ md5: true }); expect(result.exists).toBe(true); if (result.exists) { @@ -1926,7 +1955,7 @@ export async function test({ describe, expect, it, ...t }) { it('executes correctly when options are undefined', () => { const url = `${testDirectory}executes_correctly_when_options_are_undefined.txt`; const src = new File(url); - src.write('Hello World'); + src.writeSync('Hello World'); const result = src.info(); if (result.exists) { expect(result.md5).toBeNull(); @@ -1935,7 +1964,7 @@ export async function test({ describe, expect, it, ...t }) { it('returns exists false if file does not exist', () => { const url = `${testDirectory}returns_exists_false_if_file_does_not_exist.txt`; const src = new File(url); - src.write('Hello world'); + src.writeSync('Hello world'); src.delete(); const result = src.info(); expect(result.exists).toBe(false); @@ -1960,7 +1989,7 @@ export async function test({ describe, expect, it, ...t }) { const src = new Directory(url); src.create(); const file = new File(`${url}1.txt`); - file.write('Hello world'); + file.writeSync('Hello world'); const result = src.info(); @@ -1997,7 +2026,7 @@ export async function test({ describe, expect, it, ...t }) { describe('Exposes file handles', () => { it('Allows opening files', () => { const src = new File(testDirectory, 'file.txt'); - src.write('Hello world'); + src.writeSync('Hello world'); const handle = src.open(); expect(handle.readBytes(4)).toEqual(new Uint8Array([72, 101, 108, 108])); // Hell expect(handle.readBytes(4)).toEqual(new Uint8Array([111, 32, 119, 111])); // o wo @@ -2009,7 +2038,7 @@ export async function test({ describe, expect, it, ...t }) { it('Resets position on close', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); let handle = src.open(); expect(handle.readBytes(1)).toEqual(new Uint8Array([97])); // a handle.close(); @@ -2020,7 +2049,7 @@ export async function test({ describe, expect, it, ...t }) { it('Throws on reading from closed handle', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); expect(handle.readBytes(1)).toEqual(new Uint8Array([97])); // a handle.close(); @@ -2029,7 +2058,7 @@ export async function test({ describe, expect, it, ...t }) { it('Can open multiple handles to the same file', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); const handle2 = src.open(); expect(handle.readBytes(1)).toEqual(new Uint8Array([97])); // a @@ -2038,7 +2067,7 @@ export async function test({ describe, expect, it, ...t }) { it('Returns null offset on closed handle', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); handle.close(); expect(handle.offset).toBe(null); @@ -2046,7 +2075,7 @@ export async function test({ describe, expect, it, ...t }) { it('Returns null size on closed handle', () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const handle = src.open(); handle.close(); expect(handle.size).toBe(null); @@ -2058,7 +2087,7 @@ export async function test({ describe, expect, it, ...t }) { const handle = src.open(); expect(handle.readBytes(2)).toEqual(new Uint8Array([])); // a handle.close(); - src.write('abcde'); + src.writeSync('abcde'); const handle2 = src.open(); expect(handle2.readBytes(1)).toEqual(new Uint8Array([97])); // a handle2.close(); @@ -2067,7 +2096,7 @@ export async function test({ describe, expect, it, ...t }) { it('Reads a file in chunks', () => { const src = new File(testDirectory, 'abcs.txt'); const alphabet = 'abcdefghijklmnopqrstuvwxyz'; - src.write(alphabet.repeat(1000) + 'ending'); + src.writeSync(alphabet.repeat(1000) + 'ending'); const handle = src.open(); for (let i = 0; i < 250; i++) { const chunk = handle.readBytes(26 * 4); @@ -2105,7 +2134,7 @@ export async function test({ describe, expect, it, ...t }) { describe('It supports different FileMode options', () => { it('opens in ReadOnly mode and reads data', () => { const src = new File(testDirectory, 'mode-read.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.ReadOnly); expect(handle.readBytes(5)).toEqual(new Uint8Array([72, 101, 108, 108, 111])); handle.close(); @@ -2113,7 +2142,7 @@ export async function test({ describe, expect, it, ...t }) { it('throws when writing to a ReadOnly handle', () => { const src = new File(testDirectory, 'mode-read-only.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.ReadOnly); expect(() => handle.writeBytes(new Uint8Array([65]))).toThrow(); handle.close(); @@ -2138,7 +2167,7 @@ export async function test({ describe, expect, it, ...t }) { it('opens in ReadWrite mode and supports both reading and writing', () => { const src = new File(testDirectory, 'mode-rw.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.ReadWrite); expect(handle.readBytes(5)).toEqual(new Uint8Array([72, 101, 108, 108, 111])); handle.offset = 0; @@ -2149,7 +2178,7 @@ export async function test({ describe, expect, it, ...t }) { it('opens in Append mode and appends data', () => { const src = new File(testDirectory, 'mode-append.txt'); - src.write('Hello'); + src.writeSync('Hello'); const handle = src.open(FileMode.Append); handle.writeBytes(new Uint8Array([32, 87, 111, 114, 108, 100])); // ' World' handle.close(); @@ -2158,7 +2187,7 @@ export async function test({ describe, expect, it, ...t }) { it('opens in Truncate mode and wipes existing content', () => { const src = new File(testDirectory, 'mode-truncate.txt'); - src.write('Old content'); + src.writeSync('Old content'); const handle = src.open(FileMode.Truncate); expect(handle.size).toBe(0); handle.writeBytes(new Uint8Array([78, 101, 119])); // New @@ -2170,7 +2199,7 @@ export async function test({ describe, expect, it, ...t }) { it('Provides a ReadableStream', async () => { const src = new File(testDirectory, 'abcs.txt'); const alphabet = 'abcdefghijklmnopqrstuvwxyz'; - src.write(alphabet); + src.writeSync(alphabet); const stream = src.readableStream(); for await (const chunk of stream) { expect(chunk[0]).toBe(alphabet.charCodeAt(0)); @@ -2180,7 +2209,7 @@ export async function test({ describe, expect, it, ...t }) { it('Provides a ReadableStream with byob support', async () => { const src = new File(testDirectory, 'abcs.txt'); const alphabet = 'abcdefghij'.repeat(1000); - src.write(alphabet); + src.writeSync(alphabet); const stream = src.readableStream(); const array1 = new Uint8Array(5000); const array2 = new Uint8Array(5000); @@ -2212,14 +2241,14 @@ export async function test({ describe, expect, it, ...t }) { const src = new File(asset.localUri); expect(src.type).toBe('image/jpeg'); const src2 = new File(testDirectory, 'file.txt'); - src2.write('abcde'); + src2.writeSync('abcde'); expect(src2.type).toBe('text/plain'); }); // You can also use something like container twostoryrobot/simple-file-upload to test if the file is saved correctly it('Supports sending a file using blob', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const response = await fetch('https://httpbingo.org/anything', { method: 'POST', @@ -2232,7 +2261,7 @@ export async function test({ describe, expect, it, ...t }) { // You can also use this docker image: twostoryrobot/simple-file-upload to test e2e blob upload. it('Supports sending a file using blob with formdata', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const formData = new FormData(); @@ -2248,7 +2277,7 @@ export async function test({ describe, expect, it, ...t }) { it('Supports sending a named file blob using blob with formdata', async () => { const src = new File(testDirectory, 'file.txt'); - src.write('abcde'); + src.writeSync('abcde'); const formData = new FormData(); @@ -2292,13 +2321,13 @@ function addAppleAppGroupsTestSuiteAsync({ describe, expect, it, ...t }) { scopedIt('Writes a string to a file reference', () => { const outputFile = new File(sharedContainerTestDir, 'file.txt'); expect(outputFile.exists).toBe(false); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); }); scopedIt('Deletes a file reference', () => { const outputFile = new File(sharedContainerTestDir, 'file3.txt'); - outputFile.write('Hello world'); + outputFile.writeSync('Hello world'); expect(outputFile.exists).toBe(true); outputFile.delete(); diff --git a/docs/pages/build/building-on-ci.mdx b/docs/pages/build/building-on-ci.mdx index 25bc4688a07327..554e66f0b4185f 100644 --- a/docs/pages/build/building-on-ci.mdx +++ b/docs/pages/build/building-on-ci.mdx @@ -96,6 +96,8 @@ Using the information you've gathered, pass it into the build command through en - `EXPO_APPLE_TEAM_ID`: Your Apple Team ID. For example, `77KQ969CHE`. - `EXPO_APPLE_TEAM_TYPE`: Your Apple Team Type. Valid types are `IN_HOUSE`, `COMPANY_OR_ORGANIZATION`, or `INDIVIDUAL`. +If you run [internal distribution](/build/internal-distribution/) builds on CI with ad hoc provisioning, refresh the ad hoc provisioning profile so registered devices added after the last build are included. For `eas build`, pass [`--refresh-ad-hoc-provisioning-profile`](/eas/cli/#eas-build) with `--non-interactive`. For [EAS Workflows](/eas/workflows/get-started), set `refresh_ad_hoc_provisioning_profile: true` in the build job's `params` ([build job parameters](/eas/workflows/pre-packaged-jobs#build)). See [Automation on CI](/build/internal-distribution/#automation-on-ci-optional) for requirements and an example command. + ### Trigger new builds Now that we're authenticated with Expo CLI, we can create the build step. diff --git a/docs/pages/build/internal-distribution.mdx b/docs/pages/build/internal-distribution.mdx index ed9315542d6052..fd149c99e75e7e 100644 --- a/docs/pages/build/internal-distribution.mdx +++ b/docs/pages/build/internal-distribution.mdx @@ -30,10 +30,34 @@ See the tutorial on Internal distribution with EAS Build below for more informat ### Automation on CI (optional) -It's possible to run internal distribution builds non-interactively in CI using the `--non-interactive` flag. However, if you are using ad hoc provisioning on iOS you will not be able to add new devices to your provisioning profile when using this flag. After registering a device through `eas device:create`, you need to run `eas build` interactively and authenticate with Apple in order for EAS to add the device to your provisioning profile. [Learn more about triggering builds from CI](/build/building-on-ci). +You can run internal distribution builds non-interactively in CI with the [`--non-interactive`](/eas/cli/#eas-build) flag. [Learn more about triggering builds from CI](/build/building-on-ci). + +For iOS ad hoc builds, `eas build --non-interactive` reuses a valid provisioning profile without updating its device list. The build can succeed, but the app may not install on registered devices added after the profile was last updated. + +Pass `--refresh-ad-hoc-provisioning-profile` with `--non-interactive` to update the Expo-managed ad hoc provisioning profile on the Apple Developer Portal before the build. + +> **info** `--refresh-ad-hoc-provisioning-profile` requires EAS CLI 19.1.0 or later. + +EAS authenticates with an App Store Connect API key. It reads devices registered on EAS for your Apple team. It registers any missing UDIDs on the portal. Then it refreshes the profile device list. + +When you use this flag, EAS selects all matching devices for the build target's Apple platform: iPhone and iPad for iOS, Mac for macOS. + + + +For [EAS Workflows](/eas/workflows/get-started), set `refresh_ad_hoc_provisioning_profile: true` in the build job's `params` with the same profile requirements. See the [build job parameters](/eas/workflows/pre-packaged-jobs#build). + +The profile must set [`"distribution": "internal"`](/eas/json/#distribution) and use [credentials managed by EAS](/app-signing/app-credentials/). You need at least one device from [`eas device:create`](/eas/cli/#eas-devicecreate) and an App Store Connect API key in CI through [environment variables](/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team) (`EXPO_ASC_API_KEY_PATH`, `EXPO_ASC_KEY_ID`, and `EXPO_ASC_ISSUER_ID`) or a key stored in EAS for submissions on the project. + +Otherwise, after registering a device with [`eas device:create`](/eas/cli/#eas-devicecreate), run [`eas build`](/eas/cli/#eas-build) interactively and sign in with your Apple account so EAS can update the ad hoc provisioning profile. ### Managing devices +With [EAS Workflows](/eas/workflows/get-started), you can pause a workflow until a tester enrolls an iOS device and a team member approves it using the [`apple-device-registration-request`](/eas/workflows/pre-packaged-jobs#apple-device-registration-request) job. Pair it with a `build` job that sets `refresh_ad_hoc_provisioning_profile: true` to include the new device in an internal distribution build. + You can see any devices registered via `eas device:create` by running: + + +This workflow builds your iOS app for internal distribution and refreshes the ad hoc provisioning profile when you push to the main branch. + +```yaml .eas/workflows/build-ios-internal.yml +name: Build iOS internal + +on: + push: + branches: ['main'] + +jobs: + build_ios: + name: Build iOS Internal + type: build + params: + platform: ios + profile: preview + refresh_ad_hoc_provisioning_profile: true +``` + + + ## Deploy Deploy your application using [EAS Hosting](/eas/hosting/introduction). @@ -1599,6 +1624,103 @@ jobs: +## Apple device registration request + +Pause a workflow run until an iOS device enrolls for a specific [Apple Team](https://expo.fyi/apple-team) and a team member approves that enrollment on [expo.dev](https://expo.dev). Use this job to automate registering a device for [internal distribution](/build/internal-distribution/) inside EAS Workflows. + +When the workflow reaches this job, the job and the run enter `action-required` and stay paused until the flow completes: + +1. The workflow run page shows a QR code and a registration link for the device being enrolled. +2. On the iPhone or iPad, the device downloads a provisioning profile and installs it through Settings. Only one profile is ready at a time, and it is removed if not installed within eight minutes. +3. After installation, the unique device identifier (UDID) and metadata are collected. +4. On the workflow run page, a team member approves or rejects the enrollment. Approval marks the job successful and exposes outputs (UDID, model, and more) to downstream jobs. Rejection fails the job and blocks jobs that use `needs`. + +If the UDID is already registered on your account, the job still waits for enrollment and approval before continuing. + +### Syntax + +```yaml +jobs: + register_device: + type: apple-device-registration-request + params: + apple_team_identifier: string # optional +``` + +#### Parameters + +You can pass the following parameters into the `params` list: + +| Parameter | Type | Description | +| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| apple_team_identifier | string | Optional. The Apple Team ID (for example, `ABCDE12345`). If you omit this parameter and your Expo account has exactly one Apple Team, that team is used. You must set this parameter when your account has no Apple Teams or has two or more Apple Teams. When provided, EAS resolves or creates the team from the identifier. | + +#### Outputs + +After a team member approves the enrolled device, you can reference the following outputs in subsequent jobs: + +| Output | Type | Description | +| ---------------- | ------ | ---------------------------------------------------------------- | +| apple_device_id | string | Expo internal device ID. | +| identifier | string | Device UDID. | +| name | string | Device name. May be empty. | +| model | string | Hardware model string (for example, `iPhone11,2`). May be empty. | +| device_class | string | Device class: `iphone`, `ipad`, or `mac`. May be empty. | +| software_version | string | iOS version string. May be empty. | + +If a team member rejects the enrollment, the job fails and does not set outputs. Downstream jobs that use `needs` will not run. + +### Examples + +Here are some practical examples of using the Apple device registration request job: + + + +This workflow registers an iOS device and sends a Slack message with the device UDID and model from the job outputs. + +```yaml .eas/workflows/register-device-slack.yml +name: Register iOS device and notify Slack + +jobs: + register_device: + type: apple-device-registration-request + params: + apple_team_identifier: XX33YYZ44Z + notify_slack: + needs: [register_device] + type: slack + params: + webhook_url: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX + message: 'Registered device ${{ needs.register_device.outputs.identifier }} (${{ needs.register_device.outputs.model }})' +``` + + + + + +This workflow registers a device and then runs an iOS internal distribution build that refreshes the ad hoc provisioning profile so the new device is included. + +```yaml .eas/workflows/register-device-build.yml +name: Register device and build iOS internal + +jobs: + register_device: + type: apple-device-registration-request + params: + apple_team_identifier: ABCDE12345 + build_ios: + needs: [register_device] + type: build + params: + platform: ios + profile: preview + refresh_ad_hoc_provisioning_profile: true +``` + +The `preview` profile must set [`distribution: internal`](/eas/json/#distribution) and use [credentials managed by EAS](/app-signing/app-credentials/). For CI, you also need an App Store Connect API key. See [internal distribution on CI](/build/internal-distribution/#automation-on-ci-optional) and [trigger builds from CI](/build/building-on-ci/). + + + ## Require Approval Require approval from a user before continuing with the workflow. A user can approve or reject which translates to success or failure of the job. diff --git a/docs/pages/eas/workflows/syntax.mdx b/docs/pages/eas/workflows/syntax.mdx index b75fbb69c76dcf..d90e08ab96bed9 100644 --- a/docs/pages/eas/workflows/syntax.mdx +++ b/docs/pages/eas/workflows/syntax.mdx @@ -428,7 +428,7 @@ jobs: ### `jobs..env` -Sets environment variables for the job. The property is available on all jobs running a VM (all jobs except for the pre-packaged `require-approval`, `doc`, `get-build` and `slack` jobs). +Sets environment variables for the job. The property is available on all jobs running a VM (all jobs except for the pre-packaged `apple-device-registration-request`, `require-approval`, `doc`, `get-build`, and `slack` jobs). ```yaml jobs: @@ -1038,6 +1038,7 @@ jobs: platform: ios | android # required profile: string # optional, default: production message: string # optional + refresh_ad_hoc_provisioning_profile: boolean # optional ``` This job outputs the following properties: @@ -1357,6 +1358,20 @@ This job outputs the following properties: } ``` +#### `apple-device-registration-request` + +Pauses the workflow until an iOS device enrolls for an Apple Team and a team member approves the enrollment. See [Apple device registration request job documentation](/eas/workflows/pre-packaged-jobs#apple-device-registration-request) for detailed information and examples. + +```yaml +jobs: + register_device: + # @info # + type: apple-device-registration-request + params: + apple_team_identifier: string # optional + # @end # +``` + #### `require-approval` Requires approval from a user before continuing with the workflow. A user can approve or reject which translates to success or failure of the job. See [Require Approval job documentation](/eas/workflows/pre-packaged-jobs#require-approval) for detailed information and examples. diff --git a/docs/pages/versions/unversioned/sdk/crypto.mdx b/docs/pages/versions/unversioned/sdk/crypto.mdx index 9ca6d0f3f6bafa..2ddfbc8e0f3565 100644 --- a/docs/pages/versions/unversioned/sdk/crypto.mdx +++ b/docs/pages/versions/unversioned/sdk/crypto.mdx @@ -125,7 +125,7 @@ async function encryptAndSaveData(plaintextData: Uint8Array) { // Save encrypted file const file = new File(Paths.cache, 'encrypted.dat'); file.create({ overwrite: true }); - file.write(encryptedBytes); + await file.write(encryptedBytes); } ``` diff --git a/docs/pages/versions/unversioned/sdk/filesystem.mdx b/docs/pages/versions/unversioned/sdk/filesystem.mdx index 2327fbc68eccfd..eaa5a52bcba55f 100644 --- a/docs/pages/versions/unversioned/sdk/filesystem.mdx +++ b/docs/pages/versions/unversioned/sdk/filesystem.mdx @@ -107,7 +107,7 @@ import { File, Paths } from 'expo-file-system'; try { const file = new File(Paths.cache, 'example.txt'); file.create(); // can throw an error if the file already exists or no permission to create it - file.write('Hello, world!'); + await file.write('Hello, world!'); // or `file.writeSync('Hello, world!');` for synchronous call console.log(file.textSync()); // Hello, world! } catch (error) { console.error(error); @@ -179,7 +179,7 @@ import { File, Paths } from 'expo-file-system'; const url = 'https://pdfobject.com/pdf/sample.pdf'; const response = await fetch(url); const src = new File(Paths.cache, 'file.pdf'); -src.write(await response.bytes()); +await src.write(await response.bytes()); ``` @@ -193,7 +193,7 @@ import { fetch } from 'expo/fetch'; import { File, Paths } from 'expo-file-system'; const file = new File(Paths.cache, 'file.txt'); -file.write('Hello, world!'); +await file.write('Hello, world!'); const response = await fetch('https://example.com', { method: 'POST', @@ -208,7 +208,7 @@ import { fetch } from 'expo/fetch'; import { File, Paths } from 'expo-file-system'; const file = new File(Paths.cache, 'file.txt'); -file.write('Hello, world!'); +await file.write('Hello, world!'); const formData = new FormData(); formData.append('data', file); const response = await fetch('https://example.com', { diff --git a/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/navigationbar.mdx b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/navigationbar.mdx new file mode 100644 index 00000000000000..391b6eec0ce33e --- /dev/null +++ b/docs/pages/versions/unversioned/sdk/ui/jetpack-compose/navigationbar.mdx @@ -0,0 +1,91 @@ +--- +title: NavigationBar +description: A Jetpack Compose NavigationBar component for Material 3 bottom navigation. +sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui' +packageName: '@expo/ui' +platforms: ['android', 'expo-go'] +--- + +import APISection from '~/components/plugins/APISection'; +import { APIInstallSection } from '~/components/plugins/InstallSection'; +import { ContentSpotlight } from '~/ui/components/ContentSpotlight'; + +Expo UI NavigationBar matches the official Jetpack Compose [`NavigationBar`](https://developer.android.com/develop/ui/compose/components/navigation-bar) API. It displays a row of destinations for switching between top-level app sections. + + + +## Installation + + + +## Usage + +### Basic navigation bar + +Manage the selected item in React state and pass `selected` to each `NavigationBarItem`. + +```tsx BasicNavigationBar.tsx +import { useState } from 'react'; +import { Host, Icon, NavigationBar, NavigationBarItem, Text } from '@expo/ui/jetpack-compose'; + +const HOME_ICON = require('./assets/home.xml'); +const SEARCH_ICON = require('./assets/search.xml'); +const SETTINGS_ICON = require('./assets/settings.xml'); + +export default function BasicNavigationBar() { + const [selectedTab, setSelectedTab] = useState('home'); + + return ( + + + setSelectedTab('home')}> + + + + + Home + + + + setSelectedTab('search')}> + + + + + Search + + + + setSelectedTab('settings')}> + + + + + Settings + + + + + ); +} +``` + +## API + +```tsx +import { NavigationBar, NavigationBarItem } from '@expo/ui/jetpack-compose'; +``` + + + + diff --git a/docs/pages/versions/v55.0.0/sdk/picker.mdx b/docs/pages/versions/v55.0.0/sdk/picker.mdx index 03ac6f04dfb8a8..2af96fb7c468e6 100644 --- a/docs/pages/versions/v55.0.0/sdk/picker.mdx +++ b/docs/pages/versions/v55.0.0/sdk/picker.mdx @@ -12,8 +12,6 @@ import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon'; import { APIInstallSection } from '~/components/plugins/InstallSection'; import { BoxLink } from '~/ui/components/BoxLink'; -> **important** [`@expo/ui` provides a drop-in replacement](./ui/drop-in-replacements/picker) for `@react-native-picker/picker`, powered by Jetpack Compose on Android and SwiftUI on iOS. - A component that provides access to the system UI for picking between several options. ## Installation diff --git a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx b/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx index 39c64a125ecc9b..e49ac046b607a8 100644 --- a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx +++ b/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/index.mdx @@ -10,5 +10,4 @@ platforms: ['android', 'ios'] The following components provide API-compatible replacements for popular React Native community libraries, powered by `@expo/ui` native components (Jetpack Compose on Android and SwiftUI on iOS). - **[DateTimePicker](datetimepicker)**: Compatible with `@react-native-community/datetimepicker` -- **[Picker](picker)**: Compatible with `@react-native-picker/picker` - **[SegmentedControl](segmentedcontrol)**: Compatible with `@react-native-segmented-control/segmented-control` diff --git a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/picker.mdx b/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/picker.mdx deleted file mode 100644 index c97051763e8778..00000000000000 --- a/docs/pages/versions/v55.0.0/sdk/ui/drop-in-replacements/picker.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Picker -description: A picker compatible with @react-native-picker/picker. -sourceCodeUrl: 'https://github.com/expo/expo/tree/main/packages/expo-ui' -packageName: '@expo/ui' -platforms: ['android', 'ios', 'web'] ---- - -import APISection from '~/components/plugins/APISection'; -import { APIInstallSection } from '~/components/plugins/InstallSection'; - -A `Picker` component with an API compatible with `@react-native-picker/picker`. It uses a SwiftUI wheel `Picker` on iOS, a Material 3 `ExposedDropdownMenuBox` on Android, and a native ``"},{"kind":"text","text":" element."}]},"typeParameters":[{"name":"T","variant":"typeParam","kind":131072,"type":{"type":"reference","name":"PickerItemValue","package":"@expo/ui"}}],"parameters":[{"name":"props","variant":"param","kind":32768,"type":{"type":"reference","typeArguments":[{"type":"reference","name":"T","package":"@expo/ui","refersToTypeParameter":true}],"name":"PickerProps","package":"@expo/ui"}}],"type":{"type":"reference","target":{"packageName":"@types/react","packagePath":"jsx-runtime.d.ts","qualifiedName":"JSX.Element"},"name":"Element","package":"@types/react","qualifiedName":"JSX.Element"}}]},{"name":"default","variant":"reference","kind":4194304}],"packageName":"@expo/ui"} \ No newline at end of file diff --git a/docs/public/static/images/expo-ui/navigationbar/android-dark.webp b/docs/public/static/images/expo-ui/navigationbar/android-dark.webp new file mode 100644 index 00000000000000..ffd0951865fa8a Binary files /dev/null and b/docs/public/static/images/expo-ui/navigationbar/android-dark.webp differ diff --git a/docs/public/static/images/expo-ui/navigationbar/android-light.webp b/docs/public/static/images/expo-ui/navigationbar/android-light.webp new file mode 100644 index 00000000000000..70da7f9fbe58ae Binary files /dev/null and b/docs/public/static/images/expo-ui/navigationbar/android-light.webp differ diff --git a/packages/expo-dev-launcher/CHANGELOG.md b/packages/expo-dev-launcher/CHANGELOG.md index a1df921e1e2f37..8e4b297168df26 100644 --- a/packages/expo-dev-launcher/CHANGELOG.md +++ b/packages/expo-dev-launcher/CHANGELOG.md @@ -9,6 +9,7 @@ ### 🐛 Bug fixes - [iOS] Cleared the deep-link URL from cached `launchOptions` after it is consumed ([#46265](https://github.com/expo/expo/pull/46265) by [@gabrieldonadel](https://github.com/gabrieldonadel)) +- [Android] Fixed a crash when cold-launching a development build from a deep link that carries intent categories (e.g. an App Link opened from a browser). ([#46314](https://github.com/expo/expo/pull/46314) by [@lilianchiassai-fc](https://github.com/lilianchiassai-fc) & [#46328](https://github.com/expo/expo/pull/46328) by [@lukmccall](https://github.com/lukmccall)) ### 💡 Others diff --git a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt index 53f2e2b2a48126..43ed3f2e51fe1e 100644 --- a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt +++ b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt @@ -413,30 +413,38 @@ class DevLauncherController private constructor( Intent(context, DevLauncherActivity::class.java) .apply { addFlags(NEW_ACTIVITY_FLAGS) } - private fun createAppIntent() = - createBasicAppIntent().apply { - pendingIntentRegistry - .consumePendingIntent() - ?.let { intent -> - action = intent.action - data = intent.data - intent.extras?.let { - putExtras(it) - } - intent.categories?.let { - categories.addAll(it) - } - } ?: run { - // If no pending intent is available, use the extras from the intent that was used to launch the app. - pendingIntentExtras?.let { - putExtras(it) - } + private fun createAppIntent(): Intent { + val newIntent = createBasicAppIntent() + val pendingIntent = pendingIntentRegistry + .consumePendingIntent() + + if (pendingIntent != null) { + newIntent.action = pendingIntent.action + newIntent.data = pendingIntent.data + + val pendingExtras = pendingIntent.extras + if (pendingExtras != null) { + newIntent.putExtras(pendingExtras) } - // Clear the pending intent extras after using them. + val pendingCategories = pendingIntent.categories + if (pendingCategories != null) { + pendingCategories.forEach { pendingCategory -> + newIntent.addCategory(pendingCategory) + } + } + } else { + // If no pending intent is available, use the extras from the intent that was used to launch the app. + val extras = pendingIntentExtras + if (extras != null) { + newIntent.putExtras(extras) + } pendingIntentExtras = null } + return newIntent + } + private fun createBasicAppIntent() = if (sLauncherClass == null) { checkNotNull( diff --git a/packages/expo-file-system/CHANGELOG.md b/packages/expo-file-system/CHANGELOG.md index 5b7d5c2be32484..7085ca9339b7b5 100644 --- a/packages/expo-file-system/CHANGELOG.md +++ b/packages/expo-file-system/CHANGELOG.md @@ -4,6 +4,8 @@ ### 🛠 Breaking changes +- `File.write()` is now asynchronous and returns a Promise. Use `File.writeSync()` for synchronous behavior. ([#45992](https://github.com/expo/expo/pull/45992) by [@wh201906](https://github.com/wh201906)) + ### 🎉 New features ### 🐛 Bug fixes diff --git a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt index 5ad4088a817c8a..8e73636f4475b8 100644 --- a/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt +++ b/packages/expo-file-system/android/src/main/java/expo/modules/filesystem/FileSystemModule.kt @@ -29,6 +29,27 @@ class FileSystemModule : Module() { private val downloadStore = DownloadTaskStore() + private fun writeToFile( + file: FileSystemFile, + content: Either, + options: WriteOptions? + ) { + val append = options?.append ?: false + if (content.`is`(String::class)) { + content.get(String::class).let { + if (options?.encoding == EncodingType.BASE64) { + file.write(Base64.decode(it, Base64.DEFAULT), append) + } else { + file.write(it, append) + } + } + } else if (content.`is`(TypedArray::class)) { + content.get(TypedArray::class).let { + file.write(it, append) + } + } + } + @RequiresApi(Build.VERSION_CODES.O) override fun definition() = ModuleDefinition { Name("FileSystem") @@ -138,22 +159,12 @@ class FileSystemModule : Module() { file.create(options ?: CreateOptions()) } - Function("write") { file: FileSystemFile, content: Either, options: WriteOptions? -> - val append = options?.append ?: false - if (content.`is`(String::class)) { - content.get(String::class).let { - if (options?.encoding == EncodingType.BASE64) { - file.write(Base64.decode(it, Base64.DEFAULT), append) - } else { - file.write(it, append) - } - } - } - if (content.`is`(TypedArray::class)) { - content.get(TypedArray::class).let { - file.write(it, append) - } - } + AsyncFunction("write") Coroutine { file: FileSystemFile, content: Either, options: WriteOptions? -> + writeToFile(file, content, options) + } + + Function("writeSync") { file: FileSystemFile, content: Either, options: WriteOptions? -> + writeToFile(file, content, options) } AsyncFunction("text") { file: FileSystemFile -> diff --git a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts index d3c5e0a093b677..930b1841721d0c 100644 --- a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts +++ b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts @@ -152,7 +152,12 @@ export declare class NativeFileSystemFile { * Writes content to the file. * @param content The content to write into the file. */ - write(content: string | Uint8Array, options?: FileWriteOptions): void; + write(content: string | Uint8Array, options?: FileWriteOptions): Promise; + /** + * Writes content to the file. + * @param content The content to write into the file. + */ + writeSync(content: string | Uint8Array, options?: FileWriteOptions): void; /** * Deletes a file. * diff --git a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map index e630122e944ec5..bb23bffd09b80f 100644 --- a/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map +++ b/packages/expo-file-system/build/internal/NativeFileSystem.types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"NativeFileSystem.types.d.ts","sourceRoot":"","sources":["../../src/internal/NativeFileSystem.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"NativeFileSystem.types.d.ts","sourceRoot":"","sources":["../../src/internal/NativeFileSystem.types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/expo-file-system/build/legacyWarnings.d.ts b/packages/expo-file-system/build/legacyWarnings.d.ts index 647c8d3c0c23ed..c6d045cebdc205 100644 --- a/packages/expo-file-system/build/legacyWarnings.d.ts +++ b/packages/expo-file-system/build/legacyWarnings.d.ts @@ -12,7 +12,7 @@ export declare function readAsStringAsync(fileUri: string, options?: ReadingOpti */ export declare function getContentUriAsync(fileUri: string): Promise; /** - * @deprecated Use `new File().write()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. + * @deprecated Use `await new File().write()` or `new File().writeSync()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. */ export declare function writeAsStringAsync(fileUri: string, contents: string, options?: WritingOptions): Promise; /** diff --git a/packages/expo-file-system/ios/FileSystemModule.swift b/packages/expo-file-system/ios/FileSystemModule.swift index 7af943c7823e2d..7d44c0a38493a4 100644 --- a/packages/expo-file-system/ios/FileSystemModule.swift +++ b/packages/expo-file-system/ios/FileSystemModule.swift @@ -34,6 +34,26 @@ public final class FileSystemModule: Module { return attributes[.systemFreeSize] as? Int64 } + private func writeToFile( + _ file: FileSystemFile, + content: Either, + options: WriteOptions? + ) throws { + let append = options?.append ?? false + if let content: String = content.get() { + if options?.encoding == WriteEncoding.base64 { + guard let data = Data(base64Encoded: content, options: .ignoreUnknownCharacters) else { + throw UnableToWriteBase64DataException(file.url.absoluteString) + } + try file.write(data, append: append) + } else { + try file.write(content, append: append) + } + } else if let content: TypedArray = content.get() { + try file.write(content, append: append) + } + } + public func definition() -> ModuleDefinition { Name("FileSystem") @@ -174,21 +194,12 @@ public final class FileSystemModule: Module { return try file.info(options: options ?? InfoOptions()) } - Function("write") { (file: FileSystemFile, content: Either, options: WriteOptions?) in - let append = options?.append ?? false - if let content: String = content.get() { - if options?.encoding == WriteEncoding.base64 { - guard let data = Data(base64Encoded: content, options: .ignoreUnknownCharacters) else { - throw UnableToWriteBase64DataException(file.url.absoluteString) - } - try file.write(data, append: append) - } else { - try file.write(content, append: append) - } - } - if let content: TypedArray = content.get() { - try file.write(content, append: append) - } + AsyncFunction("write") { (file: FileSystemFile, content: Either, options: WriteOptions?) in + try writeToFile(file, content: content, options: options) + } + + Function("writeSync") { (file: FileSystemFile, content: Either, options: WriteOptions?) in + try writeToFile(file, content: content, options: options) } Property("size") { file in diff --git a/packages/expo-file-system/mocks/FileSystem.ts b/packages/expo-file-system/mocks/FileSystem.ts index 53bb7ad621a12b..30f6cbde9aacec 100644 --- a/packages/expo-file-system/mocks/FileSystem.ts +++ b/packages/expo-file-system/mocks/FileSystem.ts @@ -246,7 +246,7 @@ export class FileSystemFile { }); } - write( + writeSync( content: string | Uint8Array, options: { append?: boolean; encoding?: 'utf8' | 'base64' } = {} ): void { @@ -280,6 +280,13 @@ export class FileSystemFile { } } + async write( + content: string | Uint8Array, + options: { append?: boolean; encoding?: 'utf8' | 'base64' } = {} + ): Promise { + this.writeSync(content, options); + } + private readBytesOrThrow(): Uint8Array { const entry = store.get(normalizeKey(this.uri)); if (!entry || entry.kind !== 'file' || !entry.exists) { diff --git a/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts b/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts index d51f7239ed6632..7b65243e446e40 100644 --- a/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts +++ b/packages/expo-file-system/src/__tests__/FileSystem-test.native.ts @@ -62,6 +62,7 @@ describe('expo-file-system new API', () => { expect(typeof file.moveSync).toBe('function'); expect(typeof file.text).toBe('function'); expect(typeof file.write).toBe('function'); + expect(typeof file.writeSync).toBe('function'); expect(typeof file.json).toBe('function'); expect(typeof file.formData).toBe('function'); }); @@ -122,6 +123,15 @@ describe('expo-file-system new API', () => { await expect(file.move(destination, { overwrite: true })).resolves.toBeUndefined(); }); + it('File.write returns a promise and File.writeSync stays synchronous', async () => { + const file = new File(Paths.cache, 'test.txt'); + + expect(file.write('hello')).toBeInstanceOf(Promise); + const result = await file.write('hello'); + expect(result).toBeUndefined(); + expect(file.writeSync('hello')).toBeUndefined(); + }); + it('Directory copy and move return promises', async () => { const dir = new Directory(Paths.document, 'subdir'); dir.create(); @@ -193,8 +203,8 @@ describe('expo-file-system behavioral mock', () => { const dir = new Directory(Paths.cache, 'info-dir'); dir.create(); const creationTime = dir.info().creationTime; - dir.createFile('a.txt', null).write('abc'); - dir.createFile('b.txt', null).write('de'); + dir.createFile('a.txt', null).writeSync('abc'); + dir.createFile('b.txt', null).writeSync('de'); expect(dir.info()).toMatchObject({ exists: true, @@ -204,32 +214,53 @@ describe('expo-file-system behavioral mock', () => { }); }); - it('File.write(string) and File.text() roundtrip utf-8', async () => { + it('File.writeSync(string) and File.text() roundtrip utf-8', async () => { const file = new File(Paths.cache, 'hello.txt'); - file.write('hello world'); + file.writeSync('hello world'); + expect(file.textSync()).toBe('hello world'); + await expect(file.text()).resolves.toBe('hello world'); + }); + + it('File.write(string) and File.text() roundtrip utf-8', async () => { + const file = new File(Paths.cache, 'hello_async.txt'); + await file.write('hello world'); expect(file.textSync()).toBe('hello world'); await expect(file.text()).resolves.toBe('hello world'); }); - it('File.write(Uint8Array) and File.bytes() roundtrip byte-for-byte', async () => { + it('File.writeSync(Uint8Array) and File.bytes() roundtrip byte-for-byte', async () => { const file = new File(Paths.cache, 'bin.dat'); const payload = new Uint8Array([1, 2, 3, 4, 5]); - file.write(payload); + file.writeSync(payload); expect(Array.from(file.bytesSync())).toEqual([1, 2, 3, 4, 5]); await expect(file.bytes()).resolves.toEqual(payload); }); - it('File.write with append option appends to existing bytes', () => { + it('File.writeSync with append option appends to existing bytes', () => { const file = new File(Paths.cache, 'log.txt'); - file.write('a'); - file.write('b', { append: true }); - file.write('c', { append: true }); + file.writeSync('a'); + file.writeSync('b', { append: true }); + file.writeSync('c', { append: true }); + expect(file.textSync()).toBe('abc'); + }); + + it('File.write with append option appends to existing bytes', async () => { + const file = new File(Paths.cache, 'log_async.txt'); + await file.write('a'); + await file.write('b', { append: true }); + await file.write('c', { append: true }); expect(file.textSync()).toBe('abc'); }); - it('File.write with base64 encoding decodes before storing', () => { + it('File.writeSync with base64 encoding decodes before storing', () => { const file = new File(Paths.cache, 'encoded.txt'); - file.write(Buffer.from('hello').toString('base64'), { encoding: 'base64' }); + file.writeSync(Buffer.from('hello').toString('base64'), { encoding: 'base64' }); + expect(file.textSync()).toBe('hello'); + }); + + it('File.write with base64 encoding decodes before storing', async () => { + const file = new File(Paths.cache, 'encoded_async.txt'); + await file.write(Buffer.from('hello').toString('base64'), { encoding: 'base64' }); expect(file.textSync()).toBe('hello'); }); @@ -247,7 +278,7 @@ describe('expo-file-system behavioral mock', () => { expect(creationTime).toBeGreaterThan(0); expect(file.modificationTime).toBe(creationTime); - file.write('hello'); + file.writeSync('hello'); expect(file.size).toBe(5); expect(file.creationTime).toBe(creationTime); expect(file.modificationTime).toBeGreaterThan(creationTime!); @@ -266,7 +297,7 @@ describe('expo-file-system behavioral mock', () => { it('File.move updates this.uri and removes the source', async () => { const source = new File(Paths.cache, 'source.txt'); - source.write('payload'); + source.writeSync('payload'); const originalUri = source.uri; const destDir = new Directory(Paths.cache, 'moved'); @@ -285,7 +316,7 @@ describe('expo-file-system behavioral mock', () => { it('File.copy leaves the source intact and copies contents', async () => { const source = new File(Paths.cache, 'copy-src.txt'); - source.write('original'); + source.writeSync('original'); const dest = new File(Paths.cache, 'copy-dest.txt'); await source.copy(dest); @@ -295,7 +326,7 @@ describe('expo-file-system behavioral mock', () => { expect(dest.textSync()).toBe('original'); // Writing to the copy must not mutate the source. - dest.write('mutated'); + dest.writeSync('mutated'); expect(source.textSync()).toBe('original'); expect(dest.textSync()).toBe('mutated'); }); @@ -303,9 +334,9 @@ describe('expo-file-system behavioral mock', () => { it('Directory.delete removes the directory and all descendants', () => { const dir = new Directory(Paths.cache, 'doomed'); dir.create(); - dir.createFile('a.txt', null).write('a'); + dir.createFile('a.txt', null).writeSync('a'); const inner = dir.createDirectory('inner'); - inner.createFile('b.txt', null).write('b'); + inner.createFile('b.txt', null).writeSync('b'); dir.delete(); @@ -361,7 +392,7 @@ describe('expo-file-system behavioral mock', () => { function makeFile(name: string, contents = 'hello'): File { const file = new File(Paths.cache, name); file.create(); - file.write(contents); + file.writeSync(contents); return file; } @@ -401,7 +432,7 @@ describe('expo-file-system behavioral mock', () => { const file = dir.createFile('x.txt', 'text/plain'); expect(file.type).toBe('text/plain'); - file.write('hello'); + file.writeSync('hello'); expect(file.type).toBe('text/plain'); const handle = file.open(FileMode.Truncate); diff --git a/packages/expo-file-system/src/internal/NativeFileSystem.types.ts b/packages/expo-file-system/src/internal/NativeFileSystem.types.ts index 5f6aaf6aa744ba..01f8a6ad20aed6 100644 --- a/packages/expo-file-system/src/internal/NativeFileSystem.types.ts +++ b/packages/expo-file-system/src/internal/NativeFileSystem.types.ts @@ -205,7 +205,13 @@ export declare class NativeFileSystemFile { * Writes content to the file. * @param content The content to write into the file. */ - write(content: string | Uint8Array, options?: FileWriteOptions): void; + write(content: string | Uint8Array, options?: FileWriteOptions): Promise; + + /** + * Writes content to the file. + * @param content The content to write into the file. + */ + writeSync(content: string | Uint8Array, options?: FileWriteOptions): void; /** * Deletes a file. diff --git a/packages/expo-file-system/src/legacyWarnings.ts b/packages/expo-file-system/src/legacyWarnings.ts index 0c2be1d956920e..89d5f0eb0bf795 100644 --- a/packages/expo-file-system/src/legacyWarnings.ts +++ b/packages/expo-file-system/src/legacyWarnings.ts @@ -46,7 +46,7 @@ export async function getContentUriAsync(fileUri: string): Promise { } /** - * @deprecated Use `new File().write()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. + * @deprecated Use `await new File().write()` or `new File().writeSync()` or import this method from `expo-file-system/legacy`. This method will throw in runtime. */ export async function writeAsStringAsync( fileUri: string, diff --git a/packages/expo-ui/CHANGELOG.md b/packages/expo-ui/CHANGELOG.md index 58f964dd16c32b..e27eb0f0c1ce5f 100644 --- a/packages/expo-ui/CHANGELOG.md +++ b/packages/expo-ui/CHANGELOG.md @@ -6,12 +6,14 @@ ### 🎉 New features +- [jetpack-compose] Added `NavigationBar` and `NavigationBarItem` components. - [iOS] Added support for custom SF Symbols in the SwiftUI `Image` component. ([#46183](https://github.com/expo/expo/pull/46183) by [@cinques](https://github.com/cinques)) - [swift-ui] Added `` for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo)) - [universal] Added `` for custom label style. ([#46288](https://github.com/expo/expo/pull/46288) by [@kudo](https://github.com/kudo)) ### 🐛 Bug fixes + ### 💡 Others - [universal] Revamp web universal components (`Button`, `Checkbox`, `Picker`, `Slider`,`Switch`,`TextInput`,) with shared design tokens, light / dark themes, and keyboard focus styles. ([#46258](https://github.com/expo/expo/pull/46258) by [@zoontek](https://github.com/zoontek)) diff --git a/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt b/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt index 4d8a2953f2fcaf..573dc4fe9fa92c 100644 --- a/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +++ b/packages/expo-ui/android/src/main/java/expo/modules/ui/ExpoUIModule.kt @@ -610,6 +610,20 @@ class ExpoUIModule : Module() { } } + ExpoUIView("NavigationBarView") { + Content { props -> + NavigationBarContent(props) + } + } + + ExpoUIView("NavigationBarItemView") { + val onButtonPressed by Event() + + Content { props -> + NavigationBarItemContent(props) { onButtonPressed(Unit) } + } + } + ExpoUIView("SpacerView") { Content { props -> SpacerContent(props) diff --git a/packages/expo-ui/android/src/main/java/expo/modules/ui/NavigationBarView.kt b/packages/expo-ui/android/src/main/java/expo/modules/ui/NavigationBarView.kt new file mode 100644 index 00000000000000..83101fb1411db4 --- /dev/null +++ b/packages/expo-ui/android/src/main/java/expo/modules/ui/NavigationBarView.kt @@ -0,0 +1,95 @@ +package expo.modules.ui + +import android.graphics.Color +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color as ComposeColor +import androidx.compose.ui.unit.dp +import expo.modules.kotlin.records.Field +import expo.modules.kotlin.records.Record +import expo.modules.kotlin.types.OptimizedRecord +import expo.modules.kotlin.views.ComposeProps +import expo.modules.kotlin.views.FunctionalComposableScope +import expo.modules.kotlin.views.OptimizedComposeProps + +@OptimizedRecord +data class NavigationBarItemColors( + @Field val selectedIconColor: Color? = null, + @Field val selectedTextColor: Color? = null, + @Field val selectedIndicatorColor: Color? = null, + @Field val unselectedIconColor: Color? = null, + @Field val unselectedTextColor: Color? = null, + @Field val disabledIconColor: Color? = null, + @Field val disabledTextColor: Color? = null +) : Record + +@OptimizedComposeProps +data class NavigationBarProps( + val containerColor: Color? = null, + val contentColor: Color? = null, + val tonalElevation: Float? = null, + val modifiers: ModifierList = emptyList() +) : ComposeProps + +@OptimizedComposeProps +data class NavigationBarItemProps( + val selected: Boolean = false, + val enabled: Boolean = true, + val alwaysShowLabel: Boolean = true, + val colors: NavigationBarItemColors = NavigationBarItemColors(), + val modifiers: ModifierList = emptyList() +) : ComposeProps + +@Composable +fun FunctionalComposableScope.NavigationBarContent(props: NavigationBarProps) { + val resolvedContainerColor = props.containerColor.composeOrNull ?: NavigationBarDefaults.containerColor + val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher) + + NavigationBar( + modifier = modifier, + containerColor = resolvedContainerColor, + contentColor = props.contentColor.composeOrNull ?: contentColorFor(resolvedContainerColor), + tonalElevation = props.tonalElevation?.dp ?: NavigationBarDefaults.Elevation + ) { + Children(UIComposableScope(rowScope = this@NavigationBar), filter = { !isSlotView(it) }) + } +} + +@Composable +fun FunctionalComposableScope.NavigationBarItemContent( + props: NavigationBarItemProps, + onClick: () -> Unit +) { + val iconSlotView = findChildSlotView(view, "icon") + val labelSlotView = findChildSlotView(view, "label") + val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher) + val label: (@Composable () -> Unit)? = labelSlotView?.let { slot -> { slot.renderSlot() } } + val rowScope = composableScope.rowScope ?: return + + with(rowScope) { + NavigationBarItem( + selected = props.selected, + onClick = onClick, + icon = { + iconSlotView?.renderSlot() + }, + modifier = modifier, + enabled = props.enabled, + label = label, + alwaysShowLabel = props.alwaysShowLabel, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = props.colors.selectedIconColor.composeOrNull ?: ComposeColor.Unspecified, + selectedTextColor = props.colors.selectedTextColor.composeOrNull ?: ComposeColor.Unspecified, + indicatorColor = props.colors.selectedIndicatorColor.composeOrNull ?: ComposeColor.Unspecified, + unselectedIconColor = props.colors.unselectedIconColor.composeOrNull ?: ComposeColor.Unspecified, + unselectedTextColor = props.colors.unselectedTextColor.composeOrNull ?: ComposeColor.Unspecified, + disabledIconColor = props.colors.disabledIconColor.composeOrNull ?: ComposeColor.Unspecified, + disabledTextColor = props.colors.disabledTextColor.composeOrNull ?: ComposeColor.Unspecified + ) + ) + } +} diff --git a/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts new file mode 100644 index 00000000000000..e84f86222a7eb5 --- /dev/null +++ b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts @@ -0,0 +1,101 @@ +import { type ColorValue } from 'react-native'; +import { type ModifierConfig } from '../../types'; +type SlotProps = { + children: React.ReactNode; +}; +/** + * Colors for navigation bar items in different states. + */ +export type NavigationBarItemColors = { + selectedIconColor?: ColorValue; + selectedTextColor?: ColorValue; + selectedIndicatorColor?: ColorValue; + unselectedIconColor?: ColorValue; + unselectedTextColor?: ColorValue; + disabledIconColor?: ColorValue; + disabledTextColor?: ColorValue; +}; +export type NavigationBarProps = { + /** + * Background color of the navigation bar. + * @default NavigationBarDefaults.containerColor + */ + containerColor?: ColorValue; + /** + * Preferred content color inside the navigation bar. + * @default contentColorFor(containerColor) + */ + contentColor?: ColorValue; + /** + * Tonal elevation in dp. + * @default NavigationBarDefaults.Elevation + */ + tonalElevation?: number; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Navigation bar items. + */ + children?: React.ReactNode; +}; +export type NavigationBarItemProps = { + /** + * Whether this item is currently selected. + */ + selected: boolean; + /** + * Callback that is called when the item is clicked. + */ + onClick?: () => void; + /** + * Whether the item is enabled. + * @default true + */ + enabled?: boolean; + /** + * Whether to always show the label. + * @default true + */ + alwaysShowLabel?: boolean; + /** + * Colors for the item in different states. + */ + colors?: NavigationBarItemColors; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Children containing `Icon`, `SelectedIcon`, and `Label` slots. + */ + children?: React.ReactNode; +}; +/** + * Icon slot for `NavigationBarItem`. + */ +declare function NavigationBarItemIcon(props: SlotProps): import("react/jsx-runtime").JSX.Element; +/** + * Selected icon slot for `NavigationBarItem`. Falls back to `Icon` when omitted. + */ +declare function NavigationBarItemSelectedIcon(props: SlotProps): import("react/jsx-runtime").JSX.Element; +/** + * Label slot for `NavigationBarItem`. + */ +declare function NavigationBarItemLabel(props: SlotProps): import("react/jsx-runtime").JSX.Element; +/** + * A Material Design 3 navigation bar. + */ +export declare function NavigationBar(props: NavigationBarProps): import("react/jsx-runtime").JSX.Element; +/** + * A Material Design 3 navigation bar item. Must be used inside `NavigationBar`. + */ +declare function NavigationBarItemComponent(props: NavigationBarItemProps): import("react/jsx-runtime").JSX.Element; +declare namespace NavigationBarItemComponent { + var Icon: typeof NavigationBarItemIcon; + var SelectedIcon: typeof NavigationBarItemSelectedIcon; + var Label: typeof NavigationBarItemLabel; +} +export { NavigationBarItemComponent as NavigationBarItem }; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts.map b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts.map new file mode 100644 index 00000000000000..4b9f3a464c3653 --- /dev/null +++ b/packages/expo-ui/build/jetpack-compose/NavigationBar/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/jetpack-compose/NavigationBar/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,aAAa,CAAC;AAGlE,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAOF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B,sBAAsB,CAAC,EAAE,UAAU,CAAC;IACpC,mBAAmB,CAAC,EAAE,UAAU,CAAC;IACjC,mBAAmB,CAAC,EAAE,UAAU,CAAC;IACjC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B,iBAAiB,CAAC,EAAE,UAAU,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;OAGG;IACH,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B;;OAEG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC;;OAEG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B;;OAEG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC;AAuCF;;GAEG;AACH,iBAAS,qBAAqB,CAAC,KAAK,EAAE,SAAS,2CAE9C;AAED;;GAEG;AACH,iBAAS,6BAA6B,CAAC,KAAK,EAAE,SAAS,2CAEtD;AAED;;GAEG;AACH,iBAAS,sBAAsB,CAAC,KAAK,EAAE,SAAS,2CAE/C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,2CAOtD;AAED;;GAEG;AACH,iBAAS,0BAA0B,CAAC,KAAK,EAAE,sBAAsB,2CAOhE;kBAPQ,0BAA0B;;;;;AAanC,OAAO,EAAE,0BAA0B,IAAI,iBAAiB,EAAE,CAAC"} \ No newline at end of file diff --git a/packages/expo-ui/build/jetpack-compose/index.d.ts b/packages/expo-ui/build/jetpack-compose/index.d.ts index 1046f05fef6b24..d967890d40437c 100644 --- a/packages/expo-ui/build/jetpack-compose/index.d.ts +++ b/packages/expo-ui/build/jetpack-compose/index.d.ts @@ -29,6 +29,7 @@ export { TextField, OutlinedTextField, type TextFieldProps, type TextFieldRef, t export * from './ToggleButton'; export * from './Shape'; export * from './ModalBottomSheet'; +export * from './NavigationBar'; export * from './Carousel'; export { HorizontalPager, type HorizontalPagerHandle, type HorizontalPagerProps, } from './HorizontalPager'; export * from './SearchBar'; diff --git a/packages/expo-ui/build/jetpack-compose/index.d.ts.map b/packages/expo-ui/build/jetpack-compose/index.d.ts.map index 9523984cfd4b5f..48109c840de453 100644 --- a/packages/expo-ui/build/jetpack-compose/index.d.ts.map +++ b/packages/expo-ui/build/jetpack-compose/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jetpack-compose/index.ts"],"names":[],"mappings":"AAAA,OAAO,uCAAuC,CAAC;AAE/C,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9C,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AAEnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jetpack-compose/index.ts"],"names":[],"mappings":"AAAA,OAAO,uCAAuC,CAAC;AAE/C,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9C,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AAEnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,OAAO,CAAC;AACtB,cAAc,OAAO,CAAC;AACtB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"} \ No newline at end of file diff --git a/packages/expo-ui/src/jetpack-compose/NavigationBar/index.tsx b/packages/expo-ui/src/jetpack-compose/NavigationBar/index.tsx new file mode 100644 index 00000000000000..321166d82e43b3 --- /dev/null +++ b/packages/expo-ui/src/jetpack-compose/NavigationBar/index.tsx @@ -0,0 +1,174 @@ +import { requireNativeView } from 'expo'; +import { type ColorValue } from 'react-native'; + +import { type ModifierConfig, type ViewEvent } from '../../types'; +import { createViewModifierEventListener } from '../modifiers'; + +type SlotProps = { + children: React.ReactNode; +}; + +type NativeSlotViewProps = { + slotName: string; + children: React.ReactNode; +}; + +/** + * Colors for navigation bar items in different states. + */ +export type NavigationBarItemColors = { + selectedIconColor?: ColorValue; + selectedTextColor?: ColorValue; + selectedIndicatorColor?: ColorValue; + unselectedIconColor?: ColorValue; + unselectedTextColor?: ColorValue; + disabledIconColor?: ColorValue; + disabledTextColor?: ColorValue; +}; + +export type NavigationBarProps = { + /** + * Background color of the navigation bar. + * @default NavigationBarDefaults.containerColor + */ + containerColor?: ColorValue; + /** + * Preferred content color inside the navigation bar. + * @default contentColorFor(containerColor) + */ + contentColor?: ColorValue; + /** + * Tonal elevation in dp. + * @default NavigationBarDefaults.Elevation + */ + tonalElevation?: number; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Navigation bar items. + */ + children?: React.ReactNode; +}; + +export type NavigationBarItemProps = { + /** + * Whether this item is currently selected. + */ + selected: boolean; + /** + * Callback that is called when the item is clicked. + */ + onClick?: () => void; + /** + * Whether the item is enabled. + * @default true + */ + enabled?: boolean; + /** + * Whether to always show the label. + * @default true + */ + alwaysShowLabel?: boolean; + /** + * Colors for the item in different states. + */ + colors?: NavigationBarItemColors; + /** + * Modifiers for the component. + */ + modifiers?: ModifierConfig[]; + /** + * Children containing `Icon`, `SelectedIcon`, and `Label` slots. + */ + children?: React.ReactNode; +}; + +type NativeNavigationBarItemProps = Omit & + ViewEvent<'onButtonPressed', void>; + +const NavigationBarNativeView: React.ComponentType = requireNativeView( + 'ExpoUI', + 'NavigationBarView' +); + +const NavigationBarItemNativeView: React.ComponentType = + requireNativeView('ExpoUI', 'NavigationBarItemView'); + +const SlotNativeView: React.ComponentType = requireNativeView( + 'ExpoUI', + 'SlotView' +); + +function transformNavigationBarProps(props: NavigationBarProps): NavigationBarProps { + const { modifiers, ...restProps } = props; + return { + modifiers, + ...(modifiers ? createViewModifierEventListener(modifiers) : undefined), + ...restProps, + }; +} + +function transformNavigationBarItemProps( + props: NavigationBarItemProps +): NativeNavigationBarItemProps { + const { modifiers, onClick, ...restProps } = props; + return { + modifiers, + ...(modifiers ? createViewModifierEventListener(modifiers) : undefined), + ...restProps, + onButtonPressed: () => onClick?.(), + }; +} + +/** + * Icon slot for `NavigationBarItem`. + */ +function NavigationBarItemIcon(props: SlotProps) { + return {props.children}; +} + +/** + * Selected icon slot for `NavigationBarItem`. Falls back to `Icon` when omitted. + */ +function NavigationBarItemSelectedIcon(props: SlotProps) { + return {props.children}; +} + +/** + * Label slot for `NavigationBarItem`. + */ +function NavigationBarItemLabel(props: SlotProps) { + return {props.children}; +} + +/** + * A Material Design 3 navigation bar. + */ +export function NavigationBar(props: NavigationBarProps) { + const { children, ...restProps } = props; + return ( + + {children} + + ); +} + +/** + * A Material Design 3 navigation bar item. Must be used inside `NavigationBar`. + */ +function NavigationBarItemComponent(props: NavigationBarItemProps) { + const { children, ...restProps } = props; + return ( + + {children} + + ); +} + +NavigationBarItemComponent.Icon = NavigationBarItemIcon; +NavigationBarItemComponent.SelectedIcon = NavigationBarItemSelectedIcon; +NavigationBarItemComponent.Label = NavigationBarItemLabel; + +export { NavigationBarItemComponent as NavigationBarItem }; diff --git a/packages/expo-ui/src/jetpack-compose/index.ts b/packages/expo-ui/src/jetpack-compose/index.ts index a266cab370381c..5aedc12b0e97e1 100644 --- a/packages/expo-ui/src/jetpack-compose/index.ts +++ b/packages/expo-ui/src/jetpack-compose/index.ts @@ -41,6 +41,7 @@ export { export * from './ToggleButton'; export * from './Shape'; export * from './ModalBottomSheet'; +export * from './NavigationBar'; export * from './Carousel'; export { HorizontalPager, diff --git a/tools/src/commands/GenerateDocsAPIData.ts b/tools/src/commands/GenerateDocsAPIData.ts index a3881a444e45c9..7f526272426876 100644 --- a/tools/src/commands/GenerateDocsAPIData.ts +++ b/tools/src/commands/GenerateDocsAPIData.ts @@ -131,6 +131,7 @@ const uiPackagesMapping: Record = { 'expo-ui/jetpack-compose/progress': ['jetpack-compose/Progress/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/listitem': ['jetpack-compose/ListItem/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/modifiers': ['jetpack-compose/modifiers/index.ts', 'expo-ui'], + 'expo-ui/jetpack-compose/navigationbar': ['jetpack-compose/NavigationBar/index.tsx', 'expo-ui'], 'expo-ui/jetpack-compose/segmentedbutton': [ 'jetpack-compose/SegmentedButton/index.tsx', 'expo-ui',