# Generate Native Extension

> Read the approved PRD.md and generate the native sources for a third-party PAM control (the compiled `.ppmplugin` track) — iOS Obj-C `<Pascal>Module` plus optional system-frameworks podspec, Android Kotlin `<Pascal>Module` with build.gradle, AndroidManifest and ReactPackage, a dev-only private package.json (react + react-native devDeps for the builds), and the committed `./manifest.json` dispatch contract the PCF and build stage both read. No TypeScript INativeExtension layer — the contract is the manifest plus the native modules' dispatch surface. Emits the layout in shared/repo-layout.md and generates substantially complete native code (compiled later by /build-android-binary and /build-ios-binary, not here). Local only — writes files, runs no git and touches no remote or feed. PCF is generated by /generate-pcf-companion; the bundle is built by /generate-ppmplugin.

- Skill: `microsoft/generate-native-extension` (Agent Skill)
- Install (CLI): `npx skillmds@latest add microsoft/generate-native-extension`
- Raw SKILL.md: https://api.skillmd.com/api/skills/microsoft/generate-native-extension/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: Microsoft (https://skillmd.com/u/microsoft)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/microsoft/generate-native-extension

---


# /generate-native-extension

Reads `PRD.md` in the working directory and writes the native sources for a third-party PAM control following the layout in [`shared/repo-layout.md`](../../shared/repo-layout.md). This is the **native-only** (compiled `.ppmplugin`) track — there is NO TypeScript `INativeExtension` / `handleMessageAsync` layer; the wrap host dispatches straight to `NativeModules.<Pascal>Module.<method>` per the manifest's receivers contract (see [`shared/ppmplugin-format.md §2`](../../shared/ppmplugin-format.md)). The output is substantially complete native code so the engineer starts at customizing OS-specific code, not writing boilerplate.

This skill writes the **native module** half of the repo (`ios/`, `android/`, optional podspec, dev-only package.json) **and the committed `./manifest.json`** — the dispatch-contract source of truth. The manifest is authored *here*, alongside the native code it describes, because every field in it is derived from the names this scaffold emits (`getName()`, the `@ReactMethod` list, the package class); authoring it now means the **Companion PCF** (`/generate-pcf-companion`) reads a real contract instead of re-deriving one, so the composite key `<name>/<receiver>` can't drift between the PCF and the module. The build stage `/generate-ppmplugin-manifest` (inside `/generate-ppmplugin`) then **validates + reconciles + stages** this manifest rather than authoring it from scratch. The **Companion PCF** is generated separately by `/generate-pcf-companion` because it requires `pac` CLI and a different toolchain.

---

## Step 1 — Read the shared docs and the PRD

Before any write:

1. Read [`shared/shared-instructions.md`](../../shared/shared-instructions.md).
2. Apply the **per-skill minimal prereq policy** ([`shared-instructions.md §1.5`](../../shared/shared-instructions.md)). This track is **self-contained** ([`shared-instructions §0a`](../../shared/shared-instructions.md)) and uses only the working tree and public package registries. This skill needs no toolchain to write the files — optionally Node + pnpm to seed the dev-only `package.json`'s devDeps from the public npm registry (used later by `/build-android-binary` / `/build-ios-binary`, not here). Step 4's smoke check is a structural self-check — it does NOT compile anything. Run the **`/generate-native-extension` check** from [`prereq-check.md`](../../shared/prereq-check.md) (git required; Node/pnpm optional — there is no "baseline" check in this self-contained track).

   **Print the prereq status as a visible block per `shared-instructions.md §9.2`** before continuing:

   ```
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    Prereq check — /generate-native-extension
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    🟢 ✓ git installed
    🟢 ✓ Node 20+ installed     (optional — only to seed package.json devDeps from public npm)
    🟢 ✓ pnpm installed         (optional — same)

    🟢 checks passed. Ready to proceed.
   ```

   If `git` is missing, print its `→ Fix:` line and STOP. Node/pnpm are optional here — if absent, note them as `n/a (devDeps seed deferred to build skills)` rather than failing.
3. Read [`shared/naming-conventions.md`](../../shared/naming-conventions.md) — the derived-identifier table is canonical, including the **`Module`-suffix rule** for the native module symbol. Derive all file paths and class names from §2 of the PRD using that table; do not invent.
4. Read [`shared/ppmplugin-format.md`](../../shared/ppmplugin-format.md) — §2 (the runtime dispatch contract: `<name>/<receiver>` → `NativeModules.<nativeModule>.<method>`, where `<nativeModule>` = `<Pascal>Module`) and §4 (the upload-compatibility checks that the native module symbol must satisfy). The native modules this skill emits dispatch straight off that contract — there is NO TS `INativeExtension` layer mediating; see §3.3 below.
5. Read [`shared/repo-layout.md`](../../shared/repo-layout.md) — the exact tree, file list, and `package.json` shape to emit.
6. Read `./PRD.md` from the current working directory. If missing or empty, STOP with `BLOCKED: PRD.md not found — run /design-native-extension-feature first.`
7. Read `./.extension-state.md` if present. If the phase shows `scaffold-complete`, ask the user whether to **regenerate** (with confirm — overwrites files), **resume** (only fill in missing files), or **abort**.

