From e255afcfd802256a205aae51ab984f6b5a675cae Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Sat, 2 May 2026 12:47:59 -0700 Subject: [PATCH 1/8] initial commit (remove everything) --- App.js | 55 +------- app.config.js | 17 +-- package-lock.json | 32 ----- package.json | 2 - src/components/HomeHeader.js | 32 +---- src/components/Profile.js | 81 ------------ src/components/UploadButton.js | 28 ++-- src/screens/AccountScreen.js | 123 ------------------ src/screens/HomeScreen.js | 5 +- src/screens/SignInScreen.js | 195 ---------------------------- src/screens/VolunteerFormScreen.js | 7 - src/utils/forms/DanceClub.js | 12 +- src/utils/forms/Form.js | 5 +- src/utils/forms/LibraryMusicHour.js | 15 +-- src/utils/index.js | 21 --- 15 files changed, 28 insertions(+), 602 deletions(-) delete mode 100644 src/components/Profile.js delete mode 100644 src/screens/AccountScreen.js delete mode 100644 src/screens/SignInScreen.js diff --git a/App.js b/App.js index 80ff524..0bae637 100644 --- a/App.js +++ b/App.js @@ -7,88 +7,44 @@ */ import "@expo/metro-runtime"; -import AsyncStorage from "@react-native-async-storage/async-storage"; import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { StatusBar } from "expo-status-bar"; -import React, { useEffect, useState } from "react"; -import { ActivityIndicator, View } from "react-native"; +import React from "react"; +import { View } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; -import AccountScreen from "./src/screens/AccountScreen"; import EmbeddedFormScreen from "./src/screens/EmbeddedFormScreen"; import EndScreen from "./src/screens/EndScreen"; import HomeScreen from "./src/screens/HomeScreen"; -import SignInScreen from "./src/screens/SignInScreen"; import VolunteerFormScreen from "./src/screens/VolunteerFormScreen"; import VolunteerOpportunityScreen from "./src/screens/VolunteerOpportunityScreen"; import HomeHeader from "./src/components/HomeHeader"; import NoInternetBanner from "./src/components/NoInternetBanner"; import colors from "./src/constants/colors"; -import { alertError, navigationRef } from "./src/utils"; +import { navigationRef } from "./src/utils"; const Stack = createNativeStackNavigator(); export default function App() { - // Local state: loading indicator and logged-in status - const [loading, setLoading] = useState(true); - const [isLoggedIn, setIsLoggedIn] = useState(false); - - useEffect(() => { - // On mount, retrieve user info to determine if already signed in - (async () => { - try { - const userString = await AsyncStorage.getItem("user"); - setIsLoggedIn(userString != null); - } catch (error) { - // Alert on any errors retrieving user data - alertError(`While getting user in App: ${error}`); - } finally { - // Hide loading spinner after check - setLoading(false); - } - })(); - }, []); - - // Show a full-screen spinner while initializing app state - if (loading) { - return ; - } - - // Once ready, render the navigation container with all screens return ( - {/* Light-content status bar to match header styling */} - {/* Sign In screen for authentication */} - - {/* Main Home screen with custom header */} }} /> - {/* Account management screen */} - - {/* Detailed volunteer opportunity view */} - {/* Form submission screen for events */} - {/* Embedded Google Forms web view */} - {/* End screen showing submission success/failure */} =0.59" } }, - "node_modules/@react-native-google-signin/google-signin": { - "version": "12.2.1", - "resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-12.2.1.tgz", - "integrity": "sha512-qt2Cb+bN2NS8BBT06M3UbLVS1gaSYTenNh4uZtPNmyJC23xZAynLkuhvsC6gmeEnxcj0wLVUn1Es+LSHw0OhVQ==", - "license": "MIT", - "peerDependencies": { - "expo": ">=50.0.0", - "react": "*", - "react-dom": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/@react-native-picker/picker": { "version": "2.11.1", "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz", @@ -5934,16 +5912,6 @@ } } }, - "node_modules/expo-apple-authentication": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/expo-apple-authentication/-/expo-apple-authentication-8.0.8.tgz", - "integrity": "sha512-TwCHWXYR1kS0zaeV7QZKLWYluxsvqL31LFJubzK30njZqeWoWO89HZ8nZVaeXbFV1LrArKsze4BmMb+94wS0AQ==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo-application": { "version": "7.0.8", "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz", diff --git a/package.json b/package.json index 172fd1e..b87fa4f 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,12 @@ "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/cli-server-api": "^13.6.9", "@react-native-community/netinfo": "^11.4.1", - "@react-native-google-signin/google-signin": "^12.2.1", "@react-native-picker/picker": "2.11.1", "@react-navigation/core": "^7.12.4", "@react-navigation/native": "^6.1.17", "@react-navigation/native-stack": "^6.10.0", "@robinbobin/react-native-google-drive-api-wrapper": "^1.2.4", "expo": "^54.0.10", - "expo-apple-authentication": "~8.0.8", "expo-checkbox": "~5.0.8", "expo-constants": "~18.0.12", "expo-dev-client": "~6.0.20", diff --git a/src/components/HomeHeader.js b/src/components/HomeHeader.js index d526786..b5b762b 100644 --- a/src/components/HomeHeader.js +++ b/src/components/HomeHeader.js @@ -6,49 +6,21 @@ * - route: React Navigation route prop to detect focus changes */ -import FontAwesome from "@expo/vector-icons/FontAwesome"; import { LinearGradient } from "expo-linear-gradient"; -import { useEffect, useState } from "react"; -import { Image, Pressable, SafeAreaView, StyleSheet, Text } from "react-native"; +import { SafeAreaView, StyleSheet, Text } from "react-native"; import colors from "../constants/colors"; -import { getUser } from "../utils"; - -export default function HomeHeader({ navigation, route }) { - // Local state for current user info - const [user, setUser] = useState(null); - - // Fetch user from AsyncStorage whenever screen focus changes - useEffect(() => { - getUser(true).then(setUser); - }, [route]); +export default function HomeHeader() { return ( - {/* App title */} Audacity Sign Up - {/* Profile icon or user photo navigates to Account screen */} - navigation.navigate("Account")}> - {user?.photo ? ( - - ) : ( - - )} - ); diff --git a/src/components/Profile.js b/src/components/Profile.js deleted file mode 100644 index 7a61afd..0000000 --- a/src/components/Profile.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Profile.js - * Displays logged-in user's name, email, and profile image or icon. - * Retrieves user info from AsyncStorage on mount. - */ -import FontAwesome from "@expo/vector-icons/FontAwesome"; -import { useEffect, useState } from "react"; -import { Image, StyleSheet, Text, View } from "react-native"; - -import { getUser } from "../utils"; - -export default function Profile() { - // Local user state - const [user, setUser] = useState(null); - - // Load user data on mount - useEffect(() => { - getUser().then(setUser); - }, []); - - // Default display values while loading or empty - let name = user?.name ?? "Loading"; - if (name === "") name = "Anonymous Apple User"; - let email = user?.email ?? "Loading"; - if (email === "") email = "apple.com"; - - return ( - - {/* User photo if available, else default icon */} - {user?.photo ? ( - - ) : ( - - )} - {/* Name and email text */} - - - {name} - - - {email} - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: "row", - alignItems: "center", - backgroundColor: "#f5f5f5", // Light grey background color - padding: 15, - borderRadius: 10, - borderColor: "#e0e0e0", - borderWidth: 1, - margin: 10, - }, - image: { - width: 60, - height: 60, - borderRadius: 100, - marginRight: 10, - }, - name: { - fontSize: 16, - fontWeight: "bold", - }, - email: { - fontSize: 14, - color: "#555", - }, -}); diff --git a/src/components/UploadButton.js b/src/components/UploadButton.js index a744e24..fbad9ce 100644 --- a/src/components/UploadButton.js +++ b/src/components/UploadButton.js @@ -14,7 +14,6 @@ */ import Feather from "@expo/vector-icons/Feather"; -import AsyncStorage from "@react-native-async-storage/async-storage"; import { GDrive, MimeTypes, @@ -42,11 +41,20 @@ const selectFile = async () => { } }; -// Helper: retrieve stored Google access token for API calls async function getAccessToken() { try { - const accessToken = await AsyncStorage.getItem("access-token"); - return accessToken; + const res = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID, + client_secret: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_SECRET, + refresh_token: process.env.EXPO_PUBLIC_GOOGLE_REFRESH_TOKEN, + grant_type: "refresh_token", + }).toString(), + }); + const json = await res.json(); + return json.access_token ?? null; } catch (error) { alertError(`In getAccessToken: ${error}`); } @@ -56,7 +64,6 @@ export default function UploadButton({ title, state, setState, - navigation, required = false, }) { // Local UI state for showing selected file name @@ -89,17 +96,10 @@ export default function UploadButton({ // Retrieve access token const accessToken = await getAccessToken(); if (!accessToken) { - // Prompt user to re-login on failure Alert.alert( "File upload is unavailable", - "Please log in with a Google account.", - [ - { - text: "Go to Profile", - onPress: () => navigation.navigate("Account"), - }, - { text: "Cancel", style: "cancel" }, - ], + "Could not authenticate for file upload. Please check your connection and try again.", + [{ text: "OK", style: "cancel" }], ); return; } diff --git a/src/screens/AccountScreen.js b/src/screens/AccountScreen.js deleted file mode 100644 index e927258..0000000 --- a/src/screens/AccountScreen.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - * AccountScreen.js - * Allows the user to clear all app data or log out of their account. - * - Presents Profile component - * - Provides buttons to clear local storage or log out - */ - -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { GoogleSignin } from "@react-native-google-signin/google-signin"; -import Constants from "expo-constants"; -import { Alert, StyleSheet, View } from "react-native"; - -import FullWidthButton from "../components/FullWidthButton.js"; -import Profile from "../components/Profile.js"; - -import colors from "../constants/colors"; -import { alertError } from "../utils/index.js"; - -export default function AccountScreen({ navigation }) { - return ( - - {/* User profile summary */} - - - {/* Clear all local data and sign out */} - { - Alert.alert( - "Are you sure you want to clear all data on this device?", - `You will be logged out and prompted to sign in if you proceed. To clear data that you have submitted to Audacity Sign Up in previous forms, please contact the IT Team at ${Constants.expoConfig.extra.email}.`, - [ - { - text: "Cancel", - onPress: () => {}, - isPreferred: true, - }, - { - text: "Continue", - onPress: async () => { - if (GoogleSignin.getCurrentUser() != null) { - try { - await GoogleSignin.revokeAccess(); - await GoogleSignin.signOut(); - } catch (error) { - alertError( - `Unable to log out and remove Google account while clearing data: ${error}`, - ); - } - } - try { - await AsyncStorage.clear(); - } catch (error) { - alertError(`Unable to clear data: ${error}`); - } - navigation.navigate("Sign In"); - }, - }, - ], - ); - }} - > - Clear Data - - - {/* Log out of Google or local user */} - { - Alert.alert("Are you sure you want to log out?", null, [ - { - text: "Cancel", - onPress: () => {}, - isPreferred: true, - }, - { - text: "Continue", - onPress: async () => { - if (GoogleSignin.getCurrentUser() != null) { - try { - await GoogleSignin.signOut(); - } catch (error) { - alertError(`Unable to log out of Google: ${error}`); - } - } - try { - await AsyncStorage.removeItem("user"); - await AsyncStorage.removeItem("access-token"); - } catch (error) { - alertError(`Unable to remove user data: ${error}`); - } - navigation.navigate("Sign In"); - }, - }, - ]); - }} - > - Log Out - - - ); -} - -const styles = StyleSheet.create({ - container: { - height: "100%", - }, - clearDataButton: { - borderColor: colors.danger, - borderWidth: 1, - }, - clearDataText: { - color: colors.danger, - }, - logOutButton: { - backgroundColor: colors.danger, - }, - logOutText: { - color: colors.white, - }, -}); diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 2547caa..2399604 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -18,7 +18,6 @@ import Websites from "../components/Websites"; import { alertError, formatDate, - getUser, hashForm, request, strToDate, @@ -113,8 +112,6 @@ export default function HomeScreen({ navigation, route }) { return null; } const newData = []; - const user = await getUser(); - const currentDate = new Date(); const twoMonthsLater = new Date(); @@ -146,7 +143,7 @@ export default function HomeScreen({ navigation, route }) { continue; } const hash = hashForm( - user.id, + "local", opportunity.Title, opportunity.Location, formatDate(opportunity.Date), diff --git a/src/screens/SignInScreen.js b/src/screens/SignInScreen.js deleted file mode 100644 index 95544bd..0000000 --- a/src/screens/SignInScreen.js +++ /dev/null @@ -1,195 +0,0 @@ -/** - * SignInScreen.js - * Handles user authentication via Google and Apple sign-in. - * - Configures GoogleSignin on load - * - Provides buttons for Google and Apple login - * - Stores user info and access token in AsyncStorage - * - Custom Apple sign-in button (icon size adjustable) - */ - -import { - Image, - Platform, - Pressable, - SafeAreaView, - StyleSheet, - Text, - View, -} from "react-native"; - -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { - GoogleSignin, - isErrorWithCode, - statusCodes, -} from "@react-native-google-signin/google-signin"; -import * as AppleAuth from "expo-apple-authentication"; - -import { alertError } from "../utils"; - -// Initialize Google Sign-In configuration -GoogleSignin.configure({ - webClientId: - process.env.EXPO_PUBLIC_GOOGLE_OAUTH_WEB_ID ?? - alertError("Undefined EXPO_PUBLIC_GOOGLE_OAUTH_WEB_ID env variable"), - iosClientId: - process.env.EXPO_PUBLIC_GOOGLE_OAUTH_IOS_ID ?? - alertError("Undefined EXPO_PUBLIC_GOOGLE_OAUTH_IOS_ID env variable"), - scopes: [ - "https://www.googleapis.com/auth/userinfo.profile", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/drive.file", - "openid", - ], - offlineAccess: true, -}); - -export default function SignInScreen({ navigation }) { - // Apple sign-in logic - const handleAppleSignIn = async () => { - try { - const isAvailable = await AppleAuth.isAvailableAsync(); - if (!isAvailable) { - alertError("Apple Sign-In is not available on this device."); - return; - } - // Perform Apple sign-in and request full name and email scopes - const credential = await AppleAuth.signInAsync({ - requestedScopes: [ - AppleAuth.AppleAuthenticationScope.FULL_NAME, - AppleAuth.AppleAuthenticationScope.EMAIL, - ], - }); - // Extract user info - const { user, email, fullName } = credential; - // Apple only provides fullName and email the first time - await AsyncStorage.setItem( - "user", - JSON.stringify({ - id: user, - name: fullName - ? [fullName.givenName, fullName.familyName] - .filter(Boolean) - .join(" ") - : "", - email: email ?? "", - }), - ); - await AsyncStorage.removeItem("access-token"); - navigation.navigate("Home", { forceRerender: true }); - } catch (error) { - if (error.code !== "ERR_REQUEST_CANCELED") { - alertError(`While signing in with Apple: (${error.code}) ${error}`); - } - } - }; - - const handleGoogleSignIn = async () => { - try { - await GoogleSignin.hasPlayServices(); - const userInfo = await GoogleSignin.signIn(); - await AsyncStorage.setItem("user", JSON.stringify(userInfo.user)); - await AsyncStorage.setItem( - "access-token", - (await GoogleSignin.getTokens()).accessToken, - ); - navigation.navigate("Home", { forceRerender: true }); - } catch (error) { - if (isErrorWithCode(error)) { - if (error.code === statusCodes.SIGN_IN_CANCELLED) { - return; - } - alertError(`While signing in with Google: (${error.code}) ${error}`); - } else { - alertError(`While signing in with Google: (no error code) ${error}`); - } - } - }; - - return ( - - - {/* Introductory text */} - - Thank you for choosing to help make our volunteer opportunities and - concerts across the Bay Area a success! To begin, please sign in. - - {/* Google Sign-In button */} - - - - {" "} - Sign in with Google - - - - {/* Custom Apple Sign-In button */} - {Platform.OS === "ios" ? ( - - - - {" "} - Sign in with Apple - - - ) : null} - - - ); -} - -const styles = StyleSheet.create({ - container: { - backgroundColor: "#fff", - padding: "1%", - height: "100%", - }, - body: { - alignItems: "center", - justifyContent: "center", - }, - paragraph: { - fontSize: 18, - padding: 20, - }, - OAuth: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - width: 270, - height: 50, - borderRadius: 5, - margin: 5, - }, - OAuthBackground: { - backgroundColor: "#000", - }, - OAuthLogo: { - width: 30, - height: 30, - resizeMode: "contain", - }, - OAuthText: { - color: "#fff", - fontSize: 18, - fontWeight: "600", - }, - loading: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - fontSize: 20, - }, -}); diff --git a/src/screens/VolunteerFormScreen.js b/src/screens/VolunteerFormScreen.js index ff9f180..f615b93 100644 --- a/src/screens/VolunteerFormScreen.js +++ b/src/screens/VolunteerFormScreen.js @@ -183,7 +183,6 @@ export default function VolunteerFormScreen({ navigation, route }) { invalidResponses = 1; // Assume invalid if error occurs } if (invalidResponses > 0) { - // Show error alert with count of invalid questions const questionText = invalidResponses === 1 ? "question" : "questions"; const hasText = invalidResponses === 1 ? "has" : "have"; @@ -192,17 +191,11 @@ export default function VolunteerFormScreen({ navigation, route }) { `${invalidResponses} ${questionText} ${hasText} invalid or missing responses. Please fix all responses highlighted in red to submit this form.`, [{ text: "OK" }], ); - // Don't show "Submitting..." if validation fails - // Do not call form.submit() here; error alert is already shown } else { - // Only show "Submitting..." if validation passes setButtonText("Submitting..."); await form.submit(); setButtonText("Submit"); } - setButtonText("Submitting..."); - await form.submit(); - setButtonText("Submit"); }} > {buttonText} diff --git a/src/utils/forms/DanceClub.js b/src/utils/forms/DanceClub.js index a3103e6..e1f3121 100644 --- a/src/utils/forms/DanceClub.js +++ b/src/utils/forms/DanceClub.js @@ -5,12 +5,9 @@ * - Fields include full name, contact, favorite pieces, age group, styles, and consents */ -import { useEffect } from "react"; - import { Question, emptyQuestionState, - getUser, isAtLeast, isExactly, isNotEmpty, @@ -35,14 +32,7 @@ export default class DanceClub extends Form { super("Audacity Dance Club", date, location, navigation, scrollRef); // State hooks for each question - this.fullName = emptyQuestionState(); // Performer name - useEffect(() => { - // Prefill name if available - (async () => { - const user = await getUser(); - this.fullName[1]((prev) => ({ ...prev, value: user?.name })); - })(); - }, []); + this.fullName = emptyQuestionState(); this.phoneNumber = emptyQuestionState(); // Contact number this.favoritePieces = emptyQuestionState(["", "", "", ""]); // Top 4 music pieces this.age = emptyQuestionState(); // Age selection diff --git a/src/utils/forms/Form.js b/src/utils/forms/Form.js index 1eb098c..1f35d96 100644 --- a/src/utils/forms/Form.js +++ b/src/utils/forms/Form.js @@ -9,7 +9,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { Alert } from "react-native"; -import { alertError, getUser, hashForm, request } from ".."; +import { alertError, hashForm, request } from ".."; import formIDs from "../../constants/formIDs"; /** @@ -184,8 +184,7 @@ export default class Form { return; } - const user = await getUser(); - const hash = hashForm(user.id, this.title, this.location, this.date); + const hash = hashForm("local", this.title, this.location, this.date); try { const submittedForms = await AsyncStorage.getItem("submittedForms"); diff --git a/src/utils/forms/LibraryMusicHour.js b/src/utils/forms/LibraryMusicHour.js index a1a9279..4a3b847 100644 --- a/src/utils/forms/LibraryMusicHour.js +++ b/src/utils/forms/LibraryMusicHour.js @@ -5,12 +5,11 @@ * Fields include performer info, music selection, performance type, and permissions. */ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Question, emptyQuestionState, - getUser, isAtLeast, isNotEmpty, } from ".."; @@ -33,15 +32,7 @@ export default class LibraryMusicHour extends Form { super("Library Music Hour", date, location, navigation, scrollRef); // Question states: [stateValue, setState] hooks for each input - this.fullName = emptyQuestionState(); // Performer's name - - // Prefill fullName from stored user on mount - useEffect(() => { - (async () => { - const user = await getUser(); - this.fullName[1]((prev) => ({ ...prev, value: user?.name })); - })(); - }, []); + this.fullName = emptyQuestionState(); // Other fields this.city = emptyQuestionState(); // City of residence @@ -305,7 +296,6 @@ export default class LibraryMusicHour extends Form { key="pianoAccompaniment" state={this.pianoAccompaniment[0]} setState={this.pianoAccompaniment[1]} - navigation={this.navigation} /> ), validate: () => this.pianoAccompaniment[0].value != "Uploading", @@ -320,7 +310,6 @@ export default class LibraryMusicHour extends Form { key="ensembleProfile" state={this.ensembleProfile[0]} setState={this.ensembleProfile[1]} - navigation={this.navigation} required={true} /> ), diff --git a/src/utils/index.js b/src/utils/index.js index 328810c..03ad687 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -13,7 +13,6 @@ * - openInMaps: launch maps app for a location */ -import AsyncStorage from "@react-native-async-storage/async-storage"; import Constants from "expo-constants"; import { createNavigationContainerRef } from "@react-navigation/native"; import { useState } from "react"; @@ -101,26 +100,6 @@ export function maybeOpenURL(url, appName, appStoreID, playStoreID) { }); } -/** - * Retrieve the stored user object from AsyncStorage. - * @param {boolean} [isEmptySafe=false] - * @returns {Promise} - */ -export async function getUser(isEmptySafe = false) { - try { - const userString = await AsyncStorage.getItem("user"); - if (userString === null) { - if (!isEmptySafe) { - alertError("Empty getUser"); - } - return; - } - return JSON.parse(userString); - } catch (error) { - alertError("Unexpected error in getUser: " + error); - } -} - /** * Utility delay function for retry backoff. */ From b32a3df0aef83defcaf484657ca8d367a1a9b6f8 Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Sat, 2 May 2026 17:18:21 -0700 Subject: [PATCH 2/8] add email questions Co-authored-by: Copilot --- src/constants/formIDs.js | 4 ++++ src/screens/VolunteerFormScreen.js | 6 ++++++ src/utils/forms/DanceClub.js | 17 ++++++++++++++++- src/utils/forms/LibraryMusicHour.js | 15 +++++++++++++++ src/utils/forms/RequestConcert.js | 18 ++++++++++++++++-- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/constants/formIDs.js b/src/constants/formIDs.js index 07b18da..aac2d16 100644 --- a/src/constants/formIDs.js +++ b/src/constants/formIDs.js @@ -5,6 +5,7 @@ export default { date: "785137847", fullName: "194705108", city: "171289053", + email: "2039184033", phoneNumber: "1427621981", age: "2098203720", musicPiece: "762590043", @@ -26,6 +27,7 @@ export default { date: "785137847", fullName: "194705108", city: "171289053", + email: "1215232098", phoneNumber: "1427621981", age: "2098203720", musicPiece: "762590043", @@ -41,6 +43,7 @@ export default { }, "Request a Concert": { id: "1FAIpQLScBBGfjQ397ol1n6HLFIwAtjLxjc4Sw9mDWWYlVHXdbQ3Hl5g", + email: "722836817", phoneNumber: "1828463866", organization: "1450595390", eventInfo: "411385951", @@ -59,6 +62,7 @@ export default { "Audacity Dance Club": { id: "1FAIpQLSeeehgsi7DOXDSlX-X0p_lnCnXHrvIIPtoNsexRIs1xhcPZXA", fullName: "1028625525", + email: "1183890095", phoneNumber: "503217771", favoritePieces: "183639428", age: "529415268", diff --git a/src/screens/VolunteerFormScreen.js b/src/screens/VolunteerFormScreen.js index f615b93..39423c6 100644 --- a/src/screens/VolunteerFormScreen.js +++ b/src/screens/VolunteerFormScreen.js @@ -28,6 +28,7 @@ import { alertError, sendErrorEmail, openInMaps } from "../utils"; import DanceClub from "../utils/forms/DanceClub"; import LibraryMusicHour from "../utils/forms/LibraryMusicHour"; import MusicByTheTracks from "../utils/forms/MusicByTheTracks"; +import RequestConcert from "../utils/forms/RequestConcert"; import colors from "../constants/colors"; import formIDs from "../constants/formIDs"; @@ -37,6 +38,11 @@ function getForm(title, date, location, navigation, scrollRef) { // Define form options with their exact names and constructors const formOptions = [ + { + name: "REQUEST A CONCERT", + constructor: RequestConcert, + aliases: ["Request a Concert", "Request Concert"], + }, { name: "LIBRARY MUSIC HOUR", constructor: LibraryMusicHour, diff --git a/src/utils/forms/DanceClub.js b/src/utils/forms/DanceClub.js index e1f3121..71292ae 100644 --- a/src/utils/forms/DanceClub.js +++ b/src/utils/forms/DanceClub.js @@ -33,7 +33,8 @@ export default class DanceClub extends Form { // State hooks for each question this.fullName = emptyQuestionState(); - this.phoneNumber = emptyQuestionState(); // Contact number + this.email = emptyQuestionState(); + this.phoneNumber = emptyQuestionState(); this.favoritePieces = emptyQuestionState(["", "", "", ""]); // Top 4 music pieces this.age = emptyQuestionState(); // Age selection this.favoriteDanceStyles = emptyQuestionState([]); // Styles selection @@ -60,6 +61,20 @@ export default class DanceClub extends Form { validate: (value) => value.trim().split(" ").length >= 2, }), + new Question({ + name: "email", + component: ( + + ), + validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + }), + new Question({ name: "phoneNumber", component: ( diff --git a/src/utils/forms/LibraryMusicHour.js b/src/utils/forms/LibraryMusicHour.js index 4a3b847..91de4ca 100644 --- a/src/utils/forms/LibraryMusicHour.js +++ b/src/utils/forms/LibraryMusicHour.js @@ -33,6 +33,7 @@ export default class LibraryMusicHour extends Form { // Question states: [stateValue, setState] hooks for each input this.fullName = emptyQuestionState(); + this.email = emptyQuestionState(); // Other fields this.city = emptyQuestionState(); // City of residence @@ -97,6 +98,20 @@ export default class LibraryMusicHour extends Form { validate: isNotEmpty, }), + new Question({ + name: "email", + component: ( + + ), + validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + }), + new Question({ name: "phoneNumber", component: ( diff --git a/src/utils/forms/RequestConcert.js b/src/utils/forms/RequestConcert.js index 11b1e32..cd7a151 100644 --- a/src/utils/forms/RequestConcert.js +++ b/src/utils/forms/RequestConcert.js @@ -26,7 +26,8 @@ export default class RequestConcert extends Form { super("Request a Concert", date, location, navigation, scrollRef); // State hooks for each question field - this.phoneNumber = emptyQuestionState(); // Contact phone + this.email = emptyQuestionState(); + this.phoneNumber = emptyQuestionState(); this.organization = emptyQuestionState(); // Org name/description this.eventInfo = emptyQuestionState(); // Event details this.venue = emptyQuestionState(); // Venue info @@ -48,7 +49,20 @@ export default class RequestConcert extends Form { */ questions() { return [ - // Phone number field + new Question({ + name: "email", + component: ( + + ), + validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + }), + new Question({ name: "phoneNumber", component: ( From aa049694201e5659ad284db1c358b4b2cb59e359 Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Mon, 4 May 2026 20:01:36 -0700 Subject: [PATCH 3/8] fix upload button --- src/components/UploadButton.js | 213 +++++++++++++-------------------- 1 file changed, 83 insertions(+), 130 deletions(-) diff --git a/src/components/UploadButton.js b/src/components/UploadButton.js index fbad9ce..9b227c9 100644 --- a/src/components/UploadButton.js +++ b/src/components/UploadButton.js @@ -1,15 +1,15 @@ /** * UploadButton.js * Provides a PDF upload button using Google Drive API: - * - Selects a PDF file - * - Uploads to Google Drive + * - Selects a PDF file from the device + * - Authenticates via stored OAuth refresh token (no user sign-in required) + * - Uploads to Google Drive with a timestamped unique filename * - Sets sharing to public * - Updates form state with Drive URL * Props: * - title: label text for the upload field * - state: { value: url|string, y: number, valid: boolean } * - setState: setter to update component state - * - navigation: React Navigation prop for redirection * - required: whether a file is mandatory */ @@ -26,210 +26,166 @@ import * as fs from "react-native-fs"; import colors from "../constants/colors"; import { alertError } from "../utils"; -// Helper: launch document picker for PDF selection +const CLIENT_ID = process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID ?? ""; +const CLIENT_SECRET = process.env.EXPO_PUBLIC_GOOGLE_CLIENT_SECRET ?? ""; +const REFRESH_TOKEN = process.env.EXPO_PUBLIC_GOOGLE_REFRESH_TOKEN ?? ""; + +function uniqueFileName(original) { + const now = new Date(); + const pad = (n) => String(n).padStart(2, "0"); + const stamp = + `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` + + `_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; + const base = (original.endsWith(".pdf") ? original.slice(0, -4) : original) + .replace(/[^a-zA-Z0-9_-]/g, "_"); + return `${stamp}_${base}.pdf`; +} + +async function getAccessToken() { + const res = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + refresh_token: REFRESH_TOKEN, + grant_type: "refresh_token", + }).toString(), + }); + const json = await res.json(); + return json.access_token ?? null; +} + const selectFile = async () => { try { - const file = await DocumentPicker.pickSingle({ + return await DocumentPicker.pickSingle({ type: [DocumentPicker.types.pdf], }); - return file; } catch (error) { - if (DocumentPicker.isCancel(error)) { - return null; - } + if (DocumentPicker.isCancel(error)) return null; alertError(`In selectFile: ${error}`); } }; -async function getAccessToken() { - try { - const res = await fetch("https://oauth2.googleapis.com/token", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID, - client_secret: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_SECRET, - refresh_token: process.env.EXPO_PUBLIC_GOOGLE_REFRESH_TOKEN, - grant_type: "refresh_token", - }).toString(), - }); - const json = await res.json(); - return json.access_token ?? null; - } catch (error) { - alertError(`In getAccessToken: ${error}`); - } -} - export default function UploadButton({ title, state, setState, required = false, }) { - // Local UI state for showing selected file name const [fileName, setFileName] = useState(null); return ( { - // Capture Y position for scroll-to-error behavior const y = event.nativeEvent.layout.y; setState((prev) => ({ ...prev, y })); }} > - {/* Field title and required marker */} {title} {required && *} - {/* Display chosen file name */} {fileName && ( {fileName} )} - {/* Upload button triggers file selection and upload flow */} { - // Retrieve access token - const accessToken = await getAccessToken(); - if (!accessToken) { + const file = await selectFile(); + if (file == null) { + setFileName(null); + setState((prev) => ({ ...prev, value: null })); + return; + } + + if (file.size > 104857600) { Alert.alert( - "File upload is unavailable", - "Could not authenticate for file upload. Please check your connection and try again.", - [{ text: "OK", style: "cancel" }], + "File size too large", + `${file.name} exceeds the 100 MB limit. Please compress it and try again.`, ); + setFileName("Upload too large"); + setState((prev) => ({ ...prev, value: null })); return; } - // Indicate uploading state - setState((prev) => ({ ...prev, value: "Uploading" })); - const googleDrive = new GDrive(); - googleDrive.accessToken = accessToken; - googleDrive.fetchTimeout = 20000; // 20 seconds + setFileName("Uploading..."); + setState((prev) => ({ ...prev, value: "Uploading" })); - const file = await selectFile(); - if (file == null) { - // Cancel - setFileName(null); - setState((prevState) => ({ ...prevState, value: null })); + const accessToken = await getAccessToken(); + if (!accessToken) { + Alert.alert( + "File upload is unavailable", + "Could not authenticate for file upload. Please check your connection and try again.", + ); + setFileName("Upload failed"); + setState((prev) => ({ ...prev, value: null })); return; } let fileData; try { fileData = await fs.readFile(file.uri, "base64"); - } catch (error) { - setFileName("Upload failed"); - setState((prevState) => ({ ...prevState, value: null })); - - const fileName = file.name?.endsWith(".pdf") - ? file.name.slice(0, file.name.length - 4) + } catch { + const base = file.name?.endsWith(".pdf") + ? file.name.slice(0, -4) : file.name; - Alert.alert( "Invalid file", - `Please make sure your file (${fileName}) is a single PDF that can be accessed by the app.`, + `Could not read ${base}. Make sure it is a single accessible PDF.`, ); + setFileName("Upload failed"); + setState((prev) => ({ ...prev, value: null })); return; } - let id = ""; + + const googleDrive = new GDrive(); + googleDrive.accessToken = accessToken; + googleDrive.fetchTimeout = 20000; try { - setFileName("Uploading..."); - const googleFile = await googleDrive.files + const uploaded = await googleDrive.files .newMultipartUploader() .setIsBase64(true) .setData(fileData, MimeTypes.PDF) .setRequestBody({ parents: ["root"], - name: file.name, + name: uniqueFileName(file.name), }) .execute(); - id = googleFile.id; + await googleDrive.permissions.create( - id, + uploaded.id, {}, - { - role: "reader", - type: "anyone", - }, + { role: "reader", type: "anyone" }, ); + + setFileName(file.name); + setState((prev) => ({ + ...prev, + value: `https://drive.google.com/open?id=${uploaded.id}`, + })); } catch (error) { - if (error.name == "AbortError") { - Alert.alert( - "File upload aborted", - "Your file could not be uploaded due to a connection error. Please check your Internet connection and try again.", - ); - } else if (error.__response?.status == 401) { + if (error.name === "AbortError") { Alert.alert( - "Invalid Session", - "Your authentication session is malformed, most likely because your device has old data that is not compatible with the current version of the app. To fix this issue, please go to your profile, clear your data, and log in again with your Google account. We apologize for the inconvenience.", - [ - { - text: "Go to Profile", - onPress: () => { - navigation.navigate("Account"); - }, - }, - { - text: "Cancel", - isPreferred: true, - }, - ], - ); - } else if (error.__response?.status == 403) { - Alert.alert( - "Permission Denied", - 'Audacity Sign Up does not have permission to share files associated with your Google account.\n\nTo enable this feature, please go to your profile, clear your data, and reauthenticate your Google account. When prompted to select what the app can access, tap on the checkbox to "see, edit, create, and delete Google files in this app."', - [ - { - text: "Go to Profile", - onPress: () => { - navigation.navigate("Account"); - }, - }, - { - text: "Cancel", - isPreferred: true, - }, - ], + "Upload timed out", + "Check your internet connection and try again.", ); } else { alertError("Error uploading file: " + error); } setFileName("Upload failed"); - setState((prevState) => ({ ...prevState, value: null })); - return; - } - - if (file.size > 104857600) { - // 100 MB - Alert.alert( - "File size too large", - `Your file ${file.name} has a size of ${file.size} bytes. Please compress your file and upload a smaller version.`, - ); - setFileName("Upload too large"); - setState((prevState) => ({ ...prevState, value: null })); - return; + setState((prev) => ({ ...prev, value: null })); } - - setFileName(file.name); - - setState((prevState) => ({ - ...prevState, - value: `https://drive.google.com/open?id=${id}`, - })); }} > - {/* Upload icon and label */} Upload - {/* PDF-only notice and validation info */} PDF only - 100MB Limit @@ -252,14 +208,12 @@ const styles = StyleSheet.create({ fontWeight: "700", paddingBottom: 10, }, - otherInfo: { alignSelf: "center", textAlign: "center", marginVertical: 5, fontSize: 14, }, - upload: { flexDirection: "row", justifyContent: "center", @@ -270,7 +224,6 @@ const styles = StyleSheet.create({ paddingVertical: 10, paddingHorizontal: 15, }, - uploadText: { fontSize: 18, paddingLeft: "3%", From e2d60ed68bb838c1ac3087db04d782117cd94cf7 Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Mon, 4 May 2026 20:24:20 -0700 Subject: [PATCH 4/8] style & remove mark events as completed feature --- src/components/UploadButton.js | 5 +++-- src/screens/HomeScreen.js | 26 +---------------------- src/screens/VolunteerOpportunityScreen.js | 14 ++---------- src/utils/forms/Form.js | 19 +---------------- src/utils/forms/LibraryMusicHour.js | 7 +----- 5 files changed, 8 insertions(+), 63 deletions(-) diff --git a/src/components/UploadButton.js b/src/components/UploadButton.js index 9b227c9..467d41b 100644 --- a/src/components/UploadButton.js +++ b/src/components/UploadButton.js @@ -36,8 +36,9 @@ function uniqueFileName(original) { const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` + `_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; - const base = (original.endsWith(".pdf") ? original.slice(0, -4) : original) - .replace(/[^a-zA-Z0-9_-]/g, "_"); + const base = ( + original.endsWith(".pdf") ? original.slice(0, -4) : original + ).replace(/[^a-zA-Z0-9_-]/g, "_"); return `${stamp}_${base}.pdf`; } diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 2399604..f85d2ba 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -8,20 +8,13 @@ import React, { useEffect, useState, useMemo, useCallback } from "react"; import { StyleSheet, FlatList } from "react-native"; -import AsyncStorage from "@react-native-async-storage/async-storage"; import FilterPanel from "../components/FilterPanel"; import CarouselSection from "../components/CarouselSection"; import Heading from "../components/Heading"; import OtherOpportunities from "../components/OtherOpportunities"; import Websites from "../components/Websites"; // ...existing imports... -import { - alertError, - formatDate, - hashForm, - request, - strToDate, -} from "../utils"; +import { alertError, request, strToDate } from "../utils"; import PublicGoogleSheetsParser from "../utils/PublicGoogleSheetsParser"; import { initNotificationHandling, @@ -96,16 +89,6 @@ export default function HomeScreen({ navigation, route }) { }, ); - const submittedForms = []; - try { - const storedForms = await AsyncStorage.getItem("submittedForms"); - if (storedForms != null) { - submittedForms.push(...JSON.parse(storedForms)); - } - } catch (error) { - alertError(`In onRefresh: ${error}`); - } - // Fetch raw data with retry logic via request() const unparsedData = await request(() => parser.parse()); if (unparsedData == null) { @@ -142,13 +125,6 @@ export default function HomeScreen({ navigation, route }) { if (event_midnight < currentDate || opportunity.Date > twoMonthsLater) { continue; } - const hash = hashForm( - "local", - opportunity.Title, - opportunity.Location, - formatDate(opportunity.Date), - ); - opportunity.isSubmitted = submittedForms.includes(hash); newData.push(opportunity); } diff --git a/src/screens/VolunteerOpportunityScreen.js b/src/screens/VolunteerOpportunityScreen.js index ea96823..38f3d6f 100644 --- a/src/screens/VolunteerOpportunityScreen.js +++ b/src/screens/VolunteerOpportunityScreen.js @@ -42,7 +42,6 @@ export default function VolunteerOpportunityScreen({ route, navigation }) { description, tags, formURL, - isSubmitted, max, signedUp, } = route.params; @@ -161,14 +160,9 @@ export default function VolunteerOpportunityScreen({ route, navigation }) { - {isSubmitted && ( - - Warning: You have already submitted this form. - - )} - remainingSpots <= 0 || isSubmitted + remainingSpots <= 0 ? null : navigation.navigate( formURL == null ? "Sign Up Form" : "Google Forms", @@ -182,7 +176,7 @@ export default function VolunteerOpportunityScreen({ route, navigation }) { ) } > - + Sign Up {/* Show remaining spots if applicable */} @@ -261,10 +255,6 @@ const styles = StyleSheet.create({ flexWrap: "wrap", gap: 5, }, - alreadySubmitted: { - color: colors.danger, - marginBottom: 10, - }, lowerRight: { flex: 1, justifyContent: "flex-end", diff --git a/src/utils/forms/Form.js b/src/utils/forms/Form.js index 1f35d96..d35cca3 100644 --- a/src/utils/forms/Form.js +++ b/src/utils/forms/Form.js @@ -6,10 +6,8 @@ * - Form: abstract class to define questions, validation, and submission flow */ -import AsyncStorage from "@react-native-async-storage/async-storage"; - import { Alert } from "react-native"; -import { alertError, hashForm, request } from ".."; +import { alertError, request } from ".."; import formIDs from "../../constants/formIDs"; /** @@ -184,21 +182,6 @@ export default class Form { return; } - const hash = hashForm("local", this.title, this.location, this.date); - - try { - const submittedForms = await AsyncStorage.getItem("submittedForms"); - if (submittedForms == null) { - await AsyncStorage.setItem("submittedForms", JSON.stringify([hash])); - } else { - const newForms = JSON.parse(submittedForms); - newForms.push(hash); - await AsyncStorage.setItem("submittedForms", JSON.stringify(newForms)); - } - } catch (error) { - alertError(`Unable to get/save submittedForms: ${error}`); - } - this.navigation.navigate("End", { isSuccess: true }); } } diff --git a/src/utils/forms/LibraryMusicHour.js b/src/utils/forms/LibraryMusicHour.js index 91de4ca..07366f1 100644 --- a/src/utils/forms/LibraryMusicHour.js +++ b/src/utils/forms/LibraryMusicHour.js @@ -7,12 +7,7 @@ import { useState } from "react"; -import { - Question, - emptyQuestionState, - isAtLeast, - isNotEmpty, -} from ".."; +import { Question, emptyQuestionState, isAtLeast, isNotEmpty } from ".."; import Form from "./Form"; import CheckBoxQuery from "../../components/CheckBoxQuery"; From 9ca6f028ec90d6c88ad43aeabb95fd21faa58082 Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Mon, 4 May 2026 20:26:02 -0700 Subject: [PATCH 5/8] formatting --- src/screens/VolunteerOpportunityScreen.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/screens/VolunteerOpportunityScreen.js b/src/screens/VolunteerOpportunityScreen.js index 38f3d6f..a43ca75 100644 --- a/src/screens/VolunteerOpportunityScreen.js +++ b/src/screens/VolunteerOpportunityScreen.js @@ -176,9 +176,7 @@ export default function VolunteerOpportunityScreen({ route, navigation }) { ) } > - - Sign Up - + Sign Up {/* Show remaining spots if applicable */} {!isNaN(remainingSpots) && (remainingSpots <= 0 ? ( From 06051b3e826b9d3180b765bda59df2aacb816205 Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Mon, 4 May 2026 21:11:11 -0700 Subject: [PATCH 6/8] finishing touches --- package-lock.json | 12 ++++++++- package.json | 3 ++- src/components/CarouselSection.js | 1 - src/components/UploadButton.js | 37 +++++++++++++++++--------- src/components/VolunteerOpportunity.js | 7 ++--- src/utils/forms/DanceClub.js | 7 ++--- src/utils/forms/LibraryMusicHour.js | 12 ++++++--- src/utils/forms/RequestConcert.js | 12 ++++++--- src/utils/index.js | 9 +++---- 9 files changed, 66 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 168278a..0ba97d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,8 @@ "react-native-screens": "~4.16.0", "react-native-super-grid": "^6.0.1", "react-native-webview": "13.15.0", - "react-native-worklets": "0.5.1" + "react-native-worklets": "0.5.1", + "validator": "^13.15.35" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -12038,6 +12039,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index b87fa4f..d79309c 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "react-native-screens": "~4.16.0", "react-native-super-grid": "^6.0.1", "react-native-webview": "13.15.0", - "react-native-worklets": "0.5.1" + "react-native-worklets": "0.5.1", + "validator": "^13.15.35" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/src/components/CarouselSection.js b/src/components/CarouselSection.js index 6cc1116..1b04cd4 100644 --- a/src/components/CarouselSection.js +++ b/src/components/CarouselSection.js @@ -47,7 +47,6 @@ function CarouselSection({ navigation, data, onRefresh }) { .filter((str) => str.length > 0) } formURL={event["Form Link"] ?? null} - isSubmitted={event.isSubmitted} max={event.Max} signedUp={event["Signed Up"]} key={index} diff --git a/src/components/UploadButton.js b/src/components/UploadButton.js index 467d41b..6401a9e 100644 --- a/src/components/UploadButton.js +++ b/src/components/UploadButton.js @@ -43,18 +43,31 @@ function uniqueFileName(original) { } async function getAccessToken() { - const res = await fetch("https://oauth2.googleapis.com/token", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - refresh_token: REFRESH_TOKEN, - grant_type: "refresh_token", - }).toString(), - }); - const json = await res.json(); - return json.access_token ?? null; + try { + const res = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + refresh_token: REFRESH_TOKEN, + grant_type: "refresh_token", + }).toString(), + }); + if (!res.ok) { + alertError("Unable to authenticate with Google Drive. Please try again."); + return null; + } + const json = await res.json(); + if (!json.access_token) { + alertError("Unable to authenticate with Google Drive. Please try again."); + return null; + } + return json.access_token; + } catch (error) { + alertError("Unable to connect to Google Drive. Please try again."); + return null; + } } const selectFile = async () => { diff --git a/src/components/VolunteerOpportunity.js b/src/components/VolunteerOpportunity.js index 2f500b5..339ef09 100644 --- a/src/components/VolunteerOpportunity.js +++ b/src/components/VolunteerOpportunity.js @@ -11,7 +11,6 @@ * - description: event description text * - tags: array of tag strings * - formURL: URL string for sign-up form - * - isSubmitted: boolean indicating if the user has signed up * - max: maximum number of volunteers for the event * - signedUp: current number of signed up volunteers */ @@ -33,7 +32,6 @@ export default function VolunteerOpportunity({ description, tags, formURL, - isSubmitted, max, signedUp, }) { @@ -51,7 +49,6 @@ export default function VolunteerOpportunity({ description, tags, formURL, - isSubmitted, max, signedUp, }) @@ -97,10 +94,10 @@ export default function VolunteerOpportunity({ {/* Right-side indicator: checkmark if submitted, else chevron */} ); diff --git a/src/utils/forms/DanceClub.js b/src/utils/forms/DanceClub.js index 71292ae..e344af8 100644 --- a/src/utils/forms/DanceClub.js +++ b/src/utils/forms/DanceClub.js @@ -8,9 +8,10 @@ import { Question, emptyQuestionState, - isAtLeast, isExactly, isNotEmpty, + isValidEmail, + isValidPhoneNumber, } from ".."; import Form from "./Form"; @@ -72,7 +73,7 @@ export default class DanceClub extends Form { setState={this.email[1]} /> ), - validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + validate: isValidEmail, }), new Question({ @@ -87,7 +88,7 @@ export default class DanceClub extends Form { setState={this.phoneNumber[1]} /> ), - validate: (value) => isAtLeast(value, 10), + validate: isValidPhoneNumber, }), new Question({ diff --git a/src/utils/forms/LibraryMusicHour.js b/src/utils/forms/LibraryMusicHour.js index 07366f1..ec67e65 100644 --- a/src/utils/forms/LibraryMusicHour.js +++ b/src/utils/forms/LibraryMusicHour.js @@ -7,7 +7,13 @@ import { useState } from "react"; -import { Question, emptyQuestionState, isAtLeast, isNotEmpty } from ".."; +import { + Question, + emptyQuestionState, + isNotEmpty, + isValidEmail, + isValidPhoneNumber, +} from ".."; import Form from "./Form"; import CheckBoxQuery from "../../components/CheckBoxQuery"; @@ -104,7 +110,7 @@ export default class LibraryMusicHour extends Form { setState={this.email[1]} /> ), - validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + validate: isValidEmail, }), new Question({ @@ -119,7 +125,7 @@ export default class LibraryMusicHour extends Form { setState={this.phoneNumber[1]} /> ), - validate: (value) => isAtLeast(value, 10), + validate: isValidPhoneNumber, }), new Question({ diff --git a/src/utils/forms/RequestConcert.js b/src/utils/forms/RequestConcert.js index cd7a151..16a6deb 100644 --- a/src/utils/forms/RequestConcert.js +++ b/src/utils/forms/RequestConcert.js @@ -5,7 +5,13 @@ * - Fields include contact info, organization details, event logistics, resources, and time slots. */ -import { Question, emptyQuestionState, isAtLeast, isNotEmpty } from ".."; +import { + Question, + emptyQuestionState, + isNotEmpty, + isValidEmail, + isValidPhoneNumber, +} from ".."; import Form from "./Form"; import CheckBoxQuery from "../../components/CheckBoxQuery"; @@ -60,7 +66,7 @@ export default class RequestConcert extends Form { setState={this.email[1]} /> ), - validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), + validate: isValidEmail, }), new Question({ @@ -75,7 +81,7 @@ export default class RequestConcert extends Form { setState={this.phoneNumber[1]} /> ), - validate: (value) => isAtLeast(value, 10), + validate: isValidPhoneNumber, }), // Organization name and description new Question({ diff --git a/src/utils/index.js b/src/utils/index.js index 03ad687..a930c90 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -14,6 +14,7 @@ */ import Constants from "expo-constants"; +import { isEmail, isMobilePhone } from "validator"; import { createNavigationContainerRef } from "@react-navigation/native"; import { useState } from "react"; import { Alert, Linking, Platform } from "react-native"; @@ -57,11 +58,9 @@ export async function sendErrorEmail(error) { } } -/** - * Log error and show user-friendly alert with diagnostic info. - * @param {string} error - error message or object to display - * @returns {null} - */ +export const isValidEmail = (value) => isEmail(value ?? ""); +export const isValidPhoneNumber = (value) => + isMobilePhone(value ?? "", "any", { strictMode: false }); /** * Open a URL in the default browser. From 40c4a8ad13418740ef72d53eea379efee9e8bc5c Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Mon, 4 May 2026 21:34:27 -0700 Subject: [PATCH 7/8] final fixes --- App.js | 4 +--- package-lock.json | 1 - package.json | 1 - src/components/HomeHeader.js | 10 +--------- src/components/UploadButton.js | 6 +++--- src/components/VolunteerOpportunity.js | 2 +- src/utils/index.js | 10 +++++++--- 7 files changed, 13 insertions(+), 21 deletions(-) diff --git a/App.js b/App.js index 0bae637..038c685 100644 --- a/App.js +++ b/App.js @@ -1,9 +1,7 @@ /** * App.js * The main entry point of the Audacity Sign Up app. - * - Initializes user authentication state from AsyncStorage - * - Displays a loading indicator until auth status is resolved - * - Sets up React Navigation stack with screens for Sign In, Home, Account, and volunteer flows + * - Sets up React Navigation stack with screens for Home and volunteer flows */ import "@expo/metro-runtime"; diff --git a/package-lock.json b/package-lock.json index 0ba97d4..8ec825a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@emailjs/react-native": "^5.1.0", "@expo/metro-runtime": "~6.1.2", - "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/cli-server-api": "^13.6.9", "@react-native-community/netinfo": "^11.4.1", "@react-native-picker/picker": "2.11.1", diff --git a/package.json b/package.json index d79309c..24fc6bb 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "dependencies": { "@emailjs/react-native": "^5.1.0", "@expo/metro-runtime": "~6.1.2", - "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/cli-server-api": "^13.6.9", "@react-native-community/netinfo": "^11.4.1", "@react-native-picker/picker": "2.11.1", diff --git a/src/components/HomeHeader.js b/src/components/HomeHeader.js index b5b762b..c7d90a6 100644 --- a/src/components/HomeHeader.js +++ b/src/components/HomeHeader.js @@ -1,9 +1,6 @@ /** * HomeHeader.js - * Custom app header with gradient background, title, and user profile action. - * Props: - * - navigation: React Navigation prop for screen navigation - * - route: React Navigation route prop to detect focus changes + * Custom app header with gradient background and title */ import { LinearGradient } from "expo-linear-gradient"; @@ -42,9 +39,4 @@ const styles = StyleSheet.create({ color: colors.white, fontSize: 23, }, - profile: { - width: 40, - height: 40, - borderRadius: 100, - }, }); diff --git a/src/components/UploadButton.js b/src/components/UploadButton.js index 6401a9e..d684536 100644 --- a/src/components/UploadButton.js +++ b/src/components/UploadButton.js @@ -55,17 +55,17 @@ async function getAccessToken() { }).toString(), }); if (!res.ok) { - alertError("Unable to authenticate with Google Drive. Please try again."); + Alert.alert("Unable to upload file. Please try again."); return null; } const json = await res.json(); if (!json.access_token) { - alertError("Unable to authenticate with Google Drive. Please try again."); + Alert.alert("Unable to upload file. Please try again."); return null; } return json.access_token; } catch (error) { - alertError("Unable to connect to Google Drive. Please try again."); + Alert.alert("Unable to upload file. Please try again."); return null; } } diff --git a/src/components/VolunteerOpportunity.js b/src/components/VolunteerOpportunity.js index 339ef09..8162180 100644 --- a/src/components/VolunteerOpportunity.js +++ b/src/components/VolunteerOpportunity.js @@ -92,7 +92,7 @@ export default function VolunteerOpportunity({ - {/* Right-side indicator: checkmark if submitted, else chevron */} + {/* Right-side indicator chevron */} isEmail(value ?? ""); +const normalizeValidationInput = (value) => (value ?? "").trim(); +const normalizePhoneValidationInput = (value) => + normalizeValidationInput(value).replace(/[^\d+]/g, ""); +export const isValidEmail = (value) => isEmail(normalizeValidationInput(value)); export const isValidPhoneNumber = (value) => - isMobilePhone(value ?? "", "any", { strictMode: false }); + isMobilePhone(normalizePhoneValidationInput(value), "any", { + strictMode: false, + }); /** * Open a URL in the default browser. From b9aaf6f1834a95a55fe652d65bc3e7664dd46852 Mon Sep 17 00:00:00 2001 From: Pranav Maddineedi Date: Mon, 4 May 2026 21:43:57 -0700 Subject: [PATCH 8/8] fix comments --- src/components/UploadButton.js | 2 +- src/screens/EmbeddedFormScreen.js | 2 +- src/screens/HomeScreen.js | 16 +++------------- src/utils/forms/Form.js | 2 +- src/utils/index.js | 23 ++++++----------------- src/utils/notifications.js | 2 +- 6 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/components/UploadButton.js b/src/components/UploadButton.js index d684536..2bfc511 100644 --- a/src/components/UploadButton.js +++ b/src/components/UploadButton.js @@ -2,7 +2,7 @@ * UploadButton.js * Provides a PDF upload button using Google Drive API: * - Selects a PDF file from the device - * - Authenticates via stored OAuth refresh token (no user sign-in required) + * - Exchanges a stored OAuth refresh token for a short-lived access token * - Uploads to Google Drive with a timestamped unique filename * - Sets sharing to public * - Updates form state with Drive URL diff --git a/src/screens/EmbeddedFormScreen.js b/src/screens/EmbeddedFormScreen.js index f3268e1..4b0d12b 100644 --- a/src/screens/EmbeddedFormScreen.js +++ b/src/screens/EmbeddedFormScreen.js @@ -1,6 +1,6 @@ /** * EmbeddedFormScreen.js - * Displays a Google Forms URL within a WebView. + * Displays an external form URL in a full-screen WebView. * Props: * - route.params.formURL: the URL string of the form to embed */ diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index f85d2ba..fc8b4d0 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -2,7 +2,7 @@ * HomeScreen.js * Main screen showing upcoming events in a carousel, plus extra resources. * - Fetches data from Google Sheets via PublicGoogleSheetsParser - * - Filters events by date range and submission status + * - Filters events to the next two months * - Manages push notifications for each event */ @@ -13,7 +13,6 @@ import CarouselSection from "../components/CarouselSection"; import Heading from "../components/Heading"; import OtherOpportunities from "../components/OtherOpportunities"; import Websites from "../components/Websites"; -// ...existing imports... import { alertError, request, strToDate } from "../utils"; import PublicGoogleSheetsParser from "../utils/PublicGoogleSheetsParser"; import { @@ -22,13 +21,6 @@ import { cancelAllScheduled, } from "../utils/notifications"; -/** - * HomeScreen: displays upcoming events in a carousel, other opportunities, and websites. - * - Fetches event data from Google Sheets - * - Filters events by date range - * - Manages notification scheduling for each event - */ -// Suppress warning about nested VirtualizedLists export default function HomeScreen({ navigation, route }) { // State: array of event objects const [data, setData] = useState([]); @@ -74,8 +66,8 @@ export default function HomeScreen({ navigation, route }) { }, []); /** - * Fetch events from Google Sheets, filter by date and submission status, - * then update state for display and scheduling. + * Fetch events from Google Sheets, filter to the next two months, + * then update state for display and notification scheduling. * @returns {Promise} number of events loaded or null on failure */ const onRefresh = useCallback(async () => { @@ -104,8 +96,6 @@ export default function HomeScreen({ navigation, route }) { // - Provide default Title/Location // - Convert date string to Date // - Exclude past events and events beyond two months ahead - // - Mark isSubmitted based on stored hashes - // - Collect valid events for (let i = 0; i < unparsedData.length; i++) { const opportunity = unparsedData[i]; diff --git a/src/utils/forms/Form.js b/src/utils/forms/Form.js index d35cca3..7b7119a 100644 --- a/src/utils/forms/Form.js +++ b/src/utils/forms/Form.js @@ -137,7 +137,7 @@ export default class Form { } /** - * Perform validation, build form data, submit to Google Forms, track submissions, and navigate outcome. + * Validate, build form data, submit to Google Forms, and navigate to outcome screen. */ async submit() { const invalidResponses = this.validate(); diff --git a/src/utils/index.js b/src/utils/index.js index 9bd9d49..7e84f61 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,14 +1,14 @@ /** * index.js * Shared utility functions: - * - alertError: standardized error alert - * - openURL / maybeOpenURL: external link handling - * - request: retry wrapper for network calls - * - strToDate / formatDate: date parsing and formatting + * - alertError: standardized error alert + EmailJS error report + * - openURL / maybeOpenURL: external link handling with app store fallback + * - request: retry wrapper with exponential backoff for network calls + * - strToDate / formatDate: Google Sheets date parsing and formatting * - Question: form question helper class * - emptyQuestionState: hook for question state - * - validation helpers: isAtLeast, isNotEmpty, isExactly - * - hashForm: deterministic event hash + * - isAtLeast, isNotEmpty, isExactly: basic validation predicates + * - isValidEmail, isValidPhoneNumber: validator.js-backed field validators * - openInMaps: launch maps app for a location */ @@ -216,17 +216,6 @@ export const isNotEmpty = (value) => isAtLeast(value, 1); export const isExactly = (value, len) => !isAtLeast(value, len + 1) && isAtLeast(value, len); -/** - * Hash event details deterministically for submission tracking. - * @param {string} userID - * @param {string} title - * @param {string} location - * @param {string} date - */ -export function hashForm(userID, title, location, date) { - return `${userID}&&&${title}&&&${location}&&&${date}`; -} - /** * Launch maps application or fallback to web URL for a location. * @param {string} location diff --git a/src/utils/notifications.js b/src/utils/notifications.js index 3c6d369..bc3ea11 100644 --- a/src/utils/notifications.js +++ b/src/utils/notifications.js @@ -89,7 +89,7 @@ export async function scheduleEventNotifications(event) { /** * Cancel all scheduled notifications throughout the app. - * Useful for clearing events on logout or data refresh. + * Called before rescheduling on data refresh to avoid duplicates. */ export function cancelAllScheduled() { return Notifications.cancelAllScheduledNotificationsAsync();