mcpField
A
add-camera

add-camera

github.com/microsoft開発1.1k126 回インストール今日更新
リポジトリ

add-camera について

add-camera は Power Apps モバイルアプリ向けの内部スキルで、カメラや画像ピッカーのラッパー、任意のバーコード/QR スキャナー、Dataverse 画像アップロード用の補助コードを生成します。`src/native/` 配下の JS ファイルのみを書き込み、必要なネイティブモジュールがテンプレートに含まれていることを求めます。

主な機能

  1. 01カメラ撮影とギャラリー選択のラッパー生成
  2. 02任意のバーコード/QR スキャナー生成
  3. 03任意の Dataverse 画像アップロード補助コード生成
  4. 04`src/native/` 配下の JS ファイルのみを書き込み

活用シーン

  1. 01アプリにカメラ撮影やギャラリー選択を追加
  2. 02バーコードまたは QR のスキャンゲートを構築
  3. 03カスタム Dataverse 画像列へのアップロード補助コードを準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/microsoft/power-platform-skills --skill add-camera

インストール後、Claude Code で/add-cameraを入力して読み込まれたか確認します。

詳細

共通の指示:shared-instructions.md — 最初にお読みください。

作業ディレクトリ: プロジェクトの読み取りやコマンドの実行を行う前に、 native-artifact-compatibility.md Step 0を実行してください。 /add-nativeで解決された絶対パスのworking_dirを引き継ぎ、このヘルパーが別のディレクトリから起動された場合でも、すべてのシェル呼び出しとファイルツールをそのディレクトリにバインドしてください。

参照資料:

カメラの追加

内部ヘルパーです。 ユーザーは/add-native camera、/add-native image-picker、/add-native barcode-scanner、または/add-native qr-scannerを呼び出してください。/add-nativeは機能を特定した後、ここに処理を振り分けます。

型定義付きのカメラと画像ピッカーのラッパー、オプションのバーコード/QRスキャナーコントロール、およびDataverseの画像/ファイルのワークフロー向けのオプションのカスタムアップロード手順を生成します。

このスキルが書き込むのはsrc/native/配下のJSファイルのみです。モジュールのインストールやpackage.json/app.config.jsの変更は行いません。以下の承認済みアーティファクトセットで必要なモジュールは、すでにテンプレート/ネイティブビルドに含まれている必要があります。必須モジュールがない場合は、処理を中止してください。スキャナーのみ、またはアップロードのみのリクエストでは、無関係な撮影パッケージを要求しないでください。

理由:顧客向けのバイナリは、顧客のpackage.jsonからではなく、ビルド済みのリラップベースから作成されます。ここでネイティブモジュールを追加すると、実際にはバイナリに含まれていないモジュールに対してコンパイルされ、リラップ後に実行時クラッシュが発生します。同じ厳格なルールについては、/add-nativeを参照してください。

必須モジュールは承認済みアーティファクトセットによって異なります(すべてpackage.jsonにすでに含まれている必要があります)。

リクエストされたアーティファクトキー必須モジュール出力
photo / galleryexpo-image-pickercamera.ts 撮影/ピッカーAPI
scannerexpo-camerabarcodeScanner.tsx ライブスキャナー
uploadexpo-file-systemcameraUpload.ts カスタムImageヘルパー

複数のアーティファクトをまとめてリクエストする場合は、その和集合を使用してください。スキャナーのみのリクエストではexpo-image-pickerやcamera.tsは不要です。アップロードのみのリクエストでは撮影/スキャナーパッケージは不要です。

DataverseのFile/Imageの境界: 通常のDataverse File/Imageフォームフィールドでは、画面で@microsoft/power-apps-native-hostのFilePicker/ImagePickerを使用してください(/add-nativeのFile/Imageピッカーの責任範囲を参照)。/add-native cameraが扱うのは、専用の証拠撮影画面、バーコード/QRスキャンによるゲート、保存前に変換するギャラリー選択画像などのカスタムカメラ/ギャラリー/スキャナーのワークフローです。