The structural patterns this skill needs to emit (iOS module shape, Android module shape, podspec, package.json) are fully prescribed in this SKILL.md (§3.1–§3.7) and in [`shared/repo-layout.md`](../../shared/repo-layout.md). Do NOT fetch the reference extension repo at runtime — its lessons are already encoded here, and fetching it would risk copying PDF-specific code into a non-PDF extension.

If any read fails, STOP and report which file is missing.

---

## Step 2 — Confirm the scaffold plan with the user

Print a concise summary derived from the PRD, then gate on approval before any write.

```
Scaffold plan
─────────────
Repo: powerapps-<kebab>
package: <kebab>-control  (dev-only, private — not published)
Class: <Pascal>
Native module: <Pascal>Module  → NativeModules.<Pascal>Module  (== ./manifest.json receivers[].nativeModule)
iOS class: RCT<Pascal>Module  (+moduleName returns <Pascal>Module)
Android module: <Pascal>Module  (com.powerapps.<lower>)
Podspec: <Pascal>Extension.podspec (optional, system-frameworks-only)
Dispatch contract: ./manifest.json (committed — written by this skill; read by the PCF + build stage)

Frameworks
  iOS:     <list from ARCHITECTURE §1.2>
  Android: <list from ARCHITECTURE §1.3>

Operations (<count from PRD §4>): <comma-separated names>
Pattern: <one-shot | streaming | two-way>
Error codes: <count from ARCHITECTURE §5>

Target directory: <cwd> (writes <N> files; no existing files will be overwritten without confirm)
Distribution: the compiled `.ppmplugin` bundle (built later by /generate-ppmplugin). This skill is purely local — no remote, no feed, no registry.
```

Use `AskUserQuestion` (single-select):

> Proceed with this scaffold?
> - **Yes — generate the files** (recommended): write the control's sources into the current directory. This skill does **not** run git — no `git init`, no staging, no commit (the control lives in your existing repo; you commit when you're ready).
> - **Edit the PRD first** — exit; user re-runs `/design-native-extension-feature` to adjust.
> - **Cancel**

---

## Step 3 — Generate the files

Write files in the order below. After each top-level group, print a one-line progress update (`✓ wrote ios/ (3 files)`). Don't dump file contents — the user sees the diff via the IDE.

Every file path is **relative to the current working directory** (the repo root). Names are derived per [`shared/naming-conventions.md`](../../shared/naming-conventions.md).

### 3.1 Top-level repo files

Write:

- **`.gitignore`** — emit exactly the following entries:
  - Node: `node_modules/`, `dist/`, `build/`
  - **`.ppmplugin` build staging — MANDATORY**: `ppmplugin/` (the gitignored staging dir where `/generate-ppmplugin` writes the **staged copy** of the manifest, the binaries, and the final bundle — never committed. NOTE: the **committed** source-of-truth `manifest.json` lives at the repo **root** (`./manifest.json`, written below), NOT under `ppmplugin/` — do not gitignore it; see [`shared/ppmplugin-format.md §1`](../../shared/ppmplugin-format.md))
  - OS / editor: `.DS_Store`, `.idea/`, `.vscode/`
  - Claude Code local state (per-user, not shared): `.claude/`
  - Env: `.env*` (but allow `!.env.example`)
  - iOS build: `Pods/`, `*.xcworkspace`, `DerivedData/`, `*.xcodeproj/xcuserdata/`
  - Android build: `*.iml`, `.gradle/`, `local.properties`, `captures/`, `.externalNativeBuild/`, `.cxx/`
  - PCF build dirs only — NOT the `pcf/` folder itself; source files (`index.ts`, `ControlManifest.Input.xml`, `package.json`, `pcfconfig.json`, etc.) stay tracked: `pcf/**/{out,Solutions,node_modules,obj,bin,generated}/`
  - Test-harness artifacts: `test-harness/*.msapp`
  - Skill-generated backups: `*.bak.*` (skills that replace tracked content may save a timestamped backup; those are intentionally local-only)
  - Design-time previews: `.pcf-preview/` (HTML mockup of the PCF as it appears in Canvas Studio — written by `/design-native-extension-feature` Step 8.0 for visual review; regenerated each design iteration; not a source-of-truth artifact)

