Skip to content

Commit f477807

Browse files
feat(mobile): add card picker sheet with QR card switching persistence (#81)
* feat: update dependencies and configuration for mobile app * feat: implement CardPickerSheet component with selection functionality and styling * feat: enhance ScanScreen with card fetching, selection, and QR code sharing functionality * test: update CardPickerSheet tests to use async rendering and improve unmount handling * feat: update ConnectPlatformsScreen and ViewsScreen to use async storage and improve API integration * fix: lint errors * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: rohit <rohit84.official@gmail.com> * fix: address PR review comments --------- Signed-off-by: rohit <rohit84.official@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 0db6638 commit f477807

12 files changed

Lines changed: 945 additions & 124 deletions

apps/mobile/App.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import React from 'react';
22
import { NavigationContainer } from '@react-navigation/native';
33
import { SafeAreaProvider } from 'react-native-safe-area-context';
4+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
5+
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
46
import { AuthProvider, useAuth } from './src/context/AuthContext';
57
import { ThemeProvider } from './src/context/ThemeContext';
68
import AuthStack from './src/navigation/AuthStack';
79
import MainTabs from './src/navigation/MainTabs';
810

9-
import { Linking } from 'react-native';
11+
import { Linking, StyleSheet } from 'react-native';
1012

1113
function AppContent() {
1214
const { isAuthenticated, isLoading, login } = useAuth();
@@ -47,12 +49,20 @@ function AppContent() {
4749

4850
export default function App() {
4951
return (
50-
<SafeAreaProvider>
51-
<ThemeProvider>
52-
<AuthProvider>
53-
<AppContent />
54-
</AuthProvider>
55-
</ThemeProvider>
56-
</SafeAreaProvider>
52+
<GestureHandlerRootView style={styles.gestureRoot}>
53+
<SafeAreaProvider>
54+
<ThemeProvider>
55+
<AuthProvider>
56+
<BottomSheetModalProvider>
57+
<AppContent />
58+
</BottomSheetModalProvider>
59+
</AuthProvider>
60+
</ThemeProvider>
61+
</SafeAreaProvider>
62+
</GestureHandlerRootView>
5763
);
5864
}
65+
66+
const styles = StyleSheet.create({
67+
gestureRoot: { flex: 1 },
68+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React from 'react';
2+
import ReactTestRenderer from 'react-test-renderer';
3+
import { Text, TouchableOpacity } from 'react-native';
4+
import type { Card } from '@devcard/shared';
5+
import CardPickerSheet from '../src/components/CardPickerSheet';
6+
7+
jest.mock('@gorhom/bottom-sheet', () => {
8+
const ReactModule = require('react');
9+
return {
10+
BottomSheetModal: ReactModule.forwardRef(({ children }: any, _ref: any) => (
11+
<>{children}</>
12+
)),
13+
BottomSheetBackdrop: () => null,
14+
BottomSheetScrollView: ({ children }: any) => <>{children}</>,
15+
};
16+
});
17+
18+
const buildCard = (overrides: Partial<Card>): Card => ({
19+
id: 'card-1',
20+
title: 'Professional',
21+
isDefault: false,
22+
links: [
23+
{
24+
id: 'link-1',
25+
platform: 'github',
26+
username: 'octocat',
27+
url: 'https://github.com/octocat',
28+
displayOrder: 0,
29+
},
30+
],
31+
...overrides,
32+
});
33+
34+
const extractText = (node: any): string => {
35+
if (node === null || node === undefined) return '';
36+
if (typeof node === 'string' || typeof node === 'number') return String(node);
37+
if (Array.isArray(node)) return node.map(extractText).join('');
38+
return extractText(node.props?.children);
39+
};
40+
41+
describe('CardPickerSheet', () => {
42+
test('renders card titles and link counts', async () => {
43+
const cards: Card[] = [
44+
buildCard({ id: 'card-1', title: 'Professional' }),
45+
buildCard({
46+
id: 'card-2',
47+
title: 'Hackathon',
48+
links: [
49+
{
50+
id: 'link-2',
51+
platform: 'twitter',
52+
username: 'dev',
53+
url: 'https://x.com/dev',
54+
displayOrder: 0,
55+
},
56+
{
57+
id: 'link-3',
58+
platform: 'linkedin',
59+
username: 'dev',
60+
url: 'https://linkedin.com/in/dev',
61+
displayOrder: 1,
62+
},
63+
],
64+
}),
65+
];
66+
67+
let renderer: ReactTestRenderer.ReactTestRenderer;
68+
await ReactTestRenderer.act(async () => {
69+
renderer = ReactTestRenderer.create(
70+
<CardPickerSheet cards={cards} selectedCardId={null} onSelect={() => {}} />
71+
);
72+
});
73+
74+
const textNodes = renderer!.root
75+
.findAllByType(Text)
76+
.map(node => extractText(node));
77+
expect(textNodes).toContain('Professional');
78+
expect(textNodes).toContain('Hackathon');
79+
expect(textNodes).toContain('1 link');
80+
expect(textNodes).toContain('2 links');
81+
await ReactTestRenderer.act(async () => {
82+
renderer!.unmount();
83+
});
84+
});
85+
86+
test('calls onSelect when a card is selected', async () => {
87+
const cards: Card[] = [
88+
buildCard({ id: 'card-1', title: 'Professional' }),
89+
buildCard({ id: 'card-2', title: 'Hackathon' }),
90+
];
91+
const onSelect = jest.fn();
92+
93+
let renderer: ReactTestRenderer.ReactTestRenderer;
94+
await ReactTestRenderer.act(async () => {
95+
renderer = ReactTestRenderer.create(
96+
<CardPickerSheet cards={cards} selectedCardId="card-2" onSelect={onSelect} />
97+
);
98+
});
99+
100+
const buttons = renderer!.root.findAllByType(TouchableOpacity);
101+
const selectButton = buttons.find(button => {
102+
const labels = button.findAllByType(Text).map(node => extractText(node));
103+
return labels.includes('Select');
104+
});
105+
106+
ReactTestRenderer.act(() => {
107+
selectButton?.props.onPress();
108+
});
109+
110+
expect(onSelect).toHaveBeenCalledWith('card-1');
111+
await ReactTestRenderer.act(async () => {
112+
renderer!.unmount();
113+
});
114+
});
115+
116+
test('shows empty state when only one card exists', async () => {
117+
const cards: Card[] = [buildCard({ id: 'card-1' })];
118+
119+
let renderer: ReactTestRenderer.ReactTestRenderer;
120+
await ReactTestRenderer.act(async () => {
121+
renderer = ReactTestRenderer.create(
122+
<CardPickerSheet cards={cards} selectedCardId="card-1" onSelect={() => {}} />
123+
);
124+
});
125+
126+
const textNodes = renderer!.root
127+
.findAllByType(Text)
128+
.map(node => extractText(node));
129+
expect(textNodes).toContain('Create another card in Cards tab');
130+
await ReactTestRenderer.act(async () => {
131+
renderer!.unmount();
132+
});
133+
});
134+
});

apps/mobile/babel.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
module.exports = {
22
presets: ['module:@react-native/babel-preset'],
3+
plugins: ['react-native-reanimated/plugin'],
34
};

apps/mobile/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @format
33
*/
44

5+
import 'react-native-gesture-handler';
56
import { AppRegistry } from 'react-native';
67
import App from './App';
78
import { name as appName } from './app.json';

apps/mobile/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
module.exports = {
22
preset: 'react-native',
3+
setupFiles: ['<rootDir>/jest.setup.js'],
4+
transformIgnorePatterns: [
5+
'node_modules/(?!((react-native|@react-native|@react-navigation|@gorhom)/|\\.pnpm/(react-native|@react-native|@react-navigation|@gorhom)[^/]*))',
6+
],
37
};

apps/mobile/jest.setup.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* eslint-env jest */
2+
import 'react-native-gesture-handler/jestSetup';
3+
4+
global.__reanimatedWorkletInit = () => {};
5+
6+
jest.mock('@gorhom/bottom-sheet', () => {
7+
const React = require('react');
8+
return {
9+
BottomSheetModalProvider: ({ children }) => <>{children}</>,
10+
BottomSheetModal: React.forwardRef(({ children }, _ref) => <>{children}</>),
11+
BottomSheetBackdrop: () => null,
12+
BottomSheetScrollView: ({ children }) => <>{children}</>,
13+
};
14+
});
15+
16+
jest.mock('@react-native-async-storage/async-storage', () =>
17+
require('@react-native-async-storage/async-storage/jest/async-storage-mock')
18+
);
19+
20+
jest.mock('react-native-qrcode-svg', () => 'QRCode');
21+
22+
jest.mock('react-native-webview', () => ({
23+
WebView: 'WebView',
24+
}));
25+
26+
jest.mock('react-native-vector-icons/MaterialCommunityIcons', () => 'Icon');
27+
28+
jest.mock('react-native-reanimated', () => ({
29+
__esModule: true,
30+
default: {
31+
createAnimatedComponent: (Component) => Component,
32+
},
33+
createAnimatedComponent: (Component) => Component,
34+
useSharedValue: (value) => ({ value }),
35+
useAnimatedStyle: () => ({}),
36+
withTiming: (value) => value,
37+
withSpring: (value) => value,
38+
runOnJS: (fn) => fn,
39+
runOnUI: (fn) => fn,
40+
Easing: {},
41+
}));
42+
43+
try {
44+
require.resolve('react-native/Libraries/Animated/NativeAnimatedHelper');
45+
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
46+
} catch {
47+
// Module path changed in newer React Native versions.
48+
}

apps/mobile/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111
},
1212
"dependencies": {
1313
"@devcard/shared": "workspace:*",
14+
"@gorhom/bottom-sheet": "^5.0.5",
15+
"@react-native-async-storage/async-storage": "^2.1.0",
1416
"@react-native/new-app-screen": "0.84.1",
1517
"@react-navigation/bottom-tabs": "^7.0.0",
1618
"@react-navigation/native": "^7.0.0",
1719
"@react-navigation/native-stack": "^7.0.0",
1820
"react": "19.2.3",
1921
"react-dom": "^19.2.4",
2022
"react-native": "0.84.1",
23+
"react-native-gesture-handler": "^2.20.2",
2124
"react-native-qrcode-svg": "^6.3.0",
25+
"react-native-reanimated": "^3.15.0",
2226
"react-native-safe-area-context": "^5.5.2",
2327
"react-native-screens": "^4.0.0",
2428
"react-native-svg": "^15.0.0",
@@ -41,6 +45,7 @@
4145
"@react-native/typescript-config": "0.84.1",
4246
"@types/jest": "^29.5.13",
4347
"@types/react": "^19.2.0",
48+
"@types/react-native-vector-icons": "^6.4.18",
4449
"@types/react-test-renderer": "^19.1.0",
4550
"eslint": "^8.19.0",
4651
"jest": "^29.6.3",

0 commit comments

Comments
 (0)