ペン/署名の境界: 署名、確認サイン、インク、描画、ペンによる撮影は/add-native pen-inputの対象です(内部でペン用ヘルパーに振り分けられます)。カメラ写真とペンによる署名はどちらもDataverseのImage/File列に保存できますが、撮影ラッパーは別々です。

ワークフロー

  1. プロジェクトを確認 → 2. モジュールがテンプレートに含まれていることを確認 → 2a. リクエストされたアーティファクトを照合 → 3. カメラの判断を適用 → 3b. スキャナーの判断を適用 → 4. ストレージの判断を確認 → 5. アップロードの判断を適用 → 6. 検証 → 7. 要約

Step 1 — プロジェクトの確認
cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
test -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; }

ファイルが1つでも不足している場合は、その旨を報告して処理を中止してください。このスキルを使用するには、Power Appsモバイルアプリが初期化されている必要があります。

ステップ 2 — モジュールがテンプレートに含まれていることを確認する

この確認の前に、承認済みのアーティファクトキーを解決してください。不足または曖昧な意図はオーナーに差し戻します(単独で実行する場合は質問してください)。すべてのアーティファクトが対象だと仮定しないでください。以下に、正確な JSON リストを代入します。例:["scanner"] または ["photo","upload"]。

オーナーが正規化した公開機能を使って、キーを解決してください。

公開機能アーティファクトキー
camera["photo"]
image-picker["gallery"]
barcode-scanner["scanner"]
qr-scanner["scanner"]

承認済みの機能を組み合わせる場合は、キーの和集合を使用します。upload は、Dataverse Image のアップロード用カスタムヘルパーが明示的に承認されている場合にのみ追加してください。撮影またはスキャンのために自動的に追加してはいけません。チェッカーが受け付けるのはアーティファクトキーの JSON であり、生の $ARGUMENTS や公開機能の文字列ではありません。不足または曖昧な意図は、引き続きオーナーに差し戻します。

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
node - '<approved-artifact-keys-json>' <<'NODE'
const p = require('./package.json');
const modules = {
  photo: 'expo-image-picker',
  gallery: 'expo-image-picker',
  scanner: 'expo-camera',
  upload: 'expo-file-system',
};
const artifacts = JSON.parse(process.argv[2]);
if (!Array.isArray(artifacts) || artifacts.length === 0
    || artifacts.some(key => typeof key !== 'string' || !Object.hasOwn(modules, key))) {
  throw new Error('Expected a nonempty approved artifact list: photo, gallery, scanner, upload');
}
const need = [...new Set(artifacts.map(key => modules[key]))];
const missing = need.filter(name => !p.dependencies?.[name]);
if (missing.length) {
  console.error('MISSING from package.json: ' + missing.join(', ') + '. Do not install native modules.');
  process.exit(1);
}
console.log('OK: requested modules present');
NODE

確認に失敗した場合は、停止してください。エラーをそのまま表示してください。npx expo install を実行してはいけません。app.config.js を編集してはいけません。テンプレートに要求されたモジュールがないことをユーザーに伝えてください。必要なのはテンプレートのサポートであり、アプリローカルのネイティブパッケージのインストールではありません。

ステップ 2a — 要求されたアーティファクトを照合する

書き込みや再利用を行う前に、native-artifact-compatibility.md のステップ 1〜3 を読み、カメラ/ギャラリー/スキャナーの行について実行してください。/add-native から現在の承認とモードを引き継ぎ、有効なスコープ付き承認を繰り返し求めないでください。

