Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer UI & Functionality #73

Merged
merged 17 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 18 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Luke-Oldenburg marked this conversation as resolved.
Show resolved Hide resolved
"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",
Expand Down
10 changes: 10 additions & 0 deletions src/Navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -69,6 +70,7 @@ export default function Navigator() {
// headerStyle: { backgroundColor: themeColors.background },
headerShown: false,
tabBarStyle: { position: "absolute" },
tabBarHideOnKeyboard: true,
tabBarBackground: () => (
<BlurView
tint={scheme == "dark" ? "dark" : "light"}
Expand Down Expand Up @@ -163,6 +165,14 @@ export default function Navigator() {
title: "Edit Transaction Description",
}}
/>
<Stack.Screen
name="Transfer"
component={TransferPage}
options={{
presentation: "modal",
title: "Send Transfer",
}}
/>
</Stack.Navigator>
)}
</Tab.Screen>
Expand Down
9 changes: 7 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ButtonProps {
onPress?: () => void;
color?: string;
loading?: boolean;
disabled?: boolean;
icon?: React.ComponentProps<typeof Ionicons>["name"];
}

Expand Down Expand Up @@ -45,9 +46,13 @@ export default function Button(
) {
return (
<Pressable
style={{ ...styles.button, ...(props.style as object) }}
style={{
...styles.button,
...(props.style as object),
opacity: props.disabled ? 0.6 : undefined,
}}
onPress={() => props.onPress && props.onPress()}
disabled={props.loading}
disabled={props.loading || props.disabled}
>
{props.icon && (
<Ionicons
Expand Down
2 changes: 1 addition & 1 deletion src/components/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,4 @@ function Transaction({
);
}

export default memo(Transaction);
export default memo(Transaction);
190 changes: 190 additions & 0 deletions src/components/organizations/transfer/Disbursement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { Picker } from '@react-native-picker/picker';
import { useTheme } from '@react-navigation/native';
import { useContext, useEffect, useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, Alert } from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
import useSWR from 'swr';

import AuthContext from '../../../auth';
import { OrganizationExpanded } from '../../../lib/types/Organization';
import { palette } from '../../../theme';
import { renderMoney } from '../../../util';

type DisbursementScreenProps = {
organization: OrganizationExpanded;
};

const DisbursementScreen = ({ organization }: DisbursementScreenProps) => {
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<OrganizationExpanded[]>('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 (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color={themeColors.primary} />
</View>
);
}

return (
<View style={{ flex: 1, backgroundColor: themeColors.background }}>
{/* From Section */}
<Text style={{ color: themeColors.text, fontSize: 18, marginVertical: 12, fontWeight: 'bold' }}>From</Text>
<View style={{ backgroundColor: themeColors.card, borderRadius: 8, padding: 15, marginBottom: 15 }}>
<Text style={{ color: themeColors.text, fontSize: 16 }}>
{organization.name} ({renderMoney(organization.balance_cents)})
</Text>
</View>

{/* To Section */}
<Text style={{ color: themeColors.text, fontSize: 18, marginVertical: 12, fontWeight: 'bold' }}>To</Text>
<View style={{ backgroundColor: themeColors.card, borderRadius: 8, marginBottom: 15 }}>
<RNPickerSelect
onValueChange={(itemValue: string) => 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 })),
]}
/>
</View>
<Text style={{ color: palette.muted, fontSize: 14, marginBottom: 20 }}>
You can transfer to any organization you're a part of.
</Text>

{/* Amount Section */}
<Text style={{ color: themeColors.text, fontSize: 18, marginVertical: 12, fontWeight: 'bold' }}>Amount</Text>
<TextInput
style={{
backgroundColor: themeColors.card,
color: themeColors.text,
borderRadius: 8,
padding: 12,
fontSize: 16,
marginBottom: 15,
}}
value={amount}
onChangeText={(text) => {
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 */}
<Text style={{ color: themeColors.text, fontSize: 18, marginVertical: 12, fontWeight: 'bold' }}>What is the transfer for?</Text>
<TextInput
style={{
backgroundColor: themeColors.card,
color: themeColors.text,
borderRadius: 8,
padding: 12,
fontSize: 16,
marginBottom: 10,
}}
value={reason}
onChangeText={(text) => setReason(text)}
placeholder="Donating extra funds to another organization"
placeholderTextColor={palette.muted}
/>
<Text style={{ color: palette.muted, fontSize: 14, marginBottom: 20 }}>
This is to help HCB keep record of our transactions.
</Text>

{/* Transfer Button */}
<TouchableOpacity
style={{
backgroundColor: themeColors.primary,
padding: 15,
borderRadius: 8,
marginTop: 20,
alignItems: 'center',
opacity: isLoading ? 0.7 : 1,
}}
onPress={handleTransfer}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator size="small" color={themeColors.text} />
) : (
<Text style={{ color: themeColors.text, fontSize: 16, fontWeight: 'bold' }}>Make Transfer</Text>
)}
</TouchableOpacity>
</View>
);
};

export default DisbursementScreen;
1 change: 1 addition & 0 deletions src/lib/NavigatorParamList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type StackParamList = {
transaction?: Transaction;
};
RenameTransaction: { orgId: string; transaction: Transaction };
Transfer: { organization: Organization };
};

export type CardsStackParamList = {
Expand Down
4 changes: 4 additions & 0 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Loading