- **`package.json`** — per the dev-only shape in [`shared/repo-layout.md`](../../shared/repo-layout.md) §"`package.json` shape (dev-only)". Fill in `name` (a plain local name, e.g. `<kebab>-control`) and `description` from the PRD. `version` starts at `0.1.0`. Set `"private": true`.

  This manifest is **never published** — no `publishConfig`, no feed registry, no `files` array, no `main`/`types`, no `.npmrc`. Its only job is to pin the React Native version the native builds compile against:
  ```json
  {
    "name": "<kebab>-control",
    "version": "0.1.0",
    "private": true,
    "description": "<from PRD §1>",
    "devDependencies": {
      "react": "18.2.0",
      "react-native": "0.79.7"
    }
  }
  ```
  The `react-native` devDep supplies the iOS headers (`/build-ios-binary`) and pins the `react-android` coordinate the Android build resolves (`/build-android-binary`); add any other build-time devDeps the native modules need. All deps resolve from the **public npm registry** — there is no internal feed.

- **`manifest.json`** (repo root, **committed** — the dispatch-contract source of truth) — author it now from the names this scaffold emits, per [`shared/ppmplugin-format.md`](../../shared/ppmplugin-format.md) §2 (schema) + §3 (derivation). This is the single artifact the Companion PCF (`/generate-pcf-companion`) and the build stage (`/generate-ppmplugin-manifest`) both read; authoring it here, next to the native code it describes, is what keeps the composite key `<name>/<receiver>` from drifting between the PCF and the module. Fields:
  - `name` = `kebab(<Pascal>)` of the **class** name (not the repo/capability name) — e.g. class `PenInput` → `pen-input`.
  - `version` = the `package.json` version (`0.1.0`).
  - `abi` = `{ "compatibleShells": ">=1.0.0", "builtAgainst": "1.0.0" }` (default; the build skills don't change it).
  - `receivers[]` = a single entry `{ "name": "<Pascal>Extension", "nativeModule": "<Pascal>Module", "methods": [<every @ReactMethod / RCT_EXPORT_METHOD name emitted in §3.4 / §3.5>] }`. `nativeModule` MUST equal Android `getName()` and the iOS `+moduleName` return value — the **`Module`-suffixed** name (the reserved-name dodge).
  - `entrypoints` = declare **every platform this scaffold generated** (so the committed manifest is the *full* contract; the build stage trims it to the shipped target):
    - Android → `"android": { "dex": "<Pascal>Plugin.dex", "packageClass": "com.powerapps.<lower>.<Pascal>Package" }`
    - iOS → `"ios": { "framework": "<Pascal>Plugin", "moduleClass": "RCT<Pascal>Module" }`

  This is a **logical contract**, not a built artifact — it lists the platforms the module *supports*; the per-platform binaries are compiled later and the staged copy under `ppmplugin/staging/` is reconciled down to whatever actually ships. Do NOT emit any `entrypoints.js` / `extension.hbc` / `extensionClassName` / `jsLayer` field — those are SDK-era leakage `/audit-ppmplugin` rejects. (The build stage re-runs the full validator on this file, so a malformed manifest is caught either way — but emit it correctly here.)

- **`README.md`** — one-page user-facing doc tailored to the control. Sections: "What's in the box" (the compiled `.ppmplugin` bundle + PCF companion), "Build" (run `/generate-ppmplugin` to produce the `.ppmplugin`), "Architecture" (a Mermaid-or-ASCII diagram of Canvas formula → PCF → wrap-bridge → `NativeModules.<Pascal>Module`), "Development" (`pnpm install` to seed devDeps; native code is compiled by the build skills, not here), "Reference docs" (link to `shared/ppmplugin-format.md`). Use the PRD's §1 Summary verbatim. Drive every section from the PRD — never inject example values, prose, or screenshots from any other control's README.

- **`CHANGELOG.md`** — single entry:
  ```markdown
  # Changelog

  ## 0.1.0 — <ISO date>

  - Initial scaffold for <Human-Readable Name> native control.
  - Generated by pam-native-extensions plugin from PRD.md.
  ```

- **`LICENSE`** — MIT.

### 3.2 The podspec (optional, at repo root)

Write **`<Pascal>Extension.podspec`** at the repo root (NOT inside `ios/`) **only if** ARCHITECTURE §1.2 names additional iOS system frameworks the module links. The `.ppmplugin` iOS build (`/build-ios-binary`) compiles from a throwaway staged Xcode project and does NOT npm-autolink against this podspec — so it lists **system frameworks only** (no `React-Core` / RN-CLI autolink dependency, no remote `source`). It exists for local `pod lib lint` convenience, not the bundle build. Template:

```ruby
require "json"

package_json = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
  s.name         = "<Pascal>Extension"
  s.version      = package_json["version"]
  s.summary      = "<one-line description from PRD>"
  s.description  = <<-DESC
    <2-3 sentence description from PRD — what it does, what it bridges to>
  DESC
  s.license      = "MIT"
  s.author       = { "Author" => "" }
  s.platform     = :ios, "<min-deployment-target from ARCHITECTURE §1.2>"
  s.source       = { :path => "." }
  s.source_files = "ios/**/*.{h,m}"   # change to {h,m,swift} if Swift used
  s.frameworks   = <comma-quoted list of SYSTEM frameworks from ARCHITECTURE §1.2>
  # No React-Core dependency: the .ppmplugin build resolves RN headers from the
  # react-native devDep in package.json, not via CocoaPods autolinking.
end
```

### 3.3 No TypeScript layer — the dispatch contract

This is the **native-only** track: there is **no `src/` TypeScript layer**, no `src/<Pascal>Extension.ts`, no `src/types.ts`, no `INativeExtension` / `handleMessageAsync` implementation, and no `sendAsync` transport. (Those belong to the first-party SDK track — **NOT in this track**.) Do NOT generate any of them; reintroducing a TS contract layer here produces SDK-era leakage that `/audit-ppmplugin` rejects.

The contract instead is the **manifest's runtime dispatch** ([`shared/ppmplugin-format.md §2`](../../shared/ppmplugin-format.md)): the wrap host routes a call by the composite key `<name>/<receiver>` **straight to** `NativeModules.<Pascal>Module.<method>(args, promise)`. There is no JS mediator. This means:

- The **request shape** (the `args` object) and **response shape** (the object the promise resolves with) from ARCHITECTURE §4 are realized **directly** in the native `@ReactMethod` / `RCT_EXPORT_METHOD` signatures + their JSON responses — see §3.4 (iOS) and §3.5 (Android). The per-operation JSON parsing, request validation, operation branching, and error-code responses that a first-party TS `handleMessageAsync` would have done are emitted **inside each native method** instead. That dispatch logic is the valuable part this skill generates.
- The `manifest.json` that declares `name`, `receivers[].method`, and `receivers[].nativeModule` (= `<Pascal>Module`) is written by **this skill** at the repo root (§3.1) — the native module symbols it emits and the manifest's `receivers[]` are authored together, so they can't disagree. `/generate-ppmplugin-manifest` later validates + reconciles + stages this file rather than re-authoring it (§2/§3 below + [`shared/ppmplugin-format.md §3`](../../shared/ppmplugin-format.md)).
- The error-code set from ARCHITECTURE §5 is realized as the string codes the native `errorJson(code, message)` helpers emit (§3.4 / §3.5), each paired with a human-readable `message` — there is no TS error-union type to declare. These codes are the **stable strings** from the canonical catalog [`shared/error-codes.md`](../../shared/error-codes.md) (Canvas formulas branch on them, so they must not drift); emit exactly the catalog spelling for any code ARCHITECTURE §5 reuses. The PCF reads both: the `error` code to branch on, the `message` to surface as its `ErrorMessage` output.

### 3.4 iOS (`ios/`)

Write:

- **`ios/RCT<Pascal>Module.h`** — minimal Obj-C header importing `<React/RCTBridgeModule.h>`, declaring `@interface RCT<Pascal>Module : NSObject <RCTBridgeModule> @end`.

- **`ios/RCT<Pascal>Module.m`** — the implementation. **Generate complete working code, not TODO placeholders.** For each operation in PRD §4, the per-operation §3.<n> block prescribes every implementation decision (framework, hosting, key APIs, export shape, edge case handling). Generate the implementation verbatim from §3.<n>:

  - Imports: include `RCT<Pascal>Module.h`, `UIKit`, plus every framework named in ARCHITECTURE §3.<n>'s "Framework / class" field for any operation (e.g. `#import <PencilKit/PencilKit.h>` if any §3.<n> names PencilKit).
  - Module identity: **do NOT emit `RCT_EXPORT_MODULE(...)`** in a wrap plugin framework. That macro registers via `+load` and `_RCTRegisterModule`, which is not visible to the framework's `dlopen` flat namespace. Instead emit a class method `+ (NSString *)moduleName { return @"<Pascal>Module"; }` — the **`Module`-suffixed** name. The Obj-C class name stays `RCT<Pascal>Module` (matching `entrypoints.ios.moduleClass`), while `+moduleName` MUST equal the manifest's `receivers[].nativeModule` and JS sees `NativeModules.<Pascal>Module`. Do NOT strip the suffix.
  - `+ (BOOL)requiresMainQueueSetup` returning `NO` unless any §3.<n> requires main-thread init.
  - **`init` safety — the module is instantiated eagerly at load via `[cls new]`, so `init` MUST NOT throw or do heavy/side-effecting work** ([`ppmplugin-format §5`](../../shared/ppmplugin-format.md)). Do not acquire hardware, register `NSNotification`/KVO observers, or touch `AVCaptureSession`/`CLLocationManager` in `init` — defer to the first `RCT_EXPORT_METHOD` call (lazy), and wrap any unavoidable init work in `@try/@catch`. An uncaught exception in `init` crashes the host at launch (the iOS analogue of the Android Looper-less-`Handler` crash).
  - For each operation, write an `RCT_EXPORT_METHOD` taking **exactly one `NSDictionary *request` parameter**, then `RCTPromiseResolveBlock resolve`, `RCTPromiseRejectBlock reject` — e.g. `RCT_EXPORT_METHOD(capturePenInput:(NSDictionary *)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)`. This matches the wrap dispatch contract: the PCF sends `args: [request]` (a one-element array) spread positionally, so the method's first positional param is the request dictionary ([`ppmplugin-format §2`](../../shared/ppmplugin-format.md)). Read fields off `request` (`request[@"…"]`); do NOT expand into multiple positional params. Also: the Obj-C class MUST instantiate via a no-arg `[cls new]` after the runtime loads it — don't add a custom designated initializer that takes arguments. The body implements §3.<n>'s iOS spec **completely**:
    - The hosting setup ("dedicated UIViewController presented modally, full-screen" → emit a `UIViewController` subclass or inline VC + `presentViewController:animated:completion:`). **The presented VC's `viewDidLoad` MUST constrain custom content views to `view.safeAreaLayoutGuide`, not `view` directly** — this prevents content from intruding under the notch / Dynamic Island / home indicator. Set `modalPresentationStyle = UIModalPresentationFullScreen` (or `.pageSheet` per ARCHITECTURE §3.<n>). Add a `UINavigationBar` with Done / Cancel `UIBarButtonItem`s for clear action affordance — same Material-toolbar-equivalent pattern as Android.
    - The key API calls in the order §3.<n> specifies (e.g. `PKCanvasView` init, `PKToolPicker` attachment, drawing capture)
    - Each Done/Cancel/dismiss handler as §3.<n> specifies
    - The export step as §3.<n>'s "Export" line specifies (e.g. `drawing.image(from: canvas.bounds, scale: 2.0)` → PNG → base64)
    - Each edge case from §3.<n>'s "Edge cases handled" list, with the exact behavior named (e.g. "User taps Cancel → resolve with USER_CANCELLED")
  - Threading: background work on `dispatch_get_global_queue`; UI presentation on `dispatch_get_main_queue`. Long-running native work must not block the JS thread.
  - Error helper: emit `- (NSString *)errorJsonWithCode:(NSString *)code message:(NSString *)message` that builds the dict `@{@"status": @"error", @"error": code, @"message": (message ?: @"")}` and serializes it via **`NSJSONSerialization`** — the SAME serializer as the success helper. Do **NOT** use `stringWithFormat`: a `message` (or code) containing a `"`, `\`, or newline would emit invalid JSON, which the PCF's response parse would surface as a misleading `PARSE` instead of the real failure — defeating the whole point of the message. The `message` is a **human-readable diagnostic** that makes the failure debuggable from the PCF without a native debugger: for a caught exception pass `error.localizedDescription`; for a validation failure a specific reason (e.g. `@"missing required field 'uri'"`); for `USER_CANCELLED` a short note. **Every error path calls this with BOTH a code and a message — never a bare code.**
  - Success helper: emit `- (NSString *)successJsonWith:(NSDictionary *)result` that builds `{"status":"ok","result":<result>}` via `NSJSONSerialization`.
  - **Error propagation — wrap the operation body so every failure reaches the PCF with a code AND a message.** Any framework/runtime failure must `resolve` with `errorJsonWithCode:message:` carrying a specific code and reason — never throw an uncaught Obj-C exception, crash, or `resolve` empty. Use `@try/@catch` around risky synchronous work and resolve the `@catch` with `INTERNAL_ERROR` plus `exception.reason`.
  - UI hygiene boilerplate for each presented `UIViewController`'s `viewDidLoad` (mirrors Android's insets handling — prevents the most common iOS issue: content under safe areas, status bar, home indicator):
    ```objc
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor systemBackgroundColor];

        // Navigation bar with Done / Cancel — equivalent to Android's MaterialToolbar.
        UINavigationBar *navBar = [[UINavigationBar alloc] init];
        navBar.translatesAutoresizingMaskIntoConstraints = NO;
        UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:@"<Human-readable from PRD §2>"];
        navItem.leftBarButtonItem = [[UIBarButtonItem alloc]
            initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
            target:self action:@selector(handleCancel)];
        navItem.rightBarButtonItem = [[UIBarButtonItem alloc]
            initWithBarButtonSystemItem:UIBarButtonSystemItemDone
            target:self action:@selector(handleDone)];
        navBar.items = @[navItem];
        [self.view addSubview:navBar];

        // Content view — the operation-specific surface (e.g. PKCanvasView, AVCaptureVideoPreviewLayer host).
        // Constrain to safeAreaLayoutGuide so content doesn't extend under the notch / home indicator.
        UIView *contentView = [[UIView alloc] init];   // Replace with operation-specific view per ARCHITECTURE §3.<n>
        contentView.translatesAutoresizingMaskIntoConstraints = NO;
        [self.view addSubview:contentView];

        [NSLayoutConstraint activateConstraints:@[
            [navBar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
            [navBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
            [navBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
            [contentView.topAnchor constraintEqualToAnchor:navBar.bottomAnchor],
            [contentView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor],
            [contentView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor],
            [contentView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor],
        ]];
    }

    - (UIStatusBarStyle)preferredStatusBarStyle {
        // Adapt to system appearance — matches Android's windowLightStatusBar in light theme.
        return UIStatusBarStyleDefault;   // automatic light/dark per system
    }
    ```
  - Modal helper: emit `- (UIViewController *)topViewController` if any operation presents modally:
    ```objc
    - (UIViewController *)topViewController {
        UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;
        while (root.presentedViewController) { root = root.presentedViewController; }
        return root;
    }
    ```

  **No TODO placeholders. No `// implement this`.** If a §3.<n> block is incomplete (any "Key APIs and decisions" item is vague or missing), STOP with `NEEDS_CONTEXT: ARCHITECTURE §3.<n> implementation block is incomplete — re-run /design-native-extension-feature Step 7 (per-operation implementation walkthrough) to complete it`. Don't paper over a vague spec with a guess.

### 3.5 Android (`android/`)

Write:

- **`android/build.gradle`** — **library-only** gradle config. The module is consumed by the host's managed build, which provides the root project setup. (For the standalone `.ppmplugin` build, `/build-android-binary` compiles from a throwaway staged copy with pinned versions — this canonical file is never edited; see [`shared/ppmplugin-format.md §5`](../../shared/ppmplugin-format.md).) Do NOT emit a `buildscript { ... }`, `allprojects { ... }`, or any classpath declarations — those belong to the root project, not this library module.

  Library-only shape (this is the entire file — no preamble, no root-project blocks):
  ```gradle
  // <kebab>-control
  // Android library module — consumed by the host's managed build.

  apply plugin: 'com.android.library'
  apply plugin: 'kotlin-android'

  def safeExtGet(prop, fallback) {
      rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
  }

  android {
      namespace "com.powerapps.<lower>"
      compileSdkVersion safeExtGet('compileSdkVersion', 35)
      defaultConfig {
          minSdkVersion safeExtGet('minSdkVersion', <PRD min — default 24>)
          targetSdkVersion 35
      }
      compileOptions {
          sourceCompatibility JavaVersion.VERSION_17
          targetCompatibility JavaVersion.VERSION_17
      }
      kotlinOptions { jvmTarget = '17' }
  }

  dependencies {
      // 'react-android' (renamed from 'react-native' in RN 0.73). compileOnly + pinned:
      // the wrap shell provides RN at runtime, so never bundle it, and the legacy
      // 'react-native:+' coordinate does not resolve in the standalone build.
      // Read <rnVersion> from package.json devDependencies (currently 0.79.7).
      compileOnly "com.facebook.react:react-android:<rnVersion>"
      implementation 'androidx.appcompat:appcompat:1.6.1'
      implementation 'androidx.core:core-ktx:1.12.0'                  // WindowCompat / WindowInsetsCompat for UI hygiene
      implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // for the generated layout XML
      implementation 'com.google.android.material:material:1.11.0'    // Material 3 theme + components
      // Plus any ARCHITECTURE §1.3 / §1.4-specified additions (e.g. ML Kit, FusedLocationProvider)
  }
  ```

- **Files NOT to generate** (these are root-project / standalone-build concerns; the host's managed build — or, for the `.ppmplugin`, `/build-android-binary`'s staged copy — owns them):
  - `android/settings.gradle` — root project's responsibility
  - `android/gradle.properties` — root project's properties; `android.useAndroidX` and `android.enableJetifier` are supplied ambiently by the host (and generated into the staged copy by `/build-android-binary`), not by the library
  - `android/gradlew` + `android/gradle/wrapper/*` — the Gradle wrapper; library modules don't need their own wrapper
  - Any top-level `buildscript { ext, repositories, dependencies (classpath) }` block in `build.gradle` — the host provides AGP + Kotlin classpaths

  > **No standalone build script.** The `android/` directory is consumed by the host's managed build (and copied into a pinned staging dir by `/build-android-binary` for the `.ppmplugin`); it doesn't have to compile in isolation. Don't add a top-level `buildscript { ... }` / `allprojects { ... }` block — the host provides those. Standalone `./gradlew assembleDebug` against this directory is **not** a validation path we support (native compile happens in `/build-android-binary`, not here — see [`shared/ppmplugin-format.md §5`](../../shared/ppmplugin-format.md)).

- **`android/src/main/AndroidManifest.xml`** — registers permissions from ARCHITECTURE §1.4 AND the dedicated capture Activity (if ARCHITECTURE §3.<n> hosts in one) with a Material 3 theme:
  ```xml
  <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.powerapps.<lower>">
      <!-- One <uses-permission android:name="..." /> per entry in ARCHITECTURE §1.4 Android permissions -->

      <application>
          <!-- One <activity> per ARCHITECTURE §3.<n> that hosts in a dedicated Activity.
               Theme references generated themes.xml; screenOrientation per ARCHITECTURE §3.<n>. -->
          <activity
              android:name=".<Pascal>CaptureActivity"
              android:theme="@style/Theme.<Pascal>"
              android:screenOrientation="portrait"
              android:exported="false" />
      </application>
  </manifest>
  ```

- **`android/src/main/res/values/themes.xml`** — Material 3 theme so all components render with proper Material styling, not the bare AppCompat defaults. Without this, generated UIs hit issues like status bar overlap and unthemed buttons.
  ```xml
  <?xml version="1.0" encoding="utf-8"?>
  <resources xmlns:tools="http://schemas.android.com/tools">
      <style name="Theme.<Pascal>" parent="Theme.Material3.DayNight.NoActionBar">
          <!-- System bars: drawn by the OS but content extends behind them; the Activity applies insets. -->
          <item name="android:statusBarColor">@android:color/transparent</item>
          <item name="android:navigationBarColor">@android:color/transparent</item>
          <item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
          <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
      </style>
  </resources>
  ```

- **`android/src/main/res/layout/activity_<lower>_capture.xml`** — root layout uses Material components. Toolbar at top, bounded content area in a `MaterialCardView` (drawing surface, camera preview, etc.):
  ```xml
  <?xml version="1.0" encoding="utf-8"?>
  <androidx.constraintlayout.widget.ConstraintLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res/auto"
      android:id="@+id/root"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="?attr/colorSurface">

      <com.google.android.material.appbar.MaterialToolbar
          android:id="@+id/toolbar"
          android:layout_width="match_parent"
          android:layout_height="?attr/actionBarSize"
          android:elevation="4dp"
          app:layout_constraintTop_toTopOf="parent"
          app:menu="@menu/<lower>_capture_menu"
          app:navigationIcon="@drawable/ic_close"
          app:title="<Human-readable name from PRD §2>" />

      <com.google.android.material.card.MaterialCardView
          android:id="@+id/content_card"
          android:layout_width="0dp"
          android:layout_height="0dp"
          android:layout_margin="16dp"
          app:cardCornerRadius="8dp"
          app:cardElevation="2dp"
          app:layout_constraintTop_toBottomOf="@id/toolbar"
          app:layout_constraintBottom_toBottomOf="parent"
          app:layout_constraintStart_toStartOf="parent"
          app:layout_constraintEnd_toEndOf="parent">

          <!-- The operation-specific surface goes here: drawing View, camera SurfaceView,
               photo preview, etc. — substituted per ARCHITECTURE §3.<n>'s "Hosting" specification. -->
          <View
              android:id="@+id/capture_surface"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="?attr/colorSurfaceContainerLowest" />
      </com.google.android.material.card.MaterialCardView>
  </androidx.constraintlayout.widget.ConstraintLayout>
  ```

- **`android/src/main/res/menu/<lower>_capture_menu.xml`** — toolbar action items. **`action_done` is MANDATORY** for any capture-flow operation; without it the user has no way to submit. Additional actions (Clear, Undo, etc.) per ARCHITECTURE §3.<n>'s UI actions:
  ```xml
  <menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res/auto">
      <!-- MANDATORY for capture flows. NEVER omit Done — user cannot complete the operation otherwise. -->
      <item
          android:id="@+id/action_done"
          android:title="@string/action_done"
          app:showAsAction="always" />

      <!-- Optional: one <item> per additional toolbar action declared in ARCHITECTURE §3.<n>
           (e.g. Clear All, Undo). Set app:showAsAction="ifRoom" for non-critical actions. -->
  </menu>
  ```

- **For multi-mode capture operations (pen/eraser, photo/video, etc.):** the toolbar / mode-selection row uses `MaterialButtonToggleGroup`, not plain `Button`s. Toggle group provides the active-state visual feedback the user needs to know which mode is currently selected. Example layout fragment to include in `activity_<lower>_capture.xml`:
  ```xml
  <!-- Insert into the toolbar or just below it, when ARCHITECTURE §3.<n> has multiple modes. -->
  <com.google.android.material.button.MaterialButtonToggleGroup
      android:id="@+id/mode_toggle_group"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:singleSelection="true"
      app:selectionRequired="true">

      <!-- One <Button style="?attr/materialButtonOutlinedStyle"> per mode in ARCHITECTURE §3.<n>.
           Example for pen/eraser/clear: -->
      <Button android:id="@+id/mode_pen"     android:text="@string/mode_pen"     style="?attr/materialButtonOutlinedStyle" />
      <Button android:id="@+id/mode_eraser"  android:text="@string/mode_eraser"  style="?attr/materialButtonOutlinedStyle" />
  </com.google.android.material.button.MaterialButtonToggleGroup>
  ```
  And wire the listener in the Activity's `onCreate`:
  ```kotlin
  val toggleGroup: MaterialButtonToggleGroup = findViewById(R.id.mode_toggle_group)
  toggleGroup.check(R.id.mode_pen)   // default
  toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
      if (!isChecked) return@addOnButtonCheckedListener
      when (checkedId) {
          R.id.mode_pen -> captureSurface.setMode(<Pascal>Mode.PEN)
          R.id.mode_eraser -> captureSurface.setMode(<Pascal>Mode.ERASER)
      }
  }
  ```
  Without this, the user sees a row of identical-looking buttons and has no idea which mode is active. Confirmed UX-blocking failure mode in v0 extensions.

- **`android/src/main/res/values/strings.xml`** — string resources for the menu items + content descriptions (accessibility):
  ```xml
  <resources>
      <string name="action_done">Done</string>
      <!-- Plus one entry per ARCHITECTURE §3.<n> action; one content-description per accessible element. -->
  </resources>
  ```

- **`android/src/main/java/com/powerapps/<lower>/<Pascal>Module.kt`** — Kotlin native module. **Generate complete working code, not TODO placeholders.** For each operation, the per-operation §3.<n> block's "Android implementation" sub-section prescribes every implementation decision. Generate the implementation verbatim from §3.<n>:
  - Class: extends `ReactContextBaseJavaModule`.
  - `getName()` returns `"<Pascal>Module"` — the **`Module`-suffixed** name (matches `NativeModules.<Pascal>Module` on JS side, the iOS `+moduleName` return value, and the manifest's `receivers[].nativeModule`). Do NOT strip the suffix — it's the reserved-name dodge (see [`shared/ppmplugin-format.md §4`](../../shared/ppmplugin-format.md)).
  - For each operation, write a `@ReactMethod` function taking **exactly one `ReadableMap request` parameter** followed by `Promise promise` — e.g. `@ReactMethod fun capturePenInput(request: ReadableMap, promise: Promise)`. This matches the wrap dispatch contract: the PCF sends `args: [request]` (a one-element array) and the proxy does `fn.apply(mod, [request])`, so the method receives the request object as its single positional param ([`ppmplugin-format §2`](../../shared/ppmplugin-format.md)). Read each field off `request` (`request.getString("…")`, `request.getInt("…")`, etc.); do NOT expand the request into multiple positional params. The body implements §3.<n>'s Android spec **completely**:
    - The hosting (dedicated `Activity` via `Intent`, or `Fragment`, or in-place — whatever §3.<n> specifies)
    - The key API calls in the order §3.<n> specifies (e.g. `View.onTouchEvent` registration; `Path` accumulation; stylus pressure handling)
    - Each Done/Cancel handler as §3.<n> specifies
    - The export step as §3.<n>'s "Export" line specifies (e.g. render to `Bitmap`, compress to PNG, base64-encode)
    - Each edge case from §3.<n>'s "Edge cases handled" list, with the exact behavior named
  - If §3.<n> requires a dedicated `Activity`, emit it as a separate `.kt` file under the same package (e.g. `<Pascal>CaptureActivity.kt`) and register it in `AndroidManifest.xml`. The Activity's `onCreate` MUST emit the following UI hygiene boilerplate so the generated UI doesn't suffer from status bar overlap, missing Material theming, or rotation issues (these were repeat issues in v0 extensions):
    ```kotlin
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Edge-to-edge layout; we apply system-bar padding ourselves below.
        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContentView(R.layout.activity_<lower>_capture)

        // Pad root by status/nav bar insets so toolbar doesn't sit UNDER the status bar.
        // This is the fix for the most common Android UI bug in PAM extensions:
        // "buttons overlapping with system clock / status icons".
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root)) { v, insets ->
            val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
            WindowInsetsCompat.CONSUMED
        }

        // Toolbar with Done/Cancel via Material menu.
        val toolbar: MaterialToolbar = findViewById(R.id.toolbar)
        setSupportActionBar(toolbar)
        toolbar.setNavigationOnClickListener { onCancelled() }   // navigation icon = Cancel

        // Wire up the operation-specific surface (drawing View, camera preview, etc.)
        // — per ARCHITECTURE §3.<n>'s "Hosting" + "Key APIs and decisions" specification.
        val captureSurface: <PRD-§3.<n>-View-class> = findViewById(R.id.capture_surface)
        // ... operation-specific setup per §3.<n> ...
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.<lower>_capture_menu, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_done -> { onDone(); true }
            // Plus one branch per additional toolbar action declared in §3.<n>.
            else -> super.onOptionsItemSelected(item)
        }
    }
    ```
    Required imports: `androidx.core.view.WindowCompat`, `androidx.core.view.ViewCo

…(truncated)
