diff --git a/apps/easypid/src/app/(app)/_layout.tsx b/apps/easypid/src/app/(app)/_layout.tsx index 75ad832c..c2c94ac8 100644 --- a/apps/easypid/src/app/(app)/_layout.tsx +++ b/apps/easypid/src/app/(app)/_layout.tsx @@ -86,6 +86,7 @@ export default function AppLayout() { gestureEnabled: false, }} /> + diff --git a/apps/easypid/src/app/(app)/credentials/index.tsx b/apps/easypid/src/app/(app)/credentials/index.tsx new file mode 100644 index 00000000..03d93679 --- /dev/null +++ b/apps/easypid/src/app/(app)/credentials/index.tsx @@ -0,0 +1,5 @@ +import { FunkeCredentialsScreen } from '@easypid/features/wallet/FunkeCredentialsScreen' + +export default function Screen() { + return +} diff --git a/apps/easypid/src/features/wallet/FunkeCredentialsScreen.tsx b/apps/easypid/src/features/wallet/FunkeCredentialsScreen.tsx new file mode 100644 index 00000000..c94e3189 --- /dev/null +++ b/apps/easypid/src/features/wallet/FunkeCredentialsScreen.tsx @@ -0,0 +1,168 @@ +import { useCredentialsWithCustomDisplay } from '@easypid/hooks/useCredentialsWithCustomDisplay' +import { useHaptics, useScrollViewPosition } from '@package/app/src/hooks' +import { + AnimatedStack, + FlexPage, + Heading, + HeroIcons, + IconContainer, + Image, + Input, + Loader, + LucideIcons, + Paragraph, + ScrollView, + Spacer, + Stack, + XStack, + YStack, + useScaleAnimation, +} from '@package/ui' +import { useRouter } from 'expo-router' +import type { DisplayImage } from 'packages/agent/src' +import { TextBackButton } from 'packages/app/src' +import { formatDate } from 'packages/utils/src' +import React, { useMemo, useState } from 'react' +import { FadeInDown } from 'react-native-reanimated' + +export function FunkeCredentialsScreen() { + const { credentials, isLoading: isLoadingCredentials } = useCredentialsWithCustomDisplay() + + const [searchQuery, setSearchQuery] = useState('') + const filteredCredentials = useMemo(() => { + return credentials.filter((credential) => credential.display.name.toLowerCase().includes(searchQuery.toLowerCase())) + }, [credentials, searchQuery]) + + const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition() + const { push } = useRouter() + const { withHaptics } = useHaptics() + + const pushToCredential = withHaptics((id: string) => push(`/credentials/${id}`)) + + return ( + + + + + Cards + + + {credentials.length === 0 ? ( + + + There's nothing here, yet + + Credentials will appear here once you receive them. + + ) : isLoadingCredentials ? ( + + + + + ) : ( + + + + + + + {filteredCredentials.length > 0 ? ( + filteredCredentials.map((credential) => ( + { + pushToCredential(credential.id) + }} + /> + )) + ) : ( + + No cards found for "{searchQuery}" + + )} + + + )} + + + + + ) +} + +interface FunkeCredentialRowCardProps { + name: string + backgroundColor: string + textColor: string + issuer: string + logo: DisplayImage | undefined + onPress: () => void +} + +function FunkeCredentialRowCard({ name, backgroundColor, textColor, logo, onPress }: FunkeCredentialRowCardProps) { + const { pressStyle, handlePressIn, handlePressOut } = useScaleAnimation({ scaleInValue: 0.99 }) + + const icon = logo?.url ? ( + + ) : ( + + + + ) + + return ( + + {icon} + + + {name.toLocaleUpperCase()} + + + Issued on {formatDate(new Date(), { includeTime: false })} + + + } /> + + ) +} diff --git a/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx b/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx index 680c074a..ea3c0741 100644 --- a/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx +++ b/apps/easypid/src/features/wallet/FunkeWalletScreen.tsx @@ -1,13 +1,11 @@ import { AnimatedStack, - BASE_CREDENTIAL_CARD_HEIGHT, Button, FlexPage, Heading, HeroIcons, IconContainer, Loader, - LucideIcons, Paragraph, ScrollView, Spacer, @@ -20,10 +18,12 @@ import { useRouter } from 'solito/router' import { useCredentialsWithCustomDisplay } from '@easypid/hooks/useCredentialsWithCustomDisplay' import { useWalletReset } from '@easypid/hooks/useWalletReset' -import { useHaptics, useNetworkCallback } from '@package/app/src/hooks' -import type { CredentialDisplay } from 'packages/agent/src' +import { useHaptics, useNetworkCallback, useScrollViewPosition } from '@package/app/src/hooks' import { FunkeCredentialCard } from 'packages/app' -import { FadeIn, FadeInDown, LinearTransition, ZoomIn, useAnimatedStyle } from 'react-native-reanimated' +import { useState } from 'react' +import { FadeInDown, ZoomIn } from 'react-native-reanimated' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { LatestActivityCard } from './components/LatestActivityCard' export function FunkeWalletScreen() { const { push } = useRouter() @@ -32,8 +32,8 @@ export function FunkeWalletScreen() { const { withHaptics } = useHaptics() const pushToMenu = withHaptics(() => push('/menu')) - const pushToActivity = withHaptics(() => push('/activity')) const pushToScanner = withHaptics(() => push('/scan')) + const pushToCards = withHaptics(() => push('/credentials')) const { pressStyle: qrPressStyle, @@ -41,150 +41,160 @@ export function FunkeWalletScreen() { handlePressOut: qrHandlePressOut, } = useScaleAnimation({ scaleInValue: 0.95 }) + const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition() + const { bottom } = useSafeAreaInsets() + const [scrollViewHeight, setScrollViewHeight] = useState(0) + return ( - - - - } onPress={pushToMenu} /> - } onPress={pushToActivity} /> - - - - + {/* Header */} + + } onPress={pushToMenu} /> + + + {/* Body */} + 0} + onScroll={handleScroll} + scrollEventThrottle={scrollEventThrottle} + px="$4" + onLayout={(e) => { + setScrollViewHeight(e.nativeEvent.layout.height) + }} + contentContainerStyle={{ + minHeight: credentials.length <= 1 ? scrollViewHeight : '100%', + justifyContent: 'space-between', + paddingBottom: bottom, + }} + > + + + + + + + + + Scan QR-Code + + + {credentials.length === 0 && !isLoading ? ( + - + + + There's nothing here, yet + + Setup your ID or use the QR scanner to receive credentials. + + + + Setup ID + + + + ) : isLoading ? ( + + + + + ) : ( + + + + + Recently used + + + {credentials.slice(0, 2).map((credential) => ( + push(`/credentials/${credential.id}`))} + /> + ))} + + {credentials.length > 2 && ( + + See all cards + + + )} + - - - Scan QR-Code - - - - {credentials.length === 0 ? ( - - - - There's nothing here, yet - - Setup your ID or use the QR scanner to receive credentials. - - - - Setup ID - - - - ) : isLoading ? ( - - - + )} - ) : ( - - - {credentials.map((credential, idx) => ( - - ))} - - - )} - - - Learn more about{' '} + + push('/menu/about')} - variant="annotation" + variant="sub" fontSize={13} - fontWeight="$semiBold" - color="$primary-500" + fontWeight="$medium" + ta="center" + px="$4" > - using this wallet + Learn more about{' '} + + using this wallet + + . - . - - + + ) } - -function AnimatedCredentialCard({ - display, - id, - index, -}: { - display: CredentialDisplay - id: string - index: number -}) { - const { push } = useRouter() - const { withHaptics } = useHaptics() - - const animatedStyle = useAnimatedStyle(() => { - const baseMargin = index * 72 - - return { - marginTop: baseMargin, - } - }) - - return ( - - push(`/credentials/${id}`))} - /> - - ) -} diff --git a/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx b/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx new file mode 100644 index 00000000..1e107566 --- /dev/null +++ b/apps/easypid/src/features/wallet/components/LatestActivityCard.tsx @@ -0,0 +1,48 @@ +import { useActivities } from '@easypid/features/activity/activityRecord' +import { useCredentialsWithCustomDisplay } from '@easypid/hooks/useCredentialsWithCustomDisplay' +import { InfoButton } from '@package/ui/src' +import { useRouter } from 'expo-router' +import { useHaptics } from 'packages/app/src/hooks' +import { formatRelativeDate } from 'packages/utils/src' +import { useMemo } from 'react' + +export function LatestActivityCard() { + const { push } = useRouter() + const { withHaptics } = useHaptics() + const { activities } = useActivities() + const latestActivity = activities[0] + const { credentials } = useCredentialsWithCustomDisplay() + + const pushToActivity = withHaptics(() => push('/activity')) + + const content = useMemo(() => { + if (!latestActivity) return null + if (latestActivity.type === 'shared') { + const isPlural = latestActivity.request.credentials.length > 1 + return { + title: formatRelativeDate(new Date(latestActivity.date)), + description: `Shared ${isPlural ? 'cards' : 'card'}`, + } + } + if (latestActivity.type === 'received') { + const credential = credentials.find((c) => c.id.includes(latestActivity.credentialIds[0])) + return { + title: formatRelativeDate(new Date(latestActivity.date)), + description: `Added ${credential?.display.name ?? '1 card'}`, + } + } + return null + }, [latestActivity, credentials]) + + if (!content) return null + + return ( + + ) +} diff --git a/packages/app/src/components/FunkeCredentialCard.tsx b/packages/app/src/components/FunkeCredentialCard.tsx index eac34117..4f4b4e8d 100644 --- a/packages/app/src/components/FunkeCredentialCard.tsx +++ b/packages/app/src/components/FunkeCredentialCard.tsx @@ -92,7 +92,7 @@ export function FunkeCredentialCard({ overflow="hidden" accessible={true} accessibilityRole={onPress ? 'button' : undefined} - aria-label={`${name.toLocaleUpperCase()} credential`} + aria-label="Credential" > diff --git a/packages/app/src/components/PinDotsInput.tsx b/packages/app/src/components/PinDotsInput.tsx index 668e3774..521d9c68 100644 --- a/packages/app/src/components/PinDotsInput.tsx +++ b/packages/app/src/components/PinDotsInput.tsx @@ -146,7 +146,7 @@ export const PinDotsInput = forwardRef( size="$1.5" backgroundColor={filled ? '$primary-500' : '$background'} borderColor="$primary-500" - borderWidth="$0.5" + borderWidth="$1" /> ))} diff --git a/packages/ui/src/components/InfoButton.tsx b/packages/ui/src/components/InfoButton.tsx index 4f7ffa8e..8fa544eb 100644 --- a/packages/ui/src/components/InfoButton.tsx +++ b/packages/ui/src/components/InfoButton.tsx @@ -55,6 +55,8 @@ interface InfoButtonProps { description: string onPress?: () => void routingType?: 'push' | 'modal' + noIcon?: boolean + ariaLabel?: string } export function InfoButton({ @@ -64,6 +66,8 @@ export function InfoButton({ description, onPress, routingType = 'push', + noIcon, + ariaLabel, }: InfoButtonProps) { const isPressable = !!onPress const { pressStyle, handlePressIn, handlePressOut } = useScaleAnimation() @@ -81,15 +85,17 @@ export function InfoButton({ bw="$0.5" accessible={true} accessibilityRole={onPress ? 'button' : undefined} - aria-label={`${title} ${description}`} + aria-label={ariaLabel ?? `${title}. ${description}`} borderColor="$grey-100" onPress={onPress} > - - {image ? {image.alt} : infoButtonVariants[variant].icon} - + {!noIcon && ( + + {image ? {image.alt} : infoButtonVariants[variant].icon} + + )} - + {title} diff --git a/packages/ui/src/content/Icon.tsx b/packages/ui/src/content/Icon.tsx index 3d1772a7..0155e60f 100644 --- a/packages/ui/src/content/Icon.tsx +++ b/packages/ui/src/content/Icon.tsx @@ -40,6 +40,7 @@ import { InformationCircleIcon, KeyIcon, LockClosedIcon, + MagnifyingGlassIcon, NoSymbolIcon, PlusIcon, QrCodeIcon, @@ -164,6 +165,7 @@ export const HeroIcons = { ChatBubbleBottomCenterTextFilled: wrapHeroIcon(ChatBubbleBottomCenterTextFilledIcon), Cog8ToothFilled: wrapHeroIcon(Cog8ToothFilledIcon), IdentificationFilled: wrapHeroIcon(IdentificationFilledIcon), + MagnifyingGlass: wrapHeroIcon(MagnifyingGlassIcon), } as const export const CustomIcons = { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 66cf143d..af1d2d57 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,6 +1,6 @@ export { tokens, config, absoluteFill } from './config/tamagui.config' export * from './constants' -export { TamaguiProviderProps, TamaguiProvider, Spacer, AnimatePresence, Circle } from 'tamagui' +export { TamaguiProviderProps, TamaguiProvider, Spacer, Input, AnimatePresence, Circle, VisuallyHidden } from 'tamagui' export { ToastProvider, useToastController, ToastViewport, useToastState } from '@tamagui/toast' export * from './panels' export * from './base'