Add An In-App Certificate Viewer
The certificate library migration is done: TT - Save Certificate Library saves into the locked TT Certificates_LTD library, and TT - Email Certificate Library looks the file up with the same authorisation check and returns it as one JSON payload field. This guide covers the one remaining piece: getting that certificate to actually open on screen when someone clicks the button, instead of trying to email it or force a download.
TT - Save Certificate Library, btnSaveCert, and TT - Email Certificate Library itself are all correct and stay exactly as they are. This guide only touches btnOpenCertificate and adds four new controls to scrCourseDetail.
1. What This Adds And Why
The point of this app is one click, done — find the person, open the record, click the certificate, it opens. Two things were tried and both failed for the same underlying reason:
| Tried | Result | Why |
|---|---|---|
Launch("data:...;base64,...") | Nothing happened, no error. | Browsers (and Power Apps Studio's own Preview player, which runs in an iframe) block top-level navigation to a data: URI as an anti-phishing measure. |
Download("data:...;base64,...") | "The URL passed to the function is not valid." | Power Apps' own Download() function expects a real https:// address, the same as Launch() — neither function is actually built to accept base64 data URIs, despite older examples online suggesting otherwise. |
Both routes try to make the browser navigate to or fetch the file. The fix is to stop trying to navigate anywhere, and instead hand the base64 data straight to a control that renders it inline, inside the app itself. Two native Power Apps controls do exactly this and are not subject to the navigation block, because setting a property is not the same as navigating:
- PDF viewer control — its
Documentproperty natively accepts adata:application/pdf;base64,...string. - Image control — its
Imageproperty natively accepts adata:image/...;base64,...string.
Since your certificate picker allows both PDFs and images, this guide adds both controls in a small popup over scrCourseDetail, and shows whichever one matches the file that came back.
2. Add The Viewer Controls To scrCourseDetail
Add these four controls as the last controls on the screen (after everything else in the tree), so they render on top of the rest of the screen when visible. None of them need inserting inside any existing group — add them directly on the screen.
2.1 Dimmed Background
Insert → search Button → classic Button (not ModernButton). Rename it grpCertViewerBG.
| Property | Value |
|---|---|
Text | ="" |
Fill | =RGBA(0,0,0,0.75) |
BorderThickness | =0 |
X | =0 |
Y | =0 |
Width | =Parent.Width |
Height | =Parent.Height |
DisplayMode | =DisplayMode.View |
Visible | =varCertViewerOpen |
This is decorative only (DisplayMode.View makes it non-interactive) — it just dims everything behind the viewer so it's obvious you're in a popup.
2.2 PDF Viewer Control
Insert → search PDF viewer → add it. Rename it pdfCertViewer.
| Property | Value |
|---|---|
Document | =varCertDataUri |
X | =Parent.Width * 0.1 |
Y | =Parent.Height * 0.1 |
Width | =Parent.Width * 0.8 |
Height | =Parent.Height * 0.75 |
Visible | =varCertViewerOpen && varCertIsPdf |
2.3 Image Control
Insert → search Image → add it. Rename it imgCertViewer.
| Property | Value |
|---|---|
Image | =varCertDataUri |
ImagePosition | =ImagePosition.Fit |
X | =Parent.Width * 0.1 |
Y | =Parent.Height * 0.1 |
Width | =Parent.Width * 0.8 |
Height | =Parent.Height * 0.75 |
Visible | =varCertViewerOpen && !varCertIsPdf |
2.4 Close Button
Insert → search ModernButton → add it, same control type as btnSaveCert. Rename it btnCloseCertViewer.
| Property | Value |
|---|---|
Text | ="Close" |
BasePaletteColor | =RGBA(108,117,125,1) |
Color | =RGBA(255,255,255,1) |
X | =Parent.Width * 0.1 |
Y | =(Parent.Height * 0.1) + (Parent.Height * 0.75) + 12 |
Width | =140 |
Height | =44 |
Visible | =varCertViewerOpen |
OnSelect | =Set(varCertViewerOpen, false); Set(varCertDataUri, "") |
3. Update btnOpenCertificate
The button itself keeps its existing Text ("Open certificate") and Visible properties from the earlier cutover section — only OnSelect changes. Select btnOpenCertificate, open OnSelect, delete everything currently in there, and paste this complete formula in full:
=Set(varCertError, "");
Set(varCertStatusText, "Fetching certificate...");
Set(varCertBusy, true);
Set(
varCertRaw,
IfError(
'TT - Email Certificate Library'.Run(varRecord.ID, varMyEmail, varIsAdmin),
Blank()
)
);
Set(varCertBusy, false);
If(
IsBlank(varCertRaw),
Set(varCertError, "Flow could not be reached. Check your connection and try again."); Notify("Could not open certificate — see the error below.", NotificationType.Error),
With(
{parsed: ParseJSON(varCertRaw.payload)},
If(
Boolean(parsed.success),
Set(varCertStatusText, "");
Set(varCertFileNameLower, Lower(Text(parsed.fileName)));
Set(varCertIsPdf, EndsWith(varCertFileNameLower, ".pdf"));
Set(
varCertMimeType,
If(
varCertIsPdf, "application/pdf",
EndsWith(varCertFileNameLower, ".png"), "image/png",
"image/jpeg"
)
);
Set(varCertDataUri, "data:" & varCertMimeType & ";base64," & Trim(Text(parsed.fileContentBase64)));
Set(varCertViewerOpen, true),
Set(varCertError, Text(parsed.message)); Notify("Could not open certificate — see the error below.", NotificationType.Error)
)
)
)Note the Boolean(...) and Text(...) wrappers around every field read off parsed — ParseJSON returns an untyped value in Power Fx, and Power Apps will throw a type-mismatch error if you use a parsed field directly without coercing it first. This is not optional.
varCertFileNameLower, varCertIsPdf, varCertMimeType, varCertDataUri, varCertViewerOpen are all created the first time this formula runs — Power Apps creates variables on first use, you do not need to initialize them separately anywhere.
4. Test Script
- Open a record that already has a certificate saved in
TT Certificates_LTD. - Click Open certificate.
- Confirm the screen dims and the certificate renders inline — PDF viewer for a
.pdf, Image control for a.jpg/.jpeg/.png. - Click Close. Confirm the popup disappears and the rest of the screen is usable again.
- Open the same record again and click Open certificate a second time. Confirm it still works — this catches anything that only cleared on close but didn't reset properly for a second open.
- Test as a user who is not the record's owner and not an admin. Confirm they get the "not authorized" message, not the file.
5. Checklist
TT - Save Certificate Library,btnSaveCert, andTT - Email Certificate Libraryall untouched and still working.grpCertViewerBG,pdfCertViewer,imgCertViewer,btnCloseCertVieweradded toscrCourseDetail, last in the control tree.btnOpenCertificate.OnSelectreplaced with the full formula above — noLaunch()orDownload()anywhere in it.- PDF certificates render in the PDF viewer; image certificates render in the Image control; the correct one shows automatically based on file extension.
- Close button clears the viewer and it can be reopened cleanly.
- Tested as both an authorised person and an unauthorised person.
grpCertRetentionNoticepasted ontoscrCourseDetailand visible above the Save/Cancel buttons (section 6).TT - Save Certificate Librarydeletes any existing file for the record before creating the new one, confirmed by re-uploading a test record and checking the library has exactly one file (section 7).ctrlNewCertificatemade transparent andbtnCertDropZoneBGadded behind it, so the panel shows a styled drag-and-drop zone instead of native browser chrome (section 9). Drag-and-drop and click-to-browse both tested working, and the save formula confirmed untouched.
6. Retention Notice Badge
There is no download from inside the app — that's confirmed, not a gap we missed (see section 1). Since users can't retrieve their own uploaded file back out through the app afterwards, this adds a small rounded notice badge on scrCourseDetail telling them to keep their own copy before they submit, worded as a deliberate design choice rather than a limitation.
<img> in the Image control, so a normal browser right-click → "Save image as" should work on it — that's a native browser feature, nothing to do with the Launch/Download restriction. For PDFs, the PDF viewer control's own built-in toolbar sometimes includes a download/print icon, depending on version — worth a look next time one's open.
Paste This Directly Into Power Apps Studio
Select the screen (not any control inside it) in the tree view, then paste this. It adds one new top-level control, positioned above the Save/Cancel buttons regardless of the certificate panel's current height, since it's anchored off btnSaveCert.Y rather than a fixed Y position.
- grpCertRetentionNotice:
Control: GroupContainer@1.5.0
Variant: ManualLayout
Properties:
BorderColor: =RGBA(168,101,0,0.35)
BorderThickness: =1
DropShadow: =DropShadow.None
Fill: =RGBA(255,247,232,1)
Height: =60
RadiusBottomLeft: =18
RadiusBottomRight: =18
RadiusTopLeft: =18
RadiusTopRight: =18
Width: =796
X: =315
Y: =btnSaveCert.Y - 72
Children:
- icoCertRetentionNotice:
Control: Classic/Icon@2.5.0
Properties:
Color: =RGBA(168,101,0,1)
Height: =22
Icon: =Icon.Information
Width: =22
X: =14
Y: =19
- lblCertRetentionNotice:
Control: Label@2.5.1
Properties:
Align: =Align.Left
Color: =RGBA(140,84,0,1)
Font: =Font.'Open Sans'
FontWeight: =FontWeight.Semibold
Height: =Parent.Height
PaddingLeft: =8
PaddingTop: =8
Size: =9
Text: ="Uploading a new certificate permanently replaces the old one - it can't be recovered. Keep your own copy, since certificates can only be viewed here, not downloaded."
VerticalAlign: =VerticalAlign.Middle
Width: =734
Wrap: =true
X: =44
Y: =0Width 796 matches the combined span of the date and certificate panels (X 315 to 1111), so it sits flush under both. If your panels are positioned differently now, adjust Width/X to match after pasting — everything else (colours, icon, wording) will paste in correctly regardless. Height went from 44 to 60 to fit the longer wording on three lines comfortably.
7. Replace The Old Certificate On Re-Upload
Confirmed gap, not a guess: TT - Save Certificate Library currently only ever runs Create file when a new certificate is attached. There is no step that looks for an existing file for that TrainingRecordID and removes it first. Today, a re-upload adds a second file alongside the first — it does not replace it. The IsCurrent column in the library exists but nothing in the flow sets or checks it to filter old files out.
TT - Email Certificate Library, used by the viewer) filters by TrainingRecordID eq X with Top Count: 1 and no sort order. Once two files exist for the same record, SharePoint decides which one comes back — not necessarily the newest. A re-upload could silently leave the viewer showing someone's old certificate, with no error and no way to tell from the app. Fix this before it comes up in a real audit.
Add A Delete Step Before Create File
Open TT - Save Certificate Library. Find the existing SharePoint – Create file action (inside the HasNewFile? If yes branch). Insert two new actions directly above it, still inside the same branch:
- Add SharePoint – Get files (properties only).
Field Value Site Address Same Training Tracker SharePoint site. Library Name TT Certificates_LTD/TT Certificates (LTD).Filter Query TrainingRecordID eqfollowed byRecordIDfrom When Power Apps calls a flow (V2).No Top Count here — leave it blank, so this catches every existing file for the record, including any duplicates that may have already piled up before this fix.
- Add Control – Apply to each. For the loop input, choose the value output from the Get files (properties only) action you just added.
Inside that loop, add SharePoint – Delete file:
Field Value Site Address Same Training Tracker SharePoint site. File Identifier Identifierfrom the current item in this loop's Get files (properties only). Click the folder icon and switch to the dynamic content/expression view rather than browsing, sinceIdentifieris not something you can navigate to by hand.This action only has two fields, Site Address and File Identifier — there is no separate Library Name field, unlike most of the other SharePoint actions in this guide. The identifier alone is enough to locate the file.
The existing Create file and Update file properties actions stay exactly as they are, running straight after this new loop finishes. Save the flow.
Test
- Pick a test record that already has a certificate saved.
- Upload a different file and click Save record.
- Open
TT Certificates_LTDin SharePoint directly and confirm there is exactly one file for thatTrainingRecordID— the new one, not two. - Open the record in the app and click Open certificate. Confirm it shows the new file, not the old one.
9. Reskin The Certificate Upload As A Drag-And-Drop Zone
icoUploadCert.OnSelect = Select(ctrlNewCertificate) to wire up the paperclip icon. Tested live, that does nothing — the classic Attachments control has no documented OnSelect property (only OnAddFile/OnRemoveFile/OnUndoRemoveFile), and Select() only works on controls that expose one. Remove that formula if you added it — it's a harmless no-op, but it isn't doing anything.
The real fix doesn't try to trigger the control programmatically at all. The classic Attachments control (ctrlNewCertificate) already supports native drag-and-drop and click-to-browse — the only problem is how it looks. So instead of hiding or shrinking it, this makes it fully transparent and stacks a nicer-looking decorative box directly underneath it, in the exact same position and size. You see the pretty box; every click and drag actually lands on the real, still fully-functional control sitting invisibly on top of it. This is a known, real pattern — every property used below is a documented property of the Attachments control, nothing invented.
Nothing about the save formula, btnSaveCert, or how the file gets read off ctrlNewCertificate.Attachments changes. This section is entirely cosmetic.
9.1 Add The Decorative Drop-Zone Background
Insert → search Button → classic Button (not ModernButton). Rename it btnCertDropZoneBG. Its size and position are set relative to ctrlNewCertificate's current values, so it always matches exactly, however that control is currently sized — you don't need to know or copy any pixel numbers by hand.
| Property | Value |
|---|---|
Text | ="Click to select a file, or drag and drop" |
Fill | =RGBA(232,244,239,1) (adjust to taste — this is a light tint of this app's green) |
BorderColor | =RGBA(0,78,66,0.4) |
BorderStyle | =BorderStyle.Dashed |
BorderThickness | =2 |
Color | =RGBA(0,78,66,1) |
X | =ctrlNewCertificate.X |
Y | =ctrlNewCertificate.Y |
Width | =ctrlNewCertificate.Width |
Height | =ctrlNewCertificate.Height |
DisplayMode | =DisplayMode.View |
DisplayMode.View makes this purely decorative and non-interactive — it can never intercept a click or a dropped file, so there's no risk of it stealing input from the real control above it.
ctrlNewCertificate, not in front of it, or it'll visually cover the real control and block drags/clicks from reaching it. Studio adds newly inserted controls at the front by default. In the tree view (left-hand panel), drag btnCertDropZoneBG so it sits directly above ctrlNewCertificate in the list — controls listed earlier in the tree render further back. If you're unsure which is in front after moving it, right-click btnCertDropZoneBG and check for a "Send backward" option, or just drag a test file onto the zone and confirm the file gets picked up rather than nothing happening.
9.2 Make The Real Attachments Control Invisible
Select ctrlNewCertificate and change these properties. Nothing else on it — not Items, not OnAddFile, not its size or position — needs to change.
| Property | Value |
|---|---|
Fill | =RGBA(255,255,255,0) |
BorderThickness | =0 |
AddAttachmentText | ="" |
NoAttachmentsText | ="" |
DropTargetBackgroundColor | =RGBA(0,78,66,0.12) |
DropTargetBorderColor | =RGBA(0,78,66,0.6) |
DropTargetBorderThickness | =2 |
DropTargetTextColor | =RGBA(0,78,66,1) |
Blanking AddAttachmentText and NoAttachmentsText removes the native "Click to select a file, or drag and drop" / "No file chosen" text — that's now btnCertDropZoneBG's job. The DropTarget* properties still show a highlight while a file is actively being dragged over the zone, so there's visual feedback mid-drag even though the control is otherwise invisible.
If ctrlNewCertificate's current Height is taller than it needs to be for one file (from the earlier attempt to shrink it), leave it as-is rather than trying to compress it further — the control's own minimum internal height doesn't change just because it's transparent now, and forcing it smaller still produces the same internal scrollbar as before. Whatever footprint it naturally needs, btnCertDropZoneBG now covers with a deliberately-styled box instead of blank native space, so the size no longer needs fighting.
9.3 Retire The Paperclip Icon
icoUploadCert is now redundant — the entire zone is clickable and drag-droppable, so a separate icon prompting the same action is one more thing to keep aligned for no benefit. Delete its OnSelect formula (the non-functional Select(ctrlNewCertificate) from the earlier version of this guide) if present, and either delete the icon control entirely or leave it purely as static decoration inside the new zone with no OnSelect at all.
Test
- Open a record's certificate panel. Confirm you see the dashed drop-zone box with the custom text, not native browser chrome.
- Drag a PDF or image file onto the box. Confirm the drop-target highlight appears while dragging, and the file attaches on drop.
- Clear it and instead click the box, browse to a file, and select it. Confirm that works too — drag-and-drop and click-to-browse both go through the same underlying control.
- Click Save record. Confirm the save flow works exactly as before — this step touches nothing in
btnSaveCert, so it should be unaffected.
10. Sources
- Download function and Launch and Param functions — Microsoft Learn, both confirm the Address/URL parameter is a web resource address, not documented as accepting base64 data URIs.
- Community thread: Using Launch() to open a Base64 PDF in a new browser tab — confirms the same base64-via-Launch limitation and that a real URL is expected.