在Expo App

问题描述 投票:0回答:1
AuthContext

。 我的应用程序是这样结构化的:


app/index.tsx:app tree import { useState } from "react"; import { Image, View, Text, TextInput, StyleSheet, Alert, TouchableOpacity, } from "react-native"; import { useRouter } from "expo-router"; import { useAuth } from "@/context/AuthContext"; import { FontAwesome } from "@expo/vector-icons"; import Feather from "@expo/vector-icons/Feather"; const Index = () => { const router = useRouter(); const { login } = useAuth(); const [username, setUsername] = useState(""); // State for username const [password, setPassword] = useState(""); // State for password const [showPassword, setShowPassword] = useState(false); const handleLogin = async () => { try { // Make POST request to your server with the username and password const response = await fetch( "https://expense-g45.onrender.com/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), } ); if (response.ok) { const data = await response.json(); const { token } = data; // On successful login, store the token and navigate login(token); // Save token using context router.replace("/home"); // Navigate to the home screen } else { Alert.alert("Login Failed", "Invalid username or password"); } } catch (error) { Alert.alert("Login Failed", "An error occurred during login"); } }; return ( <View style={styles.container}> <Image style={styles.image} source={require("@/assets/adaptive-icon.png")} /> <Text style={styles.title}>Welcome to Expense Tracker!</Text> {/* Username Input */} <View style={styles.fieldContainer}> <Feather style={styles.icons} name="user" size={24} color="black" /> <TextInput style={styles.input} placeholder="Username" value={username} onChangeText={(text) => setUsername(text)} /> </View> {/* Password Input */} <View style={styles.fieldContainer}> <Feather style={styles.icons} name="lock" size={20} color="black" /> <TextInput style={styles.input} placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry={!showPassword} // Toggle visibility /> <TouchableOpacity style={styles.eyeIcon} onPress={() => setShowPassword(!showPassword)} > <FontAwesome name={showPassword ? "eye-slash" : "eye"} size={20} color="gray" /> </TouchableOpacity> </View> {/* Login Button */} {/* <Button title="Login" onPress={handleLogin} /> */} <TouchableOpacity style={styles.button} onPress={handleLogin}> <Text style={styles.buttonText}>Login</Text> </TouchableOpacity> </View> ); }; const styles = StyleSheet.create({ button: { width: "100%", height: 50, backgroundColor: "#1E90FF", borderRadius: 8, justifyContent: "center", alignItems: "center", marginBottom: 20, }, buttonText: { color: "#fff", fontSize: 18, }, container: { flex: 1, justifyContent: "center", alignItems: "center", padding: 20, }, eyeIcon: { position: "absolute", right: 10, top: "50%", transform: [{ translateY: -17 }], // Center vertically }, fieldContainer: { flexDirection: "row", alignItems: "center", width: "100%", }, icons: { position: "absolute", left: 10, top: "50%", transform: [{ translateY: -19 }], // Center vertically }, input: { width: "100%", padding: 10, paddingLeft: 40, borderWidth: 1, borderColor: "gray", borderRadius: 5, marginBottom: 15, }, image: { width: 200, height: 200, resizeMode: "contain", }, title: { fontSize: 24, fontWeight: "bold", marginBottom: 20 }, }); export default Index;

app/_layout.tsx:

import { Stack } from "expo-router/stack";
import { AuthProvider, useAuth } from "@/context/AuthContext";
import { ActivityIndicator, View } from "react-native";

export default function RootLayout() {
  return (
    <AuthProvider>
      <MainNavigator />
    </AuthProvider>
  );
}

function MainNavigator() {
  const { isLoggedIn, isLoading } = useAuth();

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" color="#1E90FF" />
      </View>
    ); // Render a loading spinner or blank screen
  }

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {!isLoggedIn ? (
        <Stack.Screen name="index" />
      ) : (
        <Stack.Screen name="(tabs)" />
      )}
    </Stack>
  );
}

app/(tabs)/_ layout.tsx:

import FontAwesome from "@expo/vector-icons/FontAwesome";
import AntDesign from "@expo/vector-icons/AntDesign";
import { Tabs, useRouter } from "expo-router";

