Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions apps/mobile/App.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,50 @@
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { NavigationContainer, LinkingOptions } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { AuthProvider, useAuth } from './src/context/AuthContext';
import { ThemeProvider } from './src/context/ThemeContext';
import AuthStack from './src/navigation/AuthStack';
import MainTabs from './src/navigation/MainTabs';
import SplashScreen from './src/screens/SplashScreen';
import { DEEP_LINK_SCHEME } from './src/config';

import { Linking, StyleSheet } from 'react-native';

// ── Deep Link Configuration ───────────────────────────────────────────────────

const linking: LinkingOptions<{}> = {
prefixes: [`${DEEP_LINK_SCHEME}://`],
config: {
screens: {
MainTabs: {
screens: {
Home: 'home',
Scan: 'scan',
},
},
DevCardView: 'u/:username',
},
},
};

// ── App Content ───────────────────────────────────────────────────────────────

function AppContent() {
const { isAuthenticated, isLoading, login } = useAuth();

React.useEffect(() => {
const handleDeepLink = (event: { url: string }) => {
console.log('--- DEEP LINK RECEIVED ---');
console.log('URL:', event.url);
const url = new URL(event.url);
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ''));
const token = url.searchParams.get('token') || hashParams.get('token');
if (token) {
console.log('Token found, logging in...');
login(token);
try {
const url = new URL(event.url);
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ''));
const token = url.searchParams.get('token') || hashParams.get('token');
if (token) {
login(token);
}
} catch (error) {
console.error('Deep link parse error:', error);
}
};

Expand All @@ -38,16 +60,18 @@ function AppContent() {
}, [login]);

if (isLoading) {
return null; // Splash screen could go here
return <SplashScreen />;
}

return (
<NavigationContainer>
<NavigationContainer linking={linking}>
{isAuthenticated ? <MainTabs /> : <AuthStack />}
</NavigationContainer>
);
}

// ── Root ───────────────────────────────────────────────────────────────────────

