Skip to content
237 changes: 237 additions & 0 deletions src/Navigator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { Ionicons } from "@expo/vector-icons";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { useTheme } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { BlurView } from "expo-blur";
import * as WebBrowser from "expo-web-browser";
import { StyleSheet, useColorScheme } from "react-native";
import useSWR, { useSWRConfig } from "swr";

// import OrganizationTitle from "./components/organizations/OrganizationTitle";
import {
StackParamList,
CardsStackParamList,
ReceiptsStackParamList,
TabParamList,
} from "./lib/NavigatorParamList";
import { PaginatedResponse } from "./lib/types/HcbApiObject";
import Invitation from "./lib/types/Invitation";
import CardPage from "./pages/cards/card";
import CardsPage from "./pages/cards/cards";
import OrderCardPage from "./pages/cards/OrderCard";
import Home from "./pages/index";
import InvitationPage from "./pages/Invitation";
import OrganizationPage from "./pages/organization";
import AccountNumberPage from "./pages/organization/AccountNumber";
import OrganizationTeamPage from "./pages/organization/Team";
import ReceiptsPage from "./pages/Receipts";
import RenameTransactionPage from "./pages/RenameTransaction";
import SettingsPage from "./pages/settings/Settings";
import TransactionPage from "./pages/Transaction";
import { palette } from "./styles/theme";

const Stack = createNativeStackNavigator<StackParamList>();
const CardsStack = createNativeStackNavigator<CardsStackParamList>();
const ReceiptsStack = createNativeStackNavigator<ReceiptsStackParamList>();

const Tab = createBottomTabNavigator<TabParamList>();

