@superapp_men/content-provider
v1.8.0
Published
Offline content provider for SuperApp partner applications via iframe/Capacitor
Maintainers
Readme
@superapp_men/content-provider
Bridge package for Bewize / SuperApp partner applications to request offline assets from the SuperApp host via postMessage.
Partner apps run inside an iframe managed by the SuperApp. This package provides typed clients for five categories of assets:
| Client | Function | What it returns |
|--------|----------|-----------------|
| ContentProviderClient | getContent() | The student's grade-specific content.json |
| MediaProviderClient | getMedia(path) / getMediaObjectUrl(path) | Downloaded audio / video / image files |
| BackgroundProviderClient | getBackground(name) | Shared WebP background images |
| SoundProviderClient | getSound(name) | Shared sound-design WAV files |
| RiveProviderClient | getRive() / getRiveBuffers() / getRiveUrls() | Rive WASM runtime files (rive.wasm + rive_fallback.wasm) |
| VideoProviderClient | getVideo(path) / downloadVideo(path) | Palier video status + local playable URI |
Installation
npm install @superapp_men/content-providerContentProviderClient
Retrieves the pre-downloaded content.json for the authenticated student's grade.
import { ContentProviderClient } from "@superapp_men/content-provider";
const client = new ContentProviderClient({ partnerCode: "bewize" });
try {
const content = await client.getContent();
console.log(content); // parsed content.json object
} finally {
client.destroy();
}Config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| partnerCode | string | required | Your partner code (e.g. "bewize") |
| timeout | number | 15000 | Request timeout in ms |
| debug | boolean | false | Enable console logs |
MediaProviderClient
Retrieves downloaded media files (audio, video, images) stored in the SuperApp's data directory at partner-apps/{partnerCode}/media/.
import { MediaProviderClient } from "@superapp_men/content-provider";
const media = new MediaProviderClient({ partnerCode: "bewize" });
// Option A — get base64 data + MIME type
const { data, mimeType, encoding } = await media.getMedia("consignes/00e5b2a4-98c8-409e-9b68-d8eb397d4c78.mp3");
// Option B — get a ready-to-use object URL (recommended for <audio> / <video> / <img>)
const { url, mimeType, revoke } = await media.getMediaObjectUrl("consignes/00e5b2a4-98c8-409e-9b68-d8eb397d4c78.mp3");
const audio = new Audio(url);
audio.play();
// Release the object URL when no longer needed
audio.addEventListener("ended", revoke);
media.destroy();mediaPath format
Pass the relative path exactly as listed in the manifest media.audio.files[] array:
"consignes/00e5b2a4-98c8-409e-9b68-d8eb397d4c78.mp3"
"questions/159903a1-803d-46ee-9f34-365010d378f3.mp3"The same function works for future video and image files — the MIME type is auto-detected from the extension.
Supported MIME types
| Extension | MIME type |
|-----------|-----------|
| .mp3 | audio/mpeg |
| .m4a | audio/mp4 |
| .ogg | audio/ogg |
| .wav | audio/wav |
| .mp4 | video/mp4 |
| .webm | video/webm |
| .png | image/png |
| .jpg / .jpeg | image/jpeg |
| .gif | image/gif |
| .webp | image/webp |
| .svg | image/svg+xml |
Config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| partnerCode | string | required | Your partner code |
| timeout | number | 30000 | Request timeout in ms (files can be large) |
| debug | boolean | false | Enable console logs |
BackgroundProviderClient
Retrieves shared WebP background images hosted by the SuperApp. Returns a ready-to-use base64 data URL.
import { BackgroundProviderClient } from "@superapp_men/content-provider";
import type { BackgroundName } from "@superapp_men/content-provider";
const bg = new BackgroundProviderClient();
const { url } = await bg.getBackground("champs");
// Use as CSS background
element.style.backgroundImage = `url(${url})`;
// Or as <img> src
imgElement.src = url;
bg.destroy();Available backgrounds
| Name | Description |
|------|-------------|
| "arbre" | Tree scene |
| "champs" | Green countryside fields |
| "decor_souk" | Traditional souk decoration |
| "douar" | Rural village scene |
| "embarcadere" | Embarcadère / harbour scene |
| "msid" | Mosque / msid scene |
| "oliveraie" | Olive grove |
| "puis" | Well / water scene |
Config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| timeout | number | 10000 | Request timeout in ms |
| debug | boolean | false | Enable console logs |
SoundProviderClient
Retrieves shared sound-design WAV files hosted by the SuperApp (button clicks, drag/drop, feedback stings, menu music, etc.). Returns a ready-to-use base64 data URL, the same way BackgroundProviderClient does for images.
import { SoundProviderClient } from "@superapp_men/content-provider";
import type { SoundName } from "@superapp_men/content-provider";
const sounds = new SoundProviderClient();
try {
const { url } = await sounds.getSound("drop");
const audio = new Audio(url);
await audio.play();
} catch (error) {
// Unknown sound name, SuperApp error, or timeout all land here.
console.error("Could not load sound:", error);
} finally {
sounds.destroy();
}Function signature
getSound(name: SoundName): Promise<{ url: string; mimeType: string }>| Param | Type | Description |
|-------|------|-------------|
| name | SoundName | Sound file name, without extension (see the full list below) |
Returns { url: string; mimeType: string } — url is a data:audio/wav;base64,... URL, mimeType is always "audio/wav".
Throws an Error when:
- the requested
nameis not one of the sounds the SuperApp serves (statusCode: 404, messageUnknown sound: "<name>"), - the SuperApp fails to read the file (
statusCode: 500), or - no response arrives before
timeoutelapses (default 10000ms).
In every case the promise rejects — it never resolves with a partial/empty result, so a single try/catch around getSound() is enough to handle all failure modes.
Available sounds
| Name |
|------|
| "Loader marche compagnon" |
| "cloudInOut" |
| "deroulement panneau" |
| "drag" |
| "drop" |
| "feedback KO-002" |
| "feedback OK-001" |
| "feedback OK-002" |
| "feedback OK-003" |
| "music_01" |
| "music_02" |
| "music_02_variation1-001" |
| "music_02_variation1-002" |
| "scroll menu ile-001" |
| "son appuie bouton-001" |
| "son bounce bouton-001" |
| "son burn plein ecran" |
| "son d'apparition consigne-001" |
| "son fermeture panneau exercice" |
| "son ouverture panneau exercice" |
| "son progression bar-001" |
| "son selection champ-001" |
| "son selection element parmi autre-001" |
| "sons etoiles-002" |
| "splash screen" |
Config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| timeout | number | 10000 | Request timeout in ms |
| debug | boolean | false | Enable console logs |
RiveProviderClient
Retrieves both Rive WASM runtime files (rive.wasm and rive_fallback.wasm) from the SuperApp's static assets via postMessage. Call this once before mounting any Rive component so the runtime is ready.
import { RiveProviderClient } from "@superapp_men/content-provider";
import { RuntimeLoader } from "@rive-app/canvas";
const riveProvider = new RiveProviderClient({ partnerCode: "bewize" });
// Option A — object URLs (recommended)
// Pass primaryUrl to RuntimeLoader before mounting any Rive component.
// Call revoke() once the runtime has finished loading to free memory.
const { primaryUrl, fallbackUrl, revoke } = await riveProvider.getRiveUrls();
RuntimeLoader.setWasmUrl(primaryUrl);
// ... mount Rive components ...
revoke(); // free both object URLs
// Option B — ArrayBuffers
// Use when you need direct binary access to the WASM files.
const { primary, fallback } = await riveProvider.getRiveBuffers();
const primaryUrl2 = URL.createObjectURL(new Blob([primary], { type: "application/wasm" }));
RuntimeLoader.setWasmUrl(primaryUrl2);
// Option C — raw base64 objects
// Low-level access; prefer getRiveUrls() or getRiveBuffers() in practice.
const { primary: p, fallback: f } = await riveProvider.getRive();
// p.data / f.data are base64 strings; p.mimeType === "application/wasm"
riveProvider.destroy();Methods
| Method | Returns | Description |
|--------|---------|-------------|
| getRive() | { primary: RiveWasmFile, fallback: RiveWasmFile } | Both WASM files as raw base64 objects |
| getRiveBuffers() | { primary: ArrayBuffer, fallback: ArrayBuffer } | Both WASM files decoded to ArrayBuffers |
| getRiveUrls() | { primaryUrl: string, fallbackUrl: string, revoke: () => void } | Both WASM files as temporary object URLs |
Config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| partnerCode | string | required | Your partner code (e.g. "bewize") |
| timeout | number | 30000 | Request timeout in ms |
| debug | boolean | false | Enable console logs |
SuperApp host paths
The SuperApp serves both files from its own static assets — no bundle extraction needed on the partner side:
/rive-wasm/rive.wasm → returned as primary
/rive-wasm/rive_fallback.wasm → returned as fallbackVideoProviderClient
Retrieves palier videos managed by the SuperApp's offline video download manager. Videos are defined in the partner manifest under media.videos.byPalier and stored locally at partner-apps/{partnerCode}/videos/palier{N}/.
The SuperApp preloads videos automatically after the ASER test (current learning week first, then following weeks). This client lets the partner query a video's status and explicitly request downloads.
getVideo() blocks until the outcome is final. If the video is missing and the device is online, the SuperApp downloads it (joining an already-in-progress download rather than starting a duplicate) and the call only resolves once that download finishes or fails. A partner showing a loading screen gets the real video in the same call — no polling loop required.
import { VideoProviderClient } from "@superapp_men/content-provider";
const videos = new VideoProviderClient({ partnerCode: "bewize" });
const status = await videos.getVideo("palier3/v1p3.mp4"); // or just "v1p3.mp4" — waits for the download
if (status.available) {
// Case 1 — downloaded (either it already existed, or the download you just
// waited on succeeded): play it directly
videoElement.src = status.localUri!;
} else if (status.error === "VIDEO_NOT_DOWNLOADED_AND_OFFLINE") {
// Case 2 — not downloaded AND offline: show "unavailable offline" UI
} else if (status.error) {
// Case 3 — download was attempted but failed (e.g. "DOWNLOAD_FAILED"):
// show an error state; the partner may retry by calling getVideo() again
}
// Explicitly ask the SuperApp to download a video (same shared download
// manager as the automatic preload — duplicates are impossible). Same
// blocking behavior as getVideo().
await videos.downloadVideo("palier3/v1p3.mp4");
videos.destroy();VideoStatusBody
Every response separates two independent facts — a missing local file and a missing connection are never merged into one generic field:
| Field | Type | Meaning |
|-------|------|---------|
| available | boolean | True only when downloaded and ready to play |
| downloaded | boolean | Does the file exist in local storage? |
| downloading | boolean | Internal signal only — getVideo()/downloadVideo() never return this as true; it can only appear in the SuperApp's own debug tooling |
| hasInternet | boolean | Is the device currently online? |
| localUri | string \| null | WebView-playable URI when downloaded |
| error | string \| null | "VIDEO_NOT_DOWNLOADED_AND_OFFLINE", "VIDEO_NOT_IN_MANIFEST", "DOWNLOAD_FAILED" (or a more specific code), or null |
Response examples
Downloaded (either it already existed, or getVideo() waited for a fresh download to succeed):
{ "available": true, "downloaded": true, "downloading": false, "hasInternet": true, "localUri": "...", "error": null }Unavailable — missing + offline (returned immediately, nothing to wait for):
{ "available": false, "downloaded": false, "downloading": false, "hasInternet": false, "localUri": null, "error": "VIDEO_NOT_DOWNLOADED_AND_OFFLINE" }Download failed — missing + online, but the download itself errored:
{ "available": false, "downloaded": false, "downloading": false, "hasInternet": true, "localUri": null, "error": "DOWNLOAD_FAILED" }videoPath format
Pass either the full relative path or just the file name as listed in the manifest media.videos.byPalier:
"palier3/v1p3.mp4" → direct palier lookup
"v1p3.mp4" → resolved by searching every palierConfig
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| partnerCode | string | required | Your partner code (e.g. "bewize") |
| timeout | number | 120000 | Request timeout in ms — must cover a full video download, not just a status check |
| debug | boolean | false | Enable console logs |
SuperApp integration
If you are implementing the SuperApp host side, import from the /superapp subpath to get the handler types:
import {
ContentMessageType,
MediaMessageType,
BackgroundMessageType,
SoundMessageType,
RiveMessageType,
VideoMessageType,
} from "@superapp_men/content-provider/superapp";
import type {
ContentRequestMessage,
MediaRequestMessage,
BackgroundRequestMessage,
SoundRequestMessage,
RiveRequestMessage,
VideoRequestMessage,
VideoStatusBody,
} from "@superapp_men/content-provider/superapp";The SuperApp's IframeCommunicationHandler handles all message types:
| Message type | Handler | Filesystem path |
|-------------|---------|-----------------|
| content:request | Reads content.json | partner-apps/{code}/content/current/content.json |
| media:request | Reads media file as base64 | partner-apps/{code}/media/{path} |
| background:request | Fetches WebP from static assets | /shared_backgrounds/{name}.webp |
| sound:request | Fetches WAV from static assets | /shared_sound_design/{name}.wav |
| rive:request | Fetches both WASM runtime files | /rive-wasm/rive.wasm + /rive-wasm/rive_fallback.wasm |
| video:request | Video status; auto-downloads when missing + online | partner-apps/{code}/videos/{path} |
| video:download | Explicit download request (manifest-validated) | partner-apps/{code}/videos/{path} |
Message protocol
All messages follow the same pattern:
Request (partner → SuperApp):
{
"type": "content:request",
"requestId": "1751234567890-abc123",
"payload": { "partnerCode": "bewize" },
"timestamp": 1751234567890
}Response (SuperApp → partner):
{
"type": "content:response",
"requestId": "1751234567890-abc123",
"statusCode": 200,
"body": { "content": { ... } },
"timestamp": 1751234567891
}Rive request (partner → SuperApp):
{
"type": "rive:request",
"requestId": "1751234567890-abc123",
"payload": { "partnerCode": "bewize" },
"timestamp": 1751234567890
}Rive response (SuperApp → partner):
{
"type": "rive:response",
"requestId": "1751234567890-abc123",
"statusCode": 200,
"body": {
"primary": { "data": "<base64>", "mimeType": "application/wasm", "encoding": "base64" },
"fallback": { "data": "<base64>", "mimeType": "application/wasm", "encoding": "base64" }
},
"timestamp": 1751234567891
}Video request (partner → SuperApp):
{
"type": "video:request",
"requestId": "1751234567890-abc123",
"payload": { "partnerCode": "bewize", "videoPath": "palier3/v1p3.mp4" },
"timestamp": 1751234567890
}Video response (SuperApp → partner):
{
"type": "video:response",
"requestId": "1751234567890-abc123",
"statusCode": 200,
"body": {
"available": true,
"downloaded": true,
"downloading": false,
"hasInternet": true,
"localUri": "http://localhost/_capacitor_file_/.../videos/palier3/v1p3.mp4",
"error": null
},
"timestamp": 1751234567891
}video:download / video:download-response follow the same shape with the explicit-download semantics.
Error responses use statusCode 400 / 404 / 500 and body: { "error": "...", "detail": "..." }.
License
MIT — SuperApp Team
