Skip to content

Commit

Permalink
Merge pull request #13 from trajano/devmenu
Browse files Browse the repository at this point in the history
feat: removed react-native devmenu and only use expo-dev-client menu
  • Loading branch information
trajano authored Nov 15, 2024
2 parents b667aad + 2cf668d commit 3d8891a
Show file tree
Hide file tree
Showing 23 changed files with 485 additions and 76 deletions.
67 changes: 67 additions & 0 deletions packages/my-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,70 @@ This application is designed to explore foundational functionalities of Expo, al

1. Components: Moved to the `react-native-my-components` package.
2. Component-level stories: Managed within `react-native-my-components`.

Certainly, Lord Archimedes. Here’s a refined version in Markdown format:

## App Router Layout

Certainly, Lord Archimedes. Here’s a simplified table format:

---

## App Router Layout

| Path | Description |
| ------------------------------ | -------------------------------- |
| `/_layout` | App setup |
| `/index` | Fancy splash screen |
| `/storybook` | Storybook-related components |
| `/+not-found` | The 404 page |
| **Unauthenticated Path** | |
| `/public/index` | Login screen chooser |
| `/public/sign-up` | Optional sign-up screen |
| `/public/terms-and-conditions` | Basically lorem ipsum |
| **Authenticated Path** | |
| `/secure/(tabs)/` | Tab switcher component |
| `/secure/(tabs)/details/[id]` | Dynamic route for detailed views |

## Comparing App Design Philosophies

The difference between a typical Apple product, a Google product, and "your company’s app" is that "your company’s app" is actually designed to perform work specific to your business needs. Thus, I would focus on streamlining the user experience for "your company’s app."
[![complex app](src/assets/images/complex-app-meme.png)]

### Approach to Streamlining "Your Company's App" UX

1. **Tabs Placement**

- On small devices, center the tabs evenly.
- On large devices, align tabs to the right to avoid excessive gaps.

2. **Drawer Usage**

- Use the drawer for a specific tab rather than as separate entries.
- This reduces the need for multiple steps to reach the desired content, improving flow.

3. **Tab Structure**

- Each tab should reflect a mode of operation:
- **Dashboard**: Displays the main portfolio or case view.
- **Search**: A general search page with no drawer.
- **Me**: A personal settings area in a simple scroll view, without a drawer.

### Alternative Approach: Drawer-First Navigation

After evaluating the Deco app, a drawer-first approach may be more effective:

1. **Drawer Contents**

- Include account-specific elements and a link to settings.

2. **Tabs in Drawer**

- Retain the core tabs in the drawer, keeping it simple despite "file-based" navigation challenges:
- **Dashboard**
- **Search**
- **Me**

### Observations

Regardless of approach, the main tabs remain consistent, which is beneficial for a streamlined experience. This concept fits a “caseworker” style app with an “administrative” focus, where caseworkers manage portfolios or cases, handle form data entries, and navigate through workflows.
File renamed without changes.
19 changes: 12 additions & 7 deletions packages/my-app/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ const monorepoRoot = path.resolve(__dirname, '../..');
const config = getDefaultConfig(__dirname);

