From 67c876ab7eebdee5531b4cb80c8126fe67a2e71b Mon Sep 17 00:00:00 2001 From: Nikhil Agrawal Date: Thu, 20 Aug 2026 16:08:37 +0530 Subject: [PATCH] feat(mobile-apps): support Expo haptics Ship the SDK-compatible Expo haptics module in the mobile template and replace the prior runtime ban with typed wrapper, planning, and screen usage guidance. Add regression coverage for the native dependency allowlist and document the required base-binary rebuild when the template dependency changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: abc336d1-3089-441a-88de-50bc7140e2a9 --- plugins/mobile-apps/AGENTS.md | 2 +- plugins/mobile-apps/README.md | 5 +- .../mobile-apps/agents/native-app-planner.md | 4 +- plugins/mobile-apps/agents/screen-builder.md | 6 +- plugins/mobile-apps/agents/screen-planner.md | 2 +- .../tests/validate-package-deps.test.js | 14 +++ .../shared/references/screen-templates.md | 2 +- .../mobile-apps/skills/add-native/SKILL.md | 102 ++++++++++++++++-- plugins/mobile-apps/template/package.json | 1 + 9 files changed, 122 insertions(+), 16 deletions(-) diff --git a/plugins/mobile-apps/AGENTS.md b/plugins/mobile-apps/AGENTS.md index 115b2b662..ebbd65623 100644 --- a/plugins/mobile-apps/AGENTS.md +++ b/plugins/mobile-apps/AGENTS.md @@ -48,7 +48,7 @@ Do not add preparation rewrites for `scheme`, `package`, `bundleIdentifier`, `sr ## Guiding Principles 1. **Connector-first for data** — All Power Platform data access goes through connectors and generated services in `src/generated/`. No direct Graph / Azure REST calls. -2. **Native code is allowlist-bounded; pure JavaScript is app-scoped.** Expo modules and packages that ship native source, a podspec, codegen configuration, an Expo module/config plugin, or platform projects must already exist in `template/package.json`. The rewrap binary is built from a pre-built base, so adding those packages to an app cannot add their native code. Do not classify a package from its name alone: a `react-native-*` package can still be pure JavaScript. For an explicit library request or an approved use case that benefits from an established library, the planner may select a compatible pure-JavaScript package, pin it in the app's `package.json`, and install it before builders use it; no Android/iOS rebuild is required. Do not bundle optional libraries such as `react-native-calendars` in the base template. Follow [`shared/references/javascript-dependency-planning.md`](shared/references/javascript-dependency-planning.md). `expo-haptics` remains runtime-banned even if it appears in a future template (see [`agents/screen-builder.md`](agents/screen-builder.md) HARD RULE). The native boundary and reconciliation rule are in [`skills/add-native/SKILL.md`](skills/add-native/SKILL.md). +2. **Native code is allowlist-bounded; pure JavaScript is app-scoped.** Expo modules and packages that ship native source, a podspec, codegen configuration, an Expo module/config plugin, or platform projects must already exist in `template/package.json`. The rewrap binary is built from a pre-built base, so adding those packages to an app cannot add their native code. Do not classify a package from its name alone: a `react-native-*` package can still be pure JavaScript. For an explicit library request or an approved use case that benefits from an established pure-JavaScript package, the planner may select a compatible version, pin it in the app's `package.json`, and install it before builders use it; no Android/iOS rebuild is required. Do not bundle optional libraries such as `react-native-calendars` in the base template. Follow [`shared/references/javascript-dependency-planning.md`](shared/references/javascript-dependency-planning.md). `expo-haptics` is template-shipped: add haptic behavior through `/add-native haptics`, keep native calls in `src/native/haptics.ts`, and pair tactile feedback with visible UI. When a template/base maintainer adds or changes a native dependency version, the Android and iOS base binaries must be rebuilt before release; generated apps based on that rebuilt template do not install or link it again. The native boundary and reconciliation rule are in [`skills/add-native/SKILL.md`](skills/add-native/SKILL.md). 3. **Fresh-template mode** — `/create-mobile-app` validates and prepares an existing fresh Expo standalone template working directory. Do not silently copy the bundled `template/` snapshot over the user's folder. 4. **Safety guardrails** — Confirm before deploys, before global installs, before edits outside the project root. 5. **Memory bank** — Persist `memory-bank.md` in the project root. diff --git a/plugins/mobile-apps/README.md b/plugins/mobile-apps/README.md index 366fdd47f..23c4b396b 100644 --- a/plugins/mobile-apps/README.md +++ b/plugins/mobile-apps/README.md @@ -190,9 +190,10 @@ For other capabilities (only those actually shipped by the template): > /add-native secure-store # expo-secure-store wrapper > /add-native file-system # expo-file-system wrapper > /add-native sharing # expo-sharing wrapper +> /add-native haptics # expo-haptics impact, selection, and notification wrapper ``` -Native modules are allowlist-bound by the current template `package.json`. If the relevant package is present and not runtime-banned, `/add-native` can use it through the proper wrapper or host control. If the package is absent, the skill does not install it or fake support; it adds a transparency note and stops for that capability. For example, push notifications require `expo-notifications`; if the template does not ship it, notifications cannot be added until the upstream template includes it. +Native modules are allowlist-bound by the current template `package.json`. If the relevant package is present, `/add-native` can use it through the proper wrapper or host control. If the package is absent, the skill does not install it or fake support; it adds a transparency note and stops for that capability. For example, push notifications require `expo-notifications`; if the template does not ship it, notifications cannot be added until the upstream template includes it. ### 4. Add a connector @@ -249,7 +250,7 @@ Example edit flows: | `/add-dataverse` | ✅ v0 | Add Dataverse — connect to existing tables, or create / extend tables in Tier 0 → N order via the Dataverse Web API, then generate TS services. Accepts ER diagrams via image / Mermaid / text, or spawns the data-model-architect agent. | | `/setup-datamodel` | ✅ v0 | Discoverable alias for `/add-dataverse` optimized for the design-first entry point ("how do I plan my Dataverse schema?"). Same workflow under a more searchable name. | | `/add-connector` | ✅ v0 | Generic connector — runs `npx power-apps add-data-source` for any first-party or custom connector | -| `/add-native` | ✅ v0 | Add a supported native capability/control (camera, image-picker, barcode/QR scanner, document-picker, PDF viewer/report, pen/signature, secure-store, file-system, sharing, etc.) — verifies the module already ships in the template and writes typed wrappers under `src/native/` without installing native packages or editing `app.config.js` | +| `/add-native` | ✅ v0 | Add a supported native capability/control (camera, image-picker, barcode/QR scanner, document-picker, PDF viewer/report, pen/signature, secure-store, file-system, sharing, haptics, etc.) — verifies the module already ships in the template and writes typed wrappers under `src/native/` without installing native packages or editing `app.config.js` | | `/list-connections` | ✅ v0 | Finds or creates a Power Platform connection ID, or resolves a solution connection reference, for `npx power-apps add-data-source`. Use when adding non-Dataverse connectors or re-binding after a 401. | | `/edit-app` | ✅ v0 | Post-generation app editor — updates affected sections of `native-app-plan.md`, applies Dataverse/native/design/connector changes, rebuilds affected screens, runs verification, updates `memory-bank.md`, and regenerates `preview.html` when UI changed. `--plan-only` preserves the old docs-only behavior. | | `/deploy` | ✅ v0 | Build + push — `npm run build` then `npx power-apps push` to the env in `power.config.json`. **Does not** drive `expo run:ios` or `expo run:android` (out of scope for v0). | diff --git a/plugins/mobile-apps/agents/native-app-planner.md b/plugins/mobile-apps/agents/native-app-planner.md index 94ca48083..05e1739c4 100644 --- a/plugins/mobile-apps/agents/native-app-planner.md +++ b/plugins/mobile-apps/agents/native-app-planner.md @@ -144,6 +144,7 @@ Map each shipped module to a user-facing capability slug. Use this known mapping | `video` | `expo-video` | `/add-native video` | | `sensors` | `expo-sensors` | `/add-native sensors` | | `screen-orientation` | `expo-screen-orientation` | `/add-native screen-orientation` | +| `haptics` | `expo-haptics` | `/add-native haptics` | | `date-time-picker` | `@react-native-community/datetimepicker` | screen-builder form component rule | Do not propose `native-pdf-viewer` or `pen-input` unless the exact extension package is present in the template allowlist output (`@microsoft/power-apps-native-pdf-viewer` and `@microsoft/power-apps-native-pen-input`). Do not propose `geolocation` unless `@microsoft/power-apps-native-bglocation` is present, and only for continuous/background tracking or durable Dataverse upload — use one-shot `location` (`expo-location`) for a single foreground coordinate read. When proposing `geolocation`, record that its Dataverse target table must already exist and must be verified by `/add-native geolocation` (default entity set `msdyn_locationrecords`, or a custom `tableName` whose `fieldMap` columns exist). Do not propose `pdf-report` unless `expo-print` is present. Do not propose local sharing for generated PDFs unless `expo-sharing` is present. If neither package path is present, drop the PDF capability and add a transparency note. @@ -167,9 +168,10 @@ PDF/pen inference rules: - `native-pdf-viewer` means opening an HTTPS PDF URL or local `file://` URI with `@microsoft/power-apps-native-pdf-viewer` 0.2.9+. It does not support `content://`, `blob:`, or `http://`. - `pen-input` means signature/ink capture with `@microsoft/power-apps-native-pen-input`. It returns PNG data URI and needs a Dataverse Image/File/child-row target when persisted. - `geolocation` means continuous/background GPS tracking with durable storage and inline Dataverse sync via `@microsoft/power-apps-native-bglocation`. Auth is MSAL-only; native uploads each fix to an existing Dataverse table (default entity set `msdyn_locationrecords`). It is distinct from one-shot `location` (`expo-location`). Plan it only for continuous tracking or durable upload, require `/add-native geolocation` to verify the target table exists before use, and never propose the `GeolocationExtension`/HostingSDK path. +- `haptics` means supplemental tactile feedback through `expo-haptics`. Record the interaction and feedback kind in the screen spec: impact (`light`, `medium`, `heavy`, `soft`, or `rigid`) for deliberate actions, selection for changed selections, or notification (`success`, `warning`, or `error`) after a completed outcome. Every haptic must accompany visible UI feedback. - The Power Apps extensions are use-case-specific, not generic replacements for Expo modules. For other native needs, choose the relevant Expo module or dependency already present in `template/package.json` and still enforce the allowlist. -**Capabilities not present or runtime-banned** — do not propose: anything with required native code/config whose exact package is absent, `expo-notifications` unless a future template ships it, Bluetooth/NFC/BLE/AR without a shipped package, and `expo-haptics` unless the screen-builder hard rule is explicitly removed. +**Capabilities not present** — do not propose anything with required native code/config whose exact package is absent, `expo-notifications` unless a future template ships it, or Bluetooth/NFC/BLE/AR without a shipped package. ### Pure-JavaScript dependency handoff diff --git a/plugins/mobile-apps/agents/screen-builder.md b/plugins/mobile-apps/agents/screen-builder.md index 476d60d52..718298ec4 100644 --- a/plugins/mobile-apps/agents/screen-builder.md +++ b/plugins/mobile-apps/agents/screen-builder.md @@ -201,7 +201,7 @@ You will be invoked by `/create-mobile-app` Step 11 or `/edit-app` screen-rebuil - **Dataverse image rendering rule (detail/list screens).** If a screen displays a Dataverse Image column, include the real image/base64 field in `select` and render a `data:image/;base64,...` URI when base64 is present. Do not rely on guessed URL/display pseudo-fields alone. -- **Native capabilities: use `src/native/` wrappers, NOT raw Expo modules.** `/add-native` creates typed wrappers under `src/native/` (e.g., `camera.ts`, `cameraUpload.ts`, `secureStore.ts`, `documentPicker.ts`, `pdfReport.ts`, `pdfViewer.ts`, `penInput.ts`, `geolocation.ts`). For non-Dataverse native workflows, always import from these wrappers — never import `expo-camera`, `expo-image-picker`, `expo-document-picker`, `expo-print`, `expo-secure-store`, `expo-file-system`, `expo-sharing`, `@microsoft/power-apps-native-pdf-viewer`, `@microsoft/power-apps-native-pen-input`, or `@microsoft/power-apps-native-bglocation` directly in screen files. The wrappers handle permissions, iOS/Android platform differences, URL validation, and return discriminated-union results (`{ ok: true, ... } | { ok: false, reason }`). If a wrapper doesn't exist yet, write the screen with the expected import path and a `// TODO(native-not-yet-added): run /add-native to create src/native/.ts` comment. For `camera.ts`, use `/add-native camera`; for `barcodeScanner.tsx`, use `/add-native barcode-scanner`; for `pdfReport.ts`, use `/add-native pdf-report`; for `pdfViewer.ts`, use `/add-native pdf-viewer` or `/add-native @microsoft/power-apps-native-pdf-viewer`; for `penInput.ts`, use `/add-native pen-input` or `/add-native @microsoft/power-apps-native-pen-input`; for `geolocation.ts`, use `/add-native geolocation` or `/add-native @microsoft/power-apps-native-bglocation`. **`expo-notifications` and `expo-haptics` are NOT available** — per AGENTS.md §2 and the HARD RULE below for haptics. If the plan tells you to use an unavailable capability, return `NEEDS_CONTEXT` — do not import it. +- **Native capabilities use `src/native/` wrappers.** `/add-native` creates typed wrappers under `src/native/` (e.g., `camera.ts`, `cameraUpload.ts`, `secureStore.ts`, `documentPicker.ts`, `pdfReport.ts`, `pdfViewer.ts`, `penInput.ts`, `geolocation.ts`, `haptics.ts`). For non-Dataverse native workflows, screen files import these wrappers; the wrapper implementation owns imports from `expo-camera`, `expo-image-picker`, `expo-document-picker`, `expo-print`, `expo-secure-store`, `expo-file-system`, `expo-sharing`, `expo-haptics`, `@microsoft/power-apps-native-pdf-viewer`, `@microsoft/power-apps-native-pen-input`, or `@microsoft/power-apps-native-bglocation`. The wrappers handle permissions, iOS/Android platform differences, URL validation, and return discriminated-union results (`{ ok: true, ... } | { ok: false, reason }`). If a wrapper doesn't exist yet, write the screen with the expected import path and a `// TODO(native-not-yet-added): run /add-native to create src/native/.ts` comment. For `camera.ts`, use `/add-native camera`; for `barcodeScanner.tsx`, use `/add-native barcode-scanner`; for `pdfReport.ts`, use `/add-native pdf-report`; for `pdfViewer.ts`, use `/add-native pdf-viewer` or `/add-native @microsoft/power-apps-native-pdf-viewer`; for `penInput.ts`, use `/add-native pen-input` or `/add-native @microsoft/power-apps-native-pen-input`; for `geolocation.ts`, use `/add-native geolocation` or `/add-native @microsoft/power-apps-native-bglocation`; for `haptics.ts`, use `/add-native haptics`. `expo-notifications` remains unavailable. If the plan tells you to use an unavailable capability, return `NEEDS_CONTEXT`. - **Planned pure-JavaScript dependencies may be imported directly.** Follow `${PLUGIN_ROOT}/shared/references/javascript-dependency-planning.md`'s Builder Contract. Before importing any non-template package, verify it appears with an exact version in `## Screens → ### JavaScript Dependencies` and in the project's `package.json` `dependencies`, and that `require.resolve('', { paths: [working_dir] })` succeeds. If the plan lists it but installation is missing, return `BLOCKED []: approved JavaScript dependency @ is not installed — orchestrator must install dependencies before builders run.` If it is not in the approved table, return `NEEDS_CONTEXT` instead of adding it yourself. Builders never select packages, edit `package.json`, or run installs. Import and use only the package APIs named by the approved per-screen spec. @@ -245,7 +245,7 @@ You will be invoked by `/create-mobile-app` Step 11 or `/edit-app` screen-rebuil - **Camera evidence flows MUST expose a visible `Take picture` action.** If the spec, route name, or table purpose includes camera evidence, photo evidence, proof photos, scan evidence, inspection photos, or any custom camera capture/upload flow, the screen must show a first-class button labeled `Take picture` (or a domain-specific equivalent beginning with `Take`, such as `Take evidence photo`) that calls `takePhoto()` from `src/native/camera`. Do not hide camera capture behind gallery-only upload, a file picker, an overflow menu, or a detail-only affordance. Gallery/upload can be a secondary sibling action, but camera capture must be visible in the main content or bottom action bar. After capture, if the photo is written to Dataverse, follow the create-then-navigate/upload rule: pre-generate any needed parent/evidence ID with `newId()`, check every write/upload result, and keep the user on the screen with inline error UI if capture/upload fails. -- **HARD RULE — NEVER use `expo-haptics` in any generated screen.** Even though the package may appear in `package.json`, it crashes at runtime in the current rewrap binary (the native module isn't bundled into the binary the customer's app loads — `Haptics.notificationAsync()` / `Haptics.impactAsync()` throw on first call). Visual-only feedback for all interactions: button press uses `pressStyle={{ scale: 0.98 }}`, success uses a green pill / banner / snackbar, error uses inline error text + retry button, toggle uses Switch's visible state change. NEVER write `import * as Haptics from 'expo-haptics'` — it WILL crash the running app, and the screen-builder hooks will block the write anyway. +- **Haptics usage.** When the screen spec includes haptics, import `triggerImpact`, `triggerNotification`, or `triggerSelection` from `src/native/haptics`. Use impact feedback for deliberate presses, selection feedback when a selected value changes, and notification feedback only after a success, warning, or error outcome is known. Inspect the wrapper result and log a non-OK result without turning an otherwise successful operation into a failure. Haptics supplement visible state; buttons, banners, inline errors, and selected states must remain understandable when vibration is unavailable or disabled. Do not trigger haptics during render, loading loops, or repeated list events. - **React Query (`@tanstack/react-query`) is the default for server state.** The template ships it (`package.json`), and current `@microsoft/power-apps-native-host` wraps the app with `QueryClientProvider` inside `PowerAppsProvider`. **Never re-wire the provider from a screen file** and never construct a new `QueryClient` inside a component. Use `useQuery` for reads, `useMutation` for writes (create / update / delete), `useInfiniteQuery` for cursor-paginated lists, and `useQueryClient()` for invalidation. The raw `useEffect` + `useState` skeleton in Step 3 is only a fallback for screens where the spec explicitly excludes React Query (rare). Required usage rules: - **Query keys are arrays starting with the entity, then params.** Examples: `['inspections']`, `['inspections', { status: 'open' }]`, `['inspection', id]`. Consistent shape lets `queryClient.invalidateQueries({ queryKey: ['inspections'] })` from a sibling mutation invalidate every variant in one line. - **After every successful mutation, invalidate the affected query keys.** Pattern: @@ -259,7 +259,7 @@ You will be invoked by `/create-mobile-app` Step 11 or `/edit-app` screen-rebuil }); ``` Without invalidation, the parent list shows stale data after the user pops back. This is the most common React Query bug in this plugin's generated screens. - - **Loading button rule (#38) consumes `isPending`.** A submit button on a `useMutation` flow MUST disable while `isPending`, label-swap, and trigger any visible success state inside `onSuccess` (green pill / banner / snackbar). Never haptic — see hard rule above. + - **Loading button rule (#38) consumes `isPending`.** A submit button on a `useMutation` flow MUST disable while `isPending`, label-swap, and trigger visible success state inside `onSuccess` (green pill / banner / snackbar). When the approved screen spec includes haptics, call `triggerNotification('success')` after setting that visible success state and handle its result as described above. - **Focus refetch is handled by React Query** — see the `useFocusEffect` rule below for the exception. Do NOT pair `useQuery` with `useFocusEffect`; pick one. - **Refresh on focus, never on mount.** Data loading on a screen MUST use `useFocusEffect` from `expo-router` wrapped in `React.useCallback` — NEVER `useEffect` with `[]`/`[loadX]` dependencies for the initial fetch on a list, detail, or any screen the user can navigate back to. `useEffect` only runs on mount; React Navigation keeps screens mounted when a child is pushed, so popping back to the list never re-fetches and the user sees stale data after a create/update/delete in a child screen. `useFocusEffect` re-runs every time the screen regains focus (mount AND re-focus), which is what users expect. - **Required import:** `import { useFocusEffect } from 'expo-router';` — not `@react-navigation/native`. diff --git a/plugins/mobile-apps/agents/screen-planner.md b/plugins/mobile-apps/agents/screen-planner.md index e1982db91..81cd46d1d 100644 --- a/plugins/mobile-apps/agents/screen-planner.md +++ b/plugins/mobile-apps/agents/screen-planner.md @@ -427,7 +427,7 @@ For each screen the user adds, provide this compact shape: - **Audit** (omit for read-only / non-write screens) — one line per audit-bearing action: `: event (); payload: `. Example: `On submit: event 100000006 (Inspection Submitted); payload: inspectionId, submittedAt, defectCount, openCriticalCount.` The screen-builder wraps the payload field list in `JSON.stringify({...})` and writes the full `cr3e9_audit_log_entriesService.create(...)` call from the Generated Services table — do NOT spell out the wrapper or service name. - **Lookup writes** — for form/edit screens that set a parent reference (Task → Project, Comment → Task, etc.), explicitly list each lookup field with its `@odata.bind` name + entity set, e.g. `'cr3e9_Project@odata.bind': '/cr3e9_projects()'`. Without this the screen-builder will guess and silently lose the relationship. Skip for read-only and no-lookup screens. - **Pagination** — `cursor` if the table has no natural record ceiling (visits, inspections, work orders, tickets, any user-created records over time); `none` if the table is a bounded lookup (status types, categories, job types). When `cursor`, include SDK `maxPageSize: 50`, deterministic `orderBy` with a unique key, `select`, `skipToken` continuation support, and server-side `filter` for search in the data spec. Do not imply that `top: 50` alone is pagination. -- **Native capabilities** — which native modules/wrappers it uses, and which iOS/Android platforms or permission states need fallback handling. For PDF/pen screens, be precise: `document-picker` (`expo-document-picker`) for user-picked files; `pdf-report` (`expo-print`, plus `expo-sharing` only when present and sharing is required) for generated local PDFs; `native-pdf-viewer` (`@microsoft/power-apps-native-pdf-viewer` 0.2.9+) for HTTPS PDF URLs and local `file://` URIs; `pen-input` (`@microsoft/power-apps-native-pen-input`) for signature/ink capture. For location screens, distinguish `geolocation` (`@microsoft/power-apps-native-bglocation`) — continuous/background tracking with native Dataverse sync, needs start/stop/tracking-status UI plus a permission-denied state — from one-shot `location` (`expo-location`) for a single foreground coordinate read. +- **Native capabilities** — which native modules/wrappers it uses, and which iOS/Android platforms or permission states need fallback handling. For PDF/pen screens, be precise: `document-picker` (`expo-document-picker`) for user-picked files; `pdf-report` (`expo-print`, plus `expo-sharing` only when present and sharing is required) for generated local PDFs; `native-pdf-viewer` (`@microsoft/power-apps-native-pdf-viewer` 0.2.9+) for HTTPS PDF URLs and local `file://` URIs; `pen-input` (`@microsoft/power-apps-native-pen-input`) for signature/ink capture. For location screens, distinguish `geolocation` (`@microsoft/power-apps-native-bglocation`) — continuous/background tracking with native Dataverse sync, needs start/stop/tracking-status UI plus a permission-denied state — from one-shot `location` (`expo-location`) for a single foreground coordinate read. For haptics, name the exact interaction plus wrapper call: impact and strength for deliberate presses, selection for changed choices, or notification and outcome for completed operations; also name the visible feedback that accompanies it. - **Calendar library** — REQUIRED for screens with `Calendar pattern` unless the pattern is `timeline-day-list`. Write `react-native-calendars` and name the exact components expected, for example `CalendarProvider`, `ExpandableCalendar`, `AgendaList`, `Calendar`, `CalendarList`, or `Agenda`. The package must also appear in `### JavaScript Dependencies`; the screen-builder imports it directly after the orchestrator installs it. No `/add-native` wrapper or native rebuild is involved. - **Navigation** — what links to it / what it links to - **Navigation intent** — for each outgoing action, explicitly name `navigate`, `push`, or `replace` (must match Navigation Contracts `Intent`) diff --git a/plugins/mobile-apps/scripts/tests/validate-package-deps.test.js b/plugins/mobile-apps/scripts/tests/validate-package-deps.test.js index 4cd8b1869..11a5cd1d2 100644 --- a/plugins/mobile-apps/scripts/tests/validate-package-deps.test.js +++ b/plugins/mobile-apps/scripts/tests/validate-package-deps.test.js @@ -9,6 +9,7 @@ const { spawnSync } = require('node:child_process'); const DISPATCHER = path.resolve(__dirname, '..', 'validate-mobile-files.js'); const VALIDATOR = path.resolve(__dirname, '..', '..', 'hooks', 'validate-package-deps.js'); +const TEMPLATE_PACKAGE = path.resolve(__dirname, '..', '..', 'template', 'package.json'); function makeProject(packageName, version = '1.0.0') { const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mobile-package-deps-')); @@ -75,6 +76,19 @@ test('does not let plan approval bypass a known native package block', (t) => { assert.match(result.stderr, /Native\/runtime dependency `expo-notifications`/); }); +test('allows the template-shipped expo-haptics version', (t) => { + const templatePackage = JSON.parse(fs.readFileSync(TEMPLATE_PACKAGE, 'utf8')); + const hapticsVersion = templatePackage.dependencies?.['expo-haptics']; + assert.strictEqual(hapticsVersion, '55.0.14'); + + const projectRoot = makeProject('expo-haptics', hapticsVersion); + t.after(() => fs.rmSync(projectRoot, { recursive: true, force: true })); + + const result = validate(projectRoot); + + assert.strictEqual(result.status, 0, result.stderr); +}); + test('continues to allow ordinary JavaScript packages without an exception', (t) => { const projectRoot = makeProject('date-fns', '4.1.0'); t.after(() => fs.rmSync(projectRoot, { recursive: true, force: true })); diff --git a/plugins/mobile-apps/shared/references/screen-templates.md b/plugins/mobile-apps/shared/references/screen-templates.md index 1493fc182..f3ea5e9e4 100644 --- a/plugins/mobile-apps/shared/references/screen-templates.md +++ b/plugins/mobile-apps/shared/references/screen-templates.md @@ -408,7 +408,7 @@ When the plan's `## Design` specifies a copy tone (e.g., `Copy tone: Warm`), use Apply these to every screen. They are the difference between an app that feels cheap and one that feels polished. -> **Note:** `expo-haptics` is BANNED — it crashes at runtime in the current rewrap binary. Use visual-only feedback (`pressStyle={{ scale: 0.98 }}` for press, green pill / banner / snackbar for success, inline error text for error, Switch's visible state change for toggle). See `agents/screen-builder.md` HARD RULE for details. +> **Haptics:** When the approved screen spec includes tactile feedback, call `triggerImpact`, `triggerSelection`, or `triggerNotification` from `src/native/haptics.ts`. Use light/medium impact for deliberate presses, selection feedback for changed choices, and notification feedback only after the final success/warning/error outcome. Haptics are supplemental: retain visual feedback such as `pressStyle={{ scale: 0.98 }}`, banners, inline errors, and visible selected/switch state. --- diff --git a/plugins/mobile-apps/skills/add-native/SKILL.md b/plugins/mobile-apps/skills/add-native/SKILL.md index baeef2906..461a5f6b0 100644 --- a/plugins/mobile-apps/skills/add-native/SKILL.md +++ b/plugins/mobile-apps/skills/add-native/SKILL.md @@ -1,6 +1,6 @@ --- name: add-native -description: Public entry point for native device capabilities and native controls — camera, image picker, barcode/QR scanner, document picker, file picker, secure storage, file system, sharing, PDF generation/viewing, pen/signature capture, background GPS/geolocation tracking, or supported local file workflows — in a Power Apps mobile app. Also owns routing to internal camera/PDF/pen/geolocation implementation helpers and the guidance boundary between native wrappers and Dataverse File/Image host controls. +description: Public entry point for native device capabilities and native controls — camera, image picker, barcode/QR scanner, document picker, file picker, secure storage, file system, sharing, haptic feedback, PDF generation/viewing, pen/signature capture, background GPS/geolocation tracking, or supported local file workflows — in a Power Apps mobile app. Also owns routing to internal camera/PDF/pen/geolocation implementation helpers and the guidance boundary between native wrappers and Dataverse File/Image host controls. user-invocable: true allowed-tools: Read, Edit, Write, Grep, Glob, Bash, AskUserQuestion model: sonnet @@ -52,6 +52,7 @@ Before adding any native control or wrapper, apply every gate: classify the inte | Open/preview an HTTPS or local file PDF | `/add-native pdf-viewer` | `@microsoft/power-apps-native-pdf-viewer` 0.2.9+ present and input is `https://` or `file://` | Do not pass `content://`, `blob:`, or `http://` URIs to the viewer | | Capture signature, ink, drawing, or sign-off | `/add-native pen-input` | `@microsoft/power-apps-native-pen-input` present | If persisted, plan Dataverse Image/File/child Evidence target first | | Continuous/background GPS tracking with durable Dataverse upload | `/add-native geolocation` | `@microsoft/power-apps-native-bglocation` present | Do not use one-shot `expo-location` for background tracking; do not use the `GeolocationExtension`/HostingSDK path | +| Tactile feedback for presses, selections, and operation results | `/add-native haptics` | `expo-haptics` present | Use the typed wrapper and pair every haptic with visible UI feedback | | Store generated PDF/signature artifact | Generated Dataverse services after parent row exists | File/Image column or child Evidence/Attachment table exists | Never put File bytes in create/update JSON | | Native capability not listed in this table | Resolve from `package.json`, then add an inline wrapper only when the matching package is present and not runtime-banned | Exact relevant package present in `package.json` | If no relevant package exists, or the package is runtime-banned, add a transparency note and stop | @@ -146,6 +147,7 @@ Apply the Native capability gate above. This table is a known capability-to-pack | `video` | `expo-video` | `src/native/video.ts` | Use for video playback only when package is present | | `sensors` | `expo-sensors` | `src/native/sensors.ts` | Use only for sensor APIs exposed by the installed package | | `screen-orientation` | `expo-screen-orientation` | `src/native/screenOrientation.ts` | Use only when package is present; do not edit native config | +| `haptics`, `vibration-feedback`, `impact-feedback`, `selection-feedback`, `notification-feedback` | `expo-haptics` | `src/native/haptics.ts` | Impact, selection, and success/warning/error notification feedback | | `device-info` | `expo-device` / `expo-application` / `expo-cellular` | `src/native/deviceInfo.ts` | Read-only device/app/cellular metadata wrappers | | `date-time-picker` | `@react-native-community/datetimepicker` | screen-level component usage | Use directly in form screens per screen-builder rules; no `/add-native` wrapper required | @@ -169,7 +171,7 @@ Apply the Native capability gate above. This table is a known capability-to-pack - Never put File column bytes in the create/update JSON body. File bytes are uploaded only after the parent row ID exists. - Screens must handle unsupported, cancelled, upload failed, and viewer failed states explicitly. Pen cancellation is a non-error result that screens can ignore. -**Missing or banned packages:** `package.json` plus the runtime-ban list is authoritative. If the relevant package/control is absent, or the package is runtime-banned, stop with a transparency note. `expo-haptics` remains banned unless the screen-builder hard rule is explicitly removed; use visual-only feedback instead. +**Missing or gated packages:** `package.json` plus the runtime gate is authoritative. If the relevant package/control is absent, stop with a transparency note. ## Workflow @@ -187,7 +189,7 @@ test -f app.config.js && test -f power.config.json && test -f package.json If `$ARGUMENTS` includes a capability name, package name, or control name, use it. Otherwise look for a `## Native Capabilities` section in `native-app-plan.md` and present the planned capabilities for confirmation. If neither exists, prompt the user with the supported-capabilities list above plus any relevant installed package from `package.json` that directly matches their request. -Normalize the capability name to lowercase, hyphenated form (e.g., `Camera` → `camera`, `ImagePicker` → `image-picker`, `SecureStore` → `secure-store`). Also normalize aliases: `take-photo` / `photo` / `camera-control` / `expo-camera` → `camera`; `gallery` / `pick-image` / `expo-image-picker` → `image-picker`; `scanner` / `barcode` / `qr` → `barcode-scanner`; `open-pdf` / `view-pdf` / `pdf-control` / `pdf-viewer-control` / `@microsoft/power-apps-native-pdf-viewer` → `pdf-viewer`; `native-pdf-viewer` → `pdf-viewer`; `generate-pdf` / `pdf-export` → `pdf-report`; `signature` / `sign-off` / `ink` / `draw` / `pen-control` / `@microsoft/power-apps-native-pen-input` → `pen-input`; `location-tracking` / `background-location` / `gps-tracking` / `geo-tracking` / `track-location` / `power-apps-native-bglocation` / `@microsoft/power-apps-native-bglocation` → `geolocation`. +Normalize the capability name to lowercase, hyphenated form (e.g., `Camera` → `camera`, `ImagePicker` → `image-picker`, `SecureStore` → `secure-store`). Also normalize aliases: `take-photo` / `photo` / `camera-control` / `expo-camera` → `camera`; `gallery` / `pick-image` / `expo-image-picker` → `image-picker`; `scanner` / `barcode` / `qr` → `barcode-scanner`; `open-pdf` / `view-pdf` / `pdf-control` / `pdf-viewer-control` / `@microsoft/power-apps-native-pdf-viewer` → `pdf-viewer`; `native-pdf-viewer` → `pdf-viewer`; `generate-pdf` / `pdf-export` → `pdf-report`; `signature` / `sign-off` / `ink` / `draw` / `pen-control` / `@microsoft/power-apps-native-pen-input` → `pen-input`; `location-tracking` / `background-location` / `gps-tracking` / `geo-tracking` / `track-location` / `power-apps-native-bglocation` / `@microsoft/power-apps-native-bglocation` → `geolocation`; `vibration` / `vibration-feedback` / `impact-feedback` / `selection-feedback` / `notification-feedback` / `expo-haptics` → `haptics`. When the user asks for "location" or "GPS", disambiguate by intent: continuous/background tracking or durable Dataverse upload → `geolocation` (`@microsoft/power-apps-native-bglocation`); a single foreground coordinate read → `location` (`expo-location`). If the intent is unclear, ask once before routing. @@ -239,7 +241,92 @@ Each wrapper exports: - Branch by supported native platform when a capability differs between iOS and Android - Screens import these wrappers only for non-Dataverse native workflows. Dataverse File/Image fields use `@microsoft/power-apps-native-host` controls from the File/Image Picker Ownership section above. -**Coding the wrapper:** consult the module's published API docs (linked from its npm page) for method signatures and permission patterns. Use the secure-store skeleton below as the canonical example of the discriminated-union shape — then translate to the target module's API. +#### Haptics wrapper + +For normalized capability `haptics`, generate `src/native/haptics.ts` with this contract: + +```ts +import * as Haptics from 'expo-haptics'; +import { Platform } from 'react-native'; + +export type HapticImpactStyle = 'light' | 'medium' | 'heavy' | 'soft' | 'rigid'; +export type HapticNotificationType = 'success' | 'warning' | 'error'; +export type HapticResult = + | { ok: true } + | { ok: false; reason: 'unsupported' | 'error'; message: string }; + +const impactStyles: Record = { + light: Haptics.ImpactFeedbackStyle.Light, + medium: Haptics.ImpactFeedbackStyle.Medium, + heavy: Haptics.ImpactFeedbackStyle.Heavy, + soft: Haptics.ImpactFeedbackStyle.Soft, + rigid: Haptics.ImpactFeedbackStyle.Rigid, +}; + +const notificationTypes: Record = { + success: Haptics.NotificationFeedbackType.Success, + warning: Haptics.NotificationFeedbackType.Warning, + error: Haptics.NotificationFeedbackType.Error, +}; + +async function runHaptic(effect: () => Promise): Promise { + if (Platform.OS !== 'ios' && Platform.OS !== 'android') { + return { + ok: false, + reason: 'unsupported', + message: 'Haptic feedback is available only on iOS and Android.', + }; + } + + try { + await effect(); + return { ok: true }; + } catch (error) { + return { + ok: false, + reason: 'error', + message: error instanceof Error ? error.message : 'The native haptic call failed.', + }; + } +} + +export function triggerImpact(style: HapticImpactStyle = 'medium'): Promise { + return runHaptic(() => Haptics.impactAsync(impactStyles[style])); +} + +export function triggerNotification( + type: HapticNotificationType = 'success' +): Promise { + return runHaptic(() => Haptics.notificationAsync(notificationTypes[type])); +} + +export function triggerSelection(): Promise { + return runHaptic(() => Haptics.selectionAsync()); +} +``` + +Screens call the wrapper only when haptics appear in the approved native-capabilities and per-screen plan. Choose feedback by semantics: + +- `triggerImpact('light' | 'medium' | 'heavy' | 'soft' | 'rigid')` for deliberate presses or physical-feeling actions. +- `triggerSelection()` when a picker, segmented control, or other selection changes. +- `triggerNotification('success' | 'warning' | 'error')` after the corresponding outcome is known. +- Keep visual feedback as the primary signal. Inspect non-OK results and log them, but do not turn an otherwise successful business operation into a failure because tactile feedback is unavailable. + +Screen usage after a successful operation: + +```ts +setSaveState({ kind: 'success', message: 'Saved' }); +const hapticResult = await triggerNotification('success'); +if (!hapticResult.ok) { + console.warn('[haptics] success feedback unavailable', hapticResult); +} +``` + +No permission request or `app.config.js` change is required for `expo-haptics`. + +#### Other inline wrappers + +Consult the module's published API docs (linked from its npm page) for method signatures and permission patterns. Use the secure-store skeleton below as the canonical example of the discriminated-union shape, then translate it to the target module's API. Secure-store canonical skeleton: @@ -287,7 +374,7 @@ export async function setSecret(key: string, value: string): Promise