ocyan.plugin.mobileapp
======================

Production-ready mobile app plugin for MandelBlog / Ocyan websites.

This plugin gives a Wagtail site a clean mobile-app foundation:

- Progressive Web App manifest generation
- service worker and offline fallback support
- Apple / Android app metadata
- per-site Wagtail mobile app settings
- Capacitor project shell generation for Android and iOS packaging
- native capability planning for push, camera, files, geolocation, QR, biometrics, sharing, and deep links
- generated mobile upload bridge for camera and file-picker flows
- automatic branding asset generation for PWA, Android, and iOS packaging
- capability registry metadata for implemented, planned, and experimental native features
- staff-only admin status dashboard
- validation command for rollout readiness

Important: this repository keeps the established runtime package path
`ocyan.plugin.mobileapp` for compatibility with the existing MandelBlog stack.

What it does
------------

The plugin lets one Wagtail site act as:

- a Progressive Web App
- the source website for an Android app shell
- the source website for an iOS app shell
- a reusable generated client package based on the same configuration

The generated native app shell is intentionally conservative. It routes to the
configured website and prepares a Capacitor project, but it does not pretend
that a thin website wrapper alone is enough for App Store or Play Store review.

Installation
------------

Add the plugin to `ocyan.json`:

.. code-block:: json

   {
     "ocyan_plugins": ["ocyan.plugin.mobileapp"]
   }

Install the package and run migrations:

.. code-block:: bash

   bin/manage.py migrate mobileapp

Configure the site in Wagtail:

`Settings -> Mobile app settings`

Site settings
-------------

Per site, the plugin stores:

- PWA enabled flag
- Android enabled flag
- iOS enabled flag
- app name
- short name
- app ID / bundle identifier
- theme color
- background color
- start URL
- scope
- offline fallback URL
- icon image
- splash image
- foreground color
- accent color
- status bar color
- navigation bar color
- description
- orientation
- display mode
- push notifications enabled flag (reserved for later integrations)
- native capability planning flags:

  - enable push notifications
  - enable camera uploads
  - enable file picker
  - enable geolocation
  - enable QR scanner
  - enable biometrics
  - enable share sheet
  - enable deep links

These flags are intentionally planning-focused. They document intended native
value and feed generated app metadata, but they do not silently add native
plugins, permissions, or platform entitlements.

Capability model
----------------

The plugin now uses a capability registry as the source of truth for generated
mobile feature metadata. Each capability advertises:

- a stable key
- display name and description
- required Capacitor packages, if any
- supported platforms
- implementation status
- whether runtime verification is still required

Status meanings:

- `implemented`: the generator already produces real metadata, shell behavior,
  or bridge output for the capability
- `planned`: the capability is modeled and documented, but native behavior is
  still deferred
- `experimental`: partial support exists, but it still needs stricter runtime
  validation before being treated as production-native

Current registry entries:

- `camera`
- `file_picker`
- `downloads`
- `external_links`
- `offline`
- `pwa`
- `deep_links`
- `push_notifications`
- `secure_storage`
- `biometrics`
- `qr_scanner`
- `share_sheet`
- `analytics`
- `crash_reporting`

Only capabilities that are actually generated today are marked as
`implemented`.

Template integration
--------------------

Load the template tags in theme templates:

.. code-block:: django

   {% load mobile_app_tags %}
   {% mobile_app_head %}
   {% mobile_app_service_worker %}
   {% mobile_app_bridge %}

The plugin also ships a front extension template that can inject those tags into
the shared layout when the host stack loads plugin extensions.

`{% mobile_app_bridge %}` renders nothing unless camera uploads, file picker,
or share-sheet support are enabled for the current site. When enabled, it injects a safe bridge
script that:

- uses Capacitor Camera when available
- uses the Capawesome file picker plugin when available
- uses Capacitor Share when available
- falls back to browser `<input type="file">` behavior outside Capacitor
- falls back to `navigator.share()` when the browser supports it
- never requests permissions on page load

Public routes
-------------

