├── components └── profile │ └── index.tsx ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values-night │ │ │ │ │ └── colors.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ │ └── ic_launcher_foreground.webp │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── splashscreen_logo.png │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── nativewind │ │ │ │ └── app │ │ │ │ ├── MainApplication.kt │ │ │ │ └── MainActivity.kt │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── debug.keystore │ ├── proguard-rules.pro │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── build.gradle ├── gradle.properties ├── gradlew.bat └── gradlew ├── .env ├── global.css ├── assets ├── images │ ├── icon.png │ ├── favicon.png │ ├── react-logo.png │ ├── adaptive-icon.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ ├── splash-icon.png │ └── partial-react-logo.png └── fonts │ └── SpaceMono-Regular.ttf ├── postcss.config.js ├── app ├── (auth) │ ├── login │ │ ├── _layout.tsx │ │ └── index.tsx │ ├── register │ │ ├── _layout.tsx │ │ └── index.tsx │ ├── _layout.tsx │ └── index.tsx ├── +not-found.tsx ├── (app) │ ├── profile │ │ ├── settings │ │ │ ├── _layout.tsx │ │ │ └── index.tsx │ │ ├── _layout.tsx │ │ └── index.tsx │ ├── index.tsx │ ├── discovery │ │ ├── unlock │ │ │ └── index.tsx │ │ ├── _layout.tsx │ │ └── index.tsx │ ├── chat │ │ ├── index.tsx │ │ └── _layout.tsx │ └── _layout.tsx ├── index.tsx └── _layout.tsx ├── lib ├── theme │ ├── core.ts │ ├── index.ts │ ├── components │ │ ├── StatusBarTheme.tsx │ │ ├── ThemedView.tsx │ │ ├── ThemedText.tsx │ │ └── ThemeToggle.tsx │ ├── hooks │ │ └── useThemedLayout.ts │ ├── constants.ts │ ├── context.ts │ ├── layouts │ │ ├── ThemedStackLayout.tsx │ │ └── ThemedTabLayout.tsx │ ├── provider.ts │ ├── types.ts │ ├── colors.ts │ └── README.md ├── permissions │ ├── types.ts │ ├── utils.ts │ ├── index.ts │ ├── camera.ts │ ├── location.ts │ ├── microphone.ts │ ├── photo-library.ts │ ├── tracking.ts │ ├── hooks.tsx │ ├── base.ts │ └── README.md └── storage.ts ├── nativewind-env.d.ts ├── metro.config.js ├── babel.config.js ├── tailwind.config.js ├── .gitignore ├── tsconfig.json ├── README.md ├── app.json └── package.json /components/profile/index.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_API_URL=https://ucore-staging.herokuapp.com 2 | -------------------------------------------------------------------------------- /global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/favicon.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("tailwindcss"), require("autoprefixer")], 3 | }; -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/splash-icon.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/(auth)/login/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "expo-router"; 2 | 3 | export default function Layout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /lib/theme/core.ts: -------------------------------------------------------------------------------- 1 | export * from "./context"; 2 | export * from "./colors"; 3 | export * from "./provider"; 4 | export * from "./types"; 5 | -------------------------------------------------------------------------------- /app/(auth)/register/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "expo-router"; 2 | 3 | export default function Layout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "react-native"; 2 | 3 | export default function NotFound() { 4 | return Not Found; 5 | } 6 | -------------------------------------------------------------------------------- /app/(app)/profile/settings/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "expo-router"; 2 | 3 | export default function SettingsLayout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/(app)/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from "react-native"; 2 | import { Link } from "expo-router"; 3 | 4 | export default function index() { 5 | return <>; 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielumatch/native-wind/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/(auth)/register/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from "react-native"; 2 | 3 | export default function index() { 4 | return ( 5 | 6 | Register 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Android/IntelliJ 6 | # 7 | build/ 8 | .idea 9 | .gradle 10 | local.properties 11 | *.iml 12 | *.hprof 13 | .cxx/ 14 | 15 | # Bundle artifacts 16 | *.jsbundle 17 | -------------------------------------------------------------------------------- /app/(app)/discovery/unlock/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from "react-native"; 2 | 3 | export default function index() { 4 | return ( 5 | 6 | Unlock 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | #ffffff 4 | #023c69 5 | #ffffff 6 | -------------------------------------------------------------------------------- /lib/permissions/types.ts: -------------------------------------------------------------------------------- 1 | export type PermissionType = 2 | | "camera" 3 | | "microphone" 4 | | "location" 5 | | "photoLibrary" 6 | | "tracking"; 7 | 8 | export type PermissionStatus = 9 | | "granted" 10 | | "denied" 11 | | "undetermined" 12 | | "blocked"; 13 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | const { withNativeWind } = require("nativewind/metro"); 3 | 4 | const config = getDefaultConfig(__dirname, { isCSSEnabled: true }); 5 | 6 | module.exports = withNativeWind(config, { input: "./global.css" }); -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: [ 5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 6 | "nativewind/babel", 7 | ], 8 | plugins: [ 9 | "react-native-reanimated/plugin", 10 | ] 11 | }; 12 | }; -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./app/**/*.{js,jsx,ts,tsx}"], 4 | presets: [require("nativewind/preset")], 5 | darkMode: "class", 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: "#81b0ff", 10 | }, 11 | }, 12 | }, 13 | plugins: [], 14 | }; -------------------------------------------------------------------------------- /lib/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./core"; 2 | export * from "./hooks/useThemedLayout"; 3 | export * from "./components/ThemedText"; 4 | export * from "./components/ThemeToggle"; 5 | export * from "./components/StatusBarTheme"; 6 | export * from "./components/ThemedView"; 7 | export * from "./layouts/ThemedStackLayout"; 8 | export * from "./layouts/ThemedTabLayout"; 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | nativewind 3 | automatic 4 | contain 5 | false 6 | -------------------------------------------------------------------------------- /lib/permissions/utils.ts: -------------------------------------------------------------------------------- 1 | import type { PermissionStatus } from "./types"; 2 | 3 | export function normalizeStatus(status: any): PermissionStatus { 4 | switch (status) { 5 | case "granted": 6 | return "granted"; 7 | case "denied": 8 | return "denied"; 9 | case "undetermined": 10 | return "undetermined"; 11 | default: 12 | return "blocked"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/permissions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./base"; 3 | export { cameraPermissions } from "./camera"; 4 | export { microphonePermissions } from "./microphone"; 5 | export { locationPermissions } from "./location"; 6 | export { photoLibraryPermissions } from "./photo-library"; 7 | export { trackingPermissions } from "./tracking"; 8 | 9 | // Re-export the hook 10 | export { usePermission } from "./hooks"; 11 | -------------------------------------------------------------------------------- /lib/theme/components/StatusBarTheme.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "react-native"; 2 | import { useTheme, getThemeColors } from "../core"; 3 | 4 | export function StatusBarTheme() { 5 | const { isDarkMode } = useTheme(); 6 | const colors = getThemeColors(isDarkMode); 7 | 8 | return ( 9 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | expo-env.d.ts 11 | 12 | # Native 13 | *.orig.* 14 | *.jks 15 | *.p8 16 | *.p12 17 | *.key 18 | *.mobileprovision 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | 38 | app-example 39 | -------------------------------------------------------------------------------- /lib/storage.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage"; 2 | 3 | const THEME_KEY = "@app_theme"; 4 | 5 | export const ThemeStorage = { 6 | get: async () => { 7 | try { 8 | const theme = await AsyncStorage.getItem(THEME_KEY); 9 | return theme === "dark"; 10 | } catch { 11 | return null; 12 | } 13 | }, 14 | 15 | set: async (isDark: boolean) => { 16 | try { 17 | await AsyncStorage.setItem(THEME_KEY, isDark ? "dark" : "light"); 18 | } catch { 19 | // Handle error silently 20 | } 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /app/(auth)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import { ThemedStackLayout } from "@/lib/theme/index"; 3 | 4 | export default function AuthLayout() { 5 | return ( 6 | 7 | 14 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/theme/hooks/useThemedLayout.ts: -------------------------------------------------------------------------------- 1 | import { useTheme, getThemeColors, getNavigationTheme } from "../core"; 2 | 3 | /** 4 | * Custom hook for themed layouts 5 | * Provides common theme values and configurations for layout components 6 | * @returns Theme-related values and configurations 7 | */ 8 | export function useThemedLayout() { 9 | const { isDarkMode } = useTheme(); 10 | const colors = getThemeColors(isDarkMode); 11 | const navigationTheme = getNavigationTheme(isDarkMode); 12 | 13 | return { 14 | isDarkMode, 15 | colors, 16 | navigationTheme, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": [ 8 | "./*" 9 | ] 10 | }, 11 | "moduleResolution": "node", 12 | "allowJs": true, 13 | "esModuleInterop": true, 14 | "jsx": "react-native", 15 | "resolveJsonModule": true 16 | }, 17 | "include": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | ".expo/types/**/*.ts", 21 | "expo-env.d.ts", 22 | "nativewind-env.d.ts" 23 | ], 24 | "exclude": [ 25 | "node_modules" 26 | ] 27 | } -------------------------------------------------------------------------------- /app/(app)/profile/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from "react-native"; 2 | import { useTheme, getThemeColors } from "@/lib/theme/index"; 3 | 4 | export default function Settings() { 5 | const { isDarkMode } = useTheme(); 6 | const colors = getThemeColors(isDarkMode); 7 | 8 | return ( 9 | 13 | 17 | Settings 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/(app)/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View } from "react-native"; 2 | import { useTheme, getThemeColors } from "@/lib/theme/index"; 3 | 4 | export default function Chat() { 5 | const { isDarkMode } = useTheme(); 6 | const colors = getThemeColors(isDarkMode); 7 | 8 | return ( 9 | 13 | 17 | Chat 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /app/(app)/chat/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import { useEffect } from "react"; 3 | import { usePermission } from "@/lib/permissions/hooks"; 4 | import { ThemedStackLayout } from "@/lib/theme/index"; 5 | 6 | export default function ChatLayout() { 7 | const { status, request } = usePermission("microphone"); 8 | 9 | useEffect(() => { 10 | if (status === "undetermined") { 11 | request(); 12 | } 13 | }, [status, request]); 14 | 15 | return ( 16 | 17 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/theme/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Theme color constants 3 | * Define all color values in one place for easy maintenance 4 | */ 5 | export const COLORS = { 6 | light: { 7 | background: { 8 | primary: "white", 9 | secondary: "white", 10 | tertiary: "#e5e7eb", 11 | }, 12 | text: { 13 | primary: "#1f2937", 14 | secondary: "#6b7280", 15 | }, 16 | }, 17 | dark: { 18 | background: { 19 | primary: "#111827", 20 | secondary: "#1f2937", 21 | tertiary: "#374151", 22 | }, 23 | text: { 24 | primary: "white", 25 | secondary: "#9ca3af", 26 | }, 27 | }, 28 | shared: { 29 | accent: "#81b0ff", 30 | }, 31 | } as const; 32 | -------------------------------------------------------------------------------- /app/(app)/discovery/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import { useEffect } from "react"; 3 | import { usePermission } from "@/lib/permissions/hooks"; 4 | 5 | export default function DiscoveryLayout() { 6 | const { status, request } = usePermission("location"); 7 | 8 | useEffect(() => { 9 | if (status === "undetermined") { 10 | request(); 11 | } 12 | }, [status, request]); 13 | 14 | return ( 15 | 16 | 20 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/theme/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | /** 4 | * Theme context type definition 5 | * Provides dark mode state and toggle functionality throughout the app 6 | */ 7 | type ThemeContextType = { 8 | isDarkMode: boolean; 9 | toggleTheme: () => void; 10 | }; 11 | 12 | /** 13 | * Theme context with default values 14 | * Used to provide theme state across the component tree 15 | */ 16 | export const ThemeContext = createContext({ 17 | isDarkMode: false, 18 | toggleTheme: () => {}, 19 | }); 20 | 21 | /** 22 | * Custom hook to access theme context 23 | * Use this in components that need to respond to theme changes 24 | */ 25 | export const useTheme = () => useContext(ThemeContext); 26 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "expo-router"; 2 | import { Text, View } from "react-native"; 3 | 4 | export default function Index() { 5 | return ( 6 | 7 | 8 | 9 | Root 10 | 11 | 12 | 13 | 14 | 15 | Auth 16 | 17 | 18 | 19 | 20 | 21 | App 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/theme/components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, ViewProps } from "react-native"; 2 | import { useThemedLayout } from "../hooks/useThemedLayout"; 3 | 4 | type ThemedViewProps = ViewProps & { 5 | /** Whether to use custom background color instead of theme default */ 6 | useCustomBackground?: boolean; 7 | }; 8 | 9 | export function ThemedView({ 10 | children, 11 | className = "flex-1", 12 | useCustomBackground = false, 13 | style, 14 | ...props 15 | }: ThemedViewProps) { 16 | const { colors } = useThemedLayout(); 17 | 18 | return ( 19 | 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/theme/layouts/ThemedStackLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import { ThemedView } from "../components/ThemedView"; 3 | import { PropsWithChildren } from "react"; 4 | import { useThemedLayout } from "../hooks/useThemedLayout"; 5 | 6 | type ThemedStackLayoutProps = PropsWithChildren<{ 7 | /** Additional screen options to merge with theme defaults */ 8 | screenOptions?: Partial[0]["options"]>; 9 | }>; 10 | 11 | export function ThemedStackLayout({ 12 | children, 13 | screenOptions, 14 | }: ThemedStackLayoutProps) { 15 | const { navigationTheme } = useThemedLayout(); 16 | 17 | return ( 18 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /lib/permissions/camera.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "expo-camera"; 2 | import { BasePermissionHandler } from "./base"; 3 | import type { PermissionResult } from "./base"; 4 | import { normalizeStatus } from "./utils"; 5 | 6 | export class CameraPermissionHandler extends BasePermissionHandler { 7 | protected type = "camera"; 8 | 9 | protected async checkNativePermission(): Promise { 10 | const { status } = await Camera.getCameraPermissionsAsync(); 11 | return { 12 | status: normalizeStatus(status), 13 | canAskAgain: status !== "denied", 14 | }; 15 | } 16 | 17 | protected async requestNativePermission(): Promise { 18 | const { status } = await Camera.requestCameraPermissionsAsync(); 19 | return { 20 | status: normalizeStatus(status), 21 | canAskAgain: status !== "denied", 22 | }; 23 | } 24 | } 25 | 26 | export const cameraPermissions = new CameraPermissionHandler(); 27 | -------------------------------------------------------------------------------- /lib/permissions/location.ts: -------------------------------------------------------------------------------- 1 | import * as Location from "expo-location"; 2 | import { BasePermissionHandler } from "./base"; 3 | import type { PermissionResult } from "./base"; 4 | import { normalizeStatus } from "./utils"; 5 | 6 | export class LocationPermissionHandler extends BasePermissionHandler { 7 | protected type = "location"; 8 | 9 | protected async checkNativePermission(): Promise { 10 | const { status } = await Location.getForegroundPermissionsAsync(); 11 | return { 12 | status: normalizeStatus(status), 13 | canAskAgain: status !== "denied", 14 | }; 15 | } 16 | 17 | protected async requestNativePermission(): Promise { 18 | const { status } = await Location.requestForegroundPermissionsAsync(); 19 | return { 20 | status: normalizeStatus(status), 21 | canAskAgain: status !== "denied", 22 | }; 23 | } 24 | } 25 | 26 | export const locationPermissions = new LocationPermissionHandler(); 27 | -------------------------------------------------------------------------------- /lib/permissions/microphone.ts: -------------------------------------------------------------------------------- 1 | import { Camera } from "expo-camera"; 2 | import { BasePermissionHandler } from "./base"; 3 | import type { PermissionResult } from "./base"; 4 | import { normalizeStatus } from "./utils"; 5 | 6 | export class MicrophonePermissionHandler extends BasePermissionHandler { 7 | protected type = "microphone"; 8 | 9 | protected async checkNativePermission(): Promise { 10 | const { status } = await Camera.getMicrophonePermissionsAsync(); 11 | return { 12 | status: normalizeStatus(status), 13 | canAskAgain: status !== "denied", 14 | }; 15 | } 16 | 17 | protected async requestNativePermission(): Promise { 18 | const { status } = await Camera.requestMicrophonePermissionsAsync(); 19 | return { 20 | status: normalizeStatus(status), 21 | canAskAgain: status !== "denied", 22 | }; 23 | } 24 | } 25 | 26 | export const microphonePermissions = new MicrophonePermissionHandler(); 27 | -------------------------------------------------------------------------------- /lib/permissions/photo-library.ts: -------------------------------------------------------------------------------- 1 | import * as MediaLibrary from "expo-media-library"; 2 | import { BasePermissionHandler } from "./base"; 3 | import type { PermissionResult } from "./base"; 4 | import { normalizeStatus } from "./utils"; 5 | 6 | export class PhotoLibraryPermissionHandler extends BasePermissionHandler { 7 | protected type = "photoLibrary"; 8 | 9 | protected async checkNativePermission(): Promise { 10 | const { status } = await MediaLibrary.getPermissionsAsync(); 11 | return { 12 | status: normalizeStatus(status), 13 | canAskAgain: status !== "denied", 14 | }; 15 | } 16 | 17 | protected async requestNativePermission(): Promise { 18 | const { status } = await MediaLibrary.requestPermissionsAsync(); 19 | return { 20 | status: normalizeStatus(status), 21 | canAskAgain: status !== "denied", 22 | }; 23 | } 24 | } 25 | 26 | export const photoLibraryPermissions = new PhotoLibraryPermissionHandler(); 27 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 19 | -------------------------------------------------------------------------------- /app/(app)/profile/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import { useEffect } from "react"; 3 | import { usePermission } from "@/lib/permissions/hooks"; 4 | import { ThemedStackLayout } from "@/lib/theme/index"; 5 | 6 | export default function ProfileLayout() { 7 | const camera = usePermission("camera"); 8 | const gallery = usePermission("photoLibrary"); 9 | 10 | useEffect(() => { 11 | // Request camera permission if not determined 12 | if (camera.status === "undetermined") { 13 | camera.request(); 14 | } 15 | // Request gallery permission if not determined 16 | if (gallery.status === "undetermined") { 17 | gallery.request(); 18 | } 19 | }, [camera.status, gallery.status]); 20 | 21 | return ( 22 | 23 | 30 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /lib/theme/components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { Text, TextProps } from "react-native"; 2 | import { useThemedLayout } from "../hooks/useThemedLayout"; 3 | 4 | type ThemedTextVariant = "primary" | "secondary"; 5 | 6 | type ThemedTextProps = Omit & { 7 | /** Text color variant */ 8 | variant?: ThemedTextVariant; 9 | }; 10 | 11 | /** 12 | * Themed Text Component 13 | * Provides consistent text styling with theme awareness 14 | * 15 | * @example 16 | * ```tsx 17 | * // Primary text (default) 18 | * Hello World 19 | * 20 | * // Secondary text 21 | * Subtitle 22 | * 23 | * // With custom className 24 | * Bold Text 25 | * ``` 26 | */ 27 | export function ThemedText({ 28 | variant = "primary", 29 | className = "", 30 | children, 31 | ...props 32 | }: ThemedTextProps) { 33 | const { colors } = useThemedLayout(); 34 | 35 | return ( 36 | 41 | {children} 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/(app)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import FontAwesome from "@expo/vector-icons/FontAwesome"; 2 | import { ThemedTabLayout } from "@/lib/theme/index"; 3 | import { Tabs } from "expo-router"; 4 | 5 | export default function AppLayout() { 6 | return ( 7 | 8 | ( 13 | 14 | ), 15 | }} 16 | /> 17 | ( 22 | 23 | ), 24 | }} 25 | /> 26 | ( 31 | 32 | ), 33 | headerShown: false, 34 | }} 35 | /> 36 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/(app)/discovery/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "expo-router"; 2 | import { Text, View, Pressable } from "react-native"; 3 | import { useTheme, getThemeColors } from "@/lib/theme/index"; 4 | 5 | export default function Discovery() { 6 | const { isDarkMode } = useTheme(); 7 | const colors = getThemeColors(isDarkMode); 8 | 9 | return ( 10 | 14 | 18 | App 19 | 20 | 21 | 22 | 26 | Discovery 27 | 28 | 29 | 30 | 31 | 35 | Unlock 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/(auth)/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "expo-router"; 2 | import { Text, View } from "react-native"; 3 | 4 | export default function AuthIndex() { 5 | return ( 6 | 7 | 8 | 9 | 10 | Welcome Back 11 | 12 | 13 | Please sign in to continue 14 | 15 | 16 | 17 | 18 | 22 | Login 23 | 24 | 25 | 29 | 30 | Create Account 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /lib/permissions/tracking.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | import { BasePermissionHandler } from "./base"; 3 | import type { PermissionResult } from "./base"; 4 | import { normalizeStatus } from "./utils"; 5 | 6 | // Only import tracking transparency on iOS 7 | const TrackingTransparency = 8 | Platform.OS === "ios" ? require("expo-tracking-transparency") : null; 9 | 10 | export class TrackingPermissionHandler extends BasePermissionHandler { 11 | protected type = "tracking"; 12 | 13 | protected async checkNativePermission(): Promise { 14 | if (Platform.OS === "ios" && TrackingTransparency) { 15 | const status = await TrackingTransparency.getTrackingPermissionsAsync(); 16 | return { 17 | status: normalizeStatus(status), 18 | canAskAgain: status !== "denied", 19 | }; 20 | } 21 | return { status: "granted", canAskAgain: true }; 22 | } 23 | 24 | protected async requestNativePermission(): Promise { 25 | if (Platform.OS === "ios" && TrackingTransparency) { 26 | const status = 27 | await TrackingTransparency.requestTrackingPermissionsAsync(); 28 | return { 29 | status: normalizeStatus(status), 30 | canAskAgain: status !== "denied", 31 | }; 32 | } 33 | return { status: "granted", canAskAgain: true }; 34 | } 35 | } 36 | 37 | export const trackingPermissions = new TrackingPermissionHandler(); 38 | -------------------------------------------------------------------------------- /lib/theme/layouts/ThemedTabLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from "expo-router"; 2 | import { ThemedView } from "../components/ThemedView"; 3 | import { PropsWithChildren } from "react"; 4 | import { useThemedLayout } from "../hooks/useThemedLayout"; 5 | 6 | type ThemedTabLayoutProps = PropsWithChildren<{ 7 | /** Additional screen options to merge with theme defaults */ 8 | screenOptions?: Partial[0]["options"]>; 9 | }>; 10 | 11 | export function ThemedTabLayout({ 12 | children, 13 | screenOptions, 14 | }: ThemedTabLayoutProps) { 15 | const { colors } = useThemedLayout(); 16 | 17 | return ( 18 | 19 | 38 | {children} 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /lib/theme/provider.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useColorScheme } from "nativewind"; 3 | import { ThemeStorage } from "../storage"; 4 | 5 | /** 6 | * Theme provider hook that manages theme state 7 | * Handles: 8 | * - Theme persistence with storage 9 | * - NativeWind color scheme sync 10 | * - Theme toggle functionality 11 | */ 12 | export const useThemeProvider = () => { 13 | const { colorScheme, toggleColorScheme, setColorScheme } = useColorScheme(); 14 | const [isDarkMode, setIsDarkMode] = useState(colorScheme === "dark"); 15 | const [isThemeLoaded, setIsThemeLoaded] = useState(false); 16 | 17 | // Load saved theme on startup 18 | useEffect(() => { 19 | ThemeStorage.get().then((savedTheme) => { 20 | if (savedTheme !== null) { 21 | setIsDarkMode(savedTheme); 22 | setColorScheme(savedTheme ? "dark" : "light"); 23 | } 24 | setIsThemeLoaded(true); 25 | }); 26 | }, []); 27 | 28 | // Update NativeWind color scheme when theme changes 29 | useEffect(() => { 30 | if (isThemeLoaded) { 31 | setColorScheme(isDarkMode ? "dark" : "light"); 32 | } 33 | }, [isDarkMode, isThemeLoaded]); 34 | 35 | const toggleTheme = () => { 36 | setIsDarkMode((prev) => { 37 | const newValue = !prev; 38 | ThemeStorage.set(newValue); 39 | toggleColorScheme(); 40 | return newValue; 41 | }); 42 | }; 43 | 44 | return { 45 | isDarkMode, 46 | toggleTheme, 47 | isThemeLoaded, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/theme/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { View, Switch, ViewProps } from "react-native"; 2 | import { useTheme } from "../core"; 3 | import { useThemedLayout } from "../hooks/useThemedLayout"; 4 | import { ThemedText } from "./ThemedText"; 5 | 6 | type ThemeToggleProps = Omit & { 7 | /** Show label next to the toggle */ 8 | showLabel?: boolean; 9 | /** Custom label text */ 10 | label?: string; 11 | }; 12 | 13 | /** 14 | * Theme Toggle Component 15 | * A reusable switch component for toggling between light and dark themes 16 | * 17 | * @example 18 | * ```tsx 19 | * // Basic usage 20 | * 21 | * 22 | * // With custom label 23 | * 24 | * ``` 25 | */ 26 | export function ThemeToggle({ 27 | showLabel = true, 28 | label = "Dark Mode", 29 | className = "flex-row items-center justify-between p-4", 30 | ...props 31 | }: ThemeToggleProps) { 32 | const { isDarkMode, toggleTheme } = useTheme(); 33 | const { colors } = useThemedLayout(); 34 | 35 | return ( 36 | 37 | {showLabel && ( 38 | {label} 39 | )} 40 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/(app)/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text, View, Switch } from "react-native"; 2 | import { useTheme, getThemeColors } from "@/lib/theme/index"; 3 | import { Link } from "expo-router"; 4 | 5 | export default function Profile() { 6 | const { isDarkMode, toggleTheme } = useTheme(); 7 | const colors = getThemeColors(isDarkMode); 8 | 9 | return ( 10 | 14 | 18 | 22 | Dark Mode 23 | 24 | 33 | 34 | 35 | 36 | 37 | Toggle between light and dark mode 38 | 39 | 40 | 41 | 42 | Settings 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString()) 3 | } 4 | plugins { id("com.facebook.react.settings") } 5 | 6 | extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> 7 | if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { 8 | ex.autolinkLibrariesFromCommand() 9 | } else { 10 | def command = [ 11 | 'node', 12 | '--no-warnings', 13 | '--eval', 14 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', 15 | 'react-native-config', 16 | '--json', 17 | '--platform', 18 | 'android' 19 | ].toList() 20 | ex.autolinkLibrariesFromCommand(command) 21 | } 22 | } 23 | 24 | rootProject.name = 'nativewind' 25 | 26 | dependencyResolutionManagement { 27 | versionCatalogs { 28 | reactAndroidLibs { 29 | from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml"))) 30 | } 31 | } 32 | } 33 | 34 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); 35 | useExpoModules() 36 | 37 | include ':app' 38 | includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) 39 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0' 6 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24') 7 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35') 8 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') 9 | kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.24' 10 | 11 | ndkVersion = "26.1.10909125" 12 | } 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | dependencies { 18 | classpath('com.android.tools.build:gradle') 19 | classpath('com.facebook.react:react-native-gradle-plugin') 20 | classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') 21 | } 22 | } 23 | 24 | apply plugin: "com.facebook.react.rootproject" 25 | 26 | allprojects { 27 | repositories { 28 | maven { 29 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 30 | url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) 31 | } 32 | maven { 33 | // Android JSC is installed from npm 34 | url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) 35 | } 36 | 37 | google() 38 | mavenCentral() 39 | maven { url 'https://www.jitpack.io' } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | import "@/global.css"; 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { View } from "react-native"; 5 | import { 6 | getNavigationTheme, 7 | getThemeColors, 8 | ThemeContext, 9 | useThemeProvider, 10 | StatusBarTheme, 11 | } from "@/lib/theme/index"; 12 | 13 | const queryClient = new QueryClient(); 14 | 15 | export default function RootLayout() { 16 | const { isDarkMode, toggleTheme, isThemeLoaded } = useThemeProvider(); 17 | 18 | const colors = getThemeColors(isDarkMode); 19 | const navigationTheme = getNavigationTheme(isDarkMode); 20 | 21 | if (!isThemeLoaded) { 22 | return null; 23 | } 24 | 25 | return ( 26 | 27 | 28 | 32 | 33 | 41 | 42 | 43 | 50 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Expo app 👋 2 | 3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Start the app 14 | 15 | ```bash 16 | npx expo start 17 | ``` 18 | 19 | In the output, you'll find options to open the app in a 20 | 21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 | 26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 | 28 | ## Get a fresh project 29 | 30 | When you're ready, run: 31 | 32 | ```bash 33 | npm run reset-project 34 | ``` 35 | 36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 | 38 | ## Learn more 39 | 40 | To learn more about developing your project with Expo, look at the following resources: 41 | 42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 44 | 45 | ## Join the community 46 | 47 | Join our community of developers creating universal apps. 48 | 49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 51 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "nativewind", 4 | "slug": "nativewind", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "ios": { 12 | "supportsTablet": true, 13 | "infoPlist": { 14 | "NSCameraUsageDescription": "This app needs access to the camera to take photos.", 15 | "NSMicrophoneUsageDescription": "This app needs access to the microphone for recording audio.", 16 | "NSLocationWhenInUseUsageDescription": "This app needs access to location when open to show relevant content.", 17 | "NSPhotoLibraryUsageDescription": "This app needs access to your photo library to save and share photos.", 18 | "NSUserTrackingUsageDescription": "This app uses tracking data to provide personalized content and improve your experience." 19 | } 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/images/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | }, 26 | "package": "com.nativewind.app", 27 | "permissions": [ 28 | "CAMERA", 29 | "RECORD_AUDIO", 30 | "ACCESS_COARSE_LOCATION", 31 | "ACCESS_FINE_LOCATION", 32 | "READ_EXTERNAL_STORAGE", 33 | "WRITE_EXTERNAL_STORAGE" 34 | ] 35 | }, 36 | "web": { 37 | "bundler": "metro", 38 | "output": "static", 39 | "favicon": "./assets/images/favicon.png" 40 | }, 41 | "plugins": [ 42 | "expo-router", 43 | [ 44 | "expo-splash-screen", 45 | { 46 | "image": "./assets/images/splash-icon.png", 47 | "imageWidth": 200, 48 | "resizeMode": "contain", 49 | "backgroundColor": "#ffffff" 50 | } 51 | ], 52 | "expo-camera", 53 | "expo-location", 54 | "expo-media-library", 55 | "expo-tracking-transparency" 56 | ], 57 | "experiments": { 58 | "typedRoutes": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nativewind", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start", 7 | "reset-project": "node ./scripts/reset-project.js", 8 | "android": "expo run:android", 9 | "ios": "expo run:ios", 10 | "web": "expo start --web", 11 | "test": "jest --watchAll", 12 | "lint": "expo lint" 13 | }, 14 | "jest": { 15 | "preset": "jest-expo" 16 | }, 17 | "dependencies": { 18 | "@babel/plugin-transform-react-jsx-source": "^7.25.9", 19 | "@expo/vector-icons": "^14.0.2", 20 | "@react-native-async-storage/async-storage": "^2.1.0", 21 | "@react-navigation/bottom-tabs": "^7.2.0", 22 | "@react-navigation/native": "^7.0.14", 23 | "@tanstack/react-query": "^5.65.0", 24 | "autoprefixer": "^10.4.20", 25 | "axios": "^1.7.9", 26 | "expo": "~52.0.27", 27 | "expo-blur": "~14.0.3", 28 | "expo-camera": "^16.0.14", 29 | "expo-constants": "~17.0.4", 30 | "expo-dev-client": "~5.0.10", 31 | "expo-font": "~13.0.3", 32 | "expo-haptics": "~14.0.1", 33 | "expo-linking": "~7.0.4", 34 | "expo-location": "^18.0.5", 35 | "expo-media-library": "^17.0.5", 36 | "expo-router": "~4.0.17", 37 | "expo-splash-screen": "~0.29.21", 38 | "expo-status-bar": "~2.0.1", 39 | "expo-symbols": "~0.2.1", 40 | "expo-system-ui": "~4.0.7", 41 | "expo-tracking-transparency": "^5.1.1", 42 | "expo-web-browser": "~14.0.2", 43 | "nativewind": "^4.0.1", 44 | "postcss": "^8.5.1", 45 | "react": "18.3.1", 46 | "react-dom": "18.3.1", 47 | "react-native": "0.76.6", 48 | "react-native-gesture-handler": "~2.20.2", 49 | "react-native-reanimated": "~3.16.1", 50 | "react-native-safe-area-context": "4.12.0", 51 | "react-native-screens": "~4.4.0", 52 | "react-native-web": "~0.19.13", 53 | "react-native-webview": "13.12.5" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.25.2", 57 | "@types/jest": "^29.5.12", 58 | "@types/react": "~18.3.12", 59 | "@types/react-test-renderer": "^18.3.0", 60 | "jest": "^29.2.1", 61 | "jest-expo": "~52.0.3", 62 | "react-test-renderer": "18.3.1", 63 | "tailwindcss": "^3.3.2", 64 | "typescript": "^5.3.3" 65 | }, 66 | "private": true 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/nativewind/app/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.nativewind.app 2 | 3 | import android.app.Application 4 | import android.content.res.Configuration 5 | 6 | import com.facebook.react.PackageList 7 | import com.facebook.react.ReactApplication 8 | import com.facebook.react.ReactNativeHost 9 | import com.facebook.react.ReactPackage 10 | import com.facebook.react.ReactHost 11 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 12 | import com.facebook.react.defaults.DefaultReactNativeHost 13 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 14 | import com.facebook.soloader.SoLoader 15 | 16 | import expo.modules.ApplicationLifecycleDispatcher 17 | import expo.modules.ReactNativeHostWrapper 18 | 19 | class MainApplication : Application(), ReactApplication { 20 | 21 | override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( 22 | this, 23 | object : DefaultReactNativeHost(this) { 24 | override fun getPackages(): List { 25 | val packages = PackageList(this).packages 26 | // Packages that cannot be autolinked yet can be added manually here, for example: 27 | // packages.add(new MyReactNativePackage()); 28 | return packages 29 | } 30 | 31 | override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" 32 | 33 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 34 | 35 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 36 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 37 | } 38 | ) 39 | 40 | override val reactHost: ReactHost 41 | get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) 42 | 43 | override fun onCreate() { 44 | super.onCreate() 45 | SoLoader.init(this, OpenSourceMergedSoMapping) 46 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 47 | // If you opted-in for the New Architecture, we load the native entry point for this app. 48 | load() 49 | } 50 | ApplicationLifecycleDispatcher.onApplicationCreate(this) 51 | } 52 | 53 | override fun onConfigurationChanged(newConfig: Configuration) { 54 | super.onConfigurationChanged(newConfig) 55 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/permissions/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback, useRef } from "react"; 2 | import type { PermissionType, PermissionStatus } from "./types"; 3 | import { cameraPermissions } from "./camera"; 4 | import { microphonePermissions } from "./microphone"; 5 | import { locationPermissions } from "./location"; 6 | import { photoLibraryPermissions } from "./photo-library"; 7 | import { trackingPermissions } from "./tracking"; 8 | 9 | const permissionHandlers = { 10 | camera: cameraPermissions, 11 | microphone: microphonePermissions, 12 | location: locationPermissions, 13 | photoLibrary: photoLibraryPermissions, 14 | tracking: trackingPermissions, 15 | }; 16 | 17 | export function usePermission(type: PermissionType) { 18 | const [status, setStatus] = useState("undetermined"); 19 | const [isLoading, setIsLoading] = useState(true); 20 | const [canAskAgain, setCanAskAgain] = useState(true); 21 | const isMounted = useRef(false); 22 | 23 | useEffect(() => { 24 | isMounted.current = true; 25 | checkPermissionStatus(); 26 | return () => { 27 | isMounted.current = false; 28 | }; 29 | }, []); 30 | 31 | const checkPermissionStatus = useCallback(async () => { 32 | try { 33 | setIsLoading(true); 34 | const result = await permissionHandlers[type].check(); 35 | if (isMounted.current) { 36 | setStatus(result.status); 37 | setCanAskAgain(result.canAskAgain); 38 | } 39 | } catch (error) { 40 | console.error(`Error checking ${type} permission:`, error); 41 | } finally { 42 | if (isMounted.current) { 43 | setIsLoading(false); 44 | } 45 | } 46 | }, [type]); 47 | 48 | const request = useCallback(async () => { 49 | try { 50 | setIsLoading(true); 51 | const result = await permissionHandlers[type].request(); 52 | if (isMounted.current) { 53 | setStatus(result.status); 54 | setCanAskAgain(result.canAskAgain); 55 | } 56 | return result; 57 | } catch (error) { 58 | console.error(`Error requesting ${type} permission:`, error); 59 | throw error; 60 | } finally { 61 | if (isMounted.current) { 62 | setIsLoading(false); 63 | } 64 | } 65 | }, [type]); 66 | 67 | return { 68 | status, 69 | isLoading, 70 | canAskAgain, 71 | request, 72 | check: checkPermissionStatus, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Enable AAPT2 PNG crunching 26 | android.enablePngCrunchInReleaseBuilds=true 27 | 28 | # Use this property to specify which architecture you want to build. 29 | # You can also override it from the CLI using 30 | # ./gradlew -PreactNativeArchitectures=x86_64 31 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 32 | 33 | # Use this property to enable support to the new architecture. 34 | # This will allow you to use TurboModules and the Fabric render in 35 | # your application. You should enable this flag either if you want 36 | # to write custom TurboModules/Fabric components OR use libraries that 37 | # are providing them. 38 | newArchEnabled=true 39 | 40 | # Use this property to enable or disable the Hermes JS engine. 41 | # If set to false, you will be using JSC instead. 42 | hermesEnabled=true 43 | 44 | # Enable GIF support in React Native images (~200 B increase) 45 | expo.gif.enabled=true 46 | # Enable webp support in React Native images (~85 KB increase) 47 | expo.webp.enabled=true 48 | # Enable animated webp support (~3.4 MB increase) 49 | # Disabled by default because iOS doesn't support animated webp 50 | expo.webp.animated=false 51 | 52 | # Enable network inspector 53 | EX_DEV_CLIENT_NETWORK_INSPECTOR=true 54 | 55 | # Use legacy packaging to compress native libraries in the resulting APK. 56 | expo.useLegacyPackaging=false 57 | -------------------------------------------------------------------------------- /lib/theme/types.ts: -------------------------------------------------------------------------------- 1 | import { TextProps, ViewProps } from "react-native"; 2 | import { Stack, Tabs } from "expo-router"; 3 | 4 | /** 5 | * Theme context type definition 6 | * Provides dark mode state and toggle functionality throughout the app 7 | */ 8 | export type ThemeContextType = { 9 | /** Current theme mode */ 10 | isDarkMode: boolean; 11 | /** Function to toggle between light and dark mode */ 12 | toggleTheme: () => void; 13 | }; 14 | 15 | /** 16 | * Theme colors type definition 17 | * Defines the structure of the app's color palette 18 | */ 19 | export type ThemeColors = { 20 | background: { 21 | /** Main background color */ 22 | primary: string; 23 | /** Secondary background for cards, headers */ 24 | secondary: string; 25 | /** Tertiary background for borders, dividers */ 26 | tertiary: string; 27 | }; 28 | text: { 29 | /** Primary text color */ 30 | primary: string; 31 | /** Secondary text for subtitles, captions */ 32 | secondary: string; 33 | }; 34 | statusBar: { 35 | /** Status bar content style */ 36 | style: "light-content" | "dark-content"; 37 | /** Status bar background color */ 38 | backgroundColor: string; 39 | }; 40 | navigation: { 41 | tabBar: { 42 | /** Tab bar background color */ 43 | background: string; 44 | /** Tab bar border color */ 45 | border: string; 46 | /** Active tab color */ 47 | activeColor: string; 48 | /** Inactive tab color */ 49 | inactiveColor: string; 50 | }; 51 | header: { 52 | /** Header background color */ 53 | background: string; 54 | /** Header text color */ 55 | text: string; 56 | }; 57 | }; 58 | }; 59 | 60 | /** 61 | * Props for themed stack layout 62 | */ 63 | export type ThemedStackLayoutProps = { 64 | /** Additional screen options to merge with theme defaults */ 65 | screenOptions?: Partial[0]["options"]>; 66 | children: React.ReactNode; 67 | }; 68 | 69 | /** 70 | * Props for themed tab layout 71 | */ 72 | export type ThemedTabLayoutProps = { 73 | /** Additional screen options to merge with theme defaults */ 74 | screenOptions?: Partial[0]["options"]>; 75 | children: React.ReactNode; 76 | }; 77 | 78 | /** 79 | * Props for themed view component 80 | */ 81 | export type ThemedViewProps = Omit & { 82 | /** Whether to use custom background color instead of theme default */ 83 | useCustomBackground?: boolean; 84 | }; 85 | 86 | /** 87 | * Props for themed text component 88 | */ 89 | export type ThemedTextProps = Omit & { 90 | /** Text color variant */ 91 | variant?: "primary" | "secondary"; 92 | }; 93 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/nativewind/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nativewind.app 2 | import expo.modules.splashscreen.SplashScreenManager 3 | 4 | import android.os.Build 5 | import android.os.Bundle 6 | 7 | import com.facebook.react.ReactActivity 8 | import com.facebook.react.ReactActivityDelegate 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 10 | import com.facebook.react.defaults.DefaultReactActivityDelegate 11 | 12 | import expo.modules.ReactActivityDelegateWrapper 13 | 14 | class MainActivity : ReactActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | // Set the theme to AppTheme BEFORE onCreate to support 17 | // coloring the background, status bar, and navigation bar. 18 | // This is required for expo-splash-screen. 19 | // setTheme(R.style.AppTheme); 20 | // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af 21 | SplashScreenManager.registerOnActivity(this) 22 | // @generated end expo-splashscreen 23 | super.onCreate(null) 24 | } 25 | 26 | /** 27 | * Returns the name of the main component registered from JavaScript. This is used to schedule 28 | * rendering of the component. 29 | */ 30 | override fun getMainComponentName(): String = "main" 31 | 32 | /** 33 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 34 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 35 | */ 36 | override fun createReactActivityDelegate(): ReactActivityDelegate { 37 | return ReactActivityDelegateWrapper( 38 | this, 39 | BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, 40 | object : DefaultReactActivityDelegate( 41 | this, 42 | mainComponentName, 43 | fabricEnabled 44 | ){}) 45 | } 46 | 47 | /** 48 | * Align the back button behavior with Android S 49 | * where moving root activities to background instead of finishing activities. 50 | * @see onBackPressed 51 | */ 52 | override fun invokeDefaultOnBackPressed() { 53 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 54 | if (!moveTaskToBack(false)) { 55 | // For non-root activities, use the default implementation to finish them. 56 | super.invokeDefaultOnBackPressed() 57 | } 58 | return 59 | } 60 | 61 | // Use the default back button implementation on Android S 62 | // because it's doing more than [Activity.moveTaskToBack] in fact. 63 | super.invokeDefaultOnBackPressed() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/theme/colors.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | import { ThemeColors } from "./types"; 3 | import { COLORS } from "./constants"; 4 | 5 | /** 6 | * Core theme colors configuration 7 | * Provides consistent color palette across the app based on theme mode 8 | * 9 | * @param isDarkMode - Current theme mode 10 | * @returns Theme color object with all color values 11 | */ 12 | export const getThemeColors = (isDarkMode: boolean): ThemeColors => { 13 | const mode = isDarkMode ? "dark" : "light"; 14 | const colors = COLORS[mode]; 15 | 16 | return { 17 | background: colors.background, 18 | text: colors.text, 19 | statusBar: { 20 | style: isDarkMode ? "light-content" : "dark-content", 21 | backgroundColor: colors.background.primary, 22 | }, 23 | navigation: { 24 | tabBar: { 25 | background: colors.background.secondary, 26 | border: colors.background.tertiary, 27 | activeColor: COLORS.shared.accent, 28 | inactiveColor: colors.text.secondary, 29 | }, 30 | header: { 31 | background: colors.background.secondary, 32 | text: colors.text.primary, 33 | }, 34 | }, 35 | }; 36 | }; 37 | 38 | /** 39 | * Navigation theme configuration for stack navigators 40 | * Provides consistent styling for headers and navigation elements 41 | * 42 | * @param isDarkMode - Current theme mode 43 | */ 44 | export const getNavigationTheme = (isDarkMode: boolean) => { 45 | const colors = getThemeColors(isDarkMode); 46 | return { 47 | headerStyle: { 48 | backgroundColor: colors.navigation.header.background, 49 | }, 50 | headerTintColor: colors.navigation.header.text, 51 | headerTitleStyle: { 52 | color: colors.navigation.header.text, 53 | }, 54 | contentStyle: { 55 | backgroundColor: colors.background.primary, 56 | }, 57 | }; 58 | }; 59 | 60 | /** 61 | * Tab bar theme configuration 62 | * Provides consistent styling for bottom tab navigation 63 | * 64 | * @param isDarkMode - Current theme mode 65 | */ 66 | export const getTabBarTheme = (isDarkMode: boolean) => { 67 | const colors = getThemeColors(isDarkMode); 68 | return { 69 | headerShown: false, 70 | tabBarStyle: { 71 | backgroundColor: colors.navigation.tabBar.background, 72 | borderTopColor: colors.navigation.tabBar.border, 73 | }, 74 | tabBarActiveTintColor: colors.navigation.tabBar.activeColor, 75 | tabBarInactiveTintColor: colors.navigation.tabBar.inactiveColor, 76 | }; 77 | }; 78 | 79 | /** 80 | * Status bar theme configuration 81 | * Handles platform-specific status bar styling 82 | * 83 | * @param isDarkMode - Current theme mode 84 | */ 85 | export const getStatusBarConfig = (isDarkMode: boolean) => { 86 | const colors = getThemeColors(isDarkMode); 87 | return { 88 | barStyle: colors.statusBar.style, 89 | backgroundColor: 90 | Platform.OS === "android" ? colors.statusBar.backgroundColor : undefined, 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /lib/permissions/base.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | import { PermissionStatus } from "./types"; 4 | 5 | const PERMISSION_CACHE_PREFIX = "@app_permissions:"; 6 | const isWeb = Platform.OS === "web"; 7 | 8 | export interface PermissionResult { 9 | status: PermissionStatus; 10 | canAskAgain: boolean; 11 | } 12 | 13 | export abstract class BasePermissionHandler { 14 | protected abstract type: string; 15 | private cache: Map; 16 | 17 | constructor() { 18 | this.cache = new Map(); 19 | if (!isWeb) { 20 | this.loadCachedPermission(); 21 | } 22 | } 23 | 24 | protected abstract checkNativePermission(): Promise; 25 | protected abstract requestNativePermission(): Promise; 26 | 27 | private async loadCachedPermission() { 28 | if (isWeb) return; 29 | 30 | try { 31 | const value = await AsyncStorage.getItem( 32 | `${PERMISSION_CACHE_PREFIX}${this.type}` 33 | ); 34 | if (value) { 35 | this.cache.set(this.type, JSON.parse(value)); 36 | } 37 | } catch (error) { 38 | console.error(`Error loading cached ${this.type} permission:`, error); 39 | } 40 | } 41 | 42 | private async cachePermissionResult(result: PermissionResult) { 43 | if (isWeb) return; 44 | 45 | try { 46 | await AsyncStorage.setItem( 47 | `${PERMISSION_CACHE_PREFIX}${this.type}`, 48 | JSON.stringify(result) 49 | ); 50 | this.cache.set(this.type, result); 51 | } catch (error) { 52 | console.error(`Error caching ${this.type} permission result:`, error); 53 | } 54 | } 55 | 56 | protected getWebPermissionResult(): PermissionResult { 57 | switch (this.type) { 58 | case "camera": 59 | case "microphone": 60 | return { status: "undetermined", canAskAgain: true }; 61 | default: 62 | return { status: "granted", canAskAgain: false }; 63 | } 64 | } 65 | 66 | public async check(): Promise { 67 | if (isWeb) { 68 | return this.getWebPermissionResult(); 69 | } 70 | 71 | const cached = this.cache.get(this.type); 72 | if (cached) return cached; 73 | 74 | const result = await this.checkNativePermission(); 75 | await this.cachePermissionResult(result); 76 | return result; 77 | } 78 | 79 | public async request(): Promise { 80 | if (isWeb) { 81 | return this.getWebPermissionResult(); 82 | } 83 | 84 | const currentStatus = await this.check(); 85 | 86 | if (currentStatus.status === "granted") { 87 | return currentStatus; 88 | } 89 | 90 | if (currentStatus.status === "blocked" || !currentStatus.canAskAgain) { 91 | return currentStatus; 92 | } 93 | 94 | const result = await this.requestNativePermission(); 95 | await this.cachePermissionResult(result); 96 | return result; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /lib/theme/README.md: -------------------------------------------------------------------------------- 1 | # Theme System 2 | 3 | A comprehensive theme system for React Native applications with dark mode support. 4 | 5 | ## Structure 6 | 7 | ``` 8 | lib/theme/ 9 | ├── constants.ts # Theme-related constants (colors, sizes) 10 | ├── types.ts # TypeScript type definitions 11 | ├── colors.ts # Color utilities and theme functions 12 | ├── context.ts # Theme context and basic hook 13 | ├── provider.ts # Theme provider implementation 14 | ├── components/ # Themed UI components 15 | ├── layouts/ # Themed navigation layouts 16 | └── hooks/ # Theme-related hooks 17 | ``` 18 | 19 | ## Core Features 20 | 21 | ### Theme Provider 22 | 23 | Manages theme state with persistence and NativeWind integration: 24 | 25 | ```tsx 26 | function App() { 27 | const { isDarkMode, toggleTheme } = useThemeProvider(); 28 | return ( 29 | 30 | 31 | 32 | ); 33 | } 34 | ``` 35 | 36 | ### Components 37 | 38 | 1. Base Components: 39 | 40 | ```tsx 41 | // Themed View with background 42 | 43 | {/* Content */} 44 | 45 | 46 | // Themed Text with variants 47 | 48 | Subtitle text 49 | 50 | ``` 51 | 52 | 2. UI Components: 53 | 54 | ```tsx 55 | // Theme toggle switch 56 | 57 | 58 | // Status bar with theme awareness 59 | 60 | ``` 61 | 62 | 3. Navigation Layouts: 63 | 64 | ```tsx 65 | // Stack Navigation 66 | 67 | 68 | 69 | 70 | // Tab Navigation 71 | 72 | 73 | 74 | ``` 75 | 76 | ## Theme Hook Usage 77 | 78 | ```tsx 79 | function MyComponent() { 80 | // Basic theme state 81 | const { isDarkMode, toggleTheme } = useTheme(); 82 | 83 | // Layout utilities 84 | const { colors, navigationTheme } = useThemedLayout(); 85 | 86 | return ( 87 | 88 | Current theme: {isDarkMode ? "Dark" : "Light"} 89 | 90 | ); 91 | } 92 | ``` 93 | 94 | ## Color System 95 | 96 | Colors are defined in `constants.ts` and organized by theme mode: 97 | 98 | - Background colors (primary, secondary, tertiary) 99 | - Text colors (primary, secondary) 100 | - Navigation colors (header, tab bar) 101 | - Status bar colors 102 | - Shared colors (accent) 103 | 104 | ## Best Practices 105 | 106 | 1. Component Usage: 107 | 108 | - Use themed components instead of raw React Native components 109 | - Leverage NativeWind classes for responsive styling 110 | - Use variants for consistent text styling 111 | 112 | 2. Theme Access: 113 | 114 | - Use `useTheme()` for simple theme state 115 | - Use `useThemedLayout()` for complex theme values 116 | - Access colors through the hooks, not directly 117 | 118 | 3. Customization: 119 | 120 | - Extend types for custom theme values 121 | - Add new color constants to `constants.ts` 122 | - Create new themed components as needed 123 | 124 | 4. Navigation: 125 | - Use themed layouts for consistent navigation styling 126 | - Customize through screenOptions prop 127 | - Follow the navigation theme patterns 128 | 129 | ## Type Safety 130 | 131 | The theme system is fully typed with TypeScript, providing: 132 | 133 | - Type-safe color access 134 | - Component prop validation 135 | - Theme context typing 136 | -------------------------------------------------------------------------------- /app/(auth)/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { 3 | Text, 4 | View, 5 | TextInput, 6 | TouchableOpacity, 7 | ActivityIndicator, 8 | } from "react-native"; 9 | import { router } from "expo-router"; 10 | import { useMutation } from "@tanstack/react-query"; 11 | import axios from "axios"; 12 | 13 | interface LoginCredentials { 14 | email: string; 15 | password: string; 16 | } 17 | 18 | export default function LoginScreen() { 19 | const [email, setEmail] = useState(""); 20 | const [password, setPassword] = useState(""); 21 | const [error, setError] = useState(""); 22 | 23 | const loginMutation = useMutation({ 24 | mutationFn: async (credentials: LoginCredentials) => { 25 | const response = await axios.post( 26 | `${process.env.EXPO_PUBLIC_API_URL}/sessions`, 27 | credentials 28 | ); 29 | return response.data; 30 | }, 31 | onSuccess: (data) => { 32 | // Here you would typically store the token 33 | // and handle the authentication state 34 | router.replace("/(app)"); 35 | }, 36 | onError: (error: any) => { 37 | setError( 38 | error.response?.data?.message || "Login failed. Please try again." 39 | ); 40 | }, 41 | }); 42 | 43 | const handleLogin = () => { 44 | setError(""); 45 | if (!email || !password) { 46 | setError("Please fill in all fields"); 47 | return; 48 | } 49 | loginMutation.mutate({ email, password }); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | Login 58 | 59 | 60 | Please enter your credentials 61 | 62 | 63 | 64 | 65 | {error ? ( 66 | 67 | {error} 68 | 69 | ) : null} 70 | 71 | 72 | Email 73 | 82 | 83 | 84 | 85 | Password 86 | 94 | 95 | 96 | 101 | {loginMutation.isPending ? ( 102 | 103 | ) : ( 104 | Sign In 105 | )} 106 | 107 | 108 | 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /lib/permissions/README.md: -------------------------------------------------------------------------------- 1 | # Permissions System 2 | 3 | A comprehensive permissions management system for React Native/Expo applications. This system handles all permission-related operations in a clean, type-safe, and platform-aware manner. 4 | 5 | ## Core Files Explanation 6 | 7 | - `base.ts`: The foundation of the permission system. Contains the abstract `BasePermissionHandler` class that all specific permission handlers extend. Handles common functionality like caching, web platform detection, and the basic permission lifecycle. 8 | 9 | - `hooks.tsx`: Contains the React hook `usePermission` that makes it easy to use permissions in React components. Handles loading states, mounting/unmounting, and provides a clean API for checking and requesting permissions. 10 | 11 | - `index.ts`: The main entry point that exports everything needed to use the permissions system. You should only import from this file when using the system. 12 | 13 | ## Available Permissions 14 | 15 | 1. **Camera** (`camera`) 16 | 17 | - Access to device camera 18 | - Required for taking photos/videos 19 | 20 | ```typescript 21 | const { status, request } = usePermission("camera"); 22 | ``` 23 | 24 | 2. **Microphone** (`microphone`) 25 | 26 | - Access to device microphone 27 | - Required for audio recording 28 | 29 | ```typescript 30 | const { status, request } = usePermission("microphone"); 31 | ``` 32 | 33 | 3. **Location** (`location`) 34 | 35 | - Access to device location 36 | - Required for geolocation features 37 | 38 | ```typescript 39 | const { status, request } = usePermission("location"); 40 | ``` 41 | 42 | 4. **Photo Library** (`photoLibrary`) 43 | 44 | - Access to device photo gallery 45 | - Required for saving/accessing photos 46 | 47 | ```typescript 48 | const { status, request } = usePermission("photoLibrary"); 49 | ``` 50 | 51 | 5. **Tracking** (`tracking`) 52 | - iOS only: App Tracking Transparency 53 | - Required for IDFA access on iOS 14.5+ 54 | - Automatically returns 'granted' on Android/Web 55 | ```typescript 56 | const { status, request } = usePermission("tracking"); 57 | ``` 58 | 59 | ## Usage Examples 60 | 61 | ### Basic Usage in Components 62 | 63 | ```typescript 64 | import { usePermission } from "lib/permissions"; 65 | 66 | function CameraScreen() { 67 | const { status, isLoading, canAskAgain, request } = usePermission("camera"); 68 | 69 | useEffect(() => { 70 | if (status === "undetermined") { 71 | request(); 72 | } 73 | }, [status]); 74 | 75 | if (isLoading) { 76 | return ; 77 | } 78 | 79 | if (status !== "granted") { 80 | return ( 81 | 82 | Camera permission is required 83 | {canAskAgain &&