ShareRing Me Modules Developer Guide
Estimated reading time: less than 1 minuteThis guide is for external developers who want to build a Me Module for the ShareRing Me app.
- A Me Module is a web application that runs inside the ShareRing Me mobile app (in an embedded WebView).
- A Me Module communicates with the ShareRing Me app only via a message bridge (
postMessageandaddEventListener). - This document covers the Me Module API.
What you build (high-level)
- A static web app (React/Vue/Svelte/Vanilla JS—your choice)
- Hosted over HTTPS at the root of its own (sub)domain
- With a required ShareRing manifest (
manifest.json) at the domain root — a PWA-style manifest with ShareRing-specific fields, includingwebview_version: "WEB_VIEW_V2"and astart_url - Optionally packaged for offline caching using a zip bundle
The two most common reasons a new module fails to load or has no wallet/vault access:
- The manifest is missing
webview_version: "WEB_VIEW_V2"— the app then hosts your module in the legacy V1 WebView, which denies everyWALLET_*,VAULT_*andCRYPTO_*event.- The manifest is missing
start_url(or it points somewhere other than your module's own public HTTPS URL) — the app usesstart_urlas the URL it actually loads.
Quickstart (recommended scaffolding)
You can use any stack. For best results (TypeScript + fast iteration + predictable build output) use Vite + React + TypeScript.
1) Scaffold a module
Target file layout (what a working module looks like):
2) Make the build work with relative paths
Assets must resolve via relative URLs — both for offline mode (the app loads index.html from a local file path) and because the module is served from its own domain root.
Update vite.config.ts:
Update index.html — reference the manifest and use a mobile-safe viewport:
(maximum-scale=1, user-scalable=no prevents iOS from zooming the layout when an input is focused; viewport-fit=cover enables env(safe-area-inset-*).)
If you use client-side routing, note that:
- Online mode: You can use either hash routing (e.g.
/#/route) or path routing. - Offline mode: You must use hash routing (e.g.
/#/route) because your module will be loaded from local file paths.
3) Add a small bridge helper
The bridge has several non-obvious platform behaviors that a naive postMessage + addEventListener pairing will miss. This helper encodes all of them (each is explained in the pitfalls section further down):
- Posting too early is silently dropped. The wrapper injects
window.ReactNativeWebViewafter your page starts running. Wait ~300ms, then poll for the bridge (up to 5s) before posting. - Responses are matched by
type, not by id. The app does not echo a request id back, so keep a FIFO queue of pending calls per event type. - The
messagelistener must be registered withuseCapture: true. Some wrapper builds only deliver native-injected messages in the capture phase. - Android may deliver via
documentevents. Register the same handler ondocumentas well. - Two response shapes exist. V2 wrappers respond with a
payloadfield; legacy V1 wrappers injectwindow.onMessageFromApp(json)with adatafield (sometimes URI-encoded JSON), and some builds put the response fields at the top level of the envelope. Accept all of them. - Omit the
payloadkey entirely when there is no payload. Some native handlers reject an envelope with an empty payload — send{ "type": "..." }, not{ "type": "...", "payload": undefined }.
Create src/shareringMeBridge.ts:
Tip — mock bridge for browser dev: wrap
sendbehind an interface and, whenisHostedInShareRingMe()is false, return a mock implementation backed bylocalStorage/fixtures. That lets you develop and test the whole module in a plain browser and only exercise the real bridge inside the app.
4) Use the bridge in your UI
Example src/App.tsx:
5) Add manifest.json (required)
Your module must serve a ShareRing manifest at the domain root (/manifest.json). This is a PWA-style web app manifest extended with ShareRing-specific fields — the four-field "minimal" manifest that older docs showed is not sufficient: without webview_version and start_url the module either fails to load or loads with no wallet/vault access.
If you use Vite, create public/manifest.json (plus the icon files it references) so Vite copies them verbatim into dist/ on every build:
- dev:
http://localhost:5173/manifest.json - prod build:
dist/manifest.json
Working example (online-only):
Three fields deserve special attention:
webview_version: "WEB_VIEW_V2"— required for any module that touches wallet, vault, or cryptography. If omitted (or set to V1), the app hosts your module in the legacy V1 WebView, which denies everyWALLET_*,VAULT_*andCRYPTO_*event. There is no error dialog — the calls just fail.start_url— the absolute URL the app actually loads, i.e. your module's own public HTTPS root. Keep it neutral: never embed query parameters, session tokens, or per-user data (the manifest is one shared static file — a token in it would send every user into one user's session). If you develop against a LAN URL, remember the production build must ship the productionstart_url— sincepublic/manifest.jsonis copied verbatim on everynpm run build, either keep the production URL in the source manifest permanently, or patchdist/manifest.jsonas a mandatory post-build step. A silently revertedstart_urlis a classic recurring deploy bug.icons(106×106) — the 106×106 icon is the tile ShareRing Me shows in its dApp listing; include it or the module renders without an icon.
6) Build & host
Host the dist/ folder at the root of an HTTPS domain (any static hosting works — nginx, Caddy, a CDN). HTTPS is not optional: wallet/vault access and the biometric/PIN prompts require it (plain-HTTP LAN URLs will render the page, but identity features fail).
Testing inside ShareRing Me (Developer Mode → Custom dApps)
To test a module during development:
- Ensure your ShareRing Me user has Developer Mode enabled (this is typically enabled per user/account).
- In the app, go to Settings → Developer Tool → Add Custom dApps.
- Paste your module URL and save.
- If you're using a local dev server, use a URL reachable from the device (LAN IP or a tunnel).
- Open it from the same area (or wherever your build exposes it in the app UI).
Practical testing notes:
- The app fetches
manifest.jsonfrom your URL when registering/opening the module — if it 404s or is invalid JSON, the module can refuse to load with no useful error. - Wallet/vault/crypto events require an HTTPS URL and
webview_version: "WEB_VIEW_V2"in the manifest. Over plain HTTP (e.g. a LAN dev server) the page renders but identity calls fail — for full end-to-end testing put the dev build behind an HTTPS tunnel (e.g. a Cloudflare tunnel). - The app caches manifest data. After changing the manifest, bump
versionand force-close/reopen the app; a freshly minted tunnel hostname can also be bitten by the device negative-caching DNS for a couple of minutes. - Async storage (
COMMON_*_ASYNC_STORAGE) is scoped to your module's domain name — switching between a tunnel URL and a production domain gives you two separate, empty-looking storage buckets.
Required hosting
Your module MUST serve both index.html and manifest.json at the domain root of the host, over HTTPS.
Important: Me modules do not support hosting in subpaths. You must use a dedicated domain or subdomain for your module. (The app also derives your async-storage scope — dAppsDomainName — from the URL host, so the domain is your module's identity.)
Examples:
https://example.com→index.htmlandmanifest.jsonathttps://example.com/https://module.example.com→index.htmlandmanifest.jsonathttps://module.example.com/
If manifest.json is missing or invalid, the ShareRing Me app can refuse to load the module (especially on first open).
Manifest schema
The manifest is a PWA web app manifest extended with ShareRing-specific fields. See the full working example in the Quickstart above.
Standard PWA fields the app reads:
name/short_name(string): shown in the dApp listing.description/short_description(string): listing blurbs.icons(array): include a 106×106 PNG (the dApp tile) and a 192×192.images(array): additional listing imagery (favicon, larger icons, optional banner).display(string): use"standalone".
ShareRing-specific fields:
webview_version(string): must be"WEB_VIEW_V2"for any module using wallet/vault/crypto events. V1 (or omitted) denies them all.start_url(string): the absolute HTTPS URL of your module — the URL the app actually loads. Must be neutral (no tokens/query params).version(string): version identifier. Bump this when you deploy a new build (the app caches manifest data).offline_mode(boolean): enable offline zip caching. Use""/falseplaceholders (checksum: "",zip_name: "",offline_mode: false) when not using it.zip_name(string): zip file name (required whenoffline_mode: true).checksum(string, optional): checksum for the zip file (see below).isMaintenance(boolean, optional): if true, the app may show a maintenance page.enable_secure_screen(boolean, optional): request screenshot prevention while your module is open.
Offline-enabled example (delta from the Quickstart manifest)
(Keep all the PWA + webview_version + start_url fields from the Quickstart example — only the offline fields change.)
Offline mode (zip bundle) (optional — use only when you need it)
Offline mode lets the ShareRing Me app download a zip build and load it locally.
Offline mode is powerful, but it has real trade-offs:
- It increases first-run bandwidth (the zip must be downloaded).
- It increases device storage usage (the extracted bundle is cached locally).
- It can increase memory pressure (unzipping + loading larger local assets).
When offline mode is useful
- Low / unreliable bandwidth: users can still open and use the module after a successful initial cache.
- No mobile service: modules that must work in-flight/underground/remote areas.
- Stateful/offline-first workflows: e.g. ticketing / scanning / check-in flows that must keep working and sync later.
When to avoid offline mode
- The module is used infrequently (downloading a zip is wasted bandwidth/storage).
- The module changes often (users will download many versions over time).
- The module is mostly static content that works fine online.
- The module requires real-time data or frequent API calls (offline caching provides no benefit).
1) Zip download URL
The app downloads the zip from:
Examples:
- module URL
https://example.com+zip_name: "build-1.0.3.zip"→https://example.com/build-1.0.3.zip - module URL
https://example.com/my-module+zip_name: "build-1.0.3.zip"→https://example.com/my-module/build-1.0.3.zip
2) Zip file contents (critical)
When unzipped, the app expects:
index.htmlat the root of the extracted folder- your static assets referenced by relative paths
Do not ship a zip that nests everything inside another folder level.
Good zip (root):
Bad zip (nested):
3) Checksum format
If you provide checksum, the app verifies it as:
- read the zip file as a base64 string
- compute
sha256(base64String)→ hex
Example command (Node.js) to generate this checksum:
4) Update process (recommended)
When deploying a new version:
- Build your web app (
npm run build). - Zip the build output with
index.htmlat the zip root. - Upload the zip to
<moduleUrl>/<zip_name>. - Update the domain-root
manifest.json:- bump
version - set
zip_nameto the new file - set
checksumif used
- bump
Messaging protocol
Message envelopes
All messages MUST be JSON objects.
Request (WebView → App)
Response (App → WebView)
Conventions
typeis an event name (string literal), e.g.'COMMON_APP_INFO'.payloadvaries by event:- scalar (e.g.
string,boolean) - object
- array
- scalar (e.g.
erroris optional and only present when the native handler fails.- No request id is echoed back — the app matches responses to requests by
typeonly. Keep at most one in-flight request per type (or a FIFO queue per type, as in the bridge helper above). - Omit
payloadentirely when the event takes none — send{ "type": "EVENT_TYPE" }. Some native handlers reject an envelope carrying an empty/undefinedpayload.
Web → App (request)
Timing caveat: window.ReactNativeWebView is injected by the wrapper after your page starts executing. A message posted before injection is silently dropped — no error, no response. Wait ~300ms and poll for the bridge before your first post (the Quickstart helper does this).
App → Web (response)
Legacy note: older (V1) wrapper builds don't fire message events at all — they inject a call to window.onMessageFromApp(jsonString), and the response value lives in a data field (sometimes URI-encoded JSON) instead of payload. The Quickstart bridge helper handles both transports; if you write your own, do the same.
User confirmation (PIN) for sensitive operations
Some calls require the ShareRing Me app to show a PIN confirmation UI to the user. Your module must handle:
- a delay (user is interacting)
- the user cancelling (you'll receive an error)
PIN confirmation is required for:
CRYPTO_DECRYPTCRYPTO_SIGNWALLET_SIGN_TRANSACTIONWALLET_SIGN_AND_BROADCAST_TRANSACTIONWALLET_SIGN_ARBITRARY_MESSAGEVAULT_EXEC_QUERY_SILENTVAULT_VCT_TOKEN_ID(first call per session)
Use a long timeout (e.g. 60 seconds) for these events — the user has to interact with a biometric/PIN prompt, and for broadcast events the app then also talks to the chain. The 8-second default that suits instant lookups will time out mid-prompt.
Best practices & common pitfalls
0) "My module loads but nothing works" checklist
Worked-through failure modes, in the order to check them:
- Wallet/vault/crypto events all fail or never respond → manifest is missing
webview_version: "WEB_VIEW_V2", or the module is served over plain HTTP. Fix the manifest, serve over HTTPS, bumpversion, force-close and reopen the app. - Module doesn't open at all / opens a blank or wrong page →
manifest.jsonmissing/invalid at the domain root, orstart_urlabsent/pointing at the wrong URL (a dev/LAN URL shipped to production is the classic case — remembernpm run buildcopiespublic/manifest.jsonverbatim). - The first request after page load times out with no response → you posted before the wrapper injected
window.ReactNativeWebView. Early posts are silently dropped; use the settle-then-poll pattern from the Quickstart helper. - Requests work in a plain browser test harness but not in the app → your
messagelistener isn't capture-phase (useCapture: true), or you're on an Android build that delivers viadocumentevents. - Async-storage reads always come back empty in the app → single-key reads must send the key as a bare string payload (see
COMMON_READ_ASYNC_STORAGE), and the bucket is scoped per domain — a URL change means a different (empty) bucket. - PIN-gated calls "randomly" fail → your timeout is too short for a human to answer the biometric/PIN prompt. Use ~60s for those events.
1) Always validate messages
On receive, validate:
typeexists and is a stringpayloadshape matches what your code expects
Also be liberal in where you look for the response value: depending on the app build it arrives in payload (V2), in data — possibly URI-encoded (V1) — or at the top level of the envelope.
2) Treat the bridge like an RPC channel
- Responses are matched by
typeonly (no request id) — don't fire concurrent requests of the same type without a FIFO queue. - Don't fire many concurrent requests without a strategy.
- The safest approach is sequential RPC (the provided bridge helper).
3) Keep storage small and non-sensitive
COMMON_WRITE_ASYNC_STORAGE is for small preferences/state.
Do not store secrets. Do not assume it is backed up.
4) Offline mode: make all asset paths relative
If your module must work offline, validate by loading the built dist/index.html from disk in a browser and ensuring assets resolve correctly.
5) Be a good mobile web citizen
Design principles (mobile-first)
- Responsive layout by default: avoid fixed widths/heights; use flexible layouts (Flex/Grid),
max-width, and responsive spacing. - Safe areas / notches: ensure content isn't hidden behind rounded corners/notches. Consider using:
and CSS like:
- Touch targets: design for thumbs; keep interactive elements comfortably sized and spaced (avoid tiny icons with no padding).
- Keyboard & forms: expect the on-screen keyboard to cover parts of the page; ensure focused inputs scroll into view and primary actions remain reachable.
- Dark mode & language: respect app theme (
COMMON_APP_INFO.darkMode) and language (COMMON_APP_INFO.language). - Accessibility: good contrast, visible focus states, labels for inputs, and sensible heading structure. Support reduced motion where possible.
- Performance on mid-range devices: avoid heavy animations and huge bundles; lazy-load large features, compress images, and keep JS work per frame small.
Testing checklist (practical)
- Screen sizes: small phone, large phone, and tablet; portrait + landscape.
- Text scaling: increase system font size / display size and verify layout doesn't clip or overlap.
- Keyboard behavior: test every form field; ensure it's never obscured and the page doesn't get "stuck" after closing the keyboard.
- Theme: dark + light mode; verify contrast and any images/icons.
- Network:
- slow network (throttled)
- offline
- if using offline mode: first run (download) vs subsequent runs (cached)
- Error handling: test user cancellation / timeouts for PIN-gated and long-running operations and show clear recovery options.