ShareRing Me Modules Developer Guide

Estimated reading time: less than 1 minute

This 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 (postMessage and addEventListener).
  • This document covers the Me Module API.

What you build (high-level)

  • A static web app (React/Vue/Svelte/Vanilla JS—your choice)
  • Hosted at a public URL (typically https://...)
  • With a required manifest.json at the domain root
  • Optionally packaged for offline caching using a zip bundle

You can use any stack. For best results (TypeScript + fast iteration + predictable build output) use Vite + React + TypeScript.

1) Scaffold a module

npm create vite@latest sharering-me-module -- --template react-ts
cd sharering-me-module
npm install

2) Make the build work from file:// (required for offline mode)

When ShareRing Me loads an offline bundle it loads your index.html from a local file path, so asset URLs must be relative.

Update vite.config.ts:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  // Critical: ensures assets resolve when loaded from a local file path
  base: "./",
});

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

Create src/shareringMeBridge.ts:

export type MeModuleResponse = {
  type: string; // ALWAYS returned uppercase by the app
  payload: any;
  error?: unknown;
};

/**
 * Creates a safe bridge for Me Modules.
 *
 * Important constraints of the platform:
 * - Responses can arrive out of order (async handlers).
 *
 * This bridge enforces ONE in-flight request at a time.
 */
export function createShareRingMeBridge() {
  let inFlight:
    | {
        expectedType: string;
        resolve: (value: any) => void;
        reject: (err: Error) => void;
        timeoutId: number;
      }
    | null = null;

  const queue: Array<{
    expectedType: string;
    payload: any;
    timeoutMs: number;
    resolve: (value: any) => void;
    reject: (err: Error) => void;
  }> = [];

  function parseIncoming(data: any): MeModuleResponse | null {
    if (!data) return null;
    try {
      return JSON.parse(data);
    } catch {
      return null;
    }
  }

  function onMessage(event: MessageEvent) {
    if (event.type !== "message") return;
    const msg = parseIncoming((event as any).data);
    if (!msg || !msg.type) return;

    const msgType = String(msg.type).toUpperCase();
    if (!inFlight) return;
    if (msgType !== inFlight.expectedType) return;

    window.clearTimeout(inFlight.timeoutId);
    const { resolve, reject } = inFlight;
    inFlight = null;

    if (msg.error) reject(new Error(String(msg.error)));
    else resolve(msg.payload);

    flush();
  }

  window.addEventListener("message", onMessage, true);

  function flush() {
    if (inFlight || queue.length === 0) return;
    const next = queue.shift()!;
    inFlight = {
      expectedType: next.expectedType,
      resolve: next.resolve,
      reject: next.reject,
      timeoutId: window.setTimeout(() => {
        inFlight = null;
        next.reject(new Error(`Timeout waiting for ${next.expectedType}`));
        flush();
      }, next.timeoutMs),
    };

    (window as any).ReactNativeWebView?.postMessage(
      JSON.stringify({
        type: next.expectedType,
        payload: next.payload,
      })
    );
  }

  function send(type: string, payload?: any, opts?: { timeoutMs?: number }) {
    const expectedType = String(type).toUpperCase();
    const timeoutMs = opts?.timeoutMs ?? 30_000;

    return new Promise<any>((resolve, reject) => {
      queue.push({ expectedType, payload, timeoutMs, resolve, reject });
      flush();
    });
  }

  function destroy() {
    window.removeEventListener("message", onMessage, true);
  }

  return { send, destroy };
}

4) Use the bridge in your UI

Example src/App.tsx:

import { useMemo, useState } from "react";
import { createShareRingMeBridge } from "./shareringMeBridge";

export default function App() {
  const mm = useMemo(() => createShareRingMeBridge(), []);
  const [result, setResult] = useState<any>(null);
  const [error, setError] = useState<string>("");

  async function readAppInfo() {
    setError("");
    setResult(null);
    try {
      const payload = await mm.send("COMMON_APP_INFO");
      setResult(payload);
    } catch (e: any) {
      setError(e?.message ?? String(e));
    }
  }

  return (
    <div style={{ padding: 16, fontFamily: "system-ui" }}>
      <h2>Hello Me Module (V2)</h2>
      <button onClick={readAppInfo}>COMMON_APP_INFO</button>
      {error ? <pre style={{ color: "crimson" }}>{error}</pre> : null}
      {result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}
    </div>
  );
}

