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: added custom notifications to bcwallet #833

Merged
merged 6 commits into from
Jan 20, 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
19 changes: 17 additions & 2 deletions app/src/helpers/BCIDHelper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { DidRepository } from '@aries-framework/core'
import { BifoldError, DispatchAction, Agent } from 'aries-bifold'
import { DidRepository, CredentialExchangeRecord as CredentialRecord, CredentialMetadataKeys } from '@aries-framework/core'
import {
BifoldError,
DispatchAction,
Agent
} from 'aries-bifold'
import React, { ReducerAction } from 'react'
import { TFunction } from 'react-i18next'
import { Linking, Platform } from 'react-native'
Expand Down Expand Up @@ -89,6 +93,17 @@ export const showBCIDSelector = (credentialDefinitionIDs: string[], canUseLSBCre
return false
}

export const getInvitationCredentialDate = (credentials: CredentialRecord[], canUseLSBCCredential: boolean): Date | undefined => {

const invitationCredential = credentials.find((c) => {
const credDef = c.metadata.data[CredentialMetadataKeys.IndyCredential].credentialDefinitionId as string
if (trustedInvitationIssuerRe.test(credDef) || (trustedLSBCCredentialIssuerRe.test(credDef) && canUseLSBCCredential)) {
return true
}
})
return invitationCredential?.createdAt
}

export const recieveBCIDInvite = async (
agent: Agent | undefined,
store: BCState,
Expand Down
53 changes: 53 additions & 0 deletions app/src/hooks/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
CredentialExchangeRecord as CredentialRecord,
CredentialMetadataKeys,
CredentialState,
ProofRecord,
ProofState,
} from '@aries-framework/core'
import { useCredentialByState, useProofByState } from '@aries-framework/react-hooks'
import { useStore } from 'aries-bifold'

import { CredentialMetadata, customMetadata } from 'aries-bifold/App/types/metadata'
import { getInvitationCredentialDate, showBCIDSelector } from '../helpers/BCIDHelper'
import { BCState } from '../store'

interface CustomNotification {
type: 'CustomNotification'
createdAt: Date
id: string
}

interface Notifications {
total: number
notifications: Array<CredentialRecord | ProofRecord | CustomNotification>
}

export const useNotifications = (): Notifications => {
const [store] = useStore<BCState>()
const offers = useCredentialByState(CredentialState.OfferReceived)
const proofs = useProofByState(ProofState.RequestReceived)
const revoked = useCredentialByState(CredentialState.Done).filter((cred: CredentialRecord) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const metadata = cred!.metadata.get(CredentialMetadata.customMetadata) as customMetadata
if (cred?.revocationNotification && metadata?.revoked_seen == undefined) {
return cred
}
})

const credentials = [
...useCredentialByState(CredentialState.CredentialReceived),
...useCredentialByState(CredentialState.Done),
]
const credentialDefinitionIDs = credentials.map(
(c) => c.metadata.data[CredentialMetadataKeys.IndyCredential].credentialDefinitionId as string
)
const invitationDate = getInvitationCredentialDate(credentials, true)
const custom: CustomNotification[] = showBCIDSelector(credentialDefinitionIDs, true) && invitationDate && !store.dismissPersonCredentialOffer.personCredentialOfferDismissed ? [{ type: 'CustomNotification', createdAt: invitationDate, id: 'custom' }] : []

const notifications = [...offers, ...proofs, ...revoked, ...custom].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)

return { total: notifications.length, notifications }
}
22 changes: 20 additions & 2 deletions app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { Bundles } from 'aries-bifold/lib/typescript/App/types/oca'
import merge from 'lodash.merge'

import bundles from './assets/branding/credential-branding'
import BCIDView from './components/BCIDView'
import AddCredentialButton from './components/AddCredentialButton'
import AddCredentialSlider from './components/AddCredentialSlider'
import EmptyList from './components/EmptyList'
Expand All @@ -19,7 +18,11 @@ import Developer from './screens/Developer'
import { pages } from './screens/OnboardingPages'
import Splash from './screens/Splash'
import Terms from './screens/Terms'
import PersonCredentialScreen from './screens/PersonCredential'
import { defaultTheme as theme } from './theme'
import { useNotifications } from './hooks/notifications'
import { ReducerAction } from 'react'
import { BCDispatchAction } from './store'