config.watchFolders = [monorepoRoot];
if (!config.resolver) {
throw new Error('config.resolver is undefined');
}
config.resolver.nodeModulesPaths = [
path.resolve(__dirname, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
];
config.resolver = {
...config.resolver,
nodeModulesPaths: [
path.resolve(__dirname, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
],
};
const additionalAssetExts = ['lottie', 'fbx'];
if (Array.isArray(config.resolver.assetExts)) {
config.resolver.assetExts = [
Expand All @@ -31,6 +31,11 @@ if (Array.isArray(config.resolver.blockList)) {
config.resolver.blockList = [config.resolver.blockList, testRegex];
}

config.transformer = {
...config.transformer,
unstable_allowRequireContext: true,
};

// @ts-ignore
module.exports = withStorybook(config, {
enabled: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/my-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"jest": {
"preset": "jest-expo",
"setupFiles": [
"./jest-setup.js"
"./jest-setup.ts"
]
},
"dependencies": {
Expand Down
18 changes: 14 additions & 4 deletions packages/my-app/src/__tests__/RootLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ import { renderRouter, screen } from 'expo-router/testing-library';
import { act } from '@testing-library/react-native';
import RootLayout from '@/app/_layout';
import { View } from 'react-native';
import { registerDevMenuItemsAsync } from '@/devmenu';

jest.mock('@/devMenu', () => {});
jest.mock('expo-dev-client');
jest.mock('@/devmenu', () => ({
registerDevMenuItemsAsync: jest.fn(() => Promise.resolve()),
}));

// Mock out contexts
jest.mock('@/hooks/UserPreferences', () => ({
WithUserPreferences: (i: any) => i,
WithUserPreferences: jest.fn((i: any) => i),
}));
jest.mock('@/hooks/MyBackgroundFetch', () => ({
WithMyBackgroundFetch: (i: any) => i,
WithMyBackgroundFetch: jest.fn((i: any) => i),
}));
jest.mock('react-native-my-hooks', () => ({
...jest.requireActual('react-native-my-hooks'),
WithNotifications: (i: any) => i,
WithNotifications: jest.fn((i: any) => i),
}));

test('RootLayout', async () => {
Expand All @@ -30,3 +34,9 @@ test('RootLayout', async () => {
expect(screen.getByTestId('faux')).toBeTruthy();
expect(getPathname()).toStrictEqual('/');
});

test('ensure devmenu mock', () => {
expect(
registerDevMenuItemsAsync({ router: jest.fn() as any }),
).resolves.toBeUndefined();
});
27 changes: 5 additions & 22 deletions packages/my-app/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,34 @@ import {
ThemeProvider,
} from '@react-navigation/native';

import { SplashScreen, Stack } from 'expo-router';
import { SplashScreen, Stack, useRouter } from 'expo-router';
import { FC, useEffect } from 'react';

import { WithMyBackgroundFetch } from '@/hooks/MyBackgroundFetch';
import { BACKGROUND_FETCH_TASK, BACKGROUND_NOTIFICATION_TASK } from '@/tasks';
import { WithUserPreferences } from '@/hooks/UserPreferences';
import { useColorScheme } from 'react-native';
import { WithNotifications } from 'react-native-my-hooks';
import { DevMenu } from 'expo-dev-client';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { clearLogFilesAsync } from '@/logging';

import '@/devMenu';
import 'react-native-reanimated';
import { registerDevMenuItemsAsync } from '@/devmenu';

// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();

export const RootLayout: FC = () => {
const colorScheme = useColorScheme();
const router = useRouter();
// useLoadGuard
useEffect(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 250));
// before hiding the splashscreen the fonts and assets for the loader screen should be loaded
await registerDevMenuItemsAsync({ router });
await SplashScreen.hideAsync();
await DevMenu.registerDevMenuItems([
{
name: 'Clear AsyncStorage',
callback: () => {
AsyncStorage.clear();
},
shouldCollapse: true,
},
{
name: 'Clear Log files',
callback: () => {
clearLogFilesAsync();
},
shouldCollapse: true,
},
]);
// this may be moved to load guard.
})();
}, []);
}, [router]);

// if (!loaded), but I want it already on the stack right?
return (
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 0 additions & 14 deletions packages/my-app/src/devMenu.test.tsx

This file was deleted.

9 changes: 0 additions & 9 deletions packages/my-app/src/devMenu.tsx

This file was deleted.

38 changes: 38 additions & 0 deletions packages/my-app/src/devmenu/ClearAllData.devmenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DevMenuItemModule } from '@/devmenu/types';
import * as FileSystem from 'expo-file-system';
import { DevSettings } from 'react-native';

const deleteFilesRecursivelyAsync = async (directoryUri: string) => {
// Get all items in the directory
const items = await FileSystem.readDirectoryAsync(directoryUri);

// Iterate over each item and check if it's a directory or a file
for (const item of items) {
const itemUri = `${directoryUri}/${item}`;
const info = await FileSystem.getInfoAsync(itemUri);

if (info.isDirectory) {
// Recursively delete contents of the directory
await deleteFilesRecursivelyAsync(itemUri);
}

// Delete the file or empty directory as long as it isn't the root
if (
itemUri !== FileSystem.documentDirectory &&
itemUri !== FileSystem.cacheDirectory
) {
await FileSystem.deleteAsync(itemUri, { idempotent: true });
}
}
};

export default {
name: 'Reset app',
callback: async () => {
await Promise.all([
deleteFilesRecursivelyAsync(FileSystem.documentDirectory!),
deleteFilesRecursivelyAsync(FileSystem.cacheDirectory!),
]);
DevSettings.reload('application reset');
},
} satisfies DevMenuItemModule;
80 changes: 80 additions & 0 deletions packages/my-app/src/devmenu/ClearAllData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { DevSettings } from 'react-native';
import * as FileSystem from 'expo-file-system';
import ClearAllData from './ClearAllData.devmenu';

jest.mock('expo-file-system', () => ({
readDirectoryAsync: jest.fn(),
getInfoAsync: jest.fn(),
deleteAsync: jest.fn(),
documentDirectory: 'document-directory',
cacheDirectory: 'cache-directory',
}));

jest.mock('react-native', () => ({
DevSettings: {
reload: jest.fn(),
},
}));

describe('Reset App Module', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should delete all files and directories recursively and reload the app', async () => {
// Mock file structure
const mockFileStructure: Record<string, string[]> = {
'document-directory': ['file1', 'dir1'],
'document-directory/dir1': ['file2'],
'cache-directory': ['file3'],
};

// Mock FileSystem functions
jest
.mocked(FileSystem.readDirectoryAsync)
.mockImplementation(async (dir) => {
return mockFileStructure[dir] || [];
});

jest.mocked(FileSystem.getInfoAsync).mockImplementation(async (path) => ({
isDirectory: path.includes('dir'),
exists: true,
modificationTime: Date.now(),
uri: path,
size: 42,
}));

jest.mocked(FileSystem.deleteAsync).mockResolvedValue();

// Execute the callback
await ClearAllData.callback();

// Assert file deletion
expect(FileSystem.deleteAsync).toHaveBeenCalledWith(
'document-directory/file1',
{ idempotent: true },
);
expect(FileSystem.deleteAsync).toHaveBeenCalledWith(
'document-directory/dir1/file2',
{ idempotent: true },
);
expect(FileSystem.deleteAsync).toHaveBeenCalledWith(
'document-directory/dir1',
{ idempotent: true },
);
expect(FileSystem.deleteAsync).toHaveBeenCalledWith(
'cache-directory/file3',
{ idempotent: true },
);
expect(FileSystem.deleteAsync).not.toHaveBeenCalledWith(
'document-directory',
{ idempotent: true },
);
expect(FileSystem.deleteAsync).not.toHaveBeenCalledWith('cache-directory', {
idempotent: true,
});

// Assert app reload
expect(DevSettings.reload).toHaveBeenCalledWith('application reset');
});
});
9 changes: 9 additions & 0 deletions packages/my-app/src/devmenu/ClearAsyncStorage.devmenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { DevMenuItemModule } from '@/devmenu/types';

