Merge pull request #679 from christianlouis/copilot/fix-typeerror-expo-start
feat(mobile): bootstrap expo-router app/ directory, WelcomeScreen, and web support
This commit is contained in:
+6
-124
@@ -1,127 +1,9 @@
|
||||
/**
|
||||
* App.tsx – root component for the DocuElevate mobile app.
|
||||
* App.tsx – legacy root component (not the active entry point).
|
||||
*
|
||||
* Wraps the entire app in the AuthProvider and renders either the login
|
||||
* screen (unauthenticated) or the main tab navigator (authenticated).
|
||||
* Push notification registration is handled by the usePushNotifications hook.
|
||||
* The app uses expo-router as its entry point ("main": "expo-router/entry"
|
||||
* in package.json). Routing, authentication, and navigation are all handled
|
||||
* by the file-based routes in the `app/` directory.
|
||||
*
|
||||
* This file is retained for reference only and is not executed at runtime.
|
||||
*/
|
||||
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { AuthProvider, useAuth } from "./src/context/AuthContext";
|
||||
import { usePushNotifications } from "./src/hooks/usePushNotifications";
|
||||
import FilesScreen from "./src/screens/FilesScreen";
|
||||
import LoginScreen from "./src/screens/LoginScreen";
|
||||
import ProfileScreen from "./src/screens/ProfileScreen";
|
||||
import UploadScreen from "./src/screens/UploadScreen";
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
function TabNavigator() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
usePushNotifications(isAuthenticated);
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: "#1e40af",
|
||||
tabBarInactiveTintColor: "#9ca3af",
|
||||
tabBarStyle: {
|
||||
borderTopColor: "#e5e7eb",
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#1e40af",
|
||||
},
|
||||
headerTintColor: "#ffffff",
|
||||
headerTitleStyle: {
|
||||
fontWeight: "700",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Upload"
|
||||
component={UploadScreen}
|
||||
options={{
|
||||
title: "Upload",
|
||||
tabBarLabel: "Upload",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="cloud-upload-outline" size={size} color={color} />
|
||||
),
|
||||
headerTitle: "DocuElevate",
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Files"
|
||||
component={FilesScreen}
|
||||
options={{
|
||||
title: "Files",
|
||||
tabBarLabel: "Files",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="document-text-outline" size={size} color={color} />
|
||||
),
|
||||
headerTitle: "My Documents",
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Profile"
|
||||
component={ProfileScreen}
|
||||
options={{
|
||||
title: "Profile",
|
||||
tabBarLabel: "Profile",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="person-circle-outline" size={size} color={color} />
|
||||
),
|
||||
headerTitle: "Profile",
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const { isLoading, isAuthenticated } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator size="large" color="#1e40af" />
|
||||
<Text style={styles.loadingText}>Loading…</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
{isAuthenticated ? <TabNavigator /> : <LoginScreen />}
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loading: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#f9fafb",
|
||||
gap: 12,
|
||||
},
|
||||
loadingText: {
|
||||
color: "#6b7280",
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
|
||||
+36
-15
@@ -1,6 +1,6 @@
|
||||
# DocuElevate Mobile App
|
||||
|
||||
Native mobile application for DocuElevate, built with **React Native** and **Expo** for both iOS (primary) and Android.
|
||||
Native mobile application for DocuElevate, built with **React Native** and **Expo** for iOS, Android, and Web.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -11,10 +11,11 @@ Native mobile application for DocuElevate, built with **React Native** and **Exp
|
||||
- 🔔 **Push Notifications** – receive real-time push notifications when documents finish processing (via Expo push notifications)
|
||||
- 📂 **Document List** – browse and search your processed documents
|
||||
- 👤 **Profile** – view account details and sign out
|
||||
- 🌐 **Web** – run directly in the browser via Expo web (Metro bundler)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- Node.js 20.19.4+ (use [nvm](https://github.com/nvm-sh/nvm): `nvm use` in this directory)
|
||||
- Expo CLI (`npm install -g @expo/cli`)
|
||||
- Expo Go app on device (for development) **or** Expo Application Services (EAS) for production builds
|
||||
- An Expo account: <https://expo.dev/>
|
||||
@@ -26,11 +27,14 @@ Native mobile application for DocuElevate, built with **React Native** and **Exp
|
||||
cd mobile
|
||||
npm install
|
||||
|
||||
# 2. Start the development server
|
||||
npx expo start
|
||||
# 2. Start the development server (choose a platform)
|
||||
npx expo start # interactive menu (iOS / Android / Web)
|
||||
npx expo start --ios # open directly in iOS Simulator
|
||||
npx expo start --android # open in Android Emulator
|
||||
npx expo start --web # open in the browser
|
||||
```
|
||||
|
||||
Scan the QR code with **Expo Go** on your iOS or Android device.
|
||||
Scan the QR code with **Expo Go** on your iOS or Android device, or press `w` in the interactive menu to open the web build.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -66,7 +70,12 @@ eas build --platform all
|
||||
|
||||
### Android-specific
|
||||
|
||||
- Add a `google-services.json` file (from Firebase Console) to the `mobile/` directory for push notification support
|
||||
- **Android push notifications** require a `google-services.json` file from Firebase Console. This file is intentionally excluded from the repository (`.gitignore`). To enable FCM push notifications in your Android builds:
|
||||
1. Create a Firebase project at <https://console.firebase.google.com/>
|
||||
2. Add an Android app with the package name `org.docuelevate.mobile`
|
||||
3. Download `google-services.json` and place it in the `mobile/` directory
|
||||
4. Add `"googleServicesFile": "./google-services.json"` back to the `android` section of `app.json` before building
|
||||
- The app runs and bundles correctly without `google-services.json`; only Android push notifications will be unavailable
|
||||
- For Play Store submission: create a service account in Google Play Console, download the JSON key as `google-play-service-account.json`, and update `eas.json`
|
||||
|
||||
## Configuration
|
||||
@@ -92,23 +101,35 @@ The Expo push token is sent to the backend after login via `POST /api/mobile/reg
|
||||
|
||||
```
|
||||
mobile/
|
||||
├── App.tsx # Root component
|
||||
├── app.json # Expo configuration
|
||||
├── eas.json # EAS Build configuration
|
||||
├── app/ # expo-router file-based routes
|
||||
│ ├── _layout.tsx # Root layout (AuthProvider + auth guard)
|
||||
│ ├── (auth)/ # Unauthenticated route group
|
||||
│ │ ├── _layout.tsx # Auth stack (no header)
|
||||
│ │ ├── index.tsx # Welcome screen
|
||||
│ │ └── login.tsx # Login screen
|
||||
│ └── (tabs)/ # Authenticated route group
|
||||
│ ├── _layout.tsx # Tab navigator (Upload / Files / Profile)
|
||||
│ ├── index.tsx # Upload tab
|
||||
│ ├── files.tsx # Files tab
|
||||
│ └── profile.tsx # Profile tab
|
||||
├── App.tsx # Legacy file (not the entry point; see app/)
|
||||
├── app.json # Expo configuration
|
||||
├── eas.json # EAS Build configuration
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
├── context/
|
||||
│ └── AuthContext.tsx # Authentication state management
|
||||
│ └── AuthContext.tsx # Authentication state management
|
||||
├── hooks/
|
||||
│ └── usePushNotifications.ts # Push notification registration
|
||||
├── screens/
|
||||
│ ├── LoginScreen.tsx # SSO login
|
||||
│ ├── UploadScreen.tsx # Camera capture + file picker
|
||||
│ ├── FilesScreen.tsx # Document list
|
||||
│ └── ProfileScreen.tsx # User profile + sign out
|
||||
│ ├── WelcomeScreen.tsx # Branded intro / onboarding
|
||||
│ ├── LoginScreen.tsx # SSO login
|
||||
│ ├── UploadScreen.tsx # Camera capture + file picker
|
||||
│ ├── FilesScreen.tsx # Document list
|
||||
│ └── ProfileScreen.tsx # User profile + sign out
|
||||
└── services/
|
||||
└── api.ts # DocuElevate API client
|
||||
└── api.ts # DocuElevate API client
|
||||
```
|
||||
|
||||
## Share Extension (iOS)
|
||||
|
||||
+4
-3
@@ -38,11 +38,12 @@
|
||||
"RECEIVE_BOOT_COMPLETED",
|
||||
"VIBRATE"
|
||||
],
|
||||
"versionCode": 1,
|
||||
"googleServicesFile": "./google-services.json"
|
||||
"versionCode": 1
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
"favicon": "./assets/favicon.png",
|
||||
"bundler": "metro",
|
||||
"output": "single"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Layout for the unauthenticated route group: Welcome → Login.
|
||||
*
|
||||
* Uses a headerless native stack so the screens animate naturally.
|
||||
*/
|
||||
|
||||
import { Stack } from "expo-router";
|
||||
import React from "react";
|
||||
|
||||
export default function AuthLayout() {
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="login" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Welcome screen route – first screen shown to unauthenticated users.
|
||||
*/
|
||||
export { default } from "../../src/screens/WelcomeScreen";
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Login screen route – server URL entry and SSO sign-in.
|
||||
*/
|
||||
export { default } from "../../src/screens/LoginScreen";
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Tab navigator layout for authenticated users.
|
||||
*
|
||||
* Registers three tabs: Upload (default), Files, and Profile.
|
||||
* Push notifications are initialised here so they activate as soon as the
|
||||
* user enters the authenticated area.
|
||||
*/
|
||||
|
||||
import { Tabs } from "expo-router";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { usePushNotifications } from "../../src/hooks/usePushNotifications";
|
||||
import { useAuth } from "../../src/context/AuthContext";
|
||||
|
||||
export default function TabLayout() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
usePushNotifications(isAuthenticated);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: "#1e40af",
|
||||
tabBarInactiveTintColor: "#9ca3af",
|
||||
tabBarStyle: {
|
||||
borderTopColor: "#e5e7eb",
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#1e40af",
|
||||
},
|
||||
headerTintColor: "#ffffff",
|
||||
headerTitleStyle: {
|
||||
fontWeight: "700",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Upload",
|
||||
tabBarLabel: "Upload",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="cloud-upload-outline" size={size} color={color} />
|
||||
),
|
||||
headerTitle: "DocuElevate",
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="files"
|
||||
options={{
|
||||
title: "Files",
|
||||
tabBarLabel: "Files",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="document-text-outline" size={size} color={color} />
|
||||
),
|
||||
headerTitle: "My Documents",
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
title: "Profile",
|
||||
tabBarLabel: "Profile",
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="person-circle-outline" size={size} color={color} />
|
||||
),
|
||||
headerTitle: "Profile",
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Files tab route.
|
||||
*/
|
||||
export { default } from "../../src/screens/FilesScreen";
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Upload tab route (default tab for authenticated users).
|
||||
*/
|
||||
export { default } from "../../src/screens/UploadScreen";
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Profile tab route.
|
||||
*/
|
||||
export { default } from "../../src/screens/ProfileScreen";
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Root layout for the DocuElevate mobile app.
|
||||
*
|
||||
* Wraps the entire app in AuthProvider and SafeAreaProvider, then uses the
|
||||
* AuthGuard component to redirect between the unauthenticated (auth) route
|
||||
* group and the authenticated (tabs) route group based on session state.
|
||||
*/
|
||||
|
||||
import { Stack, useRouter, useSegments } from "expo-router";
|
||||
import React, { useEffect } from "react";
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { AuthProvider, useAuth } from "../src/context/AuthContext";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth guard – redirects to the correct route group after auth state resolves
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function AuthGuard() {
|
||||
const { isLoading, isAuthenticated } = useAuth();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
const inAuthGroup = segments[0] === "(auth)";
|
||||
|
||||
if (!isAuthenticated && !inAuthGroup) {
|
||||
// Unauthenticated visitor outside the auth group → send to welcome
|
||||
router.replace("/(auth)/");
|
||||
} else if (isAuthenticated && inAuthGroup) {
|
||||
// Authenticated user on auth screens → send to main app
|
||||
router.replace("/(tabs)/");
|
||||
}
|
||||
}, [isAuthenticated, isLoading, segments, router]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator size="large" color="#1e40af" />
|
||||
<Text style={styles.loadingText}>Loading…</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="(auth)" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Root layout export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<AuthProvider>
|
||||
<AuthGuard />
|
||||
</AuthProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loading: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#f9fafb",
|
||||
gap: 12,
|
||||
},
|
||||
loadingText: {
|
||||
color: "#6b7280",
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
Generated
+225
-5
@@ -20,6 +20,7 @@
|
||||
"expo-constants": "~18.0.13",
|
||||
"expo-crypto": "~15.0.8",
|
||||
"expo-dev-client": "~6.0.20",
|
||||
"expo-device": "~7.0.3",
|
||||
"expo-document-picker": "~14.0.8",
|
||||
"expo-file-system": "~19.0.21",
|
||||
"expo-font": "~14.0.0",
|
||||
@@ -34,9 +35,11 @@
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-safe-area-context": "5.6.0",
|
||||
"react-native-screens": "4.16.0"
|
||||
"react-native-screens": "4.16.0",
|
||||
"react-native-web": "~0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
@@ -46,7 +49,7 @@
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.16.0"
|
||||
"node": ">=20.19.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@0no-co/graphql.web": {
|
||||
@@ -5530,6 +5533,15 @@
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
|
||||
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -5544,6 +5556,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-in-js-utils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
|
||||
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hyphenate-style-name": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -6949,6 +6970,18 @@
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-device": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-7.0.3.tgz",
|
||||
"integrity": "sha512-uNGhDYmpDj/3GySWZmRiYSt52Phdim11p0pXfgpCq/nMks0+UPZwl3D0vin5N8/gpVe5yzb13GYuFxiVoDyniw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ua-parser-js": "^0.7.33"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-document-picker": {
|
||||
"version": "14.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz",
|
||||
@@ -7321,6 +7354,62 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fbjs": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
|
||||
"integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.1.5",
|
||||
"fbjs-css-vars": "^1.0.0",
|
||||
"loose-envify": "^1.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"promise": "^7.1.1",
|
||||
"setimmediate": "^1.0.5",
|
||||
"ua-parser-js": "^1.0.35"
|
||||
}
|
||||
},
|
||||
"node_modules/fbjs-css-vars": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
|
||||
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fbjs/node_modules/promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asap": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fbjs/node_modules/ua-parser-js": {
|
||||
"version": "1.0.41",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
|
||||
"integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ua-parser-js"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/faisalman"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ua-parser-js": "script/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -7959,6 +8048,12 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/hyphenate-style-name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
|
||||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -8062,6 +8157,15 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inline-style-prefixer": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
|
||||
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-in-js-utils": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -9971,6 +10075,26 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
|
||||
@@ -10604,6 +10728,12 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -10860,7 +10990,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -10984,6 +11113,38 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
|
||||
"integrity": "sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@react-native/normalize-colors": "^0.74.1",
|
||||
"fbjs": "^3.0.4",
|
||||
"inline-style-prefixer": "^7.0.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"nullthrows": "^1.1.1",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"styleq": "^0.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web/node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.74.89",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz",
|
||||
"integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-web/node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
@@ -11488,8 +11649,7 @@
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
@@ -11656,6 +11816,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -12149,6 +12315,12 @@
|
||||
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/styleq": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz",
|
||||
"integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.1",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||
@@ -12406,6 +12578,12 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
@@ -12584,6 +12762,32 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ua-parser-js": {
|
||||
"version": "0.7.41",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
|
||||
"integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ua-parser-js"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/faisalman"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ua-parser-js": "script/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||
@@ -12910,6 +13114,16 @@
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url-without-unicode": {
|
||||
"version": "8.0.0-3",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz",
|
||||
@@ -12924,6 +13138,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
+4
-1
@@ -30,6 +30,7 @@
|
||||
"expo-camera": "~17.0.10",
|
||||
"expo-constants": "~18.0.13",
|
||||
"expo-crypto": "~15.0.8",
|
||||
"expo-device": "~7.0.3",
|
||||
"expo-document-picker": "~14.0.8",
|
||||
"expo-file-system": "~19.0.21",
|
||||
"expo-image-manipulator": "~14.0.8",
|
||||
@@ -43,7 +44,9 @@
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-safe-area-context": "5.6.0",
|
||||
"react-native-screens": "4.16.0"
|
||||
},
|
||||
@@ -55,7 +58,7 @@
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.16.0"
|
||||
"node": ">=20.19.4"
|
||||
},
|
||||
"private": true,
|
||||
"expo": {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* LoginScreen – entry point for unauthenticated users.
|
||||
* LoginScreen – server URL entry and SSO sign-in.
|
||||
*
|
||||
* Renders a server URL input and a "Sign in with SSO" button that opens the
|
||||
* DocuElevate web login page in the system browser. On success the
|
||||
* AuthContext stores the API token and navigates to the main app.
|
||||
*/
|
||||
|
||||
import { useRouter } from "expo-router";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -23,6 +24,7 @@ import { useAuth } from "../context/AuthContext";
|
||||
|
||||
export default function LoginScreen() {
|
||||
const { signIn } = useAuth();
|
||||
const router = useRouter();
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -56,7 +58,7 @@ export default function LoginScreen() {
|
||||
<View style={styles.card}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image
|
||||
source={require("../../../assets/logo.png")}
|
||||
source={require("../../assets/logo.png")}
|
||||
style={styles.logoImage}
|
||||
resizeMode="contain"
|
||||
accessibilityLabel="DocuElevate logo"
|
||||
@@ -97,6 +99,15 @@ export default function LoginScreen() {
|
||||
<Text style={styles.hint}>
|
||||
You will be redirected to your organisation's sign-in page.
|
||||
</Text>
|
||||
|
||||
<Pressable
|
||||
onPress={() => router.back()}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Back to welcome screen"
|
||||
style={styles.backLink}
|
||||
>
|
||||
<Text style={styles.backLinkText}>← Back</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
@@ -179,4 +190,14 @@ const styles = StyleSheet.create({
|
||||
color: "#9ca3af",
|
||||
textAlign: "center",
|
||||
},
|
||||
backLink: {
|
||||
marginTop: 20,
|
||||
alignItems: "center",
|
||||
minHeight: 44,
|
||||
justifyContent: "center",
|
||||
},
|
||||
backLinkText: {
|
||||
fontSize: 13,
|
||||
color: "#6b7280",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* WelcomeScreen – first screen shown to unauthenticated users on first launch.
|
||||
*
|
||||
* Presents the DocuElevate brand, a short description of what the app does,
|
||||
* and a "Get Started" button that navigates to the LoginScreen.
|
||||
*/
|
||||
|
||||
import { useRouter } from "expo-router";
|
||||
import React from "react";
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
const FEATURES: { icon: string; title: string; description: string }[] = [
|
||||
{
|
||||
icon: "🔍",
|
||||
title: "OCR & Text Extraction",
|
||||
description: "Convert scanned PDFs and images into fully searchable text automatically.",
|
||||
},
|
||||
{
|
||||
icon: "🤖",
|
||||
title: "AI Metadata Extraction",
|
||||
description: "AI classifies documents and pulls out key fields like dates, amounts, and subjects.",
|
||||
},
|
||||
{
|
||||
icon: "☁️",
|
||||
title: "Multi-Cloud Storage",
|
||||
description: "Route processed files to Dropbox, Google Drive, OneDrive, S3, Nextcloud, and more.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function WelcomeScreen() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Hero */}
|
||||
<View style={styles.hero}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image
|
||||
source={require("../../assets/logo.png")}
|
||||
style={styles.logoImage}
|
||||
resizeMode="contain"
|
||||
accessibilityLabel="DocuElevate logo"
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.appName}>DocuElevate</Text>
|
||||
<Text style={styles.tagline}>Intelligent Document Processing</Text>
|
||||
<Text style={styles.heroDescription}>
|
||||
Ingest documents, run OCR, extract metadata with AI, and route files
|
||||
to your cloud storage — all in one seamless pipeline.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Feature highlights */}
|
||||
<View style={styles.features}>
|
||||
{FEATURES.map((feature) => (
|
||||
<View key={feature.title} style={styles.featureRow}>
|
||||
<Text style={styles.featureIcon} aria-hidden={true}>{feature.icon}</Text>
|
||||
<View style={styles.featureText}>
|
||||
<Text style={styles.featureTitle}>{feature.title}</Text>
|
||||
<Text style={styles.featureDescription}>{feature.description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* CTA */}
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
|
||||
onPress={() => router.push("/(auth)/login")}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Get started — connect to your DocuElevate server"
|
||||
>
|
||||
<Text style={styles.buttonText}>Get Started</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text style={styles.hint}>
|
||||
Connect to your self-hosted or cloud DocuElevate server.
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: {
|
||||
flex: 1,
|
||||
backgroundColor: "#1e40af",
|
||||
},
|
||||
scroll: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 28,
|
||||
paddingTop: 48,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
hero: {
|
||||
alignItems: "center",
|
||||
marginBottom: 40,
|
||||
},
|
||||
logoContainer: {
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: 24,
|
||||
backgroundColor: "rgba(255,255,255,0.15)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 16,
|
||||
},
|
||||
logoImage: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
},
|
||||
appName: {
|
||||
fontSize: 36,
|
||||
fontWeight: "800",
|
||||
color: "#ffffff",
|
||||
textAlign: "center",
|
||||
marginBottom: 6,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
tagline: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "rgba(255,255,255,0.75)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1.5,
|
||||
textAlign: "center",
|
||||
marginBottom: 16,
|
||||
},
|
||||
heroDescription: {
|
||||
fontSize: 16,
|
||||
color: "rgba(255,255,255,0.85)",
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
maxWidth: 320,
|
||||
},
|
||||
features: {
|
||||
backgroundColor: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 16,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 36,
|
||||
},
|
||||
featureRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: "rgba(255,255,255,0.2)",
|
||||
},
|
||||
featureIcon: {
|
||||
fontSize: 24,
|
||||
marginRight: 14,
|
||||
marginTop: 1,
|
||||
},
|
||||
featureText: {
|
||||
flex: 1,
|
||||
},
|
||||
featureTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: "#ffffff",
|
||||
marginBottom: 2,
|
||||
},
|
||||
featureDescription: {
|
||||
fontSize: 13,
|
||||
color: "rgba(255,255,255,0.75)",
|
||||
lineHeight: 18,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#ffffff",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 52,
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.15,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
marginBottom: 16,
|
||||
},
|
||||
buttonPressed: {
|
||||
opacity: 0.9,
|
||||
transform: [{ scale: 0.98 }],
|
||||
},
|
||||
buttonText: {
|
||||
color: "#1e40af",
|
||||
fontSize: 17,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
hint: {
|
||||
fontSize: 12,
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user