$npx -y skills add microsoft/power-platform-skills --skill add-pen-inputInternal implementation skill invoked by /add-native for pen, signature, ink, drawing, and handwriting capture workflows using @microsoft/power-apps-native-pen-input.
| 1 | **📋 Shared instructions: [shared-instructions.md](${CLAUDE_SKILL_DIR}/../../../shared/shared-instructions.md)** — read first. |
| 2 | |
| 3 | # Add Pen Input |
| 4 | |
| 5 | **Internal helper.** Users should invoke `/add-native pen-input`, `/add-native signature`, or `/add-native @microsoft/power-apps-native-pen-input`; `/add-native` routes here after resolving the capability. |
| 6 | |
| 7 | Generate or verify the native pen input wrapper and show how to call its **native React Native API**. Do not use the HostingSDK / PCF path from the package README; that is for a different use case. |
| 8 | |
| 9 | ## Steps |
| 10 | |
| 11 | ### 1. Verify app |
| 12 | |
| 13 | ```bash |
| 14 | test -f app.config.js && test -f power.config.json && test -f package.json && test -d src |
| 15 | ``` |
| 16 | |
| 17 | If this fails, tell the user to run `/create-mobile-app` first and STOP. |
| 18 | |
| 19 | ### 2. Verify package is already present |
| 20 | |
| 21 | ```bash |
| 22 | node -e "const p=require('./package.json'); const m='@microsoft/power-apps-native-pen-input'; if (!p.dependencies?.[m]) { console.error('MISSING: ' + m + ' is not in package.json. The template/app must already ship this native extension. This skill will not install it or edit native config.'); process.exit(1); } console.log('OK: pen input package present');" |
| 23 | ``` |
| 24 | |
| 25 | If the check fails, STOP. Do not run `npm install`, `npx expo install`, `pod install`, or edit `app.config.js`. This package contains native iOS/Android code and must already be part of the app's native build. |
| 26 | |
| 27 | ### 3. Write or verify `src/native/penInput.ts` |
| 28 | |
| 29 | Create `src/native/penInput.ts` if it does not exist. If it already exists, inspect it and patch only if cancellation is treated as an error or the wrapper can throw. |
| 30 | |
| 31 | The wrapper MUST: |
| 32 | |
| 33 | - Return a discriminated union and never throw. |
| 34 | - Return `{ ok: false, reason: 'USER_CANCELLED' }` for user cancellation; this is a non-error path. |
| 35 | - Return `NATIVE_MODULE_MISSING` when the extension is installed in JS but unavailable in the native build. |
| 36 | - Return a PNG data URI (`data:image/png;base64,...`) on success. |
| 37 | |
| 38 | ```ts |
| 39 | // src/native/penInput.ts |
| 40 | import { |
| 41 | PenInputNative, |
| 42 | PenInputStatus, |
| 43 | PenInputErrorCode, |
| 44 | } from '@microsoft/power-apps-native-pen-input'; |
| 45 | |
| 46 | export type PenInputResult = |
| 47 | | { ok: true; dataUri: string } |
| 48 | | { ok: false; reason: 'USER_CANCELLED' | 'NATIVE_MODULE_MISSING' | 'CAPTURE_FAILED'; message?: string }; |
| 49 | |
| 50 | export async function captureSignature(options?: { |
| 51 | backgroundColor?: string; |
| 52 | strokeColor?: string; |
| 53 | strokeWidth?: number; |
| 54 | }): Promise<PenInputResult> { |
| 55 | if (!PenInputNative?.capturePenInput) { |
| 56 | return { ok: false, reason: 'NATIVE_MODULE_MISSING', message: 'Pen input module is not available in this build.' }; |
| 57 | } |
| 58 | |
| 59 | try { |
| 60 | const result = await PenInputNative.capturePenInput({ |
| 61 | backgroundColor: '#ffffff', |
| 62 | strokeColor: '#0078d4', |
| 63 | strokeWidth: 2, |
| 64 | ...options, |
| 65 | }); |
| 66 | |
| 67 | if (result.status === PenInputStatus.Ok && result.result) { |
| 68 | return { ok: true, dataUri: result.result }; |
| 69 | } |
| 70 | |
| 71 | if (result.error === PenInputErrorCode.UserCancelled) { |
| 72 | return { ok: false, reason: 'USER_CANCELLED' }; |
| 73 | } |
| 74 | |
| 75 | return { ok: false, reason: 'CAPTURE_FAILED', message: result.error }; |
| 76 | } catch (error: any) { |
| 77 | return { ok: false, reason: 'CAPTURE_FAILED', message: error?.message ?? String(error) }; |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | export function stripDataUriPrefix(dataUri: string): string { |
| 82 | return dataUri.replace(/^data:image\/png;base64,/, ''); |
| 83 | } |
| 84 | ``` |
| 85 | |
| 86 | ### 4. Use the wrapper |
| 87 | |
| 88 | Screens import the wrapper, not the native package directly: |
| 89 | |
| 90 | ```ts |
| 91 | import { captureSignature } from '@/native/penInput'; |
| 92 | |
| 93 | const result = await captureSignature({ |
| 94 | backgroundColor: "#ffffff", |
| 95 | strokeColor: "#0078d4", |
| 96 | strokeWidth: 2, |
| 97 | }); |
| 98 | |
| 99 | if (result.ok) { |
| 100 | setSignatureUri(result.dataUri); |
| 101 | } else if (result.reason === 'USER_CANCELLED') { |
| 102 | // User cancelled; do not show this as an app error. |
| 103 | } else { |
| 104 | console.warn("Failed to capture pen input:", result.reason, result.message); |
| 105 | } |
| 106 | ``` |
| 107 | |
| 108 | Display the captured PNG with a normal React Native image: |
| 109 | |
| 110 | ```tsx |
| 111 | {signatureUri ? ( |
| 112 | <Image |
| 113 | source={{ uri: signatureUri }} |
| 114 | style={{ width: "100%", height: 160 }} |
| 115 | resizeMode="contain" |
| 116 | /> |
| 117 | ) : null} |
| 118 | ``` |
| 119 | |
| 120 | Notes: |
| 121 | - The result is a PNG data URI: `data:image/png;base64,...`. |
| 122 | - Color inputs support `#RGB` and `#RRGGBB`. |
| 123 | - Cancel is normal and returns `USER_CANCELLED`; screens should leave current state unchanged and avoid failure banners. |
| 124 | - Use `@microsoft/power-apps-native-pen-input` only for native freehand drawing, ink, handwriting, and signature capture. For unrelated native use cases, use the relevant Expo module or other dependency already present in `package.json`. |
| 125 | |
| 126 | ### 5. Optional Dataverse save |
| 127 | |
| 128 | If the user wants to save the signature to a Dataverse Image/File column, use generated services only. Do not w |