export default {
name: 'Clear AsyncStorage',
callback: async () => {
await AsyncStorage.clear();
},
} satisfies DevMenuItemModule;
5 changes: 5 additions & 0 deletions packages/my-app/src/devmenu/ClearAsyncStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ClearAsyncStorageDevmenu from '@/devmenu/ClearAsyncStorage.devmenu';

test('ClearAsyncStorageDevmenu', async () => {
await ClearAsyncStorageDevmenu.callback();
});
26 changes: 26 additions & 0 deletions packages/my-app/src/devmenu/ClearLogFiles.devmenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DevMenuItemModule } from '@/devmenu/types';
import * as FileSystem from 'expo-file-system';

const clearLogFilesByPrefixAndSuffixAsync = async (
prefix: string,
suffix: string,
): Promise<void> => {
await Promise.all(
(await FileSystem.readDirectoryAsync(FileSystem.documentDirectory!))
.filter((it) => it.startsWith(prefix) && it.endsWith(suffix))
.map((it) => FileSystem.documentDirectory + it)
.map((it) => FileSystem.deleteAsync(it, { idempotent: true })),
);
};

export default {
name: 'Clear Log Files',
callback: async () => {
await Promise.all([
clearLogFilesByPrefixAndSuffixAsync('logs_', '.txt'),
clearLogFilesByPrefixAndSuffixAsync('background_fetch_', '.txt'),
clearLogFilesByPrefixAndSuffixAsync('notification_', '.txt'),
clearLogFilesByPrefixAndSuffixAsync('location_', '.txt'),
]);
},
} satisfies DevMenuItemModule;
Loading

0 comments on commit 3d8891a

Please sign in to comment.