- `/manifest.json`
- `/serviceworker.js`
- `/offline/`

Management commands
-------------------

Validate one site:

.. code-block:: bash

   python manage.py mobileapp_check --site-id=1

Generate the project shell:

.. code-block:: bash

   python manage.py mobileapp_init --site-id=1
   python manage.py mobileapp_generate --site-id=1

Print Android / iOS next steps:

.. code-block:: bash

   python manage.py mobileapp_build_android --site-id=1
   python manage.py mobileapp_build_ios --site-id=1

Run conservative Android verification checks:

.. code-block:: bash

   python manage.py mobileapp_verify_android --site-id=1

Generated project layout
------------------------

.. code-block:: text

   mobile_apps/
     site_1/
       package.json
       capacitor.config.ts
       dist/
         index.html
       README.md
       ANDROID.md
       IOS.md
       assets/
         app-config.json
         app-icon.png
         app-shell.css
         app-shell.js
         adaptive-icon.png
         adaptive-icon-foreground.png
         adaptive-icon-background.png
         splash.png
         favicon-32.png
         favicon-180.png
         icon-48.png
         icon-72.png
         icon-96.png
         icon-144.png
         icon-192.png
         icon-512.png
         mobile-bridge.js
         branding.json

Admin dashboard
---------------

The Wagtail admin menu includes a **Mobile App** entry for staff users. It shows:

- per-site status checks
- manifest path
- service worker path
- offline fallback path
- generated output directory
- suggested build commands
- native store readiness warning
- capability planning summary
- recommended Capacitor packages for enabled capability flags
- Android / iOS permission placeholder copy
- deep-link rollout notes
- branding asset readiness and generated asset inventory

Branding and asset generation
-----------------------------

`mobileapp_generate --site-id=<id>` now generates a branding asset package from
the uploaded Wagtail icon and splash settings.

One uploaded square icon is resized automatically for:

Android:

- 48
- 72
- 96
- 144
- 192
- 512

PWA:

- 192
- 512

Apple:

- 180

Favicon:

- 32

Adaptive icons:

- `adaptive-icon.png`
- `adaptive-icon-foreground.png`
- `adaptive-icon-background.png`

Splash:

- `splash.png`

Recommended source dimensions:

- app icon: at least `1024x1024`, square
- splash image: at least `2048x2048`

The generated splash uses a solid background color and centers the uploaded
logo/splash content. If no splash image is uploaded, the app icon is used as
the fallback source.

Branding manifest
-----------------

The generated package includes `assets/branding.json`.

Example:

.. code-block:: json

   {
     "appName": "Example App",
     "themeColor": "#112233",
     "backgroundColor": "#ffffff",
     "icons": [
       {"filename": "icon-192.png", "size": "192x192"}
     ],
     "generatedAt": "2026-07-03T12:00:00+00:00",
     "generator": "ocyan.plugin.mobileapp"
   }

Regeneration
------------

To regenerate branding and native shell outputs after changing site settings:

.. code-block:: bash

   python manage.py mobileapp_generate --site-id=1

Native UX shell
---------------

The generator now produces a small deterministic app shell layer:

- `dist/index.html`
- `assets/app-shell.css`
- `assets/app-shell.js`

Implemented behavior:

- branded first-paint shell for the generated package
- mobile viewport handling with `viewport-fit=cover`
- safe-area padding for notches and home indicators
- simple online/offline state messaging
- non-invasive reload action
- generated status-bar and splash color metadata in Capacitor config and `app-config.json`

This shell is tooling-independent and testable without Android or iOS SDKs.
It does **not** prove native platform builds. In the current architecture, the
real Capacitor runtime still depends on the configured remote server URL.

What is verified without native tooling:

- shell files are generated deterministically
- safe-area CSS is present
- offline/online state script is present
- shell references match the generated asset paths

Generated PWA / offline layer
-----------------------------

The generator now produces a self-contained PWA shell inside `dist/`:

