Mobile Integration Guide
Guide for integrating Rivellum into mobile apps using React Native, WebViews, and the TypeScript SDK.
Table of Contents
- Overview
- React Native Integration
- WebView Integration
- Mobile-Friendly SDK APIs
- Deep Link Wallet Integration
- Best Practices
Overview
Rivellum provides mobile-friendly APIs for:
- React Native apps - Full blockchain integration via TypeScript SDK
- Mobile WebViews - Embed Rivellum features in native apps
- Unity mobile games - See Unity Integration Guide
- External wallet flows - Deep link integration with natos-wallet
React Native Integration
Installation
npm install @rivellum/sdk
# or
yarn add @rivellum/sdk
Basic Setup
import { MobileRpcClient, MobileSigner } from '@rivellum/sdk/mobile';
// Create client
const client = new MobileRpcClient({
nodeUrl: 'https://mainnet.rivellum.io',
timeout: 15000
});
// Get balance
const balance = await client.getBalance('0x...');
console.log(`Balance: ${balance.rivl} RIVL`);
Mobile Signer Implementation
import { MobileSigner } from '@rivellum/sdk/mobile';
// Dev-only: Local key storage
class LocalMobileSigner implements MobileSigner {
constructor(
public address: string,
private privateKey: Uint8Array
) {}
async signIntent(intentBytes: Uint8Array): Promise<Uint8Array> {
// Use react-native-crypto or similar for Ed25519 signing
const signature = await signEd25519(this.privateKey, intentBytes);
return signature;
}
}
// Production: External wallet via deep links
class ExternalWalletSigner implements MobileSigner {
constructor(public address: string) {}
async signIntent(intentBytes: Uint8Array): Promise<Uint8Array> {
// Open natos-wallet app via deep link
const encoded = base64Encode(intentBytes);
const deepLink = `rivellum://sign?intent=${encoded}&callback=myapp://signed`;
await Linking.openURL(deepLink);
// Wait for callback with signature
return new Promise((resolve, reject) => {
const handleUrl = ({ url }: { url: string }) => {
if (url.startsWith('myapp://signed')) {
const params = parseUrlParams(url);
resolve(base64Decode(params.signature));
Linking.removeEventListener('url', handleUrl);
}
};
Linking.addEventListener('url', handleUrl);
// Timeout after 60s
setTimeout(() => {
reject(new Error('Signing timeout'));
Linking.removeEventListener('url', handleUrl);
}, 60000);
});
}
}
Send Transaction
import { IntentBuilder } from '@rivellum/sdk';
async function sendRivl(to: string, amount: number) {
// Get current nonce
const balance = await client.getBalance(signer.address);
// Build intent
const intent = new IntentBuilder()
.forSender(signer.address)
.withNonce(balance.nonce)
.transfer(to, amount)
.build();
// Simulate first
const simulation = await client.simulateIntent(intent);
if (!simulation.success) {
throw new Error(`Simulation failed: ${simulation.errorMessage}`);
}
// Send
const result = await client.sendIntent(intent, signer);
console.log(`Sent! Intent ID: ${result.intentId}`);
}
Event Subscription
// Subscribe to events for user's address
const unsubscribe = client.subscribeEvents(
{
addresses: [userAddress],
eventTypes: ['transfer', 'nft_minted']
},
(event) => {
console.log(`Event: ${event.type}`, event.data);
// Update UI
if (event.type === 'transfer') {
refreshBalance();
}
}
);
// Cleanup
useEffect(() => {
return () => unsubscribe();
}, []);
WebView Integration
HTML Setup
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rivellum Wallet</title>
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/@rivellum/sdk@latest/dist/bundle.js"></script>
<script>
const { MobileRpcClient } = RivellumSDK;
const client = new MobileRpcClient({
nodeUrl: 'https://mainnet.rivellum.io'
});
async function loadBalance(address) {
const balance = await client.getBalance(address);
document.getElementById('balance').textContent =
`${balance.rivl} RIVL`;
}
</script>
</body>
</html>
Native App Integration
iOS (Swift)
import WebKit
class RivellumWebView: UIViewController, WKScriptMessageHandler {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "rivellumBridge")
webView = WKWebView(frame: view.bounds, configuration: config)
webView.loadHTMLString(htmlContent, baseURL: nil)
view.addSubview(webView)
}
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
if message.name == "rivellumBridge" {
guard let body = message.body as? [String: Any],
let action = body["action"] as? String else { return }
switch action {
case "signIntent":
// Handle signing request
let intentBytes = body["intentBytes"] as! String
signWithNativeWallet(intentBytes)
default:
break
}
}
}
}
Android (Kotlin)
class RivellumWebView : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
webView.settings.javaScriptEnabled = true
// Add JavaScript interface
webView.addJavascriptInterface(
RivellumBridge(this),
"RivellumBridge"
)
webView.loadDataWithBaseURL(
null,
htmlContent,
"text/html",
"UTF-8",
null
)
setContentView(webView)
}
class RivellumBridge(private val context: Context) {
@JavascriptInterface
fun signIntent(intentBytes: String): String {
// Handle signing with native wallet
return signWithNativeWallet(intentBytes)
}
}
}
Mobile-Friendly SDK APIs
MobileRpcClient Interface
export interface MobileRpcClient {
/**
* Get account balance and state
*/
getBalance(address: string): Promise<BalanceResponse>;
/**
* Simulate an intent before sending
*/
simulateIntent(intent: Intent): Promise<SimulationResult>;
/**
* Send a signed intent to the network
*/
sendIntent(
intent: Intent,
signer: MobileSigner
): Promise<SendIntentResult>;
/**
* Subscribe to blockchain events
* Returns unsubscribe function
*/
subscribeEvents(
filter: EventFilter,
onEvent: (event: RivellumEvent) => void
): () => void;
}
MobileSigner Interface
export interface MobileSigner {
/**
* Account address this signer represents
*/
address: string;
/**
* Sign an intent
*/
signIntent(intentBytes: Uint8Array): Promise<Uint8Array>;
}
Response Types
interface BalanceResponse {
address: string;
rivl: number;
assets: Record<string, number>;
nonce: number;
}
interface SimulationResult {
success: boolean;
estimatedFee: number;
errorMessage?: string;
stateDiff?: Record<string, any>;
}
interface SendIntentResult {
intentId: string;
status: string;
message?: string;
}
interface RivellumEvent {
type: string;
height: number;
timestamp: number;
data: Record<string, any>;
intentId?: string;
}
Deep Link Wallet Integration
URL Scheme Registration
iOS Info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>rivellum</string>
<string>natos-wallet</string>
</array>
Android AndroidManifest.xml
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
Signing Flow
async function requestSignature(intent: Intent): Promise<Uint8Array> {
const intentBytes = encodeIntent(intent);
const intentBase64 = base64Encode(intentBytes);
// Construct deep link
const deepLink = `rivellum://sign?intent=${intentBase64}&callback=myapp://signed`;
// Open natos-wallet
await Linking.openURL(deepLink);
// Wait for callback
return new Promise((resolve, reject) => {
const handler = ({ url }: { url: string }) => {
if (url.startsWith('myapp://signed')) {
const params = new URLSearchParams(url.split('?')[1]);
const signature = base64Decode(params.get('signature')!);
Linking.removeEventListener('url', handler);
resolve(signature);
}
};
Linking.addEventListener('url', handler);
// Timeout
setTimeout(() => {
Linking.removeEventListener('url', handler);
reject(new Error('Signing timeout'));
}, 60000);
});
}
Best Practices
1. Handle Network Conditions
async function robustRpcCall<T>(
operation: () => Promise<T>,
retries = 3
): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
return await operation();
} catch (error) {
if (i === retries - 1) throw error;
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
throw new Error('Unreachable');
}
// Usage
const balance = await robustRpcCall(() =>
client.getBalance(address)
);
2. Cache Aggressively on Mobile
class CachedRpcClient {
private cache = new Map<string, { data: any; timestamp: number }>();
private cacheTtl = 5000; // 5 seconds
async getBalance(address: string): Promise<BalanceResponse> {
const cached = this.cache.get(`balance:${address}`);
if (cached && Date.now() - cached.timestamp < this.cacheTtl) {
return cached.data;
}
const balance = await this.client.getBalance(address);
this.cache.set(`balance:${address}`, {
data: balance,
timestamp: Date.now()
});
return balance;
}
}
3. Optimize for Battery Life
// Use event subscriptions sparingly
let subscription: (() => void) | null = null;
// Subscribe only when app is active
AppState.addEventListener('change', (state) => {
if (state === 'active') {
subscription = client.subscribeEvents(filter, onEvent);
} else {
subscription?.();
subscription = null;
}
});
4. Secure Key Storage
// React Native
import * as SecureStore from 'expo-secure-store';
async function savePrivateKey(key: Uint8Array) {
// ONLY for dev/testing - use external wallet in production
const base64Key = base64Encode(key);
await SecureStore.setItemAsync('rivellum_dev_key', base64Key);
}
async function loadPrivateKey(): Promise<Uint8Array | null> {
const base64Key = await SecureStore.getItemAsync('rivellum_dev_key');
return base64Key ? base64Decode(base64Key) : null;
}
5. Progressive Enhancement
// Check if external wallet is installed
async function isNatosWalletInstalled(): Promise<boolean> {
try {
const canOpen = await Linking.canOpenURL('natos-wallet://');
return canOpen;
} catch {
return false;
}
}
// Fallback to web wallet if native app not installed
async function getSigner(address: string): Promise<MobileSigner> {
const hasNatosWallet = await isNatosWalletInstalled();
if (hasNatosWallet) {
return new ExternalWalletSigner(address);
} else {
// Open web wallet in browser
await Linking.openURL(`https://wallet.rivellum.io/connect?callback=myapp://`);
return await waitForWebWalletConnection();
}
}
Next Steps
- 📱 Unity Integration Guide for mobile games
- 🔐 Account Auth Guide for wallet security
- 💳 NFT Standard for mobile NFT features
- 🎮 Example apps:
examples/react-native/RivellumWalletApp
Questions?
- GitHub: https://github.com/rivellumlabs/rivellum/issues
- Discord: #mobile-dev
- Email: mobile@rivellum.io