diff --git a/package-lock.json b/package-lock.json
index 46c7a3c..2c00284 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@expo/vector-icons": "^14.0.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-menu/menu": "^1.1.6",
+ "@react-native-picker/picker": "^2.10.2",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.10",
"@react-navigation/native-stack": "^6.9.13",
@@ -5431,6 +5432,18 @@
"react-native": "*"
}
},
+ "node_modules/@react-native-picker/picker": {
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.10.2.tgz",
+ "integrity": "sha512-kr3OvCRwTYjR/OKlb52k4xmQVU7dPRIALqpyiihexdJxEgvc1smnepgqCeM9oXmNSG4YaV5/RSxFlLC5Z/T/Eg==",
+ "workspaces": [
+ "example"
+ ],
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.4.tgz",
diff --git a/package.json b/package.json
index f36c176..65de1a4 100644
--- a/package.json
+++ b/package.json
@@ -18,11 +18,12 @@
"@expo/react-native-action-sheet": "^4.1.0",
"@expo/vector-icons": "^14.0.0",
"@react-native-async-storage/async-storage": "1.23.1",
- "@react-native-menu/menu": "^1.2.0",
- "@react-navigation/bottom-tabs": "^7.2.0",
- "@react-navigation/native": "^7.0.14",
- "@react-navigation/native-stack": "^7.2",
- "date-fns": "^4.1.0",
+ "@react-native-menu/menu": "^1.1.6",
+ "@react-native-picker/picker": "2.7.5",
+ "@react-navigation/bottom-tabs": "^6.5.8",
+ "@react-navigation/native": "^6.1.10",
+ "@react-navigation/native-stack": "^6.9.13",
+ "date-fns": "^2.30.0",
"eas-cli": "^13.4.2",
"expo": "~51.0.39",
"expo-alternate-app-icons": "^1.1.0",
@@ -32,32 +33,33 @@
"expo-constants": "~16.0.2",
"expo-crypto": "~13.0.2",
"expo-dev-client": "~4.0.29",
- "expo-device": "~7.0.1",
+ "expo-device": "~6.0.2",
"expo-haptics": "~13.0.1",
"expo-image": "~1.13.0",
"expo-image-picker": "~15.0.7",
- "expo-insights": "~0.8.1",
+ "expo-insights": "~0.7.0",
"expo-linear-gradient": "~13.0.2",
"expo-linking": "~6.3.1",
"expo-local-authentication": "~14.0.1",
- "expo-notifications": "~0.29.11",
- "expo-secure-store": "~14.0.0",
+ "expo-notifications": "^0.28.18",
+ "expo-secure-store": "~13.0.2",
"expo-splash-screen": "~0.27.7",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
- "expo-web-browser": "~14.0.1",
+ "expo-web-browser": "~13.0.3",
"geopattern": "github:thedev132/geopattern",
"humanize-string": "^3.0.0",
"ky": "^1.2.3",
"lodash": "^4.17.21",
- "react": "18.3.1",
- "react-native": "0.75",
- "react-native-gesture-handler": "~2.21.2",
+ "react": "18.2.0",
+ "react-native": "0.74.5",
+ "react-native-gesture-handler": "~2.16.1",
"react-native-image-viewing": "^0.2.2",
- "react-native-pager-view": "6.5.1",
- "react-native-reanimated": "~3.16.3",
+ "react-native-pager-view": "6.3.0",
+ "react-native-picker-select": "^9.3.1",
+ "react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5",
- "react-native-screens": "4.4.0",
+ "react-native-screens": "3.31.1",
"react-native-svg": "15.2.0",
"react-native-web": "~0.19.10",
"swr": "^2.2.1",
diff --git a/src/Navigator.tsx b/src/Navigator.tsx
index 0ea9527..58d4950 100644
--- a/src/Navigator.tsx
+++ b/src/Navigator.tsx
@@ -23,6 +23,7 @@ import InvitationPage from "./pages/Invitation";
import OrganizationPage from "./pages/organization";
import AccountNumberPage from "./pages/organization/AccountNumber";
import OrganizationSettingsPage from "./pages/organization/Settings";
+import TransferPage from "./pages/organization/transfer";
import ReceiptsPage from "./pages/Receipts";
import RenameTransactionPage from "./pages/RenameTransaction";
import SettingsPage from "./pages/Settings";
@@ -69,6 +70,7 @@ export default function Navigator() {
// headerStyle: { backgroundColor: themeColors.background },
headerShown: false,
tabBarStyle: { position: "absolute" },
+ tabBarHideOnKeyboard: true,
tabBarBackground: () => (
+
)}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 1818dd5..3aa6454 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -15,6 +15,7 @@ export interface ButtonProps {
onPress?: () => void;
color?: string;
loading?: boolean;
+ disabled?: boolean;
icon?: React.ComponentProps["name"];
}
@@ -45,9 +46,13 @@ export default function Button(
) {
return (
props.onPress && props.onPress()}
- disabled={props.loading}
+ disabled={props.loading || props.disabled}
>
{props.icon && (
{
+ const [amount, setAmount] = useState('$0.00');
+ const [chosenOrg, setOrganization] = useState('');
+ const [reason, setReason] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const { colors: themeColors } = useTheme();
+ const { data: organizations } = useSWR('user/organizations');
+ const { token } = useContext(AuthContext);
+
+ const validateInputs = () => {
+ const numericAmount = Number(amount.replace('$', '').replace(',', ''));
+ if (!chosenOrg) {
+ Alert.alert('Error', 'Please select an organization to transfer to.');
+ return false;
+ }
+ if (numericAmount <= 0 || isNaN(numericAmount)) {
+ Alert.alert('Error', 'Please enter a valid amount greater than $0.');
+ return false;
+ }
+ if (numericAmount * 100 > organization.balance_cents) {
+ Alert.alert('Error', 'Insufficient balance for this transfer.');
+ return false;
+ }
+ if (!reason.trim()) {
+ Alert.alert('Error', 'Please provide a reason for the transfer.');
+ return false;
+ }
+ return true;
+ };
+
+ const handleTransfer = async () => {
+ if (!validateInputs()) return;
+
+ setIsLoading(true);
+ try {
+ const response = await fetch(process.env.EXPO_PUBLIC_API_BASE + `/organizations/${organization.id}/transfers`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ event_id: organization.id,
+ to_organization_id: chosenOrg,
+ amount_cents: Number(amount.replace('$', '').replace(',', '')) * 100,
+ name: reason,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ Alert.alert('Error', errorData.message || 'Failed to complete the transfer. Please try again.');
+ } else {
+ Alert.alert('Success', 'Transfer completed successfully!');
+ setOrganization('');
+ setAmount('$0.00');
+ setReason('');
+ }
+ } catch (error) {
+ Alert.alert('Error', 'An unexpected error occurred. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (chosenOrg === '') {
+ setAmount('$0.00');
+ }
+ }, [chosenOrg]);
+
+ if (!organizations) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* From Section */}
+ From
+
+
+ {organization.name} ({renderMoney(organization.balance_cents)})
+
+
+
+ {/* To Section */}
+ To
+
+ setOrganization(itemValue)}
+ style={{ inputIOS: { color: themeColors.text, padding: 15, fontSize: 16 }, inputAndroid: { color: themeColors.text, padding: 15, fontSize: 16 } }}
+ items={[
+ ...organizations.map((org) => ({ label: org.name, value: org.id })),
+ ]}
+ />
+
+
+ You can transfer to any organization you're a part of.
+
+
+ {/* Amount Section */}
+ Amount
+ {
+ const sanitizedText = text.replace(/[^\d.]/g, '');
+ // remove 0.00 if user enters a new number
+ if (sanitizedText.startsWith('0.00')) {
+ setAmount(text.replace('0.00', ''));
+ return;
+ }
+ setAmount(sanitizedText ? `$${sanitizedText}` : '$0.00');
+ }}
+ placeholder="$0.00"
+ placeholderTextColor={themeColors.text}
+ keyboardType="numeric"
+ />
+
+ {/* Purpose Section */}
+ What is the transfer for?
+ setReason(text)}
+ placeholder="Donating extra funds to another organization"
+ placeholderTextColor={palette.muted}
+ />
+
+ This is to help HCB keep record of our transactions.
+
+
+ {/* Transfer Button */}
+
+ {isLoading ? (
+
+ ) : (
+ Make Transfer
+ )}
+
+
+ );
+};
+
+export default DisbursementScreen;
diff --git a/src/lib/NavigatorParamList.ts b/src/lib/NavigatorParamList.ts
index 83f35ca..6147a1f 100644
--- a/src/lib/NavigatorParamList.ts
+++ b/src/lib/NavigatorParamList.ts
@@ -17,6 +17,7 @@ export type StackParamList = {
transaction?: Transaction;
};
RenameTransaction: { orgId: string; transaction: Transaction };
+ Transfer: { organization: Organization };
};
export type CardsStackParamList = {
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 5ba839d..23a36cb 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -231,6 +231,10 @@ export default function App({ navigation }: Props) {
preload("user", fetcher!);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
preload("user/cards", fetcher!);
+ // prefetch all user organization details
+ for (const org of organizations || []) {
+ preload(`organizations/${org.id}`, fetcher!);
+ }
}, []);
const onRefresh = () => {
diff --git a/src/pages/organization/index.tsx b/src/pages/organization/index.tsx
index a5eb9a1..1918278 100644
--- a/src/pages/organization/index.tsx
+++ b/src/pages/organization/index.tsx
@@ -70,6 +70,8 @@ export default function OrganizationPage({
const { data: organization, isLoading: organizationLoading } = useSWR<
Organization | OrganizationExpanded
>(`organizations/${orgId}`, { fallbackData: _organization });
+
+ const { data: user, isLoading: userLoading } = useSWR("user");
const {
transactions: _transactions,
isLoadingMore,
@@ -79,7 +81,12 @@ export default function OrganizationPage({
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
- if (organization) {
+ if (organization && user) {
+ const isManager = "users" in organization &&
+ organization.users.some(
+ (u) => u.id === user?.id && u.role === "manager",
+ );
+
navigation.setOptions({
title: organization.name,
// headerTitle: () => ,
@@ -97,12 +104,21 @@ export default function OrganizationPage({
});
}
+ if (isManager) {
+ menuActions.push({
+ id: "transfer",
+ title: "Transfer Money",
+ image: "dollarsign.circle",
+ });
+ }
+
menuActions.push({
id: "settings",
title: "Manage Organization",
image: "gearshape",
});
+
navigation.setOptions({
headerRight: () => (
@@ -131,7 +149,7 @@ export default function OrganizationPage({
),
});
}
- }, [organization, scheme, navigation]);
+ }, [organization, scheme, navigation, user]);
const tabBarSize = useBottomTabBarHeight();
const { colors: themeColors } = useTheme();
@@ -153,13 +171,13 @@ export default function OrganizationPage({
})),
[transactions],
);
-
+
const onRefresh = () => {
mutate("organizations");
mutate(`organizations/${orgId}`);
}
- if (organizationLoading) {
+ if (organizationLoading || userLoading) {
return ;
}
@@ -188,33 +206,47 @@ export default function OrganizationPage({
{organization?.playground_mode && (
)}
-
-
+
+
+ Balance
+
+
+ {"balance_cents" in organization &&
+ renderMoney(organization.balance_cents)}
+
+
+ {/*
-
- {"balance_cents" in organization &&
- renderMoney(organization.balance_cents)}
-
+ Transfer Money
+ */}
- {/*
-
- Transactions
-
- */}
+
{isLoading && }
)}
diff --git a/src/pages/organization/transfer.tsx b/src/pages/organization/transfer.tsx
new file mode 100644
index 0000000..f4ceb30
--- /dev/null
+++ b/src/pages/organization/transfer.tsx
@@ -0,0 +1,77 @@
+import { NativeStackScreenProps } from "@react-navigation/native-stack";
+import Constants from "expo-constants";
+import { useEffect, useState } from "react";
+import { KeyboardAvoidingView, Button as NativeButton, View, ScrollView, Platform } from "react-native";
+
+import DisbursementScreen from "../../components/organizations/transfer/Disbursement";
+import { StackParamList } from "../../lib/NavigatorParamList";
+import { OrganizationExpanded } from "../../lib/types/Organization";
+import { palette } from "../../theme";
+
+type Props = NativeStackScreenProps;
+
+export default function TransferPage({ navigation, route }: Props) {
+ const { organization } = route.params as { organization: OrganizationExpanded }; // Grab the organization value from the route params
+
+ useEffect(() => {
+ navigation.setOptions({
+ headerLeft: () => (
+
+ navigation.goBack()}
+ />
+
+ ),
+ });
+ }, []);
+
+ const [transferType, setTransferType] = useState<"ach" | "check" | "hcb">("hcb");
+
+ return (
+
+
+ {/* Transfer Type Buttons */}
+ {/*
+
+
+
+ */}
+
+ {/* Display transfer screen based on transfer type */}
+ {transferType === "hcb" && }
+
+
+ );
+}