src/native/camera.ts(必須の imagePicker.ts ではなく、ギャラリーの pickImage を含む)、要求された src/native/barcodeScanner.tsx、要求された src/native/cameraUpload.ts をそれぞれ個別に調査してください。要求されたエクスポート、オプション、失敗時の処理、スキャナーのロック/リセット/オーバーレイの動作、および要求された Dataverse Image アップロードのペイロードの意味を確認してください。承認済みのストレージプラットフォームとステップ 4 の要件を、書き込み前に読み取り専用で解決してください。Dataverse 以外の保持で Dataverse の列や cameraUpload.ts が必要になってはいけません。 必須のストレージが不足している場合、または承認範囲外のアーティファクトに互換性がない場合は、NEEDS_CONTEXT をオーナーに返すか、単独で実行する場合は質問してください。これは正常なスキップではありません。

SCANNER_NEEDED=yes に設定するのは、現在の承認済みリクエスト/スコープでスキャンが対象の場合(barcode、QR、スキャナー、または提供されたスキャンゲート要件)のみにしてください。以前のプランにある無関係な一致を根拠にしてはいけません。互換性のあるカメラファイルがあっても、不足しているスキャナーやアップロードヘルパーの代わりにはなりません。一部のみがそろっている場合は、互換性のあるファイルを変更せずに保持してください。

ステップ 3 — カメララッパーを書き込む

要求されたカメララッパーを新規作成または更新する場合にのみ表示してください:

"→ src/native/camera.ts ラッパーを作成中(判別共用体の結果を返す takePhoto + pickImage)…"

src/native/camera.ts については、ステップ 2a の判断を適用してください。要求されていて存在しない場合は作成し、互換性がある場合に限り変更せず再利用するか、承認済みのスコープに限って更新してください。スキャナーのみのリクエストでは、このファイルは不要です。カスタムコードと既存のエクスポートを保持してください。再生成コメントを追加したり、例で置き換えたりしてはいけません。

// 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.
// Capture/pick functions return discriminated results; permission helpers retain boolean APIs.

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 ---

type PermissionResult =
  | { ok: true }
  | { ok: false; reason: 'permission-denied' | 'unsupported' | 'error'; message?: string };

async function requestPermission(
  request?: () => Promise<{ status: string }>,
): Promise<PermissionResult> {
  if (typeof request !== 'function') {
    return { ok: false, reason: 'unsupported', message: 'Permission API is unavailable.' };
  }
  try {
    const { status } = await request();
    return status === 'granted'
      ? { ok: true }
      : { ok: false, reason: 'permission-denied' };
  } catch (error) {
    return {
      ok: false,
      reason: 'error',
      message: error instanceof Error ? error.message : 'Permission request failed.',
    };
  }
}

async function permissionGranted(
  request?: () => Promise<{ status: string }>,
): Promise<boolean> {
  const permission = await requestPermission(request);
  if (!permission.ok && permission.reason !== 'permission-denied') {
    // Preserve the public boolean API without silently hiding a failed native request.
    console.warn(`Device permission request failed (${permission.reason}).`);
  }
  return permission.ok;
}

export async function requestCameraPermission(): Promise<boolean> {
  return permissionGranted(ImagePicker.requestCameraPermissionsAsync);
}

export async function requestMediaLibraryPermission(): Promise<boolean> {
  return permissionGranted(ImagePicker.requestMediaLibraryPermissionsAsync);
}

// --- Capture ---

/**
 * Launch the device camera and capture a photo.
 * Missing APIs return 'unsupported'; rejected native calls return 'error'.
 */
