/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. 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). 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:
Read shared/shared-instructions.md.
Apply the per-skill minimal prereq policy (shared-instructions.md §1.5). This track is self-contained (shared-instructions §0a) 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 (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.
Read 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.
Read 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.
Read shared/repo-layout.md — the exact tree, file list, and package.json shape to emit.
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.
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. 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.
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)
- 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 §"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:
{
"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 §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:
# 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:
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): 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).
- 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 (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. block prescribes every implementation decision (framework, hosting, key APIs, export shape, edge case handling). Generate the implementation verbatim from §3.:
- Imports: include
RCT<Pascal>Module.h, UIKit, plus every framework named in ARCHITECTURE §3.'s "Framework / class" field for any operation (e.g. #import <PencilKit/PencilKit.h> if any §3. 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. 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). 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). 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.'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.). Add a UINavigationBar with Done / Cancel UIBarButtonItems for clear action affordance — same Material-toolbar-equivalent pattern as Android.
- The key API calls in the order §3. specifies (e.g.
PKCanvasView init, PKToolPicker attachment, drawing capture)
- Each Done/Cancel/dismiss handler as §3. specifies
- The export step as §3.'s "Export" line specifies (e.g.
drawing.image(from: canvas.bounds, scale: 2.0) → PNG → base64)
- Each edge case from §3.'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):- (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:- (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. 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.) 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):
// <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).
android/src/main/AndroidManifest.xml — registers permissions from ARCHITECTURE §1.4 AND the dedicated capture Activity (if ARCHITECTURE §3. hosts in one) with a Material 3 theme:
<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 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 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.'s UI actions:
<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 Buttons. 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:
<!-- 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:
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):
<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. block's "Android implementation" sub-section prescribes every implementation decision. Generate the implementation verbatim from §3.:
- 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).
- 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). Read each field off request (request.getString("…"), request.getInt("…"), etc.); do NOT expand the request into multiple positional params. The body implements §3.'s Android spec completely:
- The hosting (dedicated
Activity via Intent, or Fragment, or in-place — whatever §3. specifies)
- The key API calls in the order §3. specifies (e.g.
View.onTouchEvent registration; Path accumulation; stylus pressure handling)
- Each Done/Cancel handler as §3. specifies
- The export step as §3.'s "Export" line specifies (e.g. render to
Bitmap, compress to PNG, base64-encode)
- Each edge case from §3.'s "Edge cases handled" list, with the exact behavior named
- If §3. 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):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)
1---2name: generate-native-extension3description: 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.4---56# /generate-native-extension78Reads `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.910This 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.1112---1314## Step 1 — Read the shared docs and the PRD1516Before any write:17181. Read [`shared/shared-instructions.md`](../../shared/shared-instructions.md).192. 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).2021 **Print the prereq status as a visible block per `shared-instructions.md §9.2`** before continuing:2223 ```24 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━25 Prereq check — /generate-native-extension26 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━2728 🟢 ✓ git installed29 🟢 ✓ Node 20+ installed (optional — only to seed package.json devDeps from public npm)30 🟢 ✓ pnpm installed (optional — same)3132 🟢 checks passed. Ready to proceed.33 ```3435 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.363. 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.374. 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.385. Read [`shared/repo-layout.md`](../../shared/repo-layout.md) — the exact tree, file list, and `package.json` shape to emit.396. 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.`407. 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**.4142The 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.4344If any read fails, STOP and report which file is missing.4546---4748## Step 2 — Confirm the scaffold plan with the user4950Print a concise summary derived from the PRD, then gate on approval before any write.5152```53Scaffold plan54─────────────55Repo: powerapps-<kebab>56package: <kebab>-control (dev-only, private — not published)57Class: <Pascal>58Native module: <Pascal>Module → NativeModules.<Pascal>Module (== ./manifest.json receivers[].nativeModule)59iOS class: RCT<Pascal>Module (+moduleName returns <Pascal>Module)60Android module: <Pascal>Module (com.powerapps.<lower>)61Podspec: <Pascal>Extension.podspec (optional, system-frameworks-only)62Dispatch contract: ./manifest.json (committed — written by this skill; read by the PCF + build stage)6364Frameworks65 iOS: <list from ARCHITECTURE §1.2>66 Android: <list from ARCHITECTURE §1.3>6768Operations (<count from PRD §4>): <comma-separated names>69Pattern: <one-shot | streaming | two-way>70Error codes: <count from ARCHITECTURE §5>7172Target directory: <cwd> (writes <N> files; no existing files will be overwritten without confirm)73Distribution: the compiled `.ppmplugin` bundle (built later by /generate-ppmplugin). This skill is purely local — no remote, no feed, no registry.74```7576Use `AskUserQuestion` (single-select):7778> Proceed with this scaffold?79> - **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).80> - **Edit the PRD first** — exit; user re-runs `/design-native-extension-feature` to adjust.81> - **Cancel**8283---8485## Step 3 — Generate the files8687Write 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.8889Every file path is **relative to the current working directory** (the repo root). Names are derived per [`shared/naming-conventions.md`](../../shared/naming-conventions.md).9091### 3.1 Top-level repo files9293Write:9495- **`.gitignore`** — emit exactly the following entries:96 - Node: `node_modules/`, `dist/`, `build/`97 - **`.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))98 - OS / editor: `.DS_Store`, `.idea/`, `.vscode/`99 - Claude Code local state (per-user, not shared): `.claude/`100 - Env: `.env*` (but allow `!.env.example`)101 - iOS build: `Pods/`, `*.xcworkspace`, `DerivedData/`, `*.xcodeproj/xcuserdata/`102 - Android build: `*.iml`, `.gradle/`, `local.properties`, `captures/`, `.externalNativeBuild/`, `.cxx/`103 - 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}/`104 - Test-harness artifacts: `test-harness/*.msapp`105 - Skill-generated backups: `*.bak.*` (skills that replace tracked content may save a timestamped backup; those are intentionally local-only)106 - 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)107108- **`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`.109110 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:111 ```json112 {113 "name": "<kebab>-control",114 "version": "0.1.0",115 "private": true,116 "description": "<from PRD §1>",117 "devDependencies": {118 "react": "18.2.0",119 "react-native": "0.79.7"120 }121 }122 ```123 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.124125- **`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:126 - `name` = `kebab(<Pascal>)` of the **class** name (not the repo/capability name) — e.g. class `PenInput` → `pen-input`.127 - `version` = the `package.json` version (`0.1.0`).128 - `abi` = `{ "compatibleShells": ">=1.0.0", "builtAgainst": "1.0.0" }` (default; the build skills don't change it).129 - `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).130 - `entrypoints` = declare **every platform this scaffold generated** (so the committed manifest is the *full* contract; the build stage trims it to the shipped target):131 - Android → `"android": { "dex": "<Pascal>Plugin.dex", "packageClass": "com.powerapps.<lower>.<Pascal>Package" }`132 - iOS → `"ios": { "framework": "<Pascal>Plugin", "moduleClass": "RCT<Pascal>Module" }`133134 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.)135136- **`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.137138- **`CHANGELOG.md`** — single entry:139 ```markdown140 # Changelog141142 ## 0.1.0 — <ISO date>143144 - Initial scaffold for <Human-Readable Name> native control.145 - Generated by pam-native-extensions plugin from PRD.md.146 ```147148- **`LICENSE`** — MIT.149150### 3.2 The podspec (optional, at repo root)151152Write **`<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:153154```ruby155require "json"156157package_json = JSON.parse(File.read(File.join(__dir__, "package.json")))158159Pod::Spec.new do |s|160 s.name = "<Pascal>Extension"161 s.version = package_json["version"]162 s.summary = "<one-line description from PRD>"163 s.description = <<-DESC164 <2-3 sentence description from PRD — what it does, what it bridges to>165 DESC166 s.license = "MIT"167 s.author = { "Author" => "" }168 s.platform = :ios, "<min-deployment-target from ARCHITECTURE §1.2>"169 s.source = { :path => "." }170 s.source_files = "ios/**/*.{h,m}" # change to {h,m,swift} if Swift used171 s.frameworks = <comma-quoted list of SYSTEM frameworks from ARCHITECTURE §1.2>172 # No React-Core dependency: the .ppmplugin build resolves RN headers from the173 # react-native devDep in package.json, not via CocoaPods autolinking.174end175```176177### 3.3 No TypeScript layer — the dispatch contract178179This 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.180181The 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:182183- 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.184- 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)).185- 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.186187### 3.4 iOS (`ios/`)188189Write:190191- **`ios/RCT<Pascal>Module.h`** — minimal Obj-C header importing `<React/RCTBridgeModule.h>`, declaring `@interface RCT<Pascal>Module : NSObject <RCTBridgeModule> @end`.192193- **`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>:194195 - 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).196 - 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.197 - `+ (BOOL)requiresMainQueueSetup` returning `NO` unless any §3.<n> requires main-thread init.198 - **`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).199 - 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**:200 - 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.201 - The key API calls in the order §3.<n> specifies (e.g. `PKCanvasView` init, `PKToolPicker` attachment, drawing capture)202 - Each Done/Cancel/dismiss handler as §3.<n> specifies203 - The export step as §3.<n>'s "Export" line specifies (e.g. `drawing.image(from: canvas.bounds, scale: 2.0)` → PNG → base64)204 - 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")205 - 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.206 - 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.**207 - Success helper: emit `- (NSString *)successJsonWith:(NSDictionary *)result` that builds `{"status":"ok","result":<result>}` via `NSJSONSerialization`.208 - **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`.209 - 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):210 ```objc211 - (void)viewDidLoad {212 [super viewDidLoad];213 self.view.backgroundColor = [UIColor systemBackgroundColor];214215 // Navigation bar with Done / Cancel — equivalent to Android's MaterialToolbar.216 UINavigationBar *navBar = [[UINavigationBar alloc] init];217 navBar.translatesAutoresizingMaskIntoConstraints = NO;218 UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:@"<Human-readable from PRD §2>"];219 navItem.leftBarButtonItem = [[UIBarButtonItem alloc]220 initWithBarButtonSystemItem:UIBarButtonSystemItemCancel221 target:self action:@selector(handleCancel)];222 navItem.rightBarButtonItem = [[UIBarButtonItem alloc]223 initWithBarButtonSystemItem:UIBarButtonSystemItemDone224 target:self action:@selector(handleDone)];225 navBar.items = @[navItem];226 [self.view addSubview:navBar];227228 // Content view — the operation-specific surface (e.g. PKCanvasView, AVCaptureVideoPreviewLayer host).229 // Constrain to safeAreaLayoutGuide so content doesn't extend under the notch / home indicator.230 UIView *contentView = [[UIView alloc] init]; // Replace with operation-specific view per ARCHITECTURE §3.<n>231 contentView.translatesAutoresizingMaskIntoConstraints = NO;232 [self.view addSubview:contentView];233234 [NSLayoutConstraint activateConstraints:@[235 [navBar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],236 [navBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],237 [navBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],238 [contentView.topAnchor constraintEqualToAnchor:navBar.bottomAnchor],239 [contentView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor],240 [contentView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor],241 [contentView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor],242 ]];243 }244245 - (UIStatusBarStyle)preferredStatusBarStyle {246 // Adapt to system appearance — matches Android's windowLightStatusBar in light theme.247 return UIStatusBarStyleDefault; // automatic light/dark per system248 }249 ```250 - Modal helper: emit `- (UIViewController *)topViewController` if any operation presents modally:251 ```objc252 - (UIViewController *)topViewController {253 UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;254 while (root.presentedViewController) { root = root.presentedViewController; }255 return root;256 }257 ```258259 **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.260261### 3.5 Android (`android/`)262263Write:264265- **`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.266267 Library-only shape (this is the entire file — no preamble, no root-project blocks):268 ```gradle269 // <kebab>-control270 // Android library module — consumed by the host's managed build.271272 apply plugin: 'com.android.library'273 apply plugin: 'kotlin-android'274275 def safeExtGet(prop, fallback) {276 rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback277 }278279 android {280 namespace "com.powerapps.<lower>"281 compileSdkVersion safeExtGet('compileSdkVersion', 35)282 defaultConfig {283 minSdkVersion safeExtGet('minSdkVersion', <PRD min — default 24>)284 targetSdkVersion 35285 }286 compileOptions {287 sourceCompatibility JavaVersion.VERSION_17288 targetCompatibility JavaVersion.VERSION_17289 }290 kotlinOptions { jvmTarget = '17' }291 }292293 dependencies {294 // 'react-android' (renamed from 'react-native' in RN 0.73). compileOnly + pinned:295 // the wrap shell provides RN at runtime, so never bundle it, and the legacy296 // 'react-native:+' coordinate does not resolve in the standalone build.297 // Read <rnVersion> from package.json devDependencies (currently 0.79.7).298 compileOnly "com.facebook.react:react-android:<rnVersion>"299 implementation 'androidx.appcompat:appcompat:1.6.1'300 implementation 'androidx.core:core-ktx:1.12.0' // WindowCompat / WindowInsetsCompat for UI hygiene301 implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // for the generated layout XML302 implementation 'com.google.android.material:material:1.11.0' // Material 3 theme + components303 // Plus any ARCHITECTURE §1.3 / §1.4-specified additions (e.g. ML Kit, FusedLocationProvider)304 }305 ```306307- **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):308 - `android/settings.gradle` — root project's responsibility309 - `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 library310 - `android/gradlew` + `android/gradle/wrapper/*` — the Gradle wrapper; library modules don't need their own wrapper311 - Any top-level `buildscript { ext, repositories, dependencies (classpath) }` block in `build.gradle` — the host provides AGP + Kotlin classpaths312313 > **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)).314315- **`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:316 ```xml317 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.powerapps.<lower>">318 <!-- One <uses-permission android:name="..." /> per entry in ARCHITECTURE §1.4 Android permissions -->319320 <application>321 <!-- One <activity> per ARCHITECTURE §3.<n> that hosts in a dedicated Activity.322 Theme references generated themes.xml; screenOrientation per ARCHITECTURE §3.<n>. -->323 <activity324 android:name=".<Pascal>CaptureActivity"325 android:theme="@style/Theme.<Pascal>"326 android:screenOrientation="portrait"327 android:exported="false" />328 </application>329 </manifest>330 ```331332- **`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.333 ```xml334 <?xml version="1.0" encoding="utf-8"?>335 <resources xmlns:tools="http://schemas.android.com/tools">336 <style name="Theme.<Pascal>" parent="Theme.Material3.DayNight.NoActionBar">337 <!-- System bars: drawn by the OS but content extends behind them; the Activity applies insets. -->338 <item name="android:statusBarColor">@android:color/transparent</item>339 <item name="android:navigationBarColor">@android:color/transparent</item>340 <item name="android:windowLightStatusBar" tools:targetApi="m">true</item>341 <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>342 </style>343 </resources>344 ```345346- **`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.):347 ```xml348 <?xml version="1.0" encoding="utf-8"?>349 <androidx.constraintlayout.widget.ConstraintLayout350 xmlns:android="http://schemas.android.com/apk/res/android"351 xmlns:app="http://schemas.android.com/apk/res/auto"352 android:id="@+id/root"353 android:layout_width="match_parent"354 android:layout_height="match_parent"355 android:background="?attr/colorSurface">356357 <com.google.android.material.appbar.MaterialToolbar358 android:id="@+id/toolbar"359 android:layout_width="match_parent"360 android:layout_height="?attr/actionBarSize"361 android:elevation="4dp"362 app:layout_constraintTop_toTopOf="parent"363 app:menu="@menu/<lower>_capture_menu"364 app:navigationIcon="@drawable/ic_close"365 app:title="<Human-readable name from PRD §2>" />366367 <com.google.android.material.card.MaterialCardView368 android:id="@+id/content_card"369 android:layout_width="0dp"370 android:layout_height="0dp"371 android:layout_margin="16dp"372 app:cardCornerRadius="8dp"373 app:cardElevation="2dp"374 app:layout_constraintTop_toBottomOf="@id/toolbar"375 app:layout_constraintBottom_toBottomOf="parent"376 app:layout_constraintStart_toStartOf="parent"377 app:layout_constraintEnd_toEndOf="parent">378379 <!-- The operation-specific surface goes here: drawing View, camera SurfaceView,380 photo preview, etc. — substituted per ARCHITECTURE §3.<n>'s "Hosting" specification. -->381 <View382 android:id="@+id/capture_surface"383 android:layout_width="match_parent"384 android:layout_height="match_parent"385 android:background="?attr/colorSurfaceContainerLowest" />386 </com.google.android.material.card.MaterialCardView>387 </androidx.constraintlayout.widget.ConstraintLayout>388 ```389390- **`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:391 ```xml392 <menu xmlns:android="http://schemas.android.com/apk/res/android"393 xmlns:app="http://schemas.android.com/apk/res/auto">394 <!-- MANDATORY for capture flows. NEVER omit Done — user cannot complete the operation otherwise. -->395 <item396 android:id="@+id/action_done"397 android:title="@string/action_done"398 app:showAsAction="always" />399400 <!-- Optional: one <item> per additional toolbar action declared in ARCHITECTURE §3.<n>401 (e.g. Clear All, Undo). Set app:showAsAction="ifRoom" for non-critical actions. -->402 </menu>403 ```404405- **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`:406 ```xml407 <!-- Insert into the toolbar or just below it, when ARCHITECTURE §3.<n> has multiple modes. -->408 <com.google.android.material.button.MaterialButtonToggleGroup409 android:id="@+id/mode_toggle_group"410 android:layout_width="wrap_content"411 android:layout_height="wrap_content"412 app:singleSelection="true"413 app:selectionRequired="true">414415 <!-- One <Button style="?attr/materialButtonOutlinedStyle"> per mode in ARCHITECTURE §3.<n>.416 Example for pen/eraser/clear: -->417 <Button android:id="@+id/mode_pen" android:text="@string/mode_pen" style="?attr/materialButtonOutlinedStyle" />418 <Button android:id="@+id/mode_eraser" android:text="@string/mode_eraser" style="?attr/materialButtonOutlinedStyle" />419 </com.google.android.material.button.MaterialButtonToggleGroup>420 ```421 And wire the listener in the Activity's `onCreate`:422 ```kotlin423 val toggleGroup: MaterialButtonToggleGroup = findViewById(R.id.mode_toggle_group)424 toggleGroup.check(R.id.mode_pen) // default425 toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->426 if (!isChecked) return@addOnButtonCheckedListener427 when (checkedId) {428 R.id.mode_pen -> captureSurface.setMode(<Pascal>Mode.PEN)429 R.id.mode_eraser -> captureSurface.setMode(<Pascal>Mode.ERASER)430 }431 }432 ```433 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.434435- **`android/src/main/res/values/strings.xml`** — string resources for the menu items + content descriptions (accessibility):436 ```xml437 <resources>438 <string name="action_done">Done</string>439 <!-- Plus one entry per ARCHITECTURE §3.<n> action; one content-description per accessible element. -->440 </resources>441 ```442443- **`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>:444 - Class: extends `ReactContextBaseJavaModule`.445 - `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)).446 - 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**:447 - The hosting (dedicated `Activity` via `Intent`, or `Fragment`, or in-place — whatever §3.<n> specifies)448 - The key API calls in the order §3.<n> specifies (e.g. `View.onTouchEvent` registration; `Path` accumulation; stylus pressure handling)449 - Each Done/Cancel handler as §3.<n> specifies450 - The export step as §3.<n>'s "Export" line specifies (e.g. render to `Bitmap`, compress to PNG, base64-encode)451 - Each edge case from §3.<n>'s "Edge cases handled" list, with the exact behavior named452 - 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):453 ```kotlin454 override fun onCreate(savedInstanceState: Bundle?) {455 super.onCreate(savedInstanceState)456 // Edge-to-edge layout; we apply system-bar padding ourselves below.457 WindowCompat.setDecorFitsSystemWindows(window, false)458 setContentView(R.layout.activity_<lower>_capture)459460 // Pad root by status/nav bar insets so toolbar doesn't sit UNDER the status bar.461 // This is the fix for the most common Android UI bug in PAM extensions:462 // "buttons overlapping with system clock / status icons".463 ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root)) { v, insets ->464 val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())465 v.setPadding(bars.left, bars.top, bars.right, bars.bottom)466 WindowInsetsCompat.CONSUMED467 }468469 // Toolbar with Done/Cancel via Material menu.470 val toolbar: MaterialToolbar = findViewById(R.id.toolbar)471 setSupportActionBar(toolbar)472 toolbar.setNavigationOnClickListener { onCancelled() } // navigation icon = Cancel473474 // Wire up the operation-specific surface (drawing View, camera preview, etc.)475 // — per ARCHITECTURE §3.<n>'s "Hosting" + "Key APIs and decisions" specification.476 val captureSurface: <PRD-§3.<n>-View-class> = findViewById(R.id.capture_surface)477 // ... operation-specific setup per §3.<n> ...478 }479480 override fun onCreateOptionsMenu(menu: Menu): Boolean {481 menuInflater.inflate(R.menu.<lower>_capture_menu, menu)482 return true483 }484485 override fun onOptionsItemSelected(item: MenuItem): Boolean {486 return when (item.itemId) {487 R.id.action_done -> { onDone(); true }488 // Plus one branch per additional toolbar action declared in §3.<n>.489 else -> super.onOptionsItemSelected(item)490 }491 }492 ```493 Required imports: `androidx.core.view.WindowCompat`, `androidx.core.view.ViewCo494495…(truncated)