export default function TabLayout() {
  const router = useRouter(); // Access router for navigation
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: "blue" }}>
      <Tabs.Screen
        name="home"
        options={{
          title: "Home",
          headerShown: false,
          tabBarIcon: ({ color }) => (
            <FontAwesome size={24} name="home" color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="journal"
        options={{
          title: "Journal",
          headerShown: false,
          tabBarIcon: ({ color }) => (
            <AntDesign size={24} name="form" color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="logout"
        options={{
          title: "Logout",
          headerShown: false,
          tabBarIcon: ({ color }) => (
            <AntDesign size={24} name="logout" color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

app/tabs/home.tsx:

import { useEffect, useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  Pressable,
  Alert,
  ActivityIndicator,
  StatusBar,
} from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { MaterialIcons } from "@expo/vector-icons";
import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
import BankNoteCard from "@/components/BankNoteCard";
import CustomButton from "@/components/CustomButton";

export default function Home() {
  const apiEndpoint = "https://expense-g45.onrender.com";
  const [bankNotes, setBankNotes] = useState<number[]>([]);
  const [quantities, setQuantities] = useState<number[]>([]);
  const [loading, setLoading] = useState(true);

  // Function to fetch the data from the server
  const fetchData = async () => {
    const token = await AsyncStorage.getItem("authToken");

    if (!token) {
      Alert.alert("Please log in first!");
      return;
    }

    setLoading(true); // Set loading state to true
    try {
      const response = await fetch(`${apiEndpoint}/fetchQuantities`, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      if (response.ok) {
        const { bankNotes: fetchedBankNotes, quantities: fetchedQuantities } =
          await response.json();

        setBankNotes(fetchedBankNotes || []); // Set the bankNotes from the response
        setQuantities(
          fetchedQuantities && fetchedQuantities.length
            ? fetchedQuantities
            : fetchedBankNotes.map(() => 0)
        );
      } else {
        Alert.alert("Failed to fetch quantities", "You may need to log in.");
      }
    } catch (error) {
      console.error("Error fetching data:", error);
      Alert.alert("Error", "Failed to fetch data. Please try again.");
    } finally {
      setLoading(false); // Set loading state to false after fetching is complete
    }
  };

  // Fetch data from the backend server on mount
  useEffect(() => {
    fetchData();
  }, []);

  // Sync button handler to manually trigger data fetch
  const handleSync = () => {
    fetchData();
  };

  const handleResetAll = () => {
    setQuantities(bankNotes.map(() => 0));
  };

  const handleUpdateQuantity = (index: number, newQuantity: number) => {
    setQuantities((prevQuantities) => {
      const updatedQuantities = [...prevQuantities];
      updatedQuantities[index] = newQuantity;
      return updatedQuantities;
    });
  };

  const handleUpdate = async () => {
    const token = await AsyncStorage.getItem("authToken");
    try {
      await fetch(`${apiEndpoint}/updateQuantities`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ quantities }),
      });
      // Show alert if update is successful
      Alert.alert("Success!", "Quantities updated successfully.", [
        { text: "Ok" },
      ]);
    } catch (error) {
      console.error("Error updating quantities:", error);
      // Show error alert if update fails
      Alert.alert("Error", "Failed to update quantities. Please try again.", [
        { text: "OK" },
      ]);
    }
  };

  return (
    <SafeAreaProvider>
      <SafeAreaView style={styles.container}>
        <View style={styles.headerContainer}>
          <Text style={styles.heading}>Cash in Hand</Text>
          <View style={styles.sync}>
            <Pressable
              style={({ pressed }) => [{ opacity: pressed ? 0.2 : 1 }]}
              onPress={handleSync}
            >
              <MaterialIcons name="sync" size={24} />
            </Pressable>
          </View>
        </View>
        {loading ? (
          <View style={styles.initialLoader}>
            <ActivityIndicator size="large" color="green" />
          </View>
        ) : (
          <>
            <FlatList
              data={bankNotes}
              keyExtractor={(item) => item.toString()}
              renderItem={({ item, index }) => (
                <BankNoteCard
                  note={item}
                  quantity={quantities[index]}
                  onUpdateQuantity={(newQuantity) =>
                    handleUpdateQuantity(index, newQuantity)
                  }
                />
              )}
            />
            <Text style={styles.total}>
              Total amount:{" "}
              <FontAwesome6
                name="bangladeshi-taka-sign"
                size={styles.total.fontSize}
                color="black"
              />{" "}
              {bankNotes
                .reduce(
                  (sum, note, index) => (sum += note * quantities[index]),
                  0
                )
                .toLocaleString()}
            </Text>
            <View style={styles.buttonContainer}>
              <CustomButton
                handlePress={handleResetAll}
                title="Reset all"
                buttonStyle={{ backgroundColor: "red" }}
              />
              <CustomButton handlePress={handleUpdate} title="Update" />
            </View>
          </>
        )}
        <StatusBar backgroundColor={"green"} barStyle={"light-content"} />
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

const styles = StyleSheet.create({
  buttonContainer: {
    flexDirection: "row",
    gap: 5,
  },
  container: {
    flex: 1,
    padding: 10,
  },
  headerContainer: {
    flex: 1,
  },
  heading: {
    fontSize: 20,
    alignSelf: "center",
    fontWeight: "bold",
  },
  initialLoader: {
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    justifyContent: "center",
    alignItems: "center",
  },
  sync: {
    position: "absolute",
    right: 20,
    top: 20,
  },
  total: {
    fontSize: 18,
    fontWeight: "bold",
  },
});

app/(tabs)/journal.tsx:

import { StyleSheet, Text, View } from "react-native";

export default function Journal() {
  return (
    <View>
      <Text>Journal</Text>
    </View>
  );
}

const styles = StyleSheet.create({});

app/(tabs)/logout.tsx:

import { View, Text, Button, StyleSheet } from "react-native";
import { useRouter } from "expo-router";
import { useAuth } from "@/context/AuthContext"; // Assuming you have an AuthContext

export default function Logout() {
  const { logout } = useAuth(); // Access the logout function from AuthContext
  const router = useRouter(); // Access router for navigation

  // Handle logout and navigation when the button is pressed
  const handleLogout = () => {
    logout(); // Log the user out
    router.replace("/"); // Navigate to the welcome screen
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Are you sure you want to log out?</Text>
      <Button title="Logout" onPress={handleLogout} color="red" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center", // Center the content vertically
    alignItems: "center", // Center the content horizontally
    backgroundColor: "#fff", // Optional: Set a background color
  },
  title: {
    fontSize: 18,
    marginBottom: 20, // Add space above the button
    color: "#000", // Optional: Set text color
  },
});

Context/authcontext.tsx:

import {
  createContext,
  useContext,
  useState,
  useEffect,
  ReactNode,
} from "react";
import AsyncStorage from "@react-native-async-storage/async-storage"; // AsyncStorage import
import { ActivityIndicator, View } from "react-native";

// Define the type for the AuthContext
interface AuthContextType {
  isLoggedIn: boolean;
  isLoading: boolean;
  login: (token: string) => void;
  logout: () => void;
}

// Define the type for the children prop
interface AuthProviderProps {
  children: ReactNode; // Use ReactNode type for children
}

// Create the AuthContext
const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider = ({ children }: AuthProviderProps) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  // Check if the user is logged in when the app starts
  useEffect(() => {
    const checkLoginState = async () => {
      try {
        const token = await AsyncStorage.getItem("authToken");
        if (token) {
          setIsLoggedIn(true); // User is logged in if token exists
        }
      } catch (error) {
        console.log("Error checking login state", error);
      } finally {
        setIsLoading(false);
      }
    };

    checkLoginState();
  }, []);

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" color="#1E90FF" />
      </View>
    );
  }

  const login = async (token: string) => {
    await AsyncStorage.setItem("authToken", token); // Store token
    setIsLoggedIn(true);
  };

  const logout = async () => {
    await AsyncStorage.removeItem("authToken"); // Remove token
    setIsLoggedIn(false);
  };

  return (
    <AuthContext.Provider value={{ isLoggedIn, isLoading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
};

Server/server.js:

//require("dotenv").config({ path: "../.env.local" });

const express = require("express");
const { GoogleSpreadsheet } = require("google-spreadsheet");
const { JWT } = require("google-auth-library");
const jwt = require("jsonwebtoken");

const app = express();
const port = process.env.PORT || 3000;

const validUsername = process.env.VALID_USERNAME;
const validPassword = process.env.VALID_PASSWORD;

// Middleware to check if the user is authenticated
const authenticateToken = (req, res, next) => {
  const token = req.headers["authorization"]?.split(" ")[1]; // Extract token from Authorization header

  if (!token) {
    return res.status(401).send("Access Denied: No token provided");
  }

  jwt.verify(token, process.env.SECRET_KEY, (err, user) => {
    // Use the environment variable
    if (err) {
      return res.status(403).send("Access Denied: Invalid token");
    }
    req.user = user; // Attach user info to the request object
    next(); // Proceed to the next middleware or route handler
  });
};

// Google Sheets API setup
const SPREADSHEET_ID = process.env.SPREADSHEET_ID;
const SHEET_TITLE = process.env.SHEET_TITLE;
const SCOPES = [
  "https://www.googleapis.com/auth/spreadsheets",
  "https://www.googleapis.com/auth/drive.file",
];

app.post("/login", express.json(), (req, res) => {
  const { username, password } = req.body;

  // Check credentials
  if (username === validUsername && password === validPassword) {
    // Create a JWT token with a secret key
    const token = jwt.sign({ username }, process.env.SECRET_KEY, {
      expiresIn: "1h",
    });

    return res.json({ token }); // Send token to frontend
  } else {
    return res.status(401).send("Invalid credentials");
  }
});

// Fetch initial quantities from Google Sheets
app.get("/fetchQuantities", authenticateToken, async (req, res) => {
  try {
    // Authenticate with Google Sheets API using the service account
    const jwt = new JWT({
      email: process.env.CLIENT_EMAIL,
      key: process.env.PRIVATE_KEY.replace(/\\n/g, "\n"),
      scopes: SCOPES,
    });

    const doc = new GoogleSpreadsheet(SPREADSHEET_ID, jwt);

    await doc.loadInfo();
    const sheet = doc.sheetsByTitle[SHEET_TITLE];
    const rows = await sheet.getRows();

    // Map rows to quantities (assuming column 'Qty' contains quantities)
    const bankNotes = rows.map((row) => parseInt(row._rawData[0]) || "0", 10);
    const quantities = rows.map((row) => parseInt(row._rawData[1] || "0", 10));

    res.json({ bankNotes, quantities });
  } catch (error) {
    console.error("Error fetching quantities:", error);
    res.status(500).send("Error fetching data from Google Sheets");
  }
});

// Update quantities in Google Sheets (assuming there's a 'Qty' column)
app.post(
  "/updateQuantities",
  authenticateToken,
  express.json(),
  async (req, res) => {
    try {
      const { quantities } = req.body; // An array of quantities
      if (!Array.isArray(quantities)) {
        return res.status(400).send("Invalid data format");
      }

      // Authenticate and access Google Sheets
      const jwt = new JWT({
        email: process.env.CLIENT_EMAIL,
        key: process.env.PRIVATE_KEY.replace(/\\n/g, "\n"),
        scopes: SCOPES,
      });

      const doc = new GoogleSpreadsheet(SPREADSHEET_ID, jwt);

      await doc.loadInfo();
      const sheet = doc.sheetsByTitle[SHEET_TITLE];
      const rows = await sheet.getRows();

      // Update rows based on the quantities
      rows.forEach(async (row, index) => {
        row._rawData[1] = quantities[index] || 0;
        await row.save(); // Ensure the save operation completes
      });

      res.status(200).send("Quantities updated successfully");
    } catch (error) {
      console.error("Error updating quantities:", error);
      res.status(500).send("Error updating data in Google Sheets");
    }
  }
);

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

app.json

{
  "expo": {
    "name": "Expense Tracker",
    "slug": "expense-g45",
    "version": "1.0.5",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "supportsTablet": true
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.myname.expense-g45"
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "extra": {
      "eas": {
        "projectId": "po092345234-e461-48d2-9a7b-sdfbdte2342"
      }
    },
    "newArchEnabled": true,
    "scheme": "expensetracker",
    "plugins": [
      "expo-router"
    ]
  }
}

eas.json

{
  "build": {
    "preview": {
      "android": {
        "buildType": "apk"
      },
      "autoIncrement": true
    },
    "preview2": {
      "android": {
        "gradleCommand": ":app:assembleRelease"
      }
    },
    "preview3": {
      "developmentClient": true
    },
    "preview4": {
      "distribution": "internal"
    },
    "production": {}
  },
  "cli": {
    "appVersionSource": "remote"
  }
}

package.json

{
  "name": "expense-g45",
  "version": "1.0.5",
  "main": "expo-router/entry",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.0.4",
    "@react-native-async-storage/async-storage": "1.23.1",
    "dotenv": "^16.4.7",
    "expo": "~52.0.26",
    "expo-router": "~4.0.17",
    "expo-status-bar": "~2.0.1",
    "express": "^4.21.1",
    "google-auth-library": "^9.14.2",
    "google-spreadsheet": "^4.1.4",
    "jsonwebtoken": "^9.0.2",
    "react": "18.3.1",
    "react-native": "0.76.6",
    "react-native-safe-area-context": "4.12.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@types/react": "~18.3.12",
    "nodemon": "^3.1.9",
    "typescript": "^5.3.3"
  },
  "private": true,
  "expo": {
    "doctor": {
      "reactNativeDirectoryCheck": {
        "listUnknownPackages": false
      }
    }
  }
}

I更改了以前是

"newArchEnabled": false,
true
react-native
1个回答
0
投票

反应新的安全区域封闭式
反应新屏幕
反应本机手机

反应新启用
  1. 解决了问题。
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.