$npx -y skills add microsoft/power-platform-skills --skill add-pdf-reportInternal implementation skill invoked by /add-native for app-generated PDF report workflows using expo-print and, when present, expo-sharing.
| 1 | **Shared instructions: [shared-instructions.md](${CLAUDE_SKILL_DIR}/../../../shared/shared-instructions.md)** - read first. |
| 2 | |
| 3 | # Add PDF Report |
| 4 | |
| 5 | **Internal helper.** Users should invoke `/add-native pdf-report`, `/add-native generate-pdf`, or `/add-native pdf-export`; `/add-native` routes here after resolving the capability. |
| 6 | |
| 7 | Generate or verify a local PDF report wrapper for app-owned PDFs created from records, evidence, certificates, receipts, or summaries. This helper uses `expo-print` to create a local PDF file URI. It may use `expo-sharing` only when that package is already present. It never installs packages or imports the native PDF viewer directly. |
| 8 | |
| 9 | ## Capability boundaries |
| 10 | |
| 11 | | User need | Correct path | |
| 12 | |---|---| |
| 13 | | Generate/export/print a report from app data | This helper: `expo-print` -> local PDF URI | |
| 14 | | Share the generated local PDF from the device | Add share method only if `expo-sharing` is already in `package.json` | |
| 15 | | Retain the generated PDF in Dataverse | Create/update parent row first, then upload to a Dataverse File column with generated services | |
| 16 | | Open an existing HTTPS or local file PDF in the Power Apps native viewer | `/add-native pdf-viewer`, only if `@microsoft/power-apps-native-pdf-viewer` 0.2.9+ is already present | |
| 17 | | Pick/import/upload a user-selected PDF | `/add-native document-picker` or host `<FilePicker>` for Dataverse File columns | |
| 18 | |
| 19 | Local generated PDFs are usually `file://` URIs and can be passed to `openHttpsPdf(...)` with `@microsoft/power-apps-native-pdf-viewer` 0.2.9+. |
| 20 | |
| 21 | ## Steps |
| 22 | |
| 23 | ### 1. Verify app |
| 24 | |
| 25 | ```bash |
| 26 | test -f app.config.js && test -f power.config.json && test -f package.json && test -d src |
| 27 | ``` |
| 28 | |
| 29 | If this fails, tell the user to run `/create-mobile-app` first and STOP. |
| 30 | |
| 31 | ### 2. Verify packages are already present |
| 32 | |
| 33 | `expo-print` is required. `expo-sharing` is optional unless the plan specifically needs sharing behavior. |
| 34 | |
| 35 | ```bash |
| 36 | node -e "const p=require('./package.json'); const deps={...p.dependencies,...p.devDependencies}; const required='expo-print'; if (!deps[required]) { console.error('MISSING: expo-print is not in package.json. The template/app must already ship it for /add-native pdf-report. This skill will not install it or edit native config. Capability not added.'); process.exit(1); } console.log('OK: expo-print package present'); console.log(deps['expo-sharing'] ? 'OK: expo-sharing package present' : 'OPTIONAL_MISSING: expo-sharing is not in package.json; generated PDFs can be created/viewed/uploaded, but sharing helpers must not be generated.');" |
| 37 | ``` |
| 38 | |
| 39 | If `expo-print` is missing, STOP. Do not run `npm install`, `npx expo install`, `pod install`, or edit `app.config.js`. Do not add `pdf-report` to the plan or generated wrappers for this app. |
| 40 | |
| 41 | If `expo-sharing` is missing: |
| 42 | |
| 43 | - Continue for generate-only, native-viewer preview, or Dataverse-upload flows. |
| 44 | - Do not import `expo-sharing`. |
| 45 | - Do not generate `sharePdfReport(...)`. |
| 46 | - If the user's requirement specifically includes sharing, STOP and say sharing is not supported by this template. |
| 47 | |
| 48 | ### 3. Write or verify `src/native/pdfReport.ts` |
| 49 | |
| 50 | Create `src/native/pdfReport.ts` if it does not exist. If it already exists, inspect it and patch only if it throws instead of returning a result, imports missing packages, or routes local URIs to the native PDF viewer. |
| 51 | |
| 52 | The wrapper MUST: |
| 53 | |
| 54 | - Import `expo-print` only after Step 2 confirms it is present. |
| 55 | - Import `expo-sharing` only when Step 2 confirms it is present. |
| 56 | - Return discriminated unions and never throw. |
| 57 | - Treat generated local PDFs as local files for view/share/upload flows. |
| 58 | - Never import `@microsoft/power-apps-native-pdf-viewer` directly from this wrapper. |
| 59 | - Keep HTML generation deterministic and app-owned; do not fetch remote HTML inside the wrapper. |
| 60 | |
| 61 | Base wrapper when `expo-sharing` is present: |
| 62 | |
| 63 | ```ts |
| 64 | // src/native/pdfReport.ts |
| 65 | import * as Print from 'expo-print'; |
| 66 | import * as Sharing from 'expo-sharing'; |
| 67 | |
| 68 | export type PdfReportResult = |
| 69 | | { ok: true; uri: string; numberOfPages?: number; base64?: string } |
| 70 | | { ok: false; reason: 'EMPTY_HTML' | 'PRINT_FAILED'; message?: string }; |
| 71 | |
| 72 | export type PdfShareResult = |
| 73 | | { ok: true } |
| 74 | | { ok: false; reason: 'INVALID_URI' | 'SHARING_UNAVAILABLE' | 'SHARE_FAILED'; message?: string }; |
| 75 | |
| 76 | export function escapePdfHtml(value: string): string { |
| 77 | return value |
| 78 | .replace(/&/g, '&') |
| 79 | .replace(/</g, '<') |
| 80 | .replace(/>/g, '>') |
| 81 | .replace(/"/g, '"') |
| 82 | .replace(/'/g, '''); |
| 83 | } |
| 84 | |
| 85 | export function wrapPdfDocument(input: { title: string; bodyHtml: string; styles?: string }): string { |
| 86 | const title = escapePdfHtml(input.title.trim() || 'Report'); |
| 87 | return `<!doctype html> |
| 88 | <html> |
| 89 | <head> |
| 90 | <meta charset="utf-8" /> |
| 91 | <meta name="v |