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

650 add qr code to pdfs for verification #658

Closed
wants to merge 5 commits into from
Closed
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
16 changes: 11 additions & 5 deletions app/lib/svgToPdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import Handlebars from 'handlebars';
import { Credential } from '../types/credential';
import { PDF } from '../types/pdf';

export async function convertSVGtoPDF(credential: Credential): Promise<PDF | null> {
if (!credential['renderMethod']) {
return null;
export async function convertSVGtoPDF(
credential: Credential, publicLink: string | null, qrCodeBase64: string | null): Promise<PDF | null> {
if (!credential['renderMethod'] || !publicLink || !qrCodeBase64) {
return null; // Ensure we have the necessary data
}

const templateURL = credential.renderMethod?.[0].id; // might want to sort if there are more than one renderMethod

let source = '';
const data = { credential: credential, qr_code: qrCodeBase64 };

// Fetch the template content
if (templateURL) {
try {
const response = await fetch(templateURL);
Expand All @@ -21,9 +27,8 @@ export async function convertSVGtoPDF(credential: Credential): Promise<PDF | nul
}
}

source = source.replace('{{ qr_code }}', `'${'data:image/png;base64, ' + qrCodeBase64}'`);
const template = Handlebars.compile(source);

const data = { 'credential': credential };
const svg = template(data);

const options = {
Expand All @@ -32,5 +37,6 @@ export async function convertSVGtoPDF(credential: Credential): Promise<PDF | nul
base64: false,
};
const pdf = await RNHTMLtoPDF.convert(options);

return pdf;
}
95 changes: 84 additions & 11 deletions app/screens/PublicLinkScreen/PublicLinkScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,34 @@ export default function PublicLinkScreen ({ navigation, route }: PublicLinkScree
const [justCreated, setJustCreated] = useState(false);
const [pdf, setPdf] = useState<PDF | null>(null);
const [showExportToPdfButton, setShowExportToPdfButton] = useState(false);

const [qrCodeBase64, setQrCodeBase64] = useState<string | null>(null); // State to store base64 data URL of QR code

const qrCodeRef = useRef<any>(null); // Reference to QRCode component to access toDataURL
const isVerified = useVerifyCredential(rawCredentialRecord)?.result.verified;
const inputRef = useRef<RNTextInput | null>(null);
const disableOutsidePressHandler = inputRef.current?.isFocused() ?? false;
const selectionColor = Platform.select({ ios: theme.color.brightAccent, android: theme.color.highlightAndroid });


useEffect(() => {
if (qrCodeRef.current) {
// Once the ref is available, extract the base64 data URL for the QR code
qrCodeRef.current.toDataURL((dataUrl: string) => {
setQrCodeBase64(dataUrl);
});
}
}, [qrCodeRef, publicLink]); // Re-run when publicLink changes

useEffect(() => {
const fetchData = async () => {
if (!qrCodeBase64 || publicLink) {
console.log('QR code or public link is missing.');
//return;
}
let rawPdf;
try {
rawPdf = await convertSVGtoPDF(credential);
// Pass qrCodeBase64 directly to the function
rawPdf = await convertSVGtoPDF(credential, publicLink, qrCodeBase64);
setPdf(rawPdf);
} catch (e) {
console.log('ERROR GENERATING PDF:');
Expand All @@ -58,7 +75,7 @@ export default function PublicLinkScreen ({ navigation, route }: PublicLinkScree
};

fetchData();
}, []);
}, [qrCodeBase64, publicLink]); // Run when qrCodeBase64 or publicLink changes

const screenTitle = {
[PublicLinkScreenMode.Default]: 'Public Link',
Expand All @@ -74,12 +91,6 @@ export default function PublicLinkScreen ({ navigation, route }: PublicLinkScree
});
}

const handleShareAsPdf = async() => {
if (pdf) {
Share.open({url: `file://${pdf.filePath}`});
}
};

function displayErrorModal(err: Error) {
function goToErrorSource() {
clearGlobalModal();
Expand Down Expand Up @@ -109,6 +120,63 @@ export default function PublicLinkScreen ({ navigation, route }: PublicLinkScree
});
}

// Function to handle PDF generation
async function handleGeneratePDF() {
if (!qrCodeBase64) {
console.error('QR code base64 not available.');
return;
}
try {
const generatedPdf = await convertSVGtoPDF(credential, publicLink, qrCodeBase64); // Pass the QR code data URL to PDF generator
setPdf(generatedPdf); // Store the generated PDF
} catch (error) {
console.error('Error generating PDF:', error);
}
if (pdf) {
Share.open({url: `file://${pdf.filePath}`});
}
}

// Button press handler for exporting PDF
async function handleShareAsPdfButton() {
if (!isVerified) {
return displayNotVerifiedModal(); // Show modal if the credential isn't verified
}
// Prompt for confirmation before proceeding
const confirmed = await displayGlobalModal({
title: 'Are you sure?',
confirmText: 'Export to PDF',
cancelOnBackgroundPress: true,
body: (
<>
<Text style={mixins.modalBodyText}>
{publicLink !== null
? 'This will export your credential to PDF.'
: 'You must create a public link before exporting to PDF. The link will automatically expire 1 year after creation.'
}
</Text>
<Button
buttonStyle={mixins.buttonClear}
titleStyle={[mixins.buttonClearTitle, mixins.modalLinkText]}
containerStyle={mixins.buttonClearContainer}
title="What does this mean?"
onPress={() => Linking.openURL(`${LinkConfig.appWebsite.faq}#public-link`)}
/>
</>
)
});

if (!confirmed) return;

if (!publicLink) {
// If the public link isn't created, create it before generating the PDF
await createPublicLink();
}

// Now that we have the public link, generate the PDF
await handleGeneratePDF();
}

function displayNotVerifiedModal() {
return displayGlobalModal({
title: 'Unable to Share Credential',
Expand Down Expand Up @@ -403,7 +471,7 @@ export default function PublicLinkScreen ({ navigation, route }: PublicLinkScree
containerStyle={mixins.buttonIconContainer}
titleStyle={mixins.buttonIconTitle}
iconRight
onPress={handleShareAsPdf}
onPress={handleShareAsPdfButton}
icon={
<Ionicons
name="document-text"
Expand Down Expand Up @@ -449,14 +517,19 @@ export default function PublicLinkScreen ({ navigation, route }: PublicLinkScree
</Text>
</View>
)}

{publicLink !== null && (
<View style={styles.bottomSection}>
<Text style={mixins.paragraphText}>
You may also share the public link by having another person scan this QR code.
</Text>
<View style={styles.qrCodeContainer}>
<View style={styles.qrCode}>
<QRCode value={publicLink} size={200}/>
<QRCode
value={publicLink} // The value to encode in the QR code
size={200} // The size of the QR code
getRef={(ref) => (qrCodeRef.current = ref)} // Set the ref to access toDataURL
/>
</View>
</View>
</View>
Expand Down
130 changes: 130 additions & 0 deletions test/svgToPdf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { convertSVGtoPDF } from '../app/lib/svgToPdf'
import RNHTMLtoPDF from 'react-native-html-to-pdf';
import Handlebars from 'handlebars';
import { mockCredential } from '../app/mock/credential';

// Mocks
jest.mock('react-native-html-to-pdf', () => ({
convert: jest.fn(),
}));

jest.mock('handlebars', () => ({
compile: jest.fn(),
}));

global.fetch = jest.fn();

describe('convertSVGtoPDF', () => {
const publicLink = 'https://example.com/publicLink';
const qrCodeBase64 = 'testBase64QRCode';

beforeEach(() => {
jest.clearAllMocks();
});

it('should return null if required data is missing', async () => {
// Missing publicLink
expect(await convertSVGtoPDF(mockCredential, null, qrCodeBase64)).toBeNull();

// Missing qrCodeBase64
expect(await convertSVGtoPDF(mockCredential, publicLink, null)).toBeNull();
});

it('should handle fetch errors', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network Error'));

const result = await convertSVGtoPDF(mockCredential, publicLink, qrCodeBase64);

// If fetch fails, the result should be null
expect(result).toBeNull();
});

it('should handle invalid HTTP response when fetching template', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
status: 500,
});

const result = await convertSVGtoPDF(mockCredential, publicLink, qrCodeBase64);

// If response is not OK, the result should be null
expect(result).toBeNull();
});

it('should fetch the template and generate a PDF', async () => {
const modifiedCredential = {
...mockCredential,
renderMethod: [
{
id: 'https://raw.githubusercontent.com/digitalcredentials/test-files/main/html-templates/rendermethod-qrcode-test.html',
type: 'HTML',
},
],
};

const templateHtml = '<html><body>{{ qr_code }}</body></html>';
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(templateHtml),
});

// Mock Handlebars.compile to return a function that generates the final HTML with embedded QR code
const mockCompiledTemplate = jest.fn().mockReturnValue((data: { qr_code: any }) => {
return `<html><body>data:image/png;base64, ${data.qr_code}</body></html>`;
});
(Handlebars.compile as jest.Mock).mockImplementation(mockCompiledTemplate);

const mockPdfResult = { filePath: 'path/to/pdf' };
(RNHTMLtoPDF.convert as jest.Mock).mockResolvedValueOnce(mockPdfResult);

const result = await convertSVGtoPDF(modifiedCredential, publicLink, qrCodeBase64);

// Ensure fetch was called with the correct URL
expect(global.fetch).toHaveBeenCalledWith(modifiedCredential.renderMethod[0].id);

// Ensure RNHTMLtoPDF.convert was called with the correct HTML and options
expect(RNHTMLtoPDF.convert).toHaveBeenCalledWith({
html: `<html><body>data:image/png;base64, ${qrCodeBase64}</body></html>`,
fileName: 'undefined Credential',
base64: false,
});

// Ensure the result is the expected PDF output
expect(result).toEqual(mockPdfResult);
});

it('should embed the qrCodeBase64 correctly in the template', async () => {
const modifiedCredential = {
...mockCredential,
renderMethod: [
{
id: 'https://raw.githubusercontent.com/digitalcredentials/test-files/main/html-templates/rendermethod-qrcode-test.html',
type: 'HTML',
},
],
};

const templateHtml = '<html><body>{{ qr_code }}</body></html>';
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(templateHtml),
});

const mockCompiledTemplate = jest.fn().mockReturnValue((data: { qr_code: any }) => {
return `<html><body>data:image/png;base64, ${data.qr_code}</body></html>`;
});
(Handlebars.compile as jest.Mock).mockImplementation(mockCompiledTemplate);

const mockPdfResult = { filePath: 'path/to/pdf' };
(RNHTMLtoPDF.convert as jest.Mock).mockResolvedValueOnce(mockPdfResult);

await convertSVGtoPDF(modifiedCredential, publicLink, qrCodeBase64);

// Check that the SVG with the correct QR code embedded was passed to RNHTMLtoPDF.convert
expect(RNHTMLtoPDF.convert).toHaveBeenCalledWith({
html: `<html><body>data:image/png;base64, ${qrCodeBase64}</body></html>`,
fileName: 'undefined Credential',
base64: false,
});
});
});
Loading