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 ? : infoButtonVariants[variant].icon}
-
+ {!noIcon && (
+
+ {image ? : 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'