diff --git a/app/components/record/W3CCredentialRecord.tsx b/app/components/record/W3CCredentialRecord.tsx index 7d3b88e2..8f9a58e8 100644 --- a/app/components/record/W3CCredentialRecord.tsx +++ b/app/components/record/W3CCredentialRecord.tsx @@ -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' @@ -16,13 +17,27 @@ export interface RecordProps { fields: Field[] hideFieldValues?: boolean tables: W3CCredentialAttributeField[] + w3cCredential?: Pick & { + credential: Pick, 'credential'> & { + prettyVc?: string + } + } + renderCertificate?: () => void } -const W3CCredentialRecord: React.FC = ({ header, footer, fields, hideFieldValues = false, tables }) => { +const W3CCredentialRecord: React.FC = ({ + header, + footer, + fields, + hideFieldValues = false, + tables, + w3cCredential, + renderCertificate, +}) => { const { t } = useTranslation() const [shown, setShown] = useState([]) const [showAll, setShowAll] = useState(false) - const { ListItems, TextTheme } = useTheme() + const { ListItems, TextTheme, ColorPallet } = useTheme() const styles = StyleSheet.create({ linkContainer: { @@ -40,6 +55,14 @@ const W3CCredentialRecord: React.FC = ({ 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 => { @@ -102,19 +125,35 @@ const W3CCredentialRecord: React.FC = ({ header, footer, fields, hi header ? ( {header()} - {hideFieldValues ? ( - - resetShown()} - testID={testIdWithKey('HideAll')} - accessible={true} - accessibilityLabel={showAll ? t('Record.ShowAll') : t('Record.HideAll')}> - {showAll ? t('Record.ShowAll') : t('Record.HideAll')} - - - ) : null} + + {w3cCredential?.credential?.prettyVc ? ( + + + {t('Record.ViewCertificate')} + + + ) : null} + + {hideFieldValues ? ( + + resetShown()} + testID={testIdWithKey('HideAll')} + accessible={true} + accessibilityLabel={showAll ? t('Record.ShowAll') : t('Record.HideAll')}> + {showAll ? t('Record.ShowAll') : t('Record.HideAll')} + + + ) : null} + ) : null } diff --git a/app/localization/en/index.ts b/app/localization/en/index.ts index ca53e854..cab66e1b 100644 --- a/app/localization/en/index.ts +++ b/app/localization/en/index.ts @@ -514,6 +514,7 @@ const translation = { "Hidden": "Hidden", "InvalidDate": "Invalid Date: ", "Zoom": "Zoom", + "ViewCertificate": "View Certificate", }, "Screens": { "Splash": "Splash", @@ -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.", diff --git a/app/navigators/CredentialStack.tsx b/app/navigators/CredentialStack.tsx index 72c8b81f..097ec989 100644 --- a/app/navigators/CredentialStack.tsx +++ b/app/navigators/CredentialStack.tsx @@ -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' @@ -41,6 +42,11 @@ const CredentialStack: React.FC = () => { component={CredentialDetailsW3C} options={{ title: t('Screens.CredentialDetailsW3C') }} /> + ) diff --git a/app/screens/CredentialDetailsW3C.tsx b/app/screens/CredentialDetailsW3C.tsx index 66abc3da..6727d734 100644 --- a/app/screens/CredentialDetailsW3C.tsx +++ b/app/screens/CredentialDetailsW3C.tsx @@ -295,6 +295,12 @@ const CredentialDetailsW3C: React.FC = ({ navigation, ro ) } + const navigateToRenderCertificate = () => { + navigation.navigate(Screens.RenderCertificate, { + credential: w3cCredential as W3cCredentialRecord, + }) + } + return ( {w3cCredential && ( @@ -304,6 +310,8 @@ const CredentialDetailsW3C: React.FC = ({ navigation, ro hideFieldValues header={header} footer={footer} + w3cCredential={w3cCredential} + renderCertificate={navigateToRenderCertificate} /> )} = ({ navigation }) => { useEffect(() => { if (!agent) return - getDefaultHolderDidDocument(agent) + const setupDefaultDid = async () => { + await getDefaultHolderDidDocument(agent) + } + + setupDefaultDid() }, [agent]) const styles = StyleSheet.create({ diff --git a/app/screens/RenderCertificate.tsx b/app/screens/RenderCertificate.tsx new file mode 100644 index 00000000..8c164679 --- /dev/null +++ b/app/screens/RenderCertificate.tsx @@ -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 + +const defaultHtmlContent = ` +
+
+ ` + +const RenderCertificate: React.FC = ({ 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 ? ( + + + + ) : ( + + + + ), + }) + }, [navigation, htmlContent, loader]) + + useEffect(() => { + fetchHtmlContent() + }, []) + + return ( + + + + ) +} + +export default RenderCertificate diff --git a/app/types/navigators.ts b/app/types/navigators.ts index 2ac61a67..e291388a 100644 --- a/app/types/navigators.ts +++ b/app/types/navigators.ts @@ -51,6 +51,7 @@ export enum Screens { DataRetention = 'Data Retention', Explore = 'Explore', OrganizationDetails = 'Organization Details', + RenderCertificate = 'Render Certificate', } export enum Stacks { @@ -144,6 +145,7 @@ export type CredentialStackParams = { [Screens.Credentials]: undefined [Screens.CredentialDetails]: { credential: CredentialExchangeRecord } [Screens.CredentialDetailsW3C]: { credential: W3cCredentialRecord } + [Screens.RenderCertificate]: { credential: Pick } [Screens.Scan]: undefined } export type OrganizationStackParams = { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b4a321c7..a1eeb1f1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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): @@ -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`) @@ -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: @@ -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 diff --git a/package.json b/package.json index c331e14a..18575787 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/yarn.lock b/yarn.lock index 1ceb0f3a..98c0d146 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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== + react-native-iphone-x-helper@1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz"