├── 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 && }
84 |
85 | );
86 | }
87 |
88 | return ;
89 | }
90 | ```
91 |
92 | ### Multiple Permissions
93 |
94 | ```typescript
95 | function ProfileScreen() {
96 | const camera = usePermission("camera");
97 | const photoLibrary = usePermission("photoLibrary");
98 |
99 | const handleUploadPhoto = async () => {
100 | // Request both permissions if needed
101 | if (camera.status !== "granted") {
102 | await camera.request();
103 | }
104 | if (photoLibrary.status !== "granted") {
105 | await photoLibrary.request();
106 | }
107 |
108 | if (camera.status === "granted" && photoLibrary.status === "granted") {
109 | // Both permissions granted, proceed with upload
110 | // ...
111 | }
112 | };
113 |
114 | return (
115 |
122 | );
123 | }
124 | ```
125 |
126 | ### Non-Component Usage
127 |
128 | ```typescript
129 | import { checkPermission, requestPermission } from "lib/permissions";
130 |
131 | async function takePicture() {
132 | const { status } = await checkPermission("camera");
133 |
134 | if (status !== "granted") {
135 | const result = await requestPermission("camera");
136 | if (result.status !== "granted") {
137 | throw new Error("Camera permission required");
138 | }
139 | }
140 |
141 | // Proceed with taking picture
142 | }
143 | ```
144 |
145 | ## Platform Handling
146 |
147 | - **iOS**: Full support for all permissions
148 | - **Android**: Full support except tracking (automatically returns 'granted')
149 | - **Web**:
150 | - Camera/Microphone: Uses browser permission API
151 | - Other permissions: Automatically returns 'granted'
152 | - No permission caching on web
153 |
154 | ## Permission States
155 |
156 | - `undetermined`: Permission hasn't been requested yet
157 | - `granted`: Permission has been granted
158 | - `denied`: Permission was denied
159 | - `blocked`: Permission is permanently denied/blocked
160 |
161 | ## Best Practices
162 |
163 | 1. Always check permission status before using protected features
164 | 2. Request permissions only when needed, not at app startup
165 | 3. Handle all possible permission states
166 | 4. Provide clear UI feedback when permissions are denied
167 | 5. Use the `canAskAgain` flag to determine if you should show a "request" button
168 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "org.jetbrains.kotlin.android"
3 | apply plugin: "com.facebook.react"
4 |
5 | def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
6 |
7 | /**
8 | * This is the configuration block to customize your React Native Android app.
9 | * By default you don't need to apply any configuration, just uncomment the lines you need.
10 | */
11 | react {
12 | entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
13 | reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
14 | hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
15 | codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
16 |
17 | // Use Expo CLI to bundle the app, this ensures the Metro config
18 | // works correctly with Expo projects.
19 | cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
20 | bundleCommand = "export:embed"
21 |
22 | /* Folders */
23 | // The root of your project, i.e. where "package.json" lives. Default is '../..'
24 | // root = file("../../")
25 | // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
26 | // reactNativeDir = file("../../node_modules/react-native")
27 | // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
28 | // codegenDir = file("../../node_modules/@react-native/codegen")
29 |
30 | /* Variants */
31 | // The list of variants to that are debuggable. For those we're going to
32 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
33 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
34 | // debuggableVariants = ["liteDebug", "prodDebug"]
35 |
36 | /* Bundling */
37 | // A list containing the node command and its flags. Default is just 'node'.
38 | // nodeExecutableAndArgs = ["node"]
39 |
40 | //
41 | // The path to the CLI configuration file. Default is empty.
42 | // bundleConfig = file(../rn-cli.config.js)
43 | //
44 | // The name of the generated asset file containing your JS bundle
45 | // bundleAssetName = "MyApplication.android.bundle"
46 | //
47 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
48 | // entryFile = file("../js/MyApplication.android.js")
49 | //
50 | // A list of extra flags to pass to the 'bundle' commands.
51 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
52 | // extraPackagerArgs = []
53 |
54 | /* Hermes Commands */
55 | // The hermes compiler command to run. By default it is 'hermesc'
56 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
57 | //
58 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
59 | // hermesFlags = ["-O", "-output-source-map"]
60 |
61 | /* Autolinking */
62 | autolinkLibrariesWithApp()
63 | }
64 |
65 | /**
66 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
67 | */
68 | def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
69 |
70 | /**
71 | * The preferred build flavor of JavaScriptCore (JSC)
72 | *
73 | * For example, to use the international variant, you can use:
74 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
75 | *
76 | * The international variant includes ICU i18n library and necessary data
77 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
78 | * give correct results when using with locales other than en-US. Note that
79 | * this variant is about 6MiB larger per architecture than default.
80 | */
81 | def jscFlavor = 'org.webkit:android-jsc:+'
82 |
83 | android {
84 | ndkVersion rootProject.ext.ndkVersion
85 |
86 | buildToolsVersion rootProject.ext.buildToolsVersion
87 | compileSdk rootProject.ext.compileSdkVersion
88 |
89 | namespace 'com.nativewind.app'
90 | defaultConfig {
91 | applicationId 'com.nativewind.app'
92 | minSdkVersion rootProject.ext.minSdkVersion
93 | targetSdkVersion rootProject.ext.targetSdkVersion
94 | versionCode 1
95 | versionName "1.0.0"
96 | }
97 | signingConfigs {
98 | debug {
99 | storeFile file('debug.keystore')
100 | storePassword 'android'
101 | keyAlias 'androiddebugkey'
102 | keyPassword 'android'
103 | }
104 | }
105 | buildTypes {
106 | debug {
107 | signingConfig signingConfigs.debug
108 | }
109 | release {
110 | // Caution! In production, you need to generate your own keystore file.
111 | // see https://reactnative.dev/docs/signed-apk-android.
112 | signingConfig signingConfigs.debug
113 | shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
114 | minifyEnabled enableProguardInReleaseBuilds
115 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
116 | crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
117 | }
118 | }
119 | packagingOptions {
120 | jniLibs {
121 | useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
122 | }
123 | }
124 | androidResources {
125 | ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
126 | }
127 | }
128 |
129 | // Apply static values from `gradle.properties` to the `android.packagingOptions`
130 | // Accepts values in comma delimited lists, example:
131 | // android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
132 | ["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
133 | // Split option: 'foo,bar' -> ['foo', 'bar']
134 | def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
135 | // Trim all elements in place.
136 | for (i in 0.. 0) {
141 | println "android.packagingOptions.$prop += $options ($options.length)"
142 | // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
143 | options.each {
144 | android.packagingOptions[prop] += it
145 | }
146 | }
147 | }
148 |
149 | dependencies {
150 | // The version of react-native is set by the React Native Gradle Plugin
151 | implementation("com.facebook.react:react-android")
152 |
153 | def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
154 | def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
155 | def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
156 |
157 | if (isGifEnabled) {
158 | // For animated gif support
159 | implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
160 | }
161 |
162 | if (isWebpEnabled) {
163 | // For webp support
164 | implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
165 | if (isWebpAnimatedEnabled) {
166 | // Animated webp support
167 | implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
168 | }
169 | }
170 |
171 | if (hermesEnabled.toBoolean()) {
172 | implementation("com.facebook.react:hermes-android")
173 | } else {
174 | implementation jscFlavor
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------