const localization = merge({}, translationResources, {
en: { translation: en },
Expand All @@ -31,7 +34,6 @@ const configuration: ConfigurationContext = {
pages,
splash: Splash,
terms: Terms,
homeContentView: BCIDView,
credentialListHeaderRight: AddCredentialButton,
credentialListOptions: AddCredentialSlider,
credentialEmptyList: EmptyList,
Expand All @@ -40,6 +42,22 @@ const configuration: ConfigurationContext = {
record: Record,
indyLedgers: selectedLedgers,
settings: [],
customNotification: {
component: PersonCredentialScreen,
onCloseAction: (dispatch?: React.Dispatch<ReducerAction<any>>) => {
if (dispatch) {
dispatch({
type: BCDispatchAction.PERSON_CREDENTIAL_OFFER_DISMISSED,
payload: [{personCredentialOfferDismissed: true}]
})
}
},
pageTitle: "PersonCredential.PageTitle",
jleach marked this conversation as resolved.
Show resolved Hide resolved
title: "PersonCredentialNotification.Title",
description: "PersonCredentialNotification.Description",
buttonTitle: "PersonCredentialNotification.ButtonTitle"
},
useCustomNotifications: useNotifications
}

export default { theme, localization, configuration }
20 changes: 17 additions & 3 deletions app/src/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,27 @@ const translation = {
"Warning": "Ensure only you have access to your wallet.",
"UseToUnlock": "Use biometrics to unlock wallet?"
},
"Credentials":{
"AddCredential":"Add Credential",
"Credentials": {
"AddCredential": "Add Credential",
"EmptyList": "Your wallet is empty.",
"AddFirstCredential": "Add your first credential"
},
"Screens": {
"Onboarding": "BC Wallet",
"Onboarding": "BC Wallet"
},
"PersonCredentialNotification": {
"Title": "Get your Person credential",
"Description": "Add your Person credential to your wallet and use it to get access to services online.",
"ButtonTitle": "Start",
},
"PersonCredential": {
"Issuer": "Service BC",
"Name": "Person",
"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",
"Decline": "Get this later",
"PageTitle": "Person Credential"
},
"NetInfo": {
"NoInternetConnectionTitle": "No internet connection",
Expand Down
15 changes: 14 additions & 1 deletion app/src/localization/fr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,20 @@ const translation = {
"AddFirstCredential": "Add your first credential (FR)"
},
"Screens": {
"Onboarding": "BC Wallet (FR)",
"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)"
},
"PersonCredential": {
"Issuer": "Service BC (FR)",
"Name": "Person (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)",
"Decline": "Get this later (FR)",
"PageTitle": "Person Credential (FR)"
},
"NetInfo": {
"NoInternetConnectionTitle": "No internet connection (FR)",
Expand Down
19 changes: 16 additions & 3 deletions app/src/localization/pt-br/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,26 @@ const translation = {
"Warning": "Ensure only you have access to your wallet. (PT-BR)",
"UseToUnlock": "Use biometrics to unlock wallet? (PT-BR)",
},
"Credentials":{
"AddCredential":"Add Credential (PT-BR)",
"Credentials": {
"AddCredential": "Add Credential (PT-BR)",
"EmptyList": "Your wallet is empty. (PT-BR)",
"AddFirstCredential": "Add your first credential (PT-BR)"
},
"Screens": {
"Onboarding": "BC Wallet (PT-BR)",
"Onboarding": "BC Wallet (PT-BR)"
},
"PersonCredentialNotification": {
"Title": "Get your Person credential (PT-BR)",
"Description": "Add your Person credential to your wallet and use it to get access to services online. (PT-BR)"
},
"PersonCredential": {
"Issuer": "Service BC (PT-BR)",
"Name": "Person (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)",
"Decline": "Get this later (PT-BR)",
"PageTitle": "Person Credential (PT-BR)"
},
"NetInfo": {
"NoInternetConnectionTitle": "No internet connection (PT-BR)",
Expand Down
163 changes: 163 additions & 0 deletions app/src/screens/PersonCredential.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { useAgent } from "@aries-framework/react-hooks"
import { useNavigation } from "@react-navigation/core"
import { Button, ButtonType, Screens, useStore, useTheme } from "aries-bifold"
import React, { ReducerAction, useState, useCallback } from "react"
import { useTranslation } from "react-i18next"
import { StyleSheet, Text, View, Image, Dimensions, ImageBackground, TouchableOpacity, Linking } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import LoadingIcon from "../components/LoadingIcon"
import { startFlow } from '../helpers/BCIDHelper'
import { BCDispatchAction, BCState } from "../store"

const PersonCredentialScreen: React.FC = () => {

const [workflowInFlight, setWorkflowInFlight] = useState<boolean>(false)
const { agent } = useAgent()
const navigation = useNavigation()
const [store, dispatch] = useStore<BCState>()

const paddingHorizontal = 10
const transparent = 'rgba(0,0,0,0)'
const borderRadius = 15
const borderPadding = 8
const { width } = Dimensions.get('window')
const cardHeight = width / 2 // a card height is half of the screen width
const cardHeaderHeight = cardHeight / 4 // a card has a total of 4 rows, and the header occupy 1 row

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

const styles = StyleSheet.create({
pageContent: {
marginHorizontal: 20,
flexGrow: 1,
},
pageTextContainer: {
display: 'flex',
flexDirection: 'row',
},
container: {
backgroundColor: ColorPallet.brand.primaryBackground,
height: cardHeight,
paddingTop: 15,
marginBottom: 20,
},
outerHeaderContainer: {
flexDirection: 'column',
backgroundColor: transparent,
height: cardHeaderHeight + borderPadding,
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
},
innerHeaderContainer: {
flexDirection: 'row',
margin: borderPadding,
backgroundColor: transparent,
},
buttonContainer: {
flexGrow: 1,
justifyContent: 'flex-end',
marginBottom: 20,
},
button: {
marginBottom: 15,
},
flexGrow: {
flexGrow: 1,
},
})

const startGetBCIDCredentialWorkflow = useCallback(() => {
setWorkflowInFlight(true)
startFlow(agent, store, dispatch as React.Dispatch<ReducerAction<any>>, setWorkflowInFlight, t)
}, [])

const dismissPersonCredentialOffer = useCallback(() => {
dispatch({
type: BCDispatchAction.PERSON_CREDENTIAL_OFFER_DISMISSED,
payload: [{personCredentialOfferDismissed: true}]
})
navigation.navigate(Screens.Home as never)
}, [])

const getBCServicesCardApp = useCallback(() => {
return Linking.openURL('https://www2.gov.bc.ca/gov/content/governments/government-id/bcservicescardapp/download-app')
}, [])

return (
<SafeAreaView style={[styles.pageContent]}>
<View style={[styles.container]}>
<View style={[styles.flexGrow]}>
<ImageBackground source={require('../assets/branding/service-bc-id-card.png')} style={[styles.flexGrow]} imageStyle={{ borderRadius }} >
<View style={[styles.outerHeaderContainer]}>
<View style={[styles.innerHeaderContainer]}>
<Image
source={require('../assets/branding/service-bc-header-logo.png')}
style={{
flex: 1,
resizeMode: 'contain',
maxHeight: styles.outerHeaderContainer.height - borderPadding,
}}
/>
<Text
numberOfLines={1}
ellipsizeMode="tail"
style={[
TextTheme.label,
{
color: ColorPallet.grayscale.white,
paddingHorizontal: 0.5 * paddingHorizontal,
flex: 3,
textAlignVertical: 'center',
},
]}
maxFontSizeMultiplier={1}
>
{t('PersonCredential.Issuer')}
</Text>
<Text
numberOfLines={1}
ellipsizeMode="tail"
style={[
TextTheme.label,
{
color: ColorPallet.grayscale.white,
textAlign: 'right',
paddingHorizontal: 0.5 * paddingHorizontal,
flex: 4,
textAlignVertical: 'center',
},
]}
maxFontSizeMultiplier={1}
>
{t('PersonCredential.Name')}
</Text>
</View>
</View>
</ImageBackground>
</View>
</View>
<View>
<Text style={TextTheme.normal}>{t('PersonCredential.Description') + " "}
<TouchableOpacity onPress={getBCServicesCardApp}>
<Text style={{ ...TextTheme.normal, color: ColorPallet.brand.link }}>{t('PersonCredential.LinkDescription')}</Text>
</TouchableOpacity>
</Text>
</View>
<View style={styles.buttonContainer}>
<View style={styles.button}>
<Button title={t('PersonCredential.GetCredential')} accessibilityLabel={t('PersonCredential.GetCredential')} onPress={startGetBCIDCredentialWorkflow} disabled={workflowInFlight} buttonType={ButtonType.Primary}>
{workflowInFlight && (
<LoadingIcon color={ColorPallet.grayscale.white} size={35} active={workflowInFlight} />
)}
</Button>
</View>
<View style={styles.button}>
<Button title={t('PersonCredential.Decline')} accessibilityLabel={t('PersonCredential.Decline')} onPress={dismissPersonCredentialOffer} buttonType={ButtonType.Secondary}></Button>
</View>
</View>
</SafeAreaView>
)
}

export default PersonCredentialScreen
Loading