From 014f2b541ef71fc812746e3749715d08333ec25a Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 18 Dec 2024 16:07:26 +0100 Subject: [PATCH 1/9] working Signed-off-by: Jan --- apps/easypid/assets/Blob.tsx | 36 +++ apps/easypid/src/app/(app)/(home)/offline.tsx | 5 + apps/easypid/src/app/(app)/(home)/scan.tsx | 5 +- apps/easypid/src/app/(app)/_layout.tsx | 6 + apps/easypid/src/app/(app)/index.tsx | 20 +- .../src/features/menu/FunkeMenuScreen.tsx | 15 +- .../features/scan/FunkeQrScannerScreen.tsx | 282 ------------------ .../features/wallet/FunkeOfflineQrScreen.tsx | 122 ++++++++ .../src/features/wallet/FunkeWalletScreen.tsx | 220 +++++--------- .../features/wallet/components/ActionCard.tsx | 38 +++ .../wallet/components/AllCardsCard.tsx | 14 + .../wallet/components/LatestActivityCard.tsx | 2 +- apps/easypid/src/hooks/usePidCredential.tsx | 18 +- packages/ui/assets/People.tsx | 24 ++ packages/ui/assets/Qr.tsx | 46 +++ packages/ui/src/content/Icon.tsx | 4 + packages/ui/src/content/IconContainer.tsx | 15 +- 17 files changed, 408 insertions(+), 464 deletions(-) create mode 100644 apps/easypid/assets/Blob.tsx create mode 100644 apps/easypid/src/app/(app)/(home)/offline.tsx delete mode 100644 apps/easypid/src/features/scan/FunkeQrScannerScreen.tsx create mode 100644 apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx create mode 100644 apps/easypid/src/features/wallet/components/ActionCard.tsx create mode 100644 apps/easypid/src/features/wallet/components/AllCardsCard.tsx create mode 100644 packages/ui/assets/People.tsx create mode 100644 packages/ui/assets/Qr.tsx diff --git a/apps/easypid/assets/Blob.tsx b/apps/easypid/assets/Blob.tsx new file mode 100644 index 00000000..c8d28034 --- /dev/null +++ b/apps/easypid/assets/Blob.tsx @@ -0,0 +1,36 @@ +import { View } from 'react-native' +import Svg, { Path, G, type SvgProps, Defs, LinearGradient, Stop } from 'react-native-svg' + +export function Blob({ color = 'black', ...props }: SvgProps) { + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/apps/easypid/src/app/(app)/(home)/offline.tsx b/apps/easypid/src/app/(app)/(home)/offline.tsx new file mode 100644 index 00000000..6bf48822 --- /dev/null +++ b/apps/easypid/src/app/(app)/(home)/offline.tsx @@ -0,0 +1,5 @@ +import { FunkeOfflineQrScreen } from '@easypid/features/wallet/FunkeOfflineQrScreen' + +export default function Screen() { + return +} diff --git a/apps/easypid/src/app/(app)/(home)/scan.tsx b/apps/easypid/src/app/(app)/(home)/scan.tsx index af9d8c19..10e3e29f 100644 --- a/apps/easypid/src/app/(app)/(home)/scan.tsx +++ b/apps/easypid/src/app/(app)/(home)/scan.tsx @@ -1,5 +1,4 @@ -import { FunkeQrScannerScreen } from '@easypid/features/scan/FunkeQrScannerScreen' -import type { CredentialDataHandlerOptions } from '@package/app' +import { type CredentialDataHandlerOptions, QrScannerScreen } from '@package/app' // When going form the scanner we want to replace (as we have the modal) export const credentialDataHandlerOptions = { @@ -7,5 +6,5 @@ export const credentialDataHandlerOptions = { } satisfies CredentialDataHandlerOptions export default function Screen() { - return + return } diff --git a/apps/easypid/src/app/(app)/_layout.tsx b/apps/easypid/src/app/(app)/_layout.tsx index ed96bc62..c147ce01 100644 --- a/apps/easypid/src/app/(app)/_layout.tsx +++ b/apps/easypid/src/app/(app)/_layout.tsx @@ -108,6 +108,12 @@ export default function AppLayout() { }} name="(home)/scan" /> + - { - return - }, - }} - /> - - - ) + return } diff --git a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx index 4dcbfcaf..19ea2eea 100644 --- a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { useScrollViewPosition } from '@package/app/src/hooks' +import { useHaptics, useScrollViewPosition } from '@package/app/src/hooks' import { Button, FlexPage, Heading, HeroIcons, ScrollView, Stack, XStack, YStack, useScaleAnimation } from '@package/ui' import { usePidCredential } from '@easypid/hooks' @@ -112,6 +112,7 @@ export function FunkeMenuScreen() { const MenuItem = ({ item, idx, onPress }: { item: (typeof menuItems)[number]; idx: number; onPress?: () => void }) => { const { pressStyle, handlePressIn, handlePressOut } = useScaleAnimation() + const { withHaptics } = useHaptics() const content = ( Linking.openURL('mailto:ana@animo.id?subject=Feedback on the Funke EUDI Wallet')} + onPress={withHaptics(() => Linking.openURL('mailto:ana@animo.id?subject=Feedback on the Funke EUDI Wallet'))} asChild > {content} @@ -158,14 +159,20 @@ const MenuItem = ({ item, idx, onPress }: { item: (typeof menuItems)[number]; id if (item.href === '/') { return ( - + onPress)}> {content} ) } return ( - + undefined)} + onPressIn={handlePressIn} + onPressOut={handlePressOut} + href={item.href} + asChild + > {content} ) diff --git a/apps/easypid/src/features/scan/FunkeQrScannerScreen.tsx b/apps/easypid/src/features/scan/FunkeQrScannerScreen.tsx deleted file mode 100644 index 3c309904..00000000 --- a/apps/easypid/src/features/scan/FunkeQrScannerScreen.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import { QrScanner } from '@package/scanner' -import { - AnimatedStack, - Heading, - HeroIcons, - Loader, - Page, - Paragraph, - Spinner, - Stack, - useSpringify, - useToastController, -} from '@package/ui' -import { useIsFocused } from '@react-navigation/native' -import { useRouter } from 'expo-router' -import React, { useEffect, useState } from 'react' -import QRCode from 'react-native-qrcode-svg' - -import { type CredentialDataHandlerOptions, isAndroid, useCredentialDataHandler, useHaptics } from '@package/app' -import { Alert, Linking, Platform, useWindowDimensions } from 'react-native' -import { FadeIn, FadeOut, LinearTransition, useAnimatedStyle, withTiming } from 'react-native-reanimated' -import { useSafeAreaInsets } from 'react-native-safe-area-context' - -import easypidLogo from '../../../assets/icon-rounded.png' -import { - checkMdocPermissions, - getMdocQrCode, - requestMdocPermissions, - shutdownDataTransfer, - waitForDeviceRequest, -} from '../proximity' - -const unsupportedUrlPrefixes = ['_oob='] - -interface QrScannerScreenProps { - credentialDataHandlerOptions?: CredentialDataHandlerOptions -} - -export function FunkeQrScannerScreen({ credentialDataHandlerOptions }: QrScannerScreenProps) { - const { back } = useRouter() - const { handleCredentialData } = useCredentialDataHandler() - const { bottom, top } = useSafeAreaInsets() - const toast = useToastController() - const isFocused = useIsFocused() - - const [showMyQrCode, setShowMyQrCode] = useState(false) - const [helpText, setHelpText] = useState('') - const [isProcessing, setIsProcessing] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [qrCodeData, setQrCodeData] = useState() - const [arePermissionsGranted, setArePermissionsGranted] = useState(false) - - useEffect(() => { - void checkMdocPermissions().then((result) => { - setArePermissionsGranted(!!result) - }) - }, []) - - useEffect(() => { - if (showMyQrCode) { - void getMdocQrCode().then(setQrCodeData) - } else { - setQrCodeData(undefined) - } - }, [showMyQrCode]) - - const onCancel = () => { - back() - shutdownDataTransfer() - } - - const onScan = async (scannedData: string) => { - if (isProcessing || !isFocused) return - setIsProcessing(true) - setIsLoading(true) - - const result = await handleCredentialData(scannedData, credentialDataHandlerOptions) - if (!result.success) { - const isUnsupportedUrl = - unsupportedUrlPrefixes.find((x) => scannedData.includes(x)) || result.error === 'invitation_type_not_allowed' - setHelpText( - isUnsupportedUrl - ? 'This QR-code is not supported yet. Try scanning a different one.' - : result.message - ? result.message - : 'Invalid QR code. Try scanning a different one.' - ) - setIsLoading(false) - } - - await new Promise((resolve) => setTimeout(resolve, 5000)) - setHelpText('') - setIsLoading(false) - setIsProcessing(false) - } - - const handleQrButtonPress = async () => { - if (Platform.OS !== 'android') { - toast.show('This feature is not supported on your OS yet.', { customData: { preset: 'warning' } }) - back() - return - } - - if (arePermissionsGranted) { - setShowMyQrCode(true) - } else { - const permissions = await requestMdocPermissions() - if (!permissions) { - toast.show('Failed to request permissions.', { customData: { preset: 'danger' } }) - return - } - - // Check if any permission is in 'never_ask_again' state - const hasNeverAskAgain = Object.values(permissions).some((status) => status === 'never_ask_again') - - if (hasNeverAskAgain) { - Alert.alert( - 'Please enable required permissions in your phone settings', - 'Sharing with QR-Code needs access to Bluetooth and Location.', - [ - { - text: 'Open Settings', - onPress: () => Linking.openSettings(), - }, - ] - ) - return - } - } - } - - const animatedQrOverlayOpacity = useAnimatedStyle( - () => ({ - opacity: withTiming(showMyQrCode ? 1 : 0, { duration: showMyQrCode ? 300 : 200 }), - }), - [showMyQrCode] - ) - - return ( - <> - {!showMyQrCode && ( - { - void onScan(data) - }} - helpText={helpText} - /> - )} - - - - - - - - {showMyQrCode && } - - - {isLoading && ( - - - - Loading invitation - - - )} - - - - - {showMyQrCode ? 'Scan QR code' : 'Show my QR code'} - - {showMyQrCode ? ( - - ) : ( - - )} - - - - - ) -} - -function FunkeQrOverlay({ qrCodeData }: { qrCodeData?: string }) { - const { width } = useWindowDimensions() - const { bottom, top } = useSafeAreaInsets() - const { withHaptics } = useHaptics() - const { replace } = useRouter() - - useEffect(() => { - if (qrCodeData) { - void waitForDeviceRequest().then((data) => { - if (data) { - pushToOfflinePresentation({ - sessionTranscript: Buffer.from(data.sessionTranscript).toString('base64'), - deviceRequest: Buffer.from(data.deviceRequest).toString('base64'), - }) - return - } - }) - } - }, [qrCodeData]) - - // Navigate to offline presentation route - const pushToOfflinePresentation = withHaptics((data: { sessionTranscript: string; deviceRequest: string }) => - replace({ - pathname: '/notifications/offlinePresentation', - params: data, - }) - ) - - return ( - - - - Share with QR code - - A verifier needs to scan your QR-Code. - - - {qrCodeData ? ( - - - - ) : ( - - )} - - - - ) -} diff --git a/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx b/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx new file mode 100644 index 00000000..ea4d79bc --- /dev/null +++ b/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx @@ -0,0 +1,122 @@ +import { + AnimatedStack, + Button, + Heading, + Loader, + Page, + Paragraph, + Spacer, + Stack, + XStack, + YStack, + useToastController, +} from '@package/ui' +import { useRouter } from 'expo-router' +import { useHaptics } from 'packages/app/src' +import { useEffect, useState } from 'react' +import { Platform, useWindowDimensions } from 'react-native' +import QRCode from 'react-native-qrcode-svg' +import easypidLogo from '../../../assets/icon-rounded.png' +import { checkMdocPermissions, shutdownDataTransfer, waitForDeviceRequest } from '../proximity' + +export function FunkeOfflineQrScreen() { + const { withHaptics } = useHaptics() + const { replace, back } = useRouter() + const { width } = useWindowDimensions() + const toast = useToastController() + const [isProcessing, setIsProcessing] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [qrCodeData, setQrCodeData] = useState() + const [arePermissionsGranted, setArePermissionsGranted] = useState(false) + + useEffect(() => { + void checkMdocPermissions().then((result) => { + setArePermissionsGranted(!!result) + }) + }, []) + + useEffect(() => { + if (qrCodeData) { + void waitForDeviceRequest().then((data) => { + if (data) { + pushToOfflinePresentation({ + sessionTranscript: Buffer.from(data.sessionTranscript).toString('base64'), + deviceRequest: Buffer.from(data.deviceRequest).toString('base64'), + }) + return + } + }) + } + }, [qrCodeData]) + + // Navigate to offline presentation route + const pushToOfflinePresentation = withHaptics((data: { sessionTranscript: string; deviceRequest: string }) => + replace({ + pathname: '/notifications/offlinePresentation', + params: data, + }) + ) + + // useEffect(() => { + // // Cleanup function that runs when component unmounts + // return () => { + // shutdownDataTransfer() + // } + // }, []) + + const onCancel = () => { + back() + shutdownDataTransfer() + } + + // if (Platform.OS === 'ios') { + // toast.show('This feature is not supported on your OS yet.', { customData: { preset: 'warning' } }) + // return back() + // } + + return ( + + + + Share with QR code + + A verifier needs to scan your QR-Code. + + + {qrCodeData ? ( + + + + ) : ( + + )} + + + {onCancel && ( + + + Cancel + + + )} + + + + ) +} diff --git a/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx b/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx index ef2c1fbe..68839034 100644 --- a/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx @@ -1,6 +1,7 @@ import { AnimatedStack, Button, + CustomIcons, FlexPage, Heading, HeroIcons, @@ -8,176 +9,93 @@ import { Paragraph, ScrollView, Spacer, - Stack, XStack, YStack, - useScaleAnimation, + useSpringify, } from '@package/ui' -import { useRouter } from 'solito/router' +import { useRouter } from 'expo-router' -import { useCredentialsForDisplay } from '@package/agent' -import { useHaptics, useNetworkCallback, useScrollViewPosition } from '@package/app/src/hooks' -import { FunkeCredentialCard } from 'packages/app' -import { FadeIn, FadeInDown, ZoomIn } from 'react-native-reanimated' -import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useFirstNameFromPidCredential } from '@easypid/hooks' +import { useHaptics } from '@package/app/src/hooks' +import { FadeIn } from 'react-native-reanimated' +import { Blob } from '../../../assets/Blob' +import { ActionCard } from './components/ActionCard' +import { AllCardsCard } from './components/AllCardsCard' import { LatestActivityCard } from './components/LatestActivityCard' export function FunkeWalletScreen() { const { push } = useRouter() - const { isLoading, credentials } = useCredentialsForDisplay() const { withHaptics } = useHaptics() + const { userName, isLoading } = useFirstNameFromPidCredential() + const pushToMenu = withHaptics(() => push('/menu')) const pushToScanner = withHaptics(() => push('/scan')) const pushToPidSetup = withHaptics(() => push('/pidSetup')) - const pushToCards = withHaptics(() => push('/credentials')) - - const { - pressStyle: qrPressStyle, - handlePressIn: qrHandlePressIn, - handlePressOut: qrHandlePressOut, - } = useScaleAnimation({ scaleInValue: 0.95 }) - - const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition() - const { bottom } = useSafeAreaInsets() + const pushToAbout = withHaptics(() => push('/menu/about')) + const pushToOffline = withHaptics(() => push('/offline')) return ( - - {/* Header */} - - } onPress={pushToMenu} /> - + + + + - {/* Body */} - 0} - onScroll={handleScroll} - scrollEventThrottle={scrollEventThrottle} - px="$4" - contentContainerStyle={{ - justifyContent: 'space-between', - paddingBottom: bottom, - flexGrow: 1, - }} - > - - - - - - - - - Scan QR-Code - - - {isLoading ? ( - - ) : credentials.length === 0 && !isLoading ? ( - - - - There's nothing here, yet - - Setup your ID or use the QR scanner to receive credentials. - - - + + } onPress={pushToMenu} /> + + + + + + + - Setup ID - - - - ) : credentials.length !== 0 && !isLoading ? ( - - - - - Recently used + {userName ? `Hello, ${userName}!` : 'Hello!'} - - {credentials.slice(0, 2).map((credential) => ( - push(`/credentials/${credential.id}`))} - /> - ))} - - {credentials.length > 2 && ( - - See all cards - - + Select what you want to do + + + } + title="QR-code" + onPress={pushToScanner} + /> + } + title="In-person" + onPress={pushToOffline} + /> + + + {userName ? ( + + How does it work? + + ) : ( + + Setup your ID + )} + + + + + + - - ) : ( - - )} - - - - + + + + + + ) } diff --git a/apps/easypid/src/features/wallet/components/ActionCard.tsx b/apps/easypid/src/features/wallet/components/ActionCard.tsx new file mode 100644 index 00000000..d6db9f5b --- /dev/null +++ b/apps/easypid/src/features/wallet/components/ActionCard.tsx @@ -0,0 +1,38 @@ +import { AnimatedStack, Heading, Stack, useScaleAnimation } from '@package/ui' +import type { ReactNode } from 'react' + +interface ActionCardProps { + variant?: 'primary' | 'secondary' + icon: ReactNode + title: string + onPress: () => void +} + +export function ActionCard({ icon, title, onPress, variant = 'primary' }: ActionCardProps) { + const { + pressStyle: qrPressStyle, + handlePressIn: qrHandlePressIn, + handlePressOut: qrHandlePressOut, + } = useScaleAnimation({ scaleInValue: 0.95 }) + + return ( + + {icon} + + {title} + + + ) +} diff --git a/apps/easypid/src/features/wallet/components/AllCardsCard.tsx b/apps/easypid/src/features/wallet/components/AllCardsCard.tsx new file mode 100644 index 00000000..b49fa6c1 --- /dev/null +++ b/apps/easypid/src/features/wallet/components/AllCardsCard.tsx @@ -0,0 +1,14 @@ +import { useRouter } from 'expo-router' +import { useCredentialsForDisplay } from 'packages/agent/src' +import { useHaptics } from 'packages/app/src' +import { InfoButton } from 'packages/ui/src' + +export function AllCardsCard() { + const { push } = useRouter() + const { withHaptics } = useHaptics() + + const { credentials } = useCredentialsForDisplay() + const pushToCards = withHaptics(() => push('/credentials')) + + return +} diff --git a/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx b/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx index 1aa92b33..bc8dea22 100644 --- a/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx +++ b/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx @@ -28,7 +28,7 @@ export function LatestActivityCard() { const credential = credentials.find((c) => c.id === latestActivity.credentialIds[0]) return { title: formatRelativeDate(new Date(latestActivity.date)), - description: `Added ${credential?.display.name ?? '1 card'}`, + description: `Added ${credential?.display.name}`, } } return null diff --git a/apps/easypid/src/hooks/usePidCredential.tsx b/apps/easypid/src/hooks/usePidCredential.tsx index 82ba095f..1cd99ffd 100644 --- a/apps/easypid/src/hooks/usePidCredential.tsx +++ b/apps/easypid/src/hooks/usePidCredential.tsx @@ -1,6 +1,6 @@ import { ClaimFormat, MdocRecord, SdJwtVcRecord } from '@credo-ts/core' import { type CredentialForDisplay, type CredentialMetadata, useCredentialsForDisplay } from '@package/agent' -import { sanitizeString } from '@package/utils' +import { capitalizeFirstLetter, sanitizeString } from '@package/utils' type Attributes = { given_name: string @@ -384,3 +384,19 @@ export function usePidCredential() { ), } as const } + +export function useFirstNameFromPidCredential() { + const { credential, isLoading } = usePidCredential() + + if (!credential?.attributes || typeof credential.attributes.given_name !== 'string') { + return { + userName: '', + isLoading, + } + } + + return { + userName: capitalizeFirstLetter(credential.attributes.given_name.toLowerCase()), + isLoading, + } +} diff --git a/packages/ui/assets/People.tsx b/packages/ui/assets/People.tsx new file mode 100644 index 00000000..f6374514 --- /dev/null +++ b/packages/ui/assets/People.tsx @@ -0,0 +1,24 @@ +import Svg, { Path, type SvgProps } from 'react-native-svg' + +export const PeopleIcon = ({ width = 24, height = 24, color = 'black', ...props }: SvgProps) => { + return ( + + + + + + + ) +} diff --git a/packages/ui/assets/Qr.tsx b/packages/ui/assets/Qr.tsx new file mode 100644 index 00000000..0bc20876 --- /dev/null +++ b/packages/ui/assets/Qr.tsx @@ -0,0 +1,46 @@ +import Svg, { Path, type SvgProps } from 'react-native-svg' + +export const QrIcon = ({ width = 24, height = 24, color = 'black', ...props }: SvgProps) => { + return ( + + + + + + + + + + + ) +} diff --git a/packages/ui/src/content/Icon.tsx b/packages/ui/src/content/Icon.tsx index f8358a1b..01bfcdd2 100644 --- a/packages/ui/src/content/Icon.tsx +++ b/packages/ui/src/content/Icon.tsx @@ -81,6 +81,8 @@ import { import { ExclamationIcon } from '../../assets/Exclamation' import { FaceIdIcon } from '../../assets/FaceId' +import { PeopleIcon } from '../../assets/People' +import { QrIcon } from '../../assets/Qr' import { styled } from 'tamagui' import { ConnectIcon } from '../../assets/Connect' @@ -185,6 +187,8 @@ export const CustomIcons = { Exclamation: wrapLocalSvg(ExclamationIcon as React.ComponentType), Connect: wrapLocalSvg(ConnectIcon as React.ComponentType), FaceId: wrapLocalSvg(FaceIdIcon as React.ComponentType), + Qr: wrapLocalSvg(QrIcon as React.ComponentType), + People: wrapLocalSvg(PeopleIcon as React.ComponentType), } export type CustomIconProps = SvgProps & { diff --git a/packages/ui/src/content/IconContainer.tsx b/packages/ui/src/content/IconContainer.tsx index 5b06a103..b980d74b 100644 --- a/packages/ui/src/content/IconContainer.tsx +++ b/packages/ui/src/content/IconContainer.tsx @@ -1,15 +1,22 @@ import { cloneElement } from 'react' -import type { StackProps } from 'tamagui' +import { Circle, type StackProps } from 'tamagui' import { AnimatedStack } from '../base' import { useScaleAnimation } from '../hooks' -interface IconContainerProps extends StackProps { +interface IconContainerProps extends Omit { icon: React.ReactElement scaleOnPress?: boolean + bg?: boolean 'aria-label'?: string } -export function IconContainer({ icon, scaleOnPress = true, 'aria-label': ariaLabel, ...props }: IconContainerProps) { +export function IconContainer({ + icon, + scaleOnPress = true, + bg = false, + 'aria-label': ariaLabel, + ...props +}: IconContainerProps) { const { handlePressIn, handlePressOut, pressStyle } = useScaleAnimation({ scaleInValue: scaleOnPress ? 0.9 : 1 }) return ( @@ -22,8 +29,10 @@ export function IconContainer({ icon, scaleOnPress = true, 'aria-label': ariaLab onPressIn={handlePressIn} onPressOut={handlePressOut} aria-label={ariaLabel} + pos="relative" {...props} > + {bg && } {cloneElement(icon, { strokeWidth: icon.props.strokeWidth ?? 2, size: icon.props.size ?? 24, From 9310512eebdeaca0d4d73f85a3928811f7580323 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 18 Dec 2024 16:08:58 +0100 Subject: [PATCH 2/9] chore: offline --- .../features/wallet/FunkeOfflineQrScreen.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx b/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx index ea4d79bc..dc13a194 100644 --- a/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx @@ -57,22 +57,23 @@ export function FunkeOfflineQrScreen() { }) ) - // useEffect(() => { - // // Cleanup function that runs when component unmounts - // return () => { - // shutdownDataTransfer() - // } - // }, []) + useEffect(() => { + // Cleanup function that runs when component unmounts + return () => { + shutdownDataTransfer() + } + }, []) const onCancel = () => { back() shutdownDataTransfer() } - // if (Platform.OS === 'ios') { - // toast.show('This feature is not supported on your OS yet.', { customData: { preset: 'warning' } }) - // return back() - // } + if (Platform.OS === 'ios') { + toast.show('This feature is not supported on your OS yet.', { customData: { preset: 'warning' } }) + back() + return + } return ( From a65f84c1dd329b8ed19aa9587ab9cb8829b7b727 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 19 Dec 2024 17:15:34 +0100 Subject: [PATCH 3/9] fix: offline permissions Signed-off-by: Jan --- .../features/activity/FunkeActivityScreen.tsx | 2 +- .../wallet/FunkeCredentialsScreen.tsx | 4 +- .../features/wallet/FunkeOfflineQrScreen.tsx | 82 ++++++++++++++++--- .../src/features/wallet/FunkeWalletScreen.tsx | 28 +++++-- .../features/wallet/components/ActionCard.tsx | 24 ++++-- .../wallet/components/LatestActivityCard.tsx | 6 +- packages/ui/src/components/ProgressHeader.tsx | 4 +- packages/ui/src/content/IconContainer.tsx | 11 ++- 8 files changed, 121 insertions(+), 40 deletions(-) diff --git a/apps/easypid/src/features/activity/FunkeActivityScreen.tsx b/apps/easypid/src/features/activity/FunkeActivityScreen.tsx index 3bf6be35..120e1d79 100644 --- a/apps/easypid/src/features/activity/FunkeActivityScreen.tsx +++ b/apps/easypid/src/features/activity/FunkeActivityScreen.tsx @@ -29,7 +29,7 @@ export function FunkeActivityScreen({ entityId }: { entityId?: string }) { return ( - + Activity diff --git a/apps/easypid/src/features/wallet/FunkeCredentialsScreen.tsx b/apps/easypid/src/features/wallet/FunkeCredentialsScreen.tsx index 366a17ce..fbceb1c3 100644 --- a/apps/easypid/src/features/wallet/FunkeCredentialsScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeCredentialsScreen.tsx @@ -41,7 +41,7 @@ export function FunkeCredentialsScreen() { return ( - + Cards @@ -161,7 +161,7 @@ function FunkeCredentialRowCard({ name, backgroundColor, textColor, logo, onPres Issued on {formatDate(new Date(), { includeTime: false })} - } /> + } /> ) } diff --git a/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx b/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx index dc13a194..01a2a30b 100644 --- a/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeOfflineQrScreen.tsx @@ -1,3 +1,4 @@ +import { mmkv } from '@easypid/storage/mmkv' import { AnimatedStack, Button, @@ -14,27 +15,94 @@ import { import { useRouter } from 'expo-router' import { useHaptics } from 'packages/app/src' import { useEffect, useState } from 'react' -import { Platform, useWindowDimensions } from 'react-native' +import { Alert, Linking, useWindowDimensions } from 'react-native' +import { useMMKVBoolean } from 'react-native-mmkv' import QRCode from 'react-native-qrcode-svg' import easypidLogo from '../../../assets/icon-rounded.png' -import { checkMdocPermissions, shutdownDataTransfer, waitForDeviceRequest } from '../proximity' +import { + checkMdocPermissions, + getMdocQrCode, + requestMdocPermissions, + shutdownDataTransfer, + waitForDeviceRequest, +} from '../proximity' export function FunkeOfflineQrScreen() { const { withHaptics } = useHaptics() const { replace, back } = useRouter() const { width } = useWindowDimensions() const toast = useToastController() - const [isProcessing, setIsProcessing] = useState(false) - const [isLoading, setIsLoading] = useState(false) + const [qrCodeData, setQrCodeData] = useState() const [arePermissionsGranted, setArePermissionsGranted] = useState(false) + const [arePermissionsRequested, setArePermissionsRequested] = useMMKVBoolean('arePermissionsRequested', mmkv) useEffect(() => { void checkMdocPermissions().then((result) => { setArePermissionsGranted(!!result) + if (!result) { + void requestPermissions() + } }) }, []) + useEffect(() => { + if (arePermissionsGranted) { + void getMdocQrCode().then(setQrCodeData) + } else { + setQrCodeData(undefined) + } + }, [arePermissionsGranted]) + + const handlePermissions = async () => { + const permissions = await requestMdocPermissions() + + if (!permissions) { + toast.show('Failed to request permissions.', { customData: { preset: 'danger' } }) + return { granted: false, shouldShowSettings: false } + } + + // Check if any permission is in 'never_ask_again' state + const hasNeverAskAgain = Object.values(permissions).some((status) => status === 'never_ask_again') + + if (hasNeverAskAgain) { + return { granted: false, shouldShowSettings: true } + } + + const permissionStatus = await checkMdocPermissions() + return { granted: !!permissionStatus, shouldShowSettings: false } + } + + const requestPermissions = async () => { + // First request without checking the never_ask_again state + if (!arePermissionsRequested) { + const { granted } = await handlePermissions() + setArePermissionsRequested(true) + setArePermissionsGranted(granted) + return + } + + // Subsequent requests need to check for the never_ask_again state + const { granted, shouldShowSettings } = await handlePermissions() + + if (shouldShowSettings) { + back() + Alert.alert( + 'Please enable required permissions in your phone settings', + 'Sharing with QR-Code needs access to Bluetooth and Location.', + [ + { + text: 'Open Settings', + onPress: () => Linking.openSettings(), + }, + ] + ) + return + } + + setArePermissionsGranted(granted) + } + useEffect(() => { if (qrCodeData) { void waitForDeviceRequest().then((data) => { @@ -69,12 +137,6 @@ export function FunkeOfflineQrScreen() { shutdownDataTransfer() } - if (Platform.OS === 'ios') { - toast.show('This feature is not supported on your OS yet.', { customData: { preset: 'warning' } }) - back() - return - } - return ( diff --git a/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx b/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx index 68839034..ead50a26 100644 --- a/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx @@ -12,11 +12,13 @@ import { XStack, YStack, useSpringify, + useToastController, } from '@package/ui' import { useRouter } from 'expo-router' import { useFirstNameFromPidCredential } from '@easypid/hooks' import { useHaptics } from '@package/app/src/hooks' +import { Platform } from 'react-native' import { FadeIn } from 'react-native-reanimated' import { Blob } from '../../../assets/Blob' import { ActionCard } from './components/ActionCard' @@ -26,6 +28,7 @@ import { LatestActivityCard } from './components/LatestActivityCard' export function FunkeWalletScreen() { const { push } = useRouter() const { withHaptics } = useHaptics() + const toast = useToastController() const { userName, isLoading } = useFirstNameFromPidCredential() @@ -33,20 +36,27 @@ export function FunkeWalletScreen() { const pushToScanner = withHaptics(() => push('/scan')) const pushToPidSetup = withHaptics(() => push('/pidSetup')) const pushToAbout = withHaptics(() => push('/menu/about')) - const pushToOffline = withHaptics(() => push('/offline')) + const pushToOffline = () => { + if (Platform.OS === 'ios') { + toast.show('This feature is not supported on your OS yet.', { customData: { preset: 'warning' } }) + return + } + + withHaptics(() => push('/offline'))() + } return ( - + - } onPress={pushToMenu} /> + } onPress={pushToMenu} /> - + @@ -58,19 +68,19 @@ export function FunkeWalletScreen() { > {userName ? `Hello, ${userName}!` : 'Hello!'} - Select what you want to do + Receive or share from your wallet - + } - title="QR-code" + title="Scan QR-code" onPress={pushToScanner} /> } - title="In-person" + title="Present In-person" onPress={pushToOffline} /> @@ -81,7 +91,7 @@ export function FunkeWalletScreen() { ) : ( - Setup your ID + Setup your ID )} diff --git a/apps/easypid/src/features/wallet/components/ActionCard.tsx b/apps/easypid/src/features/wallet/components/ActionCard.tsx index d6db9f5b..5439edfb 100644 --- a/apps/easypid/src/features/wallet/components/ActionCard.tsx +++ b/apps/easypid/src/features/wallet/components/ActionCard.tsx @@ -1,4 +1,4 @@ -import { AnimatedStack, Heading, Stack, useScaleAnimation } from '@package/ui' +import { AnimatedStack, Heading, Stack, XStack, YStack, useScaleAnimation } from '@package/ui' import type { ReactNode } from 'react' interface ActionCardProps { @@ -21,18 +21,24 @@ export function ActionCard({ icon, title, onPress, variant = 'primary' }: Action onPressIn={qrHandlePressIn} onPressOut={qrHandlePressOut} onPress={onPress} - ai="center" jc="center" bg={variant === 'primary' ? '$grey-900' : '$white'} - py="$4" - px="$6" - gap="$4" + p="$3" + fg={1} + gap="$3" br="$6" > - {icon} - - {title} - + + + {icon} + + + {title.split(' ').map((word) => ( + + {word} + + ))} + ) } diff --git a/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx b/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx index bc8dea22..e2829188 100644 --- a/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx +++ b/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx @@ -16,7 +16,11 @@ export function LatestActivityCard() { const pushToActivity = withHaptics(() => push('/activity')) const content = useMemo(() => { - if (!latestActivity) return null + if (!latestActivity) + return { + title: 'Recent activity', + description: 'No activity yet', + } if (latestActivity.type === 'shared') { const isPlural = latestActivity.request.credentials.length > 1 return { diff --git a/packages/ui/src/components/ProgressHeader.tsx b/packages/ui/src/components/ProgressHeader.tsx index 18023c45..92bc42a4 100644 --- a/packages/ui/src/components/ProgressHeader.tsx +++ b/packages/ui/src/components/ProgressHeader.tsx @@ -55,9 +55,9 @@ export function ProgressHeader({ mx={variant === 'small' ? '$-4' : '$0'} > {variant === 'small' ? ( - + ) : ( - + )} { icon: React.ReactElement scaleOnPress?: boolean - bg?: boolean + bg?: 'white' | 'grey' | 'transparent' 'aria-label'?: string } export function IconContainer({ icon, scaleOnPress = true, - bg = false, + bg = 'grey', 'aria-label': ariaLabel, ...props }: IconContainerProps) { @@ -23,16 +23,15 @@ export function IconContainer({ - {bg && } {cloneElement(icon, { strokeWidth: icon.props.strokeWidth ?? 2, size: icon.props.size ?? 24, From e00b69f32f533a59f6ccbdddf01e9009be622d8a Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 6 Jan 2025 13:48:19 +0100 Subject: [PATCH 4/9] chore: minor ui improvements --- .../features/activity/FunkeActivityScreen.tsx | 2 +- .../src/features/menu/FunkeAboutScreen.tsx | 2 +- .../src/features/menu/FunkeFeedbackScreen.tsx | 2 +- .../src/features/menu/FunkeMenuScreen.tsx | 2 +- .../src/features/menu/FunkeSettingsScreen.tsx | 2 +- .../features/onboarding/screens/id-card-pin.tsx | 3 ++- .../src/features/pid/PidWalletPinSlide.tsx | 7 ++++++- .../FunkeRequestedAttributesDetailScreen.tsx | 17 ++++++++++------- .../FunkeCredentialDetailAttributesScreen.tsx | 2 +- .../features/wallet/FunkeCredentialsScreen.tsx | 2 +- .../wallet/FunkeFederationDetailScreen.tsx | 2 +- .../src/features/wallet/FunkeWalletScreen.tsx | 4 ++-- packages/app/src/components/SlideWizard.tsx | 4 ++-- packages/ui/src/base/Page.tsx | 7 ++++++- packages/ui/src/content/IconContainer.tsx | 1 + 15 files changed, 37 insertions(+), 22 deletions(-) diff --git a/apps/easypid/src/features/activity/FunkeActivityScreen.tsx b/apps/easypid/src/features/activity/FunkeActivityScreen.tsx index 120e1d79..2e577bf6 100644 --- a/apps/easypid/src/features/activity/FunkeActivityScreen.tsx +++ b/apps/easypid/src/features/activity/FunkeActivityScreen.tsx @@ -29,7 +29,7 @@ export function FunkeActivityScreen({ entityId }: { entityId?: string }) { return ( - + Activity diff --git a/apps/easypid/src/features/menu/FunkeAboutScreen.tsx b/apps/easypid/src/features/menu/FunkeAboutScreen.tsx index 456df373..6cb78741 100644 --- a/apps/easypid/src/features/menu/FunkeAboutScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeAboutScreen.tsx @@ -18,7 +18,7 @@ export function FunkeAboutScreen() { return ( - + About the wallet diff --git a/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx b/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx index c03ba60f..42a40c90 100644 --- a/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx @@ -11,7 +11,7 @@ export function FunkeFeedbackScreen() { return ( - + Feedback diff --git a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx index 19ea2eea..1f7322bd 100644 --- a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx @@ -69,7 +69,7 @@ export function FunkeMenuScreen() { return ( - + Menu diff --git a/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx b/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx index ee8210dc..05b806f8 100644 --- a/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeSettingsScreen.tsx @@ -14,7 +14,7 @@ export function FunkeSettingsScreen() { return ( - + Settings diff --git a/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx b/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx index e11bfd06..a6db4341 100644 --- a/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx +++ b/apps/easypid/src/features/onboarding/screens/id-card-pin.tsx @@ -66,7 +66,8 @@ export const OnboardingIdCardPinEnter = forwardRef(({ goToNextStep }: Onboarding return ( - + {/* Overflow issue only present on smaller devices, so set to max height */} + (null) + // Make the pin pad fixed to the bottom of the screen on smaller devices + const { bottom } = useSafeAreaInsets() + const shouldStickToBottom = bottom < 16 + const onSubmitPin = async (pin: string) => { if (isLoading) return setIsLoading(true) @@ -30,7 +35,7 @@ export function PidWalletPinSlide({ title, subtitle, onEnterPin }: PidWalletPinS } return ( - + {title} diff --git a/apps/easypid/src/features/share/FunkeRequestedAttributesDetailScreen.tsx b/apps/easypid/src/features/share/FunkeRequestedAttributesDetailScreen.tsx index 7d3d215e..d8933a7b 100644 --- a/apps/easypid/src/features/share/FunkeRequestedAttributesDetailScreen.tsx +++ b/apps/easypid/src/features/share/FunkeRequestedAttributesDetailScreen.tsx @@ -19,7 +19,12 @@ import React, { useEffect, useRef, useState } from 'react' import { useRouter } from 'solito/router' import { CredentialAttributes, TextBackButton } from '@package/app/src/components' -import { useHaptics, useHasInternetConnection, useScrollViewPosition } from '@package/app/src/hooks' +import { + useHaptics, + useHasInternetConnection, + useHeaderRightAction, + useScrollViewPosition, +} from '@package/app/src/hooks' import { type CredentialForDisplayId, metadataForDisplay, useCredentialForDisplayById } from '@package/agent' import { useNavigation } from 'expo-router' @@ -45,16 +50,14 @@ export function FunkeRequestedAttributesDetailScreen({ const router = useRouter() const [scrollViewHeight, setScrollViewHeight] = useState(0) const { withHaptics } = useHaptics() - const navigation = useNavigation() const [isSheetOpen, setIsSheetOpen] = useState(false) const scrollViewRef = useRef(null) - useEffect(() => { - navigation.setOptions({ - headerRight: () => } onPress={() => setIsSheetOpen(true)} />, - }) - }, [navigation]) + useHeaderRightAction({ + icon: , + onPress: withHaptics(() => setIsSheetOpen(true)), + }) const { isVisible: isMetadataVisible, diff --git a/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx b/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx index 151ca914..ba22cbc2 100644 --- a/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx @@ -81,7 +81,7 @@ export function FunkeCredentialDetailAttributesScreen({ borderColor={isScrolledByOffset ? '$grey-200' : '$background'} /> - + Card attributes - + Cards diff --git a/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx b/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx index b8b3723c..a1082679 100644 --- a/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx @@ -44,7 +44,7 @@ export function FunkeFederationDetailScreen({ borderColor={isScrolledByOffset ? '$grey-200' : '$background'} /> - + About this party - + - + } onPress={pushToMenu} /> diff --git a/packages/app/src/components/SlideWizard.tsx b/packages/app/src/components/SlideWizard.tsx index 78ec90b4..66e2e31a 100644 --- a/packages/app/src/components/SlideWizard.tsx +++ b/packages/app/src/components/SlideWizard.tsx @@ -157,7 +157,7 @@ export const SlideWizard = forwardRef( return ( - + {cloneElement(icon, { From 36d40f2f6089962c609ca7bc2a48dd0d4ef77a5b Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 6 Jan 2025 14:33:12 +0100 Subject: [PATCH 5/9] fix: grey background on android --- apps/easypid/src/app/onboarding/index.tsx | 8 ++++---- packages/app/src/provider/Provider.tsx | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/easypid/src/app/onboarding/index.tsx b/apps/easypid/src/app/onboarding/index.tsx index 24acad67..9084fe35 100644 --- a/apps/easypid/src/app/onboarding/index.tsx +++ b/apps/easypid/src/app/onboarding/index.tsx @@ -1,5 +1,5 @@ import { useHasFinishedOnboarding, useOnboardingContext } from '@easypid/features/onboarding' -import { FlexPage, Heading, Paragraph, ProgressHeader, YStack } from '@package/ui' +import { AnimatedStack, FlexPage, Heading, Paragraph, ProgressHeader, YStack } from '@package/ui' import type React from 'react' import { useEffect, useRef } from 'react' import { AccessibilityInfo, Alert } from 'react-native' @@ -43,10 +43,10 @@ export default function OnboardingScreens() { page = onboardingContext.screen } else { page = ( - - + + - + - {children} + + {children} + From 1ea81509c655ba1f779069a7be8ef171cdc357f6 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 6 Jan 2025 15:52:57 +0100 Subject: [PATCH 6/9] fix: spacing --- .../easypid/src/features/activity/FunkeActivityScreen.tsx | 4 ++-- apps/easypid/src/features/menu/FunkeAboutScreen.tsx | 4 ++-- apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx | 4 ++-- apps/easypid/src/features/menu/FunkeMenuScreen.tsx | 8 +++----- apps/easypid/src/features/menu/FunkeSettingsScreen.tsx | 4 ++-- .../wallet/FunkeCredentialDetailAttributesScreen.tsx | 2 +- .../src/features/wallet/FunkeCredentialsScreen.tsx | 4 ++-- .../src/features/wallet/FunkeFederationDetailScreen.tsx | 2 +- 8 files changed, 15 insertions(+), 17 deletions(-) diff --git a/apps/easypid/src/features/activity/FunkeActivityScreen.tsx b/apps/easypid/src/features/activity/FunkeActivityScreen.tsx index 2e577bf6..2780f206 100644 --- a/apps/easypid/src/features/activity/FunkeActivityScreen.tsx +++ b/apps/easypid/src/features/activity/FunkeActivityScreen.tsx @@ -29,8 +29,8 @@ export function FunkeActivityScreen({ entityId }: { entityId?: string }) { return ( - - + + Activity diff --git a/apps/easypid/src/features/menu/FunkeAboutScreen.tsx b/apps/easypid/src/features/menu/FunkeAboutScreen.tsx index 6cb78741..4ab8514e 100644 --- a/apps/easypid/src/features/menu/FunkeAboutScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeAboutScreen.tsx @@ -18,8 +18,8 @@ export function FunkeAboutScreen() { return ( - - + + About the wallet diff --git a/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx b/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx index 42a40c90..737c9377 100644 --- a/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeFeedbackScreen.tsx @@ -11,8 +11,8 @@ export function FunkeFeedbackScreen() { return ( - - + + Feedback diff --git a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx index 1f7322bd..c5d651ac 100644 --- a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx @@ -69,11 +69,9 @@ export function FunkeMenuScreen() { return ( - - - - Menu - + + + Menu - - + + Settings diff --git a/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx b/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx index ba22cbc2..3aeb13cb 100644 --- a/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeCredentialDetailAttributesScreen.tsx @@ -81,7 +81,7 @@ export function FunkeCredentialDetailAttributesScreen({ borderColor={isScrolledByOffset ? '$grey-200' : '$background'} /> - + Card attributes - - + + Cards diff --git a/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx b/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx index a1082679..89d1c592 100644 --- a/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx @@ -44,7 +44,7 @@ export function FunkeFederationDetailScreen({ borderColor={isScrolledByOffset ? '$grey-200' : '$background'} /> - + About this party Date: Tue, 7 Jan 2025 10:54:17 +0100 Subject: [PATCH 7/9] fix: spinner on activity --- apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx b/apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx index 2364428e..ca3fa18c 100644 --- a/apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx +++ b/apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx @@ -61,6 +61,7 @@ export function FunkeActivityDetailScreen() { 'No information was provided on the purpose of the data request. Be cautious' } logo={activity.entity.logo} + overAskingResponse={{ validRequest: 'could_not_determine', reason: '' }} /> From 4f64a787a91a8390556045ab857585285d6bc40b Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 8 Jan 2025 11:13:50 +0100 Subject: [PATCH 8/9] fix: bug with requesting permissions --- .../src/features/proximity/mdocProximity.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/easypid/src/features/proximity/mdocProximity.ts b/apps/easypid/src/features/proximity/mdocProximity.ts index a0280542..f6713f43 100644 --- a/apps/easypid/src/features/proximity/mdocProximity.ts +++ b/apps/easypid/src/features/proximity/mdocProximity.ts @@ -25,13 +25,19 @@ type ShareDeviceResponseOptions = { submission: FormattedSubmission } -const PERMISSIONS = [ - 'android.permission.ACCESS_FINE_LOCATION', - 'android.permission.BLUETOOTH_CONNECT', - 'android.permission.BLUETOOTH_SCAN', - 'android.permission.BLUETOOTH_ADVERTISE', - 'android.permission.ACCESS_COARSE_LOCATION', -] as const as Permission[] +// Determine if device is running Android 12 or higher +const isAndroid12OrHigher = Platform.OS === 'android' && Platform.Version >= 31 + +// Older devices require different permissions for BLE transfers +const PERMISSIONS = ( + isAndroid12OrHigher + ? [ + 'android.permission.BLUETOOTH_CONNECT', + 'android.permission.BLUETOOTH_SCAN', + 'android.permission.BLUETOOTH_ADVERTISE', + ] + : ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'] +) as Permission[] export const requestMdocPermissions = async () => { if (Platform.OS !== 'android') return From 2f287ef7a8370b2d4dcc0dbddd54c2eb950812ae Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 8 Jan 2025 11:55:20 +0100 Subject: [PATCH 9/9] feat: haptics consistency --- apps/easypid/src/app/onboarding/index.tsx | 15 ++-- .../src/features/menu/FunkeMenuScreen.tsx | 1 - .../features/onboarding/onboardingContext.tsx | 11 ++- apps/easypid/src/hooks/useWalletReset.tsx | 31 ++++---- apps/easypid/src/utils/sharedPidSetup.ts | 2 - apps/easypid/tsconfig.json | 6 -- packages/app/src/components/SlideWizard.tsx | 70 ++++++++++--------- 7 files changed, 72 insertions(+), 64 deletions(-) diff --git a/apps/easypid/src/app/onboarding/index.tsx b/apps/easypid/src/app/onboarding/index.tsx index 9084fe35..55a7acf0 100644 --- a/apps/easypid/src/app/onboarding/index.tsx +++ b/apps/easypid/src/app/onboarding/index.tsx @@ -1,4 +1,5 @@ import { useHasFinishedOnboarding, useOnboardingContext } from '@easypid/features/onboarding' +import { useHaptics } from '@package/app' import { AnimatedStack, FlexPage, Heading, Paragraph, ProgressHeader, YStack } from '@package/ui' import type React from 'react' import { useEffect, useRef } from 'react' @@ -7,6 +8,7 @@ import { findNodeHandle } from 'react-native' import Animated, { FadeIn, FadeInRight, FadeOut } from 'react-native-reanimated' export default function OnboardingScreens() { + const { withHaptics } = useHaptics() const [hasFinishedOnboarding] = useHasFinishedOnboarding() const onboardingContext = useOnboardingContext() const headerRef = useRef(null) @@ -21,7 +23,7 @@ export default function OnboardingScreens() { } }, [onboardingContext.currentStep]) - const onReset = () => { + const onReset = withHaptics(() => { Alert.alert('Reset Onboarding', 'Are you sure you want to reset the onboarding process?', [ { text: 'Cancel', @@ -29,12 +31,12 @@ export default function OnboardingScreens() { }, { text: 'Yes', - onPress: () => { + onPress: withHaptics(() => { onboardingContext.reset() - }, + }), }, ]) - } + }) if (hasFinishedOnboarding) return null @@ -62,11 +64,6 @@ export default function OnboardingScreens() { )} {onboardingContext.page.subtitle && {onboardingContext.page.subtitle}} - {onboardingContext.page.caption && ( - - Remember: {onboardingContext.page.caption} - - )} {onboardingContext.screen} diff --git a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx index c5d651ac..470e53f4 100644 --- a/apps/easypid/src/features/menu/FunkeMenuScreen.tsx +++ b/apps/easypid/src/features/menu/FunkeMenuScreen.tsx @@ -61,7 +61,6 @@ export function FunkeMenuScreen() { icon: HeroIcons.IdentificationFilled, title: 'Setup digital ID', }} - onPress={onResetWallet} idx={0} /> ) diff --git a/apps/easypid/src/features/onboarding/onboardingContext.tsx b/apps/easypid/src/features/onboarding/onboardingContext.tsx index 2e41c15c..820bd686 100644 --- a/apps/easypid/src/features/onboarding/onboardingContext.tsx +++ b/apps/easypid/src/features/onboarding/onboardingContext.tsx @@ -23,6 +23,7 @@ import { getCredentialForDisplay, getCredentialForDisplayId, } from '@package/agent' +import { useHaptics } from '@package/app' import { secureWalletKey } from '@package/secure-store/secureUnlock' import { useToastController } from '@package/ui' import { capitalizeFirstLetter, getHostNameFromUrl, sleep } from '@package/utils' @@ -142,6 +143,7 @@ export function OnboardingContextProvider({ }: PropsWithChildren<{ initialStep?: OnboardingStep['step'] }>) { + const { successHaptic, lightHaptic } = useHaptics() const toast = useToastController() const secureUnlock = useSecureUnlock() const [currentStepName, setCurrentStepName] = useState(initialStep ?? 'welcome') @@ -168,6 +170,12 @@ export function OnboardingContextProvider({ const currentStep = onboardingSteps.find((step) => step.step === currentStepName) if (!currentStep) throw new Error(`Invalid step ${currentStepName}`) + useEffect(() => { + if (currentStepName && currentStepName !== 'welcome' && currentStepName !== 'pin-reenter') { + lightHaptic() + } + }, [lightHaptic, currentStepName]) + const goToNextStep = useCallback(() => { const currentStepIndex = onboardingSteps.findIndex((step) => step.step === currentStepName) // goToNextStep excludes alternative flows @@ -198,8 +206,9 @@ export function OnboardingContextProvider({ // Wait 500ms before navigating to home setTimeout(() => { router.replace('/') + successHaptic() }, 500) - }, [router, setHasFinishedOnboarding, receivePidUseCase]) + }, [router, setHasFinishedOnboarding, receivePidUseCase, successHaptic]) const onPinEnter = async (pin: string) => { setWalletPin(pin) diff --git a/apps/easypid/src/hooks/useWalletReset.tsx b/apps/easypid/src/hooks/useWalletReset.tsx index 86ded6b4..50e4c574 100644 --- a/apps/easypid/src/hooks/useWalletReset.tsx +++ b/apps/easypid/src/hooks/useWalletReset.tsx @@ -1,5 +1,7 @@ import { useSecureUnlock } from '@easypid/agent' import { resetWallet } from '@easypid/utils/resetWallet' +import { useHaptics } from '@package/app/src/hooks' + import { useRouter } from 'expo-router' import { useCallback } from 'react' import { Alert } from 'react-native' @@ -7,21 +9,24 @@ import { Alert } from 'react-native' export const useWalletReset = () => { const secureUnlock = useSecureUnlock() const router = useRouter() + const { withHaptics } = useHaptics() - const onResetWallet = useCallback(() => { - Alert.alert('Reset Wallet', 'Are you sure you want to reset the wallet?', [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Yes', - onPress: () => { - resetWallet(secureUnlock).then(() => router.replace('onboarding')) + const onResetWallet = withHaptics( + useCallback(() => { + Alert.alert('Reset Wallet', 'Are you sure you want to reset the wallet?', [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Yes', + onPress: withHaptics(() => { + resetWallet(secureUnlock).then(() => router.replace('onboarding')) + }), }, - }, - ]) - }, [secureUnlock, router]) + ]) + }, [secureUnlock, router, withHaptics]) + ) return onResetWallet } diff --git a/apps/easypid/src/utils/sharedPidSetup.ts b/apps/easypid/src/utils/sharedPidSetup.ts index f0ebda14..5c187eab 100644 --- a/apps/easypid/src/utils/sharedPidSetup.ts +++ b/apps/easypid/src/utils/sharedPidSetup.ts @@ -4,7 +4,6 @@ import { OnboardingIdCardFetch } from '@easypid/features/onboarding/screens/id-c import { OnboardingIdCardPinEnter } from '@easypid/features/onboarding/screens/id-card-pin' import { OnboardingIdCardRequestedAttributes } from '@easypid/features/onboarding/screens/id-card-requested-attributes' import { OnboardingIdCardScan } from '@easypid/features/onboarding/screens/id-card-scan' -import { OnboardingIdCardStart } from '@easypid/features/onboarding/screens/id-card-start' import { OnboardingIdCardVerify } from '@easypid/features/onboarding/screens/id-card-verify' export const SIMULATOR_PIN = '276536' @@ -140,7 +139,6 @@ export type OnboardingPage = title: string animation?: 'default' | 'delayed' subtitle?: string - caption?: string animationKey?: string } diff --git a/apps/easypid/tsconfig.json b/apps/easypid/tsconfig.json index c23a76b7..62c6515f 100644 --- a/apps/easypid/tsconfig.json +++ b/apps/easypid/tsconfig.json @@ -1,11 +1,5 @@ { "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./", - "paths": { - "@easypid/*": ["./apps/easypid/src/*"] - } - }, "include": [ "./src/**/*.ts", "./src/**/*.tsx", diff --git a/packages/app/src/components/SlideWizard.tsx b/packages/app/src/components/SlideWizard.tsx index 66e2e31a..6eeaa226 100644 --- a/packages/app/src/components/SlideWizard.tsx +++ b/packages/app/src/components/SlideWizard.tsx @@ -1,11 +1,11 @@ import { AnimatedStack, FlexPage, ProgressHeader, ScrollableStack, Stack } from '@package/ui' -import * as Haptics from 'expo-haptics' import type React from 'react' import { type ForwardedRef, forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react' import { Keyboard, type ScrollView } from 'react-native' import { Easing, runOnJS, useAnimatedStyle, useSharedValue, withSequence, withTiming } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { ConfirmationSheet } from '../components/ConfirmationSheet' +import { useHaptics } from '../hooks/useHaptics' import { useScrollViewPosition } from '../hooks/useScrollViewPosition' import { WizardProvider } from './WizardContext' @@ -41,6 +41,7 @@ export const SlideWizard = forwardRef( const opacity = useSharedValue(1) const translateX = useSharedValue(0) const scrollViewRef = useRef(null) + const { withHaptics } = useHaptics() const [currentStepIndex, setCurrentStepIndex] = useState(0) const [isCompleted, setIsCompleted] = useState(false) @@ -102,39 +103,43 @@ export const SlideWizard = forwardRef( [opacity, translateX, updateStep, scrollToTop] ) - const handleCancel = useCallback(() => { - Keyboard.dismiss() - setIsSheetOpen(true) - }, []) + const handleCancel = withHaptics( + useCallback(() => { + Keyboard.dismiss() + setIsSheetOpen(true) + }, []) + ) - const onConfirmCancel = () => { + const onConfirmCancel = withHaptics(() => { setIsSheetOpen(false) onCancel() - } - - const onBack = useCallback(() => { - if (isNavigating) return - if (isCompleted || isError || currentStepIndex === 0 || steps[currentStepIndex].backIsCancel) { - handleCancel() - } else { - setIsNavigating(true) - direction.value = 'backward' - animateTransition(false) - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) - } - }, [currentStepIndex, animateTransition, direction, handleCancel, steps, isCompleted, isNavigating, isError]) - - const onNext = useCallback( - (slide?: string) => { + }) + + const onBack = withHaptics( + useCallback(() => { if (isNavigating) return - if (currentStepIndex < steps.length - 1) { + if (isCompleted || isError || currentStepIndex === 0 || steps[currentStepIndex].backIsCancel) { + handleCancel() + } else { setIsNavigating(true) - direction.value = 'forward' - animateTransition(true, slide) - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) + direction.value = 'backward' + animateTransition(false) } - }, - [currentStepIndex, steps.length, animateTransition, direction, isNavigating] + }, [currentStepIndex, animateTransition, direction, handleCancel, steps, isCompleted, isNavigating, isError]) + ) + + const onNext = withHaptics( + useCallback( + (slide?: string) => { + if (isNavigating) return + if (currentStepIndex < steps.length - 1) { + setIsNavigating(true) + direction.value = 'forward' + animateTransition(true, slide) + } + }, + [currentStepIndex, steps.length, animateTransition, direction, isNavigating] + ) ) useImperativeHandle( @@ -146,10 +151,11 @@ export const SlideWizard = forwardRef( [onNext] ) - const completeProgressBar = useCallback(() => { - setIsCompleted(true) - void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) - }, []) + const completeProgressBar = withHaptics( + useCallback(() => { + setIsCompleted(true) + }, []) + ) const contextValue = { onNext, onBack, onCancel: handleCancel, completeProgressBar } const Screen = isError && errorScreen ? errorScreen : steps[currentStepIndex].screen