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