From 014f2b541ef71fc812746e3749715d08333ec25a Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 18 Dec 2024 16:07:26 +0100 Subject: [PATCH 1/3] 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/3] 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/3] 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,