Skip to content

Commit

Permalink
feat: add html rendering support (#203)
Browse files Browse the repository at this point in the history
Signed-off-by: Sai Ranjit Tummalapalli <[email protected]>
  • Loading branch information
sairanjit authored Jul 1, 2024
1 parent 8768578 commit 71eac99
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 17 deletions.
69 changes: 54 additions & 15 deletions app/components/record/W3CCredentialRecord.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { W3cCredentialRecord } from '@adeya/ssi'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
Expand All @@ -16,13 +17,27 @@ export interface RecordProps {
fields: Field[]
hideFieldValues?: boolean
tables: W3CCredentialAttributeField[]
w3cCredential?: Pick<W3cCredentialRecord, 'credential'> & {
credential: Pick<Pick<W3cCredentialRecord, 'credential'>, 'credential'> & {
prettyVc?: string
}
}
renderCertificate?: () => void
}

const W3CCredentialRecord: React.FC<RecordProps> = ({ header, footer, fields, hideFieldValues = false, tables }) => {
const W3CCredentialRecord: React.FC<RecordProps> = ({
header,
footer,
fields,
hideFieldValues = false,
tables,
w3cCredential,
renderCertificate,
}) => {
const { t } = useTranslation()
const [shown, setShown] = useState<boolean[]>([])
const [showAll, setShowAll] = useState<boolean>(false)
const { ListItems, TextTheme } = useTheme()
const { ListItems, TextTheme, ColorPallet } = useTheme()

const styles = StyleSheet.create({
linkContainer: {
Expand All @@ -40,6 +55,14 @@ const W3CCredentialRecord: React.FC<RecordProps> = ({ header, footer, fields, hi
padding: 16,
flexDirection: 'row',
},
rowContainer: {
flexDirection: 'row',
justifyContent: w3cCredential?.credential?.prettyVc ? 'space-between' : 'flex-end',
backgroundColor: ColorPallet.grayscale.white,
},
linkText: {
fontWeight: 'bold',
},
})

const resetShown = (): void => {
Expand Down Expand Up @@ -102,19 +125,35 @@ const W3CCredentialRecord: React.FC<RecordProps> = ({ header, footer, fields, hi
header ? (
<RecordHeader>
{header()}
{hideFieldValues ? (
<View style={styles.linkContainer}>
<TouchableOpacity
style={styles.link}
activeOpacity={1}
onPress={() => resetShown()}
testID={testIdWithKey('HideAll')}
accessible={true}
accessibilityLabel={showAll ? t('Record.ShowAll') : t('Record.HideAll')}>
<Text style={ListItems.recordLink}>{showAll ? t('Record.ShowAll') : t('Record.HideAll')}</Text>
</TouchableOpacity>
</View>
) : null}
<View style={styles.rowContainer}>
{w3cCredential?.credential?.prettyVc ? (
<View style={styles.linkContainer}>
<TouchableOpacity
style={styles.link}
activeOpacity={1}
onPress={renderCertificate}
testID={testIdWithKey('ViewCertificate')}
accessible={true}
accessibilityLabel={t('Record.ViewCertificate')}>
<Text style={[ListItems.recordLink, styles.linkText]}>{t('Record.ViewCertificate')}</Text>
</TouchableOpacity>
</View>
) : null}

{hideFieldValues ? (
<View style={styles.linkContainer}>
<TouchableOpacity
style={styles.link}
activeOpacity={1}
onPress={() => resetShown()}
testID={testIdWithKey('HideAll')}
accessible={true}
accessibilityLabel={showAll ? t('Record.ShowAll') : t('Record.HideAll')}>
<Text style={ListItems.recordLink}>{showAll ? t('Record.ShowAll') : t('Record.HideAll')}</Text>
</TouchableOpacity>
</View>
) : null}
</View>
</RecordHeader>
) : null
}
Expand Down
4 changes: 3 additions & 1 deletion app/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ const translation = {
"Hidden": "Hidden",
"InvalidDate": "Invalid Date: ",
"Zoom": "Zoom",
"ViewCertificate": "View Certificate",
},
"Screens": {
"Splash": "Splash",
Expand Down Expand Up @@ -569,7 +570,8 @@ const translation = {
"ProofChangeCredentialW3C": "Choose a W3C credential",
"DataRetention": "Data retention",
"Organization": "Explore",
"OrganizationConnection": "Connection"
"OrganizationConnection": "Connection",
"RenderCertificate": "Certificate"
},
"Loading": {
"TakingTooLong": "This is taking longer than usual. You can return to home or continue waiting.",
Expand Down
6 changes: 6 additions & 0 deletions app/navigators/CredentialStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useTheme } from '../contexts/theme'
import CredentialDetails from '../screens/CredentialDetails'
import CredentialDetailsW3C from '../screens/CredentialDetailsW3C'
import ListCredentials from '../screens/ListCredentials'
import RenderCertificate from '../screens/RenderCertificate'
import Scan from '../screens/Scan'
import { CredentialStackParams, Screens } from '../types/navigators'

Expand Down Expand Up @@ -41,6 +42,11 @@ const CredentialStack: React.FC = () => {
component={CredentialDetailsW3C}
options={{ title: t('Screens.CredentialDetailsW3C') }}
/>
<Stack.Screen
name={Screens.RenderCertificate}
component={RenderCertificate}
options={{ title: t('Screens.RenderCertificate') }}
/>
<Stack.Screen name={Screens.Scan} component={Scan} />
</Stack.Navigator>
)
Expand Down
8 changes: 8 additions & 0 deletions app/screens/CredentialDetailsW3C.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ const CredentialDetailsW3C: React.FC<CredentialDetailsProps> = ({ navigation, ro
)
}

const navigateToRenderCertificate = () => {
navigation.navigate(Screens.RenderCertificate, {
credential: w3cCredential as W3cCredentialRecord,
})
}

return (
<SafeAreaView style={{ flexGrow: 1 }} edges={['left', 'right']}>
{w3cCredential && (
Expand All @@ -304,6 +310,8 @@ const CredentialDetailsW3C: React.FC<CredentialDetailsProps> = ({ navigation, ro
hideFieldValues
header={header}
footer={footer}
w3cCredential={w3cCredential}
renderCertificate={navigateToRenderCertificate}
/>
)}
<CommonRemoveModal
Expand Down
6 changes: 5 additions & 1 deletion app/screens/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ const Home: React.FC<HomeProps> = ({ navigation }) => {
useEffect(() => {
if (!agent) return

getDefaultHolderDidDocument(agent)
const setupDefaultDid = async () => {
await getDefaultHolderDidDocument(agent)
}

setupDefaultDid()
}, [agent])

const styles = StyleSheet.create({
Expand Down
112 changes: 112 additions & 0 deletions app/screens/RenderCertificate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { StackScreenProps } from '@react-navigation/stack'

import React, { useEffect, useState } from 'react'
import { ActivityIndicator, Platform, StyleSheet, TouchableOpacity, View } from 'react-native'
import RNHTMLtoPDF from 'react-native-html-to-pdf'
import { SafeAreaView } from 'react-native-safe-area-context'
import Share, { ShareOptions } from 'react-native-share'
import Icon from 'react-native-vector-icons/MaterialIcons'
import WebView from 'react-native-webview'

import { ColorPallet } from '../theme'
import { CredentialStackParams, Screens } from '../types/navigators'

type RenderCertificateProps = StackScreenProps<CredentialStackParams, Screens.RenderCertificate>

const defaultHtmlContent = `
<div style="width:800px; height:600px; padding:20px; text-align:center; border: 10px solid #787878">
</div>
`

const RenderCertificate: React.FC<RenderCertificateProps> = ({ navigation, route }) => {
if (!route?.params) {
throw new Error('RenderCertificate route prams were not set properly')
}

const { credential } = route?.params
const [htmlContent, setHtmlContent] = useState(defaultHtmlContent)
const [loader, setLoader] = useState(false)

const styles = StyleSheet.create({
container: {
flex: 1,
},
})

const fetchHtmlContent = async () => {
try {
const certificateAttributes = credential.credential.credentialSubject.claims

let content = credential.credential.prettyVc

Object.keys(certificateAttributes).forEach(key => {
// Statically picking the value of placeholder
const placeholder = `{{credential['${key}']}}`
// Escaping the placeholder to avoid regex issues
const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
// Replacing the placeholder with the actual value
content = content.replace(new RegExp(escapedPlaceholder, 'g'), certificateAttributes[key])
})

setHtmlContent(content)
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error mapping HTML content:', error)
}
}

const downloadHtmlToPdf = async () => {
try {
setLoader(true)
const options: RNHTMLtoPDF.Options = {
html: htmlContent,
fileName: credential.credential.type[1],
directory: 'Documents',
}

const file = await RNHTMLtoPDF.convert(options)

let filePath = file.filePath as string

if (Platform.OS === 'android') {
filePath = 'file://' + filePath
}

setLoader(false)

const shareOptions: ShareOptions = { url: filePath }
await Share.open(shareOptions)
} catch (e) {
setLoader(false)
// eslint-disable-next-line no-console
console.log('error downloading html to pdf', e)
}
}

useEffect(() => {
navigation.setOptions({
headerRight: () =>
loader ? (
<View style={{ marginRight: 20 }}>
<ActivityIndicator size="small" color={ColorPallet.grayscale.white} />
</View>
) : (
<TouchableOpacity onPress={downloadHtmlToPdf} style={{ marginRight: 20 }}>
<Icon style={{ color: ColorPallet.grayscale.white }} size={25} name="download" />
</TouchableOpacity>
),
})
}, [navigation, htmlContent, loader])

useEffect(() => {
fetchHtmlContent()
}, [])

return (
<SafeAreaView style={styles.container} edges={['left', 'right']}>
<WebView source={{ html: htmlContent }} originWhitelist={['*']} />
</SafeAreaView>
)
}

export default RenderCertificate
2 changes: 2 additions & 0 deletions app/types/navigators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export enum Screens {
DataRetention = 'Data Retention',
Explore = 'Explore',
OrganizationDetails = 'Organization Details',
RenderCertificate = 'Render Certificate',
}

export enum Stacks {
Expand Down Expand Up @@ -144,6 +145,7 @@ export type CredentialStackParams = {
[Screens.Credentials]: undefined
[Screens.CredentialDetails]: { credential: CredentialExchangeRecord }
[Screens.CredentialDetailsW3C]: { credential: W3cCredentialRecord }
[Screens.RenderCertificate]: { credential: Pick<W3cCredentialRecord, 'credential'> }
[Screens.Scan]: undefined
}
export type OrganizationStackParams = {
Expand Down
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@ PODS:
- React-Core
- react-native-get-random-values (1.9.0):
- React-Core
- react-native-html-to-pdf (0.12.0):
- React-Core
- react-native-netinfo (9.4.1):
- React-Core
- react-native-randombytes (3.6.1):
Expand Down Expand Up @@ -709,6 +711,7 @@ DEPENDENCIES:
- react-native-config (from `../node_modules/react-native-config`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-html-to-pdf (from `../node_modules/react-native-html-to-pdf`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-randombytes (from `../node_modules/react-native-randombytes`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
Expand Down Expand Up @@ -833,6 +836,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-document-picker"
react-native-get-random-values:
:path: "../node_modules/react-native-get-random-values"
react-native-html-to-pdf:
:path: "../node_modules/react-native-html-to-pdf"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-randombytes:
Expand Down Expand Up @@ -967,6 +972,7 @@ SPEC CHECKSUMS:
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
react-native-document-picker: 2b8f18667caee73a96708a82b284a4f40b30a156
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-html-to-pdf: 4c5c6e26819fe202971061594058877aa9b25265
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846
react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"react-native-gesture-handler": "^2.12.1",
"react-native-get-random-values": "^1.9.0",
"react-native-gifted-chat": "^2.4.0",
"react-native-html-to-pdf": "^0.12.0",
"react-native-keychain": "^8.1.2",
"react-native-localize": "^3.0.2",
"react-native-permissions": "^3.9.0",
Expand Down Expand Up @@ -107,6 +108,7 @@
"@types/lodash.startcase": "^4.4.7",
"@types/react": "^18.0.24",
"@types/react-native": "^0.72.2",
"@types/react-native-html-to-pdf": "^0.8.3",
"@types/react-native-vector-icons": "^6.4.13",
"@types/react-test-renderer": "^18.0.0",
"@types/uuid": "^9.0.2",
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3574,6 +3574,11 @@
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==

"@types/react-native-html-to-pdf@^0.8.3":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@types/react-native-html-to-pdf/-/react-native-html-to-pdf-0.8.3.tgz#0e41b666c711b114957e42f7a7c665fb2fea8045"
integrity sha512-KjyR1F9KhcmX8p9y/8WYMiaK4DV/cYBUO1XHEzD9dDVZBkY9ujbdMLYO7ZvmAffNT2Q484Qvg+t6szgVcnN22g==

"@types/react-native-vector-icons@^6.4.13":
version "6.4.13"
resolved "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.13.tgz"
Expand Down Expand Up @@ -9866,6 +9871,11 @@ react-native-gifted-chat@^2.4.0:
use-memo-one "1.1.3"
uuid "3.4.0"

react-native-html-to-pdf@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/react-native-html-to-pdf/-/react-native-html-to-pdf-0.12.0.tgz#2b467296f85c9c9783a7288b19722a7028dcbcb8"
integrity sha512-Yb5WO9SfF86s5Yv9PqXQ7fZDr9zZOJ+6jtweT9zFLraPNHWX7pSxe2dSkeg3cGiNrib65ZXGN6ksHymfYLFSSg==

[email protected]:
version "1.3.1"
resolved "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz"
Expand Down

0 comments on commit 71eac99

Please sign in to comment.