Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: QR scanner package #1

Merged
merged 9 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/expo/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const config = {
ios: {
supportsTablet: true,
bundleIdentifier: 'id.paradym.wallet' + variant.bundle,
infoPlist: {
NSCameraUsageDescription: 'This app uses the camera to scan QR-codes.',
},
},
android: {
adaptiveIcon: {
Expand Down
11 changes: 10 additions & 1 deletion apps/expo/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,16 @@ export default function HomeLayout() {
<Provider>
<AgentProvider agent={agent}>
<ThemeProvider value={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack />
<Stack>
{/* Presentation type modal has to be manually defined */}
<Stack.Screen
name="scan"
options={{
presentation: 'modal',
headerShown: false,
}}
/>
</Stack>
</ThemeProvider>
</AgentProvider>
</Provider>
Expand Down
7 changes: 6 additions & 1 deletion apps/expo/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { H1, XStack } from '@internal/ui/src'
import { HomeScreen } from 'app/features/home/screen'
import { Stack } from 'expo-router'

Expand All @@ -6,7 +7,11 @@ export default function Screen() {
<>
<Stack.Screen
options={{
title: 'Home',
header: () => (
<XStack pt="$10" bg="$grey-200">
<H1>Credentials</H1>
</XStack>
),
}}
/>
<HomeScreen />
Expand Down
6 changes: 6 additions & 0 deletions apps/expo/app/scan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { QrScannerScreen } from 'app/features/scan/screen'

export default function Screen() {
// Presentation type modal has to be manually defined in the _layout.tsx
return <QrScannerScreen />
}
12 changes: 8 additions & 4 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,33 @@
},
"dependencies": {
"@babel/runtime": "^7.18.9",
"@hyperledger/aries-askar-react-native": "^0.1.0-dev.8",
"@hyperledger/aries-askar-react-native": "0.1.0-dev.8",
"@internal/agent": "*",
"@internal/scanner": "*",
"@internal/ui": "*",
"@react-native-masked-view/masked-view": "^0.2.9",
"@react-navigation/native": "^6.1.6",
"@types/react-native": "^0.71.3",
"app": "*",
"babel-plugin-module-resolver": "^4.1.0",
"burnt": "^0.10.0",
"expo": "~48.0.15",
"expo-barcode-scanner": "^12.3.2",
"expo-constants": "^14.2.1",
"expo-dev-client": "^2.1.5",
"expo-font": "^11.1.1",
"expo-image": "^1.0.0",
"expo-haptics": "^12.2.1",
"expo-image": "^1.2.3",
"expo-linear-gradient": "^12.1.2",
"expo-linking": "^4.0.1",
"expo-router": "^1.4.3",
"expo-splash-screen": "~0.18.2",
"expo-status-bar": "^1.4.4",
"expo-system-ui": "~2.2.1",
"expo-updates": "^0.16.3",
"react": "^18.2.0",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-native": "0.71.7",
"react-native": "0.71.8",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.9.0",
"react-native-get-random-values": "^1.8.0",
Expand Down
10 changes: 3 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
],
"scripts": {
"native": "cd apps/expo && yarn start",
"ios": "cd apps/expo && yarn ios",
"android": "cd apps/expo && yarn android",
"watch": "yarn workspaces foreach -pi run watch",
"fix": "manypkg fix",
"postinstall": "patch-package && yarn check-deps && yarn build",
"postinstall": "yarn check-deps && yarn build",
"build": "yarn workspaces foreach run build",
"upgrade:tamagui": "yarn up '*tamagui*'@latest '@tamagui/*'@latest react-native-web-lite@latest",
"upgrade:tamagui:canary": "yarn up '*tamagui*'@canary '@tamagui/*'@canary react-native-web-lite@canary",
Expand All @@ -21,11 +23,6 @@
"lint": "eslint --ignore-path .gitignore ."
},
"resolutions": {
"@types/react-native": "^0.71.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-refresh": "^0.14.0",
"react-native-svg": "13.4.0",
"@unimodules/react-native-adapter": "./noop",
"@unimodules/core": "./noop"
},
Expand All @@ -42,7 +39,6 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"node-gyp": "^9.3.1",
"patch-package": "^7.0.0",
"prettier": "^2.7.1",
"turbo": "^1.8.3",
"typescript": "^4.7.4"
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
"@aries-framework/react-native": "^0.4.0-alpha.120"
},
"peerDependencies": {
"@hyperledger/aries-askar-react-native": "^0.1.0-dev.8"
"@hyperledger/aries-askar-react-native": "0.1.0-dev.8"
},
"devDependencies": {
"@hyperledger/aries-askar-react-native": "^0.1.0-dev.8"
"@hyperledger/aries-askar-react-native": "0.1.0-dev.8"
}
}
1 change: 1 addition & 0 deletions packages/agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { initializeAgent, useAgent, AppAgent } from './agent'
export * from './providers'
export * from './parsers'
24 changes: 24 additions & 0 deletions packages/agent/src/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export enum QrTypes {
OPENID_INITIATE_ISSUANCE = 'openid-initiate-issuance',
OPENID = 'openid',
}

export const isOpenIdCredentialOffer = (url: string) => {
return url.startsWith(QrTypes.OPENID_INITIATE_ISSUANCE)
}

export const isOpenIdProofRequest = (url: string) => {
return url.startsWith(QrTypes.OPENID)
}

export const parseCredentialOffer = async ({ data }: { data: string }) => {
if (!data.startsWith(QrTypes.OPENID_INITIATE_ISSUANCE)) return null

return await Promise.resolve(data)
}

export const parseProofRequest = async ({ data }: { data: string }) => {
if (!data.startsWith(QrTypes.OPENID)) return null

return await Promise.resolve(data)
}
7 changes: 3 additions & 4 deletions packages/app/features/credentials/detail-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useW3cCredentialRecordById } from '@internal/agent'
import { Button, Paragraph, YStack } from '@internal/ui'
import { ChevronLeft } from '@tamagui/lucide-icons'
import { TextButton, Paragraph, YStack, Icon } from '@internal/ui/src'
import React from 'react'
import { createParam } from 'solito'
import { useLink } from 'solito/link'
Expand All @@ -25,9 +24,9 @@ export function CredentialDetailScreen() {
<YStack f={1} jc="center" ai="center" space>
<Paragraph ta="center" fow="700">{`Credential Record id: ${id}`}</Paragraph>
<Paragraph ta="center" fow="700">{`Record exists: ${record ? 'Yes' : 'No'}`}</Paragraph>
<Button {...link} icon={ChevronLeft}>
<TextButton {...link} icon={<Icon name="ChevronLeft" />}>
Go Home
</Button>
</TextButton>
</YStack>
)
}
104 changes: 14 additions & 90 deletions packages/app/features/home/screen.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import { useW3cCredentialRecords } from '@internal/agent'
import {
Anchor,
Button,
H1,
Paragraph,
Separator,
Sheet,
XStack,
YStack,
useToastController,
} from '@internal/ui'
import { ChevronDown, ChevronUp } from '@tamagui/lucide-icons'
import React, { useState } from 'react'
import { Page, Paragraph, SolidButton, TextButton, YStack } from '@internal/ui'
import React from 'react'
import { useLink } from 'solito/link'

export function HomeScreen() {
Expand All @@ -20,84 +9,19 @@ export function HomeScreen() {
href: '/credentials/some-random-id',
})

return (
<YStack f={1} jc="center" ai="center" p="$4" space>
<YStack space="$4" maw={600}>
<H1 ta="center">Welcome to Tamagui.</H1>
<Paragraph ta="center">
Here's a basic starter to show navigating from one screen to another. This screen uses the
same code on Next.js and React Native.
</Paragraph>

<Separator />
<Paragraph ta="center">
Made by{' '}
<Anchor color="$color12" href="https://twitter.com/natebirdman" target="_blank">
@natebirdman
</Anchor>
,{' '}
<Anchor
color="$color12"
href="https://github.com/tamagui/tamagui"
target="_blank"
rel="noreferrer"
>
give it a ⭐️
</Anchor>
</Paragraph>
</YStack>

<XStack>
<Paragraph>You have {w3cCredentialRecords.length} credentials.</Paragraph>
</XStack>

<XStack>
<Button {...linkProps}>Link to specific credential</Button>
</XStack>

<SheetDemo />
</YStack>
)
}