- `dist/manifest.webmanifest`
- `dist/service-worker.js`
- `dist/offline.html`
- `dist/assets/app-shell.css`
- `dist/assets/app-shell.js`

Implemented behavior:

- manifest metadata for installability review
- defensive service worker registration from the generated shell
- shell/static asset precaching only
- offline fallback for navigation requests
- cache versioning derived from site id and build number
- no aggressive caching of authenticated/admin/business data

What is verified without native tooling:

- generated manifest content and icon references
- generated service worker content and cache version metadata
- shell registration code is present and defensive
- generated app-config metadata includes PWA readiness information

What still needs browser/device verification:

- real browser installability behavior
- service worker lifecycle behavior in target browsers/WebViews
- offline navigation behavior inside real WebViews
- any platform-specific service worker limitations in embedded runtimes

Production readiness metadata
-----------------------------

The generator now also includes production-readiness metadata that does not
require Android or iOS tooling:

- app version and build number
- external-link handling policy
- download-handling readiness notes
- deep-link association metadata
- runtime diagnostics helpers

Generated locations:

- `package.json` under `mobileApp`
- `assets/app-config.json`
- the Wagtail admin **Mobile App** dashboard summary

External link policy
--------------------

Implemented in generated metadata and shell diagnostics:

- same-origin URLs are expected to stay inside the WebView
- cross-origin URLs are expected to open in the system browser
- `mailto:` and `tel:` are treated as external intents
- blocked schemes include `javascript:`, `data:`, and `file:`

The generated shell exposes:

- `window.MandelMobileAppDiagnostics.isExternalUrl(url)`
- `window.MandelMobileAppDiagnostics.openExternal(url)`

This is policy and shell-level behavior only. Real native browser-handling
still needs runtime verification on Android and iOS.

Downloads and file handling readiness
-------------------------------------

Implemented:

- generated download policy metadata
- `window.MandelMobileAppDiagnostics.describeDownload(url)` helper
- bridge-level file input and native file-picker planning

Not yet implemented:

- native download-manager integration
- OS-level save/share UX verification
- platform-specific MIME/preview handling

Deep link readiness
-------------------

Implemented:

- generated associated-domain metadata
- planned Android Asset Links path:
  `/.well-known/assetlinks.json`
- planned Apple association path:
  `/.well-known/apple-app-site-association`

Not verified without native tooling:

- Android App Links association
- iOS Universal Links association
- app-open behavior on device

App version metadata
--------------------

The site settings now include:

- app version
- build number

These values are carried into the generated package metadata so release builds
can be tracked before native store tooling is added.

Runtime diagnostics
-------------------

The generated shell now exposes:

- `window.MandelMobileAppDiagnostics.getSnapshot()`
- `window.MandelMobileAppDiagnostics.isExternalUrl(url)`
- `window.MandelMobileAppDiagnostics.openExternal(url)`
- `window.MandelMobileAppDiagnostics.describeDownload(url)`

This is intended for local verification and host-project debugging. It does not
replace real platform logging or native diagnostics.

PWA diagnostics
---------------

The diagnostics snapshot now includes:

- manifest path
- service worker path
- cache version
- service worker support flag
- online/offline state
- standalone display-mode detection

This is enough to verify the generated package contract locally. It is not
proof that the browser or native shell actually grants installability or
offline support on a target device.

Android verification flow
-------------------------

Implemented by the generator:

- `package.json` with Capacitor Android dependencies
- `capacitor.config.ts` with app id, app name, `webDir`, and server URL
- generated branding assets and `assets/branding.json`
- `assets/mobile-bridge.js` when camera uploads, file picker, share-sheet, or QR-scanner support are enabled

Requires local tooling:

- JDK 17 or newer available to `java`
- Android SDK command-line tools or Android Studio
- `ANDROID_HOME` / `ANDROID_SDK_ROOT`
- accepted Android SDK licenses

Recommended real Android verification flow:

.. code-block:: bash

   python manage.py mobileapp_generate --site-id=1
   cd mobile_apps/site_1
   npm install
   npx cap add android
   npx cap sync android
   npx cap doctor android
   npx cap open android

