diff --git a/.eslintrc.js b/.eslintrc.js index e0c8250..ccd54fb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,7 +46,6 @@ module.exports = { ], "@typescript-eslint/no-non-null-assertion": "off", "react/no-unescaped-entities": "off", - "no-console": "warn", }, globals: { fetch: "readonly", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d1e823d..9253b52 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,9 @@ ## Summary of the problem + ## Describe your changes + ## Checklist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b2cd7c..c219ede 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,11 @@ name: CI on: push: - branches: - - '**' + branches: + - "**" pull_request: branches: - - '**' + - "**" jobs: eslint: name: ESLint diff --git a/App.tsx b/App.tsx index aead0f3..27d4720 100644 --- a/App.tsx +++ b/App.tsx @@ -53,20 +53,20 @@ const linking: LinkingOptions = { export default function App() { const [fontsLoaded] = useFonts({ - 'JetBrainsMono-Regular': require('./assets/fonts/JetBrainsMono-Regular.ttf'), - 'JetBrainsMono-Bold': require('./assets/fonts/JetBrainsMono-Bold.ttf'), - 'Consolas-Bold': require('./assets/fonts/CONSOLAB.ttf'), - 'Damion': require('./assets/fonts/Damion-Regular.ttf'), + "JetBrainsMono-Regular": require("./assets/fonts/JetBrainsMono-Regular.ttf"), + "JetBrainsMono-Bold": require("./assets/fonts/JetBrainsMono-Bold.ttf"), + "Consolas-Bold": require("./assets/fonts/CONSOLAB.ttf"), + Damion: require("./assets/fonts/Damion-Regular.ttf"), }); const [isLoading, setIsLoading] = useState(true); const [token, setToken] = useState(null); const hcb = useClient(token); const scheme = useColorScheme(); - useStripeTerminal() - + useStripeTerminal(); + const fetcher = useCallback( - async (url: string, options: any) => { + async (url: string, options: RequestInit) => { try { return await hcb(url, options).json(); } catch (error) { @@ -114,7 +114,8 @@ export default function App() { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1dc6db9..1debcd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,5 @@ # Contributing + Thank you for choosing to contribute to HCB Mobile! Please follow the guidelines before on how to contribute. - Use the PR template diff --git a/README.md b/README.md index 3806275..c2efaa9 100644 --- a/README.md +++ b/README.md @@ -58,4 +58,5 @@ EXPO_PUBLIC_CLIENT_ID= - Bonus task: painfully watch Gradle attempt to work ## Contributing -Please see [CONTRIBUTING.md](CONTRIBUTING.md). \ No newline at end of file + +Please see [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/app.config.js b/app.config.js index d9272e7..0acfdac 100644 --- a/app.config.js +++ b/app.config.js @@ -65,24 +65,24 @@ export default { [ "@stripe/stripe-terminal-react-native", { - "bluetoothBackgroundMode": true, - "locationWhenInUsePermission": "Location access is required in order to accept payments.", - "bluetoothPeripheralPermission": "Bluetooth access is required in order to connect to supported bluetooth card readers.", - "bluetoothAlwaysUsagePermission": "This app uses Bluetooth to connect to supported card readers." - } + bluetoothBackgroundMode: true, + locationWhenInUsePermission: + "Location access is required in order to accept payments.", + bluetoothPeripheralPermission: + "Bluetooth access is required in order to connect to supported bluetooth card readers.", + bluetoothAlwaysUsagePermission: + "This app uses Bluetooth to connect to supported card readers.", + }, ], [ "expo-build-properties", { android: { - minSdkVersion: 26 - } + minSdkVersion: 26, + }, }, ], - [ - "expo-alternate-app-icons", - appIcons - ] + ["expo-alternate-app-icons", appIcons], ], }, -}; \ No newline at end of file +}; diff --git a/env.d.ts b/env.d.ts index db1b78f..ce808f7 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,7 +1,7 @@ declare namespace NodeJS { - interface ProcessEnv { - EXPO_PUBLIC_API_BASE: string; - EXPO_PUBLIC_CLIENT_ID: string; - EXPO_PUBLIC_STRIPE_API_KEY: string; - } -} \ No newline at end of file + interface ProcessEnv { + EXPO_PUBLIC_API_BASE: string; + EXPO_PUBLIC_CLIENT_ID: string; + EXPO_PUBLIC_STRIPE_API_KEY: string; + } +} diff --git a/src/Navigator.tsx b/src/Navigator.tsx index 24fd1be..ed3b4c4 100644 --- a/src/Navigator.tsx +++ b/src/Navigator.tsx @@ -22,6 +22,8 @@ import Home from "./pages/index"; import InvitationPage from "./pages/Invitation"; import OrganizationPage from "./pages/organization"; import AccountNumberPage from "./pages/organization/AccountNumber"; +import OrganizationDonationPage from "./pages/organization/Donation"; +import ProcessDonationPage from "./pages/organization/ProcessDonation"; import OrganizationSettingsPage from "./pages/organization/Settings"; import TransferPage from "./pages/organization/transfer"; import ReceiptsPage from "./pages/Receipts"; @@ -29,8 +31,6 @@ import RenameTransactionPage from "./pages/RenameTransaction"; import SettingsPage from "./pages/Settings"; import TransactionPage from "./pages/Transaction"; import { palette } from "./theme"; -import OrganizationDonationPage from "./pages/organization/Donation"; -import ProcessDonationPage from "./pages/organization/ProcessDonation"; const Stack = createNativeStackNavigator(); const CardsStack = createNativeStackNavigator(); @@ -202,7 +202,7 @@ export default function Navigator() { ({ + options={() => ({ title: "Card", })} /> diff --git a/src/cacheProvider.ts b/src/cacheProvider.ts index fd6f456..be4c7e3 100644 --- a/src/cacheProvider.ts +++ b/src/cacheProvider.ts @@ -1,34 +1,34 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { AppState } from 'react-native'; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { AppState } from "react-native"; export default function asyncStorageProvider() { const map = new Map(); // Initialize the map with data from AsyncStorage (async () => { try { - const appCache = await AsyncStorage.getItem('app-cache'); + const appCache = await AsyncStorage.getItem("app-cache"); if (appCache) { - JSON.parse(appCache).forEach(([key, value]) => { + JSON.parse(appCache).forEach(([key, value]: [string, unknown]) => { map.set(key, value); }); } } catch (error) { - console.error('Error initializing asyncStorageProvider:', error); + console.error("Error initializing asyncStorageProvider:", error); } })(); // Save the map to AsyncStorage before the app goes to the background const saveAppCache = async () => { try { const appCache = JSON.stringify(Array.from(map.entries())); - await AsyncStorage.setItem('app-cache', appCache); + await AsyncStorage.setItem("app-cache", appCache); } catch (error) { - console.error('Error saving asyncStorageProvider cache:', error); + console.error("Error saving asyncStorageProvider cache:", error); } }; - AppState.addEventListener('change', (nextAppState) => { - if (nextAppState === 'background' || nextAppState === 'inactive') { + AppState.addEventListener("change", (nextAppState) => { + if (nextAppState === "background" || nextAppState === "inactive") { saveAppCache(); } }); return map; -} \ No newline at end of file +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 9a256fc..fda1af8 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -46,11 +46,17 @@ export default function Button( ) { return ( props.onPress && props.onPress()} disabled={props.loading || props.disabled} > diff --git a/src/components/PaymentCard.tsx b/src/components/PaymentCard.tsx index 7ff2af5..531b2dc 100644 --- a/src/components/PaymentCard.tsx +++ b/src/components/PaymentCard.tsx @@ -1,5 +1,6 @@ import { useTheme } from "@react-navigation/native"; import Constants from "expo-constants"; +// @ts-expect-error will be removed later import * as Geopattern from "geopattern"; import { useEffect, useRef, useState } from "react"; import { @@ -38,12 +39,13 @@ export default function PaymentCard({ }: ViewProps & { card: Card; details?: CardDetails; - onCardLoad?: (cardId: string, dimensions: { width: number; height: number }) => void; + onCardLoad?: ( + cardId: string, + dimensions: { width: number; height: number }, + ) => void; }) { const { colors: themeColors, dark } = useTheme(); - console.log("PaymentCard", card); - const patternForMeasurements = Geopattern.generate(card.id, { scalePattern: 1.1, grayscale: card.status != "active", @@ -51,8 +53,7 @@ export default function PaymentCard({ const pattern = Geopattern.generate(card.id, { scalePattern: 1.1, - grayscale: - card.status == "active" ? false : true, + grayscale: card.status == "active" ? false : true, }).toDataUri(); const extractDimensions = (svg: string) => { @@ -74,12 +75,11 @@ export default function PaymentCard({ card.type = "virtual"; } - useEffect(() => { if (onCardLoad) { onCardLoad(card.id, { width: svgWidth, height: svgHeight }); } - }, []); + }, [card.id, onCardLoad, svgHeight, svgWidth]); useEffect(() => { const subscription = AppState.addEventListener( @@ -122,11 +122,14 @@ export default function PaymentCard({ }} > {Constants.platform?.android ? ( - + ) : ( - ) - } + )} )} @@ -158,7 +161,7 @@ export default function PaymentCard({ { initialize(); }, [initialize]); - return ( - - ); + return ; } - interface PaymentIntent { - id: string - amount: number - created: string - currency: string - sdkUuid: string - paymentMethodId: string + id: string; + amount: number; + created: string; + currency: string; + sdkUuid: string; + paymentMethodId: string; } export function PageStripe() { - const { location, accessDenied } = useLocation() + const { accessDenied } = useLocation(); + // const { location, accessDenied } = useLocation(); - const [value, setValue] = useState(0) - const [reader, setReader] = useState() - const [payment, setPayment] = useState() - const [loadingCreatePayment, setLoadingCreatePayment] = useState(false) - const [loadingCollectPayment, setLoadingCollectPayment] = useState(false) - const [loadingConfirmPayment, setLoadingConfirmPayment] = useState(false) - const [loadingConnectingReader, setLoadingConnectingReader] = useState(false) + const [value, setValue] = useState(0); + const [reader, setReader] = useState(undefined); + const [payment, setPayment] = useState(); + const [loadingCreatePayment, setLoadingCreatePayment] = useState(false); + const [loadingCollectPayment, setLoadingCollectPayment] = useState(false); + const [loadingConfirmPayment, setLoadingConfirmPayment] = useState(false); + const [loadingConnectingReader, setLoadingConnectingReader] = useState(false); - const locationIdStripeMock = 'tml_FrcFgksbiIZZ2V' + const locationIdStripeMock = "tml_FrcFgksbiIZZ2V"; const { discoverReaders, @@ -56,142 +54,140 @@ export function PageStripe() { confirmPaymentIntent, connectedReader, } = useStripeTerminal({ - onUpdateDiscoveredReaders: (readers: any) => { - setReader(readers[0]) + onUpdateDiscoveredReaders: (readers: Reader.Type[]) => { + setReader(readers[0]); }, - }) + }); useEffect(() => { discoverReaders({ - discoveryMethod: 'localMobile', + discoveryMethod: "localMobile", simulated: false, - }) - }, [discoverReaders]) + }); + }, [discoverReaders]); - async function connectReader(selectedReader: any) { - setLoadingConnectingReader(true) + async function connectReader(selectedReader: Reader.Type) { + setLoadingConnectingReader(true); try { - const { reader, error } = await connectLocalMobileReader({ + const { error } = await connectLocalMobileReader({ reader: selectedReader, locationId: locationIdStripeMock, - }) + }); if (error) { - console.log('connectLocalMobileReader error:', error) - return + console.log("connectLocalMobileReader error:", error); + return; } - Alert.alert('Reader connected successfully') - - console.log('Reader connected successfully', reader) + Alert.alert("Reader connected successfully"); } catch (error) { - console.log(error) + console.log(error); } finally { - setLoadingConnectingReader(false) + setLoadingConnectingReader(false); } } async function paymentIntent() { - setLoadingCreatePayment(true) + setLoadingCreatePayment(true); try { const { error, paymentIntent } = await createPaymentIntent({ amount: Number((value * 100).toFixed()), - currency: 'usd', - paymentMethodTypes: ['card_present'], - offlineBehavior: 'prefer_online', - }) + currency: "usd", + paymentMethodTypes: ["card_present"], + offlineBehavior: "prefer_online", + }); if (error) { - console.log('Error creating payment intent', error) - return + console.log("Error creating payment intent", error); + return; } - setPayment(paymentIntent) + setPayment(paymentIntent); - Alert.alert('Payment intent created successfully') + Alert.alert("Payment intent created successfully"); } catch (error) { - console.log(error) + console.log(error); } finally { - setLoadingCreatePayment(false) + setLoadingCreatePayment(false); } } async function collectPayment() { - setLoadingCollectPayment(true) + setLoadingCollectPayment(true); try { - const { error, paymentIntent } = await collectPaymentMethod({ + const { error } = await collectPaymentMethod({ + // @ts-expect-error works without extra PaymentIntent props paymentIntent: payment, - } as any) + }); if (error) { - console.log('Error collecting payment', error) - Alert.alert('Error collecting payment', error.message) - return + console.log("Error collecting payment", error); + Alert.alert("Error collecting payment", error.message); + return; } - Alert.alert('Payment successfully collected', '', [ + Alert.alert("Payment successfully collected", "", [ { - text: 'Ok', + text: "Ok", onPress: async () => { - console.log(paymentIntent); - await confirmPayment() + await confirmPayment(); }, }, - ]) + ]); } catch (error) { - console.log(error) + console.log(error); } finally { - setLoadingCollectPayment(false) + setLoadingCollectPayment(false); } } async function confirmPayment() { - setLoadingConfirmPayment(true) - console.log("foo", { payment }) + setLoadingConfirmPayment(true); + console.log("foo", { payment }); try { - const { error, paymentIntent } = await confirmPaymentIntent({ - paymentIntent: payment as any - }) + const { error } = await confirmPaymentIntent({ + // @ts-expect-error works without extra PaymentIntent props + paymentIntent: payment, + }); if (error) { - console.log('Error confirm payment', error) - return + console.log("Error confirm payment", error); + return; } - Alert.alert('Payment successfully confirmed!', 'Congratulations') - console.log('Payment confirmed', paymentIntent) + Alert.alert("Payment successfully confirmed!", "Congratulations"); } catch (error) { - console.log(error) + console.log(error); } finally { - setLoadingConfirmPayment(false) + setLoadingConfirmPayment(false); } } async function handleRequestLocation() { - await Linking.openSettings() + await Linking.openSettings(); } useEffect(() => { if (accessDenied) { Alert.alert( - 'Access to location', - 'To use the app, you need to allow the use of your device location.', + "Access to location", + "To use the app, you need to allow the use of your device location.", [ { - text: 'Activate', + text: "Activate", onPress: handleRequestLocation, }, - ] - ) + ], + ); } - }, [accessDenied]) + }, [accessDenied]); return ( @@ -199,8 +195,8 @@ export function PageStripe() { Stripe @@ -211,15 +207,15 @@ export function PageStripe() { Amount to be charged + > + Connecting with the reader{"disabled" + !!connectedReader} + - + - + - + - ) -} \ No newline at end of file + ); +} diff --git a/src/components/Transaction.tsx b/src/components/Transaction.tsx index 2fe9d02..f72b7ad 100644 --- a/src/components/Transaction.tsx +++ b/src/components/Transaction.tsx @@ -1,9 +1,9 @@ import { Ionicons } from "@expo/vector-icons"; -import { faPaypal } from '@fortawesome/free-brands-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { faPaypal } from "@fortawesome/free-brands-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import Icon from "@hackclub/hackclub-icons-rn"; import { useTheme } from "@react-navigation/native"; import { LinearGradient } from "expo-linear-gradient"; -import Icon from "@hackclub/hackclub-icons-rn"; import { memo } from "react"; import { View, Text, ViewProps, StyleSheet } from "react-native"; import { match } from "ts-pattern"; @@ -18,10 +18,7 @@ import { renderMoney } from "../util"; import UserAvatar from "./UserAvatar"; -function transactionIcon({ - code, - ...transaction -}: TransactionWithoutId) { +function transactionIcon({ code, ...transaction }: TransactionWithoutId) { switch (code) { case TransactionType.Donation: case TransactionType.PartnerDonation: @@ -87,11 +84,13 @@ function TransactionIcon({ ); } else { if (transactionIcon(transaction) == "paypal") { - return - } - else { + return ( + + ); + } else { return ( - + Playground Mode - To raise & spend money, wait for your organization's account to be activated by a staff member. + To raise & spend money, wait for your organization's account to + be activated by a staff member. {/* + {(card.status !== "expired" || !isGrantCard) && ( + )} {card.type == "virtual" && _card.status != "canceled" && ( - ) - return ( - - Collect donations - - {loadingConnectingReader && !currentProgress && } - {currentProgress && } - - ) + ); } return ( - - - + - + + Name @@ -458,19 +488,16 @@ function PageContent({ orgId, orgName, navigation }: any) { padding: 12, borderRadius: 8, fontSize: 16, - flex: 1 + flex: 1, }} selectTextOnFocus autoFocus - clearButtonMode="while-editing" value={name} autoCapitalize="words" onChangeText={setName} autoComplete="off" autoCorrect={false} - - placeholder={"Full name"} returnKeyType="next" onSubmitEditing={() => { @@ -478,14 +505,15 @@ function PageContent({ orgId, orgName, navigation }: any) { }} /> - - + Email @@ -496,26 +524,15 @@ function PageContent({ orgId, orgName, navigation }: any) { padding: 12, borderRadius: 8, fontSize: 16, - flex: 1 + flex: 1, }} selectTextOnFocus autoFocus - clearButtonMode="while-editing" + placeholder="Email" autoCapitalize="none" - value={email} - keyboardType="email-address" - autoComplete="off" - autoCorrect={false} onChangeText={setEmail} - ref={emailRef} - placeholder={"Email address"} - returnKeyType="done" - - onSubmitEditing={() => { - // navigation.goBack(); - }} /> @@ -525,8 +542,8 @@ function PageContent({ orgId, orgName, navigation }: any) { ) : ( - )} - - + ); } -function Keyboard({ amount, setAmount }: any) { +interface KeyboardProps { + amount: string; + setAmount: (value: string) => void; +} + +interface NumberProps { + number?: number; + symbol?: string; + onPress?: () => void; +} + +const Keyboard = ({ amount, setAmount }: KeyboardProps) => { const [error, setError] = useState(false); const theme = useTheme(); + function pressNumber(amount: string, number: number) { if ( parseFloat(amount.replace("$", "0") + number) > 9999.99 || @@ -587,15 +617,7 @@ function Keyboard({ amount, setAmount }: any) { } } - const Number = ({ - number, - symbol, - onPress, - }: { - number?: number; - symbol?: string; - onPress?: () => void; - }) => ( + const Number = ({ number, symbol, onPress }: NumberProps) => ( { + onPress={() => { if (onPress) { onPress(); - } else if (typeof number != undefined) { + } else if (number !== undefined) { pressNumber(amount, number as number); } }} @@ -618,52 +640,63 @@ function Keyboard({ amount, setAmount }: any) { ); return ( - + {amount} {amount == "$" && 0} - {amount[amount.length - 1] == "." && 00} - {amount[amount.length - 2] == "." && 0} + {amount[amount.length - 1] == "." && ( + 00 + )} + {amount[amount.length - 2] == "." && ( + 0 + )} - + - + - + - + pressDecimal(amount)} /> pressBackspace(amount)} /> - ) -} + ); +}; diff --git a/src/pages/organization/ProcessDonation.tsx b/src/pages/organization/ProcessDonation.tsx index 91173d3..1e9cd71 100644 --- a/src/pages/organization/ProcessDonation.tsx +++ b/src/pages/organization/ProcessDonation.tsx @@ -10,155 +10,224 @@ import { palette } from "../../theme"; type Props = NativeStackScreenProps; - export default function ProcessDonationPage({ - navigation, - route: { - params: { payment, collectPayment, email, name }, - }, + navigation, + route: { + params: { payment, collectPayment, email, name }, + }, }: Props) { - const [status, setStatus] = useState<"ready" | "loading" | "success" | "error">("ready"); - const theme = useTheme(); - useEffect(() => { - navigation.setOptions({ - headerLeft: () => ( -