export default function App() {
return (
<GestureHandlerRootView style={styles.gestureRoot}>
Expand Down
7 changes: 3 additions & 4 deletions apps/mobile/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';

// registerRootComponent handles mounting and bootstrapping the app
// on both native mobile devices (Expo Go) and web browsers seamlessly.
registerRootComponent(App);
AppRegistry.registerComponent(appName, () => App);
73 changes: 38 additions & 35 deletions apps/mobile/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,52 @@
const { getDefaultConfig } = require('expo/metro-config');
const { getDefaultConfig } = require('@react-native/metro-config');
const path = require('path');

// Monorepo root
const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, '../..');

/**
* Metro configuration for Expo monorepo
* Metro configuration for React Native monorepo
*/
const config = getDefaultConfig(projectRoot);
module.exports = (async () => {
const config = await getDefaultConfig(projectRoot);

config.watchFolders = [monorepoRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
];
config.resolver.disableHierarchicalLookup = false;
config.watchFolders = [monorepoRoot];
config.resolver = config.resolver || {};
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(monorepoRoot, 'node_modules'),
];
config.resolver.disableHierarchicalLookup = false;

const pinnedModules = {
react: path.resolve(projectRoot, 'node_modules/react'),
'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
'react-native-reanimated': path.resolve(
projectRoot,
'node_modules/react-native-reanimated'
),
'react-native-worklets': path.resolve(
projectRoot,
'node_modules/react-native-worklets'
),
'react-native-gesture-handler': path.resolve(
projectRoot,
'node_modules/react-native-gesture-handler'
),
};
const pinnedModules = {
react: path.resolve(projectRoot, 'node_modules/react'),
'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
'react-native-reanimated': path.resolve(
projectRoot,
'node_modules/react-native-reanimated'
),
'react-native-worklets': path.resolve(
projectRoot,
'node_modules/react-native-worklets'
),
'react-native-gesture-handler': path.resolve(
projectRoot,
'node_modules/react-native-gesture-handler'
),
};

config.resolver.extraNodeModules = pinnedModules;
config.resolver.resolveRequest = (context, moduleName, platform) => {
for (const [name, modulePath] of Object.entries(pinnedModules)) {
if (moduleName === name || moduleName.startsWith(`${name}/`)) {
const target = path.join(modulePath, moduleName.slice(name.length));
return context.resolveRequest(context, target, platform);
config.resolver.extraNodeModules = pinnedModules;
config.resolver.resolveRequest = (context, moduleName, platform) => {
for (const [name, modulePath] of Object.entries(pinnedModules)) {
if (moduleName === name || moduleName.startsWith(`${name}/`)) {
const target = path.join(modulePath, moduleName.slice(name.length));
return context.resolveRequest(context, target, platform);
}
}
}

return context.resolveRequest(context, moduleName, platform);
};
return context.resolveRequest(context, moduleName, platform);
};

module.exports = config;
return config;
})();
74 changes: 74 additions & 0 deletions apps/mobile/src/components/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { COLORS, SPACING, BORDER_RADIUS } from '../theme/tokens';

// ── Predefined Accent Color Palette ───────────────────────────────────────────
// 8 curated colors that work well as card accent on the dark DevCard theme.

export const ACCENT_COLORS = [
'#6366F1', // Indigo (default)
'#8B5CF6', // Violet
'#EC4899', // Pink
'#EF4444', // Red
'#F59E0B', // Amber
'#22C55E', // Green
'#06B6D4', // Cyan
'#3B82F6', // Blue
] as const;

export type AccentColor = (typeof ACCENT_COLORS)[number];

interface ColorPickerProps {
selected: string;
onSelect: (color: string) => void;
}

export default function ColorPicker({ selected, onSelect }: ColorPickerProps) {
return (
<View style={styles.container}>
{ACCENT_COLORS.map((color) => {
const isActive = selected === color;
return (
<TouchableOpacity
key={color}
style={[
styles.swatch,
{ backgroundColor: color },
isActive && styles.swatchActive,
]}
onPress={() => onSelect(color)}
activeOpacity={0.7}
accessibilityLabel={`Select accent color ${color}`}
accessibilityRole="radio"
accessibilityState={{ selected: isActive }}
/>
);
})}
</View>
);
}

const styles = StyleSheet.create({
container: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: SPACING.sm,
justifyContent: 'center',
},
swatch: {
width: 40,
height: 40,
borderRadius: BORDER_RADIUS.full,
borderWidth: 2,
borderColor: COLORS.transparent,
},
swatchActive: {
borderColor: COLORS.white,
transform: [{ scale: 1.15 }],
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.4,
shadowRadius: 4,
elevation: 6,
},
});
30 changes: 16 additions & 14 deletions apps/mobile/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import Constants from 'expo-constants';
import * as Linking from 'expo-linking';
import { Platform } from 'react-native';

// DevCard API Configuration
// ── DevCard API Configuration ─────────────────────────────────────────────────
// Environment-aware URLs with no Expo dependency. On Android emulators the
// loopback address is 10.0.2.2; on iOS simulators localhost works directly.

// Prefer explicit configuration via Expo/EAS extras. Fallback to sensible defaults
const extras = (Constants as any).manifest?.extra || (Constants as any).expoConfig?.extra;
const ANDROID_LOCALHOST = '10.0.2.2';
const IOS_LOCALHOST = 'localhost';
const DEV_HOST = Platform.OS === 'android' ? ANDROID_LOCALHOST : IOS_LOCALHOST;

const DEV_API = extras?.API_BASE_URL || extras?.DEV_API_BASE_URL;
const DEV_APP = extras?.APP_URL;
export const API_BASE_URL: string = __DEV__
? `http://${DEV_HOST}:3000`
: 'https://api.devcard.dev';

export const API_BASE_URL = __DEV__
? DEV_API ?? `http://10.0.2.2:3000` // 10.0.2.2 is a common emulator host for Android
: extras?.API_BASE_URL ?? 'https://api.devcard.dev';
export const APP_URL: string = __DEV__
? 'http://localhost:5173'
: 'https://devcard.dev';

export const APP_URL = __DEV__
? DEV_APP ?? `http://localhost:5173`
: extras?.APP_URL ?? 'https://devcard.dev';
// Deep link scheme — must match android/app/build.gradle and ios/Info.plist
export const DEEP_LINK_SCHEME = 'devcard';

export const OAUTH_REDIRECT_URI = Linking.createURL('oauth/callback');
export const OAUTH_REDIRECT_URI = `${DEEP_LINK_SCHEME}://oauth/callback`;
Loading