@openmobile/sdk-web
v0.1.0-alpha
Published
OpenMobile Web SDK - React components for server-driven UI
Maintainers
Readme
OpenMobile Web SDK
Native in-app experiences for React applications - update your UI instantly without app releases.
Features
- 🎯 Native rendering - React components from JSON definitions
- 📊 Surveys - NPS, CSAT, and custom feedback forms
- 🚀 Onboarding flows - Interactive user tours and walkthroughs
- 📢 Feature announcements - In-app messages and changelogs
- 🎨 Visual editor - Design screens without coding
- ⚡ Instant updates - No app store deployment required
- 🔒 Type-safe - Full TypeScript support
- 🧪 Well-tested - Comprehensive test coverage
📦 Installation
npm
npm install @openmobile/sdk-webyarn
yarn add @openmobile/sdk-webpnpm
pnpm add @openmobile/sdk-webRequirements:
- React 18.0+
- TypeScript 5.0+ (recommended)
🚀 Quick Start
1. Initialize SDK
In your app entry point (main.tsx or App.tsx):
import { OpenMobileSDK } from '@openmobile/sdk-web';
// Initialize once at app startup
OpenMobileSDK.initialize({
apiKey: 'your-api-key-here',
baseUrl: 'https://api.openmobile.io' // optional
});2. Use OpenMobileScreen Component
import React from 'react';
import { OpenMobileScreen, EventListener } from '@openmobile/sdk-web';
const json = `
{
"version": "1.0",
"id": "home_screen",
"root": {
"type": "column",
"properties": {
"id": "main",
"spacing": 16,
"padding": 20
},
"children": [
{
"type": "text",
"properties": {
"id": "title",
"text": "Welcome to OpenMobile!",
"fontSize": 24,
"fontWeight": "bold"
}
},
{
"type": "button",
"properties": {
"id": "cta",
"text": "Get Started"
},
"events": {
"onTap": {
"type": "navigate",
"route": "/getting-started"
}
}
}
]
}
}
`;
function HomeScreen() {
return (
<OpenMobileScreen
json={json}
eventListener={new MyEventListener()}
/>
);
}3. Handle Events
import { EventListener } from '@openmobile/sdk-web';
import { useNavigate } from 'react-router-dom'; // or your routing library
class MyEventListener implements EventListener {
constructor(private navigate?: any) {}
onNavigate(route: string, params?: Record<string, any>) {
console.log('Navigate to:', route, params);
// Use your routing library
this.navigate?.(route);
}
onOpenUrl(url: string, external: boolean) {
console.log('Open URL:', url, 'external:', external);
if (external) {
window.open(url, '_blank');
} else {
window.location.href = url;
}
}
onCustomEvent(action: string, payload?: Record<string, any>) {
switch (action) {
case 'add_to_cart':
console.log('Add to cart:', payload?.product_id);
// Handle add to cart
break;
case 'share':
console.log('Share:', payload);
// Handle share
break;
}
}
onSubmitForm(endpoint: string, method: string, fields?: Record<string, any>) {
console.log('Submit form to:', endpoint);
// Make API call
fetch(endpoint, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields)
});
}
onTrackAnalytics(eventName: string, properties?: Record<string, any>) {
console.log('Track:', eventName, properties);
// Send to your analytics service (GA, Mixpanel, etc.)
}
}
// Usage in component
function MyComponent() {
const navigate = useNavigate();
const eventListener = new MyEventListener(navigate);
return <OpenMobileScreen json={json} eventListener={eventListener} />;
}📖 Components
Column
Vertical layout container (flex column).
{
"type": "column",
"properties": {
"id": "main_column",
"spacing": 16,
"padding": 20,
"scrollable": true,
"horizontalAlignment": "center",
"backgroundColor": "#F5F5F5"
},
"children": [...]
}Text
Display text content.
{
"type": "text",
"properties": {
"id": "title",
"text": "Hello World",
"fontSize": 24,
"fontWeight": "bold",
"color": "#000000",
"textAlign": "center",
"maxLines": 2
}
}Button
Interactive button.
{
"type": "button",
"properties": {
"id": "submit_btn",
"text": "Submit",
"variant": "primary",
"size": "large",
"fullWidth": true,
"backgroundColor": "#FF6B35",
"textColor": "#FFFFFF"
},
"events": {
"onTap": {
"type": "navigate",
"route": "/success"
}
}
}Button Variants:
primary: Filled button with background colorsecondary: Filled button with secondary coloroutline: Outlined button with transparent backgroundtext: Text-only button (no background, no border)
Button Sizes:
small: Compact buttonmedium: Default sizelarge: Large button
Image
Display images from URL.
{
"type": "image",
"properties": {
"id": "hero_image",
"url": "https://example.com/image.jpg",
"aspectRatio": "16:9",
"resizeMode": "cover",
"alt": "Hero banner",
"borderRadius": 12
}
}Resize Modes:
cover: Fills container, may crop imagecontain: Fits entirely, may have empty spacefill: Stretches to fill containercenter: Original size, centered
Card
Container with elevation (shadow).
{
"type": "card",
"properties": {
"id": "promo_card",
"elevation": 4,
"cornerRadius": 16,
"padding": 20,
"clickable": true
},
"children": [...]
}⚡ Events
Navigate
Navigate to another screen/route.
{
"type": "navigate",
"route": "/product/detail",
"params": {
"product_id": "PROD-123"
}
}Open URL
Open external URL or webview.
{
"type": "open_url",
"url": "https://example.com",
"external": true
}Custom
Trigger custom actions in your app.
{
"type": "custom",
"action": "add_to_cart",
"payload": {
"product_id": "PROD-123",
"quantity": 1
}
}Submit Form
Submit data to API.
{
"type": "submit_form",
"endpoint": "https://api.example.com/contact",
"method": "POST",
"fields": {
"name": "John Doe",
"email": "[email protected]"
}
}Track Analytics
Send analytics events.
{
"type": "track_analytics",
"eventName": "button_clicked",
"properties": {
"button_id": "cta_primary",
"screen": "home"
}
}🎨 Styling
Colors
Supports hex colors and named colors:
"color": "#FF6B35"
"backgroundColor": "#FFFFFF"
"textColor": "blue"Named colors: black, white, red, green, blue, yellow, orange, purple, pink, gray, brown, cyan, indigo, mint, teal
Spacing
All spacing values in pixels:
"padding": 16,
"margin": 8,
"spacing": 12Typography
"fontSize": 18,
"fontWeight": "bold",
"lineHeight": 1.5,
"textAlign": "center"Font weights: thin (100), ultralight (200), light (300), regular (400), medium (500), semibold (600), bold (700), heavy (800), black (900)
🔍 Examples
Full Example: Product Card
import React, { useState, useEffect } from 'react';
import { OpenMobileScreen, EventListener } from '@openmobile/sdk-web';
function ProductPage({ productId }: { productId: string }) {
const [json, setJson] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Fetch JSON from your API
fetch(`https://api.example.com/products/${productId}/screen`)
.then(res => res.json())
.then(data => {
setJson(JSON.stringify(data));
setIsLoading(false);
})
.catch(error => {
console.error('Error loading product:', error);
setIsLoading(false);
});
}, [productId]);
if (isLoading) return <div>Loading...</div>;
if (!json) return <div>Error loading product</div>;
return (
<OpenMobileScreen
json={json}
eventListener={new ProductEventListener()}
/>
);
}
class ProductEventListener implements EventListener {
onCustomEvent(action: string, payload?: Record<string, any>) {
if (action === 'add_to_cart') {
const productId = payload?.product_id;
const price = payload?.price;
console.log(`Adding product ${productId} ($${price}) to cart`);
// Add to cart logic
}
}
}Custom Loading & Error Components
import { OpenMobileScreen } from '@openmobile/sdk-web';
function MyScreen() {
return (
<OpenMobileScreen
json={json}
loadingComponent={
<div className="custom-loading">
<Spinner />
<p>Loading your content...</p>
</div>
}
errorComponent={(error) => (
<div className="custom-error">
<h2>Oops!</h2>
<p>{error}</p>
<button onClick={() => window.location.reload()}>
Try Again
</button>
</div>
)}
onSuccess={(screen) => {
console.log('Screen loaded:', screen.id);
}}
onError={(error) => {
console.error('Failed to load screen:', error);
// Report to error tracking service
}}
/>
);
}TypeScript Usage
import { Screen, Component, ComponentType } from '@openmobile/sdk-web';
// Type-safe screen creation
const screen: Screen = {
version: '1.0',
id: 'my_screen',
root: {
type: ComponentType.COLUMN,
properties: {
id: 'root',
spacing: 16,
padding: 20,
},
children: [
{
type: ComponentType.TEXT,
properties: {
id: 'title',
text: 'Hello TypeScript!',
fontSize: 24,
},
},
],
},
};
// Use with OpenMobileScreen
<OpenMobileScreen screen={screen} />🧪 Testing
Unit Tests with Jest
import { render, screen } from '@testing-library/react';
import { OpenMobileScreen } from '@openmobile/sdk-web';
describe('OpenMobileScreen', () => {
it('renders text component', () => {
const json = `{
"version": "1.0",
"id": "test",
"root": {
"type": "text",
"properties": {
"id": "test_text",
"text": "Hello Test"
}
}
}`;
render(<OpenMobileScreen json={json} />);
expect(screen.getByText('Hello Test')).toBeInTheDocument();
});
it('handles invalid JSON', () => {
const onError = jest.fn();
render(<OpenMobileScreen json="invalid json" onError={onError} />);
expect(onError).toHaveBeenCalled();
});
});📚 Documentation
🐛 Troubleshooting
Error: "SDK not initialized"
Solution: Call OpenMobileSDK.initialize() before using any SDK features.
Error: "Invalid JSON format"
Solution: Validate your JSON against the schema. Check JSON_SCHEMA.md for correct format.
Images not loading
Solution:
- Ensure URLs are HTTPS
- Check CORS headers on image server
- Verify image URLs are accessible
TypeScript errors
Solution:
- Ensure
@openmobile/sdk-coreis installed - Check
tsconfig.jsonhas"moduleResolution": "node" - Run
npm installto ensure all dependencies are installed
🤝 Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
📄 License
MIT License - see LICENSE file for details.
📞 Support
Built with ❤️ by OpenMobile Team