export async function takePhoto(options?: {
  quality?: number;
  allowsEditing?: boolean;
}): Promise<PhotoResult> {
  try {
    if (typeof ImagePicker.launchCameraAsync !== 'function') {
      return { ok: false, reason: 'unsupported', message: 'Camera API is unavailable.' };
    }
    const permission = await requestPermission(ImagePicker.requestCameraPermissionsAsync);
    if (!permission.ok) return permission;

    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 (error) {
    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Camera capture failed.' };
  }
}

/**
 * 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> {
  try {
    if (typeof ImagePicker.launchImageLibraryAsync !== 'function') {
      return { ok: false, reason: 'unsupported', message: 'Image picker API is unavailable.' };
    }
    const permission = await requestPermission(ImagePicker.requestMediaLibraryPermissionsAsync);
    if (!permission.ok) return permission;

    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 (error) {
    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Image selection failed.' };
  }
}
ステップ 3b — 必要な場合にバーコード/QR スキャナーの制御を記述する

SCANNER_NEEDED=yes でない場合は、このステップをスキップしてください。 写真のみ、またはギャラリーのみのフローには、ライブの CameraView は不要です。

開始前に出力してください:

「→ src/native/barcodeScanner.tsx を記述しています(CameraView のバーコード/QR スキャナー制御)…」

src/native/barcodeScanner.tsx について、ステップ 2a の個別の判断を適用してください。要求されていてファイルが存在しない場合は作成し、互換性がある場合に限り変更せず再利用し、または承認された範囲の更新のみを行います。ファイルが存在するだけでは、BarcodeScannerView、onScanned、paused、resetKey、または要求されたバーコードの種類が備わっていることの確認にはなりません。

// 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 [permissionError, setPermissionError] = React.useState<string | null>(null);
  const permissionAttemptedRef = React.useRef(false);
  const scanLockedRef = React.useRef(false);

  React.useEffect(() => {
    if (permission && !permission.granted && permission.canAskAgain && !permissionAttemptedRef.current) {
      permissionAttemptedRef.current = true;
      async function requestScannerPermission() {
        try {
          await requestPermission();
        } catch {
          setPermissionError('Camera permission request failed. Check device settings and reopen the scanner.');
        }
      }
      void requestScannerPermission();
    }
  }, [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 (permissionError) {
    return <View style={[styles.fallback, style]}><Text>{permissionError}</Text></View>;
  }

  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 },
});

スキャナーの描画ルール:オーバーレイ UI を CameraView の子要素として配置しないでください。React の子要素をネイティブカメラプレビュー内にネストすると、Expo Camera の描画が正しく行われないことがあります。生成される制御コンポーネントでは、カメラを 1 つのレイヤーとして描画し、overlay/children はその上に兄弟要素として配置される絶対位置指定のレイヤーに描画します。

スキャンのミューテーションルール:生成される制御コンポーネントには、onBarcodeScanned のコールバックが短時間に複数回呼び出されても二重送信されないよう、内部にワンショットのスキャンロックがあります。それでも、画面側では遷移またはデータのミューテーション前に paused=true を設定し、画面にフォーカスが戻った際に paused=false に戻して resetKey を変更してください。これにより、スキャン成功後に戻ってきた際も、スキャナーが確実に動作します。

スキャンゲートのビジネスルール:QR コードの検索フローでは、まずスキャンしたコードを対象エンティティ(たとえば Test Item)に照合します。検索に失敗した場合は、分かりやすい Item does not exist メッセージをインライン表示し、スキャナーフローはその場にとどめてください。承認済みの計画でフォールバック動作が明示的に求められていない限り、スキャンした不明な項目を自動作成しないでください。

スキャナーの読み込み UI ルール:スキャン処理(検索/作成ミューテーション)に時間がかかる場合は、overlay プロパティを使って、スキャナープレビュー内にスピナーのみのオーバーレイを表示してください。カメラプレビューの上に、別個の読み込みカード/パネルを表示しないでください。

Step 4 — Dataverse の画像/ファイル列を検出する

Step 2a の読み取り専用ストレージ調査の結果を再利用してください。対象ファイルが変更された場合にのみ、確認を繰り返します。Dataverse を検索する前に、承認済みの保存先を分類してください。

承認済みの保存範囲ヘルパーの対応
保持なし/ローカルプレビューのみDataverse の検索と Step 5 を省略し、依頼されたキャプチャ/スキャナー成果物を作成します。
Dataverse 以外での保持(既存の SharePoint ドキュメントライブラリなど)または承認済みのローカルファイル保持Dataverse の検索と Step 5 を省略します。必要なキャプチャ出力/ペイロードが承認済みの保存先の契約に適合することを検証してから、コネクター/ローカル永続化の作業をオーナーに返します。この経路では、Dataverse の列が見つからなくてもエラーではありません。cameraUpload.ts を生成したり、キャプチャだけで永続化が実装済みだと主張したりしないでください。
Dataverse File での保持、または通常のホスト File/Image フィールド共通のストレージ確認を適用し、File/ホストコントロールの統合をオーナーに返します。カスタム Image update() ヘルパーは使用しないでください。File ターゲットは Image update() の例ではサポートされていません。
カスタム Dataverse Image アップロードヘルパーの依頼実際に承認された Image 列と、以下の生成済みサービスのシグネチャを確認してから、Step 5 を適用します。
保存先または必要なペイロードの契約が不明NEEDS_CONTEXT をオーナーに返します(単独で作業する場合は確認してください)。Dataverse を前提としたり、保持要件を黙って無視したりしないでください。

列検索が必要なのは、カスタム Dataverse Image ヘルパーの分岐のみです。

Grep pattern="ImageColumnName|FileColumnName|UploadColumnName" path="src/generated/"

一致する列が見つかった場合: 承認済みのターゲット、列の種類、ペイロード、書き込み結果の契約を確認します。ホストコントロール外でカスタムのカメラ/ギャラリー Image を保持する依頼の場合のみ、Step 5 に進みます。生成済みの列が存在するだけでは、アップロードヘルパーの使用は承認されません。一致したターゲットが承認済みの Image ターゲットではなく File の場合、Image ヘルパーを生成したり、File の永続化が実装済みだと主張したりせず、不一致をオーナーに返します。

File/Image のホストコントロールに関する安全性:このスキルは、通常の Dataverse フォームコントロールに代わるものではありません。標準の Dataverse フォームにバインドされた Image/File フィールドには、ホストの ImagePicker/FilePicker を使用してください。

依頼された Dataverse Image ターゲットが見つからない、または検証できない場合: NEEDS_CONTEXT をオーナーに返すか、単独で作業する場合は確認してください。依頼されたヘルパーについて、キャプチャのみで成功したと黙って回答しないでください。このブロッカーが適用されるのは Dataverse Image ヘルパーの分岐のみであり、承認済みのコネクター/ローカル保存先には適用されません。スキーマ/サービスを生成したり、src/generated/ を手動編集したりしないでください。

ステップ 5 — 画像アップロードヘルパーを作成する

開始前に出力:

「→ src/native/cameraUpload.ts(Dataverse の画像列に base64 をパッチするヘルパー)を作成しています…」

src/native/cameraUpload.ts について、ステップ 2a の個別判断を適用します。要求されていてファイルが存在しない場合は作成し、承認済みの画像ターゲットとインストール済みのファイル API に互換性がある場合に限り変更せず再利用するか、承認済みの範囲内でのみ更新してください。既存の Image ヘルパーでは、File の保持要件を満たせません。

通常の Dataverse File/Image フォームフィールド用に、このヘルパーを生成しないでください。それらにはホストの FilePicker / ImagePicker コントロールを使用します。カスタムのカメラ/ギャラリー画像の保持が現在の承認済みスコープで要求されている場合に限り生成してください。明示的な実装のみのアップロードヘルパー要求も含まれます。この限定された操作には画面計画は必要ありません。

このヘルパーは、ホストの ImagePicker / FilePicker の動作を変更しません。カメラ URI をアプリが受け取り、その後 Dataverse の Image 列を明示的に更新するカスタムの写真撮影フローのみを対象とします。

このヘルパーは、ネイティブアプリの Dataverse Image 列用です。Expo のファイル API でローカルの写真 URI を読み取り、base64 に変換してから、生成済みサービスの update() を { [imageColumnName]: base64 } で呼び出します。

この処理では、カメラ URI をブラウザー形式の File / Blob オブジェクトに変換しないでください。このパターンは RN/Expo ランタイムでは不安定で、arrayBuffer is not a function のようなアップロード失敗を引き起こします。

// 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 = {
  update: (id: string, body: any) => Promise<{ success: boolean; error?: { message?: string } }>;
};

/**
 * Upload a photo from takePhoto() / pickImage() to a Dataverse Image column.
 *
 * @param uri             - Photo URI from the camera wrapper result
 * @param service         - Generated Dataverse service with update()
 * @param recordId        - The Dataverse record GUID to patch
 * @param imageColumnName - Dataverse Image column logical name
 */
export async function uploadPhotoToImageColumn(
  uri: string,
  service: ImageUpdateService,
  recordId: string,
  imageColumnName: string,
): Promise<UploadResult> {
  try {
    const base64 = await readUriAsBase64(uri);
    if (!base64) {
      return { ok: false, reason: 'read-failed', message: 'Could not read the image URI as base64.' };
    }

    // Dataverse Image columns expect base64 payload value (without data URI prefix).
    const result = await service.update(recordId, { [imageColumnName]: base64 });

    if (!result.success || result.error) {
      return { ok: false, reason: 'update-failed', message: result.error?.message ?? 'Dataverse image update failed.' };
    }

    return { ok: true };
  } catch (error) {
    return { ok: false, reason: 'error', message: error instanceof Error ? error.message : 'Unable to upload image.' };
  }
}

/**
 * Read a local photo URI to a base64 string using Expo file APIs.
 */
async function readUriAsBase64(uri: string): Promise<string | null> {
  try {
    const fsAny = FileSystem as any;
    if (typeof fsAny.readAsStringAsync !== 'function') {
      return null;
    }

    const normalizedUri = uri.startsWith('file://') ? uri : `file://${uri}`;
    const base64 = await fsAny.readAsStringAsync(normalizedUri, {
      encoding: fsAny.EncodingType?.Base64 ?? 'base64',
    });
    if (typeof base64 !== 'string' || base64.length === 0) {
      return null;
    }

    return base64;
  } catch {
    return null;
  }
}

重要: service パラメーターは、update() を持つ任意の生成済みサービスで動作するよう、型を緩く定義した ImageUpdateService です。呼び出し元が具体的なサービスを渡すため、ヘルパー内で特定の生成済みサービスをインポートする必要はありません。

書き込みに関する契約: result.error が存在しない場合でも、result.success が truthy でなければ、ヘルパーは失敗として扱う必要があります。生成済みサービスは IOperationResult を返します。ネイティブアプリの画面は、success が truthy でない限り、Dataverse への書き込み後に成功を表示したり、画面遷移したりしてはいけません。

テンプレート/API に関する注意: 使用しているテンプレートのバージョンで expo-file-system から readAsStringAsync を利用できない場合は、expo-file-system/legacy からインポートし、同じ base64 の動作を維持してください。

ステップ 6 — 型チェック

開始前に出力:

「→ 要求されたネイティブ成果物をコンパイルできることを確認するため、tsc を実行しています(約 10~20 秒)。」

cd -- '<working_dir>' || { echo "BLOCKED: cannot enter working_dir" >&2; exit 1; }
npx --no-install tsc --noEmit

スコープ内のヘルパーのエラーのみ修正してください。よくある問題:

  • expo-file-system に readAsStringAsync が見つからない — このヘルパーのインポート先を expo-file-system/legacy に変更してください。
  • インポートパスの不一致 — 画面コンポーネントから src/native/ にアクセスできることを確認してください。

型チェック後、共有互換性契約のステップ 4 を実行してください。権限エラー、スキャンコールバックの重複、URI の読み取り、false の書き込み結果を含め、要求されたカメラ/スキャナー/アップロード成果物とストレージ要件をすべて再確認してください。型チェックの成功だけでは不十分です。未解決の要件がある場合は、部分的な成果物セットを完成扱いにせず、未解決として返してください。画面ファイルや生成ファイルは修正しないでください。

ステップ 7 — 概要

共有互換性の結果と、実際に作成/更新/再利用したパスを返してください。該当する使用例のみを示してください。保持が未解決の場合はアップロード成功を主張せず、実装のみの作業では全機能対応を主張しないでください。

Requested native artifacts ready
---
Capabilities               : <fulfilled requested capabilities only>
Modules (template-shipped) : <required modules for those capabilities only>
package.json               : unchanged ✓
app.config.js              : unchanged ✓
Artifacts                  : <one row per requested path: created / updated / reused>

Type-check                 : <actual result>
UI integration             : <returned to owner / intentionally not performed>
---

スキャナーのみ、またはアップロードのみの結果では、camera.ts を記載しないでください。既に存在していても、要求されていない関連パスを生成済みとして記載しないでください。

撮影とカスタム Image アップロードの両方が完了した場合に限り、次の使用例を含めてください:

  import { takePhoto } from '../native/camera';
  import { uploadPhotoToImageColumn } from '../native/cameraUpload';
  import { Cr123_inspectionService } from '../generated/services/Cr123_inspectionService';

  const result = await takePhoto();
  if (result.ok) {
    const upload = await uploadPhotoToImageColumn(
      result.uri,
      Cr123_inspectionService,
      recordId,
      'cr123_sitephoto'  // Dataverse Image column logical name
    );
    if (upload.ok) {
      showToast('Photo attached to record');
    }
  }

ギャラリーからの選択が完了した場合に限り、次の使用例を含めてください:

  import { pickImage } from '../native/camera';

  const result = await pickImage();
  if (result.ok) {
    setPreviewUri(result.uri);
  }

スキャナー対応が完了した場合に限り、次の使用例を含めてください:

  import { BarcodeScannerView } from '../native/barcodeScanner';
  import { useFocusEffect } from 'expo-router';

  const [paused, setPaused] = useState(false);
  const [scanResetKey, setScanResetKey] = useState(0);

  useFocusEffect(
    React.useCallback(() => {
      setPaused(false);
      setScanResetKey((value) => value + 1);
      return () => setPaused(true);
    }, [])
  );

  <BarcodeScannerView
    paused={paused}
    resetKey={scanResetKey}
    barcodeTypes={['qr', 'ean13', 'code128']}
    overlay={
      <>
        <ScannerFrame />
        {isScanning ? <ScannerOverlaySpinner /> : null}
      </>
    }
    onScanned={({ data, type }) => {
      setPaused(true);
      handleCode(data, type);
    }}
  />

ネイティブの再ビルドや権限/設定の変更は行いません。これらは、テンプレートに同梱されているネイティブモジュール上での JS ラッパー/コントロールの変更です。

注意事項

  • このスキルは、src/playerConfig.ts、src/generated/、または画面ファイルを変更しません。
  • takePhoto() は、API が存在しない場合は unsupported を、ネイティブ呼び出しが拒否された場合は error を返します。どちらの場合も、Promise は reject されません。
  • バーコード/QR コードのスキャンは、要求された場合に src/native/barcodeScanner.tsx を介してここで処理します。スキャンゲートや検索フローにはこれを使用してください。Dataverse の File/Image ホストコントロールの代替として使用しないでください。
  • このスキルの cameraUpload.ts は、カスタムの Dataverse Image 列へのキャプチャフロー専用です。標準の Dataverse フォームにあるホストの ImagePicker/FilePicker コントロールの代替ではありません。
  • 後から Dataverse テーブルを追加する場合(/add-dataverse 経由)、承認済みのカスタム Image 保持要件に基づいて /add-native camera を再実行してください。要求されたすべての成果物を再度照合し、互換性のあるラッパーは変更せずに再利用し、cameraUpload.ts の作成または更新はその範囲内でのみ行ってください。モジュールがインストールされることはありません。

コメント