diff --git a/packages/app/modules/auth/components/AuthWrapper.tsx b/packages/app/modules/auth/components/AuthWrapper.tsx index 5e2ff4875..c7a20e7ca 100644 --- a/packages/app/modules/auth/components/AuthWrapper.tsx +++ b/packages/app/modules/auth/components/AuthWrapper.tsx @@ -1,10 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { AuthLoader } from './AuthLoader'; import { Redirect } from 'app/components/Redirect'; -import { RSpinner, RText, RButton } from '@packrat/ui'; -import { Platform, View, Alert } from 'react-native'; +import { RSpinner, RText } from '@packrat/ui'; +import { Platform, View } from 'react-native'; import LandingPage from 'app/components/landing_page'; -import * as LocalAuthentication from 'expo-local-authentication'; import useTheme from 'app/hooks/useTheme'; interface AuthWrapperProps { @@ -16,65 +15,8 @@ export const AuthWrapper = ({ children, unauthorizedElement, }: AuthWrapperProps) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); const { currentTheme } = useTheme(); - const authenticate = async () => { - if (Platform.OS === 'web') { - setIsAuthenticated(true); - return; - } - - const hasHardware = await LocalAuthentication.hasHardwareAsync(); - if (!hasHardware) { - Alert.alert( - 'Error', - 'Your device does not support biometric authentication.', - ); - return; - } - - const hasBiometrics = await LocalAuthentication.isEnrolledAsync(); - if (!hasBiometrics) { - Alert.alert( - 'Error', - 'No biometrics are enrolled. Please set up biometrics in your device settings.', - ); - return; - } - - const result = await LocalAuthentication.authenticateAsync({ - promptMessage: 'Authenticate to continue', - fallbackLabel: 'Use Passcode', - }); - - if (result.success) { - setIsAuthenticated(true); - } - }; - - useEffect(() => { - authenticate(); - }, []); - - if (!isAuthenticated) { - return ( - - - Please unlock to continue - - Unlock - - ); - } - const loadingElement = Platform.OS === 'web' ? ( Loading... diff --git a/packages/app/modules/auth/hooks/useUser.ts b/packages/app/modules/auth/hooks/useUser.ts index 4a98c9c3c..c25b9c75e 100644 --- a/packages/app/modules/auth/hooks/useUser.ts +++ b/packages/app/modules/auth/hooks/useUser.ts @@ -17,6 +17,8 @@ export const useUserQuery = () => { isLoading: isRequestLoading, } = queryTrpc.getMe.useQuery(undefined, { enabled: isRequestEnabled, + staleTime: Infinity, + cacheTime: Infinity, }); // Sometimes the isLoading state don't work as expected so we have this solution here diff --git a/packages/ui/src/Bento/elements/tables/Basic.tsx b/packages/ui/src/Bento/elements/tables/Basic.tsx index be6b1f990..6c04480f9 100644 --- a/packages/ui/src/Bento/elements/tables/Basic.tsx +++ b/packages/ui/src/Bento/elements/tables/Basic.tsx @@ -10,12 +10,13 @@ import { Text, View, getTokenValue } from 'tamagui'; import { Table } from './common/tableParts'; import { AddItem } from 'app/modules/item'; import { DeletePackItemModal, EditPackItemModal } from 'app/modules/pack'; -import { ThreeDotsMenu, YStack, RButton } from '@packrat/ui'; +import { ThreeDotsMenu, YStack, RButton, RText } from '@packrat/ui'; import { Platform } from 'react-native'; import { RDropdownMenu } from '../../../ZDropdown'; import RIconButton from '../../../RIconButton'; import { ChevronDown } from '@tamagui/lucide-icons'; +import { BaseAlert } from '@packrat/ui'; type ModalName = 'edit' | 'delete'; @@ -100,13 +101,34 @@ export function BasicTable({ /> )} - - onDelete({ itemId: item.id, packId: currentPack.id }) - } - /> + hideIcon={true} + title={'Delete Item'} + footerButtons={[ + { + label: 'Cancel', + onClick: () => { + closeModal(); + }, + color: 'gray', + disabled: false, + }, + { + label: 'Delete', + onClick: () => { + closeModal(); + onDelete({ itemId: item.id, packId: currentPack.id }); + }, + color: '#B22222', + disabled: false, + }, + ]} + > + Are you sure you want to delete this item? + + {hasPermissions ? ( Platform.OS === 'android' || Platform.OS === 'ios' || diff --git a/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx b/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx index c6b6653b6..50a04582c 100644 --- a/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx +++ b/packages/ui/src/Bento/forms/layouts/SignInScreen.tsx @@ -1,4 +1,4 @@ -// import { Facebook, Github } from '@tamagui/lucide-icons'; +import * as LocalAuthentication from 'expo-local-authentication'; import { AnimatePresence, H1, @@ -9,13 +9,11 @@ import { Theme, View, } from 'tamagui'; -import { Text, Platform } from 'react-native'; +import { Text, Platform, Alert } from 'react-native'; import { FormCard } from './components/layoutParts'; import { RLink } from '@packrat/ui'; import { Form, FormInput, SubmitButton } from '@packrat/ui'; import { userSignIn } from '@packrat/validations'; -import { FontAwesome } from '@expo/vector-icons'; -import { RIconButton } from '@packrat/ui'; import useTheme from 'app/hooks/useTheme'; import useResponsive from 'app/hooks/useResponsive'; @@ -27,6 +25,49 @@ export function SignInScreen({ }) { const { currentTheme } = useTheme(); const { xxs, xs } = useResponsive(); + + const handleBiometricAuth = async () => { + if (Platform.OS === 'web') { + return true; + } + + const hasHardware = await LocalAuthentication.hasHardwareAsync(); + if (!hasHardware) { + Alert.alert( + 'Error', + 'Your device does not support biometric authentication.', + ); + return false; + } + + const hasBiometrics = await LocalAuthentication.isEnrolledAsync(); + if (!hasBiometrics) { + Alert.alert( + 'Error', + 'No biometrics are enrolled. Please set up biometrics in your device settings.', + ); + return false; + } + + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Authenticate to continue', + fallbackLabel: 'Use Passcode', + }); + + if (!result.success) { + Alert.alert('Authentication failed', 'Please try again.'); + } + + return result.success; + }; + + const handleSubmit = async (data) => { + const isBiometricallyAuthenticated = await handleBiometricAuth(); + if (isBiometricallyAuthenticated) { + signIn(data); + } + }; + return ( signIn(data)} + onSubmit={handleSubmit} style={{ marginTop: 16, backgroundColor: currentTheme.colors.tertiaryBlue, @@ -121,20 +162,6 @@ export function SignInScreen({ Or - {/* - { - event.preventDefault(); - await promptAsync(); - }} - icon={} - > - Continue with Google - - */} - {/* */} diff --git a/packages/ui/src/alert/BaseAlert.tsx b/packages/ui/src/alert/BaseAlert.tsx new file mode 100644 index 000000000..a425431e4 --- /dev/null +++ b/packages/ui/src/alert/BaseAlert.tsx @@ -0,0 +1,165 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Button, AlertDialog } from 'tamagui'; +import { X } from '@tamagui/lucide-icons'; +import RButton from '@packrat/ui/src/RButton'; +import RStack from '@packrat/ui/src/RStack'; +import { useAlert, AlertProvider } from './provider'; + +export interface BaseAlertProps { + id?: string; + title: string; + trigger?: string; + children: React.ReactNode; + buttonColor?: string; + footerButtons?: any[]; + triggerComponent?: React.DetailedReactHTMLElement; + footerComponent: React.DetailedReactHTMLElement; + hideIcon?: boolean; + isOpen?: boolean | undefined; + toggle?: any; + onClose: any; +} + +export const BaseAlert = ({ + triggerComponent, + title, + trigger, + footerButtons, + footerComponent, + children, + hideIcon = false, + isOpen = undefined, + toggle, + onClose, +}: BaseAlertProps) => { + const [isAlertOpen, setIsAlertOpen] = useState(false); + + useEffect(() => { + if(isOpen !== undefined) { + setIsAlertOpen(isOpen) + } + }, [isOpen]) + + const triggerElement = useMemo(() => { + return triggerComponent ? ( + setIsAlertOpen(true)} + style={{ backgroundColor: 'transparent' }} + backgroundColor={'transparent'} + > + {React.cloneElement(triggerComponent, { setIsAlertOpen })} + + ) : ( + setIsAlertOpen(true)} + > + {trigger} + + ); + }, [triggerComponent]); + + const memoFooterButtons = useMemo(() => { + if (!Array.isArray(footerButtons)) return null; + + return footerButtons.map(({ color, label, onClick, ...button }, index) => ( + { + setIsAlertOpen(false) + if(toggle) { + toggle() + } + })} + backgroundColor={color} + disabled={button.disabled} + color="white" + {...button} + > + {label} + + )); + }, [footerButtons]); + + const footerElement = useMemo(() => { + return ( + footerComponent && React.cloneElement(footerComponent, { setIsAlertOpen }) + ); + }, [footerComponent]); + + return ( + { + setIsAlertOpen(open); + }} + > + {!hideIcon && {triggerElement}} + + + + + {title} + + + {children} + + + + + {memoFooterButtons} + + {footerElement} + +