最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

react native - Struck in white screen after splash screen in expo app - Stack Overflow

programmeradmin1浏览0评论

I am developing a React native app for Android which will change some cell values of a Google sheet. It worked well before I made significant changes to incorporate async-storage and AuthContext.

My app is structured like this:

app/index.tsx:

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(
        ";,
        {
          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 = ";;
  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 = [
  ";,
  ".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
      }
    }
  }
}
发布评论

评论列表(0)

  1. 暂无评论