--- name: capacitor description: Wrap the mkstack web app as a native iOS and Android application using Capacitor. Provides haptics, native file downloads / share sheet, OS-level secure storage (Keychain / KeyStore), deep-link routing, status-bar theme sync, and safe-area CSS utilities. --- # Capacitor Native Wrapper This skill turns the web app into a native iOS and Android binary using [Capacitor](https://capacitorjs.com/) — no Swift or Kotlin required for the basics. The React UI runs unchanged inside a native WebView; the helpers below provide the cross-platform primitives that make it feel native. **Not included in the project by default.** Run the setup below when the user wants to ship to an app store or produce an `.apk` / `.ipa`. ## What this skill provides | Capability | Web behavior | Native behavior | |---|---|---| | **Haptics** (`impactLight`, `notificationSuccess`, …) | `navigator.vibrate()` on Android browsers | Taptic engine / Android haptics | | **`downloadTextFile(filename, content)`** | `` click | Writes to app Documents directory | | **`openUrl(url)`** | `window.open(url, '_blank')` | Native share sheet | | **`secureStorage` / `useSecureLocalStorage`** | `localStorage` | iOS Keychain / Android KeyStore, auto-migrates plaintext values | | **``** | no-op | Forwards OS `appUrlOpen` into React Router | | **`bootstrapNative()`** | no-op | Hides iOS keyboard accessory bar; syncs system-bar icon style with theme | | **Safe-area utilities** (Tailwind) | `env(safe-area-inset-*)` | `env(safe-area-inset-*)` + `SystemBars` back-fill on Android | Every helper is SSR-safe and web-safe — import and call unconditionally from shared components. Each source file ships with a JSDoc header covering the exact API, gotchas, and usage examples; read them after copying. ## Files to copy Copy from `.agents/skills/capacitor/files/` into the matching project location: | Skill file | Copy to | |---|---| | `files/lib/haptics.ts` | `src/lib/haptics.ts` | | `files/lib/downloadFile.ts` | `src/lib/downloadFile.ts` | | `files/lib/secureStorage.ts` | `src/lib/secureStorage.ts` | | `files/lib/nativeBootstrap.ts` | `src/lib/nativeBootstrap.ts` | | `files/hooks/useSecureLocalStorage.ts` | `src/hooks/useSecureLocalStorage.ts` | | `files/components/DeepLinkHandler.tsx` | `src/components/DeepLinkHandler.tsx` | | `files/capacitor.config.ts` | `capacitor.config.ts` (project root — edit `appId`, `appName`, `scheme`, background colors) | | `files/scripts/patch-cap-config.mjs` | `scripts/patch-cap-config.mjs` — only needed if you add **local** (non-SPM) native plugin classes; otherwise skip | | `files/safe-area-shim.css` | `src/safe-area-shim.css` — **only if** you need Android WebView <140 support (see shim file header and troubleshooting below) | ## Setup ### 1. Install ```bash # Runtime + plugins npm install @capacitor/core @capacitor/app @capacitor/filesystem \ @capacitor/haptics @capacitor/keyboard @capacitor/share \ capacitor-secure-storage-plugin # Toolchain npm install -D @capacitor/cli @capacitor/android @capacitor/ios # Safe-area utilities (Tailwind v4 — this project) npm install -D tailwindcss-safe-area ``` mkstack ships **Tailwind v4** with CSS-first config (`@import "tailwindcss"` in `src/index.css`, no `tailwind.config.ts`). If you're adapting these instructions to a Tailwind v3 project, install `tailwindcss-safe-area@0.8.0` instead and register it in `tailwind.config.ts`'s `plugins` array — `1.x` emits no utilities on v3. ### 2. Copy the skill files Copy every file from the table above. ### 3. Register the safe-area plugin Add a second `@import` to `src/index.css`, right after the existing `@import "tailwindcss";`: ```css @import "tailwindcss"; @import "tailwindcss-safe-area"; @import "tw-animate-css"; ``` **Use `@import`, not `@plugin`.** The package ships a plain CSS file for Tailwind v4, not a JS plugin entry point. ### 4. Fill in `capacitor.config.ts` Edit `appId` (reverse-DNS; **cannot be changed after publishing**), `appName`, `ios.scheme`, and the iOS/Android `backgroundColor` values. All other defaults in the skill's config are production-safe. ### 5. Update `index.html` iOS won't expose `env(safe-area-inset-*)` without `viewport-fit=cover`: ```html ``` ### 6. Wire up bootstrap and deep links Call `bootstrapNative()` **before** React mounts, so system bars are themed at first paint: ```tsx // src/main.tsx import { bootstrapNative } from '@/lib/nativeBootstrap'; bootstrapNative(); createRoot(document.getElementById('root')!).render(); ``` Render `` **inside** `` so `useNavigate()` works: ```tsx // src/AppRouter.tsx {/* … */} ``` ### 7. Add platforms and build ```bash npx cap add android npx cap add ios # macOS only npm run build # produces dist/ npx cap sync # copies dist/ into android/ and ios/ npx cap open android # Android Studio; hit Run npx cap open ios # Xcode (macOS only); hit Run ``` Commit the generated `android/` and `ios/` directories (signing keys stay excluded via the generated `.gitignore`s). If you later add a **local** (non-SPM) Capacitor plugin class, append `node scripts/patch-cap-config.mjs` to your sync command so `packageClassList` survives each `npx cap sync`. ## Using the APIs Each file's JSDoc header has full usage details. Quick tour: ```tsx import { impactLight, notificationSuccess } from '@/lib/haptics'; import { downloadTextFile, openUrl } from '@/lib/downloadFile'; import { secureStorage } from '@/lib/secureStorage'; import { useSecureLocalStorage } from '@/hooks/useSecureLocalStorage'; impactLight(); // fire-and-forget notificationSuccess(); // silent no-op on unsupported platforms await downloadTextFile('export.json', JSON.stringify(data)); await openUrl('https://example.com'); await secureStorage.setItem('nwc:active', conn); await secureStorage.getItem('nwc:active'); const [conn, setConn, ready] = useSecureLocalStorage('nwc:active', null); if (!ready) return ; // native reads are async ``` ## Safe-area utilities Once step 3 is done, `*-safe` utilities are available for padding, margin, position, height, border, and scroll properties. The most useful ones: - `pt-safe`, `pb-safe`, `px-safe`, `py-safe`, `p-safe` (also logical `ps-`/`pe-` and `start-`/`end-` positions) - `top-safe`, `bottom-safe`, `inset-safe`, `inset-x-safe` - `h-dvh-safe`, `min-h-dvh-safe`, `h-svh-safe`, `h-lvh-safe`, `h-screen-safe` - `{prop}-safe-offset-{n}` — safe area **plus** `{n}` (`pb-safe-offset-4` = inset + 1rem) - `{prop}-safe-or-{n}` — `max(safe-area, {n})` (`pb-safe-or-8` = at least 2rem) ```tsx
…
…
…
``` Full list: [plugin README](https://github.com/mvllow/tailwindcss-safe-area). ## Common follow-ups - **App icons / splash screen** — standard icon generator → `android/app/src/main/res/mipmap-*/` and `ios/App/App/Assets.xcassets/AppIcon.appiconset/`. For splash, add `@capacitor/splash-screen` and configure under `plugins.SplashScreen`. - **Push notifications** — add `@capacitor/push-notifications` (FCM / APNs); the web side keeps using Web Push. - **Deep-link verification** — host `apple-app-site-association` and `assetlinks.json` on your domain so the OS opens verified `https://` links directly. Full checklist in the `DeepLinkHandler.tsx` header. - **Nsec autofill via iCloud Keychain / Credential Manager** — add `@capgo/capacitor-autofill-save-password` and wrap it analogously to `secureStorage`. ## Troubleshooting - **Safe-area utilities render as `0` on Android** — Chromium WebView <140 reports `env(safe-area-inset-*)` as `0` ([bug 40699457](https://issues.chromium.org/issues/40699457)). `SystemBars` (`insetsHandling: 'css'`, already set) injects `--safe-area-inset-*` CSS variables, but `tailwindcss-safe-area` uses `env(…)` directly. **Preferred fix:** target WebView 140+ (Aug 2025; auto-updated on Android ≥7). **Fallback:** copy `files/safe-area-shim.css` to `src/` and `@import` it **after** `@import "tailwindcss-safe-area";` so the variables take over when `env()` is 0. - **`pt-safe` doesn't exist** — either `@import "tailwindcss-safe-area";` is missing / in the wrong position (must come **after** `@import "tailwindcss";`), or you installed `0.8.0` (v3-only) on a v4 project. `npm ls tailwindcss-safe-area` should show `>=1.0.0`. - **Safe-area is 0 on iOS Safari / simulator** — missing `viewport-fit=cover` in the `` tag. - **Deep links don't navigate** — `` must be **inside** ``; `useNavigate()` throws silently otherwise. - **iOS keyboard still shows the accessory bar** — `bootstrapNative()` must run **before** any `` is focused. Invoke at the top of `main.tsx`. Remaining edge cases (`secureStorage` returning `null` after debug reinstall, status-bar theming with custom CSS-variable setups) are covered in the corresponding file headers.