function SheetDemo() {
const [open, setOpen] = useState(false)
const [position, setPosition] = useState(0)
const toast = useToastController()
const qrLinkProps = useLink({
href: '/scan',
})

return (
<>
<Button
size="$6"
icon={open ? ChevronDown : ChevronUp}
circular
onPress={() => setOpen((x) => !x)}
/>
<Sheet
modal
open={open}
onOpenChange={setOpen}
snapPoints={[80]}
position={position}
onPositionChange={setPosition}
dismissOnSnapToBottom
>
<Sheet.Overlay />
<Sheet.Frame ai="center" jc="center">
<Sheet.Handle />
<Button
size="$6"
circular
icon={ChevronDown}
onPress={() => {
setOpen(false)
toast.show('Sheet closed!', {
message: 'Just showing how toast works...',
})
}}
/>
</Sheet.Frame>
</Sheet>
</>
<Page jc="space-between">
<YStack jc="center" ai="center" space>
<Paragraph>You have {w3cCredentialRecords.length} credentials.</Paragraph>
<TextButton {...linkProps}>Link to specific credential</TextButton>
</YStack>
<YStack jc="center">
<SolidButton {...qrLinkProps}>Scan</SolidButton>
</YStack>
</Page>
)
}
85 changes: 85 additions & 0 deletions packages/app/features/scan/screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
parseCredentialOffer,
parseProofRequest,
isOpenIdCredentialOffer,
isOpenIdProofRequest,
} from '@internal/agent'
import { QrScanner } from '@internal/scanner'
import { useToastController } from '@internal/ui'
import * as Haptics from 'expo-haptics'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'solito/router'

