Bridge API
The bridge is the communication layer between mini-apps (running in WebViews/JS containers) and the host app (React Native). It handles request/response messaging, event subscriptions, and service routing.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Mini-App (SDK) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Hooks │ │ Services │ │ Components │ │
│ │ useHostAuth │ │ storage │ │ PaymentButton │ │
│ │ useHostPush │ │ push │ │ ShareButton │ │
│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │
│ │ │ │ │
│ └────────────────┼─────────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Bridge │ │
│ │ (SDK side) │ │
│ └──────┬──────┘ │
└───────────────────────────┼─────────────────────────────────────┘
│
┌───────┴───────┐
│ Native Bridge │
│ (postMessage) │
└───────┬───────┘
│
┌───────────────────────────┼─────────────────────────────────────┐
│ ▼ │
│ ┌────────────────┐ │
│ │ ServiceRouter │ │
│ │ (Runtime side) │ │
│ └───────┬────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Storage │ │ Push │ │
│ │Handler │ │ Handler │ │ Handler │ │
│ └────────┘ └──────────┘ └──────────┘ │
│ │
│ Host App (Runtime) │
└─────────────────────────────────────────────────────────────────┘
Bridge (SDK Side)
The Bridge class manages communication from the mini-app to the host.
TypeScript Interface
type BridgeMessageType = 'request' | 'response' | 'event';
interface BridgeMessage {
/** Unique message identifier */
id: string;
/** Message type */
type: BridgeMessageType;
/** Action/method name (e.g., 'storage.get') */
action: string;
/** Message payload */
payload?: unknown;
/** Timestamp when message was created */
timestamp: number;
}
interface BridgeRequest extends BridgeMessage {
type: 'request';
}
interface BridgeResponse extends BridgeMessage {
type: 'response';
/** Whether the request succeeded */
success: boolean;
/** Response data (if success) */
data?: unknown;
/** Error message (if failed) */
error?: string;
}
interface BridgeEvent extends BridgeMessage {
type: 'event';
}
interface Bridge {
/**
* Initialize the bridge with mini-app ID
*/
initialize(appId: string): void;
/**
* Wait for bridge to be ready
*/
waitForReady(): Promise<void>;
/**
* Send a request and wait for response
* @param action - The action name (e.g., 'storage.get')
* @param payload - Request payload
* @returns Promise resolving to response data
*/
send<T = unknown>(action: string, payload?: unknown): Promise<T>;
/**
* Subscribe to events from the host
* @param eventName - Event to listen for
* @param callback - Handler function
* @returns Unsubscribe function
*/
on(eventName: string, callback: (event: BridgeEvent) => void): () => void;
/**
* Emit a one-way event (no response expected)
* @param action - Event action name
* @param payload - Event payload
*/
emit(action: string, payload?: unknown): void;
}
Example Usage
import { bridge } from '@54vie/miniapp-sdk/bridge';
// Initialize bridge (usually done by MiniAppRoot)
bridge.initialize('com.example.myapp');
// Wait for bridge to be ready
await bridge.waitForReady();
// Send request and get response
const user = await bridge.send<User>('auth.getUser');
console.log('User:', user);
// Send request with payload
const value = await bridge.send<string>('storage.get', { key: 'theme' });
// Listen for events from host
const unsubscribe = bridge.on('lifecycle.suspend', (event) => {
console.log('App suspended at:', event.timestamp);
saveState();
});
// Emit event (fire-and-forget)
bridge.emit('analytics.track', {
eventName: 'button_clicked',
properties: { button: 'submit' },
});
// Cleanup
unsubscribe();
ServiceRouter (Runtime Side)
The ServiceRouter routes incoming bridge requests to appropriate service handlers in the host app.
TypeScript Interface
type ServiceCategory =
| 'auth'
| 'wallet'
| 'ui'
| 'navigation'
| 'storage'
| 'media'
| 'location'
| 'share'
| 'analytics'
| 'host'
| 'device'
| 'network'
| 'haptics'
| 'clipboard'
| 'push'
| 'biometric'
| 'api';
interface BridgeRequest {
/** Unique request identifier */
id: string;
/** Mini-app ID making the request */
appId: string;
/** Method name (e.g., 'storage.get') */
method: string;
/** Request parameters */
params?: Record<string, unknown>;
/** Request timestamp */
timestamp: number;
}
interface BridgeResponse {
/** Request ID this responds to */
id: string;
/** Whether request succeeded */
success: boolean;
/** Response data */
data?: unknown;
/** Error details */
error?: {
code: string;
message: string;
details?: unknown;
};
}
interface BridgeError {
code: string;
message: string;
details?: unknown;
}
type BridgeHandler = (
params: Record<string, unknown>,
request?: BridgeRequest
) => Promise<unknown>;
interface ServiceHandler {
category: ServiceCategory;
methods: Record<string, BridgeHandler>;
}
interface ServiceRouter {
/**
* Register a service handler
*/
registerService(handler: ServiceHandler): void;
/**
* Register methods for a category
*/
register(category: ServiceCategory, methods: Record<string, BridgeHandler>): void;
/**
* Unregister a service
*/
unregisterService(category: ServiceCategory): void;
/**
* Route a request to appropriate handler
*/
route(request: BridgeRequest): Promise<BridgeResponse>;
/**
* Get all registered service categories
*/
getRegisteredServices(): ServiceCategory[];
/**
* Get all registered method names
*/
getRegisteredMethods(): string[];
/**
* Check if a method is registered
*/
hasMethod(method: string): boolean;
}
Example: Registering Service Handlers
import { serviceRouter } from '@54vie/miniapp-runtime';
// Register storage service
serviceRouter.register('storage', {
async get(params) {
const { key, scope } = params as { key: string; scope: string };
return AsyncStorage.getItem(`${scope}:${key}`);
},
async set(params) {
const { key, value, scope } = params as { key: string; value: unknown; scope: string };
await AsyncStorage.setItem(`${scope}:${key}`, JSON.stringify(value));
},
async remove(params) {
const { key, scope } = params as { key: string; scope: string };
await AsyncStorage.removeItem(`${scope}:${key}`);
},
async clear(params) {
const { scope } = params as { scope: string };
const keys = await AsyncStorage.getAllKeys();
const scopedKeys = keys.filter(k => k.startsWith(`${scope}:`));
await AsyncStorage.multiRemove(scopedKeys);
},
});
// Register auth service
serviceRouter.register('auth', {
async getUser(params, request) {
// request contains appId for permission checking
const user = await authStore.getUser();
return {
id: user.id,
name: user.name,
avatar: user.avatar,
};
},
async getAccessToken() {
return authStore.getAccessToken();
},
async isAuthenticated() {
return authStore.isAuthenticated();
},
});
// Register using ServiceHandler object
serviceRouter.registerService({
category: 'push',
methods: {
async getToken() {
return messaging().getToken();
},
async hasPermission() {
const status = await messaging().hasPermission();
return status === messaging.AuthorizationStatus.AUTHORIZED;
},
async requestPermission() {
const status = await messaging().requestPermission();
return status === messaging.AuthorizationStatus.AUTHORIZED;
},
},
});
Request/Response Format
Request Format
// Request sent from mini-app to host
{
id: "com.example.app-1-1706601234567",
type: "request",
action: "storage.get",
payload: {
appId: "com.example.app",
key: "user_preferences"
},
timestamp: 1706601234567
}
Response Format
// Successful response
{
id: "com.example.app-1-1706601234567",
type: "response",
success: true,
data: {
theme: "dark",
language: "vi"
},
timestamp: 1706601234600
}
// Error response
{
id: "com.example.app-1-1706601234567",
type: "response",
success: false,
error: {
code: "PERMISSION_DENIED",
message: "Storage permission not granted"
},
timestamp: 1706601234600
}
Event Format
// Event from mini-app to host (fire-and-forget)
{
id: "com.example.app-2-1706601235000",
type: "event",
action: "analytics.track",
payload: {
appId: "com.example.app",
eventName: "purchase_completed",
properties: {
orderId: "ORD-123",
amount: 500000
}
},
timestamp: 1706601235000
}
// Event from host to mini-app
{
id: "host-event-1706601236000",
type: "event",
action: "lifecycle.suspend",
payload: {
reason: "app_backgrounded"
},
timestamp: 1706601236000
}
Available Bridge Methods
Authentication (auth.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
auth.getUser | - | User | Get current user |
auth.getAccessToken | - | string | Get JWT token |
auth.isAuthenticated | - | boolean | Check auth status |
auth.requestLogin | - | boolean | Request user login |
auth.checkPermission | { permission } | boolean | Check if permission granted |
auth.requestPermission | { permission, reason? } | boolean | Request permission |
Wallet (wallet.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
wallet.getBalance | - | number | Get wallet balance |
wallet.requestPayment | { amount, description, orderId?, metadata? } | PaymentResult | Request payment |
wallet.checkTransaction | { transactionId } | Transaction | Check transaction status |
Storage (storage.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
storage.get | { key, scope } | T | null | Get stored value |
storage.set | { key, value, scope } | void | Set value |
storage.remove | { key, scope } | void | Remove value |
storage.clear | { scope } | void | Clear all storage |
Navigation (navigation.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
navigation.goBack | - | void | Go back |
navigation.navigate | { route, params? } | void | Navigate to route |
navigation.setTitle | { title } | void | Set screen title |
navigation.setOptions | { options } | void | Set nav options |
navigation.openMiniApp | { appId, params? } | void | Open another mini-app |
navigation.openDeepLink | { url } | void | Open deep link |
navigation.exitMiniApp | - | void | Exit mini-app |
UI (ui.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
ui.showToast | { message, type?, duration? } | void | Show toast |
ui.showAlert | { title, message, confirmText?, cancelText? } | boolean | Show alert dialog |
ui.showActionSheet | { title?, options, cancelIndex? } | number | Show action sheet |
ui.showLoading | - | void | Show loading overlay |
ui.hideLoading | - | void | Hide loading overlay |
Device (device.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
device.getInfo | - | DeviceInfo | Get device info |
device.getDeviceId | - | string | Get device ID |
device.getAppVersion | - | string | Get app version |
device.getBuildNumber | - | string | Get build number |
device.isEmulator | - | boolean | Check if emulator |
device.getCarrier | - | string | null | Get carrier name |
Network (network.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
network.getState | - | NetworkState | Get network state |
network.isOnline | - | boolean | Check if online |
network.getConnectionType | - | ConnectionType | Get connection type |
network.isWifi | - | boolean | Check if WiFi |
network.isCellular | - | boolean | Check if cellular |
Push Notifications (push.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
push.getToken | - | string | null | Get FCM/APNs token |
push.hasPermission | - | boolean | Check permission |
push.requestPermission | - | boolean | Request permission |
push.subscribeToTopic | { topic } | void | Subscribe to topic |
push.unsubscribeFromTopic | { topic } | void | Unsubscribe from topic |
push.scheduleNotification | { notification } | string | Schedule notification |
push.cancelNotification | { notificationId } | void | Cancel notification |
push.getBadgeCount | - | number | Get badge count |
push.setBadgeCount | { count } | void | Set badge count |
Analytics (analytics.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
analytics.track | { eventName, properties? } | void | Track event |
analytics.screen | { screenName } | void | Track screen view |
Media (media.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
media.takePhoto | { options? } | MediaAsset | Take photo |
media.recordVideo | { options? } | MediaAsset | Record video |
media.pickImage | { options? } | MediaAsset | Pick from gallery |
media.pickMultipleImages | { options? } | MediaAsset[] | Pick multiple |
media.pickVideo | - | MediaAsset | Pick video |
media.resizeImage | { uri, options } | MediaAsset | Resize image |
media.compressImage | { uri, quality } | MediaAsset | Compress image |
media.cropImage | { uri, options } | MediaAsset | Crop image |
media.scanQR | - | string | Scan QR code |
Location (location.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
location.getCurrentPosition | { options? } | Location | Get current location |
location.checkPermission | - | PermissionStatus | Check permission |
location.requestPermission | - | PermissionStatus | Request permission |
location.geocode | { address } | Location | Address to coordinates |
location.reverseGeocode | { latitude, longitude } | Address | Coordinates to address |
location.searchPlaces | { query, location? } | PlaceResult[] | Search places |
location.watchPosition | { callback } | void | Watch position |
location.stopWatchPosition | - | void | Stop watching |
Share (share.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
share.text | { text } | void | Share text |
share.image | { imageUri } | void | Share image |
share.toChat | { content } | void | Share to chat |
Biometric (biometric.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
biometric.isSupported | - | boolean | Check if supported |
biometric.isEnabled | - | boolean | Check if enabled |
biometric.authenticate | { reason } | boolean | Authenticate user |
API Proxy (api.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
api.request | { url, method?, headers?, body?, timeout? } | ApiResponse | Make API request |
Lifecycle (lifecycle.*)
| Method | Parameters | Returns | Description |
|---|---|---|---|
lifecycle.ready | - | void | Signal mini-app is ready |
lifecycle.suspend | - | void | Request suspend |
lifecycle.resume | - | void | Request resume |
lifecycle.destroy | - | void | Request destroy |
Error Codes
| Code | Description |
|---|---|
METHOD_NOT_FOUND | The requested method does not exist |
HANDLER_ERROR | Handler threw an exception |
PERMISSION_DENIED | Mini-app lacks required permission |
INVALID_PARAMS | Invalid or missing parameters |
TIMEOUT | Request timed out (30s default) |
NETWORK_ERROR | Network request failed |
AUTH_REQUIRED | User must be authenticated |
RATE_LIMITED | Too many requests |
Best Practices
-
Use hooks/services instead of direct bridge calls - The SDK hooks and services provide better abstractions with automatic cleanup and error handling.
-
Handle errors gracefully - Always wrap bridge calls in try/catch and handle specific error codes.
-
Respect timeouts - Bridge requests timeout after 30 seconds. Long-running operations should use different patterns.
-
Clean up subscriptions - Always unsubscribe from events when components unmount.
useEffect(() => {
const unsubscribe = bridge.on('lifecycle.suspend', handleSuspend);
return () => unsubscribe();
}, []);
- Check permissions before sensitive operations - Use
auth.checkPermissionbefore accessing sensitive data.
For more information on the higher-level APIs, see the Hooks API and Services API documentation.