Skip to main content

add-camera

Internal implementation skill invoked by /add-native for camera, image picker, barcode scanner, QR scanner, and camera/gallery Dataverse artifact workflows.

Source facts

Repository
microsoft/power-platform-skills
Last source activity
October 7, 2026 at 11:02
Detected SKILL.md language
English
Stars
969
Forks
196

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
add-camera
description
Internal implementation skill invoked by /add-native for camera, image picker, barcode scanner, QR scanner, and camera/gallery Dataverse artifact workflows.
user-invocable
false
disable-model-invocation
true
allowed-tools
Read, Edit, Write, Grep, Glob, Bash, AskUserQuestion
model
sonnet
**Shared instructions: [shared-instructions.md](${PLUGIN_ROOT}/shared/shared-instructions.md)** — read first. **References:** - [dataverse-reference.md](${PLUGIN_ROOT}/skills/add-dataverse/references/dataverse-reference.md) — File/image column upload patterns (Step 7–8) # Add Camera **Internal helper.** Users should invoke `/add-native camera`, `/add-native image-picker`, `/add-native barcode-scanner`, or `/add-native qr-scanner`; `/add-native` routes here after resolving the capability. Generate typed camera + image-picker wrappers, an optional barcode/QR scanner control, and optional custom-upload guidance for Dataverse image/file workflows. This skill **only writes JS files under `src/native/`**. It does not install modules and does not touch `package.json` or `app.config.js` — the underlying Expo modules (`expo-camera`, `expo-image-picker`) and their config plugins must already be shipped by the `microsoft/power-platform-skills/plugins/mobile-apps/template#main` template. If they're missing, STOP and tell the user the template doesn't ship them yet. Why: customer binaries are built from a pre-built rewrap base, not from the customer's `package.json`. Adding a native module here would compile against modules the binary doesn't actually contain, causing runtime crashes after rewrap. See [`/add-native`](../SKILL.md) for the same hard rules. Two modules are required (must already be in `package.json`): - **`expo-camera`** — live viewfinder, barcode scanning - **`expo-image-picker`** — gallery selection + quick camera capture (simpler API, no viewfinder) **Dataverse File/Image boundary:** for normal Dataverse File/Image form fields, screens should use `FilePicker` / `ImagePicker` from `@microsoft/power-apps-native-host` (see [`/add-native` File/Image Picker Ownership](../SKILL.md#fileimage-picker-ownership)). `/add-native camera` owns custom camera/gallery/scanner workflows, such as a dedicated evidence-capture screen, barcode/QR scan gate, or gallery-selected image that is transformed before saving. **Pen/signature boundary:** signature, sign-off, ink, drawing, or pen capture belongs to `/add-native pen-input` (which routes internally to the pen helper). Both camera photos and pen signatures can persist to Dataverse Image/File columns, but the capture wrappers are separate. ## Workflow 1. Verify project → 2. Verify modules are template-shipped → 3. Write camera wrapper → 3b. Write scanner control if requested → 4. Detect Dataverse columns → 5. Write upload helper only for custom capture flows → 6. Type-check → 7. Summary --- ### Step 1 — Verify project ```bash test -f app.config.js && test -f power.config.json && test -f package.json ``` If any file is missing, report and STOP — this skill requires an initialized Power Apps mobile app. ### Step 2 — Verify modules are template-shipped Both `expo-camera` and `expo-image-picker` must already be in `package.json`. Do **not** install them — if they're missing, the upstream template hasn't shipped them yet, and this skill STOPs. ```bash node -e "const p = require('./package.json'); const need = ['expo-camera','expo-image-picker']; const missing = need.filter(m => !p.dependencies?.[m]); if (missing.length) { console.error('MISSING from package.json: ' + missing.join(', ') + '. The upstream template must ship these for /add-native camera to run. Do NOT install them yourself — file an issue at the template repo (plugins/mobile-apps/template) instead.'); process.exit(1); } console.log('OK: both modules present');" ``` If the check fails, STOP. Print the error verbatim. Do not run `npx expo install`. Do not edit `app.config.js`. Tell the user the template version they scaffolded from doesn't include the camera modules — they need to wait for a newer template release or open a request upstream. Also check if the wrapper already exists: ```bash test -f src/native/camera.ts && echo "exists" || echo "missing" ``` If the wrapper exists, skip Step 3 — do NOT overwrite. Continue to Step 3b / Step 4 as needed. Detect whether barcode/QR scanning is requested by checking `$ARGUMENTS` and `native-app-plan.md` for `barcode`, `bar code`, `QR`, `scanner`, `scan gate`, `SKU scan`, or `inventory scan`. If present, set `SCANNER_NEEDED=yes`; otherwise skip Step 3b unless the user explicitly asks for scanner support. ### Step 3 — Write camera wrapper **Print before starting:** > "→ Writing src/native/camera.ts wrapper (takePhoto + pickImage with discriminated-union results)…" Create `src/native/camera.ts`. If the file already exists, **do NOT overwrite** — append a comment noting "regenerated by /add-native camera" and STOP this step. ```typescript // src/native/camera.ts // Camera capture and image picker wrapper for Power Apps mobile apps. // Uses expo-image-picker for both camera capture and gallery selection. // All functions return discriminated-union results — never throw. import * as ImagePicker from 'expo-image-picker'; // --- Result types --- export type PhotoResult = | { ok: true; uri: string; width: number; height: number; mimeType?: string; fileSize?: number } | { ok: false; reason: 'permission-denied' | 'cancelled' | 'unsupported' | 'error'; message?: string }; // --- Permission --- export async function requestCameraPermission(): Promise<boolean> { const { status } = await ImagePicker.requestCameraPermissionsAsync(); return status === 'granted'; } export async function requestMediaLibraryPermission(): Promise<boolean> { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); return status === 'granted'; } // --- Capture --- /** * Launch the device camera and capture a photo. * Returns `{ ok: false, reason: 'unsupported' }` when native camera capture is unavailable. */ export async function takePhoto(options?: { quality?: number; allowsEditing?: boolean; }): Promise<PhotoResult> { const granted = await requestCameraPermission(); if (!granted) return { ok: false, reason: 'permission-denied' }; try { const result = await ImagePicker.launchCameraAsync({ mediaTypes: ['images'], quality: options?.quality ?? 0.8, allowsEditing: options?.allowsEditing ?? false, exif: false, }); if (result.canceled) return { ok: false, reason: 'cancelled' }; const asset = result.assets[0]; return { ok: true, uri: asset.uri, width: asset.width ?? 0, height: asset.height ?? 0, mimeType: asset.mimeType ?? undefined, fileSize: asset.fileSize ?? undefined, }; } catch (e: any) { return { ok: false, reason: 'error', message: e?.message }; } } /** * Open the device photo gallery and pick an image. * Works on all platforms including web (uses native file picker). */ export async function pickImage(options?: { quality?: number; allowsEditing?: boolean; allowsMultipleSelection?: boolean; }): Promise<PhotoResult> { const granted = await requestMediaLibraryPermission(); if (!granted) return { ok: false, reason: 'permission-denied' }; try { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: options?.quality ?? 0.8, allowsEditing: options?.allowsEditing ?? false, allowsMultipleSelection: options?.allowsMultipleSelection ?? false, exif: false, }); if (result.canceled) return { ok: false, reason: 'cancelled' }; const asset = result.assets[0]; return { ok: true, uri: asset.uri, width: asset.width ?? 0, height: asset.height ?? 0, mimeType: asset.mimeType ?? undefined, fileSize: asset.fileSize ?? undefined, }; } catch (e: any) { return { ok: false, reason: 'error', message: e?.message }; } } ``` ### Step 3b — Write barcode/QR scanner control when requested **Skip this step unless `SCANNER_NEEDED=yes`.** Photo-only and gallery-only flows do not need a live `CameraView`. **Print before starting:** > "→ Writing src/native/barcodeScanner.tsx (CameraView barcode/QR scanner control)…" Create `src/native/barcodeScanner.tsx`. If it already exists, do not overwrite. ```tsx // src/native/barcodeScanner.tsx // Barcode / QR scanner control for Power Apps mobile apps. // Uses expo-camera CameraView. Never throws; permission state is rendered inline. import React from 'react'; import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'; import { CameraView, useCameraPermissions } from 'expo-camera'; import type { BarcodeScanningResult, BarcodeType } from 'expo-camera'; export type ScannerResult = { ok: true; data: string; type: string; raw: BarcodeScanningResult; }; export type BarcodeScannerViewProps = { onScanned: (result: ScannerResult) => void; paused?: boolean; resetKey?: unknown; barcodeTypes?: BarcodeType[]; style?: StyleProp<ViewStyle>; overlay?: React.ReactNode; children?: React.ReactNode; }; const DEFAULT_BARCODE_TYPES = [ 'aztec', 'qr', 'ean13', 'ean8', 'upc_a', 'upc_e', 'datamatrix', 'code39', 'code93', 'code128', 'pdf417', 'itf14', 'codabar', ] as BarcodeType[]; export function BarcodeScannerView({ onScanned, paused = false, resetKey, barcodeTypes = DEFAULT_BARCODE_TYPES, style, overlay, children, }: BarcodeScannerViewProps) { const [permission, requestPermission] = useCameraPermissions(); const scanLockedRef = React.useRef(false); React.useEffect(() => { if (permission && !permission.granted && permission.canAskAgain) { requestPermission(); } }, [permission, requestPermission]); React.useEffect(() => { if (!paused) { scanLockedRef.current = false; } }, [paused, resetKey]); const handleBarcodeScanned = React.useCallback((event: BarcodeScanningResult) => { if (paused || scanLockedRef.current) return; scanLockedRef.current = true; onScanned({ ok: true, data: event.data, type: event.type, raw: event }); }, [onScanned, paused]); if (!permission) { return <View style={[styles.fallback, style]}><Text>Checking camera permission...</Text></View>; } if (!permission.granted) { return <View style={[styles.fallback, style]}><Text>Camera permission is required to scan codes.</Text></View>; } return ( <View style={[styles.container, style]}> <CameraView style={StyleSheet.absoluteFill} facing="back" active={!paused} barcodeScannerSettings={{ barcodeTypes }} onBarcodeScanned={paused ? undefined : handleBarcodeScanned} /> {overlay || children ? <View pointerEvents="box-none" style={styles.overlay}>{overlay ?? children}</View> : null} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, overflow: 'hidden', position: 'relative' }, overlay: { ...StyleSheet.absoluteFillObject }, fallback: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }, }); ``` Scanner rendering rule: do **not** put overlay UI as `CameraView` children. Expo Camera can render incorrectly when React children are nested inside the native camera preview. The generated control renders the camera as one layer and renders `overlay` / `children` as a sibling absolute layer above it. Scan mutation rule: the generated control has an internal one-shot scan lock so rapid `onBarcodeScanned` callbacks cannot double-submit. Screens should still set `paused=true` before navigating or mutating data, then reset `paused=false` and change `resetKey` when the screen regains focus. This makes returning to the scanner reliable after a successful scan. Scan-gate business rule: for QR lookup flows, resolve the scanned code against the target entity first (for example, `Test Item`). If the lookup misses, show a clear inline `Item does not exist` message and keep scanner flow in-place. Do not auto-create `Unknown` scan rows unless the approved plan explicitly requires that fallback behavior. Scanner loading UI rule: when scan processing takes time (lookup/create mutation), render a spinner-only overlay inside the scanner preview via the `overlay` prop. Do not render a separate loading card/panel above the camera preview. ### Step 4 — Detect Dataverse image/file columns Check if the project has generated services with file or image upload support: ```text Grep pattern="ImageColumnName|FileColumnName|UploadColumnName" path="src/generated/" ``` **If matches found:** note the table names and column types. Continue to Step 5 only when the plan explicitly requires a custom camera/gallery capture flow outside the host `ImagePicker` / `FilePicker` controls. File/Image host-control safety: this skill does not replace normal Dataverse form controls. Keep host `ImagePicker` / `FilePicker` for standard Dataverse form-bound Image/File fields. **If no matches (or `src/generated/` doesn't exist):** skip Step 5. The camera wrapper (Step 3) and scanner control (Step 3b, when requested) are still useful standalone — screens can display photos or scan codes without uploading to Dataverse. Mention in the summary that Dataverse file/image form fields should use host controls after running `/add-dataverse` with image/file columns. ### Step 5 — Write image upload helper **Print before starting:** > "→ Writing src/native/cameraUpload.ts (Dataverse image column base64 patch helper)…" Create `src/native/cameraUpload.ts`. If the file already exists, **do NOT overwrite**. Do **not** generate this helper for normal Dataverse File/Image form fields. Those use host `FilePicker` / `ImagePicker` controls. Generate it only when the approved screen plan needs a custom camera/gallery capture flow. This helper does not change host `ImagePicker` / `FilePicker` behavior. It only covers custom photo-capture flows where the app receives a camera URI and then updates a Dataverse Image column explicitly. This helper is for **Dataverse Image columns** in native apps. It reads the local photo URI using Expo file APIs, converts it to base64, then calls the generated service `update()` with `{ [imageColumnName]: base64 }`. Do **not** convert camera URIs into browser-style `File` / `Blob` objects for this path. That pattern is fragile in RN/Expo runtimes and causes upload failures like `arrayBuffer is not a function`. ```typescript // src/native/cameraUpload.ts // Bridges camera/gallery photo output to Dataverse Image column updates. // Reads local image URI as base64 and PATCHes the image column via service.update(). import * as FileSystem from 'expo-file-system'; export type UploadResult = | { ok: true } | { ok: false; reason: 'read-failed' | 'update-failed' | 'error'; message?: string }; export type ImageUpdateService = {
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub