diff --git a/example/app.json b/example/app.json index a5c7689..4b10992 100644 --- a/example/app.json +++ b/example/app.json @@ -11,7 +11,20 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "plugins": ["@animo-id/react-native-ble-didcomm"], + "plugins": [ + [ + "@animo-id/react-native-ble-didcomm", + { + "bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices for DIDComm" + } + ], + [ + "expo-camera", + { + "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera" + } + ] + ], "ios": { "supportsTablet": true, "bundleIdentifier": "id.animo.BluetoothDidcommExample" diff --git a/example/index.js b/example/index.js index 90813e8..68d3fc7 100644 --- a/example/index.js +++ b/example/index.js @@ -1,3 +1,6 @@ +import 'react-native-get-random-values' +import 'fast-text-encoding' + import { registerRootComponent } from 'expo' import { App } from './src/App' diff --git a/example/metro.config.js b/example/metro.config.js index 354f43a..19acfd9 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -12,4 +12,6 @@ config.resolver.nodeModulesPaths = [ path.resolve(monorepoRoot, 'node_modules'), ] +config.resolver.sourceExts = ['js', 'json', 'ts', 'tsx', 'cjs'] + module.exports = config diff --git a/example/package.json b/example/package.json index ecd0ce0..77a7fec 100644 --- a/example/package.json +++ b/example/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { + "issue": "ts-node scripts/issueCredential.ts", "start": "expo start", "android": "expo run:android", "prebuild": "expo prebuild", @@ -10,14 +11,38 @@ }, "dependencies": { "@animo-id/react-native-ble-didcomm": "workspace:*", + "@credo-ts/anoncreds": "^0.5.11", + "@credo-ts/askar": "^0.5.11", + "@credo-ts/core": "^0.5.11", + "@credo-ts/indy-vdr": "^0.5.11", + "@credo-ts/node": "^0.5.11", + "@credo-ts/react-hooks": "^0.6.1", + "@credo-ts/react-native": "^0.5.11", + "@credo-ts/transport-ble": "^0.3.0", + "@hyperledger/anoncreds-nodejs": "^0.2.4", + "@hyperledger/anoncreds-react-native": "^0.2.4", + "@hyperledger/aries-askar-nodejs": "^0.2.3", + "@hyperledger/aries-askar-react-native": "^0.2.3", + "@hyperledger/indy-vdr-nodejs": "^0.2.2", + "@hyperledger/indy-vdr-react-native": "^0.2.2", "expo": "*", + "expo-camera": "~15.0.16", "expo-status-bar": "~1.12.1", + "fast-text-encoding": "^1.0.6", + "qrcode": "^1.5.4", + "qrcode-terminal": "^0.12.0", "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-fs": "^2.20.0", + "react-native-get-random-values": "^1.11.0" }, "devDependencies": { "@babel/core": "^7.20.0", + "@types/fast-text-encoding": "^1.0.3", + "@types/qrcode": "^1.5.5", + "@types/qrcode-terminal": "^0.12.2", "babel-plugin-module-resolver": "^5.0.2", + "ts-node": "^10.9.2", "typescript": "*" }, "private": true diff --git a/example/scripts/issueCredential.ts b/example/scripts/issueCredential.ts new file mode 100644 index 0000000..5df5410 --- /dev/null +++ b/example/scripts/issueCredential.ts @@ -0,0 +1,174 @@ +import { AnonCredsCredentialFormatService, AnonCredsModule } from '@credo-ts/anoncreds' +import { AskarModule } from '@credo-ts/askar' +import { + Agent, + AutoAcceptCredential, + ConnectionEventTypes, + type ConnectionStateChangedEvent, + ConnectionsModule, + ConsoleLogger, + CredentialEventTypes, + CredentialState, + type CredentialStateChangedEvent, + CredentialsModule, + DidExchangeState, + DidsModule, + HttpOutboundTransport, + KeyType, + LogLevel, + TypedArrayEncoder, + V2CredentialProtocol, + WsOutboundTransport, +} from '@credo-ts/core' +import { + IndyVdrAnonCredsRegistry, + IndyVdrIndyDidRegistrar, + IndyVdrIndyDidResolver, + IndyVdrModule, +} from '@credo-ts/indy-vdr' +import { HttpInboundTransport, agentDependencies } from '@credo-ts/node' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' +import QRCode from 'qrcode' +import { BC_GOV_TEST_NET } from '../src/credo/utils/genesis' + +const modules = { + askar: new AskarModule({ ariesAskar }), + indyVdr: new IndyVdrModule({ + indyVdr, + networks: [ + { + isProduction: false, + indyNamespace: 'bcovrin:test', + genesisTransactions: BC_GOV_TEST_NET, + }, + ], + }), + anoncreds: new AnonCredsModule({ + anoncreds, + registries: [new IndyVdrAnonCredsRegistry()], + }), + dids: new DidsModule({ + resolvers: [new IndyVdrIndyDidResolver()], + registrars: [new IndyVdrIndyDidRegistrar()], + }), + connections: new ConnectionsModule({ autoAcceptConnections: true }), + credentials: new CredentialsModule({ + autoAcceptCredentials: AutoAcceptCredential.Always, + credentialProtocols: [ + new V2CredentialProtocol({ + credentialFormats: [new AnonCredsCredentialFormatService()], + }), + ], + }), +} + +void (async () => { + const agent = new Agent({ + config: { + label: 'nodejs-register-agent', + walletConfig: { id: 'nodejs-register-agent', key: 'nodejs-register-key' }, + logger: new ConsoleLogger(LogLevel.off), + endpoints: ['https://44a5-94-157-0-163.ngrok-free.app'], + }, + modules, + dependencies: agentDependencies, + }) + + agent.registerOutboundTransport(new WsOutboundTransport()) + agent.registerOutboundTransport(new HttpOutboundTransport()) + agent.registerInboundTransport(new HttpInboundTransport({ port: 3001 })) + + await agent.initialize() + + const { outOfBandInvitation } = await agent.oob.createInvitation() + const url = outOfBandInvitation.toUrl({ domain: 'https://example.org' }) + const qrcode = await QRCode.toString(url, { type: 'terminal', small: true }) + console.log(qrcode) + + agent.events.on( + ConnectionEventTypes.ConnectionStateChanged, + async ({ payload: { connectionRecord } }) => { + console.log(`NEW CONNECTION STATE: ${connectionRecord.state}`) + if (connectionRecord.state === DidExchangeState.Completed) { + await issue(agent, connectionRecord.id) + } + } + ) + + agent.events.on( + CredentialEventTypes.CredentialStateChanged, + async ({ payload: { credentialRecord } }) => { + console.log(`NEW CREDENTIAL STATE: ${credentialRecord.state}`) + if (credentialRecord.state === CredentialState.Done) { + console.log('Issued credential!') + process.exit(0) + } + } + ) +})() + +const issue = async (agent: Agent, connectionId: string) => { + const seed = TypedArrayEncoder.fromString('abbakabba00000000000000000000000') + const unqualifiedIndyDid = 'NyKUCBjGVmmaUopiXAnLpj' + const indyDid = `did:indy:bcovrin:test:${unqualifiedIndyDid}` + await agent.dids.import({ + did: indyDid, + overwrite: true, + privateKeys: [{ keyType: KeyType.Ed25519, privateKey: seed }], + }) + + const schemaResult = await agent.modules.anoncreds.registerSchema({ + schema: { + name: 'react-native-ble-didcomm-schema', + version: `1.0.${Date.now()}`, + attrNames: ['name', 'age'], + issuerId: indyDid, + }, + options: {}, + }) + + if (schemaResult.schemaState.state === 'failed') { + throw new Error(`Error creating schema: ${schemaResult.schemaState.reason}`) + } + + console.log('registered schema') + + const credentialDefinitionResult = await agent.modules.anoncreds.registerCredentialDefinition({ + credentialDefinition: { + tag: 'default', + issuerId: indyDid, + schemaId: schemaResult.schemaState.schemaId, + }, + options: { + supportRevocation: false, + }, + }) + + if (credentialDefinitionResult.credentialDefinitionState.state === 'failed') { + throw new Error( + `Error creating credential definition: ${credentialDefinitionResult.credentialDefinitionState.reason}` + ) + } + + if (!credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId) { + throw new Error('Could not find credential definition id on state') + } + + console.log('registered credential definition!') + + await agent.credentials.offerCredential({ + protocolVersion: 'v2', + connectionId: connectionId, + credentialFormats: { + anoncreds: { + credentialDefinitionId: credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId, + attributes: [ + { name: 'name', value: 'Jane Doe' }, + { name: 'age', value: '23' }, + ], + }, + }, + }) +} diff --git a/example/src/App.tsx b/example/src/App.tsx index b7e5e9b..4f6137d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,7 +1,9 @@ -import React, { type ReactElement, useState } from 'react' -import { Button, StyleSheet, View } from 'react-native' +import React, { type ReactElement, useState, useEffect } from 'react' +import { Button, Platform, StyleSheet, View } from 'react-native' +import { requestPermissions } from './RequestPermissions' import { Spacer } from './Spacer' +import { CredoScreen } from './credo/CredoScreen' import { RegularScreen } from './regular/Screen' export const App = () => { @@ -9,6 +11,12 @@ export const App = () => { let component: ReactElement + useEffect(() => { + if (Platform.OS === 'android') { + void requestPermissions() + } + }, []) + if (!flow) { component = ( <> @@ -24,7 +32,7 @@ export const App = () => { } if (flow === 'credo') { - component = + component = } return ( diff --git a/example/src/RequestPermissions.ts b/example/src/RequestPermissions.ts new file mode 100644 index 0000000..e474390 --- /dev/null +++ b/example/src/RequestPermissions.ts @@ -0,0 +1,11 @@ +import { type Permission, PermissionsAndroid } from 'react-native' + +const PERMISSIONS = [ + 'android.permission.ACCESS_FINE_LOCATION', + 'android.permission.BLUETOOTH_CONNECT', + 'android.permission.BLUETOOTH_SCAN', + 'android.permission.BLUETOOTH_ADVERTISE', + 'android.permission.ACCESS_COARSE_LOCATION', +] as const as Permission[] + +export const requestPermissions = async () => PermissionsAndroid.requestMultiple(PERMISSIONS) diff --git a/example/src/RequestPermissions.tsx b/example/src/RequestPermissions.tsx deleted file mode 100644 index 50852c6..0000000 --- a/example/src/RequestPermissions.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PermissionsAndroid } from 'react-native' - -export const requestPermissions = async () => { - await PermissionsAndroid.requestMultiple([ - 'android.permission.ACCESS_FINE_LOCATION', - 'android.permission.BLUETOOTH_CONNECT', - 'android.permission.BLUETOOTH_SCAN', - 'android.permission.BLUETOOTH_ADVERTISE', - 'android.permission.ACCESS_COARSE_LOCATION', - ]) -} diff --git a/example/src/credo/BleProver.tsx b/example/src/credo/BleProver.tsx new file mode 100644 index 0000000..f0fb74b --- /dev/null +++ b/example/src/credo/BleProver.tsx @@ -0,0 +1,48 @@ +import { + bleShareProof, + useCentral, + useCentralShutdownOnUnmount, + useCloseTransportsOnUnmount, +} from '@animo-id/react-native-ble-didcomm' +import type { Agent } from '@credo-ts/core' +import type React from 'react' +import { useState } from 'react' +import { Button, Text, View } from 'react-native' +import { Spacer } from '../Spacer' + +type BleProverProps = { + agent: Agent + serviceUuid: string +} + +export const BleProver: React.FC = ({ agent, serviceUuid }) => { + const [hasSharedProof, setHasSharedProof] = useState(false) + const { central } = useCentral() + + const onFailure = () => console.error('[CENTRAL]: failure') + const onConnected = () => console.log('[CENTRAL]: connected') + const onDisconnected = () => console.log('[CENTRAL]: disconnected') + + useCentralShutdownOnUnmount() + useCloseTransportsOnUnmount(agent) + + const shareProof = () => + bleShareProof({ + onFailure, + serviceUuid, + central, + agent, + onConnected, + onDisconnected, + }).then(() => setHasSharedProof(true)) + + return ( + + Ble Prover + +