diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d360cbc..e78ca2e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "editor.defaultFormatter": "biomejs.biome" } diff --git a/apps/funke/app/_layout.tsx b/apps/funke/app/_layout.tsx index cac8a003..52bb3e57 100644 --- a/apps/funke/app/_layout.tsx +++ b/apps/funke/app/_layout.tsx @@ -15,6 +15,7 @@ import { DefaultTheme, ThemeProvider } from '@react-navigation/native' import { Stack } from 'expo-router' import * as SplashScreen from 'expo-splash-screen' import { useEffect, useState } from 'react' +import { GestureHandlerRootView } from 'react-native-gesture-handler' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { initializeAppAgent } from '.' @@ -102,57 +103,59 @@ export default function HomeLayout() { return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ) } diff --git a/apps/funke/babel.config.js b/apps/funke/babel.config.js index e053a657..cab5f28f 100644 --- a/apps/funke/babel.config.js +++ b/apps/funke/babel.config.js @@ -17,6 +17,8 @@ module.exports = (api) => { disableExtraction: process.env.NODE_ENV === 'development', }, ], + // used for bottom sheet + 'react-native-reanimated/plugin', ], } } diff --git a/apps/funke/package.json b/apps/funke/package.json index 70ae84b3..2882b7b6 100644 --- a/apps/funke/package.json +++ b/apps/funke/package.json @@ -10,6 +10,7 @@ "prebuild": "APP_VARIANT=development expo prebuild --no-install" }, "dependencies": { + "@gorhom/bottom-sheet": "^4.6.3", "@hyperledger/anoncreds-react-native": "^0.2.2", "@hyperledger/aries-askar-react-native": "^0.2.0", "@hyperledger/indy-vdr-react-native": "^0.2.0", @@ -43,6 +44,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.16.2", "react-native-get-random-values": "~1.11.0", + "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.1", "react-native-screens": "~3.31.1", "react-native-svg": "15.2.0" diff --git a/apps/paradym/app/_layout.tsx b/apps/paradym/app/_layout.tsx index 5411aee7..36b4df25 100644 --- a/apps/paradym/app/_layout.tsx +++ b/apps/paradym/app/_layout.tsx @@ -19,6 +19,7 @@ import { useEffect, useState } from 'react' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { initializeAppAgent } from '.' +import { GestureHandlerRootView } from 'react-native-gesture-handler' import { mediatorDid } from './constants' void SplashScreen.preventAutoHideAsync() @@ -143,58 +144,63 @@ export default function HomeLayout() { return ( - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ) } diff --git a/apps/paradym/package.json b/apps/paradym/package.json index d1fb52c1..fe5811a8 100644 --- a/apps/paradym/package.json +++ b/apps/paradym/package.json @@ -43,6 +43,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.16.2", "react-native-get-random-values": "~1.11.0", + "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.1", "react-native-screens": "~3.31.1", "react-native-svg": "15.2.0" diff --git a/packages/agent/src/display.ts b/packages/agent/src/display.ts index a9bddebb..9be4e244 100644 --- a/packages/agent/src/display.ts +++ b/packages/agent/src/display.ts @@ -202,6 +202,11 @@ function getW3cCredentialDisplay( } } + // Use background color from the JFF credential if not provided by the OID4VCI metadata + if (!credentialDisplay.backgroundColor && jffCredential.credentialBranding?.backgroundColor) { + credentialDisplay.backgroundColor = jffCredential.credentialBranding.backgroundColor + } + return { ...credentialDisplay, // Last fallback, if there's really no name for the credential, we use a generic name diff --git a/packages/agent/src/format/formatPresentation.ts b/packages/agent/src/format/formatPresentation.ts index 781c09e3..2b6b46b5 100644 --- a/packages/agent/src/format/formatPresentation.ts +++ b/packages/agent/src/format/formatPresentation.ts @@ -12,51 +12,53 @@ export interface FormattedSubmission { } export interface FormattedSubmissionEntry { - name: string + /** can be either AnonCreds groupName or PEX inputDescriptorId */ + inputDescriptorId: string isSatisfied: boolean - credentialName: string - issuerName?: string + + name: string description?: string - requestedAttributes?: string[] - backgroundColor?: string + + credentials: Array<{ + id: string + credentialName: string + issuerName?: string + requestedAttributes?: string[] + backgroundColor?: string + }> } export function formatDifPexCredentialsForRequest( credentialsForRequest: DifPexCredentialsForRequest ): FormattedSubmission { const entries = credentialsForRequest.requirements.flatMap((requirement) => { - return requirement.submissionEntry.map((submission) => { - // FIXME: support credential selection from JFF branch - const [firstVerifiableCredential] = submission.verifiableCredentials - if (firstVerifiableCredential) { - // Credential can be satisfied - const { display, credential } = getCredentialForDisplay(firstVerifiableCredential.credentialRecord) - - // TODO: support nesting - let requestedAttributes: string[] - if (firstVerifiableCredential.type === ClaimFormat.SdJwtVc) { - const { metadata, visibleProperties } = filterAndMapSdJwtKeys(firstVerifiableCredential.disclosedPayload) - requestedAttributes = [...Object.keys(visibleProperties), ...Object.keys(metadata)] - } else { - requestedAttributes = Object.keys(credential?.credentialSubject ?? {}) - } - - return { - name: submission.name ?? 'Unknown', - description: submission.purpose, - isSatisfied: true, - credentialName: display.name, - issuerName: display.issuer.name, - requestedAttributes, - backgroundColor: display.backgroundColor, - } - } + return requirement.submissionEntry.map((submission): FormattedSubmissionEntry => { return { + inputDescriptorId: submission.inputDescriptorId, name: submission.name ?? 'Unknown', description: submission.purpose, - isSatisfied: false, - // fallback to submission name because there is no credential - credentialName: submission.name ?? 'Credential name', + isSatisfied: submission.verifiableCredentials.length >= 1, + + credentials: submission.verifiableCredentials.map((verifiableCredential) => { + const { display, credential } = getCredentialForDisplay(verifiableCredential.credentialRecord) + + // TODO: support nesting + let requestedAttributes: string[] + if (verifiableCredential.type === ClaimFormat.SdJwtVc) { + const { metadata, visibleProperties } = filterAndMapSdJwtKeys(verifiableCredential.disclosedPayload) + requestedAttributes = [...Object.keys(visibleProperties), ...Object.keys(metadata)] + } else { + requestedAttributes = Object.keys(credential?.credentialSubject ?? {}) + } + + return { + id: verifiableCredential.credentialRecord.id, + credentialName: display.name, + issuerName: display.issuer.name, + requestedAttributes, + backgroundColor: display.backgroundColor, + } + }), } }) }) diff --git a/packages/agent/src/hooks/useDidCommPresentationActions.ts b/packages/agent/src/hooks/useDidCommPresentationActions.ts index a8898e29..03b502ed 100644 --- a/packages/agent/src/hooks/useDidCommPresentationActions.ts +++ b/packages/agent/src/hooks/useDidCommPresentationActions.ts @@ -3,9 +3,10 @@ import type { AnonCredsRequestedAttributeMatch, AnonCredsRequestedPredicate, AnonCredsRequestedPredicateMatch, + AnonCredsSelectedCredentials, } from '@credo-ts/anoncreds' import type { ProofStateChangedEvent } from '@credo-ts/core' -import type { FormattedSubmission } from '../format/formatPresentation' +import type { FormattedSubmission, FormattedSubmissionEntry } from '../format/formatPresentation' import { CredentialRepository, CredoError, ProofEventTypes, ProofState } from '@credo-ts/core' import { useConnectionById, useProofById } from '@credo-ts/react-hooks' @@ -16,8 +17,6 @@ import { filter, first, timeout } from 'rxjs/operators' import { useAgent } from '../agent' import { getDidCommCredentialExchangeDisplayMetadata } from '../didcomm/metadata' -type ProofedCredentialEntry = FormattedSubmission['entries'][number] - export function useDidCommPresentationActions(proofExchangeId: string) { const { agent } = useAgent() @@ -26,126 +25,134 @@ export function useDidCommPresentationActions(proofExchangeId: string) { const { data } = useQuery({ queryKey: ['didCommPresentationSubmission', proofExchangeId], - queryFn: async (): Promise => { + queryFn: async () => { const repository = agent.dependencyManager.resolve(CredentialRepository) const formatData = await agent.proofs.getFormatData(proofExchangeId) + const proofRequest = formatData.request?.anoncreds ?? formatData.request?.indy const credentialsForRequest = await agent.proofs.getCredentialsForRequest({ proofRecordId: proofExchangeId, }) + const formatKey = formatData.request?.anoncreds !== undefined ? 'anoncreds' : 'indy' const anonCredsCredentials = credentialsForRequest.proofFormats.anoncreds ?? credentialsForRequest.proofFormats.indy - if (!anonCredsCredentials || !proofRequest) { throw new CredoError('Invalid proof request.') } - const entries = new Map() - const mergeOrSetEntry = (key: string, newEntry: ProofedCredentialEntry) => { - const entry = entries.get(key) - if (entry) { - entries.set(key, { - name: entry.name || newEntry.name, - backgroundColor: entry.backgroundColor || newEntry.backgroundColor, - description: entry.description || newEntry.description, - credentialName: entry.credentialName || newEntry.credentialName, - issuerName: entry.issuerName || newEntry.issuerName, - isSatisfied: entry.isSatisfied && newEntry.isSatisfied, // Check if both are true otherwise it's not satisfied - requestedAttributes: [...(entry.requestedAttributes ?? []), ...(newEntry.requestedAttributes ?? [])], + const entries = new Map< + string, + { + groupNames: { + attributes: string[] + predicates: string[] + } + matches: Array + requestedAttributes: Set + } + >() + + const mergeOrSetEntry = ( + type: 'attribute' | 'predicate', + groupName: string, + requestedAttributeNames: string[], + matches: AnonCredsRequestedAttributeMatch[] | AnonCredsRequestedPredicateMatch[] + ) => { + // We create an entry hash. This way we can group all items that have the same credentials + // available. If no credentials are available for a group, we create a entry hash based + // on the group name + const entryHash = groupName.includes('__CREDENTIAL__') + ? groupName.split('__CREDENTIAL__')[0] + : matches.length > 0 + ? matches + .map((a) => a.credentialId) + .sort() + .join(',') + : groupName + + const entry = entries.get(entryHash) + + if (!entry) { + entries.set(entryHash, { + groupNames: { + attributes: type === 'attribute' ? [groupName] : [], + predicates: type === 'predicate' ? [groupName] : [], + }, + matches, + requestedAttributes: new Set(requestedAttributeNames), }) + return + } + + if (type === 'attribute') { + entry.groupNames.attributes.push(groupName) } else { - entries.set(key, newEntry) + entry.groupNames.predicates.push(groupName) } - } - await Promise.all( - Object.keys(anonCredsCredentials.attributes).map(async (groupName) => { - const requestedAttribute = proofRequest.requested_attributes[groupName] - const attributeNames = requestedAttribute?.names ?? [requestedAttribute?.name as string] - const attributeArray = anonCredsCredentials.attributes[groupName] as AnonCredsRequestedAttributeMatch[] - - const firstMatch = attributeArray[0] - - // When the credentialId isn't available and there is no __CREDENTIAL__ in the groupName, we use the groupName as the key but it will result in multiple entries in the view. But I think it's not an easy task to merge them - const credentialKey = - firstMatch?.credentialId ?? - (groupName.includes('__CREDENTIAL__') ? groupName.split('__CREDENTIAL__')[0] : groupName) - - if (!firstMatch) { - mergeOrSetEntry(credentialKey, { - credentialName: 'Credential', // TODO: we can extract this from the schema name, but we would have to fetch it - isSatisfied: false, - name: groupName, // TODO - requestedAttributes: attributeNames, - }) - } else { - const credentialExchange = await repository.findSingleByQuery(agent.context, { - credentialIds: [firstMatch.credentialId], - }) + entry.requestedAttributes = new Set([...requestedAttributeNames, ...entry.requestedAttributes]) - const credentialDisplayMetadata = credentialExchange - ? getDidCommCredentialExchangeDisplayMetadata(credentialExchange) - : undefined + // We only include the matches which are present in both entries. If we use the __CREDENTIAL__ it means we can only use + // credentials that match both (we want this in Paradym). For the other ones we create a 'hash' from all available credentialIds + // first already, so it should give the same result. + entry.matches = entry.matches.filter((match) => + matches.some((innerMatch) => match.credentialId === innerMatch.credentialId) + ) + } - mergeOrSetEntry(credentialKey, { - name: groupName, // TODO: humanize string? Or should we let this out? - credentialName: credentialDisplayMetadata?.credentialName ?? 'Credential', - isSatisfied: true, - issuerName: credentialDisplayMetadata?.issuerName ?? 'Unknown', - requestedAttributes: attributeNames, - }) - } - }) - ) + const allCredentialIds = [ + ...Object.values(anonCredsCredentials.attributes).flatMap((matches) => + matches.map((match) => match.credentialId) + ), + ...Object.values(anonCredsCredentials.predicates).flatMap((matches) => + matches.map((match) => match.credentialId) + ), + ] + const credentialExchanges = await repository.findByQuery(agent.context, { + $or: allCredentialIds.map((credentialId) => ({ credentialIds: [credentialId] })), + }) - await Promise.all( - Object.keys(anonCredsCredentials.predicates).map(async (groupName) => { - const requestedPredicate = proofRequest.requested_predicates[groupName] - const predicateArray = anonCredsCredentials.predicates[groupName] as AnonCredsRequestedPredicateMatch[] + for (const [groupName, attributeArray] of Object.entries(anonCredsCredentials.attributes)) { + const requestedAttribute = proofRequest.requested_attributes[groupName] + if (!requestedAttribute) throw new Error('Invalid presentation request') + const requestedAttributesNames = requestedAttribute.names ?? [requestedAttribute.name as string] - if (!requestedPredicate) { - throw new Error('Invalid presentation request') - } + mergeOrSetEntry('attribute', groupName, requestedAttributesNames, attributeArray) + } - // FIXME: we need to still filter based on the predicate (e.g. age is actually >= 18) - // This should probably be fixed in AFJ. - const firstMatch = predicateArray[0] - - // When the credentialId isn't available and there is no __CREDENTIAL__ in the groupName, we use the groupName as the key but it will result in multiple entries in the view. But I think it's not an easy task to merge them - const credentialKey = - firstMatch?.credentialId ?? - (groupName.includes('__CREDENTIAL__') ? groupName.split('__CREDENTIAL__')[0] : groupName) - - if (!firstMatch) { - mergeOrSetEntry(credentialKey, { - credentialName: 'Credential', // TODO: we can extract this from the schema name, but we would have to fetch it - isSatisfied: false, - name: groupName, // TODO - requestedAttributes: [formatPredicate(requestedPredicate)], - }) - } else { - const credentialExchange = await repository.findSingleByQuery(agent.context, { - credentialIds: [firstMatch.credentialId], - }) + for (const [groupName, predicateArray] of Object.entries(anonCredsCredentials.predicates)) { + const requestedPredicate = proofRequest.requested_predicates[groupName] + if (!requestedPredicate) throw new Error('Invalid presentation request') + mergeOrSetEntry('predicate', groupName, [formatPredicate(requestedPredicate)], predicateArray) + } + + const entriesArray = Array.from(entries.entries()).map(([entryHash, entry]): FormattedSubmissionEntry => { + return { + inputDescriptorId: entryHash, + credentials: entry.matches.map((match) => { + const credentialExchange = credentialExchanges.find((c) => + c.credentials.find((cc) => cc.credentialRecordId === match.credentialId) + ) const credentialDisplayMetadata = credentialExchange ? getDidCommCredentialExchangeDisplayMetadata(credentialExchange) : undefined - mergeOrSetEntry(credentialKey, { - name: groupName, // TODO: humanize string? Or should we let this out? + return { + id: match.credentialId, credentialName: credentialDisplayMetadata?.credentialName ?? 'Credential', isSatisfied: true, issuerName: credentialDisplayMetadata?.issuerName ?? 'Unknown', - requestedAttributes: [formatPredicate(requestedPredicate)], - }) - } - }) - ) - - const entriesArray = Array.from(entries.values()) + requestedAttributes: Array.from(entry.requestedAttributes), + } + }), + isSatisfied: entry.matches.length > 0, + // TODO: we can fetch the schema name based on requirements + name: 'Credential', + } + }) const submission: FormattedSubmission = { areAllSatisfied: entriesArray.every((entry) => entry.isSatisfied), @@ -153,15 +160,47 @@ export function useDidCommPresentationActions(proofExchangeId: string) { name: proofRequest?.name ?? 'Unknown', } - submission.areAllSatisfied = submission.entries.every((entry) => entry.isSatisfied) - - return submission + return { submission, formatKey, entries } }, }) const { mutateAsync: acceptMutateAsync, status: acceptStatus } = useMutation({ mutationKey: ['acceptDidCommPresentation', proofExchangeId], - mutationFn: async () => { + mutationFn: async (selectedCredentials?: { [inputDescriptorId: string]: string }) => { + let formatInput: { indy?: AnonCredsSelectedCredentials; anoncreds?: AnonCredsSelectedCredentials } | undefined = + undefined + + if (selectedCredentials && Object.keys(selectedCredentials).length > 0) { + if (!data?.formatKey || !data.entries) throw new Error('Unable to accept presentation without credentials') + + const selectedAttributes: Record = {} + const selectedPredicates: Record = {} + + for (const [inputDescriptorId, entry] of Array.from(data.entries.entries())) { + const credentialId = selectedCredentials[inputDescriptorId] + const match = entry.matches.find((match) => match.credentialId === credentialId) ?? entry.matches[0] + + for (const groupName of entry.groupNames.attributes) { + selectedAttributes[groupName] = { + ...match, + revealed: true, + } + } + + for (const groupName of entry.groupNames.predicates) { + selectedPredicates[groupName] = match + } + } + + formatInput = { + [data.formatKey]: { + attributes: selectedAttributes, + predicates: selectedPredicates, + selfAttestedAttributes: {}, + }, + } + } + const presentationDone$ = agent.events.observable(ProofEventTypes.ProofStateChanged).pipe( // Correct record with id and state filter( @@ -175,8 +214,10 @@ export function useDidCommPresentationActions(proofExchangeId: string) { ) const presentationDonePromise = firstValueFrom(presentationDone$) - - await agent.proofs.acceptRequest({ proofRecordId: proofExchangeId }) + await agent.proofs.acceptRequest({ + proofRecordId: proofExchangeId, + proofFormats: formatInput, + }) await presentationDonePromise }, }) @@ -210,7 +251,7 @@ export function useDidCommPresentationActions(proofExchangeId: string) { acceptStatus, declineStatus, proofExchange, - submission: data, + submission: data?.submission, verifierName: connection?.theirLabel, } } diff --git a/packages/agent/src/invitation/handler.ts b/packages/agent/src/invitation/handler.ts index bdc7b528..2b0e8a80 100644 --- a/packages/agent/src/invitation/handler.ts +++ b/packages/agent/src/invitation/handler.ts @@ -21,7 +21,6 @@ import { CredentialState, DidJwk, DidKey, - DifPresentationExchangeService, JwaSignatureAlgorithm, OutOfBandRepository, ProofEventTypes, @@ -226,15 +225,33 @@ export const shareProof = async ({ agent, authorizationRequest, credentialsForRequest, + selectedCredentials, }: { agent: FullAppAgent authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest - // TODO: support selection credentialsForRequest: DifPexCredentialsForRequest + selectedCredentials: { [inputDescriptorId: string]: string } }) => { - const presentationExchangeService = agent.dependencyManager.resolve(DifPresentationExchangeService) + if (!credentialsForRequest.areRequirementsSatisfied) { + throw new Error('Requirements from proof request are not satisfied') + } + + // Map all requirements and entries to a credential record. If a credential record for an + // input descriptor has been provided in `selectedCredentials` we will use that. Otherwise + // it will pick the first available credential. + const credentials = Object.fromEntries( + credentialsForRequest.requirements.flatMap((requirement) => + requirement.submissionEntry.map((entry) => { + const credentialId = selectedCredentials[entry.inputDescriptorId] + const credential = + entry.verifiableCredentials.find((vc) => vc.credentialRecord.id === credentialId) ?? + entry.verifiableCredentials[0] + + return [entry.inputDescriptorId, [credential.credentialRecord]] + }) + ) + ) - const credentials = presentationExchangeService.selectCredentialsForRequest(credentialsForRequest) const result = await agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ authorizationRequest, presentationExchange: { diff --git a/packages/app/src/features/notifications/DidCommPresentationNotificationScreen.tsx b/packages/app/src/features/notifications/DidCommPresentationNotificationScreen.tsx index 6b4e4c19..2d55cf96 100644 --- a/packages/app/src/features/notifications/DidCommPresentationNotificationScreen.tsx +++ b/packages/app/src/features/notifications/DidCommPresentationNotificationScreen.tsx @@ -1,6 +1,6 @@ import { useAgent, useDidCommPresentationActions } from '@package/agent' import { useToastController } from '@package/ui' -import React from 'react' +import React, { useState } from 'react' import { useRouter } from 'solito/router' import { GettingInformationScreen } from './components/GettingInformationScreen' @@ -19,6 +19,10 @@ export function DidCommPresentationNotificationScreen({ proofExchangeId }: DidCo const { acceptPresentation, declinePresentation, proofExchange, acceptStatus, submission, verifierName } = useDidCommPresentationActions(proofExchangeId) + const [selectedCredentials, setSelectedCredentials] = useState<{ + [inputDescriptorId: string]: string + }>({}) + const pushToWallet = () => { router.back() router.push('/') @@ -29,7 +33,7 @@ export function DidCommPresentationNotificationScreen({ proofExchangeId }: DidCo } const onProofAccept = () => { - acceptPresentation() + acceptPresentation(selectedCredentials) .then(() => { toast.show('Information has been successfully shared.') }) @@ -58,6 +62,13 @@ export function DidCommPresentationNotificationScreen({ proofExchangeId }: DidCo // If state is not idle, it means we have pressed accept isAccepting={acceptStatus !== 'idle'} verifierName={verifierName} + selectedCredentials={selectedCredentials} + onSelectCredentialForInputDescriptor={(groupName: string, credentialId: string) => + setSelectedCredentials((selectedCredentials) => ({ + ...selectedCredentials, + [groupName]: credentialId, + })) + } /> ) } diff --git a/packages/app/src/features/notifications/OpenIdPresentationNotificationScreen.tsx b/packages/app/src/features/notifications/OpenIdPresentationNotificationScreen.tsx index 08c1ef0c..9eacb599 100644 --- a/packages/app/src/features/notifications/OpenIdPresentationNotificationScreen.tsx +++ b/packages/app/src/features/notifications/OpenIdPresentationNotificationScreen.tsx @@ -30,6 +30,10 @@ export function OpenIdPresentationNotificationScreen() { [credentialsForRequest] ) + const [selectedCredentials, setSelectedCredentials] = useState<{ + [inputDescriptorId: string]: string + }>({}) + const pushToWallet = useCallback(() => { router.back() router.push('/') @@ -68,6 +72,7 @@ export function OpenIdPresentationNotificationScreen() { agent, authorizationRequest: credentialsForRequest.authorizationRequest, credentialsForRequest: credentialsForRequest.credentialsForRequest, + selectedCredentials, }) .then(() => { toast.show('Information has been successfully shared.') @@ -95,6 +100,13 @@ export function OpenIdPresentationNotificationScreen() { submission={submission} isAccepting={isSharing} verifierName={credentialsForRequest.verifierHostName} + selectedCredentials={selectedCredentials} + onSelectCredentialForInputDescriptor={(inputDescriptorId: string, credentialId: string) => + setSelectedCredentials((selectedCredentials) => ({ + ...selectedCredentials, + [inputDescriptorId]: credentialId, + })) + } /> ) } diff --git a/packages/app/src/features/notifications/components/PresentationNotificationScreen.tsx b/packages/app/src/features/notifications/components/PresentationNotificationScreen.tsx index 15c01246..1a436525 100644 --- a/packages/app/src/features/notifications/components/PresentationNotificationScreen.tsx +++ b/packages/app/src/features/notifications/components/PresentationNotificationScreen.tsx @@ -1,10 +1,23 @@ +import type BottomSheet from '@gorhom/bottom-sheet' import type { FormattedSubmission } from '@package/agent' -import { Button, Heading, Paragraph, ScrollView, YStack } from '@package/ui' +import { + BottomSheetScrollView, + Button, + Heading, + Paragraph, + RefreshCw, + ScrollView, + Sheet, + Stack, + XStack, + YStack, +} from '@package/ui' import { sanitizeString } from '@package/utils' -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useNavigation } from 'expo-router' import { CredentialRowCard, DualResponseButtons } from '../../../components' interface PresentationNotificationScreenProps { @@ -13,6 +26,8 @@ interface PresentationNotificationScreenProps { onAccept: () => void onDecline: () => void verifierName?: string + selectedCredentials: { [inputDescriptorId: string]: string } + onSelectCredentialForInputDescriptor: (inputDescriptorId: string, credentialId: string) => void } export function PresentationNotificationScreen({ @@ -21,79 +36,143 @@ export function PresentationNotificationScreen({ isAccepting, submission, verifierName, + selectedCredentials, + onSelectCredentialForInputDescriptor, }: PresentationNotificationScreenProps) { + const [changeSubmissionCredentialIndex, setChangeSubmissionCredentialIndex] = useState(-1) const { bottom } = useSafeAreaInsets() + + const currentSubmissionEntry = + changeSubmissionCredentialIndex !== -1 ? submission.entries[changeSubmissionCredentialIndex] : undefined + + const navigation = useNavigation() + const ref = useRef(null) + + useEffect(() => { + if (currentSubmissionEntry) { + ref.current?.expand() + } else { + ref.current?.close() + } + }, [currentSubmissionEntry]) + + useEffect(() => { + navigation.setOptions({ + gestureEnabled: true, + }) + }, [navigation]) + return ( - - - - - - You have received an information request - {verifierName ? ` from ${verifierName}` : ''}. - - {submission.purpose && ( - - {submission.purpose} - - )} - - - {submission.entries.map((s) => ( - - - - - {s.description && ( - - {s.description} - - )} - - {s.isSatisfied && s.requestedAttributes ? ( - - The following information will be presented: - - {s.requestedAttributes.map((a) => ( - - • {sanitizeString(a)} + <> + + + + + + You have received an information request + {verifierName ? ` from ${verifierName}` : ''}. + + {submission.purpose && ( + + {submission.purpose} + + )} + + + {submission.entries.map((s, i) => { + const selectedCredentialId = selectedCredentials[s.inputDescriptorId] + const selectedCredential = s.credentials.find((c) => c.id === selectedCredentialId) ?? s.credentials[0] + + return ( + + 1 ? () => setChangeSubmissionCredentialIndex(i) : undefined} + pressStyle={{ backgroundColor: s.isSatisfied ? '$grey-100' : undefined }} + > + + + + + + {/* Disable credential selection until we have better UX */} + {/* {s.credentials.length > 1 && } */} + + {s.description && ( + + {s.description} - ))} + )} + {s.isSatisfied && selectedCredential?.requestedAttributes ? ( + + The following information will be presented: + + {selectedCredential.requestedAttributes.map((a) => ( + + • {sanitizeString(a)} + + ))} + + + ) : ( + + This credential is not present in your wallet. + + )} - ) : ( - - This credential is not present in your wallet. - - )} - - - ))} + + ) + })} + + {submission.areAllSatisfied ? ( + + ) : ( + + + You don't have the required credentials to satisfy this request. + + Close + + )} - {submission.areAllSatisfied ? ( - - ) : ( - - - You don't have the required credentials to satisfy this request. - - Close - - )} - - + + setChangeSubmissionCredentialIndex(-1)}> + + + {currentSubmissionEntry?.credentials.map((c, credentialIndex) => ( + { + onSelectCredentialForInputDescriptor(currentSubmissionEntry.inputDescriptorId, c.id) + setChangeSubmissionCredentialIndex(-1) + }} + key={c.id} + issuer={c.issuerName} + name={c.credentialName} + hideBorder={credentialIndex === currentSubmissionEntry.credentials.length - 1} + bgColor={c.backgroundColor} + /> + ))} + + + + ) } diff --git a/packages/ui/src/content/Icon.tsx b/packages/ui/src/content/Icon.tsx index 4eabfbf1..77253559 100644 --- a/packages/ui/src/content/Icon.tsx +++ b/packages/ui/src/content/Icon.tsx @@ -8,4 +8,5 @@ export { AlertOctagon, Inbox, X, + RefreshCw, } from '@tamagui/lucide-icons' diff --git a/packages/ui/src/panels/Sheet.tsx b/packages/ui/src/panels/Sheet.tsx index 6ac49ef6..8a71fea7 100644 --- a/packages/ui/src/panels/Sheet.tsx +++ b/packages/ui/src/panels/Sheet.tsx @@ -1,52 +1,41 @@ -import { Sheet as TSheet } from '@tamagui/sheet' -import { useState } from 'react' +import type { ForwardedRef } from 'react' -import { Button } from '../base' -import { ChevronDown, ChevronUp } from '../content' +import BottomSheet, { BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet' +import { forwardRef } from 'react' +import { StyleSheet } from 'react-native' type Props = { - open: boolean - setOpen: React.Dispatch> - showChevron?: boolean - snapPoints?: number[] + snapPoints?: string[] children?: React.ReactNode + onClose?: () => void } -export const Sheet = ({ open, setOpen, showChevron = false, snapPoints = [80], children }: Props) => { - const [position, setPosition] = useState(0) +export { BottomSheetScrollView } - return ( - <> - {showChevron && ( - : } - circular - onPress={() => setOpen((x) => !x)} - /> - )} - ) => { + return ( + ( + + )} + index={-1} snapPoints={snapPoints} - position={position} - onPositionChange={setPosition} - dismissOnSnapToBottom > - - - - {children} - - - - ) -} + {children} + + ) + } +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bdc9ba2..d076e9a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ importers: apps/funke: dependencies: + '@gorhom/bottom-sheet': + specifier: ^4.6.3 + version: 4.6.3(@types/react@18.2.79)(react-native-gesture-handler@2.16.2(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) '@hyperledger/anoncreds-react-native': specifier: ^0.2.2 version: 0.2.2(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) @@ -95,7 +98,7 @@ importers: version: 3.0.6(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) expo-router: specifier: ~3.5.16 - version: 3.5.16(expo-constants@16.0.2(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)(typescript@5.3.3) + version: 3.5.16(yh3fnxcrfoi2lc6zcgkyb5qnya) expo-secure-store: specifier: ~13.0.1 version: 13.0.1(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) @@ -126,6 +129,9 @@ importers: react-native-get-random-values: specifier: ~1.11.0 version: 1.11.0(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0)) + react-native-reanimated: + specifier: ~3.10.1 + version: 3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) react-native-safe-area-context: specifier: 4.10.1 version: 4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) @@ -216,7 +222,7 @@ importers: version: 3.0.6(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) expo-router: specifier: ~3.5.16 - version: 3.5.16(expo-constants@16.0.2(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)(typescript@5.3.3) + version: 3.5.16(yh3fnxcrfoi2lc6zcgkyb5qnya) expo-secure-store: specifier: ~13.0.1 version: 13.0.1(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) @@ -247,6 +253,9 @@ importers: react-native-get-random-values: specifier: ~1.11.0 version: 1.11.0(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0)) + react-native-reanimated: + specifier: ~3.10.1 + version: 3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) react-native-safe-area-context: specifier: 4.10.1 version: 4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) @@ -376,7 +385,7 @@ importers: version: 3.0.6(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) expo-router: specifier: ~3.5.16 - version: 3.5.16(expo-constants@16.0.2(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)(typescript@5.3.3) + version: 3.5.16(43v2hg54mtm624tu4gmcsqcpna) fast-text-encoding: specifier: ^1.0.6 version: 1.0.6 @@ -1701,6 +1710,27 @@ packages: '@floating-ui/utils@0.2.2': resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} + '@gorhom/bottom-sheet@4.6.3': + resolution: {integrity: sha512-fSuSfbtoKsjmSeyz+tG2C0GtcEL7PS63iEXI23c9M+HeCT1IFK6ffmIa2pqyqB43L1jtkR+BWkpZwqXnN4H8xA==} + peerDependencies: + '@types/react': ~18.2.79 + '@types/react-native': '*' + react: 18.2.0 + react-native: '*' + react-native-gesture-handler: '>=1.10.1' + react-native-reanimated: '>=2.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-native': + optional: true + + '@gorhom/portal@1.0.14': + resolution: {integrity: sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==} + peerDependencies: + react: 18.2.0 + react-native: '*' + '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: @@ -5536,6 +5566,20 @@ packages: peerDependencies: react: 18.2.0 + react-native-reanimated@3.10.1: + resolution: {integrity: sha512-sfxg6vYphrDc/g4jf/7iJ7NRi+26z2+BszPmvmk0Vnrz6FL7HYljJqTf531F1x6tFmsf+FEAmuCtTUIXFLVo9w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + react: 18.2.0 + react-native: '*' + + react-native-reanimated@3.12.1: + resolution: {integrity: sha512-aXyV1ydKNA2u9fqRL8Z4fJ2RxNAusujNDdC4k0y9CawNEay5AGYgxhANqmjAabGRzHxsvfCXJC09lvbTRMHIFA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + react: 18.2.0 + react-native: '*' + react-native-safe-area-context@4.10.1: resolution: {integrity: sha512-w8tCuowDorUkPoWPXmhqosovBr33YsukkwYCDERZFHAxIkx6qBadYxfeoaJ91nCQKjkNzGrK5qhoNOeSIcYSpA==} peerDependencies: @@ -8602,6 +8646,23 @@ snapshots: '@floating-ui/utils@0.2.2': {} + '@gorhom/bottom-sheet@4.6.3(@types/react@18.2.79)(react-native-gesture-handler@2.16.2(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)': + dependencies: + '@gorhom/portal': 1.0.14(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + invariant: 2.2.4 + react: 18.2.0 + react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0) + react-native-gesture-handler: 2.16.2(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + react-native-reanimated: 3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.79 + + '@gorhom/portal@1.0.14(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)': + dependencies: + nanoid: 3.3.7 + react: 18.2.0 + react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0) + '@graphql-typed-document-node/core@3.2.0(graphql@15.8.0)': dependencies: graphql: 15.8.0 @@ -11739,7 +11800,34 @@ snapshots: expo: 51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)) optional: true - expo-router@3.5.16(expo-constants@16.0.2(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-linking@6.3.1(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))))(expo-modules-autolinking@1.11.1)(expo-status-bar@1.12.1)(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)(typescript@5.3.3): + expo-router@3.5.16(43v2hg54mtm624tu4gmcsqcpna): + dependencies: + '@expo/metro-runtime': 3.2.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0)) + '@expo/server': 0.4.3(typescript@5.3.3) + '@radix-ui/react-slot': 1.0.1(react@18.2.0) + '@react-navigation/bottom-tabs': 6.5.20(@react-navigation/native@6.1.17(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + '@react-navigation/native': 6.1.17(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + '@react-navigation/native-stack': 6.9.26(@react-navigation/native@6.1.17(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + expo: 51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7)) + expo-constants: 16.0.2(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) + expo-linking: 6.3.1(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) + expo-splash-screen: 0.27.5(expo-modules-autolinking@1.11.1)(expo@51.0.12(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))) + expo-status-bar: 1.12.1 + react-native-helmet-async: 2.0.4(react@18.2.0) + react-native-safe-area-context: 4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + react-native-screens: 3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + schema-utils: 4.2.0 + optionalDependencies: + react-native-reanimated: 3.12.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) + transitivePeerDependencies: + - encoding + - expo-modules-autolinking + - react + - react-native + - supports-color + - typescript + + expo-router@3.5.16(yh3fnxcrfoi2lc6zcgkyb5qnya): dependencies: '@expo/metro-runtime': 3.2.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0)) '@expo/server': 0.4.3(typescript@5.3.3) @@ -11756,6 +11844,8 @@ snapshots: react-native-safe-area-context: 4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) react-native-screens: 3.31.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) schema-utils: 4.2.0 + optionalDependencies: + react-native-reanimated: 3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0) transitivePeerDependencies: - encoding - expo-modules-autolinking @@ -13576,6 +13666,39 @@ snapshots: react-fast-compare: 3.2.2 shallowequal: 1.1.0 + react-native-reanimated@3.10.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7) + '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) + convert-source-map: 2.0.0 + invariant: 2.2.4 + react: 18.2.0 + react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0) + transitivePeerDependencies: + - supports-color + + react-native-reanimated@3.12.1(@babel/core@7.24.7)(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7) + '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) + convert-source-map: 2.0.0 + invariant: 2.2.4 + react: 18.2.0 + react-native: 0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0) + transitivePeerDependencies: + - supports-color + optional: true + react-native-safe-area-context@4.10.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0