5) Add manifest.json (required)

Your module must serve a ShareRing manifest at /<manifest.json>.

If you use Vite, create public/manifest.json so it is available at:

  • dev: http://localhost:5173/manifest.json
  • prod build: dist/manifest.json

Minimal example (online-only):

{
  "version": "0.0.1",
  "offline_mode": false,
  "isMaintenance": false,
  "enable_secure_screen": false
}

6) Build & host

npm run build

Host the dist/ folder (any static hosting works).


Testing inside ShareRing Me (Developer Mode → Custom dApps)

To test a module during development:

  1. Ensure your ShareRing Me user has Developer Mode enabled (this is typically enabled per user/account).
  2. In the app, go to Settings → Developer Tool → Add Custom dApps.
  3. 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).
  4. Open it from the same area (or wherever your build exposes it in the app UI).

Required hosting

Your module MUST serve both index.html and manifest.json at the domain root of the host.

Important: Me modules do not support hosting in subpaths. You must use a dedicated domain or subdomain for your module.

Examples:

  • https://example.comindex.html and manifest.json at https://example.com/
  • https://module.example.comindex.html and manifest.json at https://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 ShareRing Me app uses a PWA manifest with some additional attributes.

Recommended fields:

  • version (string): version identifier. Bump this when you deploy a new build.
  • offline_mode (boolean): enable offline zip caching.
  • zip_name (string): zip file name (required when offline_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.

Minimal example (online-only)

{
  "version": "1.0.0",
  "offline_mode": false,
  "isMaintenance": false,
  "enable_secure_screen": false
}

Offline-enabled example

{
  "version": "1.0.3",
  "offline_mode": true,
  "zip_name": "build-1.0.3.zip",
  "checksum": "PUT_SHA256_OF_BASE64_ZIP_HERE",
  "isMaintenance": false,
  "enable_secure_screen": true
}

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:

<moduleUrl>/<zip_name>

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.html at 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):

index.html
assets/...
manifest.json   (recommended to include too)

Bad zip (nested):

my-build/
  index.html
  assets/...

3) Checksum format

If you provide checksum, the app verifies it as:

  1. read the zip file as a base64 string
  2. compute sha256(base64String) → hex

Example command (Node.js) to generate this checksum:

node -e "const fs=require('fs'); const crypto=require('crypto'); const b64=fs.readFileSync('build-1.0.3.zip').toString('base64'); console.log(crypto.createHash('sha256').update(b64).digest('hex'));"

When deploying a new version:

  1. Build your web app (npm run build).
  2. Zip the build output with index.html at the zip root.
  3. Upload the zip to <moduleUrl>/<zip_name>.
  4. Update the domain-root manifest.json:
    • bump version
    • set zip_name to the new file
    • set checksum if used

Messaging protocol

Message envelopes

All messages MUST be JSON objects.

Request (WebView → App)

type Request = { type: string; payload?: unknown };

Response (App → WebView)

type Response = { type: string; payload: unknown; error?: unknown };

Conventions

  • type is an event name (string literal), e.g. 'COMMON_APP_INFO'.
  • payload varies by event:
    • scalar (e.g. string, boolean)
    • object
    • array
  • error is optional and only present when the native handler fails.

Web → App (request)

window.ReactNativeWebView?.postMessage(JSON.stringify({
  type: 'EVENT_TYPE',
  payload: { /* your data (optional) */ }
}));

App → Web (response)

const handleMessage = (event: MessageEvent) => {
  if (event.type !== "message") return;
  const msg = JSON.parse(event.data);
  // msg.type, msg.payload, msg.error 
}
window.addEventListener("message", handleMessage, true);

// later on remove the listener to avoid memory leaks and/or collisions
// window.removeEventListener('message', handleMessage);

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_DECRYPT
  • CRYPTO_SIGN
  • WALLET_SIGN_TRANSACTION
  • WALLET_SIGN_AND_BROADCAST_TRANSACTION
  • VAULT_EXEC_QUERY_SILENT

Best practices & common pitfalls

1) Always validate messages

On receive, validate:

  • type exists and is a string
  • payload shape matches what your code expects

2) Treat the bridge like an RPC channel

  • 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:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

and CSS like:

/* Example: keep content away from device cutouts */
.page {
  padding-top: env(safe-area-inset-top);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
}
  • 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.