If the local Android SDK and Java runtime are available, you can also try:

.. code-block:: bash

   cd android
   ./gradlew assembleDebug

What is verified without Android tooling:

- generated Capacitor config structure
- generated Android package id / app id values
- generated branding assets and bridge script presence
- Android project sync viability up to the locally available toolchain

What is not verified until local tooling is present:

- `npx cap doctor android` with a working SDK
- `./gradlew assembleDebug`
- APK production

The generator now includes:

- `devDependencies.typescript` for `capacitor.config.ts`
- a placeholder `dist/index.html` so `webDir` exists during sync
- generated `assets/app-shell.css` and `assets/app-shell.js` for first-paint polish
- root-level `ANDROID.md` / `IOS.md` setup notes instead of placeholder native platform directories

iOS verification flow
---------------------

Implemented by the generator:

- `package.json` with Capacitor iOS dependency metadata
- `capacitor.config.ts` with bundle id, app name, `webDir`, and server URL
- generated branding assets and permission-planning notes

Requires local tooling:

- full Xcode, not only Command Line Tools
- CocoaPods
- iOS Simulator SDK installed via Xcode

Recommended real iOS verification flow:

.. code-block:: bash

   python manage.py mobileapp_generate --site-id=1
   cd mobile_apps/site_1
   npm install
   npx cap add ios
   npx cap sync ios
   npx cap doctor ios
   npx cap open ios

If Xcode and CocoaPods are available, you can also try:

.. code-block:: bash

   cd ios/App
   xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug -sdk iphonesimulator build

What is verified without iOS tooling:

- generated Capacitor config structure
- generated iOS bundle id / app id values
- generated branding assets and permission-planning docs

What is not verified until local tooling is present:

- `npx cap add ios`
- `npx cap sync ios`
- `npx cap doctor ios`
- Xcode simulator build

Offline and network handling
----------------------------

Implemented WebView / PWA behavior:

- generated shell script listens to browser `online` / `offline` events
- generated shell updates visible state without requesting permissions
- plugin offline fallback page now uses a mobile-safe layout with safe-area padding
- service worker continues to provide the plugin offline route for the website side

Not yet verified without platform tooling:

- WebView-specific runtime nuances on real Android/iOS devices
- native app resume / reconnect behavior after backgrounding
- platform-specific status bar and splash interactions

Generated capability planning
-----------------------------

`mobileapp_generate --site-id=<id>` now carries native capability planning into:

- `assets/app-config.json`
- generated `README.md`
- `package.json` custom metadata
- Wagtail admin dashboard status output

The plugin keeps `package.json` dependencies conservative. It does not auto-add
every possible Capacitor plugin. Instead it lists recommended packages for the
enabled capability flags so the host project can add only what it really uses.

Generated capability metadata now includes:

- the full capability registry
- enabled capability keys
- implemented capability keys
- planned capability keys
- experimental capability keys
- runtime-verification backlog entries

For real upload support, the generator now adds these dependencies only when the
matching capability is enabled:

- `@capacitor/camera`
- `@capawesome/capacitor-file-picker`

Permission placeholders
-----------------------

Generated app packages now include documentation placeholders for:

Android:

- camera
- location
- notifications

iOS:

- camera usage description
- location usage description
- Face ID usage description
- notification setup notes

Deep links
----------

The plugin now generates planning notes for future deep-link support. Example:

- `https://client-domain.com/...` should open the installed app directly

The plugin does not implement Android App Links or iOS Universal Links
automatically yet. Domain association files and platform validation still need
to be handled in the host app rollout.

Generated mobile interaction bridge
-----------------------------------

When camera uploads, file picker support, share sheet support, or QR scanner support is enabled, the generated app package
now includes:

- `assets/mobile-bridge.js`
- native bridge dependency hints
- README usage examples

Example usage:

.. code-block:: html

   <script src="./assets/mobile-bridge.js"></script>
   <button onclick="window.MandelMobileApp.pickImage()">Upload photo</button>
   <button onclick="window.MandelMobileApp.pickFile()">Upload document</button>
   <button onclick="window.MandelMobileApp.shareCurrentPage()">Share this page</button>
   <button onclick="window.MandelMobileApp.scanQrCode()">Scan QR code</button>

Behavior:

- In Capacitor, the bridge uses native plugins when available.
- In a normal browser or PWA, uploads fall back to file inputs and share actions fall back to `navigator.share()` when supported.
- Camera or file access starts only after explicit user interaction.
- Share requests also start only after explicit user interaction.
- Disabled capabilities throw clear errors instead of silently failing.
- If no native/browser share API exists, the bridge throws `share_not_supported`.
- If no native barcode scanner plugin exists, the bridge throws `qr_scanner_not_supported`.

Share sheet capability
----------------------

Implemented by the generator:

- `@capacitor/share` is added to generated dependencies when share sheet support is enabled
- `window.MandelMobileApp.share(payload)` is generated
- `window.MandelMobileApp.shareCurrentPage()` is generated
- generated metadata exposes share-sheet capability and diagnostics support

Still requiring runtime/device verification:

- Android share-intent behavior
- iOS share-sheet behavior
- WebView restrictions on shared text/URLs, if any

QR scanner capability
---------------------

Chosen package:

- `@capacitor-mlkit/barcode-scanning`

Why:

- it was already the registry's declared dependency
- it matches the current Capacitor-first plugin style in this repo
- it keeps QR scanning isolated to generated mobile bridge behavior without changing host runtime code

Implemented by the generator:

- the dependency is added only when QR scanner support is enabled
- `window.MandelMobileApp.scanQrCode()` is generated
- generated metadata exposes QR-scanner capability and diagnostics support
- the admin dashboard now shows QR-scanner dependency/readiness details

Still requiring runtime/device verification:

- Android camera permission + scan flow
- iOS camera permission + scan flow
- actual plugin method/return-shape confirmation on device

Secure storage capability
-------------------------

Chosen package:

- `capacitor-secure-storage-plugin@0.12.0`

Why:

- the upstream documentation explicitly lists `0.12.0` for Capacitor 7
- it provides the exact key/value operations this plugin needs: `get`, `set`,
  `remove`, and `clear`
- it stays open-source and fits the existing lightweight generator approach

Generated API:

.. code-block:: javascript

   await window.MandelMobileApp.secureStorage.set("token", "value")
   const result = await window.MandelMobileApp.secureStorage.get("token")
   await window.MandelMobileApp.secureStorage.remove("token")
   await window.MandelMobileApp.secureStorage.clear()

Native vs browser behavior:

- On Android/iOS, the bridge prefers the native secure storage plugin.
- In browser contexts, the bridge uses an in-memory fallback only.
- The browser fallback is not durable and must not be treated as secure secret
  storage.
- The bridge does not silently downgrade secrets into `localStorage`.

Still requiring runtime/device verification:

- Android native storage behavior
- iOS native storage behavior
- exact native plugin registration/runtime availability on device

Testing
-------

Isolated plugin tests:

.. code-block:: bash

   DJANGO_SETTINGS_MODULE=ocyan.plugin.mobileapp.test_settings \\
   python -m django test --testrunner=django.test.runner.DiscoverRunner \\
   ocyan.plugin.mobileapp.tests

Recommended focused checks:

.. code-block:: bash

   git diff --check
   ruff check ocyan/plugin/mobileapp setup.py
   black --check ocyan/plugin/mobileapp setup.py

Documentation
-------------

See:

- `docs/mobile_app_plugin.md`

Store review warning
--------------------

This plugin can generate a valid PWA and a valid Capacitor shell, but that does
not guarantee App Store or Play Store approval. Client projects should add
clear app-specific value such as:

- push notifications
- offline features
- account dashboard workflows
- deep links
- camera or upload capabilities
- app-only support or service flows

Native capability planning reduces that risk by making app value explicit, but
it still does not replace real platform implementation work.
