[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M699MS14BWDTN4B4QHWCKABQ":3,"skill-same:01M699MS14BWDTN4B4QHWCKABQ":74},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":51,"commitSha":52,"skillDirectory":53,"forksCount":54,"repositoryHtmlUrl":55,"repositoryHomepageUrl":56,"ownerHtmlUrl":57,"ownerBlogUrl":58,"ownerDescription":59,"updatedAt":60,"install":61,"resources":73},"01M699MS14BWDTN4B4QHWCKABQ","add-camera",null,"Internal implementation skill invoked by \u002Fadd-native for camera, image picker, barcode scanner, QR scanner, and camera\u002Fgallery Dataverse artifact workflows.","microsoft\u002Fpower-platform-skills","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M699JVYJFB75D9JM70BN4H0Q",1143,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fmobile-apps\u002Fskills\u002Fadd-native\u002Fadd-camera\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"add camera","camera","add","artifact","for","true","sonnet","qr","gallery","native","false","barcode","image","scanner","edit","write","read","Bash","dataverse","workflows","2026-11-10 10:12:30",false,"---\nname: add-camera\ndescription: Internal implementation skill invoked by \u002Fadd-native for camera, image picker, barcode scanner, QR scanner, and camera\u002Fgallery Dataverse artifact workflows.\nuser-invocable: false\ndisable-model-invocation: true\nallowed-tools: Read, Edit, Write, Grep, Glob, Bash, AskUserQuestion\nmodel: sonnet\n---\n\n**Shared instructions: [shared-instructions.md](${PLUGIN_ROOT}\u002Fshared\u002Fshared-instructions.md)** — read first.\n\n**Working directory:** before any project read or command, execute\n[native-artifact-compatibility.md Step 0](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Fnative-artifact-compatibility.md#0-bind-every-operation-to-the-app-root).\nInherit `\u002Fadd-native`'s resolved absolute `working_dir`; bind every shell call and\nfile tool to it, even when this helper starts from another directory.\n\n**References:**\n\n- [dataverse-reference.md](${PLUGIN_ROOT}\u002Fskills\u002Fadd-dataverse\u002Freferences\u002Fdataverse-reference.md) — File\u002Fimage column upload patterns (Step 7–8)\n\n# Add Camera\n\n**Internal helper.** Users should invoke `\u002Fadd-native camera`, `\u002Fadd-native image-picker`, `\u002Fadd-native barcode-scanner`, or `\u002Fadd-native qr-scanner`; `\u002Fadd-native` routes here after resolving the capability.\n\nGenerate typed camera + image-picker wrappers, an optional barcode\u002FQR scanner control, and optional custom-upload guidance for Dataverse image\u002Ffile workflows.\n\nThis skill **only writes JS files under `src\u002Fnative\u002F`**. It does not install\nmodules or touch `package.json` \u002F `app.config.js`. Only the modules required by\nthe approved artifact set below must already ship in the template\u002Fnative build;\nif a required module is missing, STOP. Do not require unrelated capture packages\nfor a scanner-only or upload-only request.\n\nWhy: 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 [`\u002Fadd-native`](..\u002FSKILL.md) for the same hard rules.\n\nRequired modules depend on the approved artifact set (all must already be in\n`package.json`):\n\n| Requested artifact key | Required module | Output |\n|---|---|---|\n| `photo` \u002F `gallery` | `expo-image-picker` | `camera.ts` capture\u002Fpicker APIs |\n| `scanner` | `expo-camera` | `barcodeScanner.tsx` live scanner |\n| `upload` | `expo-file-system` | `cameraUpload.ts` custom Image helper |\n\nUse the union for combined requests. A scanner-only request does not require\n`expo-image-picker` or `camera.ts`; an upload-only request does not require a\ncapture\u002Fscanner package.\n\n**Dataverse File\u002FImage boundary:** for normal Dataverse File\u002FImage form fields, screens should use `FilePicker` \u002F `ImagePicker` from `@microsoft\u002Fpower-apps-native-host` (see [`\u002Fadd-native` File\u002FImage Picker Ownership](..\u002FSKILL.md#fileimage-picker-ownership)). `\u002Fadd-native camera` owns custom camera\u002Fgallery\u002Fscanner workflows, such as a dedicated evidence-capture screen, barcode\u002FQR scan gate, or gallery-selected image that is transformed before saving.\n\n**Pen\u002Fsignature boundary:** signature, sign-off, ink, drawing, or pen capture belongs to `\u002Fadd-native pen-input` (which routes internally to the pen helper). Both camera photos and pen signatures can persist to Dataverse Image\u002FFile columns, but the capture wrappers are separate.\n\n## Workflow\n\n1. Verify project → 2. Verify modules are template-shipped → 2a. Reconcile requested artifacts → 3. Apply camera decision → 3b. Apply scanner decision → 4. Confirm storage decision → 5. Apply upload decision → 6. Verify → 7. Summary\n\n---\n\n### Step 1 — Verify project\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\ntest -f app.config.js && test -f power.config.json && test -f package.json || { echo \"BLOCKED: working_dir is not an initialized app\" >&2; exit 1; }\n```\n\nIf any file is missing, report and STOP — this skill requires an initialized Power Apps mobile app.\n\n### Step 2 — Verify modules are template-shipped\n\nResolve the approved artifact keys before this check. Missing\u002Fambiguous intent\nreturns to the owner (ask standalone); do not assume all artifacts. Substitute\nthe exact JSON list, e.g. `[\"scanner\"]` or `[\"photo\",\"upload\"]`, below:\n\nUse the owner's normalized public capability to resolve those keys:\n\n| Public capability | Artifact keys |\n|---|---|\n| `camera` | `[\"photo\"]` |\n| `image-picker` | `[\"gallery\"]` |\n| `barcode-scanner` | `[\"scanner\"]` |\n| `qr-scanner` | `[\"scanner\"]` |\n\nFor combined approved capabilities use the union. Add `upload` only for an\nexplicitly approved custom Dataverse Image-upload helper, not automatically for\ncapture or scanning. The checker accepts artifact-key JSON, not raw `$ARGUMENTS`\nor a public capability string. Missing\u002Fambiguous intent still returns to the owner.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode - '\u003Capproved-artifact-keys-json>' \u003C\u003C'NODE'\nconst p = require('.\u002Fpackage.json');\nconst modules = {\n  photo: 'expo-image-picker',\n  gallery: 'expo-image-picker',\n  scanner: 'expo-camera',\n  upload: 'expo-file-system',\n};\nconst artifacts = JSON.parse(process.argv[2]);\nif (!Array.isArray(artifacts) || artifacts.length === 0\n    || artifacts.some(key => typeof key !== 'string' || !Object.hasOwn(modules, key))) {\n  throw new Error('Expected a nonempty approved artifact list: photo, gallery, scanner, upload');\n}\nconst need = [...new Set(artifacts.map(key => modules[key]))];\nconst missing = need.filter(name => !p.dependencies?.[name]);\nif (missing.length) {\n  console.error('MISSING from package.json: ' + missing.join(', ') + '. Do not install native modules.');\n  process.exit(1);\n}\nconsole.log('OK: requested modules present');\nNODE\n```\n\nIf the check fails, STOP. Print the error verbatim. Do not run `npx expo install`. Do not edit `app.config.js`. Tell the user which requested modules the template lacks; they need template support, not an app-local native-package install.\n\n### Step 2a — Reconcile requested artifacts\n\nRead and execute [native-artifact-compatibility.md](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Fnative-artifact-compatibility.md)\nSteps 1–3 for the camera\u002Fgallery\u002Fscanner row before any writes or reuse. Inherit\nthe current approval\u002Fmode from `\u002Fadd-native`; do not repeat a valid scoped approval.\n\nInventory `src\u002Fnative\u002Fcamera.ts` (including gallery `pickImage`, not a required\n`imagePicker.ts`), requested `src\u002Fnative\u002FbarcodeScanner.tsx`, and requested\n`src\u002Fnative\u002FcameraUpload.ts` independently. Check requested exports, options,\nfailure paths, scanner lock\u002Freset\u002Foverlay behavior, and any requested Dataverse\nImage-upload payload semantics. Resolve the approved storage platform and Step 4\nrequirements read-only now, before writes; non-Dataverse retention must not\nrequire Dataverse columns or `cameraUpload.ts`.\nMissing required storage or an incompatible artifact outside approval returns\n`NEEDS_CONTEXT` to the owner, or asks standalone; it is not a successful skip.\n\nSet `SCANNER_NEEDED=yes` only for scanning in the current approved request\u002Fscope\n(`barcode`, `QR`, scanner, or a supplied scan-gate requirement), not unrelated\nmatches in an older plan. A compatible camera file cannot satisfy a missing\nscanner or upload helper. Preserve compatible files unchanged in partial sets.\n\n### Step 3 — Write camera wrapper\n\n**Print only when creating\u002Fupdating the requested camera wrapper:**\n> \"→ Writing src\u002Fnative\u002Fcamera.ts wrapper (takePhoto + pickImage with discriminated-union results)…\"\n\nApply Step 2a's decision for `src\u002Fnative\u002Fcamera.ts`: create if requested and\nmissing, reuse unchanged only if compatible, or make only the approved scoped\nupdate. Scanner-only requests do not require this file. Preserve custom code and\nexisting exports; do not append regeneration comments or replace it with the example.\n\n```typescript\n\u002F\u002F src\u002Fnative\u002Fcamera.ts\n\u002F\u002F Camera capture and image picker wrapper for Power Apps mobile apps.\n\u002F\u002F Uses expo-image-picker for both camera capture and gallery selection.\n\u002F\u002F Capture\u002Fpick functions return discriminated results; permission helpers retain boolean APIs.\n\nimport * as ImagePicker from 'expo-image-picker';\n\n\u002F\u002F --- Result types ---\n\nexport type PhotoResult =\n  | { ok: true; uri: string; width: number; height: number; mimeType?: string; fileSize?: number }\n  | { ok: false; reason: 'permission-denied' | 'cancelled' | 'unsupported' | 'error'; message?: string };\n\n\u002F\u002F --- Permission ---\n\ntype PermissionResult =\n  | { ok: true }\n  | { ok: false; reason: 'permission-denied' | 'unsupported' | 'error'; message?: string };\n\nasync function requestPermission(\n  request?: () => Promise\u003C{ status: string }>,\n): Promise\u003CPermissionResult> {\n  if (typeof request !== 'function') {\n    return { ok: false, reason: 'unsupported', message: 'Permission API is unavailable.' };\n  }\n  try {\n    const { status } = await request();\n    return status === 'granted'\n      ? { ok: true }\n      : { ok: false, reason: 'permission-denied' };\n  } catch (error) {\n    return {\n      ok: false,\n      reason: 'error',\n      message: error instanceof Error ? error.message : 'Permission request failed.',\n    };\n  }\n}\n\nasync function permissionGranted(\n  request?: () => Promise\u003C{ status: string }>,\n): Promise\u003Cboolean> {\n  const permission = await requestPermission(request);\n  if (!permission.ok && permission.reason !== 'permission-denied') {\n    \u002F\u002F Preserve the public boolean API without silently hiding a failed native request.\n    console.warn(`Device permission request failed (${permission.reason}).`);\n  }\n  return permission.ok;\n}\n\nexport async function requestCameraPermission(): Promise\u003Cboolean> {\n  return permissionGranted(ImagePicker.requestCameraPermissionsAsync);\n}\n\nexport async function requestMediaLibraryPermission(): Promise\u003Cboolean> {\n  return permissionGranted(ImagePicker.requestMediaLibraryPermissionsAsync);\n}\n\n\u002F\u002F --- Capture ---\n\n\u002F**\n * Launch the device camera and capture a photo.\n * Missing APIs return 'unsupported'; rejected native calls return 'error'.\n *\u002F\nexport async function takePhoto(options?: {\n  quality?: number;\n  allowsEditing?: boolean;\n}): Promise\u003CPhotoResult> {\n  try {\n    if (typeof ImagePicker.launchCameraAsync !== 'function') {\n      return { ok: false, reason: 'unsupported', message: 'Camera API is unavailable.' };\n    }\n    const permission = await requestPermission(ImagePicker.requestCameraPermissionsAsync);\n    if (!permission.ok) return permission;\n\n    const result = await ImagePicker.launchCameraAsync({\n      mediaTypes: ['images'],\n      quality: options?.quality ?? 0.8,\n      allowsEditing: options?.allowsEditing ?? false,\n      exif: false,\n    });\n\n    if (result.canceled) return { ok: false, reason: 'cancelled' };\n\n    const asset = result.assets[0];\n    return {\n      ok: true,\n      uri: asset.uri,\n      width: asset.width ?? 0,\n      height: asset.height ?? 0,\n      mimeType: asset.mimeType ?? undefined,\n      fileSize: asset.fileSize ?? undefined,\n    };\n  } catch (error) {\n    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Camera capture failed.' };\n  }\n}\n\n\u002F**\n * Open the device photo gallery and pick an image.\n * Works on all platforms including web (uses native file picker).\n *\u002F\nexport async function pickImage(options?: {\n  quality?: number;\n  allowsEditing?: boolean;\n  allowsMultipleSelection?: boolean;\n}): Promise\u003CPhotoResult> {\n  try {\n    if (typeof ImagePicker.launchImageLibraryAsync !== 'function') {\n      return { ok: false, reason: 'unsupported', message: 'Image picker API is unavailable.' };\n    }\n    const permission = await requestPermission(ImagePicker.requestMediaLibraryPermissionsAsync);\n    if (!permission.ok) return permission;\n\n    const result = await ImagePicker.launchImageLibraryAsync({\n      mediaTypes: ['images'],\n      quality: options?.quality ?? 0.8,\n      allowsEditing: options?.allowsEditing ?? false,\n      allowsMultipleSelection: options?.allowsMultipleSelection ?? false,\n      exif: false,\n    });\n\n    if (result.canceled) return { ok: false, reason: 'cancelled' };\n\n    const asset = result.assets[0];\n    return {\n      ok: true,\n      uri: asset.uri,\n      width: asset.width ?? 0,\n      height: asset.height ?? 0,\n      mimeType: asset.mimeType ?? undefined,\n      fileSize: asset.fileSize ?? undefined,\n    };\n  } catch (error) {\n    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Image selection failed.' };\n  }\n}\n```\n\n### Step 3b — Write barcode\u002FQR scanner control when requested\n\n**Skip this step unless `SCANNER_NEEDED=yes`.** Photo-only and gallery-only flows do not need a live `CameraView`.\n\n**Print before starting:**\n> \"→ Writing src\u002Fnative\u002FbarcodeScanner.tsx (CameraView barcode\u002FQR scanner control)…\"\n\nApply Step 2a's independent decision for `src\u002Fnative\u002FbarcodeScanner.tsx`: create\nwhen requested and missing, reuse unchanged only if compatible, or make only the\napproved scoped update. Existence alone does not verify `BarcodeScannerView`,\n`onScanned`, `paused`, `resetKey`, or the requested barcode types.\n\n```tsx\n\u002F\u002F src\u002Fnative\u002FbarcodeScanner.tsx\n\u002F\u002F Barcode \u002F QR scanner control for Power Apps mobile apps.\n\u002F\u002F Uses expo-camera CameraView. Never throws; permission state is rendered inline.\n\nimport React from 'react';\nimport { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';\nimport { CameraView, useCameraPermissions } from 'expo-camera';\nimport type { BarcodeScanningResult, BarcodeType } from 'expo-camera';\n\nexport type ScannerResult = {\n  ok: true;\n  data: string;\n  type: string;\n  raw: BarcodeScanningResult;\n};\n\nexport type BarcodeScannerViewProps = {\n  onScanned: (result: ScannerResult) => void;\n  paused?: boolean;\n  resetKey?: unknown;\n  barcodeTypes?: BarcodeType[];\n  style?: StyleProp\u003CViewStyle>;\n  overlay?: React.ReactNode;\n  children?: React.ReactNode;\n};\n\nconst DEFAULT_BARCODE_TYPES = [\n  'aztec',\n  'qr',\n  'ean13',\n  'ean8',\n  'upc_a',\n  'upc_e',\n  'datamatrix',\n  'code39',\n  'code93',\n  'code128',\n  'pdf417',\n  'itf14',\n  'codabar',\n] as BarcodeType[];\n\nexport function BarcodeScannerView({\n  onScanned,\n  paused = false,\n  resetKey,\n  barcodeTypes = DEFAULT_BARCODE_TYPES,\n  style,\n  overlay,\n  children,\n}: BarcodeScannerViewProps) {\n  const [permission, requestPermission] = useCameraPermissions();\n  const [permissionError, setPermissionError] = React.useState\u003Cstring | null>(null);\n  const permissionAttemptedRef = React.useRef(false);\n  const scanLockedRef = React.useRef(false);\n\n  React.useEffect(() => {\n    if (permission && !permission.granted && permission.canAskAgain && !permissionAttemptedRef.current) {\n      permissionAttemptedRef.current = true;\n      async function requestScannerPermission() {\n        try {\n          await requestPermission();\n        } catch {\n          setPermissionError('Camera permission request failed. Check device settings and reopen the scanner.');\n        }\n      }\n      void requestScannerPermission();\n    }\n  }, [permission, requestPermission]);\n\n  React.useEffect(() => {\n    if (!paused) {\n      scanLockedRef.current = false;\n    }\n  }, [paused, resetKey]);\n\n  const handleBarcodeScanned = React.useCallback((event: BarcodeScanningResult) => {\n    if (paused || scanLockedRef.current) return;\n    scanLockedRef.current = true;\n    onScanned({ ok: true, data: event.data, type: event.type, raw: event });\n  }, [onScanned, paused]);\n\n  if (permissionError) {\n    return \u003CView style={[styles.fallback, style]}>\u003CText>{permissionError}\u003C\u002FText>\u003C\u002FView>;\n  }\n\n  if (!permission) {\n    return \u003CView style={[styles.fallback, style]}>\u003CText>Checking camera permission...\u003C\u002FText>\u003C\u002FView>;\n  }\n\n  if (!permission.granted) {\n    return \u003CView style={[styles.fallback, style]}>\u003CText>Camera permission is required to scan codes.\u003C\u002FText>\u003C\u002FView>;\n  }\n\n  return (\n    \u003CView style={[styles.container, style]}>\n      \u003CCameraView\n        style={StyleSheet.absoluteFill}\n        facing=\"back\"\n        active={!paused}\n        barcodeScannerSettings={{ barcodeTypes }}\n        onBarcodeScanned={paused ? undefined : handleBarcodeScanned}\n      \u002F>\n      {overlay || children ? \u003CView pointerEvents=\"box-none\" style={styles.overlay}>{overlay ?? children}\u003C\u002FView> : null}\n    \u003C\u002FView>\n  );\n}\n\nconst styles = StyleSheet.create({\n  container: { flex: 1, overflow: 'hidden', position: 'relative' },\n  overlay: { ...StyleSheet.absoluteFillObject },\n  fallback: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 },\n});\n```\n\nScanner 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` \u002F `children` as a sibling absolute layer above it.\n\nScan 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.\n\nScan-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.\n\nScanner loading UI rule: when scan processing takes time (lookup\u002Fcreate mutation), render a spinner-only overlay inside the scanner preview via the `overlay` prop. Do not render a separate loading card\u002Fpanel above the camera preview.\n\n### Step 4 — Detect Dataverse image\u002Ffile columns\n\nReuse Step 2a's read-only storage findings; repeat the check only if those files\nchanged. Classify the approved storage destination before searching Dataverse:\n\n| Approved storage scope | Helper action |\n|---|---|\n| No retention \u002F local preview only | Skip the Dataverse search and Step 5; fulfill the requested capture\u002Fscanner artifacts. |\n| Non-Dataverse retention (for example an existing SharePoint document library) or approved local-file retention | Skip the Dataverse search and Step 5. Validate the required capture output\u002Fpayload against the approved destination contract, then return the connector\u002Flocal persistence work to the owner. Missing Dataverse columns are not an error on this path. Do not generate `cameraUpload.ts` or claim persistence is implemented by capture alone. |\n| Dataverse File retention or a normal host File\u002FImage field | Apply the shared storage checks and return the File\u002Fhost-control integration to the owner; do not use the custom Image `update()` helper. A File target is not supported by the Image `update()` example. |\n| Custom Dataverse Image upload helper requested | Inspect the actual approved Image column and generated service signature below, then apply Step 5. |\n| Destination or required payload contract is unknown | Return `NEEDS_CONTEXT` to the owner (ask standalone); do not assume Dataverse or silently drop retention. |\n\nOnly the custom Dataverse Image-helper branch requires this column search:\n\n```text\nGrep pattern=\"ImageColumnName|FileColumnName|UploadColumnName\" path=\"src\u002Fgenerated\u002F\"\n```\n\n**If matches found:** verify the approved target, column type, payload, and write\nresult contract. Continue to Step 5 only for requested custom camera\u002Fgallery\n**Image** retention outside host controls. Mere presence of generated columns does\nnot authorize an upload helper. If the matched target is File rather than the\napproved Image target, return the mismatch to the owner without generating an\nImage helper or claiming File persistence is implemented.\n\nFile\u002FImage host-control safety: this skill does not replace normal Dataverse form controls. Keep host `ImagePicker` \u002F `FilePicker` for standard Dataverse form-bound Image\u002FFile fields.\n\n**If the requested Dataverse Image target is missing\u002Funverified:** return\n`NEEDS_CONTEXT` to the owner, or ask standalone; do not silently deliver a\ncapture-only success for that requested helper. This blocker applies only to\nthe Dataverse Image-helper branch, not an approved connector\u002Flocal destination.\nDo not generate schema\u002Fservices or hand-edit `src\u002Fgenerated\u002F`.\n\n### Step 5 — Write image upload helper\n\n**Print before starting:**\n> \"→ Writing src\u002Fnative\u002FcameraUpload.ts (Dataverse image column base64 patch helper)…\"\n\nApply Step 2a's independent decision for `src\u002Fnative\u002FcameraUpload.ts`: create\nwhen requested and missing, reuse unchanged only if compatible with the approved\nImage target and installed file API, or make only the approved scoped update.\nAn existing Image helper cannot satisfy File retention.\n\nDo **not** generate this helper for normal Dataverse File\u002FImage form fields. Those\nuse host `FilePicker` \u002F `ImagePicker` controls. Generate it only when the current\napproved scope requests custom camera\u002Fgallery Image retention, including an\nexplicit implementation-only upload-helper request; a screen plan is not required\nfor that bounded operation.\n\nThis helper does not change host `ImagePicker` \u002F `FilePicker` behavior. It only covers custom photo-capture flows where the app receives a camera URI and then updates a Dataverse Image column explicitly.\n\nThis 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 }`.\n\nDo **not** convert camera URIs into browser-style `File` \u002F `Blob` objects for this path. That pattern is fragile in RN\u002FExpo runtimes and causes upload failures like `arrayBuffer is not a function`.\n\n```typescript\n\u002F\u002F src\u002Fnative\u002FcameraUpload.ts\n\u002F\u002F Bridges camera\u002Fgallery photo output to Dataverse Image column updates.\n\u002F\u002F Reads local image URI as base64 and PATCHes the image column via service.update().\n\nimport * as FileSystem from 'expo-file-system';\n\nexport type UploadResult =\n  | { ok: true }\n  | { ok: false; reason: 'read-failed' | 'update-failed' | 'error'; message?: string };\n\nexport type ImageUpdateService = {\n  update: (id: string, body: any) => Promise\u003C{ success: boolean; error?: { message?: string } }>;\n};\n\n\u002F**\n * Upload a photo from takePhoto() \u002F pickImage() to a Dataverse Image column.\n *\n * @param uri             - Photo URI from the camera wrapper result\n * @param service         - Generated Dataverse service with update()\n * @param recordId        - The Dataverse record GUID to patch\n * @param imageColumnName - Dataverse Image column logical name\n *\u002F\nexport async function uploadPhotoToImageColumn(\n  uri: string,\n  service: ImageUpdateService,\n  recordId: string,\n  imageColumnName: string,\n): Promise\u003CUploadResult> {\n  try {\n    const base64 = await readUriAsBase64(uri);\n    if (!base64) {\n      return { ok: false, reason: 'read-failed', message: 'Could not read the image URI as base64.' };\n    }\n\n    \u002F\u002F Dataverse Image columns expect base64 payload value (without data URI prefix).\n    const result = await service.update(recordId, { [imageColumnName]: base64 });\n\n    if (!result.success || result.error) {\n      return { ok: false, reason: 'update-failed', message: result.error?.message ?? 'Dataverse image update failed.' };\n    }\n\n    return { ok: true };\n  } catch (error) {\n    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Unable to upload image.' };\n  }\n}\n\n\u002F**\n * Read a local photo URI to a base64 string using Expo file APIs.\n *\u002F\nasync function readUriAsBase64(uri: string): Promise\u003Cstring | null> {\n  try {\n    const fsAny = FileSystem as any;\n    if (typeof fsAny.readAsStringAsync !== 'function') {\n      return null;\n    }\n\n    const normalizedUri = uri.startsWith('file:\u002F\u002F') ? uri : `file:\u002F\u002F${uri}`;\n    const base64 = await fsAny.readAsStringAsync(normalizedUri, {\n      encoding: fsAny.EncodingType?.Base64 ?? 'base64',\n    });\n    if (typeof base64 !== 'string' || base64.length === 0) {\n      return null;\n    }\n\n    return base64;\n  } catch {\n    return null;\n  }\n}\n```\n\n**Important:** The `service` parameter is typed loosely (`ImageUpdateService`) so it works with any generated service that has `update()`. The caller passes the concrete service — this avoids importing a specific generated service in the helper.\n\n**Write contract:** the helper must treat any non-truthy `result.success` as failure even when `result.error` is missing. Generated services return `IOperationResult`, and native app screens must not show success or navigate after any Dataverse write unless `success` is truthy.\n\n**Template\u002FAPI note:** if `readAsStringAsync` is not available from `expo-file-system` in your template version, import from `expo-file-system\u002Flegacy` and keep the same base64 behavior.\n\n### Step 6 — Type-check\n\n**Print before starting:**\n> \"→ Running tsc to verify the requested native artifacts compile (~10–20 seconds).\"\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnpx --no-install tsc --noEmit\n```\n\nFix only in-scope helper errors. Common issues:\n- `readAsStringAsync` not found on `expo-file-system` — switch import to `expo-file-system\u002Flegacy` for this helper.\n- Import path mismatches — verify `src\u002Fnative\u002F` is reachable from screen components.\n\nExecute the shared compatibility contract's Step 4 after type-checking. Recheck\nevery requested camera\u002Fscanner\u002Fupload artifact and storage obligation, including\npermission failures, repeated scan callbacks, URI reads, and false write results.\nType-check success alone is insufficient; return unresolved requirements rather\nthan marking a partial output set complete. Do not fix screen\u002Fgenerated files.\n\n### Step 7 — Summary\n\nReturn the shared compatibility result and actual created\u002Fupdated\u002Freused paths.\nShow only applicable usage examples; no upload success claim when retention is\nunresolved, and no full-feature claim for implementation-only work.\n\n```\nRequested native artifacts ready\n---\nCapabilities               : \u003Cfulfilled requested capabilities only>\nModules (template-shipped) : \u003Crequired modules for those capabilities only>\npackage.json               : unchanged ✓\napp.config.js              : unchanged ✓\nArtifacts                  : \u003Cone row per requested path: created \u002F updated \u002F reused>\n\nType-check                 : \u003Cactual result>\nUI integration             : \u003Creturned to owner \u002F intentionally not performed>\n---\n```\n\nDo not list `camera.ts` for scanner-only or upload-only results. Do not list\nunrequested companion paths as generated, even if they already existed.\n\n**Include this usage only when capture and custom Image upload were fulfilled:**\n\n```text\n  import { takePhoto } from '..\u002Fnative\u002Fcamera';\n  import { uploadPhotoToImageColumn } from '..\u002Fnative\u002FcameraUpload';\n  import { Cr123_inspectionService } from '..\u002Fgenerated\u002Fservices\u002FCr123_inspectionService';\n\n  const result = await takePhoto();\n  if (result.ok) {\n    const upload = await uploadPhotoToImageColumn(\n      result.uri,\n      Cr123_inspectionService,\n      recordId,\n      'cr123_sitephoto'  \u002F\u002F Dataverse Image column logical name\n    );\n    if (upload.ok) {\n      showToast('Photo attached to record');\n    }\n  }\n```\n\n**Include this usage only when gallery picking was fulfilled:**\n\n```text\n  import { pickImage } from '..\u002Fnative\u002Fcamera';\n\n  const result = await pickImage();\n  if (result.ok) {\n    setPreviewUri(result.uri);\n  }\n```\n\n**Include this usage only when scanner support was fulfilled:**\n\n```text\n  import { BarcodeScannerView } from '..\u002Fnative\u002FbarcodeScanner';\n  import { useFocusEffect } from 'expo-router';\n\n  const [paused, setPaused] = useState(false);\n  const [scanResetKey, setScanResetKey] = useState(0);\n\n  useFocusEffect(\n    React.useCallback(() => {\n      setPaused(false);\n      setScanResetKey((value) => value + 1);\n      return () => setPaused(true);\n    }, [])\n  );\n\n  \u003CBarcodeScannerView\n    paused={paused}\n    resetKey={scanResetKey}\n    barcodeTypes={['qr', 'ean13', 'code128']}\n    overlay={\n      \u003C>\n        \u003CScannerFrame \u002F>\n        {isScanning ? \u003CScannerOverlaySpinner \u002F> : null}\n      \u003C\u002F>\n    }\n    onScanned={({ data, type }) => {\n      setPaused(true);\n      handleCode(data, type);\n    }}\n  \u002F>\n```\n\nNo native rebuild or permission\u002Fconfig change is performed. These are JS\nwrapper\u002Fcontrol changes over template-shipped native modules.\n\n## Notes\n\n- This skill never modifies `src\u002FplayerConfig.ts`, `src\u002Fgenerated\u002F`, or any screen file.\n- `takePhoto()` returns `unsupported` for missing APIs and `error` for rejected native calls; neither rejects its promise.\n- Barcode\u002FQR scanning is handled here via `src\u002Fnative\u002FbarcodeScanner.tsx` when requested. Use it for scan gates and lookup flows; do not use it as a replacement for Dataverse File\u002FImage host controls.\n- `cameraUpload.ts` in this skill targets custom Dataverse Image-column capture flows only. It does not replace host `ImagePicker` \u002F `FilePicker` controls for standard Dataverse forms.\n- If Dataverse tables are added later (via `\u002Fadd-dataverse`), re-run `\u002Fadd-native camera` with the approved custom Image-retention requirement. Reconcile all requested artifacts again, reuse compatible wrappers unchanged, and create\u002Fupdate `cameraUpload.ts` only within that scope. This never installs modules.\n","**Shared instructions: [shared-instructions.md](${PLUGIN_ROOT}\u002Fshared\u002Fshared-instructions.md)** — read first.\n\n**Working directory:** before any project read or command, execute\n[native-artifact-compatibility.md Step 0](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Fnative-artifact-compatibility.md#0-bind-every-operation-to-the-app-root).\nInherit `\u002Fadd-native`'s resolved absolute `working_dir`; bind every shell call and\nfile tool to it, even when this helper starts from another directory.\n\n**References:**\n\n- [dataverse-reference.md](${PLUGIN_ROOT}\u002Fskills\u002Fadd-dataverse\u002Freferences\u002Fdataverse-reference.md) — File\u002Fimage column upload patterns (Step 7–8)\n\n# Add Camera\n\n**Internal helper.** Users should invoke `\u002Fadd-native camera`, `\u002Fadd-native image-picker`, `\u002Fadd-native barcode-scanner`, or `\u002Fadd-native qr-scanner`; `\u002Fadd-native` routes here after resolving the capability.\n\nGenerate typed camera + image-picker wrappers, an optional barcode\u002FQR scanner control, and optional custom-upload guidance for Dataverse image\u002Ffile workflows.\n\nThis skill **only writes JS files under `src\u002Fnative\u002F`**. It does not install\nmodules or touch `package.json` \u002F `app.config.js`. Only the modules required by\nthe approved artifact set below must already ship in the template\u002Fnative build;\nif a required module is missing, STOP. Do not require unrelated capture packages\nfor a scanner-only or upload-only request.\n\nWhy: 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 [`\u002Fadd-native`](..\u002FSKILL.md) for the same hard rules.\n\nRequired modules depend on the approved artifact set (all must already be in\n`package.json`):\n\n| Requested artifact key | Required module | Output |\n|---|---|---|\n| `photo` \u002F `gallery` | `expo-image-picker` | `camera.ts` capture\u002Fpicker APIs |\n| `scanner` | `expo-camera` | `barcodeScanner.tsx` live scanner |\n| `upload` | `expo-file-system` | `cameraUpload.ts` custom Image helper |\n\nUse the union for combined requests. A scanner-only request does not require\n`expo-image-picker` or `camera.ts`; an upload-only request does not require a\ncapture\u002Fscanner package.\n\n**Dataverse File\u002FImage boundary:** for normal Dataverse File\u002FImage form fields, screens should use `FilePicker` \u002F `ImagePicker` from `@microsoft\u002Fpower-apps-native-host` (see [`\u002Fadd-native` File\u002FImage Picker Ownership](..\u002FSKILL.md#fileimage-picker-ownership)). `\u002Fadd-native camera` owns custom camera\u002Fgallery\u002Fscanner workflows, such as a dedicated evidence-capture screen, barcode\u002FQR scan gate, or gallery-selected image that is transformed before saving.\n\n**Pen\u002Fsignature boundary:** signature, sign-off, ink, drawing, or pen capture belongs to `\u002Fadd-native pen-input` (which routes internally to the pen helper). Both camera photos and pen signatures can persist to Dataverse Image\u002FFile columns, but the capture wrappers are separate.\n\n## Workflow\n\n1. Verify project → 2. Verify modules are template-shipped → 2a. Reconcile requested artifacts → 3. Apply camera decision → 3b. Apply scanner decision → 4. Confirm storage decision → 5. Apply upload decision → 6. Verify → 7. Summary\n\n---\n\n### Step 1 — Verify project\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\ntest -f app.config.js && test -f power.config.json && test -f package.json || { echo \"BLOCKED: working_dir is not an initialized app\" >&2; exit 1; }\n```\n\nIf any file is missing, report and STOP — this skill requires an initialized Power Apps mobile app.\n\n### Step 2 — Verify modules are template-shipped\n\nResolve the approved artifact keys before this check. Missing\u002Fambiguous intent\nreturns to the owner (ask standalone); do not assume all artifacts. Substitute\nthe exact JSON list, e.g. `[\"scanner\"]` or `[\"photo\",\"upload\"]`, below:\n\nUse the owner's normalized public capability to resolve those keys:\n\n| Public capability | Artifact keys |\n|---|---|\n| `camera` | `[\"photo\"]` |\n| `image-picker` | `[\"gallery\"]` |\n| `barcode-scanner` | `[\"scanner\"]` |\n| `qr-scanner` | `[\"scanner\"]` |\n\nFor combined approved capabilities use the union. Add `upload` only for an\nexplicitly approved custom Dataverse Image-upload helper, not automatically for\ncapture or scanning. The checker accepts artifact-key JSON, not raw `$ARGUMENTS`\nor a public capability string. Missing\u002Fambiguous intent still returns to the owner.\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnode - '\u003Capproved-artifact-keys-json>' \u003C\u003C'NODE'\nconst p = require('.\u002Fpackage.json');\nconst modules = {\n  photo: 'expo-image-picker',\n  gallery: 'expo-image-picker',\n  scanner: 'expo-camera',\n  upload: 'expo-file-system',\n};\nconst artifacts = JSON.parse(process.argv[2]);\nif (!Array.isArray(artifacts) || artifacts.length === 0\n    || artifacts.some(key => typeof key !== 'string' || !Object.hasOwn(modules, key))) {\n  throw new Error('Expected a nonempty approved artifact list: photo, gallery, scanner, upload');\n}\nconst need = [...new Set(artifacts.map(key => modules[key]))];\nconst missing = need.filter(name => !p.dependencies?.[name]);\nif (missing.length) {\n  console.error('MISSING from package.json: ' + missing.join(', ') + '. Do not install native modules.');\n  process.exit(1);\n}\nconsole.log('OK: requested modules present');\nNODE\n```\n\nIf the check fails, STOP. Print the error verbatim. Do not run `npx expo install`. Do not edit `app.config.js`. Tell the user which requested modules the template lacks; they need template support, not an app-local native-package install.\n\n### Step 2a — Reconcile requested artifacts\n\nRead and execute [native-artifact-compatibility.md](${PLUGIN_ROOT}\u002Fshared\u002Freferences\u002Fnative-artifact-compatibility.md)\nSteps 1–3 for the camera\u002Fgallery\u002Fscanner row before any writes or reuse. Inherit\nthe current approval\u002Fmode from `\u002Fadd-native`; do not repeat a valid scoped approval.\n\nInventory `src\u002Fnative\u002Fcamera.ts` (including gallery `pickImage`, not a required\n`imagePicker.ts`), requested `src\u002Fnative\u002FbarcodeScanner.tsx`, and requested\n`src\u002Fnative\u002FcameraUpload.ts` independently. Check requested exports, options,\nfailure paths, scanner lock\u002Freset\u002Foverlay behavior, and any requested Dataverse\nImage-upload payload semantics. Resolve the approved storage platform and Step 4\nrequirements read-only now, before writes; non-Dataverse retention must not\nrequire Dataverse columns or `cameraUpload.ts`.\nMissing required storage or an incompatible artifact outside approval returns\n`NEEDS_CONTEXT` to the owner, or asks standalone; it is not a successful skip.\n\nSet `SCANNER_NEEDED=yes` only for scanning in the current approved request\u002Fscope\n(`barcode`, `QR`, scanner, or a supplied scan-gate requirement), not unrelated\nmatches in an older plan. A compatible camera file cannot satisfy a missing\nscanner or upload helper. Preserve compatible files unchanged in partial sets.\n\n### Step 3 — Write camera wrapper\n\n**Print only when creating\u002Fupdating the requested camera wrapper:**\n> \"→ Writing src\u002Fnative\u002Fcamera.ts wrapper (takePhoto + pickImage with discriminated-union results)…\"\n\nApply Step 2a's decision for `src\u002Fnative\u002Fcamera.ts`: create if requested and\nmissing, reuse unchanged only if compatible, or make only the approved scoped\nupdate. Scanner-only requests do not require this file. Preserve custom code and\nexisting exports; do not append regeneration comments or replace it with the example.\n\n```typescript\n\u002F\u002F src\u002Fnative\u002Fcamera.ts\n\u002F\u002F Camera capture and image picker wrapper for Power Apps mobile apps.\n\u002F\u002F Uses expo-image-picker for both camera capture and gallery selection.\n\u002F\u002F Capture\u002Fpick functions return discriminated results; permission helpers retain boolean APIs.\n\nimport * as ImagePicker from 'expo-image-picker';\n\n\u002F\u002F --- Result types ---\n\nexport type PhotoResult =\n  | { ok: true; uri: string; width: number; height: number; mimeType?: string; fileSize?: number }\n  | { ok: false; reason: 'permission-denied' | 'cancelled' | 'unsupported' | 'error'; message?: string };\n\n\u002F\u002F --- Permission ---\n\ntype PermissionResult =\n  | { ok: true }\n  | { ok: false; reason: 'permission-denied' | 'unsupported' | 'error'; message?: string };\n\nasync function requestPermission(\n  request?: () => Promise\u003C{ status: string }>,\n): Promise\u003CPermissionResult> {\n  if (typeof request !== 'function') {\n    return { ok: false, reason: 'unsupported', message: 'Permission API is unavailable.' };\n  }\n  try {\n    const { status } = await request();\n    return status === 'granted'\n      ? { ok: true }\n      : { ok: false, reason: 'permission-denied' };\n  } catch (error) {\n    return {\n      ok: false,\n      reason: 'error',\n      message: error instanceof Error ? error.message : 'Permission request failed.',\n    };\n  }\n}\n\nasync function permissionGranted(\n  request?: () => Promise\u003C{ status: string }>,\n): Promise\u003Cboolean> {\n  const permission = await requestPermission(request);\n  if (!permission.ok && permission.reason !== 'permission-denied') {\n    \u002F\u002F Preserve the public boolean API without silently hiding a failed native request.\n    console.warn(`Device permission request failed (${permission.reason}).`);\n  }\n  return permission.ok;\n}\n\nexport async function requestCameraPermission(): Promise\u003Cboolean> {\n  return permissionGranted(ImagePicker.requestCameraPermissionsAsync);\n}\n\nexport async function requestMediaLibraryPermission(): Promise\u003Cboolean> {\n  return permissionGranted(ImagePicker.requestMediaLibraryPermissionsAsync);\n}\n\n\u002F\u002F --- Capture ---\n\n\u002F**\n * Launch the device camera and capture a photo.\n * Missing APIs return 'unsupported'; rejected native calls return 'error'.\n *\u002F\nexport async function takePhoto(options?: {\n  quality?: number;\n  allowsEditing?: boolean;\n}): Promise\u003CPhotoResult> {\n  try {\n    if (typeof ImagePicker.launchCameraAsync !== 'function') {\n      return { ok: false, reason: 'unsupported', message: 'Camera API is unavailable.' };\n    }\n    const permission = await requestPermission(ImagePicker.requestCameraPermissionsAsync);\n    if (!permission.ok) return permission;\n\n    const result = await ImagePicker.launchCameraAsync({\n      mediaTypes: ['images'],\n      quality: options?.quality ?? 0.8,\n      allowsEditing: options?.allowsEditing ?? false,\n      exif: false,\n    });\n\n    if (result.canceled) return { ok: false, reason: 'cancelled' };\n\n    const asset = result.assets[0];\n    return {\n      ok: true,\n      uri: asset.uri,\n      width: asset.width ?? 0,\n      height: asset.height ?? 0,\n      mimeType: asset.mimeType ?? undefined,\n      fileSize: asset.fileSize ?? undefined,\n    };\n  } catch (error) {\n    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Camera capture failed.' };\n  }\n}\n\n\u002F**\n * Open the device photo gallery and pick an image.\n * Works on all platforms including web (uses native file picker).\n *\u002F\nexport async function pickImage(options?: {\n  quality?: number;\n  allowsEditing?: boolean;\n  allowsMultipleSelection?: boolean;\n}): Promise\u003CPhotoResult> {\n  try {\n    if (typeof ImagePicker.launchImageLibraryAsync !== 'function') {\n      return { ok: false, reason: 'unsupported', message: 'Image picker API is unavailable.' };\n    }\n    const permission = await requestPermission(ImagePicker.requestMediaLibraryPermissionsAsync);\n    if (!permission.ok) return permission;\n\n    const result = await ImagePicker.launchImageLibraryAsync({\n      mediaTypes: ['images'],\n      quality: options?.quality ?? 0.8,\n      allowsEditing: options?.allowsEditing ?? false,\n      allowsMultipleSelection: options?.allowsMultipleSelection ?? false,\n      exif: false,\n    });\n\n    if (result.canceled) return { ok: false, reason: 'cancelled' };\n\n    const asset = result.assets[0];\n    return {\n      ok: true,\n      uri: asset.uri,\n      width: asset.width ?? 0,\n      height: asset.height ?? 0,\n      mimeType: asset.mimeType ?? undefined,\n      fileSize: asset.fileSize ?? undefined,\n    };\n  } catch (error) {\n    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Image selection failed.' };\n  }\n}\n```\n\n### Step 3b — Write barcode\u002FQR scanner control when requested\n\n**Skip this step unless `SCANNER_NEEDED=yes`.** Photo-only and gallery-only flows do not need a live `CameraView`.\n\n**Print before starting:**\n> \"→ Writing src\u002Fnative\u002FbarcodeScanner.tsx (CameraView barcode\u002FQR scanner control)…\"\n\nApply Step 2a's independent decision for `src\u002Fnative\u002FbarcodeScanner.tsx`: create\nwhen requested and missing, reuse unchanged only if compatible, or make only the\napproved scoped update. Existence alone does not verify `BarcodeScannerView`,\n`onScanned`, `paused`, `resetKey`, or the requested barcode types.\n\n```tsx\n\u002F\u002F src\u002Fnative\u002FbarcodeScanner.tsx\n\u002F\u002F Barcode \u002F QR scanner control for Power Apps mobile apps.\n\u002F\u002F Uses expo-camera CameraView. Never throws; permission state is rendered inline.\n\nimport React from 'react';\nimport { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';\nimport { CameraView, useCameraPermissions } from 'expo-camera';\nimport type { BarcodeScanningResult, BarcodeType } from 'expo-camera';\n\nexport type ScannerResult = {\n  ok: true;\n  data: string;\n  type: string;\n  raw: BarcodeScanningResult;\n};\n\nexport type BarcodeScannerViewProps = {\n  onScanned: (result: ScannerResult) => void;\n  paused?: boolean;\n  resetKey?: unknown;\n  barcodeTypes?: BarcodeType[];\n  style?: StyleProp\u003CViewStyle>;\n  overlay?: React.ReactNode;\n  children?: React.ReactNode;\n};\n\nconst DEFAULT_BARCODE_TYPES = [\n  'aztec',\n  'qr',\n  'ean13',\n  'ean8',\n  'upc_a',\n  'upc_e',\n  'datamatrix',\n  'code39',\n  'code93',\n  'code128',\n  'pdf417',\n  'itf14',\n  'codabar',\n] as BarcodeType[];\n\nexport function BarcodeScannerView({\n  onScanned,\n  paused = false,\n  resetKey,\n  barcodeTypes = DEFAULT_BARCODE_TYPES,\n  style,\n  overlay,\n  children,\n}: BarcodeScannerViewProps) {\n  const [permission, requestPermission] = useCameraPermissions();\n  const [permissionError, setPermissionError] = React.useState\u003Cstring | null>(null);\n  const permissionAttemptedRef = React.useRef(false);\n  const scanLockedRef = React.useRef(false);\n\n  React.useEffect(() => {\n    if (permission && !permission.granted && permission.canAskAgain && !permissionAttemptedRef.current) {\n      permissionAttemptedRef.current = true;\n      async function requestScannerPermission() {\n        try {\n          await requestPermission();\n        } catch {\n          setPermissionError('Camera permission request failed. Check device settings and reopen the scanner.');\n        }\n      }\n      void requestScannerPermission();\n    }\n  }, [permission, requestPermission]);\n\n  React.useEffect(() => {\n    if (!paused) {\n      scanLockedRef.current = false;\n    }\n  }, [paused, resetKey]);\n\n  const handleBarcodeScanned = React.useCallback((event: BarcodeScanningResult) => {\n    if (paused || scanLockedRef.current) return;\n    scanLockedRef.current = true;\n    onScanned({ ok: true, data: event.data, type: event.type, raw: event });\n  }, [onScanned, paused]);\n\n  if (permissionError) {\n    return \u003CView style={[styles.fallback, style]}>\u003CText>{permissionError}\u003C\u002FText>\u003C\u002FView>;\n  }\n\n  if (!permission) {\n    return \u003CView style={[styles.fallback, style]}>\u003CText>Checking camera permission...\u003C\u002FText>\u003C\u002FView>;\n  }\n\n  if (!permission.granted) {\n    return \u003CView style={[styles.fallback, style]}>\u003CText>Camera permission is required to scan codes.\u003C\u002FText>\u003C\u002FView>;\n  }\n\n  return (\n    \u003CView style={[styles.container, style]}>\n      \u003CCameraView\n        style={StyleSheet.absoluteFill}\n        facing=\"back\"\n        active={!paused}\n        barcodeScannerSettings={{ barcodeTypes }}\n        onBarcodeScanned={paused ? undefined : handleBarcodeScanned}\n      \u002F>\n      {overlay || children ? \u003CView pointerEvents=\"box-none\" style={styles.overlay}>{overlay ?? children}\u003C\u002FView> : null}\n    \u003C\u002FView>\n  );\n}\n\nconst styles = StyleSheet.create({\n  container: { flex: 1, overflow: 'hidden', position: 'relative' },\n  overlay: { ...StyleSheet.absoluteFillObject },\n  fallback: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 },\n});\n```\n\nScanner 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` \u002F `children` as a sibling absolute layer above it.\n\nScan 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.\n\nScan-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.\n\nScanner loading UI rule: when scan processing takes time (lookup\u002Fcreate mutation), render a spinner-only overlay inside the scanner preview via the `overlay` prop. Do not render a separate loading card\u002Fpanel above the camera preview.\n\n### Step 4 — Detect Dataverse image\u002Ffile columns\n\nReuse Step 2a's read-only storage findings; repeat the check only if those files\nchanged. Classify the approved storage destination before searching Dataverse:\n\n| Approved storage scope | Helper action |\n|---|---|\n| No retention \u002F local preview only | Skip the Dataverse search and Step 5; fulfill the requested capture\u002Fscanner artifacts. |\n| Non-Dataverse retention (for example an existing SharePoint document library) or approved local-file retention | Skip the Dataverse search and Step 5. Validate the required capture output\u002Fpayload against the approved destination contract, then return the connector\u002Flocal persistence work to the owner. Missing Dataverse columns are not an error on this path. Do not generate `cameraUpload.ts` or claim persistence is implemented by capture alone. |\n| Dataverse File retention or a normal host File\u002FImage field | Apply the shared storage checks and return the File\u002Fhost-control integration to the owner; do not use the custom Image `update()` helper. A File target is not supported by the Image `update()` example. |\n| Custom Dataverse Image upload helper requested | Inspect the actual approved Image column and generated service signature below, then apply Step 5. |\n| Destination or required payload contract is unknown | Return `NEEDS_CONTEXT` to the owner (ask standalone); do not assume Dataverse or silently drop retention. |\n\nOnly the custom Dataverse Image-helper branch requires this column search:\n\n```text\nGrep pattern=\"ImageColumnName|FileColumnName|UploadColumnName\" path=\"src\u002Fgenerated\u002F\"\n```\n\n**If matches found:** verify the approved target, column type, payload, and write\nresult contract. Continue to Step 5 only for requested custom camera\u002Fgallery\n**Image** retention outside host controls. Mere presence of generated columns does\nnot authorize an upload helper. If the matched target is File rather than the\napproved Image target, return the mismatch to the owner without generating an\nImage helper or claiming File persistence is implemented.\n\nFile\u002FImage host-control safety: this skill does not replace normal Dataverse form controls. Keep host `ImagePicker` \u002F `FilePicker` for standard Dataverse form-bound Image\u002FFile fields.\n\n**If the requested Dataverse Image target is missing\u002Funverified:** return\n`NEEDS_CONTEXT` to the owner, or ask standalone; do not silently deliver a\ncapture-only success for that requested helper. This blocker applies only to\nthe Dataverse Image-helper branch, not an approved connector\u002Flocal destination.\nDo not generate schema\u002Fservices or hand-edit `src\u002Fgenerated\u002F`.\n\n### Step 5 — Write image upload helper\n\n**Print before starting:**\n> \"→ Writing src\u002Fnative\u002FcameraUpload.ts (Dataverse image column base64 patch helper)…\"\n\nApply Step 2a's independent decision for `src\u002Fnative\u002FcameraUpload.ts`: create\nwhen requested and missing, reuse unchanged only if compatible with the approved\nImage target and installed file API, or make only the approved scoped update.\nAn existing Image helper cannot satisfy File retention.\n\nDo **not** generate this helper for normal Dataverse File\u002FImage form fields. Those\nuse host `FilePicker` \u002F `ImagePicker` controls. Generate it only when the current\napproved scope requests custom camera\u002Fgallery Image retention, including an\nexplicit implementation-only upload-helper request; a screen plan is not required\nfor that bounded operation.\n\nThis helper does not change host `ImagePicker` \u002F `FilePicker` behavior. It only covers custom photo-capture flows where the app receives a camera URI and then updates a Dataverse Image column explicitly.\n\nThis 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 }`.\n\nDo **not** convert camera URIs into browser-style `File` \u002F `Blob` objects for this path. That pattern is fragile in RN\u002FExpo runtimes and causes upload failures like `arrayBuffer is not a function`.\n\n```typescript\n\u002F\u002F src\u002Fnative\u002FcameraUpload.ts\n\u002F\u002F Bridges camera\u002Fgallery photo output to Dataverse Image column updates.\n\u002F\u002F Reads local image URI as base64 and PATCHes the image column via service.update().\n\nimport * as FileSystem from 'expo-file-system';\n\nexport type UploadResult =\n  | { ok: true }\n  | { ok: false; reason: 'read-failed' | 'update-failed' | 'error'; message?: string };\n\nexport type ImageUpdateService = {\n  update: (id: string, body: any) => Promise\u003C{ success: boolean; error?: { message?: string } }>;\n};\n\n\u002F**\n * Upload a photo from takePhoto() \u002F pickImage() to a Dataverse Image column.\n *\n * @param uri             - Photo URI from the camera wrapper result\n * @param service         - Generated Dataverse service with update()\n * @param recordId        - The Dataverse record GUID to patch\n * @param imageColumnName - Dataverse Image column logical name\n *\u002F\nexport async function uploadPhotoToImageColumn(\n  uri: string,\n  service: ImageUpdateService,\n  recordId: string,\n  imageColumnName: string,\n): Promise\u003CUploadResult> {\n  try {\n    const base64 = await readUriAsBase64(uri);\n    if (!base64) {\n      return { ok: false, reason: 'read-failed', message: 'Could not read the image URI as base64.' };\n    }\n\n    \u002F\u002F Dataverse Image columns expect base64 payload value (without data URI prefix).\n    const result = await service.update(recordId, { [imageColumnName]: base64 });\n\n    if (!result.success || result.error) {\n      return { ok: false, reason: 'update-failed', message: result.error?.message ?? 'Dataverse image update failed.' };\n    }\n\n    return { ok: true };\n  } catch (error) {\n    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Unable to upload image.' };\n  }\n}\n\n\u002F**\n * Read a local photo URI to a base64 string using Expo file APIs.\n *\u002F\nasync function readUriAsBase64(uri: string): Promise\u003Cstring | null> {\n  try {\n    const fsAny = FileSystem as any;\n    if (typeof fsAny.readAsStringAsync !== 'function') {\n      return null;\n    }\n\n    const normalizedUri = uri.startsWith('file:\u002F\u002F') ? uri : `file:\u002F\u002F${uri}`;\n    const base64 = await fsAny.readAsStringAsync(normalizedUri, {\n      encoding: fsAny.EncodingType?.Base64 ?? 'base64',\n    });\n    if (typeof base64 !== 'string' || base64.length === 0) {\n      return null;\n    }\n\n    return base64;\n  } catch {\n    return null;\n  }\n}\n```\n\n**Important:** The `service` parameter is typed loosely (`ImageUpdateService`) so it works with any generated service that has `update()`. The caller passes the concrete service — this avoids importing a specific generated service in the helper.\n\n**Write contract:** the helper must treat any non-truthy `result.success` as failure even when `result.error` is missing. Generated services return `IOperationResult`, and native app screens must not show success or navigate after any Dataverse write unless `success` is truthy.\n\n**Template\u002FAPI note:** if `readAsStringAsync` is not available from `expo-file-system` in your template version, import from `expo-file-system\u002Flegacy` and keep the same base64 behavior.\n\n### Step 6 — Type-check\n\n**Print before starting:**\n> \"→ Running tsc to verify the requested native artifacts compile (~10–20 seconds).\"\n\n```bash\ncd -- '\u003Cworking_dir>' || { echo \"BLOCKED: cannot enter working_dir\" >&2; exit 1; }\nnpx --no-install tsc --noEmit\n```\n\nFix only in-scope helper errors. Common issues:\n- `readAsStringAsync` not found on `expo-file-system` — switch import to `expo-file-system\u002Flegacy` for this helper.\n- Import path mismatches — verify `src\u002Fnative\u002F` is reachable from screen components.\n\nExecute the shared compatibility contract's Step 4 after type-checking. Recheck\nevery requested camera\u002Fscanner\u002Fupload artifact and storage obligation, including\npermission failures, repeated scan callbacks, URI reads, and false write results.\nType-check success alone is insufficient; return unresolved requirements rather\nthan marking a partial output set complete. Do not fix screen\u002Fgenerated files.\n\n### Step 7 — Summary\n\nReturn the shared compatibility result and actual created\u002Fupdated\u002Freused paths.\nShow only applicable usage examples; no upload success claim when retention is\nunresolved, and no full-feature claim for implementation-only work.\n\n```\nRequested native artifacts ready\n---\nCapabilities               : \u003Cfulfilled requested capabilities only>\nModules (template-shipped) : \u003Crequired modules for those capabilities only>\npackage.json               : unchanged ✓\napp.config.js              : unchanged ✓\nArtifacts                  : \u003Cone row per requested path: created \u002F updated \u002F reused>\n\nType-check                 : \u003Cactual result>\nUI integration             : \u003Creturned to owner \u002F intentionally not performed>\n---\n```\n\nDo not list `camera.ts` for scanner-only or upload-only results. Do not list\nunrequested companion paths as generated, even if they already existed.\n\n**Include this usage only when capture and custom Image upload were fulfilled:**\n\n```text\n  import { takePhoto } from '..\u002Fnative\u002Fcamera';\n  import { uploadPhotoToImageColumn } from '..\u002Fnative\u002FcameraUpload';\n  import { Cr123_inspectionService } from '..\u002Fgenerated\u002Fservices\u002FCr123_inspectionService';\n\n  const result = await takePhoto();\n  if (result.ok) {\n    const upload = await uploadPhotoToImageColumn(\n      result.uri,\n      Cr123_inspectionService,\n      recordId,\n      'cr123_sitephoto'  \u002F\u002F Dataverse Image column logical name\n    );\n    if (upload.ok) {\n      showToast('Photo attached to record');\n    }\n  }\n```\n\n**Include this usage only when gallery picking was fulfilled:**\n\n```text\n  import { pickImage } from '..\u002Fnative\u002Fcamera';\n\n  const result = await pickImage();\n  if (result.ok) {\n    setPreviewUri(result.uri);\n  }\n```\n\n**Include this usage only when scanner support was fulfilled:**\n\n```text\n  import { BarcodeScannerView } from '..\u002Fnative\u002FbarcodeScanner';\n  import { useFocusEffect } from 'expo-router';\n\n  const [paused, setPaused] = useState(false);\n  const [scanResetKey, setScanResetKey] = useState(0);\n\n  useFocusEffect(\n    React.useCallback(() => {\n      setPaused(false);\n      setScanResetKey((value) => value + 1);\n      return () => setPaused(true);\n    }, [])\n  );\n\n  \u003CBarcodeScannerView\n    paused={paused}\n    resetKey={scanResetKey}\n    barcodeTypes={['qr', 'ean13', 'code128']}\n    overlay={\n      \u003C>\n        \u003CScannerFrame \u002F>\n        {isScanning ? \u003CScannerOverlaySpinner \u002F> : null}\n      \u003C\u002F>\n    }\n    onScanned={({ data, type }) => {\n      setPaused(true);\n      handleCode(data, type);\n    }}\n  \u002F>\n```\n\nNo native rebuild or permission\u002Fconfig change is performed. These are JS\nwrapper\u002Fcontrol changes over template-shipped native modules.\n\n## Notes\n\n- This skill never modifies `src\u002FplayerConfig.ts`, `src\u002Fgenerated\u002F`, or any screen file.\n- `takePhoto()` returns `unsupported` for missing APIs and `error` for rejected native calls; neither rejects its promise.\n- Barcode\u002FQR scanning is handled here via `src\u002Fnative\u002FbarcodeScanner.tsx` when requested. Use it for scan gates and lookup flows; do not use it as a replacement for Dataverse File\u002FImage host controls.\n- `cameraUpload.ts` in this skill targets custom Dataverse Image-column capture flows only. It does not replace host `ImagePicker` \u002F `FilePicker` controls for standard Dataverse forms.\n- If Dataverse tables are added later (via `\u002Fadd-dataverse`), re-run `\u002Fadd-native camera` with the approved custom Image-retention requirement. Reconcile all requested artifacts again, reuse compatible wrappers unchanged, and create\u002Fupdate `cameraUpload.ts` only within that scope. This never installs modules.\n",{"name":5,"model":30,"description":7,"allowed-tools":49,"user-invocable":45,"disable-model-invocation":50},"Read, Edit, Write, Grep, Glob, Bash, AskUserQuestion",true,"main","0a55a88fa2855404029d1e5450faf92878baf763","plugins\u002Fmobile-apps\u002Fskills\u002Fadd-native\u002Fadd-camera",229,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills","https:\u002F\u002Fmicrosoft.github.io\u002Fpower-platform-skills\u002F","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-13 01:33:52",{"source":8,"skillName":5,"commands":62,"prompt":72,"zipAvailable":50},[63,66,69],{"tool":64,"command":65},"npx","npx skills add microsoft\u002Fpower-platform-skills --skill add-camera",{"tool":67,"command":68},"bunx","bunx skills add microsoft\u002Fpower-platform-skills --skill add-camera",{"tool":70,"command":71},"pnpm","pnpm dlx skills add microsoft\u002Fpower-platform-skills --skill add-camera","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fmobile-apps\u002Fskills\u002Fadd-native\u002Fadd-camera\u002FSKILL.md",[],[75,95,117,135,149,159],{"skillNo":76,"name":77,"nameZh":6,"description":78,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":79,"categories":80,"keywords":81,"publishedAt":44},"01M699WVHX0XN59EPBTTPEKYPG","setup","Use when Process Intelligence needs building, explicit cloud\u002Faccount\u002Fenvironment configuration, sign-in, connection diagnostics, or recovery from an authentication error.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fsetup\u002FSKILL.md",[],[77,40,82,83,84,85,86,87,88,89,90,91,92,93,94,41],"when","connection","error","sign","intelligence","configuration","use","process","diagnostics","environment","authentication","cloud","recovery",{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":106,"publishedAt":44},"01M699WSR9EX55YY1WD9J22FEG","report-issue","Use when the user wants a Process Intelligence bug-report draft or support summary for a failed operation.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Freport-issue\u002FSKILL.md",[101],{"categoryNo":102,"categoryName":103,"taxonomyType":104,"taxonomyTypeDesc":105},"01JBSEEDCAT000000000000034","行政与办公人员","PROFESSION","适用职业",[107,108,109,110,111,89,88,112,113,86,114,115,28,82,116],"report issue","report","issue","support","user","bug","summary","draft","operation","bug-report",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":123,"publishedAt":44},"01M699WR26YQ9Q21KE6MJF5B2G","investigate-process","Use when a broad Process Mining business question, observed pattern or competing explanation needs investigation across analytical perspectives.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Finvestigate-process\u002FSKILL.md",[],[89,124,125,126,82,127,128,129,130,40,131,88,132,133,134],"investigate","investigate process","mining","process mining","investigation","perspectives","question","business","pattern","across","analytical",{"skillNo":136,"name":137,"nameZh":6,"description":138,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":139,"categories":140,"keywords":141,"publishedAt":44},"01M699WP9SDG5ZR12P8VYNXKTN","derive-metric","Use when a requested measure is missing, a saved custom metric needs reuse, or an ephemeral formula or derived scalar is needed.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fderive-metric\u002FSKILL.md",[],[142,143,40,88,144,145,146,147,148,82],"metric","derive metric","formula","missing","reuse","saved","custom",{"skillNo":150,"name":151,"nameZh":6,"description":152,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":153,"categories":154,"keywords":155,"publishedAt":44},"01M699WMG5R8P6SMTDAS1TKF9E","compare-cohorts","Use when comparing process cohorts, departments, periods or before-and-after results under matched definitions.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fcompare-cohorts\u002FSKILL.md",[],[156,157,82,88,89,158,40],"compare","compare cohorts","results",{"skillNo":160,"name":161,"nameZh":6,"description":162,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":163,"categories":164,"keywords":165,"publishedAt":44},"01M699WJWNVTMP9XK3NCJHV14D","analyze-variants","Use when investigating common or slow variants, repeated work, unusual paths, or a specific Process Mining case.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fpower-platform-skills\u002Fblob\u002Fmain\u002Fplugins\u002Fprocess-intelligence\u002Fskills\u002Fanalyze-variants\u002FSKILL.md",[],[166,167,168,127,82,126,169,170,88,89,40,171,172],"analyze","variants","analyze variants","case","paths","work","common"]