export function QrScannerScreen() {
const { push } = useRouter()
const toast = useToastController()

const [scannedData, setScannedData] = useState('')
const [readData, setReadData] = useState('')
const [isProcessing, setIsProcessing] = useState(false)
const [helpText, setHelpText] = useState('')

const unsupportedUrlPrefixes = ['c_i=', 'd_m=', 'oob=', '_oob=']

useEffect(() => {
const onScan = async (data: string) => {
// don't do anything if we already scanned the data
if (scannedData === readData) return
setScannedData(data)

if (isOpenIdCredentialOffer(scannedData)) {
setIsProcessing(true)
await parseCredentialOffer({ data })
.then(() => {
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
toast.show('Success!')
})
.catch(() => {
toast.show('Fail!')
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
})
.finally(() => push('/'))
} else if (isOpenIdProofRequest(scannedData)) {
setIsProcessing(true)
await parseProofRequest({ data })
.then(() => {
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
toast.show('Success!')
})
.catch(() => {
toast.show('Fail!')
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
})
.finally(() => push('/'))
} else {
setReadData(data)
triggerHelpText(data)
}
setIsProcessing(false)
}

if (scannedData) void onScan(scannedData)
}, [scannedData])

const triggerHelpText = (data: string) => {
const isUnsupportedUrl = unsupportedUrlPrefixes.find((f) => data.includes(f))
setHelpText(
isUnsupportedUrl
? 'This QR-code is not supported yet. Try another.'
: 'This QR-code format can not be used. Try another.'
)
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
//clear the help text after 3 seconds
setTimeout(() => {
setHelpText('')
}, 5000)
}

return (
<QrScanner
onScan={(data) => setScannedData(data)}
isProcessing={isProcessing}
helpText={helpText}
/>
)
}
Loading