export default function Navigator() {
const { data: missingReceiptData } = useSWR<PaginatedResponse<never>>(
`user/transactions/missing_receipt`,
);
const { data: invitations } = useSWR<Invitation[]>(`user/invitations`);

const scheme = useColorScheme();
const { colors: themeColors } = useTheme();

const { mutate } = useSWRConfig();

return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: React.ComponentProps<typeof Ionicons>["name"];

if (route.name === "Home") {
iconName = focused ? "home" : "home-outline";
} else if (route.name === "Cards") {
iconName = focused ? "card" : "card-outline";
} else if (route.name === "Receipts") {
iconName = focused ? "receipt" : "receipt-outline";
} else if (route.name === "Settings") {
iconName = focused ? "settings" : "settings-outline";
} else {
throw new Error("unknown route name");
}

return <Ionicons name={iconName} size={size} color={color} />;
},
// headerStyle: { backgroundColor: themeColors.background },
headerShown: false,
tabBarStyle: { position: "absolute" },
tabBarHideOnKeyboard: true,
tabBarBackground: () => (
<BlurView
tint={scheme == "dark" ? "dark" : "light"}
intensity={100}
style={StyleSheet.absoluteFill}
/>
),
})}
>
<Tab.Screen
name="Home"
options={{ tabBarBadge: invitations?.length || undefined }}
>
{() => (
<Stack.Navigator
screenOptions={{
headerLargeTitleShadowVisible: false,
}}
>
<Stack.Screen
name="Organizations"
component={Home}
options={{
title: "Home",
headerLargeTitle: true,
headerRight: () => (
<Ionicons.Button
name="add-circle-outline"
backgroundColor="transparent"
size={24}
underlayColor={themeColors.card}
color={palette.primary}
iconStyle={{ marginRight: 0 }}
onPress={() =>
WebBrowser.openBrowserAsync(
"https://hackclub.com/hcb/apply",
{
presentationStyle:
WebBrowser.WebBrowserPresentationStyle.POPOVER,
controlsColor: palette.primary,
dismissButtonStyle: "cancel",
},
).then(() => {
mutate("user/organizations");
mutate("user/invitations");
})
}
/>
),
}}
/>
<Stack.Screen
name="Invitation"
component={InvitationPage}
options={{
presentation: "modal",
headerShown: false,
}}
/>
<Stack.Screen
name="Event"
options={({ route }) => ({
// headerTitle: () => <OrganizationTitle {...route.params} />,
title: route.params.organization?.name || "Organization",
headerBackTitle: "Back",
})}
component={OrganizationPage}
/>
<Stack.Screen
name="AccountNumber"
component={AccountNumberPage}
options={{ presentation: "modal", title: "Account Details" }}
/>
<Stack.Screen
name="OrganizationTeam"
component={OrganizationTeamPage}
options={{
headerBackTitle: "Back",
title: "Manage Organization",
}}
/>
<Stack.Screen
options={{ headerBackTitle: "Back" }}
name="Transaction"
component={TransactionPage}
/>
<Stack.Screen
name="RenameTransaction"
component={RenameTransactionPage}
options={{
presentation: "modal",
title: "Edit Transaction Description",
}}
/>
</Stack.Navigator>
)}
</Tab.Screen>
<Tab.Screen name="Cards" options={{ tabBarLabel: "Cards" }}>
{() => (
<CardsStack.Navigator>
<CardsStack.Screen
name="CardList"
component={CardsPage}
options={{ title: "Cards" }}
/>
<CardsStack.Screen
name="Card"
component={CardPage}
options={() => ({
title: "Card",
})}
/>
<CardsStack.Screen
name="OrderCard"
component={OrderCardPage}
options={{
presentation: "card",
headerShown: true,
title: "",
headerBackTitle: "Cards",
headerShadowVisible: false,
headerTransparent: true,
}}
/>
<Stack.Screen
options={{ headerBackTitle: "Back" }}
name="Transaction"
component={TransactionPage}
/>
<Stack.Screen
name="RenameTransaction"
component={RenameTransactionPage}
options={{
presentation: "modal",
title: "Edit Transaction Description",
}}
/>
</CardsStack.Navigator>
)}
</Tab.Screen>
<Tab.Screen
name="Receipts"
options={{
tabBarBadge: missingReceiptData?.total_count || undefined,
}}
>
{() => (
<ReceiptsStack.Navigator>
<ReceiptsStack.Screen
name="MissingReceiptList"
options={{ title: "Missing Receipts" }}
component={ReceiptsPage}
/>
</ReceiptsStack.Navigator>
)}
</Tab.Screen>
<Tab.Screen
name="Settings"
options={{ headerShown: true }}
component={SettingsPage}
/>
</Tab.Navigator>
);
}
6 changes: 0 additions & 6 deletions src/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {

if (!response.ok) {
const errorBody = await response.text();
console.error(
`Token refresh failed with status ${response.status}`,
new Error(errorBody),
{ action: "token_refresh", status: response.status, errorBody },
);

try {
const errorJson = JSON.parse(errorBody);
console.error(
Expand Down
2 changes: 1 addition & 1 deletion src/components/PaymentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export default function PaymentCard({
width: "auto",
height: 40,
tintColor:
card.personalization?.color == "black" ? "white" : undefined,
card.personalization?.color == "black" ? "white" : "black",
aspectRatio: logoWidth / logoHeight,
}}
/>
Expand Down
110 changes: 59 additions & 51 deletions src/components/ReceiptActionSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useActionSheet } from "@expo/react-native-action-sheet";
import * as DocumentPicker from "expo-document-picker";
import * as ImagePicker from "expo-image-picker";
import React from "react";
import { findNodeHandle } from "react-native";
import { ALERT_TYPE, Toast } from "react-native-alert-notification";

import useClient from "../lib/client";
Expand Down Expand Up @@ -81,61 +83,67 @@ export function useReceiptActionSheet({
},
);

const handleActionSheet = withOfflineCheck(() => {
const options = ["Camera", "Photo Library", "Document", "Cancel"];
const cancelButtonIndex = 3;
const handleActionSheet = withOfflineCheck(
(buttonRef?: React.RefObject<unknown>) => {
const options = ["Camera", "Photo Library", "Document", "Cancel"];
const cancelButtonIndex = 3;

showActionSheetWithOptions(
{
options,
cancelButtonIndex,
userInterfaceStyle: isDark ? "dark" : "light",
containerStyle: {
backgroundColor: isDark ? "#252429" : "white",
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
userInterfaceStyle: isDark ? "dark" : "light",
containerStyle: {
backgroundColor: isDark ? "#252429" : "white",
},
textStyle: {
color: isDark ? "white" : "black",
},
anchor: buttonRef?.current
? (findNodeHandle(buttonRef.current as React.Component) ??
undefined)
: undefined,
},
textStyle: {
color: isDark ? "white" : "black",
},
},
async (buttonIndex) => {
if (buttonIndex === 0) {
// Take a photo
ImagePicker.requestCameraPermissionsAsync();
const result = await ImagePicker.launchCameraAsync({
mediaTypes: "images",
quality: 1,
});
if (!result.canceled) {
await uploadFile({
uri: result.assets[0].uri,
fileName: result.assets[0].fileName || undefined,
async (buttonIndex) => {
if (buttonIndex === 0) {
// Take a photo
ImagePicker.requestCameraPermissionsAsync();
const result = await ImagePicker.launchCameraAsync({
mediaTypes: "images",
quality: 1,
});
if (!result.canceled) {
await uploadFile({
uri: result.assets[0].uri,
fileName: result.assets[0].fileName || undefined,
});
}
} else if (buttonIndex === 1) {
// Pick from photo library
ImagePicker.requestMediaLibraryPermissionsAsync();
const result = await ImagePicker.launchImageLibraryAsync({
quality: 1,
allowsMultipleSelection: true,
selectionLimit: 10,
});
if (!result.canceled && result.assets.length > 0) {
await uploadMultipleFiles(result.assets);
}
} else if (buttonIndex === 2) {
// Pick a document
const result = await DocumentPicker.getDocumentAsync({
type: ["application/pdf", "image/*"],
copyToCacheDirectory: true,
multiple: true,
});
if (!result.canceled && result.assets.length > 0) {
await uploadMultipleFiles(result.assets);
}
}
} else if (buttonIndex === 1) {
// Pick from photo library
ImagePicker.requestMediaLibraryPermissionsAsync();
const result = await ImagePicker.launchImageLibraryAsync({
quality: 1,
allowsMultipleSelection: true,
selectionLimit: 10,
});
if (!result.canceled && result.assets.length > 0) {
await uploadMultipleFiles(result.assets);
}
} else if (buttonIndex === 2) {
// Pick a document
const result = await DocumentPicker.getDocumentAsync({
type: ["application/pdf", "image/*"],
copyToCacheDirectory: true,
multiple: true,
});
if (!result.canceled && result.assets.length > 0) {
await uploadMultipleFiles(result.assets);
}
}
},
);
});
},
);
},
);

return {
handleActionSheet,
Expand Down
Loading
Loading