$npx -y skills add microsoft/power-platform-skills --skill add-pdf-viewerInternal implementation skill invoked by /add-native for native PDF control workflows. Handles HTTPS and file URI PDF viewing with @microsoft/power-apps-native-pdf-viewer 0.2.9+.
| 1 | **📋 Shared instructions: [shared-instructions.md](${CLAUDE_SKILL_DIR}/../../../shared/shared-instructions.md)** — read first. |
| 2 | |
| 3 | # Add PDF Viewer |
| 4 | |
| 5 | **Internal helper.** Users should invoke `/add-native pdf-viewer`, `/add-native pdf-control`, or `/add-native @microsoft/power-apps-native-pdf-viewer`; `/add-native` routes here after resolving the capability. |
| 6 | |
| 7 | Generate or verify the native PDF viewer wrapper and show how to call its **native React Native API**. Version 0.2.9 and later support HTTPS PDF URLs and local `file://` URIs. Do not use the HostingSDK / PCF path from the package README; that is for a different use case. |
| 8 | |
| 9 | ## Steps |
| 10 | |
| 11 | ### 1. Verify app |
| 12 | |
| 13 | ```bash |
| 14 | test -f app.config.js && test -f power.config.json && test -f package.json && test -d src |
| 15 | ``` |
| 16 | |
| 17 | If this fails, tell the user to run `/create-mobile-app` first and STOP. |
| 18 | |
| 19 | ### 2. Verify package is already present |
| 20 | |
| 21 | ```bash |
| 22 | node -e "const p=require('./package.json'); const m='@microsoft/power-apps-native-pdf-viewer'; if (!p.dependencies?.[m]) { console.error('MISSING: ' + m + ' is not in package.json. The template/app must already ship this native extension. This skill will not install it or edit native config.'); process.exit(1); } let v; try { v=require('./node_modules/' + m + '/package.json').version; } catch { console.error('MISSING_INSTALL: cannot verify the installed ' + m + ' version. Run the project package install first.'); process.exit(1); } const [major,minor,patch]=v.split('.').map(Number); if (!(major > 0 || minor > 2 || (minor === 2 && patch >= 9))) { console.error('UNSUPPORTED_VERSION: ' + m + ' ' + v + ' does not support file:// URIs. Version 0.2.9+ is required.'); process.exit(1); } console.log('OK: native PDF viewer ' + v + ' supports https:// and file://');" |
| 23 | ``` |
| 24 | |
| 25 | If the check fails, STOP. Do not run `npm install`, `npx expo install`, `pod install`, or edit `app.config.js`. Version 0.2.9+ must already be part of the app's native build. |
| 26 | |
| 27 | ### 3. Write or verify `src/native/pdfViewer.ts` |
| 28 | |
| 29 | Create `src/native/pdfViewer.ts` if it does not exist. If it already exists, inspect it and patch only if it violates the supported URI rules or throws instead of returning a result. |
| 30 | |
| 31 | The wrapper MUST: |
| 32 | |
| 33 | - Accept `https://` URLs and `file://` URIs. |
| 34 | - Reject `content://`, `blob:`, `http://`, empty, and malformed URLs before calling native code. |
| 35 | - Return a discriminated union and never throw. |
| 36 | - Return `NATIVE_MODULE_MISSING` when the extension is installed in JS but unavailable in the native build. |
| 37 | - Surface viewer action results (`shared`, `printed`, `dismissed`) when available. |
| 38 | - Return `VIEWER_FAILED` for native errors not covered by validation/module checks. |
| 39 | |
| 40 | ```ts |
| 41 | // src/native/pdfViewer.ts |
| 42 | import { NativePdfViewer } from '@microsoft/power-apps-native-pdf-viewer'; |
| 43 | |
| 44 | export type PdfViewerResult = |
| 45 | | { ok: true; action?: 'shared' | 'printed' | 'dismissed' } |
| 46 | | { ok: false; reason: 'INVALID_URL' | 'NATIVE_MODULE_MISSING' | 'VIEWER_FAILED'; message?: string }; |
| 47 | |
| 48 | export async function openHttpsPdf( |
| 49 | url: string, |
| 50 | options?: { title?: string; maxFileSizeMb?: number; cacheEnabled?: boolean }, |
| 51 | ): Promise<PdfViewerResult> { |
| 52 | let parsed: URL; |
| 53 | try { |
| 54 | parsed = new URL(url); |
| 55 | } catch { |
| 56 | return { ok: false, reason: 'INVALID_URL', message: 'PDF location must be a valid https:// URL or file:// URI.' }; |
| 57 | } |
| 58 | |
| 59 | const isHttpsUrl = parsed.protocol === 'https:'; |
| 60 | const isFileUri = parsed.protocol === 'file:' && url.startsWith('file://') && parsed.pathname !== '/'; |
| 61 | if (!isHttpsUrl && !isFileUri) { |
| 62 | return { ok: false, reason: 'INVALID_URL', message: 'Native PDF viewer supports https:// and file:// inputs only.' }; |
| 63 | } |
| 64 | |
| 65 | if (!NativePdfViewer?.openPdf) { |
| 66 | return { ok: false, reason: 'NATIVE_MODULE_MISSING', message: 'Native PDF viewer module is not available in this build.' }; |
| 67 | } |
| 68 | |
| 69 | try { |
| 70 | const response = await NativePdfViewer.openPdf(url, { |
| 71 | maxFileSizeMb: 50, |
| 72 | cacheEnabled: true, |
| 73 | ...options, |
| 74 | }); |
| 75 | |
| 76 | if (response.status === 'ok') { |
| 77 | return { ok: true, action: response.result?.action }; |
| 78 | } |
| 79 | |
| 80 | return { ok: false, reason: 'VIEWER_FAILED', message: response.message ?? response.error }; |
| 81 | } catch (error: any) { |
| 82 | return { ok: false, reason: 'VIEWER_FAILED', message: error?.message ?? String(error) }; |
| 83 | } |
| 84 | } |
| 85 | ``` |
| 86 | |
| 87 | ### 4. Use the wrapper |
| 88 | |
| 89 | Screens import the wrapper, not the native package directly: |
| 90 | |
| 91 | ```ts |
| 92 | import { openHttpsPdf } from '@/native/pdfViewer'; |
| 93 | |
| 94 | const response = await openHttpsPdf('https://example.com/report.pdf', { |
| 95 | title: "Inspection report", |
| 96 | }); |
| 97 | |
| 98 | if (response.ok) { |
| 99 | switch (response.action) { |
| 100 | case "shared": |
| 101 | // User completed native Share. |
| 102 | break; |
| 103 | case "printed": |
| 104 | // User |