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

fix: person credential workflow pt. 2 #973

Merged
merged 15 commits into from
Mar 14, 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
99 changes: 33 additions & 66 deletions app/src/components/AddCredentialSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
import { CredentialMetadataKeys, CredentialState } from '@aries-framework/core'
import { useAgent, useCredentialByState } from '@aries-framework/react-hooks'
import { useCredentialByState } from '@aries-framework/react-hooks'
import { useNavigation } from '@react-navigation/core'
import { useTheme, useStore, Screens, Stacks } from 'aries-bifold'
import { useTheme, Screens, Stacks } from 'aries-bifold'
import React, { useEffect, useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { DeviceEventEmitter, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'

import { BCWalletEventTypes } from '../events/eventTypes'
import { showBCIDSelector, startFlow } from '../helpers/BCIDHelper'
import { BCState } from '../store'

import LoadingIcon from './LoadingIcon'
import { showBCIDSelector } from '../helpers/BCIDHelper'

const AddCredentialSlider: React.FC = () => {
const { ColorPallet, TextTheme } = useTheme()
const { agent } = useAgent()
const { t } = useTranslation()
const [store] = useStore<BCState>()
const [showGetFoundationCredential, setShowGetFoundationCredential] = useState<boolean>(false)
const [addCredentialPressed, setAddCredentialPressed] = useState<boolean>(false)
const [workflowInFlight, setWorkflowInFlight] = useState<boolean>(false)
const [workflowConnectionId, setWorkflowConnectionId] = useState<string | undefined>()
const navigation = useNavigation()

const offers = useCredentialByState(CredentialState.OfferReceived)
const [addCredentialPressed, setAddCredentialPressed] = useState<boolean>(false)
const [showGetFoundationCredential, setShowGetFoundationCredential] = useState<boolean>(false)

const credentials = [
...useCredentialByState(CredentialState.CredentialReceived),
...useCredentialByState(CredentialState.Done),
]

const navigation = useNavigation()
const [canUseLSBCredential] = useState<boolean>(true)

const styles = StyleSheet.create({
centeredView: {
marginTop: 'auto',
Expand Down Expand Up @@ -79,39 +67,25 @@ const AddCredentialSlider: React.FC = () => {
DeviceEventEmitter.emit(BCWalletEventTypes.ADD_CREDENTIAL_PRESSED, false)
}, [])

const goToHomeScreen = (credentialId?: string) => {
navigation.getParent()?.navigate(Stacks.NotificationStack, {
screen: Screens.CredentialOffer,
params: { credentialId },
})
}

const goToScanScreen = useCallback(() => {
deactivateSlider()
navigation.getParent()?.navigate(Stacks.ConnectStack, { screen: Screens.Scan })
}, [])

const onBCIDPress = useCallback(() => {
setWorkflowInFlight(true)
startFlow(agent!, store, setWorkflowInFlight, t, (connectionId) => setWorkflowConnectionId(connectionId))
}, [store])

useEffect(() => {
for (const credential of offers) {
if (credential.state == CredentialState.OfferReceived && credential.connectionId === workflowConnectionId) {
goToHomeScreen(credential.id)
deactivateSlider()
}
}
}, [offers, workflowConnectionId])
const goToPersonCredentialScreen = useCallback(() => {
deactivateSlider()
navigation.getParent()?.navigate(Stacks.NotificationStack, {
screen: Screens.CustomNotification,
})
}, [])

useEffect(() => {
const credentialDefinitionIDs = credentials.map(
(c) => c.metadata.data[CredentialMetadataKeys.IndyCredential].credentialDefinitionId as string
)

setShowGetFoundationCredential(showBCIDSelector(credentialDefinitionIDs, canUseLSBCredential))
}, [credentials, canUseLSBCredential])
setShowGetFoundationCredential(showBCIDSelector(credentialDefinitionIDs, true))
}, [credentials])

useEffect(() => {
const handle = DeviceEventEmitter.addListener(BCWalletEventTypes.ADD_CREDENTIAL_PRESSED, (value?: boolean) => {
Expand All @@ -125,34 +99,27 @@ const AddCredentialSlider: React.FC = () => {
}, [])

return (
<View>
<Modal animationType="slide" transparent={true} visible={addCredentialPressed} onRequestClose={deactivateSlider}>
<TouchableOpacity style={styles.outsideListener} onPress={deactivateSlider} />
<View style={styles.centeredView}>
<View style={styles.modalView}>
<TouchableOpacity onPress={deactivateSlider}>
<Icon name="window-close" size={35} style={styles.drawerRowItem}></Icon>
</TouchableOpacity>
<Text style={styles.drawerTitleText}>Choose</Text>
{showGetFoundationCredential && (
<TouchableOpacity style={styles.drawerRow} disabled={workflowInFlight} onPress={onBCIDPress}>
{workflowInFlight ? (
<LoadingIcon size={30} color={styles.drawerRowItem.color} active={workflowInFlight} />
) : (
<Icon name="credit-card" size={30} style={styles.drawerRowItem}></Icon>
)}

<Text style={{ ...styles.drawerRowItem, marginLeft: 5 }}>Get your Person credential</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.drawerRow} onPress={goToScanScreen}>
<Icon name="qrcode" size={30} style={styles.drawerRowItem}></Icon>
<Text style={{ ...styles.drawerRowItem, marginLeft: 5 }}>Scan a QR code</Text>
<Modal animationType="slide" transparent={true} visible={addCredentialPressed} onRequestClose={deactivateSlider}>
<TouchableOpacity style={styles.outsideListener} onPress={deactivateSlider} />
<View style={styles.centeredView}>
<View style={styles.modalView}>
<TouchableOpacity onPress={deactivateSlider}>
<Icon name="window-close" size={35} style={styles.drawerRowItem}></Icon>
</TouchableOpacity>
<Text style={styles.drawerTitleText}>Choose</Text>
{showGetFoundationCredential && (
<TouchableOpacity style={styles.drawerRow} onPress={goToPersonCredentialScreen}>
<Icon name="credit-card" size={30} style={styles.drawerRowItem}></Icon>
<Text style={{ ...styles.drawerRowItem, marginLeft: 5 }}>Get your Person credential</Text>
</TouchableOpacity>
</View>
)}
<TouchableOpacity style={styles.drawerRow} onPress={goToScanScreen}>
<Icon name="qrcode" size={30} style={styles.drawerRowItem}></Icon>
<Text style={{ ...styles.drawerRowItem, marginLeft: 5 }}>Scan a QR code</Text>
</TouchableOpacity>
</View>
</Modal>
</View>
</View>
</Modal>
)
}

Expand Down
26 changes: 7 additions & 19 deletions app/src/helpers/BCIDHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const cleanupAfterServiceCardAuthentication = (status: AuthenticationResu

export const authenticateWithServiceCard = async (
store: BCState,
setWorkflow: React.Dispatch<React.SetStateAction<boolean>>,
setWorkflowInProgress: React.Dispatch<React.SetStateAction<boolean>>,
agentDetails: WellKnownAgentDetails,
t: TFunction<'translation', undefined>,
callback?: (connectionId?: string) => void
Expand All @@ -195,15 +195,14 @@ export const authenticateWithServiceCard = async (
result.type === AuthenticationResultType.Cancel &&
typeof (result as unknown as RedirectResult).url === 'undefined'
) {
setWorkflow(false)
setWorkflowInProgress(false)
return
}

if (
result.type === AuthenticationResultType.Dismiss &&
typeof (result as unknown as RedirectResult).url === 'undefined'
) {
setWorkflow(false)
callback && callback(agentDetails.connectionId)
}

Expand All @@ -214,7 +213,6 @@ export const authenticateWithServiceCard = async (
(result as unknown as RedirectResult).url.includes(did) &&
(result as unknown as RedirectResult).url.includes('success')
) {
setWorkflow(false)
callback && callback(agentDetails.connectionId)
}

Expand All @@ -226,15 +224,8 @@ export const authenticateWithServiceCard = async (
(result as unknown as RedirectResult).url.includes(did) &&
(result as unknown as RedirectResult).url.includes('cancel')
) {
setWorkflow(false)

// FIXME: This does nothing unlit the catch below is updated.
// throw new BifoldError(
// t('Error.Title2025'),
// t('Error.Message2025'),
// t('Error.NoMessage'),
// ErrorCodes.ServiceCardError
// )
setWorkflowInProgress(false)
return
}
} else {
await Linking.openURL(url)
Expand All @@ -243,19 +234,17 @@ export const authenticateWithServiceCard = async (
cleanupAfterServiceCardAuthentication(AuthenticationResultType.Success)
} catch (error: unknown) {
const code = (error as BifoldError).code

cleanupAfterServiceCardAuthentication(
code === ErrorCodes.CanceledByUser ? AuthenticationResultType.Cancel : AuthenticationResultType.Fail
)

DeviceEventEmitter.emit(BifoldEventTypes.ERROR_ADDED, error)
}
}

export const startFlow = async (
agent: Agent,
store: BCState,
setWorkflowInFlight: React.Dispatch<React.SetStateAction<boolean>>,
setWorkflowInProgress: React.Dispatch<React.SetStateAction<boolean>>,
t: TFunction<'translation', undefined>,
callback?: (connectionId?: string) => void
) => {
Expand All @@ -264,12 +253,11 @@ export const startFlow = async (

if (agentDetails.legacyConnectionDid !== undefined) {
setTimeout(async () => {
await authenticateWithServiceCard(store, setWorkflowInFlight, agentDetails, t, callback)
await authenticateWithServiceCard(store, setWorkflowInProgress, agentDetails, t, callback)
}, connectionDelayInMs)
}
} catch (error: unknown) {
setWorkflowInFlight(false)

setWorkflowInProgress(false)
DeviceEventEmitter.emit(BifoldEventTypes.ERROR_ADDED, error)
}
}
26 changes: 26 additions & 0 deletions app/src/hooks/credential-offer-trigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CredentialState } from '@aries-framework/core'
import { useCredentialByState } from '@aries-framework/react-hooks'
import { useNavigation } from '@react-navigation/core'
import { Screens, Stacks } from 'aries-bifold'
import { useEffect } from 'react'

export const useCredentialOfferTrigger = (workflowConnectionId?: string): void => {
const navigation = useNavigation()

const offers = useCredentialByState(CredentialState.OfferReceived)

const goToCredentialOffer = (credentialId?: string) => {
navigation.getParent()?.navigate(Stacks.NotificationStack, {
screen: Screens.CredentialOffer,
params: { credentialId },
})
}

useEffect(() => {
for (const credential of offers) {
if (credential.state == CredentialState.OfferReceived && credential.connectionId === workflowConnectionId) {
goToCredentialOffer(credential.id)
}
}
}, [offers, workflowConnectionId])
}
4 changes: 2 additions & 2 deletions app/src/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ const translation = {
"PersonCredential": {
"Issuer": "Service BC",
"Name": "Person",
"GivenName":"Sample Given Name",
"FamilyName":"Sample Family Name",
"GivenName": "Sample Given Name",
"FamilyName": "Sample Family Name",
"Description": "Add your Person credential to your wallet to prove your personal information online and get access to services online.\n\nYou'll need the BC Service Card app set up on this mobile device.",
"LinkDescription": "Get the BC Services Card app",
"GetCredential": "Get your Person credential",
Expand Down
12 changes: 6 additions & 6 deletions app/src/localization/fr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,23 @@ const translation = {
"Warning": "Ensure only you have access to your wallet. (FR)",
"UseToUnlock": "Use biometrics to unlock wallet? (FR)",
},
"Credentials":{
"AddCredential":"Add Credential (FR)",
"Credentials": {
"AddCredential": "Add Credential (FR)",
"EmptyList": "Your wallet is empty. (FR)",
"AddFirstCredential": "Add your first credential (FR)"
},
"Screens": {
"Onboarding": "BC Wallet (FR)"
},
"PersonCredentialNotification": {
"Title":"Get your Person credential (FR)",
"Description":"Add your Person credential to your wallet and use it to get access to services online. (FR)"
"Title": "Get your Person credential (FR)",
"Description": "Add your Person credential to your wallet and use it to get access to services online. (FR)"
},
"PersonCredential": {
"Issuer": "Service BC (FR)",
"Name": "Person (FR)",
"GivenName":"Sample Given Name (FR)",
"FamilyName":"Sample Family Name (FR)",
"GivenName": "Sample Given Name (FR)",
"FamilyName": "Sample Family Name (FR)",
"Description": "Add your Person credential to your wallet to prove your personal information online and get access to services online.\n\nYou'll need the BC Service Card app set up on this mobile device. (FR)",
"LinkDescription": "Get the BC Services Card app (FR)",
"GetCredential": "Get your Person credential (FR)",
Expand Down
4 changes: 2 additions & 2 deletions app/src/localization/pt-br/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ const translation = {
"PersonCredential": {
"Issuer": "Service BC (PT-BR)",
"Name": "Person (PT-BR)",
"GivenName":"Sample Given Name (PT-BR)",
"FamilyName":"Sample Family Name (PT-BR)",
"GivenName": "Sample Given Name (PT-BR)",
"FamilyName": "Sample Family Name (PT-BR)",
"Description": "Add your Person credential to your wallet to prove your personal information online and get access to services online.\n\nYou'll need the BC Service Card app set up on this mobile device. (PT-BR)",
"LinkDescription": "Get the BC Services Card app (PT-BR)",
"GetCredential": "Get your Person credential (PT-BR)",
Expand Down
27 changes: 16 additions & 11 deletions app/src/screens/PersonCredential.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import { useAgent } from '@aries-framework/react-hooks'
import { useNavigation } from '@react-navigation/core'
import { Button, ButtonType, Screens, useStore, useTheme, CredentialCard } from 'aries-bifold'
import { Button, ButtonType, Screens, useStore, useTheme, CredentialCard, TabStacks } from 'aries-bifold'
import React, { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View, TouchableOpacity, Linking, FlatList } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'

import LoadingIcon from '../components/LoadingIcon'
import { startFlow } from '../helpers/BCIDHelper'
import { useCredentialOfferTrigger } from '../hooks/credential-offer-trigger'
import { BCDispatchAction, BCState } from '../store'

const PersonCredential: React.FC = () => {
const [workflowInFlight, setWorkflowInFlight] = useState<boolean>(false)
const { agent } = useAgent()
const navigation = useNavigation()

const [store, dispatch] = useStore<BCState>()
const [workflowInProgress, setWorkflowInProgress] = useState<boolean>(false)
const [workflowConnectionId, setWorkflowConnectionId] = useState<string | undefined>()

const { ColorPallet, TextTheme } = useTheme()
const { t } = useTranslation()

useCredentialOfferTrigger(workflowConnectionId)

const styles = StyleSheet.create({
pageContainer: {
flex: 1,
Expand All @@ -41,13 +46,12 @@ const PersonCredential: React.FC = () => {
type: BCDispatchAction.PERSON_CREDENTIAL_OFFER_DISMISSED,
payload: [{ personCredentialOfferDismissed: true }],
})

navigation.navigate(Screens.Home as never)
navigation.getParent()?.navigate(TabStacks.HomeStack, { screen: Screens.Home })
}, [])

const startGetBCIDCredentialWorkflow = useCallback(() => {
setWorkflowInFlight(true)
startFlow(agent!, store, setWorkflowInFlight, t, dismissPersonCredentialOffer)
const acceptPersonCredentialOffer = useCallback(() => {
setWorkflowInProgress(true)
startFlow(agent!, store, setWorkflowInProgress, t, (connectionId) => setWorkflowConnectionId(connectionId))
}, [])

const getBCServicesCardApp = useCallback(() => {
Expand All @@ -73,12 +77,12 @@ const PersonCredential: React.FC = () => {
<Button
title={t('PersonCredential.GetCredential')}
accessibilityLabel={t('PersonCredential.GetCredential')}
onPress={startGetBCIDCredentialWorkflow}
disabled={workflowInFlight}
onPress={acceptPersonCredentialOffer}
disabled={workflowInProgress}
buttonType={ButtonType.Primary}
>
{workflowInFlight && (
<LoadingIcon color={ColorPallet.grayscale.white} size={35} active={workflowInFlight} />
{workflowInProgress && (
<LoadingIcon color={ColorPallet.grayscale.white} size={35} active={workflowInProgress} />
)}
</Button>
</View>
Expand All @@ -87,6 +91,7 @@ const PersonCredential: React.FC = () => {
title={t('PersonCredential.Decline')}
accessibilityLabel={t('PersonCredential.Decline')}
onPress={dismissPersonCredentialOffer}
disabled={workflowInProgress}
buttonType={ButtonType.Secondary}
></Button>
</View>
Expand Down